mirror of
https://github.com/daeuniverse/dae.git
synced 2024-12-23 01:34:42 +07:00
feat: support independent tcp4, tcp6, udp4, udp6 connectivity check
This commit is contained in:
parent
551e79d9e5
commit
5e7b68822a
@ -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
|
||||
}
|
||||
|
@ -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
102
common/netutils/dns.go
Normal 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
25
common/netutils/url.go
Normal 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 ""
|
||||
}
|
@ -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,
|
||||
}))
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
@ -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", "")
|
||||
}
|
||||
|
340
component/outbound/dialer/connectivity_check.go
Normal file
340
component/outbound/dialer/connectivity_check.go
Normal 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
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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>")
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
2
go.mod
@ -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
4
go.sum
@ -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=
|
||||
|
Loading…
Reference in New Issue
Block a user