mirror of
https://github.com/SagerNet/sing-box.git
synced 2025-06-13 21:54:13 +08:00
add outbound 'leastping' ...
* extract outbound.Balancer * options refactor
This commit is contained in:
parent
1c0ad66fc8
commit
ebe5a80f19
6
balancer/balancer.go
Normal file
6
balancer/balancer.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package balancer
|
||||||
|
|
||||||
|
// Balancer is interface for load balancers
|
||||||
|
type Balancer interface {
|
||||||
|
Select() *Node
|
||||||
|
}
|
@ -71,22 +71,24 @@ func NewHealthCheck(outbounds []*Node, logger log.Logger, config *option.HealthC
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start starts the health check service
|
// Start starts the health check service
|
||||||
func (h *HealthCheck) Start() {
|
func (h *HealthCheck) Start() error {
|
||||||
if h.ticker != nil {
|
if h.ticker != nil {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
interval := h.Settings.Interval * time.Duration(h.Settings.SamplingCount)
|
interval := h.Settings.Interval * time.Duration(h.Settings.SamplingCount)
|
||||||
ticker := time.NewTicker(interval)
|
ticker := time.NewTicker(interval)
|
||||||
h.ticker = ticker
|
h.ticker = ticker
|
||||||
go func() {
|
go func() {
|
||||||
|
h.doCheck(0, 1)
|
||||||
for {
|
for {
|
||||||
h.doCheck(interval, h.Settings.SamplingCount)
|
|
||||||
_, ok := <-ticker.C
|
_, ok := <-ticker.C
|
||||||
if !ok {
|
if !ok {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
h.doCheck(interval, h.Settings.SamplingCount)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop stops the health check service
|
// Stop stops the health check service
|
||||||
@ -105,7 +107,7 @@ func (h *HealthCheck) Check() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type rtt struct {
|
type rtt struct {
|
||||||
handler string
|
tag string
|
||||||
value time.Duration
|
value time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,7 +137,7 @@ func (h *HealthCheck) doCheck(duration time.Duration, rounds int) {
|
|||||||
delay, err := client.MeasureDelay()
|
delay, err := client.MeasureDelay()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
ch <- &rtt{
|
ch <- &rtt{
|
||||||
handler: tag,
|
tag: tag,
|
||||||
value: delay,
|
value: delay,
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@ -143,7 +145,7 @@ func (h *HealthCheck) doCheck(duration time.Duration, rounds int) {
|
|||||||
if !h.checkConnectivity() {
|
if !h.checkConnectivity() {
|
||||||
h.logger.Debug("network is down")
|
h.logger.Debug("network is down")
|
||||||
ch <- &rtt{
|
ch <- &rtt{
|
||||||
handler: tag,
|
tag: tag,
|
||||||
value: 0,
|
value: 0,
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@ -155,7 +157,7 @@ func (h *HealthCheck) doCheck(duration time.Duration, rounds int) {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
ch <- &rtt{
|
ch <- &rtt{
|
||||||
handler: tag,
|
tag: tag,
|
||||||
value: rttFailed,
|
value: rttFailed,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -164,8 +166,9 @@ func (h *HealthCheck) doCheck(duration time.Duration, rounds int) {
|
|||||||
for i := 0; i < count; i++ {
|
for i := 0; i < count; i++ {
|
||||||
rtt := <-ch
|
rtt := <-ch
|
||||||
if rtt.value > 0 {
|
if rtt.value > 0 {
|
||||||
|
// h.logger.Debug("ping ", rtt.tag, ":", rtt.value)
|
||||||
// should not put results when network is down
|
// should not put results when network is down
|
||||||
h.PutResult(rtt.handler, rtt.value)
|
h.PutResult(rtt.tag, rtt.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package balancer
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"math"
|
"math"
|
||||||
|
"math/rand"
|
||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -9,6 +10,8 @@ import (
|
|||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var _ Balancer = (*LeastLoad)(nil)
|
||||||
|
|
||||||
// LeastLoad is leastload balancer
|
// LeastLoad is leastload balancer
|
||||||
type LeastLoad struct {
|
type LeastLoad struct {
|
||||||
nodes []*Node
|
nodes []*Node
|
||||||
@ -22,11 +25,11 @@ type LeastLoad struct {
|
|||||||
func NewLeastLoad(
|
func NewLeastLoad(
|
||||||
nodes []*Node, logger log.ContextLogger,
|
nodes []*Node, logger log.ContextLogger,
|
||||||
options option.LeastLoadOutboundOptions,
|
options option.LeastLoadOutboundOptions,
|
||||||
) (*LeastLoad, error) {
|
) (Balancer, error) {
|
||||||
return &LeastLoad{
|
return &LeastLoad{
|
||||||
nodes: nodes,
|
nodes: nodes,
|
||||||
options: &options,
|
options: &options,
|
||||||
HealthCheck: NewHealthCheck(nodes, logger, options.HealthCheck),
|
HealthCheck: NewHealthCheck(nodes, logger, &options.HealthCheck),
|
||||||
costs: NewWeightManager(
|
costs: NewWeightManager(
|
||||||
logger, options.Costs, 1,
|
logger, options.Costs, 1,
|
||||||
func(value, cost float64) float64 {
|
func(value, cost float64) float64 {
|
||||||
@ -37,9 +40,14 @@ func NewLeastLoad(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Select selects qualified nodes
|
// Select selects qualified nodes
|
||||||
func (s *LeastLoad) Select() []*Node {
|
func (s *LeastLoad) Select() *Node {
|
||||||
qualified, _ := s.getNodes()
|
qualified, _ := s.getNodes()
|
||||||
return s.selectLeastLoad(qualified)
|
selects := s.selectLeastLoad(qualified)
|
||||||
|
count := len(selects)
|
||||||
|
if count == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return selects[rand.Intn(count)]
|
||||||
}
|
}
|
||||||
|
|
||||||
// selectLeastLoad selects nodes according to Baselines and Expected Count.
|
// selectLeastLoad selects nodes according to Baselines and Expected Count.
|
||||||
@ -150,9 +158,6 @@ func leastloadSort(nodes []*Node) {
|
|||||||
if left.applied != right.applied {
|
if left.applied != right.applied {
|
||||||
return left.applied < right.applied
|
return left.applied < right.applied
|
||||||
}
|
}
|
||||||
if left.applied != right.applied {
|
|
||||||
return left.applied < right.applied
|
|
||||||
}
|
|
||||||
if left.Average != right.Average {
|
if left.Average != right.Average {
|
||||||
return left.Average < right.Average
|
return left.Average < right.Average
|
||||||
}
|
}
|
||||||
|
109
balancer/leastping.go
Normal file
109
balancer/leastping.go
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
package balancer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Balancer = (*LeastPing)(nil)
|
||||||
|
|
||||||
|
// LeastPing is least ping balancer
|
||||||
|
type LeastPing struct {
|
||||||
|
nodes []*Node
|
||||||
|
options *option.LeastPingOutboundOptions
|
||||||
|
|
||||||
|
*HealthCheck
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLeastPing creates a new LeastPing outbound
|
||||||
|
func NewLeastPing(
|
||||||
|
nodes []*Node, logger log.ContextLogger,
|
||||||
|
options option.LeastPingOutboundOptions,
|
||||||
|
) (Balancer, error) {
|
||||||
|
return &LeastPing{
|
||||||
|
nodes: nodes,
|
||||||
|
options: &options,
|
||||||
|
HealthCheck: NewHealthCheck(nodes, logger, &options.HealthCheck),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select selects least ping node
|
||||||
|
func (s *LeastPing) Select() *Node {
|
||||||
|
qualified, _ := s.getNodes()
|
||||||
|
if len(qualified) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return qualified[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LeastPing) getNodes() ([]*Node, []*Node) {
|
||||||
|
s.HealthCheck.Lock()
|
||||||
|
defer s.HealthCheck.Unlock()
|
||||||
|
|
||||||
|
qualified := make([]*Node, 0)
|
||||||
|
unqualified := make([]*Node, 0)
|
||||||
|
failed := make([]*Node, 0)
|
||||||
|
untested := make([]*Node, 0)
|
||||||
|
others := make([]*Node, 0)
|
||||||
|
for _, node := range s.nodes {
|
||||||
|
node.FetchStats(s.HealthCheck)
|
||||||
|
switch {
|
||||||
|
case node.All == 0:
|
||||||
|
node.applied = rttUntested
|
||||||
|
untested = append(untested, node)
|
||||||
|
case s.options.MaxRTT > 0 && node.Average > time.Duration(s.options.MaxRTT):
|
||||||
|
node.applied = rttUnqualified
|
||||||
|
unqualified = append(unqualified, node)
|
||||||
|
case float64(node.Fail)/float64(node.All) > float64(s.options.Tolerance):
|
||||||
|
node.applied = rttFailed
|
||||||
|
if node.All-node.Fail == 0 {
|
||||||
|
// no good, put them after has-good nodes
|
||||||
|
node.applied = rttFailed
|
||||||
|
node.Deviation = rttFailed
|
||||||
|
node.Average = rttFailed
|
||||||
|
}
|
||||||
|
failed = append(failed, node)
|
||||||
|
default:
|
||||||
|
node.applied = node.Average
|
||||||
|
qualified = append(qualified, node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(qualified) > 0 {
|
||||||
|
leastPingSort(qualified)
|
||||||
|
others = append(others, unqualified...)
|
||||||
|
others = append(others, untested...)
|
||||||
|
others = append(others, failed...)
|
||||||
|
} else {
|
||||||
|
// random node if not tested
|
||||||
|
shuffle(untested)
|
||||||
|
qualified = untested
|
||||||
|
others = append(others, unqualified...)
|
||||||
|
others = append(others, failed...)
|
||||||
|
}
|
||||||
|
return qualified, others
|
||||||
|
}
|
||||||
|
|
||||||
|
func leastPingSort(nodes []*Node) {
|
||||||
|
sort.Slice(nodes, func(i, j int) bool {
|
||||||
|
left := nodes[i]
|
||||||
|
right := nodes[j]
|
||||||
|
if left.applied != right.applied {
|
||||||
|
return left.applied < right.applied
|
||||||
|
}
|
||||||
|
if left.Fail != right.Fail {
|
||||||
|
return left.Fail < right.Fail
|
||||||
|
}
|
||||||
|
return left.All > right.All
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func shuffle(nodes []*Node) {
|
||||||
|
rand.Seed(time.Now().Unix())
|
||||||
|
rand.Shuffle(len(nodes), func(i, j int) {
|
||||||
|
nodes[i], nodes[j] = nodes[j], nodes[i]
|
||||||
|
})
|
||||||
|
}
|
@ -24,4 +24,5 @@ const (
|
|||||||
const (
|
const (
|
||||||
TypeSelector = "selector"
|
TypeSelector = "selector"
|
||||||
TypeLeastLoad = "leastload"
|
TypeLeastLoad = "leastload"
|
||||||
|
TypeLeastPing = "leastping"
|
||||||
)
|
)
|
||||||
|
@ -1,23 +1,39 @@
|
|||||||
package option
|
package option
|
||||||
|
|
||||||
// LeastLoadOutboundOptions is the options for leastload outbound
|
// BalancerOutboundOptions is the options for balancer outbound
|
||||||
type LeastLoadOutboundOptions struct {
|
type BalancerOutboundOptions struct {
|
||||||
Outbounds []string `json:"outbounds"`
|
Outbounds []string `json:"outbounds"`
|
||||||
Fallback string `json:"fallback,omitempty"`
|
Fallback string `json:"fallback,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HealthCheckOptions is the options for health check
|
||||||
|
type HealthCheckOptions struct {
|
||||||
// health check settings
|
// health check settings
|
||||||
HealthCheck *HealthCheckSettings `json:"healthCheck,omitempty"`
|
HealthCheck HealthCheckSettings `json:"healthCheck,omitempty"`
|
||||||
// cost settings
|
|
||||||
Costs []*StrategyWeight `json:"costs,omitempty"`
|
|
||||||
// ping rtt baselines (ms)
|
|
||||||
Baselines []Duration `json:"baselines,omitempty"`
|
|
||||||
// expected nodes count to select
|
|
||||||
Expected int32 `json:"expected,omitempty"`
|
|
||||||
// max acceptable rtt (ms), filter away high delay nodes. defalut 0
|
// max acceptable rtt (ms), filter away high delay nodes. defalut 0
|
||||||
MaxRTT Duration `json:"maxRTT,omitempty"`
|
MaxRTT Duration `json:"maxRTT,omitempty"`
|
||||||
// acceptable failure rate
|
// acceptable failure rate
|
||||||
Tolerance float64 `json:"tolerance,omitempty"`
|
Tolerance float64 `json:"tolerance,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LeastPingOutboundOptions is the options for leastping outbound
|
||||||
|
type LeastPingOutboundOptions struct {
|
||||||
|
BalancerOutboundOptions
|
||||||
|
HealthCheckOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
// LeastLoadOutboundOptions is the options for leastload outbound
|
||||||
|
type LeastLoadOutboundOptions struct {
|
||||||
|
BalancerOutboundOptions
|
||||||
|
HealthCheckOptions
|
||||||
|
// expected nodes count to select
|
||||||
|
Expected int32 `json:"expected,omitempty"`
|
||||||
|
// ping rtt baselines (ms)
|
||||||
|
Baselines []Duration `json:"baselines,omitempty"`
|
||||||
|
// cost settings
|
||||||
|
Costs []*StrategyWeight `json:"costs,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// HealthCheckSettings is the settings for health check
|
// HealthCheckSettings is the settings for health check
|
||||||
type HealthCheckSettings struct {
|
type HealthCheckSettings struct {
|
||||||
Destination string `json:"destination"`
|
Destination string `json:"destination"`
|
||||||
|
@ -23,6 +23,7 @@ type _Outbound struct {
|
|||||||
ShadowTLSOptions ShadowTLSOutboundOptions `json:"-"`
|
ShadowTLSOptions ShadowTLSOutboundOptions `json:"-"`
|
||||||
SelectorOptions SelectorOutboundOptions `json:"-"`
|
SelectorOptions SelectorOutboundOptions `json:"-"`
|
||||||
LeastLoadOptions LeastLoadOutboundOptions `json:"-"`
|
LeastLoadOptions LeastLoadOutboundOptions `json:"-"`
|
||||||
|
LeastPingOptions LeastPingOutboundOptions `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Outbound _Outbound
|
type Outbound _Outbound
|
||||||
@ -58,6 +59,8 @@ func (h Outbound) MarshalJSON() ([]byte, error) {
|
|||||||
v = h.SelectorOptions
|
v = h.SelectorOptions
|
||||||
case C.TypeLeastLoad:
|
case C.TypeLeastLoad:
|
||||||
v = h.LeastLoadOptions
|
v = h.LeastLoadOptions
|
||||||
|
case C.TypeLeastPing:
|
||||||
|
v = h.LeastPingOptions
|
||||||
default:
|
default:
|
||||||
return nil, E.New("unknown outbound type: ", h.Type)
|
return nil, E.New("unknown outbound type: ", h.Type)
|
||||||
}
|
}
|
||||||
@ -99,6 +102,8 @@ func (h *Outbound) UnmarshalJSON(bytes []byte) error {
|
|||||||
v = &h.SelectorOptions
|
v = &h.SelectorOptions
|
||||||
case C.TypeLeastLoad:
|
case C.TypeLeastLoad:
|
||||||
v = &h.LeastLoadOptions
|
v = &h.LeastLoadOptions
|
||||||
|
case C.TypeLeastPing:
|
||||||
|
v = &h.LeastPingOptions
|
||||||
default:
|
default:
|
||||||
return E.New("unknown outbound type: ", h.Type)
|
return E.New("unknown outbound type: ", h.Type)
|
||||||
}
|
}
|
||||||
|
137
outbound/balancer.go
Normal file
137
outbound/balancer.go
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
package outbound
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"math/rand"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/balancer"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ adapter.Outbound = (*Balancer)(nil)
|
||||||
|
_ adapter.OutboundGroup = (*Balancer)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Balancer is a outbound group that picks outbound with least load
|
||||||
|
type Balancer struct {
|
||||||
|
myOutboundAdapter
|
||||||
|
|
||||||
|
tags []string
|
||||||
|
fallbackTag string
|
||||||
|
|
||||||
|
balancer.Balancer
|
||||||
|
nodes []*balancer.Node
|
||||||
|
fallback adapter.Outbound
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBalancer creates a new Balancer outbound
|
||||||
|
func NewBalancer(
|
||||||
|
protocol string, router adapter.Router, logger log.ContextLogger, tag string,
|
||||||
|
outbounds []string, fallbackTag string,
|
||||||
|
) *Balancer {
|
||||||
|
b := &Balancer{
|
||||||
|
myOutboundAdapter: myOutboundAdapter{
|
||||||
|
protocol: protocol,
|
||||||
|
router: router,
|
||||||
|
logger: logger,
|
||||||
|
tag: tag,
|
||||||
|
},
|
||||||
|
tags: outbounds,
|
||||||
|
fallbackTag: fallbackTag,
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Network implements adapter.Outbound
|
||||||
|
func (s *Balancer) Network() []string {
|
||||||
|
picked := s.pick()
|
||||||
|
if picked == nil {
|
||||||
|
return []string{N.NetworkTCP, N.NetworkUDP}
|
||||||
|
}
|
||||||
|
return picked.Network()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now implements adapter.OutboundGroup
|
||||||
|
func (s *Balancer) Now() string {
|
||||||
|
return s.pick().Tag()
|
||||||
|
}
|
||||||
|
|
||||||
|
// All implements adapter.OutboundGroup
|
||||||
|
func (s *Balancer) All() []string {
|
||||||
|
return s.tags
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialContext implements adapter.Outbound
|
||||||
|
func (s *Balancer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||||
|
return s.pick().DialContext(ctx, network, destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListenPacket implements adapter.Outbound
|
||||||
|
func (s *Balancer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||||
|
return s.pick().ListenPacket(ctx, destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConnection implements adapter.Outbound
|
||||||
|
func (s *Balancer) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
|
||||||
|
return s.pick().NewConnection(ctx, conn, metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPacketConnection implements adapter.Outbound
|
||||||
|
func (s *Balancer) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
|
||||||
|
return s.pick().NewPacketConnection(ctx, conn, metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize inits the balancer
|
||||||
|
func (s *Balancer) initialize() error {
|
||||||
|
for i, tag := range s.tags {
|
||||||
|
outbound, loaded := s.router.Outbound(tag)
|
||||||
|
if !loaded {
|
||||||
|
return E.New("outbound ", i, " not found: ", tag)
|
||||||
|
}
|
||||||
|
s.nodes = append(s.nodes, balancer.NewNode(outbound))
|
||||||
|
}
|
||||||
|
if s.fallbackTag != "" {
|
||||||
|
outbound, loaded := s.router.Outbound(s.fallbackTag)
|
||||||
|
if !loaded {
|
||||||
|
return E.New("fallback outbound not found: ", s.fallbackTag)
|
||||||
|
}
|
||||||
|
s.fallback = outbound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Balancer) setBalancer(b balancer.Balancer) error {
|
||||||
|
s.Balancer = b
|
||||||
|
if starter, isStarter := b.(common.Starter); isStarter {
|
||||||
|
err := starter.Start()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Balancer) pick() adapter.Outbound {
|
||||||
|
if s.Balancer != nil {
|
||||||
|
selected := s.Balancer.Select()
|
||||||
|
if selected == nil {
|
||||||
|
return s.fallback
|
||||||
|
}
|
||||||
|
return selected.Outbound
|
||||||
|
}
|
||||||
|
// not started
|
||||||
|
count := len(s.nodes)
|
||||||
|
if count == 0 {
|
||||||
|
// goes to fallbackTag
|
||||||
|
return s.fallback
|
||||||
|
}
|
||||||
|
picked := s.nodes[rand.Intn(count)]
|
||||||
|
return picked.Outbound
|
||||||
|
}
|
@ -45,6 +45,8 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, o
|
|||||||
return NewSelector(router, logger, options.Tag, options.SelectorOptions)
|
return NewSelector(router, logger, options.Tag, options.SelectorOptions)
|
||||||
case C.TypeLeastLoad:
|
case C.TypeLeastLoad:
|
||||||
return NewLeastLoad(router, logger, options.Tag, options.LeastLoadOptions)
|
return NewLeastLoad(router, logger, options.Tag, options.LeastLoadOptions)
|
||||||
|
case C.TypeLeastPing:
|
||||||
|
return NewLeastPing(router, logger, options.Tag, options.LeastPingOptions)
|
||||||
default:
|
default:
|
||||||
return nil, E.New("unknown outbound type: ", options.Type)
|
return nil, E.New("unknown outbound type: ", options.Type)
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,12 @@
|
|||||||
package outbound
|
package outbound
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"math/rand"
|
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/balancer"
|
"github.com/sagernet/sing-box/balancer"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -22,106 +16,34 @@ var (
|
|||||||
|
|
||||||
// LeastLoad is a outbound group that picks outbound with least load
|
// LeastLoad is a outbound group that picks outbound with least load
|
||||||
type LeastLoad struct {
|
type LeastLoad struct {
|
||||||
myOutboundAdapter
|
*Balancer
|
||||||
options option.LeastLoadOutboundOptions
|
|
||||||
|
|
||||||
*balancer.LeastLoad
|
options option.LeastLoadOutboundOptions
|
||||||
nodes []*balancer.Node
|
|
||||||
fallback adapter.Outbound
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLeastLoad creates a new LeastLoad outbound
|
// NewLeastLoad creates a new LeastLoad outbound
|
||||||
func NewLeastLoad(router adapter.Router, logger log.ContextLogger, tag string, options option.LeastLoadOutboundOptions) (*LeastLoad, error) {
|
func NewLeastLoad(router adapter.Router, logger log.ContextLogger, tag string, options option.LeastLoadOutboundOptions) (*LeastLoad, error) {
|
||||||
outbound := &LeastLoad{
|
|
||||||
myOutboundAdapter: myOutboundAdapter{
|
|
||||||
protocol: C.TypeLeastLoad,
|
|
||||||
router: router,
|
|
||||||
logger: logger,
|
|
||||||
tag: tag,
|
|
||||||
},
|
|
||||||
options: options,
|
|
||||||
nodes: make([]*balancer.Node, 0, len(options.Outbounds)),
|
|
||||||
}
|
|
||||||
if len(options.Outbounds) == 0 {
|
if len(options.Outbounds) == 0 {
|
||||||
return nil, E.New("missing tags")
|
return nil, E.New("missing tags")
|
||||||
}
|
}
|
||||||
return outbound, nil
|
return &LeastLoad{
|
||||||
}
|
Balancer: NewBalancer(
|
||||||
|
C.TypeLeastLoad, router, logger, tag,
|
||||||
// Network implements adapter.Outbound
|
options.Outbounds, options.Fallback,
|
||||||
func (s *LeastLoad) Network() []string {
|
),
|
||||||
picked := s.pick()
|
options: options,
|
||||||
if picked == nil {
|
}, nil
|
||||||
return []string{N.NetworkTCP, N.NetworkUDP}
|
|
||||||
}
|
|
||||||
return picked.Network()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start implements common.Starter
|
// Start implements common.Starter
|
||||||
func (s *LeastLoad) Start() error {
|
func (s *LeastLoad) Start() error {
|
||||||
for i, tag := range s.options.Outbounds {
|
err := s.Balancer.initialize()
|
||||||
outbound, loaded := s.router.Outbound(tag)
|
|
||||||
if !loaded {
|
|
||||||
return E.New("outbound ", i, " not found: ", tag)
|
|
||||||
}
|
|
||||||
s.nodes = append(s.nodes, balancer.NewNode(outbound))
|
|
||||||
}
|
|
||||||
if s.options.Fallback != "" {
|
|
||||||
outbound, loaded := s.router.Outbound(s.options.Fallback)
|
|
||||||
if !loaded {
|
|
||||||
return E.New("fallback outbound not found: ", s.options.Fallback)
|
|
||||||
}
|
|
||||||
s.fallback = outbound
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
s.LeastLoad, err = balancer.NewLeastLoad(s.nodes, s.logger, s.options)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
s.HealthCheck.Start()
|
b, err := balancer.NewLeastLoad(s.nodes, s.logger, s.options)
|
||||||
return nil
|
if err != nil {
|
||||||
}
|
return err
|
||||||
|
|
||||||
// Now implements adapter.OutboundGroup
|
|
||||||
func (s *LeastLoad) Now() string {
|
|
||||||
return s.pick().Tag()
|
|
||||||
}
|
|
||||||
|
|
||||||
// All implements adapter.OutboundGroup
|
|
||||||
func (s *LeastLoad) All() []string {
|
|
||||||
return s.options.Outbounds
|
|
||||||
}
|
|
||||||
|
|
||||||
// DialContext implements adapter.Outbound
|
|
||||||
func (s *LeastLoad) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
|
||||||
return s.pick().DialContext(ctx, network, destination)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListenPacket implements adapter.Outbound
|
|
||||||
func (s *LeastLoad) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
|
||||||
return s.pick().ListenPacket(ctx, destination)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewConnection implements adapter.Outbound
|
|
||||||
func (s *LeastLoad) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
|
|
||||||
return s.pick().NewConnection(ctx, conn, metadata)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewPacketConnection implements adapter.Outbound
|
|
||||||
func (s *LeastLoad) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
|
|
||||||
return s.pick().NewPacketConnection(ctx, conn, metadata)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *LeastLoad) pick() adapter.Outbound {
|
|
||||||
selects := s.nodes
|
|
||||||
if s.LeastLoad != nil {
|
|
||||||
selects = s.LeastLoad.Select()
|
|
||||||
}
|
}
|
||||||
count := len(selects)
|
return s.setBalancer(b)
|
||||||
if count == 0 {
|
|
||||||
// goes to fallbackTag
|
|
||||||
return s.fallback
|
|
||||||
}
|
|
||||||
picked := selects[rand.Intn(count)]
|
|
||||||
return picked.Outbound
|
|
||||||
}
|
}
|
||||||
|
49
outbound/leastping.go
Normal file
49
outbound/leastping.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package outbound
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/balancer"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ adapter.Outbound = (*LeastPing)(nil)
|
||||||
|
_ adapter.OutboundGroup = (*LeastPing)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
// LeastPing is a outbound group that picks outbound with least load
|
||||||
|
type LeastPing struct {
|
||||||
|
*Balancer
|
||||||
|
|
||||||
|
options option.LeastPingOutboundOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLeastPing creates a new LeastPing outbound
|
||||||
|
func NewLeastPing(router adapter.Router, logger log.ContextLogger, tag string, options option.LeastPingOutboundOptions) (*LeastPing, error) {
|
||||||
|
if len(options.Outbounds) == 0 {
|
||||||
|
return nil, E.New("missing tags")
|
||||||
|
}
|
||||||
|
return &LeastPing{
|
||||||
|
Balancer: NewBalancer(
|
||||||
|
C.TypeLeastPing, router, logger, tag,
|
||||||
|
options.Outbounds, options.Fallback,
|
||||||
|
),
|
||||||
|
options: options,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start implements common.Starter
|
||||||
|
func (s *LeastPing) Start() error {
|
||||||
|
err := s.Balancer.initialize()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
b, err := balancer.NewLeastPing(s.nodes, s.logger, s.options)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.setBalancer(b)
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user