package network import ( "context" "fmt" "math/rand/v2" "net/url" "github.com/9seconds/mtg/v2/essentials" ) type loadBalancedSocks5Dialer struct { dialers []Dialer } func (l loadBalancedSocks5Dialer) Dial(network, address string) (essentials.Conn, error) { return l.DialContext(context.Background(), network, address) } func (l loadBalancedSocks5Dialer) DialContext(ctx context.Context, network, address string) (essentials.Conn, error) { length := len(l.dialers) start := rand.IntN(length) moved := false for i := start; i != start || !moved; i = (i + 1) % length { moved = true if conn, err := l.dialers[i].DialContext(ctx, network, address); err == nil { return conn, nil } } return nil, ErrCannotDialWithAllProxies } // NewLoadBalancedSocks5Dialer builds a new load balancing SOCKS5 dialer. // // The main difference from one which is made by NewSocks5Dialer is that we // actually have a list of these proxies. When dial is requested, any proxy is // picked and used. If proxy fails for some reason, we try another one. // // So, it is mostly useful if you have some routes with proxies which are not // always online or having buggy network. func NewLoadBalancedSocks5Dialer(baseDialer Dialer, proxyURLs []*url.URL) (Dialer, error) { dialers := make([]Dialer, 0, len(proxyURLs)) for _, u := range proxyURLs { dialer, err := NewSocks5Dialer(newProxyDialer(baseDialer, u), u) if err != nil { return nil, fmt.Errorf("cannot build dialer for %s: %w", u.String(), err) } dialers = append(dialers, dialer) } return loadBalancedSocks5Dialer{ dialers: dialers, }, nil }