feat: support independent tcp4, tcp6, udp4, udp6 connectivity check

This commit is contained in:
mzz2017 2023-02-08 20:15:24 +08:00 committed by mzz
parent 551e79d9e5
commit 5e7b68822a
26 changed files with 738 additions and 222 deletions

View File

@ -5,6 +5,8 @@
package consts
import "net/netip"
type DialerSelectionPolicy string
const (
@ -13,3 +15,32 @@ const (
DialerSelectionPolicy_MinAverage10Latencies DialerSelectionPolicy = "min_avg10"
DialerSelectionPolicy_MinLastLatency DialerSelectionPolicy = "min"
)
const (
UdpCheckLookupHost = "connectivitycheck.gstatic.com."
)
type L4ProtoStr string
const (
L4ProtoStr_TCP L4ProtoStr = "tcp"
L4ProtoStr_UDP L4ProtoStr = "udp"
)
type IpVersionStr string
const (
IpVersionStr_4 IpVersionStr = "4"
IpVersionStr_6 IpVersionStr = "6"
)
func IpVersionFromAddr(addr netip.Addr) IpVersionStr {
var ipversion IpVersionStr
switch {
case addr.Is4() || addr.Is4In6():
ipversion = IpVersionStr_4
case addr.Is6():
ipversion = IpVersionStr_6
}
return ipversion
}

View File

@ -1,9 +1,9 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) since 2023, v2rayA Organization <team@v2raya.org>
* Copyright (c) since 2023, mzz2017 <mzz@tuta.io>
*/
package dialer
package netutils
import (
"context"

102
common/netutils/dns.go Normal file
View File

@ -0,0 +1,102 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) since 2023, mzz2017 <mzz@tuta.io>
*/
package netutils
import (
"context"
"fmt"
"github.com/mzz2017/softwind/pool"
"golang.org/x/net/dns/dnsmessage"
"golang.org/x/net/proxy"
"net/netip"
"strings"
)
func ResolveNetip(ctx context.Context, d proxy.Dialer, dns netip.AddrPort, host string, typ dnsmessage.Type) (addrs []netip.Addr, err error) {
if addr, err := netip.ParseAddr(host); err == nil {
if (addr.Is4() || addr.Is4In6()) && typ == dnsmessage.TypeA {
return []netip.Addr{addr}, nil
} else if addr.Is6() && typ == dnsmessage.TypeAAAA {
return []netip.Addr{addr}, nil
}
return nil, nil
}
switch typ {
case dnsmessage.TypeA, dnsmessage.TypeAAAA:
default:
return nil, fmt.Errorf("only support to lookup A/AAAA record")
}
// Build DNS req.
builder := dnsmessage.NewBuilder(nil, dnsmessage.Header{})
if err = builder.StartQuestions(); err != nil {
return nil, err
}
fqdn := host
if !strings.HasSuffix(fqdn, ".") {
fqdn += "."
}
if err = builder.Question(dnsmessage.Question{
Name: dnsmessage.MustNewName(fqdn),
Type: typ,
Class: dnsmessage.ClassINET,
}); err != nil {
return nil, err
}
b, err := builder.Finish()
if err != nil {
return nil, err
}
// Dial and write.
cd := ContextDialer{d}
c, err := cd.DialContext(ctx, "udp", dns.String())
if err != nil {
return nil, err
}
defer c.Close()
_, err = c.Write(b)
if err != nil {
return nil, err
}
ch := make(chan error, 1)
go func() {
buf := pool.Get(512)
n, err := c.Read(buf)
if err != nil {
ch <- err
return
}
// Resolve DNS response and extract A/AAAA record.
var msg dnsmessage.Message
if err = msg.Unpack(buf[:n]); err != nil {
ch <- err
return
}
for _, ans := range msg.Answers {
if ans.Header.Type != typ {
continue
}
switch typ {
case dnsmessage.TypeA:
a := ans.Body.(*dnsmessage.AResource)
addrs = append(addrs, netip.AddrFrom4(a.A))
case dnsmessage.TypeAAAA:
a := ans.Body.(*dnsmessage.AAAAResource)
addrs = append(addrs, netip.AddrFrom16(a.AAAA))
}
}
ch <- nil
}()
select {
case <-ctx.Done():
return nil, fmt.Errorf("timeout")
case err = <-ch:
if err != nil {
return nil, err
}
return addrs, nil
}
}

25
common/netutils/url.go Normal file
View File

@ -0,0 +1,25 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) since 2023, mzz2017 <mzz@tuta.io>
*/
package netutils
import "net/url"
type URL struct {
*url.URL
}
func (u *URL) Port() string {
if port := u.URL.Port(); port != "" {
return port
}
switch u.Scheme {
case "http":
return "80"
case "https":
return "443"
}
return ""
}

View File

@ -10,7 +10,6 @@ import (
"encoding/binary"
"encoding/hex"
"fmt"
"github.com/v2rayA/dae/config"
"net/url"
"reflect"
"strconv"
@ -22,6 +21,11 @@ var (
ErrOverlayHierarchicalKey = fmt.Errorf("overlay hierarchical key")
)
type UrlOrEmpty struct {
Url *url.URL
Empty bool
}
func CloneStrings(slice []string) []string {
c := make([]string, len(slice))
copy(c, slice)
@ -279,9 +283,9 @@ func FuzzyDecode(to interface{}, val string) bool {
v.SetString(val)
case reflect.Struct:
switch v.Interface().(type) {
case config.UrlOrEmpty:
case UrlOrEmpty:
if val == "" {
v.Set(reflect.ValueOf(config.UrlOrEmpty{
v.Set(reflect.ValueOf(UrlOrEmpty{
Url: nil,
Empty: true,
}))
@ -290,7 +294,7 @@ func FuzzyDecode(to interface{}, val string) bool {
if err != nil {
return false
}
v.Set(reflect.ValueOf(config.UrlOrEmpty{
v.Set(reflect.ValueOf(UrlOrEmpty{
Url: u,
Empty: false,
}))

View File

@ -24,6 +24,8 @@ type minLatency struct {
type AliveDialerSet struct {
log *logrus.Logger
dialerGroupName string
l4proto consts.L4ProtoStr
ipversion consts.IpVersionStr
mu sync.Mutex
dialerToIndex map[*Dialer]int // *Dialer -> index of inorderedAliveDialerSet
@ -37,6 +39,8 @@ type AliveDialerSet struct {
func NewAliveDialerSet(
log *logrus.Logger,
dialerGroupName string,
l4proto consts.L4ProtoStr,
ipversion consts.IpVersionStr,
selectionPolicy consts.DialerSelectionPolicy,
dialers []*Dialer,
setAlive bool,
@ -44,6 +48,8 @@ func NewAliveDialerSet(
a := &AliveDialerSet{
log: log,
dialerGroupName: dialerGroupName,
l4proto: l4proto,
ipversion: ipversion,
dialerToIndex: make(map[*Dialer]int),
dialerToLatency: make(map[*Dialer]time.Duration),
inorderedAliveDialerSet: make([]*Dialer, 0, len(dialers)),
@ -89,10 +95,10 @@ func (a *AliveDialerSet) SetAlive(dialer *Dialer, alive bool) {
switch a.selectionPolicy {
case consts.DialerSelectionPolicy_MinLastLatency:
latency, hasLatency = dialer.Latencies10.LastLatency()
latency, hasLatency = dialer.MustGetLatencies10(a.l4proto, a.ipversion).LastLatency()
minPolicy = true
case consts.DialerSelectionPolicy_MinAverage10Latencies:
latency, hasLatency = dialer.Latencies10.AvgLatency()
latency, hasLatency = dialer.MustGetLatencies10(a.l4proto, a.ipversion).AvgLatency()
minPolicy = true
}
@ -149,11 +155,13 @@ func (a *AliveDialerSet) SetAlive(dialer *Dialer, alive bool) {
a.log.WithFields(logrus.Fields{
string(a.selectionPolicy): a.minLatency.latency,
"group": a.dialerGroupName,
"l4proto": a.l4proto,
"dialer": a.minLatency.dialer.Name(),
}).Infof("Group re-selects dialer")
} else {
a.log.WithFields(logrus.Fields{
"group": a.dialerGroupName,
"group": a.dialerGroupName,
"l4proto": a.l4proto,
}).Infof("Group has no dialer alive")
}
}
@ -162,8 +170,9 @@ func (a *AliveDialerSet) SetAlive(dialer *Dialer, alive bool) {
// Use first dialer if no dialer has alive state (usually happen at the very beginning).
a.minLatency.dialer = dialer
a.log.WithFields(logrus.Fields{
"group": a.dialerGroupName,
"dialer": a.minLatency.dialer.Name(),
"group": a.dialerGroupName,
"l4proto": a.l4proto,
"dialer": a.minLatency.dialer.Name(),
}).Infof("Group selects dialer")
}
}

View File

@ -16,5 +16,5 @@ func (*blockDialer) Dial(network string, addr string) (c net.Conn, err error) {
}
func NewBlockDialer(option *GlobalOption) *Dialer {
return NewDialer(&blockDialer{}, option, InstanceOption{Check: false}, true, "block", "block", "")
return NewDialer(&blockDialer{}, option, InstanceOption{CheckEnabled: false}, "block", "block", "")
}

View File

@ -0,0 +1,340 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) since 2023, mzz2017 <mzz@tuta.io>
*/
package dialer
import (
"context"
"errors"
"fmt"
"github.com/mzz2017/softwind/pkg/fastrand"
"github.com/sirupsen/logrus"
"github.com/v2rayA/dae/common/consts"
"github.com/v2rayA/dae/common/netutils"
"golang.org/x/net/dns/dnsmessage"
"net"
"net/http"
"net/netip"
"net/url"
"path"
"strconv"
"strings"
"time"
)
var (
BootstrapDns = netip.MustParseAddrPort("223.5.5.5:53")
)
type Ip46 struct {
Ip4 netip.Addr
Ip6 netip.Addr
}
func ParseIp46(ctx context.Context, host string) (ipv46 *Ip46, err error) {
addrs4, err := netutils.ResolveNetip(ctx, SymmetricDirect, BootstrapDns, host, dnsmessage.TypeA)
if err != nil {
return nil, err
}
if len(addrs4) == 0 {
return nil, fmt.Errorf("domain \"%v\" has no ipv4 record", host)
}
addrs6, err := netutils.ResolveNetip(ctx, SymmetricDirect, BootstrapDns, host, dnsmessage.TypeAAAA)
if err != nil {
return nil, err
}
if len(addrs6) == 0 {
return nil, fmt.Errorf("domain \"%v\" has no ipv6 record", host)
}
return &Ip46{
Ip4: addrs4[0],
Ip6: addrs6[0],
}, nil
}
type TcpCheckOption struct {
Url *netutils.URL
*Ip46
}
func ParseTcpCheckOption(ctx context.Context, rawURL string) (opt *TcpCheckOption, err error) {
u, err := url.Parse(rawURL)
if err != nil {
return nil, err
}
ip46, err := ParseIp46(ctx, u.Hostname())
if err != nil {
return nil, err
}
return &TcpCheckOption{
Url: &netutils.URL{URL: u},
Ip46: ip46,
}, nil
}
type UdpCheckOption struct {
DnsHost string
DnsPort uint16
*Ip46
}
func ParseUdpCheckOption(ctx context.Context, dnsHostPort string) (opt *UdpCheckOption, err error) {
host, _port, err := net.SplitHostPort(dnsHostPort)
if err != nil {
return nil, err
}
port, err := strconv.ParseUint(_port, 10, 16)
if err != nil {
return nil, fmt.Errorf("bad port: %v", err)
}
ip46, err := ParseIp46(ctx, host)
if err != nil {
return nil, err
}
return &UdpCheckOption{
DnsHost: host,
DnsPort: uint16(port),
Ip46: ip46,
}, nil
}
type CheckOption struct {
ResultLogger LatencyLogger
CheckFunc func(ctx context.Context) (ok bool, err error)
}
type LatencyLogger struct {
L4proto consts.L4ProtoStr
IpVersion consts.IpVersionStr
LatencyN *LatenciesN
AliveDialerSetSet AliveDialerSetSet
}
func (d *Dialer) ActivateCheck() {
d.tickerMu.Lock()
defer d.tickerMu.Unlock()
if d.instanceOption.CheckEnabled {
return
}
d.instanceOption.CheckEnabled = true
go d.aliveBackground()
}
func (d *Dialer) aliveBackground() {
timeout := 10 * time.Second
cycle := d.CheckInterval
tcp4CheckOpt := &CheckOption{
ResultLogger: LatencyLogger{
L4proto: consts.L4ProtoStr_TCP,
IpVersion: consts.IpVersionStr_4,
LatencyN: d.tcp4Latencies10,
AliveDialerSetSet: d.tcp4AliveDialerSetSet,
},
CheckFunc: func(ctx context.Context) (ok bool, err error) {
return d.HttpCheck(ctx, d.TcpCheckOption.Url, d.TcpCheckOption.Ip4)
},
}
tcp6CheckOpt := &CheckOption{
ResultLogger: LatencyLogger{
L4proto: consts.L4ProtoStr_TCP,
IpVersion: consts.IpVersionStr_6,
LatencyN: d.tcp6Latencies10,
AliveDialerSetSet: d.tcp6AliveDialerSetSet,
},
CheckFunc: func(ctx context.Context) (ok bool, err error) {
return d.HttpCheck(ctx, d.TcpCheckOption.Url, d.TcpCheckOption.Ip6)
},
}
udp4CheckOpt := &CheckOption{
ResultLogger: LatencyLogger{
L4proto: consts.L4ProtoStr_UDP,
IpVersion: consts.IpVersionStr_4,
LatencyN: d.udp4Latencies10,
AliveDialerSetSet: d.udp4AliveDialerSetSet,
},
CheckFunc: func(ctx context.Context) (ok bool, err error) {
return d.DnsCheck(ctx, netip.AddrPortFrom(d.UdpCheckOption.Ip4, d.UdpCheckOption.DnsPort))
},
}
udp6CheckOpt := &CheckOption{
ResultLogger: LatencyLogger{
L4proto: consts.L4ProtoStr_UDP,
IpVersion: consts.IpVersionStr_6,
LatencyN: d.udp6Latencies10,
AliveDialerSetSet: d.udp6AliveDialerSetSet,
},
CheckFunc: func(ctx context.Context) (ok bool, err error) {
return d.DnsCheck(ctx, netip.AddrPortFrom(d.UdpCheckOption.Ip4, d.UdpCheckOption.DnsPort))
},
}
// Check once immediately.
go d.Check(timeout, tcp4CheckOpt)
go d.Check(timeout, udp4CheckOpt)
go d.Check(timeout, tcp6CheckOpt)
go d.Check(timeout, udp6CheckOpt)
// Sleep to avoid avalanche.
time.Sleep(time.Duration(fastrand.Int63n(int64(cycle))))
d.tickerMu.Lock()
d.ticker.Reset(cycle)
d.tickerMu.Unlock()
for range d.ticker.C {
// No need to test if there is no dialer selection policy using its latency.
if len(d.tcp4AliveDialerSetSet) > 0 {
go d.Check(timeout, tcp4CheckOpt)
}
if len(d.tcp6AliveDialerSetSet) > 0 {
go d.Check(timeout, tcp6CheckOpt)
}
if len(d.udp4AliveDialerSetSet) > 0 {
go d.Check(timeout, udp4CheckOpt)
}
if len(d.udp6AliveDialerSetSet) > 0 {
go d.Check(timeout, udp6CheckOpt)
}
}
}
func (d *Dialer) mustGetAliveDialerSetSet(l4proto consts.L4ProtoStr, ipversion consts.IpVersionStr) AliveDialerSetSet {
switch l4proto {
case consts.L4ProtoStr_TCP:
switch ipversion {
case consts.IpVersionStr_4:
return d.tcp4AliveDialerSetSet
case consts.IpVersionStr_6:
return d.tcp6AliveDialerSetSet
}
case consts.L4ProtoStr_UDP:
switch ipversion {
case consts.IpVersionStr_4:
return d.udp4AliveDialerSetSet
case consts.IpVersionStr_6:
return d.udp6AliveDialerSetSet
}
}
panic("invalid param")
}
func (d *Dialer) MustGetLatencies10(l4proto consts.L4ProtoStr, ipversion consts.IpVersionStr) *LatenciesN {
switch l4proto {
case consts.L4ProtoStr_TCP:
switch ipversion {
case consts.IpVersionStr_4:
return d.tcp4Latencies10
case consts.IpVersionStr_6:
return d.tcp6Latencies10
}
case consts.L4ProtoStr_UDP:
switch ipversion {
case consts.IpVersionStr_4:
return d.udp4Latencies10
case consts.IpVersionStr_6:
return d.udp6Latencies10
}
}
panic("invalid param")
}
// RegisterAliveDialerSet is thread-safe.
func (d *Dialer) RegisterAliveDialerSet(a *AliveDialerSet, l4proto consts.L4ProtoStr, ipversion consts.IpVersionStr) {
d.aliveDialerSetSetMu.Lock()
d.mustGetAliveDialerSetSet(l4proto, ipversion)[a]++
d.aliveDialerSetSetMu.Unlock()
}
// UnregisterAliveDialerSet is thread-safe.
func (d *Dialer) UnregisterAliveDialerSet(a *AliveDialerSet, l4proto consts.L4ProtoStr, ipversion consts.IpVersionStr) {
d.aliveDialerSetSetMu.Lock()
defer d.aliveDialerSetSetMu.Unlock()
setSet := d.mustGetAliveDialerSetSet(l4proto, ipversion)
setSet[a]--
if setSet[a] <= 0 {
delete(setSet, a)
}
}
func (d *Dialer) Check(timeout time.Duration,
opts *CheckOption,
) (ok bool, err error) {
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
start := time.Now()
// Calc latency.
var alive bool
if ok, err = opts.CheckFunc(ctx); ok && err == nil {
// No error.
latency := time.Since(start)
opts.ResultLogger.LatencyN.AppendLatency(latency)
avg, _ := opts.ResultLogger.LatencyN.AvgLatency()
d.Log.WithFields(logrus.Fields{
// Add a space to ensure alphabetical order is first.
"network": string(opts.ResultLogger.L4proto) + string(opts.ResultLogger.IpVersion),
"node": d.name,
"last": latency.Truncate(time.Millisecond),
"avg_10": avg.Truncate(time.Millisecond),
}).Debugln("Connectivity Check")
alive = true
} else {
// Append timeout if there is any error or unexpected status code.
if err != nil {
d.Log.WithFields(logrus.Fields{
// Add a space to ensure alphabetical order is first.
"network": string(opts.ResultLogger.L4proto) + string(opts.ResultLogger.IpVersion),
"node": d.name,
"err": err.Error(),
}).Debugln("Connectivity Check")
}
opts.ResultLogger.LatencyN.AppendLatency(timeout)
}
// Inform DialerGroups to update state.
d.aliveDialerSetSetMu.Lock()
for a := range opts.ResultLogger.AliveDialerSetSet {
a.SetAlive(d, alive)
}
d.aliveDialerSetSetMu.Unlock()
return ok, err
}
func (d *Dialer) HttpCheck(ctx context.Context, u *netutils.URL, ip netip.Addr) (ok bool, err error) {
// HTTP(S) check.
cd := netutils.ContextDialer{d.Dialer}
cli := http.Client{
Transport: &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (c net.Conn, err error) {
// Force to dial "ip".
return cd.DialContext(ctx, "tcp", net.JoinHostPort(ip.String(), u.Port()))
},
},
}
req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil)
if err != nil {
return false, err
}
resp, err := cli.Do(req)
if err != nil {
var netErr net.Error
if errors.As(err, &netErr); netErr.Timeout() {
err = fmt.Errorf("timeout")
}
return false, err
}
defer resp.Body.Close()
// Judge the status code.
if page := path.Base(req.URL.Path); strings.HasPrefix(page, "generate_") {
return strconv.Itoa(resp.StatusCode) == strings.TrimPrefix(page, "generate_"), nil
}
return resp.StatusCode >= 200 && resp.StatusCode < 400, nil
}
func (d *Dialer) DnsCheck(ctx context.Context, dns netip.AddrPort) (ok bool, err error) {
addrs, err := netutils.ResolveNetip(ctx, d, dns, consts.UdpCheckLookupHost, dnsmessage.TypeA)
if err != nil {
return false, err
}
if len(addrs) == 0 {
return false, fmt.Errorf("bad DNS response: no record")
}
return true, nil
}

View File

@ -1,103 +1,79 @@
package dialer
import (
"context"
"errors"
"fmt"
"github.com/mzz2017/softwind/pkg/fastrand"
"github.com/sirupsen/logrus"
"golang.org/x/net/proxy"
"net"
"net/http"
"path"
"strconv"
"strings"
"sync"
"time"
)
var (
ConnectivityTestFailedErr = fmt.Errorf("Connectivity Check failed")
UnexpectedFieldErr = fmt.Errorf("unexpected field")
InvalidParameterErr = fmt.Errorf("invalid parameters")
UnexpectedFieldErr = fmt.Errorf("unexpected field")
InvalidParameterErr = fmt.Errorf("invalid parameters")
)
type Dialer struct {
*GlobalOption
instanceOption InstanceOption
proxy.Dialer
supportUDP bool
name string
protocol string
link string
name string
protocol string
link string
Latencies10 *LatenciesN
tcp4Latencies10 *LatenciesN
tcp6Latencies10 *LatenciesN
udp4Latencies10 *LatenciesN
udp6Latencies10 *LatenciesN
aliveDialerSetSetMu sync.Mutex
// aliveDialerSetSet uses reference counting.
aliveDialerSetSet map[*AliveDialerSet]int
tcp4AliveDialerSetSet AliveDialerSetSet
tcp6AliveDialerSetSet AliveDialerSetSet
udp4AliveDialerSetSet AliveDialerSetSet
udp6AliveDialerSetSet AliveDialerSetSet
tickerMu sync.Mutex
ticker *time.Ticker
}
type GlobalOption struct {
Log *logrus.Logger
CheckUrl string
CheckInterval time.Duration
Log *logrus.Logger
TcpCheckOption *TcpCheckOption
UdpCheckOption *UdpCheckOption
CheckInterval time.Duration
}
type InstanceOption struct {
Check bool
CheckEnabled bool
}
type AliveDialerSetSet map[*AliveDialerSet]int
// NewDialer is for register in general.
func NewDialer(dialer proxy.Dialer, option *GlobalOption, iOption InstanceOption, supportUDP bool, name string, protocol string, link string) *Dialer {
func NewDialer(dialer proxy.Dialer, option *GlobalOption, iOption InstanceOption, name string, protocol string, link string) *Dialer {
d := &Dialer{
Dialer: dialer,
GlobalOption: option,
instanceOption: iOption,
supportUDP: supportUDP,
name: name,
protocol: protocol,
link: link,
Latencies10: NewLatenciesN(10),
Dialer: dialer,
GlobalOption: option,
instanceOption: iOption,
name: name,
protocol: protocol,
link: link,
tcp4Latencies10: NewLatenciesN(10),
tcp6Latencies10: NewLatenciesN(10),
udp4Latencies10: NewLatenciesN(10),
udp6Latencies10: NewLatenciesN(10),
// Set a very big cycle to wait for init.
ticker: time.NewTicker(time.Hour),
aliveDialerSetSet: make(map[*AliveDialerSet]int),
ticker: time.NewTicker(time.Hour),
tcp4AliveDialerSetSet: make(AliveDialerSetSet),
tcp6AliveDialerSetSet: make(AliveDialerSetSet),
udp4AliveDialerSetSet: make(AliveDialerSetSet),
udp6AliveDialerSetSet: make(AliveDialerSetSet),
}
if iOption.Check {
if iOption.CheckEnabled {
go d.aliveBackground()
}
return d
}
func (d *Dialer) ActiveCheck() {
d.tickerMu.Lock()
defer d.tickerMu.Unlock()
if d.instanceOption.Check {
return
}
d.instanceOption.Check = true
go d.aliveBackground()
}
func (d *Dialer) aliveBackground() {
timeout := 10 * time.Second
cycle := d.CheckInterval
// Check once immediately.
go d.Check(timeout, d.CheckUrl)
// Sleep to avoid avalanche.
time.Sleep(time.Duration(fastrand.Int63n(int64(cycle))))
d.tickerMu.Lock()
d.ticker.Reset(cycle)
d.tickerMu.Unlock()
for range d.ticker.C {
// No need to test if there is no dialer selection policy using its latency.
if len(d.aliveDialerSetSet) > 0 {
d.Check(timeout, d.CheckUrl)
}
}
}
func (d *Dialer) Close() error {
d.tickerMu.Lock()
@ -106,27 +82,6 @@ func (d *Dialer) Close() error {
return nil
}
// RegisterAliveDialerSet is thread-safe.
func (d *Dialer) RegisterAliveDialerSet(a *AliveDialerSet) {
d.aliveDialerSetSetMu.Lock()
d.aliveDialerSetSet[a]++
d.aliveDialerSetSetMu.Unlock()
}
// UnregisterAliveDialerSet is thread-safe.
func (d *Dialer) UnregisterAliveDialerSet(a *AliveDialerSet) {
d.aliveDialerSetSetMu.Lock()
defer d.aliveDialerSetSetMu.Unlock()
d.aliveDialerSetSet[a]--
if d.aliveDialerSetSet[a] <= 0 {
delete(d.aliveDialerSetSet, a)
}
}
func (d *Dialer) SupportUDP() bool {
return d.supportUDP
}
func (d *Dialer) Name() string {
return d.name
}
@ -138,60 +93,3 @@ func (d *Dialer) Protocol() string {
func (d *Dialer) Link() string {
return d.link
}
func (d *Dialer) Check(timeout time.Duration, url string) (ok bool, err error) {
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
start := time.Now()
// Calc latency.
defer func() {
var alive bool
if ok && err == nil {
// No error.
latency := time.Since(start)
d.Latencies10.AppendLatency(latency)
avg, _ := d.Latencies10.AvgLatency()
d.Log.WithField("node", d.name).WithField("last", latency.Truncate(time.Millisecond)).WithField("avg_10", avg.Truncate(time.Millisecond)).Debugf("Connectivity Check")
alive = true
} else {
// Append timeout if there is any error or unexpected status code.
if err != nil {
d.Log.Debugf("Connectivity Check <%v>: %v", d.name, err.Error())
}
d.Latencies10.AppendLatency(timeout)
}
// Inform DialerGroups to update state.
d.aliveDialerSetSetMu.Lock()
for a := range d.aliveDialerSetSet {
a.SetAlive(d, alive)
}
d.aliveDialerSetSetMu.Unlock()
}()
// HTTP(S) test.
cd := ContextDialer{d.Dialer}
cli := http.Client{
Transport: &http.Transport{
DialContext: cd.DialContext,
},
Timeout: timeout,
}
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return false, fmt.Errorf("%v: %w", ConnectivityTestFailedErr, err)
}
resp, err := cli.Do(req)
if err != nil {
var netErr net.Error
if errors.As(err, &netErr); netErr.Timeout() {
err = fmt.Errorf("timeout")
}
return false, fmt.Errorf("%v: %w", ConnectivityTestFailedErr, err)
}
defer resp.Body.Close()
// Judge the status code.
if page := path.Base(req.URL.Path); strings.HasPrefix(page, "generate_") {
return strconv.Itoa(resp.StatusCode) == strings.TrimPrefix(page, "generate_"), nil
}
return resp.StatusCode >= 200 && resp.StatusCode < 400, nil
}

View File

@ -10,9 +10,9 @@ var FullconeDirect = newDirect(true)
func NewDirectDialer(option *GlobalOption, fullcone bool) *Dialer {
if fullcone {
return NewDialer(FullconeDirect, option, InstanceOption{Check: false}, true, "direct", "direct", "")
return NewDialer(FullconeDirect, option, InstanceOption{CheckEnabled: false}, "direct", "direct", "")
} else {
return NewDialer(SymmetricDirect, option, InstanceOption{Check: false}, true, "direct", "direct", "")
return NewDialer(SymmetricDirect, option, InstanceOption{CheckEnabled: false}, "direct", "direct", "")
}
}
@ -25,7 +25,7 @@ type direct struct {
func newDirect(fullCone bool) proxy.Dialer {
return &direct{
netDialer: &net.Dialer{},
fullCone: fullCone,
fullCone: fullCone,
}
}

View File

@ -67,7 +67,7 @@ func (s *HTTP) Dialer(option *dialer.GlobalOption, iOption dialer.InstanceOption
if err != nil {
return nil, err
}
return dialer.NewDialer(d, option, iOption, false, s.Name, s.Protocol, u.String()), nil
return dialer.NewDialer(d, option, iOption, s.Name, s.Protocol, u.String()), nil
}
func (s *HTTP) URL() url.URL {

View File

@ -48,7 +48,6 @@ func (s *Shadowsocks) Dialer(option *dialer.GlobalOption, iOption dialer.Instanc
default:
return nil, fmt.Errorf("unsupported shadowsocks encryption method: %v", s.Cipher)
}
supportUDP := s.UDP
d := dialer.FullconeDirect // Shadowsocks Proxy supports full-cone.
d, err := protocol.NewDialer("shadowsocks", d, protocol.Header{
ProxyAddress: net.JoinHostPort(s.Server, strconv.Itoa(s.Port)),
@ -74,9 +73,8 @@ func (s *Shadowsocks) Dialer(option *dialer.GlobalOption, iOption dialer.Instanc
if err != nil {
return nil, err
}
supportUDP = false
}
return dialer.NewDialer(d, option, iOption, supportUDP, s.Name, s.Protocol, s.ExportToURL()), nil
return dialer.NewDialer(d, option, iOption, s.Name, s.Protocol, s.ExportToURL()), nil
}
func ParseSSURL(u string) (data *Shadowsocks, err error) {

View File

@ -54,7 +54,7 @@ func (s *ShadowsocksR) Dialer(option *dialer.GlobalOption, iOption dialer.Instan
if err != nil {
return nil, err
}
return dialer.NewDialer(d, option, iOption, false, s.Name, s.Protocol, s.ExportToURL()), nil
return dialer.NewDialer(d, option, iOption, s.Name, s.Protocol, s.ExportToURL()), nil
}
func ParseSSRURL(u string) (data *ShadowsocksR, err error) {

View File

@ -42,7 +42,7 @@ func (s *Socks) Dialer(option *dialer.GlobalOption, iOption dialer.InstanceOptio
if err != nil {
return nil, err
}
return dialer.NewDialer(d, option, iOption, true, s.Name, s.Protocol, link), nil
return dialer.NewDialer(d, option, iOption, s.Name, s.Protocol, link), nil
//case "socks4", "socks4a":
// d, err := socks4.NewSocks4Dialer(link, &proxy.Direct{})
// if err != nil {

View File

@ -101,7 +101,7 @@ func (s *Trojan) Dialer(option *dialer.GlobalOption, iOption dialer.InstanceOpti
}); err != nil {
return nil, err
}
return dialer.NewDialer(d, option, iOption, true, s.Name, s.Protocol, s.ExportToURL()), nil
return dialer.NewDialer(d, option, iOption, s.Name, s.Protocol, s.ExportToURL()), nil
}
func ParseTrojanURL(u string) (data *Trojan, err error) {

View File

@ -147,7 +147,7 @@ func (s *V2Ray) Dialer(option *dialer.GlobalOption, iOption dialer.InstanceOptio
}); err != nil {
return nil, err
}
return dialer.NewDialer(d, option, iOption, true, s.Ps, s.Protocol, s.ExportToURL()), nil
return dialer.NewDialer(d, option, iOption, s.Ps, s.Protocol, s.ExportToURL()), nil
}
func ParseVlessURL(vless string) (data *V2Ray, err error) {

View File

@ -12,6 +12,8 @@ import (
"github.com/v2rayA/dae/component/outbound/dialer"
"golang.org/x/net/proxy"
"net"
"net/netip"
"strings"
)
type DialerGroup struct {
@ -24,7 +26,10 @@ type DialerGroup struct {
Dialers []*dialer.Dialer
registeredAliveDialerSet bool
AliveDialerSet *dialer.AliveDialerSet
AliveTcp4DialerSet *dialer.AliveDialerSet
AliveTcp6DialerSet *dialer.AliveDialerSet
AliveUdp4DialerSet *dialer.AliveDialerSet
AliveUdp6DialerSet *dialer.AliveDialerSet
selectionPolicy *DialerSelectionPolicy
}
@ -32,7 +37,10 @@ type DialerGroup struct {
func NewDialerGroup(option *dialer.GlobalOption, name string, dialers []*dialer.Dialer, p DialerSelectionPolicy) *DialerGroup {
log := option.Log
var registeredAliveDialerSet bool
a := dialer.NewAliveDialerSet(log, name, p.Policy, dialers, true)
aliveTcp4DialerSet := dialer.NewAliveDialerSet(log, name, consts.L4ProtoStr_TCP, consts.IpVersionStr_4, p.Policy, dialers, true)
aliveTcp6DialerSet := dialer.NewAliveDialerSet(log, name, consts.L4ProtoStr_TCP, consts.IpVersionStr_6, p.Policy, dialers, true)
aliveUdp4DialerSet := dialer.NewAliveDialerSet(log, name, consts.L4ProtoStr_UDP, consts.IpVersionStr_4, p.Policy, dialers, true)
aliveUdp6DialerSet := dialer.NewAliveDialerSet(log, name, consts.L4ProtoStr_UDP, consts.IpVersionStr_6, p.Policy, dialers, true)
switch p.Policy {
case consts.DialerSelectionPolicy_Random,
@ -40,7 +48,10 @@ func NewDialerGroup(option *dialer.GlobalOption, name string, dialers []*dialer.
consts.DialerSelectionPolicy_MinAverage10Latencies:
// Need to know the alive state or latency.
for _, d := range dialers {
d.RegisterAliveDialerSet(a)
d.RegisterAliveDialerSet(aliveTcp4DialerSet, consts.L4ProtoStr_TCP, consts.IpVersionStr_4)
d.RegisterAliveDialerSet(aliveTcp6DialerSet, consts.L4ProtoStr_TCP, consts.IpVersionStr_6)
d.RegisterAliveDialerSet(aliveUdp4DialerSet, consts.L4ProtoStr_UDP, consts.IpVersionStr_4)
d.RegisterAliveDialerSet(aliveUdp6DialerSet, consts.L4ProtoStr_UDP, consts.IpVersionStr_6)
}
registeredAliveDialerSet = true
@ -56,7 +67,10 @@ func NewDialerGroup(option *dialer.GlobalOption, name string, dialers []*dialer.
Name: name,
Dialers: dialers,
block: dialer.NewBlockDialer(option),
AliveDialerSet: a,
AliveTcp4DialerSet: aliveTcp4DialerSet,
AliveTcp6DialerSet: aliveTcp6DialerSet,
AliveUdp4DialerSet: aliveUdp4DialerSet,
AliveUdp6DialerSet: aliveUdp6DialerSet,
registeredAliveDialerSet: registeredAliveDialerSet,
selectionPolicy: &p,
}
@ -65,7 +79,10 @@ func NewDialerGroup(option *dialer.GlobalOption, name string, dialers []*dialer.
func (g *DialerGroup) Close() error {
if g.registeredAliveDialerSet {
for _, d := range g.Dialers {
d.UnregisterAliveDialerSet(g.AliveDialerSet)
d.UnregisterAliveDialerSet(g.AliveTcp4DialerSet, consts.L4ProtoStr_TCP, consts.IpVersionStr_4)
d.UnregisterAliveDialerSet(g.AliveTcp6DialerSet, consts.L4ProtoStr_TCP, consts.IpVersionStr_6)
d.UnregisterAliveDialerSet(g.AliveUdp4DialerSet, consts.L4ProtoStr_UDP, consts.IpVersionStr_4)
d.UnregisterAliveDialerSet(g.AliveUdp6DialerSet, consts.L4ProtoStr_UDP, consts.IpVersionStr_6)
}
}
return nil
@ -77,17 +94,39 @@ func (g *DialerGroup) SetSelectionPolicy(policy DialerSelectionPolicy) {
}
// Select selects a dialer from group according to selectionPolicy.
func (g *DialerGroup) Select() (*dialer.Dialer, error) {
func (g *DialerGroup) Select(l4proto consts.L4ProtoStr, ipversion consts.IpVersionStr) (*dialer.Dialer, error) {
if len(g.Dialers) == 0 {
return nil, fmt.Errorf("no dialer in this group")
}
var a *dialer.AliveDialerSet
switch l4proto {
case consts.L4ProtoStr_TCP:
switch ipversion {
case consts.IpVersionStr_4:
a = g.AliveTcp4DialerSet
case consts.IpVersionStr_6:
a = g.AliveTcp6DialerSet
}
case consts.L4ProtoStr_UDP:
switch ipversion {
case consts.IpVersionStr_4:
a = g.AliveUdp4DialerSet
case consts.IpVersionStr_6:
a = g.AliveUdp6DialerSet
}
default:
return nil, fmt.Errorf("DialerGroup.Select: unexpected l4proto type: %v", l4proto)
}
switch g.selectionPolicy.Policy {
case consts.DialerSelectionPolicy_Random:
d := g.AliveDialerSet.GetRand()
d := a.GetRand()
if d == nil {
// No alive dialer.
g.log.Warnf("No alive dialer in DialerGroup %v, use \"block\".", g.Name)
g.log.WithFields(logrus.Fields{
"l4proto": l4proto,
"group": g.Name,
}).Warnf("No alive dialer in DialerGroup, use \"block\".")
return g.block, nil
}
return d, nil
@ -99,10 +138,13 @@ func (g *DialerGroup) Select() (*dialer.Dialer, error) {
return g.Dialers[g.selectionPolicy.FixedIndex], nil
case consts.DialerSelectionPolicy_MinLastLatency, consts.DialerSelectionPolicy_MinAverage10Latencies:
d := g.AliveDialerSet.GetMinLatency()
d := a.GetMinLatency()
if d == nil {
// No alive dialer.
g.log.Warnf("No alive dialer in DialerGroup %v, use \"block\".", g.Name)
g.log.WithFields(logrus.Fields{
"l4proto": l4proto,
"group": g.Name,
}).Warnf("No alive dialer in DialerGroup, use \"block\".")
return g.block, nil
}
return d, nil
@ -113,7 +155,20 @@ func (g *DialerGroup) Select() (*dialer.Dialer, error) {
}
func (g *DialerGroup) Dial(network string, addr string) (c net.Conn, err error) {
d, err := g.Select()
var d proxy.Dialer
ipAddr, err := netip.ParseAddr(addr)
if err != nil {
return nil, fmt.Errorf("DialerGroup.Dial only supports ip as addr")
}
ipversion := consts.IpVersionFromAddr(ipAddr)
switch {
case strings.HasPrefix(network, "tcp"):
d, err = g.Select(consts.L4ProtoStr_TCP, ipversion)
case strings.HasPrefix(network, "udp"):
d, err = g.Select(consts.L4ProtoStr_UDP, ipversion)
default:
return nil, fmt.Errorf("unexpected network: %v", network)
}
if err != nil {
return nil, err
}

View File

@ -6,6 +6,7 @@
package outbound
import (
"context"
"github.com/mzz2017/softwind/pkg/fastrand"
"github.com/v2rayA/dae/common/consts"
"github.com/v2rayA/dae/component/outbound/dialer"
@ -15,14 +16,25 @@ import (
)
const (
testCheckUrl = "https://connectivitycheck.gstatic.com/generate_204"
testTcpCheckUrl = "https://connectivitycheck.gstatic.com/generate_204"
testUdpCheckDns = "https://connectivitycheck.gstatic.com/generate_204"
)
func TestDialerGroup_Select_Fixed(t *testing.T) {
log := logger.NewLogger(2)
log := logger.NewLogger("trace", false)
topt, err := dialer.ParseTcpCheckOption(context.TODO(), testTcpCheckUrl)
if err != nil {
t.Fatal(err)
}
uopt, err := dialer.ParseUdpCheckOption(context.TODO(), testUdpCheckDns)
if err != nil {
t.Fatal(err)
}
option := &dialer.GlobalOption{
Log: log,
CheckUrl: testCheckUrl,
Log: log,
TcpCheckOption: topt,
UdpCheckOption: uopt,
CheckInterval: 15 * time.Second,
}
dialers := []*dialer.Dialer{
dialer.NewDirectDialer(option, true),
@ -34,7 +46,7 @@ func TestDialerGroup_Select_Fixed(t *testing.T) {
FixedIndex: fixedIndex,
})
for i := 0; i < 10; i++ {
d, err := g.Select()
d, err := g.Select(consts.L4ProtoStr_TCP, consts.IpVersionStr_4)
if err != nil {
t.Fatal(err)
}
@ -46,7 +58,7 @@ func TestDialerGroup_Select_Fixed(t *testing.T) {
fixedIndex = 0
g.selectionPolicy.FixedIndex = fixedIndex
for i := 0; i < 10; i++ {
d, err := g.Select()
d, err := g.Select(consts.L4ProtoStr_TCP, consts.IpVersionStr_4)
if err != nil {
t.Fatal(err)
}
@ -57,10 +69,20 @@ func TestDialerGroup_Select_Fixed(t *testing.T) {
}
func TestDialerGroup_Select_MinLastLatency(t *testing.T) {
log := logger.NewLogger(2)
log := logger.NewLogger("trace", false)
topt, err := dialer.ParseTcpCheckOption(context.TODO(), testTcpCheckUrl)
if err != nil {
t.Fatal(err)
}
uopt, err := dialer.ParseUdpCheckOption(context.TODO(), testUdpCheckDns)
if err != nil {
t.Fatal(err)
}
option := &dialer.GlobalOption{
Log: log,
CheckUrl: testCheckUrl,
Log: log,
TcpCheckOption: topt,
UdpCheckOption: uopt,
CheckInterval: 15 * time.Second,
}
dialers := []*dialer.Dialer{
dialer.NewDirectDialer(option, false),
@ -98,14 +120,14 @@ func TestDialerGroup_Select_MinLastLatency(t *testing.T) {
latency = time.Duration(fastrand.Int63n(int64(1000 * time.Millisecond)))
alive = true
}
d.Latencies10.AppendLatency(latency)
d.MustGetLatencies10(consts.L4ProtoStr_TCP, consts.IpVersionStr_4).AppendLatency(latency)
if jMinLatency == -1 || latency < minLatency {
jMinLatency = j
minLatency = latency
}
g.AliveDialerSet.SetAlive(d, alive)
g.AliveTcp4DialerSet.SetAlive(d, alive)
}
d, err := g.Select()
d, err := g.Select(consts.L4ProtoStr_TCP, consts.IpVersionStr_4)
if err != nil {
t.Fatal(err)
}
@ -124,10 +146,20 @@ func TestDialerGroup_Select_MinLastLatency(t *testing.T) {
}
func TestDialerGroup_Select_Random(t *testing.T) {
log := logger.NewLogger(2)
log := logger.NewLogger("trace", false)
topt, err := dialer.ParseTcpCheckOption(context.TODO(), testTcpCheckUrl)
if err != nil {
t.Fatal(err)
}
uopt, err := dialer.ParseUdpCheckOption(context.TODO(), testUdpCheckDns)
if err != nil {
t.Fatal(err)
}
option := &dialer.GlobalOption{
Log: log,
CheckUrl: testCheckUrl,
Log: log,
TcpCheckOption: topt,
UdpCheckOption: uopt,
CheckInterval: 15 * time.Second,
}
dialers := []*dialer.Dialer{
dialer.NewDirectDialer(option, false),
@ -141,7 +173,7 @@ func TestDialerGroup_Select_Random(t *testing.T) {
})
count := make([]int, len(dialers))
for i := 0; i < 100; i++ {
d, err := g.Select()
d, err := g.Select(consts.L4ProtoStr_TCP, consts.IpVersionStr_4)
if err != nil {
t.Fatal(err)
}
@ -161,10 +193,20 @@ func TestDialerGroup_Select_Random(t *testing.T) {
}
func TestDialerGroup_SetAlive(t *testing.T) {
log := logger.NewLogger(2)
log := logger.NewLogger("trace", false)
topt, err := dialer.ParseTcpCheckOption(context.TODO(), testTcpCheckUrl)
if err != nil {
t.Fatal(err)
}
uopt, err := dialer.ParseUdpCheckOption(context.TODO(), testUdpCheckDns)
if err != nil {
t.Fatal(err)
}
option := &dialer.GlobalOption{
Log: log,
CheckUrl: testCheckUrl,
Log: log,
TcpCheckOption: topt,
UdpCheckOption: uopt,
CheckInterval: 15 * time.Second,
}
dialers := []*dialer.Dialer{
dialer.NewDirectDialer(option, false),
@ -177,10 +219,10 @@ func TestDialerGroup_SetAlive(t *testing.T) {
Policy: consts.DialerSelectionPolicy_Random,
})
zeroTarget := 3
g.AliveDialerSet.SetAlive(dialers[zeroTarget], false)
g.AliveTcp4DialerSet.SetAlive(dialers[zeroTarget], false)
count := make([]int, len(dialers))
for i := 0; i < 100; i++ {
d, err := g.Select()
d, err := g.Select(consts.L4ProtoStr_UDP, consts.IpVersionStr_4)
if err != nil {
t.Fatal(err)
}

View File

@ -30,7 +30,7 @@ type DialerSet struct {
func NewDialerSetFromLinks(option *dialer.GlobalOption, nodes []string) *DialerSet {
s := &DialerSet{Dialers: make([]*dialer.Dialer, 0, len(nodes))}
for _, node := range nodes {
d, err := dialer.NewFromLink(option, dialer.InstanceOption{Check: false}, node)
d, err := dialer.NewFromLink(option, dialer.InstanceOption{CheckEnabled: false}, node)
if err != nil {
option.Log.Infof("failed to parse node: %v: %v", node, err)
continue
@ -40,7 +40,7 @@ func NewDialerSetFromLinks(option *dialer.GlobalOption, nodes []string) *DialerS
return s
}
func hit(dialer *dialer.Dialer, filters []*config_parser.Function) (hit bool, err error) {
func filterHit(dialer *dialer.Dialer, filters []*config_parser.Function) (hit bool, err error) {
// Example
// filter: name(regex:'^.*hk.*$', keyword:'sg') && name(keyword:'disney')
// filter: !name(regex: 'HK|TW|SG') && name(keyword: disney)
@ -85,7 +85,7 @@ func hit(dialer *dialer.Dialer, filters []*config_parser.Function) (hit bool, er
func (s *DialerSet) Filter(filters []*config_parser.Function) (dialers []*dialer.Dialer, err error) {
for _, d := range s.Dialers {
hit, err := hit(d, filters)
hit, err := filterHit(d, filters)
if err != nil {
return nil, err
}

View File

@ -7,26 +7,21 @@ package config
import (
"fmt"
"github.com/v2rayA/dae/common"
"github.com/v2rayA/dae/pkg/config_parser"
"net/url"
"reflect"
"time"
)
type UrlOrEmpty struct {
Url *url.URL
Empty bool
}
type Global struct {
TproxyPort uint16 `mapstructure:"tproxy_port" default:"12345"`
LogLevel string `mapstructure:"log_level" default:"info"`
TcpCheckUrl string `mapstructure:"tcp_check_url" default:"https://connectivitycheck.gstatic.com/generate_204"`
UdpCheckDns string `mapstructure:"udp_check_dns" default:"8.8.8.8:53"`
CheckInterval time.Duration `mapstructure:"check_interval" default:"15s"`
DnsUpstream UrlOrEmpty `mapstructure:"dns_upstream" require:""`
LanInterface []string `mapstructure:"lan_interface"`
WanInterface []string `mapstructure:"wan_interface"`
TproxyPort uint16 `mapstructure:"tproxy_port" default:"12345"`
LogLevel string `mapstructure:"log_level" default:"info"`
TcpCheckUrl string `mapstructure:"tcp_check_url" default:"http://cp.cloudflare.com"`
UdpCheckDns string `mapstructure:"udp_check_dns" default:"cloudflare-dns.com:53"`
CheckInterval time.Duration `mapstructure:"check_interval" default:"30s"`
DnsUpstream common.UrlOrEmpty `mapstructure:"dns_upstream" require:""`
LanInterface []string `mapstructure:"lan_interface"`
WanInterface []string `mapstructure:"wan_interface"`
}
type Group struct {

View File

@ -29,6 +29,7 @@ import (
"strings"
"sync"
"syscall"
"time"
)
type ControlPlane struct {
@ -186,10 +187,21 @@ func NewControlPlane(
}
/// DialerGroups (outbounds).
ctx, cancel := context.WithTimeout(context.TODO(), 10*time.Second)
defer cancel()
tcpCheckOption, err := dialer.ParseTcpCheckOption(ctx, global.TcpCheckUrl)
if err != nil {
return nil, fmt.Errorf("failed to parse tcp_check_url: %w", err)
}
udpCheckOption, err := dialer.ParseUdpCheckOption(ctx, global.UdpCheckDns)
if err != nil {
return nil, fmt.Errorf("failed to parse udp_check_dns: %w", err)
}
option := &dialer.GlobalOption{
Log: log,
CheckUrl: global.TcpCheckUrl,
CheckInterval: global.CheckInterval,
Log: log,
TcpCheckOption: tcpCheckOption,
UdpCheckOption: udpCheckOption,
CheckInterval: global.CheckInterval,
}
outbounds := []*outbound.DialerGroup{
outbound.NewDialerGroup(option, consts.OutboundDirect.String(),
@ -214,7 +226,7 @@ func NewControlPlane(
if err != nil {
return nil, fmt.Errorf("failed to create group %v: %w", group.Name, err)
}
// Filter nodes.
// Filter nodes with user given filters.
dialers, err := dialerSet.Filter(group.Param.Filter)
if err != nil {
return nil, fmt.Errorf(`failed to create group "%v": %w`, group.Name, err)
@ -223,7 +235,8 @@ func NewControlPlane(
log.Infof(`Group "%v" node list:`, group.Name)
for _, d := range dialers {
log.Infoln("\t" + d.Name())
d.ActiveCheck()
// We only activate check of nodes that have a group.
d.ActivateCheck()
}
if len(dialers) == 0 {
log.Infoln("\t<Empty>")

View File

@ -58,7 +58,7 @@ func (c *ControlPlane) handleConn(lConn net.Conn) (err error) {
if outboundIndex < 0 || int(outboundIndex) >= len(c.outbounds) {
return fmt.Errorf("outbound id from bpf is out of range: %v not in [0, %v]", outboundIndex, len(c.outbounds)-1)
}
dialer, err := outbound.Select()
dialer, err := outbound.Select(consts.L4ProtoStr_TCP, consts.IpVersionFromAddr(dst.Addr()))
if err != nil {
return fmt.Errorf("failed to select dialer from group %v: %w", outbound.Name, err)
}

View File

@ -184,11 +184,13 @@ func (c *ControlPlane) handlePkt(data []byte, src, dst netip.AddrPort, outboundI
// Because additional record OPT may not be supported by home router.
// So se should trust home devices even if they make rush-answer (or looks like).
validateRushAns := outboundIndex == consts.OutboundDirect && !destToSend.Addr().IsPrivate()
// Get udp endpoint.
ue, err := DefaultUdpEndpointPool.GetOrCreate(src, &UdpEndpointOptions{
Handler: c.RelayToUDP(src, isDns, dummyFrom, validateRushAns),
NatTimeout: natTimeout,
DialerFunc: func() (*dialer.Dialer, error) {
newDialer, err := outbound.Select()
newDialer, err := outbound.Select(consts.L4ProtoStr_UDP, consts.IpVersionFromAddr(dst.Addr()))
if err != nil {
return nil, fmt.Errorf("failed to select dialer from group %v: %w", outbound.Name, err)
}

View File

@ -6,7 +6,9 @@ global {
log_level: info
# Node connectivity check.
tcp_check_url: 'https://connectivitycheck.gstatic.com/generate_204'
# Url and dns should have both IPv4 and IPv6.
tcp_check_url: 'http://cp.cloudflare.com'
udp_check_dns: 'cloudflare-dns.com:53'
check_interval: 30s
# Now only support udp://IP:Port. Empty value '' indicates as-is.

2
go.mod
View File

@ -9,7 +9,7 @@ require (
github.com/gorilla/websocket v1.5.0
github.com/json-iterator/go v1.1.12
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
github.com/mzz2017/softwind v0.0.0-20230127172609-05c5264aa6a4
github.com/mzz2017/softwind v0.0.0-20230208101341-471784899114
github.com/safchain/ethtool v0.2.0
github.com/sirupsen/logrus v1.9.0
github.com/spf13/cobra v1.6.1

4
go.sum
View File

@ -69,8 +69,8 @@ github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/mzz2017/disk-bloom v1.0.1 h1:rEF9MiXd9qMW3ibRpqcerLXULoTgRlM21yqqJl1B90M=
github.com/mzz2017/disk-bloom v1.0.1/go.mod h1:JLHETtUu44Z6iBmsqzkOtFlRvXSlKnxjwiBRDapizDI=
github.com/mzz2017/softwind v0.0.0-20230127172609-05c5264aa6a4 h1:A5SQXPnd96JTjUusEZ2U+KTo7sQeAu8q38iu6TPhl2o=
github.com/mzz2017/softwind v0.0.0-20230127172609-05c5264aa6a4/go.mod h1:K1nXwtBokwEsfOfdT/5zV6R8QabGkyhcR0iuTrRZcYY=
github.com/mzz2017/softwind v0.0.0-20230208101341-471784899114 h1:7VK3nkOhmzcJrRtBGocZGXi4/9CDyqDDp4ezGIvk5BM=
github.com/mzz2017/softwind v0.0.0-20230208101341-471784899114/go.mod h1:K1nXwtBokwEsfOfdT/5zV6R8QabGkyhcR0iuTrRZcYY=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=