package network import ( "context" "fmt" "io" "net" "net/url" "github.com/dolonet/mtg-multi/essentials" "github.com/txthinking/socks5" ) type socks5Dialer struct { Dialer username []byte password []byte proxyAddress string } func (s socks5Dialer) Dial(network, address string) (essentials.Conn, error) { return s.DialContext(context.Background(), network, address) } func (s socks5Dialer) DialContext(ctx context.Context, network, address string) (essentials.Conn, error) { switch network { case "tcp", "tcp4", "tcp6": default: return nil, fmt.Errorf("%s network type is not supported", network) } conn, err := s.Dialer.DialContext(ctx, network, s.proxyAddress) if err != nil { return nil, fmt.Errorf("cannot dial to the proxy: %w", err) } if err := s.handshake(conn); err != nil { conn.Close() //nolint: errcheck return nil, fmt.Errorf("cannot perform a handshake: %w", err) } if err := s.connect(conn, address); err != nil { conn.Close() //nolint: errcheck return nil, fmt.Errorf("cannot connect to a destination host %s: %w", address, err) } return conn, nil } func (s socks5Dialer) handshake(conn io.ReadWriter) error { authMethod := socks5.MethodUsernamePassword if len(s.username)+len(s.password) == 0 { authMethod = socks5.MethodNone } if err := s.handshakeNegotiation(conn, authMethod); err != nil { return fmt.Errorf("cannot perform negotiation: %w", err) } if authMethod == socks5.MethodNone { return nil } if err := s.handshakeAuth(conn); err != nil { return fmt.Errorf("cannot authenticate: %w", err) } return nil } func (s socks5Dialer) handshakeNegotiation(conn io.ReadWriter, authMethod byte) error { request := socks5.NewNegotiationRequest([]byte{authMethod}) if _, err := request.WriteTo(conn); err != nil { return fmt.Errorf("cannot send request: %w", err) } response, err := socks5.NewNegotiationReplyFrom(conn) if err != nil { return fmt.Errorf("cannot read response: %w", err) } if response.Method != authMethod { return fmt.Errorf("%v is unsupported auth method", authMethod) } return nil } func (s socks5Dialer) handshakeAuth(conn io.ReadWriter) error { request := socks5.NewUserPassNegotiationRequest(s.username, s.password) if _, err := request.WriteTo(conn); err != nil { return fmt.Errorf("cannot send a request: %w", err) } response, err := socks5.NewUserPassNegotiationReplyFrom(conn) if err != nil { return fmt.Errorf("cannot read a response: %w", err) } if response.Status != socks5.UserPassStatusSuccess { return fmt.Errorf("authenticate has failed: %v", response.Status) } return nil } func (s socks5Dialer) connect(conn io.ReadWriter, address string) error { addrType, host, port, err := socks5.ParseAddress(address) if err != nil { return fmt.Errorf("cannot parse address: %w", err) } if addrType == socks5.ATYPDomain { host = host[1:] } request := socks5.NewRequest(socks5.CmdConnect, addrType, host, port) if _, err := request.WriteTo(conn); err != nil { return fmt.Errorf("cannot send a request: %w", err) } response, err := socks5.NewReplyFrom(conn) if err != nil { return fmt.Errorf("cannot read a response: %w", err) } if response.Rep != socks5.RepSuccess { return fmt.Errorf("unsuccessful request: %v", response.Rep) } return nil } // NewSocks5Dialer build a new dialer from a given one (so, in theory you can // chain here). Proxy parameters are passed with URI in a form of: // // socks5://[user:[password]]@host:port func NewSocks5Dialer(baseDialer Dialer, proxyURL *url.URL) (Dialer, error) { if _, _, err := net.SplitHostPort(proxyURL.Host); err != nil { return nil, fmt.Errorf("incorrect url %s", proxyURL.Redacted()) } dialer := socks5Dialer{ Dialer: baseDialer, proxyAddress: proxyURL.Host, } if proxyURL.User != nil { password, isSet := proxyURL.User.Password() if isSet { dialer.username = []byte(proxyURL.User.Username()) dialer.password = []byte(password) } } return dialer, nil }