mirror of
https://github.com/daeuniverse/dae.git
synced 2025-07-13 01:08:39 +07:00
feat: support independent tcp4, tcp6, udp4, udp6 connectivity check
This commit is contained in:
@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
package consts
|
package consts
|
||||||
|
|
||||||
|
import "net/netip"
|
||||||
|
|
||||||
type DialerSelectionPolicy string
|
type DialerSelectionPolicy string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -13,3 +15,32 @@ const (
|
|||||||
DialerSelectionPolicy_MinAverage10Latencies DialerSelectionPolicy = "min_avg10"
|
DialerSelectionPolicy_MinAverage10Latencies DialerSelectionPolicy = "min_avg10"
|
||||||
DialerSelectionPolicy_MinLastLatency DialerSelectionPolicy = "min"
|
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
|
* 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 (
|
import (
|
||||||
"context"
|
"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/binary"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/v2rayA/dae/config"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -22,6 +21,11 @@ var (
|
|||||||
ErrOverlayHierarchicalKey = fmt.Errorf("overlay hierarchical key")
|
ErrOverlayHierarchicalKey = fmt.Errorf("overlay hierarchical key")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type UrlOrEmpty struct {
|
||||||
|
Url *url.URL
|
||||||
|
Empty bool
|
||||||
|
}
|
||||||
|
|
||||||
func CloneStrings(slice []string) []string {
|
func CloneStrings(slice []string) []string {
|
||||||
c := make([]string, len(slice))
|
c := make([]string, len(slice))
|
||||||
copy(c, slice)
|
copy(c, slice)
|
||||||
@ -279,9 +283,9 @@ func FuzzyDecode(to interface{}, val string) bool {
|
|||||||
v.SetString(val)
|
v.SetString(val)
|
||||||
case reflect.Struct:
|
case reflect.Struct:
|
||||||
switch v.Interface().(type) {
|
switch v.Interface().(type) {
|
||||||
case config.UrlOrEmpty:
|
case UrlOrEmpty:
|
||||||
if val == "" {
|
if val == "" {
|
||||||
v.Set(reflect.ValueOf(config.UrlOrEmpty{
|
v.Set(reflect.ValueOf(UrlOrEmpty{
|
||||||
Url: nil,
|
Url: nil,
|
||||||
Empty: true,
|
Empty: true,
|
||||||
}))
|
}))
|
||||||
@ -290,7 +294,7 @@ func FuzzyDecode(to interface{}, val string) bool {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
v.Set(reflect.ValueOf(config.UrlOrEmpty{
|
v.Set(reflect.ValueOf(UrlOrEmpty{
|
||||||
Url: u,
|
Url: u,
|
||||||
Empty: false,
|
Empty: false,
|
||||||
}))
|
}))
|
||||||
|
@ -24,6 +24,8 @@ type minLatency struct {
|
|||||||
type AliveDialerSet struct {
|
type AliveDialerSet struct {
|
||||||
log *logrus.Logger
|
log *logrus.Logger
|
||||||
dialerGroupName string
|
dialerGroupName string
|
||||||
|
l4proto consts.L4ProtoStr
|
||||||
|
ipversion consts.IpVersionStr
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
dialerToIndex map[*Dialer]int // *Dialer -> index of inorderedAliveDialerSet
|
dialerToIndex map[*Dialer]int // *Dialer -> index of inorderedAliveDialerSet
|
||||||
@ -37,6 +39,8 @@ type AliveDialerSet struct {
|
|||||||
func NewAliveDialerSet(
|
func NewAliveDialerSet(
|
||||||
log *logrus.Logger,
|
log *logrus.Logger,
|
||||||
dialerGroupName string,
|
dialerGroupName string,
|
||||||
|
l4proto consts.L4ProtoStr,
|
||||||
|
ipversion consts.IpVersionStr,
|
||||||
selectionPolicy consts.DialerSelectionPolicy,
|
selectionPolicy consts.DialerSelectionPolicy,
|
||||||
dialers []*Dialer,
|
dialers []*Dialer,
|
||||||
setAlive bool,
|
setAlive bool,
|
||||||
@ -44,6 +48,8 @@ func NewAliveDialerSet(
|
|||||||
a := &AliveDialerSet{
|
a := &AliveDialerSet{
|
||||||
log: log,
|
log: log,
|
||||||
dialerGroupName: dialerGroupName,
|
dialerGroupName: dialerGroupName,
|
||||||
|
l4proto: l4proto,
|
||||||
|
ipversion: ipversion,
|
||||||
dialerToIndex: make(map[*Dialer]int),
|
dialerToIndex: make(map[*Dialer]int),
|
||||||
dialerToLatency: make(map[*Dialer]time.Duration),
|
dialerToLatency: make(map[*Dialer]time.Duration),
|
||||||
inorderedAliveDialerSet: make([]*Dialer, 0, len(dialers)),
|
inorderedAliveDialerSet: make([]*Dialer, 0, len(dialers)),
|
||||||
@ -89,10 +95,10 @@ func (a *AliveDialerSet) SetAlive(dialer *Dialer, alive bool) {
|
|||||||
|
|
||||||
switch a.selectionPolicy {
|
switch a.selectionPolicy {
|
||||||
case consts.DialerSelectionPolicy_MinLastLatency:
|
case consts.DialerSelectionPolicy_MinLastLatency:
|
||||||
latency, hasLatency = dialer.Latencies10.LastLatency()
|
latency, hasLatency = dialer.MustGetLatencies10(a.l4proto, a.ipversion).LastLatency()
|
||||||
minPolicy = true
|
minPolicy = true
|
||||||
case consts.DialerSelectionPolicy_MinAverage10Latencies:
|
case consts.DialerSelectionPolicy_MinAverage10Latencies:
|
||||||
latency, hasLatency = dialer.Latencies10.AvgLatency()
|
latency, hasLatency = dialer.MustGetLatencies10(a.l4proto, a.ipversion).AvgLatency()
|
||||||
minPolicy = true
|
minPolicy = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,11 +155,13 @@ func (a *AliveDialerSet) SetAlive(dialer *Dialer, alive bool) {
|
|||||||
a.log.WithFields(logrus.Fields{
|
a.log.WithFields(logrus.Fields{
|
||||||
string(a.selectionPolicy): a.minLatency.latency,
|
string(a.selectionPolicy): a.minLatency.latency,
|
||||||
"group": a.dialerGroupName,
|
"group": a.dialerGroupName,
|
||||||
|
"l4proto": a.l4proto,
|
||||||
"dialer": a.minLatency.dialer.Name(),
|
"dialer": a.minLatency.dialer.Name(),
|
||||||
}).Infof("Group re-selects dialer")
|
}).Infof("Group re-selects dialer")
|
||||||
} else {
|
} else {
|
||||||
a.log.WithFields(logrus.Fields{
|
a.log.WithFields(logrus.Fields{
|
||||||
"group": a.dialerGroupName,
|
"group": a.dialerGroupName,
|
||||||
|
"l4proto": a.l4proto,
|
||||||
}).Infof("Group has no dialer alive")
|
}).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).
|
// Use first dialer if no dialer has alive state (usually happen at the very beginning).
|
||||||
a.minLatency.dialer = dialer
|
a.minLatency.dialer = dialer
|
||||||
a.log.WithFields(logrus.Fields{
|
a.log.WithFields(logrus.Fields{
|
||||||
"group": a.dialerGroupName,
|
"group": a.dialerGroupName,
|
||||||
"dialer": a.minLatency.dialer.Name(),
|
"l4proto": a.l4proto,
|
||||||
|
"dialer": a.minLatency.dialer.Name(),
|
||||||
}).Infof("Group selects dialer")
|
}).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 {
|
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
|
package dialer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/mzz2017/softwind/pkg/fastrand"
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"golang.org/x/net/proxy"
|
"golang.org/x/net/proxy"
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"path"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ConnectivityTestFailedErr = fmt.Errorf("Connectivity Check failed")
|
UnexpectedFieldErr = fmt.Errorf("unexpected field")
|
||||||
UnexpectedFieldErr = fmt.Errorf("unexpected field")
|
InvalidParameterErr = fmt.Errorf("invalid parameters")
|
||||||
InvalidParameterErr = fmt.Errorf("invalid parameters")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Dialer struct {
|
type Dialer struct {
|
||||||
*GlobalOption
|
*GlobalOption
|
||||||
instanceOption InstanceOption
|
instanceOption InstanceOption
|
||||||
proxy.Dialer
|
proxy.Dialer
|
||||||
supportUDP bool
|
name string
|
||||||
name string
|
protocol string
|
||||||
protocol string
|
link string
|
||||||
link string
|
|
||||||
|
|
||||||
Latencies10 *LatenciesN
|
tcp4Latencies10 *LatenciesN
|
||||||
|
tcp6Latencies10 *LatenciesN
|
||||||
|
udp4Latencies10 *LatenciesN
|
||||||
|
udp6Latencies10 *LatenciesN
|
||||||
aliveDialerSetSetMu sync.Mutex
|
aliveDialerSetSetMu sync.Mutex
|
||||||
// aliveDialerSetSet uses reference counting.
|
// aliveDialerSetSet uses reference counting.
|
||||||
aliveDialerSetSet map[*AliveDialerSet]int
|
tcp4AliveDialerSetSet AliveDialerSetSet
|
||||||
|
tcp6AliveDialerSetSet AliveDialerSetSet
|
||||||
|
udp4AliveDialerSetSet AliveDialerSetSet
|
||||||
|
udp6AliveDialerSetSet AliveDialerSetSet
|
||||||
|
|
||||||
tickerMu sync.Mutex
|
tickerMu sync.Mutex
|
||||||
ticker *time.Ticker
|
ticker *time.Ticker
|
||||||
}
|
}
|
||||||
|
|
||||||
type GlobalOption struct {
|
type GlobalOption struct {
|
||||||
Log *logrus.Logger
|
Log *logrus.Logger
|
||||||
CheckUrl string
|
TcpCheckOption *TcpCheckOption
|
||||||
CheckInterval time.Duration
|
UdpCheckOption *UdpCheckOption
|
||||||
|
CheckInterval time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
type InstanceOption struct {
|
type InstanceOption struct {
|
||||||
Check bool
|
CheckEnabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AliveDialerSetSet map[*AliveDialerSet]int
|
||||||
|
|
||||||
// NewDialer is for register in general.
|
// 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{
|
d := &Dialer{
|
||||||
Dialer: dialer,
|
Dialer: dialer,
|
||||||
GlobalOption: option,
|
GlobalOption: option,
|
||||||
instanceOption: iOption,
|
instanceOption: iOption,
|
||||||
supportUDP: supportUDP,
|
name: name,
|
||||||
name: name,
|
protocol: protocol,
|
||||||
protocol: protocol,
|
link: link,
|
||||||
link: link,
|
tcp4Latencies10: NewLatenciesN(10),
|
||||||
Latencies10: NewLatenciesN(10),
|
tcp6Latencies10: NewLatenciesN(10),
|
||||||
|
udp4Latencies10: NewLatenciesN(10),
|
||||||
|
udp6Latencies10: NewLatenciesN(10),
|
||||||
// Set a very big cycle to wait for init.
|
// Set a very big cycle to wait for init.
|
||||||
ticker: time.NewTicker(time.Hour),
|
ticker: time.NewTicker(time.Hour),
|
||||||
aliveDialerSetSet: make(map[*AliveDialerSet]int),
|
tcp4AliveDialerSetSet: make(AliveDialerSetSet),
|
||||||
|
tcp6AliveDialerSetSet: make(AliveDialerSetSet),
|
||||||
|
udp4AliveDialerSetSet: make(AliveDialerSetSet),
|
||||||
|
udp6AliveDialerSetSet: make(AliveDialerSetSet),
|
||||||
}
|
}
|
||||||
if iOption.Check {
|
if iOption.CheckEnabled {
|
||||||
go d.aliveBackground()
|
go d.aliveBackground()
|
||||||
}
|
}
|
||||||
return d
|
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 {
|
func (d *Dialer) Close() error {
|
||||||
d.tickerMu.Lock()
|
d.tickerMu.Lock()
|
||||||
@ -106,27 +82,6 @@ func (d *Dialer) Close() error {
|
|||||||
return nil
|
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 {
|
func (d *Dialer) Name() string {
|
||||||
return d.name
|
return d.name
|
||||||
}
|
}
|
||||||
@ -138,60 +93,3 @@ func (d *Dialer) Protocol() string {
|
|||||||
func (d *Dialer) Link() string {
|
func (d *Dialer) Link() string {
|
||||||
return d.link
|
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 {
|
func NewDirectDialer(option *GlobalOption, fullcone bool) *Dialer {
|
||||||
if fullcone {
|
if fullcone {
|
||||||
return NewDialer(FullconeDirect, option, InstanceOption{Check: false}, true, "direct", "direct", "")
|
return NewDialer(FullconeDirect, option, InstanceOption{CheckEnabled: false}, "direct", "direct", "")
|
||||||
} else {
|
} 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 {
|
func newDirect(fullCone bool) proxy.Dialer {
|
||||||
return &direct{
|
return &direct{
|
||||||
netDialer: &net.Dialer{},
|
netDialer: &net.Dialer{},
|
||||||
fullCone: fullCone,
|
fullCone: fullCone,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,7 +67,7 @@ func (s *HTTP) Dialer(option *dialer.GlobalOption, iOption dialer.InstanceOption
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
func (s *HTTP) URL() url.URL {
|
||||||
|
@ -48,7 +48,6 @@ func (s *Shadowsocks) Dialer(option *dialer.GlobalOption, iOption dialer.Instanc
|
|||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported shadowsocks encryption method: %v", s.Cipher)
|
return nil, fmt.Errorf("unsupported shadowsocks encryption method: %v", s.Cipher)
|
||||||
}
|
}
|
||||||
supportUDP := s.UDP
|
|
||||||
d := dialer.FullconeDirect // Shadowsocks Proxy supports full-cone.
|
d := dialer.FullconeDirect // Shadowsocks Proxy supports full-cone.
|
||||||
d, err := protocol.NewDialer("shadowsocks", d, protocol.Header{
|
d, err := protocol.NewDialer("shadowsocks", d, protocol.Header{
|
||||||
ProxyAddress: net.JoinHostPort(s.Server, strconv.Itoa(s.Port)),
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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":
|
//case "socks4", "socks4a":
|
||||||
// d, err := socks4.NewSocks4Dialer(link, &proxy.Direct{})
|
// d, err := socks4.NewSocks4Dialer(link, &proxy.Direct{})
|
||||||
// if err != nil {
|
// if err != nil {
|
||||||
|
@ -101,7 +101,7 @@ func (s *Trojan) Dialer(option *dialer.GlobalOption, iOption dialer.InstanceOpti
|
|||||||
}); err != nil {
|
}); err != nil {
|
||||||
return nil, err
|
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) {
|
func ParseTrojanURL(u string) (data *Trojan, err error) {
|
||||||
|
@ -147,7 +147,7 @@ func (s *V2Ray) Dialer(option *dialer.GlobalOption, iOption dialer.InstanceOptio
|
|||||||
}); err != nil {
|
}); err != nil {
|
||||||
return nil, err
|
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) {
|
func ParseVlessURL(vless string) (data *V2Ray, err error) {
|
||||||
|
@ -12,6 +12,8 @@ import (
|
|||||||
"github.com/v2rayA/dae/component/outbound/dialer"
|
"github.com/v2rayA/dae/component/outbound/dialer"
|
||||||
"golang.org/x/net/proxy"
|
"golang.org/x/net/proxy"
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DialerGroup struct {
|
type DialerGroup struct {
|
||||||
@ -24,7 +26,10 @@ type DialerGroup struct {
|
|||||||
Dialers []*dialer.Dialer
|
Dialers []*dialer.Dialer
|
||||||
|
|
||||||
registeredAliveDialerSet bool
|
registeredAliveDialerSet bool
|
||||||
AliveDialerSet *dialer.AliveDialerSet
|
AliveTcp4DialerSet *dialer.AliveDialerSet
|
||||||
|
AliveTcp6DialerSet *dialer.AliveDialerSet
|
||||||
|
AliveUdp4DialerSet *dialer.AliveDialerSet
|
||||||
|
AliveUdp6DialerSet *dialer.AliveDialerSet
|
||||||
|
|
||||||
selectionPolicy *DialerSelectionPolicy
|
selectionPolicy *DialerSelectionPolicy
|
||||||
}
|
}
|
||||||
@ -32,7 +37,10 @@ type DialerGroup struct {
|
|||||||
func NewDialerGroup(option *dialer.GlobalOption, name string, dialers []*dialer.Dialer, p DialerSelectionPolicy) *DialerGroup {
|
func NewDialerGroup(option *dialer.GlobalOption, name string, dialers []*dialer.Dialer, p DialerSelectionPolicy) *DialerGroup {
|
||||||
log := option.Log
|
log := option.Log
|
||||||
var registeredAliveDialerSet bool
|
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 {
|
switch p.Policy {
|
||||||
case consts.DialerSelectionPolicy_Random,
|
case consts.DialerSelectionPolicy_Random,
|
||||||
@ -40,7 +48,10 @@ func NewDialerGroup(option *dialer.GlobalOption, name string, dialers []*dialer.
|
|||||||
consts.DialerSelectionPolicy_MinAverage10Latencies:
|
consts.DialerSelectionPolicy_MinAverage10Latencies:
|
||||||
// Need to know the alive state or latency.
|
// Need to know the alive state or latency.
|
||||||
for _, d := range dialers {
|
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
|
registeredAliveDialerSet = true
|
||||||
|
|
||||||
@ -56,7 +67,10 @@ func NewDialerGroup(option *dialer.GlobalOption, name string, dialers []*dialer.
|
|||||||
Name: name,
|
Name: name,
|
||||||
Dialers: dialers,
|
Dialers: dialers,
|
||||||
block: dialer.NewBlockDialer(option),
|
block: dialer.NewBlockDialer(option),
|
||||||
AliveDialerSet: a,
|
AliveTcp4DialerSet: aliveTcp4DialerSet,
|
||||||
|
AliveTcp6DialerSet: aliveTcp6DialerSet,
|
||||||
|
AliveUdp4DialerSet: aliveUdp4DialerSet,
|
||||||
|
AliveUdp6DialerSet: aliveUdp6DialerSet,
|
||||||
registeredAliveDialerSet: registeredAliveDialerSet,
|
registeredAliveDialerSet: registeredAliveDialerSet,
|
||||||
selectionPolicy: &p,
|
selectionPolicy: &p,
|
||||||
}
|
}
|
||||||
@ -65,7 +79,10 @@ func NewDialerGroup(option *dialer.GlobalOption, name string, dialers []*dialer.
|
|||||||
func (g *DialerGroup) Close() error {
|
func (g *DialerGroup) Close() error {
|
||||||
if g.registeredAliveDialerSet {
|
if g.registeredAliveDialerSet {
|
||||||
for _, d := range g.Dialers {
|
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
|
return nil
|
||||||
@ -77,17 +94,39 @@ func (g *DialerGroup) SetSelectionPolicy(policy DialerSelectionPolicy) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Select selects a dialer from group according to selectionPolicy.
|
// 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 {
|
if len(g.Dialers) == 0 {
|
||||||
return nil, fmt.Errorf("no dialer in this group")
|
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 {
|
switch g.selectionPolicy.Policy {
|
||||||
case consts.DialerSelectionPolicy_Random:
|
case consts.DialerSelectionPolicy_Random:
|
||||||
d := g.AliveDialerSet.GetRand()
|
d := a.GetRand()
|
||||||
if d == nil {
|
if d == nil {
|
||||||
// No alive dialer.
|
// 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 g.block, nil
|
||||||
}
|
}
|
||||||
return d, nil
|
return d, nil
|
||||||
@ -99,10 +138,13 @@ func (g *DialerGroup) Select() (*dialer.Dialer, error) {
|
|||||||
return g.Dialers[g.selectionPolicy.FixedIndex], nil
|
return g.Dialers[g.selectionPolicy.FixedIndex], nil
|
||||||
|
|
||||||
case consts.DialerSelectionPolicy_MinLastLatency, consts.DialerSelectionPolicy_MinAverage10Latencies:
|
case consts.DialerSelectionPolicy_MinLastLatency, consts.DialerSelectionPolicy_MinAverage10Latencies:
|
||||||
d := g.AliveDialerSet.GetMinLatency()
|
d := a.GetMinLatency()
|
||||||
if d == nil {
|
if d == nil {
|
||||||
// No alive dialer.
|
// 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 g.block, nil
|
||||||
}
|
}
|
||||||
return d, 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) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
package outbound
|
package outbound
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"github.com/mzz2017/softwind/pkg/fastrand"
|
"github.com/mzz2017/softwind/pkg/fastrand"
|
||||||
"github.com/v2rayA/dae/common/consts"
|
"github.com/v2rayA/dae/common/consts"
|
||||||
"github.com/v2rayA/dae/component/outbound/dialer"
|
"github.com/v2rayA/dae/component/outbound/dialer"
|
||||||
@ -15,14 +16,25 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
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) {
|
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{
|
option := &dialer.GlobalOption{
|
||||||
Log: log,
|
Log: log,
|
||||||
CheckUrl: testCheckUrl,
|
TcpCheckOption: topt,
|
||||||
|
UdpCheckOption: uopt,
|
||||||
|
CheckInterval: 15 * time.Second,
|
||||||
}
|
}
|
||||||
dialers := []*dialer.Dialer{
|
dialers := []*dialer.Dialer{
|
||||||
dialer.NewDirectDialer(option, true),
|
dialer.NewDirectDialer(option, true),
|
||||||
@ -34,7 +46,7 @@ func TestDialerGroup_Select_Fixed(t *testing.T) {
|
|||||||
FixedIndex: fixedIndex,
|
FixedIndex: fixedIndex,
|
||||||
})
|
})
|
||||||
for i := 0; i < 10; i++ {
|
for i := 0; i < 10; i++ {
|
||||||
d, err := g.Select()
|
d, err := g.Select(consts.L4ProtoStr_TCP, consts.IpVersionStr_4)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -46,7 +58,7 @@ func TestDialerGroup_Select_Fixed(t *testing.T) {
|
|||||||
fixedIndex = 0
|
fixedIndex = 0
|
||||||
g.selectionPolicy.FixedIndex = fixedIndex
|
g.selectionPolicy.FixedIndex = fixedIndex
|
||||||
for i := 0; i < 10; i++ {
|
for i := 0; i < 10; i++ {
|
||||||
d, err := g.Select()
|
d, err := g.Select(consts.L4ProtoStr_TCP, consts.IpVersionStr_4)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -57,10 +69,20 @@ func TestDialerGroup_Select_Fixed(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDialerGroup_Select_MinLastLatency(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{
|
option := &dialer.GlobalOption{
|
||||||
Log: log,
|
Log: log,
|
||||||
CheckUrl: testCheckUrl,
|
TcpCheckOption: topt,
|
||||||
|
UdpCheckOption: uopt,
|
||||||
|
CheckInterval: 15 * time.Second,
|
||||||
}
|
}
|
||||||
dialers := []*dialer.Dialer{
|
dialers := []*dialer.Dialer{
|
||||||
dialer.NewDirectDialer(option, false),
|
dialer.NewDirectDialer(option, false),
|
||||||
@ -98,14 +120,14 @@ func TestDialerGroup_Select_MinLastLatency(t *testing.T) {
|
|||||||
latency = time.Duration(fastrand.Int63n(int64(1000 * time.Millisecond)))
|
latency = time.Duration(fastrand.Int63n(int64(1000 * time.Millisecond)))
|
||||||
alive = true
|
alive = true
|
||||||
}
|
}
|
||||||
d.Latencies10.AppendLatency(latency)
|
d.MustGetLatencies10(consts.L4ProtoStr_TCP, consts.IpVersionStr_4).AppendLatency(latency)
|
||||||
if jMinLatency == -1 || latency < minLatency {
|
if jMinLatency == -1 || latency < minLatency {
|
||||||
jMinLatency = j
|
jMinLatency = j
|
||||||
minLatency = latency
|
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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -124,10 +146,20 @@ func TestDialerGroup_Select_MinLastLatency(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDialerGroup_Select_Random(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{
|
option := &dialer.GlobalOption{
|
||||||
Log: log,
|
Log: log,
|
||||||
CheckUrl: testCheckUrl,
|
TcpCheckOption: topt,
|
||||||
|
UdpCheckOption: uopt,
|
||||||
|
CheckInterval: 15 * time.Second,
|
||||||
}
|
}
|
||||||
dialers := []*dialer.Dialer{
|
dialers := []*dialer.Dialer{
|
||||||
dialer.NewDirectDialer(option, false),
|
dialer.NewDirectDialer(option, false),
|
||||||
@ -141,7 +173,7 @@ func TestDialerGroup_Select_Random(t *testing.T) {
|
|||||||
})
|
})
|
||||||
count := make([]int, len(dialers))
|
count := make([]int, len(dialers))
|
||||||
for i := 0; i < 100; i++ {
|
for i := 0; i < 100; i++ {
|
||||||
d, err := g.Select()
|
d, err := g.Select(consts.L4ProtoStr_TCP, consts.IpVersionStr_4)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -161,10 +193,20 @@ func TestDialerGroup_Select_Random(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDialerGroup_SetAlive(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{
|
option := &dialer.GlobalOption{
|
||||||
Log: log,
|
Log: log,
|
||||||
CheckUrl: testCheckUrl,
|
TcpCheckOption: topt,
|
||||||
|
UdpCheckOption: uopt,
|
||||||
|
CheckInterval: 15 * time.Second,
|
||||||
}
|
}
|
||||||
dialers := []*dialer.Dialer{
|
dialers := []*dialer.Dialer{
|
||||||
dialer.NewDirectDialer(option, false),
|
dialer.NewDirectDialer(option, false),
|
||||||
@ -177,10 +219,10 @@ func TestDialerGroup_SetAlive(t *testing.T) {
|
|||||||
Policy: consts.DialerSelectionPolicy_Random,
|
Policy: consts.DialerSelectionPolicy_Random,
|
||||||
})
|
})
|
||||||
zeroTarget := 3
|
zeroTarget := 3
|
||||||
g.AliveDialerSet.SetAlive(dialers[zeroTarget], false)
|
g.AliveTcp4DialerSet.SetAlive(dialers[zeroTarget], false)
|
||||||
count := make([]int, len(dialers))
|
count := make([]int, len(dialers))
|
||||||
for i := 0; i < 100; i++ {
|
for i := 0; i < 100; i++ {
|
||||||
d, err := g.Select()
|
d, err := g.Select(consts.L4ProtoStr_UDP, consts.IpVersionStr_4)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ type DialerSet struct {
|
|||||||
func NewDialerSetFromLinks(option *dialer.GlobalOption, nodes []string) *DialerSet {
|
func NewDialerSetFromLinks(option *dialer.GlobalOption, nodes []string) *DialerSet {
|
||||||
s := &DialerSet{Dialers: make([]*dialer.Dialer, 0, len(nodes))}
|
s := &DialerSet{Dialers: make([]*dialer.Dialer, 0, len(nodes))}
|
||||||
for _, node := range 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 {
|
if err != nil {
|
||||||
option.Log.Infof("failed to parse node: %v: %v", node, err)
|
option.Log.Infof("failed to parse node: %v: %v", node, err)
|
||||||
continue
|
continue
|
||||||
@ -40,7 +40,7 @@ func NewDialerSetFromLinks(option *dialer.GlobalOption, nodes []string) *DialerS
|
|||||||
return s
|
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
|
// Example
|
||||||
// filter: name(regex:'^.*hk.*$', keyword:'sg') && name(keyword:'disney')
|
// filter: name(regex:'^.*hk.*$', keyword:'sg') && name(keyword:'disney')
|
||||||
// filter: !name(regex: 'HK|TW|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) {
|
func (s *DialerSet) Filter(filters []*config_parser.Function) (dialers []*dialer.Dialer, err error) {
|
||||||
for _, d := range s.Dialers {
|
for _, d := range s.Dialers {
|
||||||
hit, err := hit(d, filters)
|
hit, err := filterHit(d, filters)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -7,26 +7,21 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/v2rayA/dae/common"
|
||||||
"github.com/v2rayA/dae/pkg/config_parser"
|
"github.com/v2rayA/dae/pkg/config_parser"
|
||||||
"net/url"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UrlOrEmpty struct {
|
|
||||||
Url *url.URL
|
|
||||||
Empty bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type Global struct {
|
type Global struct {
|
||||||
TproxyPort uint16 `mapstructure:"tproxy_port" default:"12345"`
|
TproxyPort uint16 `mapstructure:"tproxy_port" default:"12345"`
|
||||||
LogLevel string `mapstructure:"log_level" default:"info"`
|
LogLevel string `mapstructure:"log_level" default:"info"`
|
||||||
TcpCheckUrl string `mapstructure:"tcp_check_url" default:"https://connectivitycheck.gstatic.com/generate_204"`
|
TcpCheckUrl string `mapstructure:"tcp_check_url" default:"http://cp.cloudflare.com"`
|
||||||
UdpCheckDns string `mapstructure:"udp_check_dns" default:"8.8.8.8:53"`
|
UdpCheckDns string `mapstructure:"udp_check_dns" default:"cloudflare-dns.com:53"`
|
||||||
CheckInterval time.Duration `mapstructure:"check_interval" default:"15s"`
|
CheckInterval time.Duration `mapstructure:"check_interval" default:"30s"`
|
||||||
DnsUpstream UrlOrEmpty `mapstructure:"dns_upstream" require:""`
|
DnsUpstream common.UrlOrEmpty `mapstructure:"dns_upstream" require:""`
|
||||||
LanInterface []string `mapstructure:"lan_interface"`
|
LanInterface []string `mapstructure:"lan_interface"`
|
||||||
WanInterface []string `mapstructure:"wan_interface"`
|
WanInterface []string `mapstructure:"wan_interface"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Group struct {
|
type Group struct {
|
||||||
|
@ -29,6 +29,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ControlPlane struct {
|
type ControlPlane struct {
|
||||||
@ -186,10 +187,21 @@ func NewControlPlane(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// DialerGroups (outbounds).
|
/// 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{
|
option := &dialer.GlobalOption{
|
||||||
Log: log,
|
Log: log,
|
||||||
CheckUrl: global.TcpCheckUrl,
|
TcpCheckOption: tcpCheckOption,
|
||||||
CheckInterval: global.CheckInterval,
|
UdpCheckOption: udpCheckOption,
|
||||||
|
CheckInterval: global.CheckInterval,
|
||||||
}
|
}
|
||||||
outbounds := []*outbound.DialerGroup{
|
outbounds := []*outbound.DialerGroup{
|
||||||
outbound.NewDialerGroup(option, consts.OutboundDirect.String(),
|
outbound.NewDialerGroup(option, consts.OutboundDirect.String(),
|
||||||
@ -214,7 +226,7 @@ func NewControlPlane(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create group %v: %w", group.Name, err)
|
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)
|
dialers, err := dialerSet.Filter(group.Param.Filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf(`failed to create group "%v": %w`, group.Name, err)
|
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)
|
log.Infof(`Group "%v" node list:`, group.Name)
|
||||||
for _, d := range dialers {
|
for _, d := range dialers {
|
||||||
log.Infoln("\t" + d.Name())
|
log.Infoln("\t" + d.Name())
|
||||||
d.ActiveCheck()
|
// We only activate check of nodes that have a group.
|
||||||
|
d.ActivateCheck()
|
||||||
}
|
}
|
||||||
if len(dialers) == 0 {
|
if len(dialers) == 0 {
|
||||||
log.Infoln("\t<Empty>")
|
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) {
|
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)
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to select dialer from group %v: %w", outbound.Name, err)
|
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.
|
// 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).
|
// So se should trust home devices even if they make rush-answer (or looks like).
|
||||||
validateRushAns := outboundIndex == consts.OutboundDirect && !destToSend.Addr().IsPrivate()
|
validateRushAns := outboundIndex == consts.OutboundDirect && !destToSend.Addr().IsPrivate()
|
||||||
|
|
||||||
|
// Get udp endpoint.
|
||||||
ue, err := DefaultUdpEndpointPool.GetOrCreate(src, &UdpEndpointOptions{
|
ue, err := DefaultUdpEndpointPool.GetOrCreate(src, &UdpEndpointOptions{
|
||||||
Handler: c.RelayToUDP(src, isDns, dummyFrom, validateRushAns),
|
Handler: c.RelayToUDP(src, isDns, dummyFrom, validateRushAns),
|
||||||
NatTimeout: natTimeout,
|
NatTimeout: natTimeout,
|
||||||
DialerFunc: func() (*dialer.Dialer, error) {
|
DialerFunc: func() (*dialer.Dialer, error) {
|
||||||
newDialer, err := outbound.Select()
|
newDialer, err := outbound.Select(consts.L4ProtoStr_UDP, consts.IpVersionFromAddr(dst.Addr()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to select dialer from group %v: %w", outbound.Name, err)
|
return nil, fmt.Errorf("failed to select dialer from group %v: %w", outbound.Name, err)
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,9 @@ global {
|
|||||||
log_level: info
|
log_level: info
|
||||||
|
|
||||||
# Node connectivity check.
|
# 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
|
check_interval: 30s
|
||||||
|
|
||||||
# Now only support udp://IP:Port. Empty value '' indicates as-is.
|
# 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/gorilla/websocket v1.5.0
|
||||||
github.com/json-iterator/go v1.1.12
|
github.com/json-iterator/go v1.1.12
|
||||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
|
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/safchain/ethtool v0.2.0
|
||||||
github.com/sirupsen/logrus v1.9.0
|
github.com/sirupsen/logrus v1.9.0
|
||||||
github.com/spf13/cobra v1.6.1
|
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/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 h1:rEF9MiXd9qMW3ibRpqcerLXULoTgRlM21yqqJl1B90M=
|
||||||
github.com/mzz2017/disk-bloom v1.0.1/go.mod h1:JLHETtUu44Z6iBmsqzkOtFlRvXSlKnxjwiBRDapizDI=
|
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-20230208101341-471784899114 h1:7VK3nkOhmzcJrRtBGocZGXi4/9CDyqDDp4ezGIvk5BM=
|
||||||
github.com/mzz2017/softwind v0.0.0-20230127172609-05c5264aa6a4/go.mod h1:K1nXwtBokwEsfOfdT/5zV6R8QabGkyhcR0iuTrRZcYY=
|
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/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.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||||
|
Reference in New Issue
Block a user