diff --git a/component/outbound/dialer/alive_dialer_set.go b/component/outbound/dialer/alive_dialer_set.go index b809259..a98a780 100644 --- a/component/outbound/dialer/alive_dialer_set.go +++ b/component/outbound/dialer/alive_dialer_set.go @@ -26,6 +26,7 @@ type AliveDialerSet struct { dialerGroupName string l4proto consts.L4ProtoStr ipversion consts.IpVersionStr + tolerance time.Duration aliveChangeCallback func(alive bool) @@ -43,6 +44,7 @@ func NewAliveDialerSet( dialerGroupName string, l4proto consts.L4ProtoStr, ipversion consts.IpVersionStr, + tolerance time.Duration, selectionPolicy consts.DialerSelectionPolicy, dialers []*Dialer, aliveChangeCallback func(alive bool), @@ -53,6 +55,7 @@ func NewAliveDialerSet( dialerGroupName: dialerGroupName, l4proto: l4proto, ipversion: ipversion, + tolerance: tolerance, aliveChangeCallback: aliveChangeCallback, dialerToIndex: make(map[*Dialer]int), dialerToLatency: make(map[*Dialer]time.Duration), @@ -146,14 +149,19 @@ func (a *AliveDialerSet) NotifyLatencyChange(dialer *Dialer, alive bool) { bakOldBestDialer := a.minLatency.dialer // Calc minLatency. a.dialerToLatency[dialer] = latency - if alive && latency < a.minLatency.latency { + if alive && latency <= a.minLatency.latency-a.tolerance { a.minLatency.latency = latency a.minLatency.dialer = dialer } else if a.minLatency.dialer == dialer { - a.minLatency.latency = time.Hour - a.minLatency.dialer = nil - a.calcMinLatency() - // Now `a.minLatency.dialer` will be nil if there is no alive dialer. + if latency > a.minLatency.latency { + // Latency increases. + a.minLatency.latency = time.Hour + a.minLatency.dialer = nil + a.calcMinLatency() + // Now `a.minLatency.dialer` will be nil if there is no alive dialer. + } else { + a.minLatency.latency = latency + } } currentAlive := a.minLatency.dialer != nil // If best dialer changed. @@ -169,7 +177,8 @@ func (a *AliveDialerSet) NotifyLatencyChange(dialer *Dialer, alive bool) { string(a.selectionPolicy): a.minLatency.latency, "group": a.dialerGroupName, "network": string(a.l4proto) + string(a.ipversion), - "dialer": a.minLatency.dialer.Name(), + "new dialer": a.minLatency.dialer.Name(), + "old dialer": bakOldBestDialer.Name(), }).Infof("Group %vselects dialer", re) } else { // Alive -> not alive @@ -199,7 +208,7 @@ func (a *AliveDialerSet) calcMinLatency() { if !ok { continue } - if latency < a.minLatency.latency { + if latency <= a.minLatency.latency-a.tolerance { a.minLatency.latency = latency a.minLatency.dialer = d } diff --git a/component/outbound/dialer/dialer.go b/component/outbound/dialer/dialer.go index c502594..38e7763 100644 --- a/component/outbound/dialer/dialer.go +++ b/component/outbound/dialer/dialer.go @@ -37,6 +37,7 @@ type GlobalOption struct { TcpCheckOptionRaw TcpCheckOptionRaw // Lazy parse UdpCheckOptionRaw UdpCheckOptionRaw // Lazy parse CheckInterval time.Duration + CheckTolerance time.Duration } type InstanceOption struct { diff --git a/component/outbound/dialer_group.go b/component/outbound/dialer_group.go index bb8e312..179c50e 100644 --- a/component/outbound/dialer_group.go +++ b/component/outbound/dialer_group.go @@ -39,10 +39,10 @@ type DialerGroup struct { func NewDialerGroup(option *dialer.GlobalOption, name string, dialers []*dialer.Dialer, p DialerSelectionPolicy, aliveChangeCallback func(alive bool, l4proto uint8, ipversion uint8)) *DialerGroup { log := option.Log var registeredAliveDialerSet bool - aliveTcp4DialerSet := dialer.NewAliveDialerSet(log, name, consts.L4ProtoStr_TCP, consts.IpVersionStr_4, p.Policy, dialers, func(alive bool) { aliveChangeCallback(alive, unix.IPPROTO_TCP, 4) }, true) - aliveTcp6DialerSet := dialer.NewAliveDialerSet(log, name, consts.L4ProtoStr_TCP, consts.IpVersionStr_6, p.Policy, dialers, func(alive bool) { aliveChangeCallback(alive, unix.IPPROTO_TCP, 6) }, true) - aliveUdp4DialerSet := dialer.NewAliveDialerSet(log, name, consts.L4ProtoStr_UDP, consts.IpVersionStr_4, p.Policy, dialers, func(alive bool) { aliveChangeCallback(alive, unix.IPPROTO_UDP, 4) }, true) - aliveUdp6DialerSet := dialer.NewAliveDialerSet(log, name, consts.L4ProtoStr_UDP, consts.IpVersionStr_6, p.Policy, dialers, func(alive bool) { aliveChangeCallback(alive, unix.IPPROTO_UDP, 6) }, true) + aliveTcp4DialerSet := dialer.NewAliveDialerSet(log, name, consts.L4ProtoStr_TCP, consts.IpVersionStr_4, option.CheckTolerance, p.Policy, dialers, func(alive bool) { aliveChangeCallback(alive, unix.IPPROTO_TCP, 4) }, true) + aliveTcp6DialerSet := dialer.NewAliveDialerSet(log, name, consts.L4ProtoStr_TCP, consts.IpVersionStr_6, option.CheckTolerance, p.Policy, dialers, func(alive bool) { aliveChangeCallback(alive, unix.IPPROTO_TCP, 6) }, true) + aliveUdp4DialerSet := dialer.NewAliveDialerSet(log, name, consts.L4ProtoStr_UDP, consts.IpVersionStr_4, option.CheckTolerance, p.Policy, dialers, func(alive bool) { aliveChangeCallback(alive, unix.IPPROTO_UDP, 4) }, true) + aliveUdp6DialerSet := dialer.NewAliveDialerSet(log, name, consts.L4ProtoStr_UDP, consts.IpVersionStr_6, option.CheckTolerance, p.Policy, dialers, func(alive bool) { aliveChangeCallback(alive, unix.IPPROTO_UDP, 6) }, true) switch p.Policy { case consts.DialerSelectionPolicy_Random, diff --git a/config/config.go b/config/config.go index 52cee23..59ecfa0 100644 --- a/config/config.go +++ b/config/config.go @@ -14,14 +14,15 @@ import ( ) type Global struct { - 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"` + 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"` + CheckTolerance time.Duration `mapstructure:"check_tolerance" default:"0"` + DnsUpstream common.UrlOrEmpty `mapstructure:"dns_upstream" require:""` + LanInterface []string `mapstructure:"lan_interface"` + WanInterface []string `mapstructure:"wan_interface"` } type Group struct { diff --git a/control/control_plane.go b/control/control_plane.go index 2936284..82fd0fb 100644 --- a/control/control_plane.go +++ b/control/control_plane.go @@ -196,6 +196,7 @@ func NewControlPlane( TcpCheckOptionRaw: dialer.TcpCheckOptionRaw{Raw: global.TcpCheckUrl}, UdpCheckOptionRaw: dialer.UdpCheckOptionRaw{Raw: global.UdpCheckDns}, CheckInterval: global.CheckInterval, + CheckTolerance: global.CheckTolerance, } outbounds := []*outbound.DialerGroup{ outbound.NewDialerGroup(option, consts.OutboundDirect.String(), diff --git a/example.dae b/example.dae index 3c45f7c..3fa2361 100644 --- a/example.dae +++ b/example.dae @@ -10,6 +10,8 @@ global { tcp_check_url: 'http://cp.cloudflare.com' udp_check_dns: 'dns.google:53' check_interval: 30s + # Group will switch node only when new_latency <= old_latency - tolerance + check_tolerance: 50ms # Value can be scheme://host:port or empty string ''. # The scheme can be tcp/udp/tcp+udp. Empty string '' indicates as-is.