From e793764c49e565c107c9eb1f3c109ed9d8d6d9eb Mon Sep 17 00:00:00 2001 From: amir gh Date: Mon, 15 Jan 2024 23:21:59 -0800 Subject: [PATCH 01/32] add dynamic config support --- x/examples/test-connectivity/dynamiccofig.go | 148 +++++++++++++++++++ x/examples/test-connectivity/main.go | 139 ++++++++++------- 2 files changed, 236 insertions(+), 51 deletions(-) create mode 100644 x/examples/test-connectivity/dynamiccofig.go diff --git a/x/examples/test-connectivity/dynamiccofig.go b/x/examples/test-connectivity/dynamiccofig.go new file mode 100644 index 00000000..33225a16 --- /dev/null +++ b/x/examples/test-connectivity/dynamiccofig.go @@ -0,0 +1,148 @@ +package main + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "net/url" + "strings" +) + +// FormatA struct for the first JSON format +type ServerInfo struct { + ID string `json:"id,omitempty"` + Remarks string `json:"remarks,omitempty"` + Server string `json:"server"` + ServerPort int `json:"server_port"` + Password string `json:"password"` + Method string `json:"method"` + Prefix string `json:"prefix"` + Plugin string `json:"plugin,omitempty"` + PluginOpts string `json:"plugin_opts,omitempty"` +} + +// FormatC struct for the SIP008 JSON format +type SIP008Config struct { + Version int `json:"version"` + Servers []ServerInfo `json:"servers"` + BytesUsed uint64 `json:"bytes_used,omitempty"` + BytesRemaining uint64 `json:"bytes_remaining,omitempty"` + AdditionalProps map[string]interface{} // For custom fields +} + +func parseDynamicConfig(data []byte) ([]string, error) { + //Parse if simple JSON format + server, err := parseSingleJSON(data) + if err == nil { + return []string{server}, nil + } + // Parse if SIP008 JSON format + servers, err := parseSIP008(data) + if err == nil { + return servers, nil + } else { + fmt.Println("parseSIP008 error:", err) + } + // Parse if CSV format + servers, err = parseBase64URLLine(data) + if err == nil { + return servers, nil + } else { + fmt.Println("parseBase64URLLine error:", err) + } + servers, err = parseCSVformat(data) + if err == nil { + return servers, nil + } else { + fmt.Println("parseCSVformat error:", err) + } + return []string{}, fmt.Errorf("unknown format") + // parse +} + +func parseSingleJSON(data []byte) (string, error) { + //Parse if simple JSON format + var config ServerInfo + err := json.Unmarshal(data, &config) + if err != nil { + return "", err + } + return makeShadowsocksURLfromJSON(&config) +} + +func parseSIP008(data []byte) ([]string, error) { + //Parse if SIP008 JSON format + var config SIP008Config + err := json.Unmarshal(data, &config) + if err != nil { + return []string{}, err + } + if config.Version == 1 { + var result []string + for _, server := range config.Servers { + configURL, err := makeShadowsocksURLfromJSON(&server) + if err != nil { + return []string{}, err + } + result = append(result, configURL) + } + return result, nil + } + return []string{}, fmt.Errorf("unknown SIP008 version: %d", config.Version) +} + +func parseCSVformat(data []byte) ([]string, error) { + // fmt.Println("Printing response string:") + str := string(data) + configs := strings.Split(str, "\n") + fmt.Println("Printing response string:") + fmt.Println(configs) + // check of each line contains a valid URL + for _, config := range configs { + // Ignore blank lines + if config == "" { + continue + } + u, err := url.Parse(config) + if err != nil { + return []string{}, fmt.Errorf("invalid URL: %s", config) + } + fmt.Println("scheme:", u.Scheme) + if u.Scheme == "" { + return []string{}, fmt.Errorf("invalid scheme: %s", config) + } + } + return configs, nil +} + +// https://www.v2fly.org/en_US/v5/config/service/subscription.html#subscription-container +func parseBase64URLLine(data []byte) ([]string, error) { + decoded, err := base64.URLEncoding.WithPadding(base64.NoPadding).DecodeString(string(data)) + if err != nil { + return []string{}, err + } + return parseCSVformat(decoded) +} + +func makeShadowsocksURLfromJSON(config *ServerInfo) (string, error) { + if config.ServerPort == 0 { + return "", fmt.Errorf("missing server port") + } + if config.Method == "" { + return "", fmt.Errorf("missing method") + } + if config.Password == "" { + return "", fmt.Errorf("missing password") + } + if config.Server == "" { + return "", fmt.Errorf("missing server") + } + configURL := "ss://" + config.Method + ":" + config.Password + "@" + config.Server + ":" + fmt.Sprint(config.ServerPort) + if config.Prefix != "" { + configURL += "/?prefix=" + url.QueryEscape(config.Prefix) + } + if config.Plugin != "" { + configURL += "&plugin=" + url.QueryEscape(config.Plugin) + } + return configURL, nil +} diff --git a/x/examples/test-connectivity/main.go b/x/examples/test-connectivity/main.go index 8cc76c2c..04774679 100644 --- a/x/examples/test-connectivity/main.go +++ b/x/examples/test-connectivity/main.go @@ -127,6 +127,19 @@ func main() { debugLog = *log.New(os.Stderr, "[DEBUG] ", log.LstdFlags|log.Lmicroseconds|log.Lshortfile) } + var transportConfigs []string + var err error + if strings.HasPrefix(*transportFlag, "ssconfig:") { + newURL := strings.Replace(*transportFlag, "ssconfig", "https", -1) + transportConfigs, err = getDynamicConfig(newURL) + if err != nil { + fmt.Println("Error:", err) + return + } + } else { + transportConfigs = []string{*transportFlag} + } + var reportCollector report.Collector if *reportToFlag != "" { collectorURL, err := url.Parse(*reportToFlag) @@ -158,63 +171,87 @@ func main() { // - Server IPv4 dial support // - Server IPv6 dial support - success := false - jsonEncoder := json.NewEncoder(os.Stdout) - jsonEncoder.SetEscapeHTML(false) - for _, resolverHost := range strings.Split(*resolverFlag, ",") { - resolverHost := strings.TrimSpace(resolverHost) - resolverAddress := net.JoinHostPort(resolverHost, "53") - for _, proto := range strings.Split(*protoFlag, ",") { - proto = strings.TrimSpace(proto) - var resolver dns.Resolver - switch proto { - case "tcp": - streamDialer, err := config.NewStreamDialer(*transportFlag) - if err != nil { - log.Fatalf("Failed to create StreamDialer: %v", err) + for _, c := range transportConfigs { + //success := false + jsonEncoder := json.NewEncoder(os.Stdout) + jsonEncoder.SetEscapeHTML(false) + for _, resolverHost := range strings.Split(*resolverFlag, ",") { + resolverHost := strings.TrimSpace(resolverHost) + resolverAddress := net.JoinHostPort(resolverHost, "53") + for _, proto := range strings.Split(*protoFlag, ",") { + proto = strings.TrimSpace(proto) + var resolver dns.Resolver + switch proto { + case "tcp": + streamDialer, err := config.NewStreamDialer(c) + if err != nil { + log.Fatalf("Failed to create StreamDialer: %v", err) + } + resolver = dns.NewTCPResolver(streamDialer, resolverAddress) + case "udp": + packetDialer, err := config.NewPacketDialer(c) + if err != nil { + log.Fatalf("Failed to create PacketDialer: %v", err) + } + resolver = dns.NewUDPResolver(packetDialer, resolverAddress) + default: + log.Fatalf(`Invalid proto %v. Must be "tcp" or "udp"`, proto) } - resolver = dns.NewTCPResolver(streamDialer, resolverAddress) - case "udp": - packetDialer, err := config.NewPacketDialer(*transportFlag) + startTime := time.Now() + result, err := connectivity.TestConnectivityWithResolver(context.Background(), resolver, *domainFlag) if err != nil { - log.Fatalf("Failed to create PacketDialer: %v", err) + log.Fatalf("Connectivity test failed to run: %v", err) } - resolver = dns.NewUDPResolver(packetDialer, resolverAddress) - default: - log.Fatalf(`Invalid proto %v. Must be "tcp" or "udp"`, proto) - } - startTime := time.Now() - result, err := connectivity.TestConnectivityWithResolver(context.Background(), resolver, *domainFlag) - if err != nil { - log.Fatalf("Connectivity test failed to run: %v", err) - } - testDuration := time.Since(startTime) - if result == nil { - success = true - } - debugLog.Printf("Test %v %v result: %v", proto, resolverAddress, result) - sanitizedConfig, err := config.SanitizeConfig(*transportFlag) - if err != nil { - log.Fatalf("Failed to sanitize config: %v", err) - } - var r report.Report = connectivityReport{ - Resolver: resolverAddress, - Proto: proto, - Time: startTime.UTC().Truncate(time.Second), - // TODO(fortuna): Add sanitized config: - Transport: sanitizedConfig, - DurationMs: testDuration.Milliseconds(), - Error: makeErrorRecord(result), - } - if reportCollector != nil { - err = reportCollector.Collect(context.Background(), r) + testDuration := time.Since(startTime) + // if result == nil { + // success = true + // } + debugLog.Printf("Test %v %v result: %v", proto, resolverAddress, result) + sanitizedConfig, err := config.SanitizeConfig(c) if err != nil { - debugLog.Printf("Failed to collect report: %v\n", err) + log.Fatalf("Failed to sanitize config: %v", err) + } + var r report.Report = connectivityReport{ + Resolver: resolverAddress, + Proto: proto, + Time: startTime.UTC().Truncate(time.Second), + // TODO(fortuna): Add sanitized config: + Transport: sanitizedConfig, + DurationMs: testDuration.Milliseconds(), + Error: makeErrorRecord(result), + } + if reportCollector != nil { + err = reportCollector.Collect(context.Background(), r) + if err != nil { + debugLog.Printf("Failed to collect report: %v\n", err) + } } } + // if !success { + // os.Exit(1) + // } } - if !success { - os.Exit(1) - } } } + +func getDynamicConfig(url string) ([]string, error) { + response, err := http.Get(url) + if err != nil { + fmt.Println("Error fetching URL:", err) + return []string{}, err + } + defer response.Body.Close() + + body, err := io.ReadAll(response.Body) + if err != nil { + fmt.Println("Error reading response:", err) + return []string{}, err + } + + conf, err := parseDynamicConfig(body) + if err != nil { + fmt.Println("Error detecting format:", err) + return []string{}, err + } + return conf, nil +} From ba988715d20b40bdd3a8955c715fa572a756a8d4 Mon Sep 17 00:00:00 2001 From: amir gh Date: Sun, 21 Jan 2024 15:13:48 -0800 Subject: [PATCH 02/32] Revert "add dynamic config support" This reverts commit e793764c49e565c107c9eb1f3c109ed9d8d6d9eb. --- x/examples/test-connectivity/dynamiccofig.go | 148 ------------------- x/examples/test-connectivity/main.go | 139 +++++++---------- 2 files changed, 51 insertions(+), 236 deletions(-) delete mode 100644 x/examples/test-connectivity/dynamiccofig.go diff --git a/x/examples/test-connectivity/dynamiccofig.go b/x/examples/test-connectivity/dynamiccofig.go deleted file mode 100644 index 33225a16..00000000 --- a/x/examples/test-connectivity/dynamiccofig.go +++ /dev/null @@ -1,148 +0,0 @@ -package main - -import ( - "encoding/base64" - "encoding/json" - "fmt" - "net/url" - "strings" -) - -// FormatA struct for the first JSON format -type ServerInfo struct { - ID string `json:"id,omitempty"` - Remarks string `json:"remarks,omitempty"` - Server string `json:"server"` - ServerPort int `json:"server_port"` - Password string `json:"password"` - Method string `json:"method"` - Prefix string `json:"prefix"` - Plugin string `json:"plugin,omitempty"` - PluginOpts string `json:"plugin_opts,omitempty"` -} - -// FormatC struct for the SIP008 JSON format -type SIP008Config struct { - Version int `json:"version"` - Servers []ServerInfo `json:"servers"` - BytesUsed uint64 `json:"bytes_used,omitempty"` - BytesRemaining uint64 `json:"bytes_remaining,omitempty"` - AdditionalProps map[string]interface{} // For custom fields -} - -func parseDynamicConfig(data []byte) ([]string, error) { - //Parse if simple JSON format - server, err := parseSingleJSON(data) - if err == nil { - return []string{server}, nil - } - // Parse if SIP008 JSON format - servers, err := parseSIP008(data) - if err == nil { - return servers, nil - } else { - fmt.Println("parseSIP008 error:", err) - } - // Parse if CSV format - servers, err = parseBase64URLLine(data) - if err == nil { - return servers, nil - } else { - fmt.Println("parseBase64URLLine error:", err) - } - servers, err = parseCSVformat(data) - if err == nil { - return servers, nil - } else { - fmt.Println("parseCSVformat error:", err) - } - return []string{}, fmt.Errorf("unknown format") - // parse -} - -func parseSingleJSON(data []byte) (string, error) { - //Parse if simple JSON format - var config ServerInfo - err := json.Unmarshal(data, &config) - if err != nil { - return "", err - } - return makeShadowsocksURLfromJSON(&config) -} - -func parseSIP008(data []byte) ([]string, error) { - //Parse if SIP008 JSON format - var config SIP008Config - err := json.Unmarshal(data, &config) - if err != nil { - return []string{}, err - } - if config.Version == 1 { - var result []string - for _, server := range config.Servers { - configURL, err := makeShadowsocksURLfromJSON(&server) - if err != nil { - return []string{}, err - } - result = append(result, configURL) - } - return result, nil - } - return []string{}, fmt.Errorf("unknown SIP008 version: %d", config.Version) -} - -func parseCSVformat(data []byte) ([]string, error) { - // fmt.Println("Printing response string:") - str := string(data) - configs := strings.Split(str, "\n") - fmt.Println("Printing response string:") - fmt.Println(configs) - // check of each line contains a valid URL - for _, config := range configs { - // Ignore blank lines - if config == "" { - continue - } - u, err := url.Parse(config) - if err != nil { - return []string{}, fmt.Errorf("invalid URL: %s", config) - } - fmt.Println("scheme:", u.Scheme) - if u.Scheme == "" { - return []string{}, fmt.Errorf("invalid scheme: %s", config) - } - } - return configs, nil -} - -// https://www.v2fly.org/en_US/v5/config/service/subscription.html#subscription-container -func parseBase64URLLine(data []byte) ([]string, error) { - decoded, err := base64.URLEncoding.WithPadding(base64.NoPadding).DecodeString(string(data)) - if err != nil { - return []string{}, err - } - return parseCSVformat(decoded) -} - -func makeShadowsocksURLfromJSON(config *ServerInfo) (string, error) { - if config.ServerPort == 0 { - return "", fmt.Errorf("missing server port") - } - if config.Method == "" { - return "", fmt.Errorf("missing method") - } - if config.Password == "" { - return "", fmt.Errorf("missing password") - } - if config.Server == "" { - return "", fmt.Errorf("missing server") - } - configURL := "ss://" + config.Method + ":" + config.Password + "@" + config.Server + ":" + fmt.Sprint(config.ServerPort) - if config.Prefix != "" { - configURL += "/?prefix=" + url.QueryEscape(config.Prefix) - } - if config.Plugin != "" { - configURL += "&plugin=" + url.QueryEscape(config.Plugin) - } - return configURL, nil -} diff --git a/x/examples/test-connectivity/main.go b/x/examples/test-connectivity/main.go index 04774679..8cc76c2c 100644 --- a/x/examples/test-connectivity/main.go +++ b/x/examples/test-connectivity/main.go @@ -127,19 +127,6 @@ func main() { debugLog = *log.New(os.Stderr, "[DEBUG] ", log.LstdFlags|log.Lmicroseconds|log.Lshortfile) } - var transportConfigs []string - var err error - if strings.HasPrefix(*transportFlag, "ssconfig:") { - newURL := strings.Replace(*transportFlag, "ssconfig", "https", -1) - transportConfigs, err = getDynamicConfig(newURL) - if err != nil { - fmt.Println("Error:", err) - return - } - } else { - transportConfigs = []string{*transportFlag} - } - var reportCollector report.Collector if *reportToFlag != "" { collectorURL, err := url.Parse(*reportToFlag) @@ -171,87 +158,63 @@ func main() { // - Server IPv4 dial support // - Server IPv6 dial support - for _, c := range transportConfigs { - //success := false - jsonEncoder := json.NewEncoder(os.Stdout) - jsonEncoder.SetEscapeHTML(false) - for _, resolverHost := range strings.Split(*resolverFlag, ",") { - resolverHost := strings.TrimSpace(resolverHost) - resolverAddress := net.JoinHostPort(resolverHost, "53") - for _, proto := range strings.Split(*protoFlag, ",") { - proto = strings.TrimSpace(proto) - var resolver dns.Resolver - switch proto { - case "tcp": - streamDialer, err := config.NewStreamDialer(c) - if err != nil { - log.Fatalf("Failed to create StreamDialer: %v", err) - } - resolver = dns.NewTCPResolver(streamDialer, resolverAddress) - case "udp": - packetDialer, err := config.NewPacketDialer(c) - if err != nil { - log.Fatalf("Failed to create PacketDialer: %v", err) - } - resolver = dns.NewUDPResolver(packetDialer, resolverAddress) - default: - log.Fatalf(`Invalid proto %v. Must be "tcp" or "udp"`, proto) - } - startTime := time.Now() - result, err := connectivity.TestConnectivityWithResolver(context.Background(), resolver, *domainFlag) + success := false + jsonEncoder := json.NewEncoder(os.Stdout) + jsonEncoder.SetEscapeHTML(false) + for _, resolverHost := range strings.Split(*resolverFlag, ",") { + resolverHost := strings.TrimSpace(resolverHost) + resolverAddress := net.JoinHostPort(resolverHost, "53") + for _, proto := range strings.Split(*protoFlag, ",") { + proto = strings.TrimSpace(proto) + var resolver dns.Resolver + switch proto { + case "tcp": + streamDialer, err := config.NewStreamDialer(*transportFlag) if err != nil { - log.Fatalf("Connectivity test failed to run: %v", err) + log.Fatalf("Failed to create StreamDialer: %v", err) } - testDuration := time.Since(startTime) - // if result == nil { - // success = true - // } - debugLog.Printf("Test %v %v result: %v", proto, resolverAddress, result) - sanitizedConfig, err := config.SanitizeConfig(c) + resolver = dns.NewTCPResolver(streamDialer, resolverAddress) + case "udp": + packetDialer, err := config.NewPacketDialer(*transportFlag) if err != nil { - log.Fatalf("Failed to sanitize config: %v", err) + log.Fatalf("Failed to create PacketDialer: %v", err) } - var r report.Report = connectivityReport{ - Resolver: resolverAddress, - Proto: proto, - Time: startTime.UTC().Truncate(time.Second), - // TODO(fortuna): Add sanitized config: - Transport: sanitizedConfig, - DurationMs: testDuration.Milliseconds(), - Error: makeErrorRecord(result), - } - if reportCollector != nil { - err = reportCollector.Collect(context.Background(), r) - if err != nil { - debugLog.Printf("Failed to collect report: %v\n", err) - } + resolver = dns.NewUDPResolver(packetDialer, resolverAddress) + default: + log.Fatalf(`Invalid proto %v. Must be "tcp" or "udp"`, proto) + } + startTime := time.Now() + result, err := connectivity.TestConnectivityWithResolver(context.Background(), resolver, *domainFlag) + if err != nil { + log.Fatalf("Connectivity test failed to run: %v", err) + } + testDuration := time.Since(startTime) + if result == nil { + success = true + } + debugLog.Printf("Test %v %v result: %v", proto, resolverAddress, result) + sanitizedConfig, err := config.SanitizeConfig(*transportFlag) + if err != nil { + log.Fatalf("Failed to sanitize config: %v", err) + } + var r report.Report = connectivityReport{ + Resolver: resolverAddress, + Proto: proto, + Time: startTime.UTC().Truncate(time.Second), + // TODO(fortuna): Add sanitized config: + Transport: sanitizedConfig, + DurationMs: testDuration.Milliseconds(), + Error: makeErrorRecord(result), + } + if reportCollector != nil { + err = reportCollector.Collect(context.Background(), r) + if err != nil { + debugLog.Printf("Failed to collect report: %v\n", err) } } - // if !success { - // os.Exit(1) - // } + } + if !success { + os.Exit(1) } } } - -func getDynamicConfig(url string) ([]string, error) { - response, err := http.Get(url) - if err != nil { - fmt.Println("Error fetching URL:", err) - return []string{}, err - } - defer response.Body.Close() - - body, err := io.ReadAll(response.Body) - if err != nil { - fmt.Println("Error reading response:", err) - return []string{}, err - } - - conf, err := parseDynamicConfig(body) - if err != nil { - fmt.Println("Error detecting format:", err) - return []string{}, err - } - return conf, nil -} From b4674ec45d0dd3fdf0542abd5e52ceddf224f962 Mon Sep 17 00:00:00 2001 From: amir gh Date: Sun, 25 Feb 2024 13:18:39 -0800 Subject: [PATCH 03/32] add auth to socks5 in single rountrip --- transport/socks5/stream_dialer.go | 190 ++++++++++++++++++++++-------- 1 file changed, 143 insertions(+), 47 deletions(-) diff --git a/transport/socks5/stream_dialer.go b/transport/socks5/stream_dialer.go index 9d4febef..c30f7e0e 100644 --- a/transport/socks5/stream_dialer.go +++ b/transport/socks5/stream_dialer.go @@ -23,23 +23,53 @@ import ( "github.com/Jigsaw-Code/outline-sdk/transport" ) +type Credentials struct { + username []byte + password []byte +} + +// SetUsername sets the username field, ensuring it doesn't exceed 255 bytes in length and is at least 1 byte. +func (c *Credentials) SetUsername(username string) error { + if len([]byte(username)) > 255 { + return errors.New("username exceeds 255 bytes") + } + if len([]byte(username)) < 1 { + return errors.New("username must be at least 1 byte") + } + c.username = []byte(username) + return nil +} + +// SetPassword sets the password field, ensuring it doesn't exceed 255 bytes in length and is at least 1 byte. +func (c *Credentials) SetPassword(password string) error { + if len([]byte(password)) > 255 { + return errors.New("password exceeds 255 bytes") + } + if len([]byte(password)) < 1 { + return errors.New("password must be at least 1 byte") + } + c.password = []byte(password) + return nil +} + // NewStreamDialer creates a [transport.StreamDialer] that routes connections to a SOCKS5 // proxy listening at the given [transport.StreamEndpoint]. -func NewStreamDialer(endpoint transport.StreamEndpoint) (transport.StreamDialer, error) { +func NewStreamDialer(endpoint transport.StreamEndpoint, cred *Credentials) (transport.StreamDialer, error) { if endpoint == nil { return nil, errors.New("argument endpoint must not be nil") } - return &streamDialer{proxyEndpoint: endpoint}, nil + return &streamDialer{proxyEndpoint: endpoint, credentials: cred}, nil } type streamDialer struct { proxyEndpoint transport.StreamEndpoint + credentials *Credentials } var _ transport.StreamDialer = (*streamDialer)(nil) // DialStream implements [transport.StreamDialer].DialStream using SOCKS5. -// It will send the method and the connect requests in one packet, to avoid an unnecessary roundtrip. +// It will send the auth method, sub-negotiation, and the connect requests in one packet, to avoid an unnecessary roundtrip. // The returned [error] will be of type [ReplyCode] if the server sends a SOCKS error reply code, which // you can check against the error constants in this package using [errors.Is]. func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (transport.StreamConn, error) { @@ -55,78 +85,144 @@ func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (trans }() // For protocol details, see https://datatracker.ietf.org/doc/html/rfc1928#section-3 + // Creating a single buffer for method selection, authentication, and connection request + // Buffer large enough for method, auth, and connect requests with a domain name address. + + var buffer []byte + + if c.credentials == nil { + // Method selection part: VER = 5, NMETHODS = 1, METHODS = 0 (no auth) + // +----+----------+----------+ + // |VER | NMETHODS | METHODS | + // +----+----------+----------+ + // | 1 | 1 | 1 to 255 | + // +----+----------+----------+ + header := [3 + 3 + 256 + 2]byte{} + buffer = append(header[:0], 5, 1, 0) + } else { + // https://datatracker.ietf.org/doc/html/rfc1929 + // Method selection part: VER = 5, NMETHODS = 1, METHODS = 2 (username/password) + header := [3 + 3 + 255 + 255 + 3 + 256 + 2]byte{} + buffer = append(header[:0], 5, 1, 2) + + // Authentication part: VER = 1, ULEN, UNAME, PLEN, PASSWD + // +----+------+----------+------+----------+ + // |VER | ULEN | UNAME | PLEN | PASSWD | + // +----+------+----------+------+----------+ + // | 1 | 1 | 1 to 255 | 1 | 1 to 255 | + // +----+------+----------+------+----------+ + buffer = append(buffer, 1) // Auth version + buffer = append(buffer, byte(len(c.credentials.username))) + buffer = append(buffer, c.credentials.username...) + buffer = append(buffer, byte(len(c.credentials.password))) + buffer = append(buffer, c.credentials.password...) + } - // Buffer large enough for method and connect requests with a domain name address. - header := [3 + 4 + 256 + 2]byte{} - - // Method request: - // VER = 5, NMETHODS = 1, METHODS = 0 (no auth) - b := append(header[:0], 5, 1, 0) - - // Connect request: - // VER = 5, CMD = 1 (connect), RSV = 0 - b = append(b, 5, 1, 0) - // Destination address Address (ATYP, DST.ADDR, DST.PORT) - b, err = appendSOCKS5Address(b, remoteAddr) + // Connect request part: VER = 5, CMD = 1 (connect), RSV = 0, DST.ADDR, DST.PORT + // +----+-----+-------+------+----------+----------+ + // |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT | + // +----+-----+-------+------+----------+----------+ + // | 1 | 1 | X'00' | 1 | Variable | 2 | + // +----+-----+-------+------+----------+----------+ + connectRequest, err := appendSOCKS5Address([]byte{5, 1, 0}, remoteAddr) if err != nil { return nil, fmt.Errorf("failed to create SOCKS5 address: %w", err) } + buffer = append(buffer, connectRequest...) - // We merge the method and connect requests because we send a single authentication - // method, so there's no point in waiting for the response. This eliminates a roundtrip. - _, err = proxyConn.Write(b) + // Sending the combined request + _, err = proxyConn.Write(buffer) if err != nil { - return nil, fmt.Errorf("failed to write SOCKS5 request: %w", err) + return nil, fmt.Errorf("failed to write combined SOCKS5 request: %w", err) } - // Read method response (VER, METHOD). - if _, err = io.ReadFull(proxyConn, header[:2]); err != nil { + // Read several response parts in one go, to avoid an unnecessary roundtrip. + // 1. Read method response (VER, METHOD). + // +----+--------+ + // |VER | METHOD | + // +----+--------+ + // | 1 | 1 | + // +----+--------+ + var methodResponse [2]byte + if _, err = io.ReadFull(proxyConn, methodResponse[:]); err != nil { return nil, fmt.Errorf("failed to read method server response") } - if header[0] != 5 { - return nil, fmt.Errorf("invalid protocol version %v. Expected 5", header[0]) + if methodResponse[0] != 5 { + return nil, fmt.Errorf("invalid protocol version %v. Expected 5", methodResponse[0]) + } + if methodResponse[1] == 2 { + // 2. Read sub-negotiation version and status + // VER = 1, STATUS = 0 + // +----+--------+ + // |VER | STATUS | + // +----+--------+ + // | 1 | 1 | + // +----+--------+ + var subNegotiation [2]byte + if _, err = io.ReadFull(proxyConn, subNegotiation[:]); err != nil { + return nil, fmt.Errorf("failed to read sub-negotiation version and status: %w", err) + } + if subNegotiation[0] != 1 { + return nil, fmt.Errorf("unkown sub-negotioation version") + } + if subNegotiation[1] != 0 { + return nil, fmt.Errorf("authentication failed: %v", subNegotiation[1]) + } } - if header[1] != 0 { - return nil, fmt.Errorf("unsupported SOCKS authentication method %v. Expected 0 (no auth)", header[1]) + // Check if the server supports the authentication method we sent. + // 0 is no auth, 2 is username/password + if methodResponse[1] != 0 && methodResponse[1] != 2 { + return nil, fmt.Errorf("unsupported SOCKS authentication method %v. Expected 2", methodResponse[1]) } - - // Read connect response (VER, REP, RSV, ATYP, BND.ADDR, BND.PORT). + // 3. Read connect response (VER, REP, RSV, ATYP, BND.ADDR, BND.PORT). // See https://datatracker.ietf.org/doc/html/rfc1928#section-6. - if _, err = io.ReadFull(proxyConn, header[:4]); err != nil { - return nil, fmt.Errorf("failed to read connect server response") + // +----+-----+-------+------+----------+----------+ + // |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT | + // +----+-----+-------+------+----------+----------+ + // | 1 | 1 | X'00' | 1 | Variable | 2 | + // +----+-----+-------+------+----------+----------+ + var connectResponse [4]byte + if _, err = io.ReadFull(proxyConn, connectResponse[:]); err != nil { + fmt.Printf("failed to read connect server response: %v", err) + return nil, fmt.Errorf("failed to read connect server response: %w", err) } - if header[0] != 5 { - return nil, fmt.Errorf("invalid protocol version %v. Expected 5", header[0]) + + if connectResponse[0] != 5 { + return nil, fmt.Errorf("invalid protocol version %v. Expected 5", connectResponse[0]) } - // Check reply code (REP) - if header[1] != 0 { - return nil, ReplyCode(header[1]) + if connectResponse[1] != 0 { + return nil, ReplyCode(connectResponse[1]) } - toRead := 0 - switch header[3] { + // 4. Read and ignore the BND.ADDR and BND.PORT + var bndAddrLen int + switch connectResponse[3] { case addrTypeIPv4: - toRead = 4 + bndAddrLen = 4 case addrTypeIPv6: - toRead = 16 + bndAddrLen = 16 case addrTypeDomainName: - _, err := io.ReadFull(proxyConn, header[:1]) + var lengthByte [1]byte + _, err := io.ReadFull(proxyConn, lengthByte[:]) if err != nil { return nil, fmt.Errorf("failed to read address length in connect response: %w", err) } - toRead = int(header[0]) + bndAddrLen = int(lengthByte[0]) + default: + return nil, fmt.Errorf("invalid address type %v", connectResponse[3]) } - // Reads the bound address and port, but we currently ignore them. + // 5. Reads the bound address and port, but we currently ignore them. // TODO(fortuna): Should we expose the remote bound address as the net.Conn.LocalAddr()? - _, err = io.ReadFull(proxyConn, header[:toRead]) - if err != nil { - return nil, fmt.Errorf("failed to read address in connect response: %w", err) + bndAddr := make([]byte, bndAddrLen) + if _, err = io.ReadFull(proxyConn, bndAddr); err != nil { + return nil, fmt.Errorf("failed to read bound address: %w", err) } // We also ignore the remote bound port number. - _, err = io.ReadFull(proxyConn, header[:2]) - if err != nil { - return nil, fmt.Errorf("failed to read port number in connect response: %w", err) + // Read the port (2 bytes) + var bndPort [2]byte + if _, err = io.ReadFull(proxyConn, bndPort[:]); err != nil { + return nil, fmt.Errorf("failed to read bound port: %w", err) } dialSuccess = true From d07696ea184a10e186bae46c5241d8bf9a508f58 Mon Sep 17 00:00:00 2001 From: amir gh Date: Sun, 25 Feb 2024 13:18:55 -0800 Subject: [PATCH 04/32] add auth tests with local server --- transport/socks5/stream_dialer_test.go | 81 ++++++++++++++++++++++++-- 1 file changed, 77 insertions(+), 4 deletions(-) diff --git a/transport/socks5/stream_dialer_test.go b/transport/socks5/stream_dialer_test.go index e79fdcac..869df449 100644 --- a/transport/socks5/stream_dialer_test.go +++ b/transport/socks5/stream_dialer_test.go @@ -23,20 +23,22 @@ import ( "sync" "testing" "testing/iotest" + "time" "github.com/Jigsaw-Code/outline-sdk/transport" + "github.com/armon/go-socks5" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestSOCKS5Dialer_NewStreamDialerNil(t *testing.T) { - dialer, err := NewStreamDialer(nil) + dialer, err := NewStreamDialer(nil, nil) require.Nil(t, dialer) require.Error(t, err) } func TestSOCKS5Dialer_BadConnection(t *testing.T) { - dialer, err := NewStreamDialer(&transport.TCPEndpoint{Address: "127.0.0.0:0"}) + dialer, err := NewStreamDialer(&transport.TCPEndpoint{Address: "127.0.0.0:0"}, nil) require.NotNil(t, dialer) require.NoError(t, err) _, err = dialer.DialStream(context.Background(), "example.com:443") @@ -48,7 +50,7 @@ func TestSOCKS5Dialer_BadAddress(t *testing.T) { require.NoError(t, err, "Failed to create TCP listener: %v", err) defer listener.Close() - dialer, err := NewStreamDialer(&transport.TCPEndpoint{Address: listener.Addr().String()}) + dialer, err := NewStreamDialer(&transport.TCPEndpoint{Address: listener.Addr().String()}, nil) require.NotNil(t, dialer) require.NoError(t, err) @@ -95,7 +97,7 @@ func testExchange(tb testing.TB, listener *net.TCPListener, destAddr string, req // Client go func() { defer running.Done() - dialer, err := NewStreamDialer(&transport.TCPEndpoint{Address: listener.Addr().String()}) + dialer, err := NewStreamDialer(&transport.TCPEndpoint{Address: listener.Addr().String()}, nil) require.NoError(tb, err) serverConn, err := dialer.DialStream(context.Background(), destAddr) if replyCode != 0 { @@ -164,3 +166,74 @@ func testExchange(tb testing.TB, listener *net.TCPListener, destAddr string, req running.Wait() } + +func TestConnectWithoutAuth(t *testing.T) { + // Create a SOCKS5 server + conf := &socks5.Config{} + server, err := socks5.New(conf) + require.NoError(t, err) + + // Create SOCKS5 proxy on localhost port 8000 + go func() { + err := server.ListenAndServe("tcp", "127.0.0.1:8000") + require.NoError(t, err) + fmt.Println("server is listening") + }() + // wait for server to start + time.Sleep(10 * time.Millisecond) + + // Create a SOCKS5 client + dialer, err := NewStreamDialer(&transport.TCPEndpoint{Address: "127.0.0.1:8000"}, nil) + require.NotNil(t, dialer) + require.NoError(t, err) + _, err = dialer.DialStream(context.Background(), "127.0.0.1:8000") + require.NoError(t, err) +} + +func TestConnectWithAuth(t *testing.T) { + // Create a SOCKS5 server + creds := socks5.StaticCredentials{ + "testusername": "testpassword", + } + + cator := socks5.UserPassAuthenticator{Credentials: creds} + conf := &socks5.Config{ + AuthMethods: []socks5.Authenticator{cator}, + } + + // Create SOCKS5 proxy with configured credentials + server, err := socks5.New(conf) + require.NoError(t, err) + + // Create SOCKS5 proxy on localhost port 8001 + go func() { + err := server.ListenAndServe("tcp", "127.0.0.1:8001") + require.NoError(t, err) + fmt.Println("server is listening") + }() + // wait for server to start + time.Sleep(10 * time.Millisecond) + + // Create a SOCKS5 client + c := Credentials{} + + // Try to connect with correct credentials + err = c.SetUsername("testusername") + require.NoError(t, err) + err = c.SetPassword("testpassword") + require.NoError(t, err) + dialer, err := NewStreamDialer(&transport.TCPEndpoint{Address: "127.0.0.1:8001"}, &c) + require.NotNil(t, dialer) + require.NoError(t, err) + _, err = dialer.DialStream(context.Background(), "127.0.0.1:8001") + require.NoError(t, err) + + // Try to connect with incorrect credentials + err = c.SetPassword("wrongpassword") + require.NoError(t, err) + dialer, err = NewStreamDialer(&transport.TCPEndpoint{Address: "127.0.0.1:8001"}, &c) + require.NotNil(t, dialer) + require.NoError(t, err) + _, err = dialer.DialStream(context.Background(), "127.0.0.1:8001") + require.Error(t, err) +} From 98a6f58d553e4d9c12a221185981385d0eff0fad Mon Sep 17 00:00:00 2001 From: amir gh Date: Sun, 25 Feb 2024 13:19:27 -0800 Subject: [PATCH 05/32] include go-socks5 server implementation for tests --- go.mod | 1 + go.sum | 2 ++ 2 files changed, 3 insertions(+) diff --git a/go.mod b/go.mod index 5ff0f143..1aa85490 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/Jigsaw-Code/outline-sdk go 1.20 require ( + github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 github.com/eycorsican/go-tun2socks v1.16.11 github.com/google/gopacket v1.1.19 github.com/shadowsocks/go-shadowsocks2 v0.1.5 diff --git a/go.sum b/go.sum index f1f3f92a..3ca81bd0 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= From 3760c9edf333d1c27e644111e6fd6cfc1fb5212a Mon Sep 17 00:00:00 2001 From: amir gh Date: Sun, 25 Feb 2024 13:24:40 -0800 Subject: [PATCH 06/32] add rfc1929 as reference --- transport/socks5/stream_dialer.go | 1 + 1 file changed, 1 insertion(+) diff --git a/transport/socks5/stream_dialer.go b/transport/socks5/stream_dialer.go index c30f7e0e..954c3d63 100644 --- a/transport/socks5/stream_dialer.go +++ b/transport/socks5/stream_dialer.go @@ -23,6 +23,7 @@ import ( "github.com/Jigsaw-Code/outline-sdk/transport" ) +// https://datatracker.ietf.org/doc/html/rfc1929 type Credentials struct { username []byte password []byte From f81426323e9cd689e3b54611a672af5e2400cd06 Mon Sep 17 00:00:00 2001 From: Amir Gh <117060873+amircybersec@users.noreply.github.com> Date: Sun, 10 Mar 2024 23:28:05 -0700 Subject: [PATCH 07/32] Update transport/socks5/stream_dialer.go Co-authored-by: Vinicius Fortuna --- transport/socks5/stream_dialer.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/transport/socks5/stream_dialer.go b/transport/socks5/stream_dialer.go index 954c3d63..8590ac2d 100644 --- a/transport/socks5/stream_dialer.go +++ b/transport/socks5/stream_dialer.go @@ -112,11 +112,13 @@ func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (trans // +----+------+----------+------+----------+ // | 1 | 1 | 1 to 255 | 1 | 1 to 255 | // +----+------+----------+------+----------+ - buffer = append(buffer, 1) // Auth version - buffer = append(buffer, byte(len(c.credentials.username))) - buffer = append(buffer, c.credentials.username...) - buffer = append(buffer, byte(len(c.credentials.password))) - buffer = append(buffer, c.credentials.password...) + buffer = append(buffer, + 1, + byte(len(c.credentials.username)), + c.credentials.username..., + byte(len(c.credentials.password)), + c.credentials.password... + ) } // Connect request part: VER = 5, CMD = 1 (connect), RSV = 0, DST.ADDR, DST.PORT From bedec1d6b0a7b1ecc51eebf53cf02d28a957774d Mon Sep 17 00:00:00 2001 From: amir gh Date: Sat, 16 Mar 2024 23:22:29 -0700 Subject: [PATCH 08/32] buffer allocation with array for faster mem access --- transport/socks5/stream_dialer.go | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/transport/socks5/stream_dialer.go b/transport/socks5/stream_dialer.go index 8590ac2d..8f9079f6 100644 --- a/transport/socks5/stream_dialer.go +++ b/transport/socks5/stream_dialer.go @@ -98,12 +98,17 @@ func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (trans // +----+----------+----------+ // | 1 | 1 | 1 to 255 | // +----+----------+----------+ - header := [3 + 3 + 256 + 2]byte{} + // Method selection part: VER = 5, NMETHODS = 1, METHODS = 0 (no auth) + header := [3]byte{} buffer = append(header[:0], 5, 1, 0) } else { + // header array is allocated to the maximum size of the buffer + // The maximum size of the buffer is + // 3 (1 socks version + 1 method selection + 1 methods) + // + 1 (auth version) + 1 (username length) + 255 (username) + 1 (password length) + 255 (password) + header := [3 + 1 + 1 + 255 + 1 + 255]byte{} // https://datatracker.ietf.org/doc/html/rfc1929 // Method selection part: VER = 5, NMETHODS = 1, METHODS = 2 (username/password) - header := [3 + 3 + 255 + 255 + 3 + 256 + 2]byte{} buffer = append(header[:0], 5, 1, 2) // Authentication part: VER = 1, ULEN, UNAME, PLEN, PASSWD @@ -112,13 +117,12 @@ func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (trans // +----+------+----------+------+----------+ // | 1 | 1 | 1 to 255 | 1 | 1 to 255 | // +----+------+----------+------+----------+ - buffer = append(buffer, - 1, - byte(len(c.credentials.username)), - c.credentials.username..., - byte(len(c.credentials.password)), - c.credentials.password... - ) + // Authentication part: VER = 1, ULEN, UNAME, PLEN, PASSWD + buffer = append(buffer, 1) // Auth version + buffer = append(buffer, byte(len(c.credentials.username))) + buffer = append(buffer, c.credentials.username...) + buffer = append(buffer, byte(len(c.credentials.password))) + buffer = append(buffer, c.credentials.password...) } // Connect request part: VER = 5, CMD = 1 (connect), RSV = 0, DST.ADDR, DST.PORT From 786db9484a703b20c94991a8bd9525d18ae96258 Mon Sep 17 00:00:00 2001 From: amir gh Date: Mon, 18 Mar 2024 08:56:44 -0700 Subject: [PATCH 09/32] defining error type --- transport/socks5/socks5.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/transport/socks5/socks5.go b/transport/socks5/socks5.go index 2623cd1b..f95fde59 100644 --- a/transport/socks5/socks5.go +++ b/transport/socks5/socks5.go @@ -37,6 +37,10 @@ const ( ErrAddressTypeNotSupported = ReplyCode(0x08) ) +const ( + authVersionMismatch = "unknown authentication version" +) + var _ error = (ReplyCode)(0) // Error returns a human-readable description of the error, based on the SOCKS5 RFC. @@ -113,3 +117,9 @@ func appendSOCKS5Address(b []byte, address string) ([]byte, error) { b = binary.BigEndian.AppendUint16(b, uint16(portNum)) return b, nil } + +type authVersionError byte + +func (e authVersionError) Error() string { + return fmt.Sprintf("%s: %d. Expected %d", authVersionMismatch, byte(e), authVersion) +} From 5ea56239895c8a15570992be616642f5f2e016b9 Mon Sep 17 00:00:00 2001 From: amir gh Date: Mon, 18 Mar 2024 08:57:04 -0700 Subject: [PATCH 10/32] using NewCredentials in tests --- transport/socks5/stream_dialer_test.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/transport/socks5/stream_dialer_test.go b/transport/socks5/stream_dialer_test.go index 869df449..c1d1d5c9 100644 --- a/transport/socks5/stream_dialer_test.go +++ b/transport/socks5/stream_dialer_test.go @@ -218,10 +218,8 @@ func TestConnectWithAuth(t *testing.T) { c := Credentials{} // Try to connect with correct credentials - err = c.SetUsername("testusername") - require.NoError(t, err) - err = c.SetPassword("testpassword") - require.NoError(t, err) + c, err = NewCredentials("testusername", "testpassword") + dialer, err := NewStreamDialer(&transport.TCPEndpoint{Address: "127.0.0.1:8001"}, &c) require.NotNil(t, dialer) require.NoError(t, err) @@ -229,7 +227,7 @@ func TestConnectWithAuth(t *testing.T) { require.NoError(t, err) // Try to connect with incorrect credentials - err = c.SetPassword("wrongpassword") + c, err = NewCredentials("testusername", "wrongpassword") require.NoError(t, err) dialer, err = NewStreamDialer(&transport.TCPEndpoint{Address: "127.0.0.1:8001"}, &c) require.NotNil(t, dialer) From 92aab9eddae56efbfba354bb0aca8dd069076b0f Mon Sep 17 00:00:00 2001 From: amir gh Date: Mon, 18 Mar 2024 08:57:41 -0700 Subject: [PATCH 11/32] using buffer array for memory efficiency --- transport/socks5/stream_dialer.go | 173 +++++++++++++++++++----------- 1 file changed, 111 insertions(+), 62 deletions(-) diff --git a/transport/socks5/stream_dialer.go b/transport/socks5/stream_dialer.go index 8f9079f6..a9a6fdf0 100644 --- a/transport/socks5/stream_dialer.go +++ b/transport/socks5/stream_dialer.go @@ -23,33 +23,63 @@ import ( "github.com/Jigsaw-Code/outline-sdk/transport" ) +const ( + maxCredentialLength = 255 + minCredentialLength = 0x01 + socksProtocolVer = 0x05 + noAuthMethod = 0 + userPassAuthMethod = 2 + numberOfAuthMethods = 1 + authVersion = 1 + connectCommand = 1 + rsv = 0 + authSuccess = 0 + addrLengthIPv4 = 4 + addrLengthIPv6 = 16 + bufferSize = 3 + 1 + 1 + 255 + 1 + 255 +) + // https://datatracker.ietf.org/doc/html/rfc1929 +// Credentials can be nil, and that means no authentication. type Credentials struct { username []byte password []byte } +func NewCredentials(username, password string) (Credentials, error) { + var c Credentials + if err := c.setUsername(username); err != nil { + return c, err + } + if err := c.setPassword(password); err != nil { + return c, err + } + return c, nil +} + // SetUsername sets the username field, ensuring it doesn't exceed 255 bytes in length and is at least 1 byte. -func (c *Credentials) SetUsername(username string) error { - if len([]byte(username)) > 255 { +func (c *Credentials) setUsername(username string) error { + usernameBytes := []byte(username) + if len(usernameBytes) > maxCredentialLength { return errors.New("username exceeds 255 bytes") } - if len([]byte(username)) < 1 { + if len(usernameBytes) < minCredentialLength { return errors.New("username must be at least 1 byte") } - c.username = []byte(username) + c.username = usernameBytes return nil } // SetPassword sets the password field, ensuring it doesn't exceed 255 bytes in length and is at least 1 byte. -func (c *Credentials) SetPassword(password string) error { - if len([]byte(password)) > 255 { +func (c *Credentials) setPassword(password string) error { + passwordBytes := []byte(password) + if len(passwordBytes) > maxCredentialLength { return errors.New("password exceeds 255 bytes") } - if len([]byte(password)) < 1 { + if len(passwordBytes) < minCredentialLength { return errors.New("password must be at least 1 byte") } - c.password = []byte(password) + c.password = passwordBytes return nil } @@ -70,7 +100,8 @@ type streamDialer struct { var _ transport.StreamDialer = (*streamDialer)(nil) // DialStream implements [transport.StreamDialer].DialStream using SOCKS5. -// It will send the auth method, sub-negotiation, and the connect requests in one packet, to avoid an unnecessary roundtrip. +// It will send the auth method, auth credentials (if auth is chosen), and +// the connect requests in one packet, to avoid an additional roundtrip. // The returned [error] will be of type [ReplyCode] if the server sends a SOCKS error reply code, which // you can check against the error constants in this package using [errors.Is]. func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (transport.StreamConn, error) { @@ -88,8 +119,11 @@ func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (trans // For protocol details, see https://datatracker.ietf.org/doc/html/rfc1928#section-3 // Creating a single buffer for method selection, authentication, and connection request // Buffer large enough for method, auth, and connect requests with a domain name address. + // The maximum size of the buffer is + // 3 (1 socks version + 1 method selection + 1 methods) + // + 1 (auth version) + 1 (username length) + 255 (username) + 1 (password length) + 255 (password) - var buffer []byte + var buffer [bufferSize]byte if c.credentials == nil { // Method selection part: VER = 5, NMETHODS = 1, METHODS = 0 (no auth) @@ -98,18 +132,19 @@ func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (trans // +----+----------+----------+ // | 1 | 1 | 1 to 255 | // +----+----------+----------+ - // Method selection part: VER = 5, NMETHODS = 1, METHODS = 0 (no auth) - header := [3]byte{} - buffer = append(header[:0], 5, 1, 0) + buffer[0] = socksProtocolVer + buffer[1] = numberOfAuthMethods + buffer[2] = noAuthMethod + if _, err := proxyConn.Write(buffer[:3]); err != nil { + return nil, fmt.Errorf("failed to write method selection: %w", err) + } } else { - // header array is allocated to the maximum size of the buffer - // The maximum size of the buffer is - // 3 (1 socks version + 1 method selection + 1 methods) - // + 1 (auth version) + 1 (username length) + 255 (username) + 1 (password length) + 255 (password) - header := [3 + 1 + 1 + 255 + 1 + 255]byte{} // https://datatracker.ietf.org/doc/html/rfc1929 // Method selection part: VER = 5, NMETHODS = 1, METHODS = 2 (username/password) - buffer = append(header[:0], 5, 1, 2) + buffer[0] = socksProtocolVer + buffer[1] = numberOfAuthMethods + buffer[2] = userPassAuthMethod + offset := 3 // Authentication part: VER = 1, ULEN, UNAME, PLEN, PASSWD // +----+------+----------+------+----------+ @@ -118,69 +153,83 @@ func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (trans // | 1 | 1 | 1 to 255 | 1 | 1 to 255 | // +----+------+----------+------+----------+ // Authentication part: VER = 1, ULEN, UNAME, PLEN, PASSWD - buffer = append(buffer, 1) // Auth version - buffer = append(buffer, byte(len(c.credentials.username))) - buffer = append(buffer, c.credentials.username...) - buffer = append(buffer, byte(len(c.credentials.password))) - buffer = append(buffer, c.credentials.password...) + buffer[offset] = authVersion + offset++ + buffer[offset] = byte(len(c.credentials.username)) + offset++ + copy(buffer[offset:], c.credentials.username) + offset += len(c.credentials.username) + buffer[offset] = byte(len(c.credentials.password)) + offset++ + copy(buffer[offset:], c.credentials.password) + offset += len(c.credentials.password) + + if _, err := proxyConn.Write(buffer[:offset]); err != nil { + return nil, fmt.Errorf("failed to write method selection and authentication: %w", err) + } } - // Connect request part: VER = 5, CMD = 1 (connect), RSV = 0, DST.ADDR, DST.PORT + // Connect request: + // VER = 5, CMD = 1 (connect), RSV = 0, DST.ADDR, DST.PORT // +----+-----+-------+------+----------+----------+ // |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT | // +----+-----+-------+------+----------+----------+ // | 1 | 1 | X'00' | 1 | Variable | 2 | // +----+-----+-------+------+----------+----------+ - connectRequest, err := appendSOCKS5Address([]byte{5, 1, 0}, remoteAddr) + buffer[0] = socksProtocolVer + buffer[1] = connectCommand + buffer[2] = rsv + connectRequest, err := appendSOCKS5Address(buffer[:3], remoteAddr) if err != nil { return nil, fmt.Errorf("failed to create SOCKS5 address: %w", err) } - buffer = append(buffer, connectRequest...) - // Sending the combined request - _, err = proxyConn.Write(buffer) + // We merge the method and connect requests and only perform one write + // because we send a single authentication method, so there's no point + // in waiting for the response. This eliminates a roundtrip. + _, err = proxyConn.Write(connectRequest) if err != nil { return nil, fmt.Errorf("failed to write combined SOCKS5 request: %w", err) } - // Read several response parts in one go, to avoid an unnecessary roundtrip. + // Reading the response: // 1. Read method response (VER, METHOD). // +----+--------+ // |VER | METHOD | // +----+--------+ // | 1 | 1 | // +----+--------+ - var methodResponse [2]byte - if _, err = io.ReadFull(proxyConn, methodResponse[:]); err != nil { + if _, err = io.ReadFull(proxyConn, buffer[:2]); err != nil { return nil, fmt.Errorf("failed to read method server response") } - if methodResponse[0] != 5 { - return nil, fmt.Errorf("invalid protocol version %v. Expected 5", methodResponse[0]) + if buffer[0] != socksProtocolVer { + return nil, fmt.Errorf("invalid protocol version %v. Expected 5", buffer[0]) } - if methodResponse[1] == 2 { - // 2. Read sub-negotiation version and status + if buffer[1] == userPassAuthMethod { + // 2. Read authentication version and status // VER = 1, STATUS = 0 // +----+--------+ // |VER | STATUS | // +----+--------+ // | 1 | 1 | // +----+--------+ - var subNegotiation [2]byte - if _, err = io.ReadFull(proxyConn, subNegotiation[:]); err != nil { + // VER = 1 means the server should be expecting username/password authentication. + // var subNegotiation [2]byte + if _, err = io.ReadFull(proxyConn, buffer[2:4]); err != nil { return nil, fmt.Errorf("failed to read sub-negotiation version and status: %w", err) } - if subNegotiation[0] != 1 { - return nil, fmt.Errorf("unkown sub-negotioation version") + if buffer[2] != authVersion { + return nil, authVersionError(buffer[2]) } - if subNegotiation[1] != 0 { - return nil, fmt.Errorf("authentication failed: %v", subNegotiation[1]) + if buffer[3] != authSuccess { + return nil, fmt.Errorf("authentication failed: %v", buffer[3]) } } // Check if the server supports the authentication method we sent. // 0 is no auth, 2 is username/password - if methodResponse[1] != 0 && methodResponse[1] != 2 { - return nil, fmt.Errorf("unsupported SOCKS authentication method %v. Expected 2", methodResponse[1]) - } + // if buffer[1] != 0 && buffer[1] != 2 { + // return nil, fmt.Errorf("unsupported SOCKS authentication method %v. Expected 2", buffer[1]) + // } // 3. Read connect response (VER, REP, RSV, ATYP, BND.ADDR, BND.PORT). // See https://datatracker.ietf.org/doc/html/rfc1928#section-6. // +----+-----+-------+------+----------+----------+ @@ -188,47 +237,47 @@ func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (trans // +----+-----+-------+------+----------+----------+ // | 1 | 1 | X'00' | 1 | Variable | 2 | // +----+-----+-------+------+----------+----------+ - var connectResponse [4]byte - if _, err = io.ReadFull(proxyConn, connectResponse[:]); err != nil { + //var connectResponse [4]byte + if _, err = io.ReadFull(proxyConn, buffer[4:8]); err != nil { fmt.Printf("failed to read connect server response: %v", err) return nil, fmt.Errorf("failed to read connect server response: %w", err) } - if connectResponse[0] != 5 { - return nil, fmt.Errorf("invalid protocol version %v. Expected 5", connectResponse[0]) + if buffer[4] != socksProtocolVer { + return nil, fmt.Errorf("invalid protocol version %v. Expected 5", buffer[4]) } - if connectResponse[1] != 0 { - return nil, ReplyCode(connectResponse[1]) + if buffer[5] != 0 { + return nil, ReplyCode(buffer[5]) } // 4. Read and ignore the BND.ADDR and BND.PORT var bndAddrLen int - switch connectResponse[3] { + switch buffer[7] { case addrTypeIPv4: - bndAddrLen = 4 + bndAddrLen = addrLengthIPv4 case addrTypeIPv6: - bndAddrLen = 16 + bndAddrLen = addrLengthIPv6 case addrTypeDomainName: - var lengthByte [1]byte - _, err := io.ReadFull(proxyConn, lengthByte[:]) + //var lengthByte [1]byte + _, err := io.ReadFull(proxyConn, buffer[9:10]) if err != nil { return nil, fmt.Errorf("failed to read address length in connect response: %w", err) } - bndAddrLen = int(lengthByte[0]) + bndAddrLen = int(buffer[9]) default: - return nil, fmt.Errorf("invalid address type %v", connectResponse[3]) + return nil, fmt.Errorf("invalid address type %v", buffer[7]) } // 5. Reads the bound address and port, but we currently ignore them. // TODO(fortuna): Should we expose the remote bound address as the net.Conn.LocalAddr()? - bndAddr := make([]byte, bndAddrLen) - if _, err = io.ReadFull(proxyConn, bndAddr); err != nil { + //bndAddr := make([]byte, bndAddrLen) + if _, err = io.ReadFull(proxyConn, buffer[8:8+bndAddrLen]); err != nil { return nil, fmt.Errorf("failed to read bound address: %w", err) } // We also ignore the remote bound port number. // Read the port (2 bytes) - var bndPort [2]byte - if _, err = io.ReadFull(proxyConn, bndPort[:]); err != nil { + //var bndPort [2]byte + if _, err = io.ReadFull(proxyConn, buffer[9+bndAddrLen:11+bndAddrLen]); err != nil { return nil, fmt.Errorf("failed to read bound port: %w", err) } From b062bc304dfdb1f7349737e7d0245c723353c6ec Mon Sep 17 00:00:00 2001 From: amir gh Date: Mon, 18 Mar 2024 17:17:30 -0700 Subject: [PATCH 12/32] use constants + more readable --- transport/socks5/stream_dialer.go | 70 +++++++++++++++++-------------- 1 file changed, 39 insertions(+), 31 deletions(-) diff --git a/transport/socks5/stream_dialer.go b/transport/socks5/stream_dialer.go index a9a6fdf0..e77dd1cb 100644 --- a/transport/socks5/stream_dialer.go +++ b/transport/socks5/stream_dialer.go @@ -24,19 +24,19 @@ import ( ) const ( - maxCredentialLength = 255 - minCredentialLength = 0x01 - socksProtocolVer = 0x05 - noAuthMethod = 0 - userPassAuthMethod = 2 - numberOfAuthMethods = 1 - authVersion = 1 - connectCommand = 1 - rsv = 0 - authSuccess = 0 - addrLengthIPv4 = 4 - addrLengthIPv6 = 16 - bufferSize = 3 + 1 + 1 + 255 + 1 + 255 + maxCredentialLength = 255 + minCredentialLength = 0x01 + socksProtocolVer = 0x05 + noAuthMethod = 0x00 + userPassAuthMethod = 0x02 + numberOfAuthMethods = 0x01 + authVersion = 0x01 + connectCommand = 0x01 + rsv = 0x00 + authSuccess = 0x00 + addrLengthIPv4 int = 4 + addrLengthIPv6 int = 16 + bufferSize int = 3 + 1 + 1 + 255 + 1 + 255 ) // https://datatracker.ietf.org/doc/html/rfc1929 @@ -124,6 +124,7 @@ func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (trans // + 1 (auth version) + 1 (username length) + 255 (username) + 1 (password length) + 255 (password) var buffer [bufferSize]byte + var offset int if c.credentials == nil { // Method selection part: VER = 5, NMETHODS = 1, METHODS = 0 (no auth) @@ -135,16 +136,17 @@ func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (trans buffer[0] = socksProtocolVer buffer[1] = numberOfAuthMethods buffer[2] = noAuthMethod - if _, err := proxyConn.Write(buffer[:3]); err != nil { - return nil, fmt.Errorf("failed to write method selection: %w", err) - } + offset = 3 + // if _, err := proxyConn.Write(buffer[:3]); err != nil { + // return nil, fmt.Errorf("failed to write method selection: %w", err) + // } } else { // https://datatracker.ietf.org/doc/html/rfc1929 // Method selection part: VER = 5, NMETHODS = 1, METHODS = 2 (username/password) buffer[0] = socksProtocolVer buffer[1] = numberOfAuthMethods buffer[2] = userPassAuthMethod - offset := 3 + offset = 3 // Authentication part: VER = 1, ULEN, UNAME, PLEN, PASSWD // +----+------+----------+------+----------+ @@ -164,9 +166,9 @@ func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (trans copy(buffer[offset:], c.credentials.password) offset += len(c.credentials.password) - if _, err := proxyConn.Write(buffer[:offset]); err != nil { - return nil, fmt.Errorf("failed to write method selection and authentication: %w", err) - } + // if _, err := proxyConn.Write(buffer[:offset]); err != nil { + // return nil, fmt.Errorf("failed to write method selection and authentication: %w", err) + // } } // Connect request: @@ -176,10 +178,14 @@ func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (trans // +----+-----+-------+------+----------+----------+ // | 1 | 1 | X'00' | 1 | Variable | 2 | // +----+-----+-------+------+----------+----------+ - buffer[0] = socksProtocolVer - buffer[1] = connectCommand - buffer[2] = rsv - connectRequest, err := appendSOCKS5Address(buffer[:3], remoteAddr) + buffer[offset] = socksProtocolVer + offset++ + buffer[offset] = connectCommand + offset++ + buffer[offset] = rsv + offset++ + // Probably more memory efficient if remoteAddr is added to the buffer directly.: + connectRequest, err := appendSOCKS5Address(buffer[:offset], remoteAddr) if err != nil { return nil, fmt.Errorf("failed to create SOCKS5 address: %w", err) } @@ -199,6 +205,8 @@ func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (trans // +----+--------+ // | 1 | 1 | // +----+--------+ + // Reuse buffer for better performance. + // buffer[0]: VER, buffer[1]: METHOD if _, err = io.ReadFull(proxyConn, buffer[:2]); err != nil { return nil, fmt.Errorf("failed to read method server response") } @@ -214,7 +222,7 @@ func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (trans // | 1 | 1 | // +----+--------+ // VER = 1 means the server should be expecting username/password authentication. - // var subNegotiation [2]byte + // buffer[2]: VER, buffer[3]: STATUS if _, err = io.ReadFull(proxyConn, buffer[2:4]); err != nil { return nil, fmt.Errorf("failed to read sub-negotiation version and status: %w", err) } @@ -237,7 +245,7 @@ func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (trans // +----+-----+-------+------+----------+----------+ // | 1 | 1 | X'00' | 1 | Variable | 2 | // +----+-----+-------+------+----------+----------+ - //var connectResponse [4]byte + // buffer[4]: VER, buffer[5]: REP, buffer[6]: RSV, buffer[7]: ATYP if _, err = io.ReadFull(proxyConn, buffer[4:8]); err != nil { fmt.Printf("failed to read connect server response: %v", err) return nil, fmt.Errorf("failed to read connect server response: %w", err) @@ -251,7 +259,7 @@ func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (trans return nil, ReplyCode(buffer[5]) } - // 4. Read and ignore the BND.ADDR and BND.PORT + // 4. Read address and length var bndAddrLen int switch buffer[7] { case addrTypeIPv4: @@ -259,12 +267,12 @@ func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (trans case addrTypeIPv6: bndAddrLen = addrLengthIPv6 case addrTypeDomainName: - //var lengthByte [1]byte - _, err := io.ReadFull(proxyConn, buffer[9:10]) + // buffer[8]: length of the domain name + _, err := io.ReadFull(proxyConn, buffer[8:9]) if err != nil { return nil, fmt.Errorf("failed to read address length in connect response: %w", err) } - bndAddrLen = int(buffer[9]) + bndAddrLen = int(buffer[8]) default: return nil, fmt.Errorf("invalid address type %v", buffer[7]) } @@ -274,7 +282,7 @@ func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (trans if _, err = io.ReadFull(proxyConn, buffer[8:8+bndAddrLen]); err != nil { return nil, fmt.Errorf("failed to read bound address: %w", err) } - // We also ignore the remote bound port number. + // We read but ignore the remote bound port number: BND.PORT // Read the port (2 bytes) //var bndPort [2]byte if _, err = io.ReadFull(proxyConn, buffer[9+bndAddrLen:11+bndAddrLen]); err != nil { From 67bbf184cccd9cc351627a446469374d9fb95292 Mon Sep 17 00:00:00 2001 From: amir gh Date: Mon, 18 Mar 2024 17:31:25 -0700 Subject: [PATCH 13/32] use switch case for auth method --- transport/socks5/stream_dialer.go | 41 ++++++++++++++----------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/transport/socks5/stream_dialer.go b/transport/socks5/stream_dialer.go index e77dd1cb..7efea5e5 100644 --- a/transport/socks5/stream_dialer.go +++ b/transport/socks5/stream_dialer.go @@ -23,6 +23,9 @@ import ( "github.com/Jigsaw-Code/outline-sdk/transport" ) +// bufferSize: The maximum buffer size is +// 3 (1 socks version + 1 method selection + 1 methods) +// + 1 (auth version) + 1 (username length) + 255 (username) + 1 (password length) + 255 (password) const ( maxCredentialLength = 255 minCredentialLength = 0x01 @@ -119,10 +122,6 @@ func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (trans // For protocol details, see https://datatracker.ietf.org/doc/html/rfc1928#section-3 // Creating a single buffer for method selection, authentication, and connection request // Buffer large enough for method, auth, and connect requests with a domain name address. - // The maximum size of the buffer is - // 3 (1 socks version + 1 method selection + 1 methods) - // + 1 (auth version) + 1 (username length) + 255 (username) + 1 (password length) + 255 (password) - var buffer [bufferSize]byte var offset int @@ -137,9 +136,6 @@ func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (trans buffer[1] = numberOfAuthMethods buffer[2] = noAuthMethod offset = 3 - // if _, err := proxyConn.Write(buffer[:3]); err != nil { - // return nil, fmt.Errorf("failed to write method selection: %w", err) - // } } else { // https://datatracker.ietf.org/doc/html/rfc1929 // Method selection part: VER = 5, NMETHODS = 1, METHODS = 2 (username/password) @@ -148,13 +144,12 @@ func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (trans buffer[2] = userPassAuthMethod offset = 3 - // Authentication part: VER = 1, ULEN, UNAME, PLEN, PASSWD + // Authentication part: VER = 1, ULEN = 1, UNAME = 1~255, PLEN = 1, PASSWD = 1~255 // +----+------+----------+------+----------+ // |VER | ULEN | UNAME | PLEN | PASSWD | // +----+------+----------+------+----------+ // | 1 | 1 | 1 to 255 | 1 | 1 to 255 | // +----+------+----------+------+----------+ - // Authentication part: VER = 1, ULEN, UNAME, PLEN, PASSWD buffer[offset] = authVersion offset++ buffer[offset] = byte(len(c.credentials.username)) @@ -166,9 +161,6 @@ func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (trans copy(buffer[offset:], c.credentials.password) offset += len(c.credentials.password) - // if _, err := proxyConn.Write(buffer[:offset]); err != nil { - // return nil, fmt.Errorf("failed to write method selection and authentication: %w", err) - // } } // Connect request: @@ -184,7 +176,7 @@ func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (trans offset++ buffer[offset] = rsv offset++ - // Probably more memory efficient if remoteAddr is added to the buffer directly.: + // TODO: Probably more memory efficient if remoteAddr is added to the buffer directly. connectRequest, err := appendSOCKS5Address(buffer[:offset], remoteAddr) if err != nil { return nil, fmt.Errorf("failed to create SOCKS5 address: %w", err) @@ -205,15 +197,19 @@ func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (trans // +----+--------+ // | 1 | 1 | // +----+--------+ - // Reuse buffer for better performance. // buffer[0]: VER, buffer[1]: METHOD + // Reuse buffer for better performance. if _, err = io.ReadFull(proxyConn, buffer[:2]); err != nil { return nil, fmt.Errorf("failed to read method server response") } if buffer[0] != socksProtocolVer { return nil, fmt.Errorf("invalid protocol version %v. Expected 5", buffer[0]) } - if buffer[1] == userPassAuthMethod { + + switch buffer[1] { + case noAuthMethod: + // No authentication required. + case userPassAuthMethod: // 2. Read authentication version and status // VER = 1, STATUS = 0 // +----+--------+ @@ -224,7 +220,7 @@ func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (trans // VER = 1 means the server should be expecting username/password authentication. // buffer[2]: VER, buffer[3]: STATUS if _, err = io.ReadFull(proxyConn, buffer[2:4]); err != nil { - return nil, fmt.Errorf("failed to read sub-negotiation version and status: %w", err) + return nil, fmt.Errorf("failed to read authentication version and status: %w", err) } if buffer[2] != authVersion { return nil, authVersionError(buffer[2]) @@ -232,12 +228,10 @@ func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (trans if buffer[3] != authSuccess { return nil, fmt.Errorf("authentication failed: %v", buffer[3]) } + default: + return nil, fmt.Errorf("unsupported SOCKS authentication method %v. Expected 2", buffer[1]) } - // Check if the server supports the authentication method we sent. - // 0 is no auth, 2 is username/password - // if buffer[1] != 0 && buffer[1] != 2 { - // return nil, fmt.Errorf("unsupported SOCKS authentication method %v. Expected 2", buffer[1]) - // } + // 3. Read connect response (VER, REP, RSV, ATYP, BND.ADDR, BND.PORT). // See https://datatracker.ietf.org/doc/html/rfc1928#section-6. // +----+-----+-------+------+----------+----------+ @@ -245,7 +239,10 @@ func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (trans // +----+-----+-------+------+----------+----------+ // | 1 | 1 | X'00' | 1 | Variable | 2 | // +----+-----+-------+------+----------+----------+ - // buffer[4]: VER, buffer[5]: REP, buffer[6]: RSV, buffer[7]: ATYP + // buffer[4]: VER + // buffer[5]: REP + // buffer[6]: RSV + // buffer[7]: ATYP if _, err = io.ReadFull(proxyConn, buffer[4:8]); err != nil { fmt.Printf("failed to read connect server response: %v", err) return nil, fmt.Errorf("failed to read connect server response: %w", err) From ca3b5627a21c69feec0522cb42810aaff440cd16 Mon Sep 17 00:00:00 2001 From: amir gh Date: Mon, 18 Mar 2024 20:01:17 -0700 Subject: [PATCH 14/32] making code more readable --- transport/socks5/stream_dialer.go | 72 +++++++++++-------------------- 1 file changed, 26 insertions(+), 46 deletions(-) diff --git a/transport/socks5/stream_dialer.go b/transport/socks5/stream_dialer.go index 7efea5e5..9b361475 100644 --- a/transport/socks5/stream_dialer.go +++ b/transport/socks5/stream_dialer.go @@ -37,6 +37,7 @@ const ( connectCommand = 0x01 rsv = 0x00 authSuccess = 0x00 + connectSuccess = 0x00 addrLengthIPv4 int = 4 addrLengthIPv6 int = 16 bufferSize int = 3 + 1 + 1 + 255 + 1 + 255 @@ -123,7 +124,7 @@ func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (trans // Creating a single buffer for method selection, authentication, and connection request // Buffer large enough for method, auth, and connect requests with a domain name address. var buffer [bufferSize]byte - var offset int + var b []byte if c.credentials == nil { // Method selection part: VER = 5, NMETHODS = 1, METHODS = 0 (no auth) @@ -132,17 +133,11 @@ func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (trans // +----+----------+----------+ // | 1 | 1 | 1 to 255 | // +----+----------+----------+ - buffer[0] = socksProtocolVer - buffer[1] = numberOfAuthMethods - buffer[2] = noAuthMethod - offset = 3 + b = append(buffer[:0], socksProtocolVer, numberOfAuthMethods, noAuthMethod) } else { // https://datatracker.ietf.org/doc/html/rfc1929 // Method selection part: VER = 5, NMETHODS = 1, METHODS = 2 (username/password) - buffer[0] = socksProtocolVer - buffer[1] = numberOfAuthMethods - buffer[2] = userPassAuthMethod - offset = 3 + b = append(buffer[:0], socksProtocolVer, numberOfAuthMethods, userPassAuthMethod) // Authentication part: VER = 1, ULEN = 1, UNAME = 1~255, PLEN = 1, PASSWD = 1~255 // +----+------+----------+------+----------+ @@ -150,17 +145,11 @@ func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (trans // +----+------+----------+------+----------+ // | 1 | 1 | 1 to 255 | 1 | 1 to 255 | // +----+------+----------+------+----------+ - buffer[offset] = authVersion - offset++ - buffer[offset] = byte(len(c.credentials.username)) - offset++ - copy(buffer[offset:], c.credentials.username) - offset += len(c.credentials.username) - buffer[offset] = byte(len(c.credentials.password)) - offset++ - copy(buffer[offset:], c.credentials.password) - offset += len(c.credentials.password) - + b = append(b, authVersion) + b = append(b, byte(len(c.credentials.username))) + b = append(b, c.credentials.username...) + b = append(b, byte(len(c.credentials.password))) + b = append(b, c.credentials.password...) } // Connect request: @@ -170,14 +159,9 @@ func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (trans // +----+-----+-------+------+----------+----------+ // | 1 | 1 | X'00' | 1 | Variable | 2 | // +----+-----+-------+------+----------+----------+ - buffer[offset] = socksProtocolVer - offset++ - buffer[offset] = connectCommand - offset++ - buffer[offset] = rsv - offset++ + b = append(b, socksProtocolVer, connectCommand, rsv) // TODO: Probably more memory efficient if remoteAddr is added to the buffer directly. - connectRequest, err := appendSOCKS5Address(buffer[:offset], remoteAddr) + connectRequest, err := appendSOCKS5Address(b, remoteAddr) if err != nil { return nil, fmt.Errorf("failed to create SOCKS5 address: %w", err) } @@ -239,53 +223,49 @@ func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (trans // +----+-----+-------+------+----------+----------+ // | 1 | 1 | X'00' | 1 | Variable | 2 | // +----+-----+-------+------+----------+----------+ - // buffer[4]: VER - // buffer[5]: REP - // buffer[6]: RSV - // buffer[7]: ATYP - if _, err = io.ReadFull(proxyConn, buffer[4:8]); err != nil { + // buffer[0]: VER + // buffer[1]: REP + // buffer[2]: RSV + // buffer[3]: ATYP + if _, err = io.ReadFull(proxyConn, buffer[:4]); err != nil { fmt.Printf("failed to read connect server response: %v", err) return nil, fmt.Errorf("failed to read connect server response: %w", err) } - if buffer[4] != socksProtocolVer { - return nil, fmt.Errorf("invalid protocol version %v. Expected 5", buffer[4]) + if buffer[0] != socksProtocolVer { + return nil, fmt.Errorf("invalid protocol version %v. Expected 5", buffer[0]) } - if buffer[5] != 0 { - return nil, ReplyCode(buffer[5]) + if buffer[1] != connectSuccess { + return nil, ReplyCode(buffer[1]) } // 4. Read address and length var bndAddrLen int - switch buffer[7] { + switch buffer[3] { case addrTypeIPv4: bndAddrLen = addrLengthIPv4 case addrTypeIPv6: bndAddrLen = addrLengthIPv6 case addrTypeDomainName: // buffer[8]: length of the domain name - _, err := io.ReadFull(proxyConn, buffer[8:9]) + _, err := io.ReadFull(proxyConn, buffer[:1]) if err != nil { return nil, fmt.Errorf("failed to read address length in connect response: %w", err) } - bndAddrLen = int(buffer[8]) + bndAddrLen = int(buffer[0]) default: - return nil, fmt.Errorf("invalid address type %v", buffer[7]) + return nil, fmt.Errorf("invalid address type %v", buffer[3]) } // 5. Reads the bound address and port, but we currently ignore them. // TODO(fortuna): Should we expose the remote bound address as the net.Conn.LocalAddr()? - //bndAddr := make([]byte, bndAddrLen) - if _, err = io.ReadFull(proxyConn, buffer[8:8+bndAddrLen]); err != nil { + if _, err := io.ReadFull(proxyConn, buffer[:bndAddrLen]); err != nil { return nil, fmt.Errorf("failed to read bound address: %w", err) } // We read but ignore the remote bound port number: BND.PORT - // Read the port (2 bytes) - //var bndPort [2]byte - if _, err = io.ReadFull(proxyConn, buffer[9+bndAddrLen:11+bndAddrLen]); err != nil { + if _, err = io.ReadFull(proxyConn, buffer[:2]); err != nil { return nil, fmt.Errorf("failed to read bound port: %w", err) } - dialSuccess = true return proxyConn, nil } From d3302c3c5c4b27e7b0fad77fd838cd0131820213 Mon Sep 17 00:00:00 2001 From: amir gh Date: Mon, 18 Mar 2024 22:20:20 -0700 Subject: [PATCH 15/32] using things-go/go-socks5 package instead --- transport/socks5/stream_dialer_test.go | 27 +++++++++++--------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/transport/socks5/stream_dialer_test.go b/transport/socks5/stream_dialer_test.go index c1d1d5c9..f17ce661 100644 --- a/transport/socks5/stream_dialer_test.go +++ b/transport/socks5/stream_dialer_test.go @@ -26,9 +26,9 @@ import ( "time" "github.com/Jigsaw-Code/outline-sdk/transport" - "github.com/armon/go-socks5" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/things-go/go-socks5" ) func TestSOCKS5Dialer_NewStreamDialerNil(t *testing.T) { @@ -169,9 +169,7 @@ func testExchange(tb testing.TB, listener *net.TCPListener, destAddr string, req func TestConnectWithoutAuth(t *testing.T) { // Create a SOCKS5 server - conf := &socks5.Config{} - server, err := socks5.New(conf) - require.NoError(t, err) + server := socks5.NewServer() // Create SOCKS5 proxy on localhost port 8000 go func() { @@ -192,18 +190,14 @@ func TestConnectWithoutAuth(t *testing.T) { func TestConnectWithAuth(t *testing.T) { // Create a SOCKS5 server - creds := socks5.StaticCredentials{ - "testusername": "testpassword", - } - - cator := socks5.UserPassAuthenticator{Credentials: creds} - conf := &socks5.Config{ - AuthMethods: []socks5.Authenticator{cator}, + cator := socks5.UserPassAuthenticator{ + Credentials: socks5.StaticCredentials{ + "testusername": "testpassword", + }, } - - // Create SOCKS5 proxy with configured credentials - server, err := socks5.New(conf) - require.NoError(t, err) + server := socks5.NewServer( + socks5.WithAuthMethods([]socks5.Authenticator{cator}), + ) // Create SOCKS5 proxy on localhost port 8001 go func() { @@ -218,7 +212,8 @@ func TestConnectWithAuth(t *testing.T) { c := Credentials{} // Try to connect with correct credentials - c, err = NewCredentials("testusername", "testpassword") + c, err := NewCredentials("testusername", "testpassword") + require.NoError(t, err) dialer, err := NewStreamDialer(&transport.TCPEndpoint{Address: "127.0.0.1:8001"}, &c) require.NotNil(t, dialer) From e3981593ce66dd6b912ad4a3c222fec1ca880e17 Mon Sep 17 00:00:00 2001 From: amir gh Date: Mon, 18 Mar 2024 22:45:56 -0700 Subject: [PATCH 16/32] updating packages --- go.mod | 10 +++++----- go.sum | 27 ++++++++++----------------- 2 files changed, 15 insertions(+), 22 deletions(-) diff --git a/go.mod b/go.mod index 1aa85490..000ca3cc 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,13 @@ module github.com/Jigsaw-Code/outline-sdk go 1.20 require ( - github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 github.com/eycorsican/go-tun2socks v1.16.11 github.com/google/gopacket v1.1.19 github.com/shadowsocks/go-shadowsocks2 v0.1.5 - github.com/stretchr/testify v1.8.2 - golang.org/x/crypto v0.17.0 - golang.org/x/net v0.19.0 + github.com/stretchr/testify v1.8.4 + github.com/things-go/go-socks5 v0.0.5 + golang.org/x/crypto v0.18.0 + golang.org/x/net v0.20.0 ) require ( @@ -18,7 +18,7 @@ require ( github.com/kr/text v0.2.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect - golang.org/x/sys v0.15.0 // indirect + golang.org/x/sys v0.16.0 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 3ca81bd0..21b77e5a 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,4 @@ -github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= -github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/eycorsican/go-tun2socks v1.16.11 h1:+hJDNgisrYaGEqoSxhdikMgMJ4Ilfwm/IZDrWRrbaH8= @@ -21,31 +18,28 @@ github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstv github.com/shadowsocks/go-shadowsocks2 v0.1.5 h1:PDSQv9y2S85Fl7VBeOMF9StzeXZyK1HakRm86CUbr28= github.com/shadowsocks/go-shadowsocks2 v0.1.5/go.mod h1:AGGpIoek4HRno4xzyFiAtLHkOpcoznZEkAccaI/rplM= github.com/songgao/water v0.0.0-20190725173103-fd331bda3f4b/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/things-go/go-socks5 v0.0.5 h1:qvKaGcBkfDrUL33SchHN93srAmYGzb4CxSM2DPYufe8= +github.com/things-go/go-socks5 v0.0.5/go.mod h1:mtzInf8v5xmsBpHZVbIw2YQYhc4K0jRwzfsH64Uh0IQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191021144547-ec77196f6094/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= @@ -53,6 +47,5 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 45c3958c311f5b76cf8f26267bc2161aad690e34 Mon Sep 17 00:00:00 2001 From: amir gh Date: Mon, 18 Mar 2024 22:59:21 -0700 Subject: [PATCH 17/32] code clean up --- transport/socks5/stream_dialer.go | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/transport/socks5/stream_dialer.go b/transport/socks5/stream_dialer.go index 9b361475..cfc95b75 100644 --- a/transport/socks5/stream_dialer.go +++ b/transport/socks5/stream_dialer.go @@ -27,20 +27,20 @@ import ( // 3 (1 socks version + 1 method selection + 1 methods) // + 1 (auth version) + 1 (username length) + 255 (username) + 1 (password length) + 255 (password) const ( - maxCredentialLength = 255 - minCredentialLength = 0x01 - socksProtocolVer = 0x05 - noAuthMethod = 0x00 - userPassAuthMethod = 0x02 - numberOfAuthMethods = 0x01 - authVersion = 0x01 - connectCommand = 0x01 - rsv = 0x00 - authSuccess = 0x00 - connectSuccess = 0x00 - addrLengthIPv4 int = 4 - addrLengthIPv6 int = 16 - bufferSize int = 3 + 1 + 1 + 255 + 1 + 255 + maxCredentialLength = 255 + minCredentialLength = 1 + socksProtocolVer = 0x05 + noAuthMethod = 0x00 + userPassAuthMethod = 0x02 + numberOfAuthMethods = 0x01 + authVersion = 0x01 + connectCommand = 0x01 + rsv = 0x00 + authSuccess = 0x00 + connectSuccess = 0x00 + addrLengthIPv4 = 4 + addrLengthIPv6 = 16 + bufferSize = (1 + 1 + 1) + (1 + 1 + 255 + 1 + 255) ) // https://datatracker.ietf.org/doc/html/rfc1929 From aad81fdd8825f79951d5be02047699e361b42828 Mon Sep 17 00:00:00 2001 From: amir gh Date: Tue, 19 Mar 2024 10:45:46 -0700 Subject: [PATCH 18/32] reusing buffer for connect request --- transport/socks5/stream_dialer.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/transport/socks5/stream_dialer.go b/transport/socks5/stream_dialer.go index cfc95b75..8af807ed 100644 --- a/transport/socks5/stream_dialer.go +++ b/transport/socks5/stream_dialer.go @@ -40,7 +40,7 @@ const ( connectSuccess = 0x00 addrLengthIPv4 = 4 addrLengthIPv6 = 16 - bufferSize = (1 + 1 + 1) + (1 + 1 + 255 + 1 + 255) + bufferSize = (1 + 1 + 1) + (1 + 1 + 255 + 1 + 255) + 256 ) // https://datatracker.ietf.org/doc/html/rfc1929 @@ -161,7 +161,7 @@ func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (trans // +----+-----+-------+------+----------+----------+ b = append(b, socksProtocolVer, connectCommand, rsv) // TODO: Probably more memory efficient if remoteAddr is added to the buffer directly. - connectRequest, err := appendSOCKS5Address(b, remoteAddr) + b, err = appendSOCKS5Address(b, remoteAddr) if err != nil { return nil, fmt.Errorf("failed to create SOCKS5 address: %w", err) } @@ -169,7 +169,7 @@ func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (trans // We merge the method and connect requests and only perform one write // because we send a single authentication method, so there's no point // in waiting for the response. This eliminates a roundtrip. - _, err = proxyConn.Write(connectRequest) + _, err = proxyConn.Write(b) if err != nil { return nil, fmt.Errorf("failed to write combined SOCKS5 request: %w", err) } From 322572f21349549303ea13c503d5f34ff7bf4cb6 Mon Sep 17 00:00:00 2001 From: amir gh Date: Tue, 19 Mar 2024 10:52:40 -0700 Subject: [PATCH 19/32] reverted error type change --- transport/socks5/socks5.go | 6 ------ transport/socks5/stream_dialer.go | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/transport/socks5/socks5.go b/transport/socks5/socks5.go index f95fde59..96011a5c 100644 --- a/transport/socks5/socks5.go +++ b/transport/socks5/socks5.go @@ -117,9 +117,3 @@ func appendSOCKS5Address(b []byte, address string) ([]byte, error) { b = binary.BigEndian.AppendUint16(b, uint16(portNum)) return b, nil } - -type authVersionError byte - -func (e authVersionError) Error() string { - return fmt.Sprintf("%s: %d. Expected %d", authVersionMismatch, byte(e), authVersion) -} diff --git a/transport/socks5/stream_dialer.go b/transport/socks5/stream_dialer.go index 8af807ed..9f2b7605 100644 --- a/transport/socks5/stream_dialer.go +++ b/transport/socks5/stream_dialer.go @@ -207,7 +207,7 @@ func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (trans return nil, fmt.Errorf("failed to read authentication version and status: %w", err) } if buffer[2] != authVersion { - return nil, authVersionError(buffer[2]) + return nil, fmt.Errorf("invalid authentication version %v. Expected 1", buffer[2]) } if buffer[3] != authSuccess { return nil, fmt.Errorf("authentication failed: %v", buffer[3]) From ecf64a999dde957b85b44cdbaebec7053b548c1c Mon Sep 17 00:00:00 2001 From: amir gh Date: Thu, 21 Mar 2024 14:28:39 -0700 Subject: [PATCH 20/32] moved to protocol enumerations to socks.go --- transport/socks5/socks5.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/transport/socks5/socks5.go b/transport/socks5/socks5.go index 96011a5c..d5a376dc 100644 --- a/transport/socks5/socks5.go +++ b/transport/socks5/socks5.go @@ -38,7 +38,8 @@ const ( ) const ( - authVersionMismatch = "unknown authentication version" + authMethodNoAuth = 0x00 + authMethodUserPass = 0x02 ) var _ error = (ReplyCode)(0) From 2715668c7b50d7bb0a572befae8df8660241ffdb Mon Sep 17 00:00:00 2001 From: amir gh Date: Thu, 21 Mar 2024 14:28:54 -0700 Subject: [PATCH 21/32] using port 0 for dynamic port assignment --- transport/socks5/stream_dialer_test.go | 46 +++++++++++++++++--------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/transport/socks5/stream_dialer_test.go b/transport/socks5/stream_dialer_test.go index f17ce661..0a727bfb 100644 --- a/transport/socks5/stream_dialer_test.go +++ b/transport/socks5/stream_dialer_test.go @@ -20,6 +20,7 @@ import ( "fmt" "io" "net" + "strconv" "sync" "testing" "testing/iotest" @@ -170,21 +171,31 @@ func testExchange(tb testing.TB, listener *net.TCPListener, destAddr string, req func TestConnectWithoutAuth(t *testing.T) { // Create a SOCKS5 server server := socks5.NewServer() + host := "127.0.0.1" + + // Create SOCKS5 proxy on localhost with a random port + listener, err := net.Listen("tcp", net.JoinHostPort(host, "0")) + require.NoError(t, err) + + // Get the selected port number + port := listener.Addr().(*net.TCPAddr).Port - // Create SOCKS5 proxy on localhost port 8000 go func() { - err := server.ListenAndServe("tcp", "127.0.0.1:8000") + err := server.Serve(listener) + defer listener.Close() + fmt.Println("server is listening in the goroutin") require.NoError(t, err) - fmt.Println("server is listening") }() + // wait for server to start time.Sleep(10 * time.Millisecond) // Create a SOCKS5 client - dialer, err := NewStreamDialer(&transport.TCPEndpoint{Address: "127.0.0.1:8000"}, nil) + dialer, err := NewStreamDialer(&transport.TCPEndpoint{Address: net.JoinHostPort(host, strconv.Itoa(port))}, nil) require.NotNil(t, dialer) require.NoError(t, err) - _, err = dialer.DialStream(context.Background(), "127.0.0.1:8000") + + _, err = dialer.DialStream(context.Background(), net.JoinHostPort(host, strconv.Itoa(port))) require.NoError(t, err) } @@ -199,34 +210,39 @@ func TestConnectWithAuth(t *testing.T) { socks5.WithAuthMethods([]socks5.Authenticator{cator}), ) + // Create SOCKS5 proxy on localhost with a random port + host := "127.0.0.1" + listener, err := net.Listen("tcp", net.JoinHostPort(host, "0")) + require.NoError(t, err) + port := listener.Addr().(*net.TCPAddr).Port + // Create SOCKS5 proxy on localhost port 8001 go func() { - err := server.ListenAndServe("tcp", "127.0.0.1:8001") + err := server.Serve(listener) + defer listener.Close() require.NoError(t, err) - fmt.Println("server is listening") }() // wait for server to start time.Sleep(10 * time.Millisecond) - // Create a SOCKS5 client - c := Credentials{} - - // Try to connect with correct credentials + // Create a SOCKS5 credentials c, err := NewCredentials("testusername", "testpassword") require.NoError(t, err) - dialer, err := NewStreamDialer(&transport.TCPEndpoint{Address: "127.0.0.1:8001"}, &c) + address := net.JoinHostPort(host, strconv.Itoa(port)) + + dialer, err := NewStreamDialer(&transport.TCPEndpoint{Address: address}, &c) require.NotNil(t, dialer) require.NoError(t, err) - _, err = dialer.DialStream(context.Background(), "127.0.0.1:8001") + _, err = dialer.DialStream(context.Background(), address) require.NoError(t, err) // Try to connect with incorrect credentials c, err = NewCredentials("testusername", "wrongpassword") require.NoError(t, err) - dialer, err = NewStreamDialer(&transport.TCPEndpoint{Address: "127.0.0.1:8001"}, &c) + dialer, err = NewStreamDialer(&transport.TCPEndpoint{Address: address}, &c) require.NotNil(t, dialer) require.NoError(t, err) - _, err = dialer.DialStream(context.Background(), "127.0.0.1:8001") + _, err = dialer.DialStream(context.Background(), address) require.Error(t, err) } From 9bc31aae074b8a0ef75a63c9e407c9116315ae13 Mon Sep 17 00:00:00 2001 From: amir gh Date: Thu, 21 Mar 2024 14:29:32 -0700 Subject: [PATCH 22/32] using number instead of const where applicable --- transport/socks5/stream_dialer.go | 93 +++++++++++-------------------- 1 file changed, 32 insertions(+), 61 deletions(-) diff --git a/transport/socks5/stream_dialer.go b/transport/socks5/stream_dialer.go index 9f2b7605..bf617a79 100644 --- a/transport/socks5/stream_dialer.go +++ b/transport/socks5/stream_dialer.go @@ -23,26 +23,6 @@ import ( "github.com/Jigsaw-Code/outline-sdk/transport" ) -// bufferSize: The maximum buffer size is -// 3 (1 socks version + 1 method selection + 1 methods) -// + 1 (auth version) + 1 (username length) + 255 (username) + 1 (password length) + 255 (password) -const ( - maxCredentialLength = 255 - minCredentialLength = 1 - socksProtocolVer = 0x05 - noAuthMethod = 0x00 - userPassAuthMethod = 0x02 - numberOfAuthMethods = 0x01 - authVersion = 0x01 - connectCommand = 0x01 - rsv = 0x00 - authSuccess = 0x00 - connectSuccess = 0x00 - addrLengthIPv4 = 4 - addrLengthIPv6 = 16 - bufferSize = (1 + 1 + 1) + (1 + 1 + 255 + 1 + 255) + 256 -) - // https://datatracker.ietf.org/doc/html/rfc1929 // Credentials can be nil, and that means no authentication. type Credentials struct { @@ -52,39 +32,26 @@ type Credentials struct { func NewCredentials(username, password string) (Credentials, error) { var c Credentials - if err := c.setUsername(username); err != nil { - return c, err - } - if err := c.setPassword(password); err != nil { - return c, err - } - return c, nil -} - -// SetUsername sets the username field, ensuring it doesn't exceed 255 bytes in length and is at least 1 byte. -func (c *Credentials) setUsername(username string) error { usernameBytes := []byte(username) - if len(usernameBytes) > maxCredentialLength { - return errors.New("username exceeds 255 bytes") + passwordBytes := []byte(password) + + if len(usernameBytes) > 255 { + return c, errors.New("username exceeds 255 bytes") } - if len(usernameBytes) < minCredentialLength { - return errors.New("username must be at least 1 byte") + if len(usernameBytes) == 0 { + return c, errors.New("username must be at least 1 byte") } c.username = usernameBytes - return nil -} -// SetPassword sets the password field, ensuring it doesn't exceed 255 bytes in length and is at least 1 byte. -func (c *Credentials) setPassword(password string) error { - passwordBytes := []byte(password) - if len(passwordBytes) > maxCredentialLength { - return errors.New("password exceeds 255 bytes") + if len(passwordBytes) > 255 { + return c, errors.New("password exceeds 255 bytes") } - if len(passwordBytes) < minCredentialLength { - return errors.New("password must be at least 1 byte") + if len(passwordBytes) == 0 { + return c, errors.New("password must be at least 1 byte") } c.password = passwordBytes - return nil + + return c, nil } // NewStreamDialer creates a [transport.StreamDialer] that routes connections to a SOCKS5 @@ -123,7 +90,11 @@ func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (trans // For protocol details, see https://datatracker.ietf.org/doc/html/rfc1928#section-3 // Creating a single buffer for method selection, authentication, and connection request // Buffer large enough for method, auth, and connect requests with a domain name address. - var buffer [bufferSize]byte + // The maximum buffer size is: + // 3 (1 socks version + 1 method selection + 1 methods) + // + 1 (auth version) + 1 (username length) + 255 (username) + 1 (password length) + 255 (password) + // + 256 (max domain name length) + var buffer [(1 + 1 + 1) + (1 + 1 + 255 + 1 + 255) + 256]byte var b []byte if c.credentials == nil { @@ -133,11 +104,11 @@ func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (trans // +----+----------+----------+ // | 1 | 1 | 1 to 255 | // +----+----------+----------+ - b = append(buffer[:0], socksProtocolVer, numberOfAuthMethods, noAuthMethod) + b = append(buffer[:0], 5, 1, 0) } else { // https://datatracker.ietf.org/doc/html/rfc1929 // Method selection part: VER = 5, NMETHODS = 1, METHODS = 2 (username/password) - b = append(buffer[:0], socksProtocolVer, numberOfAuthMethods, userPassAuthMethod) + b = append(buffer[:0], 5, 1, authMethodUserPass) // Authentication part: VER = 1, ULEN = 1, UNAME = 1~255, PLEN = 1, PASSWD = 1~255 // +----+------+----------+------+----------+ @@ -145,7 +116,7 @@ func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (trans // +----+------+----------+------+----------+ // | 1 | 1 | 1 to 255 | 1 | 1 to 255 | // +----+------+----------+------+----------+ - b = append(b, authVersion) + b = append(b, 1) b = append(b, byte(len(c.credentials.username))) b = append(b, c.credentials.username...) b = append(b, byte(len(c.credentials.password))) @@ -159,7 +130,7 @@ func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (trans // +----+-----+-------+------+----------+----------+ // | 1 | 1 | X'00' | 1 | Variable | 2 | // +----+-----+-------+------+----------+----------+ - b = append(b, socksProtocolVer, connectCommand, rsv) + b = append(b, 5, 1, 0) // TODO: Probably more memory efficient if remoteAddr is added to the buffer directly. b, err = appendSOCKS5Address(b, remoteAddr) if err != nil { @@ -184,16 +155,16 @@ func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (trans // buffer[0]: VER, buffer[1]: METHOD // Reuse buffer for better performance. if _, err = io.ReadFull(proxyConn, buffer[:2]); err != nil { - return nil, fmt.Errorf("failed to read method server response") + return nil, fmt.Errorf("failed to read method server response: %w", err) } - if buffer[0] != socksProtocolVer { + if buffer[0] != 5 { return nil, fmt.Errorf("invalid protocol version %v. Expected 5", buffer[0]) } switch buffer[1] { - case noAuthMethod: + case authMethodNoAuth: // No authentication required. - case userPassAuthMethod: + case authMethodUserPass: // 2. Read authentication version and status // VER = 1, STATUS = 0 // +----+--------+ @@ -206,10 +177,10 @@ func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (trans if _, err = io.ReadFull(proxyConn, buffer[2:4]); err != nil { return nil, fmt.Errorf("failed to read authentication version and status: %w", err) } - if buffer[2] != authVersion { + if buffer[2] != 1 { return nil, fmt.Errorf("invalid authentication version %v. Expected 1", buffer[2]) } - if buffer[3] != authSuccess { + if buffer[3] != 0 { return nil, fmt.Errorf("authentication failed: %v", buffer[3]) } default: @@ -228,15 +199,15 @@ func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (trans // buffer[2]: RSV // buffer[3]: ATYP if _, err = io.ReadFull(proxyConn, buffer[:4]); err != nil { - fmt.Printf("failed to read connect server response: %v", err) return nil, fmt.Errorf("failed to read connect server response: %w", err) } - if buffer[0] != socksProtocolVer { + if buffer[0] != 5 { return nil, fmt.Errorf("invalid protocol version %v. Expected 5", buffer[0]) } - if buffer[1] != connectSuccess { + // if REP is not 0, it means the server returned an error. + if buffer[1] != 0 { return nil, ReplyCode(buffer[1]) } @@ -244,9 +215,9 @@ func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (trans var bndAddrLen int switch buffer[3] { case addrTypeIPv4: - bndAddrLen = addrLengthIPv4 + bndAddrLen = 4 case addrTypeIPv6: - bndAddrLen = addrLengthIPv6 + bndAddrLen = 16 case addrTypeDomainName: // buffer[8]: length of the domain name _, err := io.ReadFull(proxyConn, buffer[:1]) From c6fcb7cf297151fa45a8f2d6ac04a07a4b1234a3 Mon Sep 17 00:00:00 2001 From: amir gh Date: Thu, 21 Mar 2024 14:58:38 -0700 Subject: [PATCH 23/32] set creds on dialer --- transport/socks5/stream_dialer.go | 22 +++++++++++----------- transport/socks5/stream_dialer_test.go | 16 +++++++++------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/transport/socks5/stream_dialer.go b/transport/socks5/stream_dialer.go index bf617a79..823649bf 100644 --- a/transport/socks5/stream_dialer.go +++ b/transport/socks5/stream_dialer.go @@ -56,26 +56,26 @@ func NewCredentials(username, password string) (Credentials, error) { // NewStreamDialer creates a [transport.StreamDialer] that routes connections to a SOCKS5 // proxy listening at the given [transport.StreamEndpoint]. -func NewStreamDialer(endpoint transport.StreamEndpoint, cred *Credentials) (transport.StreamDialer, error) { +func NewStreamDialer(endpoint transport.StreamEndpoint) (*StreamDialer, error) { if endpoint == nil { return nil, errors.New("argument endpoint must not be nil") } - return &streamDialer{proxyEndpoint: endpoint, credentials: cred}, nil + return &StreamDialer{proxyEndpoint: endpoint, Credentials: nil}, nil } -type streamDialer struct { +type StreamDialer struct { proxyEndpoint transport.StreamEndpoint - credentials *Credentials + Credentials *Credentials } -var _ transport.StreamDialer = (*streamDialer)(nil) +var _ transport.StreamDialer = (*StreamDialer)(nil) // DialStream implements [transport.StreamDialer].DialStream using SOCKS5. // It will send the auth method, auth credentials (if auth is chosen), and // the connect requests in one packet, to avoid an additional roundtrip. // The returned [error] will be of type [ReplyCode] if the server sends a SOCKS error reply code, which // you can check against the error constants in this package using [errors.Is]. -func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (transport.StreamConn, error) { +func (c *StreamDialer) DialStream(ctx context.Context, remoteAddr string) (transport.StreamConn, error) { proxyConn, err := c.proxyEndpoint.ConnectStream(ctx) if err != nil { return nil, fmt.Errorf("could not connect to SOCKS5 proxy: %w", err) @@ -97,7 +97,7 @@ func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (trans var buffer [(1 + 1 + 1) + (1 + 1 + 255 + 1 + 255) + 256]byte var b []byte - if c.credentials == nil { + if c.Credentials == nil { // Method selection part: VER = 5, NMETHODS = 1, METHODS = 0 (no auth) // +----+----------+----------+ // |VER | NMETHODS | METHODS | @@ -117,10 +117,10 @@ func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (trans // | 1 | 1 | 1 to 255 | 1 | 1 to 255 | // +----+------+----------+------+----------+ b = append(b, 1) - b = append(b, byte(len(c.credentials.username))) - b = append(b, c.credentials.username...) - b = append(b, byte(len(c.credentials.password))) - b = append(b, c.credentials.password...) + b = append(b, byte(len(c.Credentials.username))) + b = append(b, c.Credentials.username...) + b = append(b, byte(len(c.Credentials.password))) + b = append(b, c.Credentials.password...) } // Connect request: diff --git a/transport/socks5/stream_dialer_test.go b/transport/socks5/stream_dialer_test.go index 0a727bfb..12210e32 100644 --- a/transport/socks5/stream_dialer_test.go +++ b/transport/socks5/stream_dialer_test.go @@ -33,13 +33,13 @@ import ( ) func TestSOCKS5Dialer_NewStreamDialerNil(t *testing.T) { - dialer, err := NewStreamDialer(nil, nil) + dialer, err := NewStreamDialer(nil) require.Nil(t, dialer) require.Error(t, err) } func TestSOCKS5Dialer_BadConnection(t *testing.T) { - dialer, err := NewStreamDialer(&transport.TCPEndpoint{Address: "127.0.0.0:0"}, nil) + dialer, err := NewStreamDialer(&transport.TCPEndpoint{Address: "127.0.0.0:0"}) require.NotNil(t, dialer) require.NoError(t, err) _, err = dialer.DialStream(context.Background(), "example.com:443") @@ -51,7 +51,7 @@ func TestSOCKS5Dialer_BadAddress(t *testing.T) { require.NoError(t, err, "Failed to create TCP listener: %v", err) defer listener.Close() - dialer, err := NewStreamDialer(&transport.TCPEndpoint{Address: listener.Addr().String()}, nil) + dialer, err := NewStreamDialer(&transport.TCPEndpoint{Address: listener.Addr().String()}) require.NotNil(t, dialer) require.NoError(t, err) @@ -98,7 +98,7 @@ func testExchange(tb testing.TB, listener *net.TCPListener, destAddr string, req // Client go func() { defer running.Done() - dialer, err := NewStreamDialer(&transport.TCPEndpoint{Address: listener.Addr().String()}, nil) + dialer, err := NewStreamDialer(&transport.TCPEndpoint{Address: listener.Addr().String()}) require.NoError(tb, err) serverConn, err := dialer.DialStream(context.Background(), destAddr) if replyCode != 0 { @@ -191,7 +191,7 @@ func TestConnectWithoutAuth(t *testing.T) { time.Sleep(10 * time.Millisecond) // Create a SOCKS5 client - dialer, err := NewStreamDialer(&transport.TCPEndpoint{Address: net.JoinHostPort(host, strconv.Itoa(port))}, nil) + dialer, err := NewStreamDialer(&transport.TCPEndpoint{Address: net.JoinHostPort(host, strconv.Itoa(port))}) require.NotNil(t, dialer) require.NoError(t, err) @@ -231,18 +231,20 @@ func TestConnectWithAuth(t *testing.T) { address := net.JoinHostPort(host, strconv.Itoa(port)) - dialer, err := NewStreamDialer(&transport.TCPEndpoint{Address: address}, &c) + dialer, err := NewStreamDialer(&transport.TCPEndpoint{Address: address}) require.NotNil(t, dialer) require.NoError(t, err) + dialer.Credentials = &c _, err = dialer.DialStream(context.Background(), address) require.NoError(t, err) // Try to connect with incorrect credentials c, err = NewCredentials("testusername", "wrongpassword") require.NoError(t, err) - dialer, err = NewStreamDialer(&transport.TCPEndpoint{Address: address}, &c) + dialer, err = NewStreamDialer(&transport.TCPEndpoint{Address: address}) require.NotNil(t, dialer) require.NoError(t, err) + dialer.Credentials = &c _, err = dialer.DialStream(context.Background(), address) require.Error(t, err) } From 6752581559c63b7978afd9355336b5a10239174a Mon Sep 17 00:00:00 2001 From: Amir Gh <117060873+amircybersec@users.noreply.github.com> Date: Fri, 22 Mar 2024 10:44:26 -0700 Subject: [PATCH 24/32] Update transport/socks5/socks5.go Co-authored-by: Vinicius Fortuna --- transport/socks5/socks5.go | 1 + 1 file changed, 1 insertion(+) diff --git a/transport/socks5/socks5.go b/transport/socks5/socks5.go index d5a376dc..59e2bf06 100644 --- a/transport/socks5/socks5.go +++ b/transport/socks5/socks5.go @@ -37,6 +37,7 @@ const ( ErrAddressTypeNotSupported = ReplyCode(0x08) ) +// SOCKS5 authentication methods, as specified in https://datatracker.ietf.org/doc/html/rfc1928#section-3 const ( authMethodNoAuth = 0x00 authMethodUserPass = 0x02 From b0636a0e5bd268753ae701daa4afe9fa80c31139 Mon Sep 17 00:00:00 2001 From: Amir Gh <117060873+amircybersec@users.noreply.github.com> Date: Fri, 22 Mar 2024 10:44:42 -0700 Subject: [PATCH 25/32] Update transport/socks5/stream_dialer_test.go Co-authored-by: Vinicius Fortuna --- transport/socks5/stream_dialer_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/transport/socks5/stream_dialer_test.go b/transport/socks5/stream_dialer_test.go index 12210e32..a044791d 100644 --- a/transport/socks5/stream_dialer_test.go +++ b/transport/socks5/stream_dialer_test.go @@ -174,7 +174,7 @@ func TestConnectWithoutAuth(t *testing.T) { host := "127.0.0.1" // Create SOCKS5 proxy on localhost with a random port - listener, err := net.Listen("tcp", net.JoinHostPort(host, "0")) + listener, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) // Get the selected port number From 9060a94ca54dbd126376f248fae6d513ef8fb283 Mon Sep 17 00:00:00 2001 From: Amir Gh <117060873+amircybersec@users.noreply.github.com> Date: Fri, 22 Mar 2024 10:45:01 -0700 Subject: [PATCH 26/32] Update transport/socks5/stream_dialer_test.go Co-authored-by: Vinicius Fortuna --- transport/socks5/stream_dialer_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/transport/socks5/stream_dialer_test.go b/transport/socks5/stream_dialer_test.go index a044791d..6cf4aa89 100644 --- a/transport/socks5/stream_dialer_test.go +++ b/transport/socks5/stream_dialer_test.go @@ -191,7 +191,7 @@ func TestConnectWithoutAuth(t *testing.T) { time.Sleep(10 * time.Millisecond) // Create a SOCKS5 client - dialer, err := NewStreamDialer(&transport.TCPEndpoint{Address: net.JoinHostPort(host, strconv.Itoa(port))}) + dialer, err := NewStreamDialer(&transport.TCPEndpoint{Address: listener.Addr().String()) require.NotNil(t, dialer) require.NoError(t, err) From 1196789aff651225d097d793dd5e49b2a5e9c011 Mon Sep 17 00:00:00 2001 From: Amir Gh <117060873+amircybersec@users.noreply.github.com> Date: Fri, 22 Mar 2024 10:45:19 -0700 Subject: [PATCH 27/32] Update transport/socks5/stream_dialer_test.go Co-authored-by: Vinicius Fortuna --- transport/socks5/stream_dialer_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/transport/socks5/stream_dialer_test.go b/transport/socks5/stream_dialer_test.go index 6cf4aa89..592e8235 100644 --- a/transport/socks5/stream_dialer_test.go +++ b/transport/socks5/stream_dialer_test.go @@ -195,7 +195,7 @@ func TestConnectWithoutAuth(t *testing.T) { require.NotNil(t, dialer) require.NoError(t, err) - _, err = dialer.DialStream(context.Background(), net.JoinHostPort(host, strconv.Itoa(port))) + _, err = dialer.DialStream(context.Background(), listener.Addr().String()) require.NoError(t, err) } From 6374dc02b6221c2675e54b49e84e2f288f4ae9dd Mon Sep 17 00:00:00 2001 From: Amir Gh <117060873+amircybersec@users.noreply.github.com> Date: Fri, 22 Mar 2024 10:45:45 -0700 Subject: [PATCH 28/32] Update transport/socks5/stream_dialer_test.go Co-authored-by: Vinicius Fortuna --- transport/socks5/stream_dialer_test.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/transport/socks5/stream_dialer_test.go b/transport/socks5/stream_dialer_test.go index 592e8235..0a7802b1 100644 --- a/transport/socks5/stream_dialer_test.go +++ b/transport/socks5/stream_dialer_test.go @@ -177,9 +177,6 @@ func TestConnectWithoutAuth(t *testing.T) { listener, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) - // Get the selected port number - port := listener.Addr().(*net.TCPAddr).Port - go func() { err := server.Serve(listener) defer listener.Close() From 4ad826602af27fcb9d2226c59773e6a5cd557427 Mon Sep 17 00:00:00 2001 From: amir gh Date: Fri, 22 Mar 2024 11:08:45 -0700 Subject: [PATCH 29/32] made tests more readable --- transport/socks5/stream_dialer_test.go | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/transport/socks5/stream_dialer_test.go b/transport/socks5/stream_dialer_test.go index 0a7802b1..26982daf 100644 --- a/transport/socks5/stream_dialer_test.go +++ b/transport/socks5/stream_dialer_test.go @@ -20,7 +20,6 @@ import ( "fmt" "io" "net" - "strconv" "sync" "testing" "testing/iotest" @@ -171,7 +170,6 @@ func testExchange(tb testing.TB, listener *net.TCPListener, destAddr string, req func TestConnectWithoutAuth(t *testing.T) { // Create a SOCKS5 server server := socks5.NewServer() - host := "127.0.0.1" // Create SOCKS5 proxy on localhost with a random port listener, err := net.Listen("tcp", "127.0.0.1:0") @@ -187,12 +185,14 @@ func TestConnectWithoutAuth(t *testing.T) { // wait for server to start time.Sleep(10 * time.Millisecond) + address := listener.Addr().String() + // Create a SOCKS5 client - dialer, err := NewStreamDialer(&transport.TCPEndpoint{Address: listener.Addr().String()) + dialer, err := NewStreamDialer(&transport.TCPEndpoint{Address: address}) require.NotNil(t, dialer) require.NoError(t, err) - _, err = dialer.DialStream(context.Background(), listener.Addr().String()) + _, err = dialer.DialStream(context.Background(), address) require.NoError(t, err) } @@ -208,10 +208,9 @@ func TestConnectWithAuth(t *testing.T) { ) // Create SOCKS5 proxy on localhost with a random port - host := "127.0.0.1" - listener, err := net.Listen("tcp", net.JoinHostPort(host, "0")) + listener, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) - port := listener.Addr().(*net.TCPAddr).Port + address := listener.Addr().String() // Create SOCKS5 proxy on localhost port 8001 go func() { @@ -226,8 +225,6 @@ func TestConnectWithAuth(t *testing.T) { c, err := NewCredentials("testusername", "testpassword") require.NoError(t, err) - address := net.JoinHostPort(host, strconv.Itoa(port)) - dialer, err := NewStreamDialer(&transport.TCPEndpoint{Address: address}) require.NotNil(t, dialer) require.NoError(t, err) @@ -238,9 +235,6 @@ func TestConnectWithAuth(t *testing.T) { // Try to connect with incorrect credentials c, err = NewCredentials("testusername", "wrongpassword") require.NoError(t, err) - dialer, err = NewStreamDialer(&transport.TCPEndpoint{Address: address}) - require.NotNil(t, dialer) - require.NoError(t, err) dialer.Credentials = &c _, err = dialer.DialStream(context.Background(), address) require.Error(t, err) From bc33a8313cd78a1e752c4d51070160b4229c9947 Mon Sep 17 00:00:00 2001 From: amir gh Date: Fri, 22 Mar 2024 11:14:10 -0700 Subject: [PATCH 30/32] using t.Log in testing --- transport/socks5/stream_dialer_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/transport/socks5/stream_dialer_test.go b/transport/socks5/stream_dialer_test.go index 26982daf..f23d3205 100644 --- a/transport/socks5/stream_dialer_test.go +++ b/transport/socks5/stream_dialer_test.go @@ -178,7 +178,7 @@ func TestConnectWithoutAuth(t *testing.T) { go func() { err := server.Serve(listener) defer listener.Close() - fmt.Println("server is listening in the goroutin") + t.Log("server is listening...") require.NoError(t, err) }() From 13b871a34091fc3462ef76f24f347a21c4904ce5 Mon Sep 17 00:00:00 2001 From: amir gh Date: Fri, 22 Mar 2024 11:54:20 -0700 Subject: [PATCH 31/32] using SetCredentials instead --- transport/socks5/stream_dialer.go | 57 ++++++++++++-------------- transport/socks5/stream_dialer_test.go | 10 ++--- 2 files changed, 29 insertions(+), 38 deletions(-) diff --git a/transport/socks5/stream_dialer.go b/transport/socks5/stream_dialer.go index 823649bf..0826fd77 100644 --- a/transport/socks5/stream_dialer.go +++ b/transport/socks5/stream_dialer.go @@ -30,46 +30,41 @@ type Credentials struct { password []byte } -func NewCredentials(username, password string) (Credentials, error) { - var c Credentials - usernameBytes := []byte(username) - passwordBytes := []byte(password) - - if len(usernameBytes) > 255 { - return c, errors.New("username exceeds 255 bytes") - } - if len(usernameBytes) == 0 { - return c, errors.New("username must be at least 1 byte") - } - c.username = usernameBytes - - if len(passwordBytes) > 255 { - return c, errors.New("password exceeds 255 bytes") - } - if len(passwordBytes) == 0 { - return c, errors.New("password must be at least 1 byte") - } - c.password = passwordBytes - - return c, nil -} - // NewStreamDialer creates a [transport.StreamDialer] that routes connections to a SOCKS5 // proxy listening at the given [transport.StreamEndpoint]. func NewStreamDialer(endpoint transport.StreamEndpoint) (*StreamDialer, error) { if endpoint == nil { return nil, errors.New("argument endpoint must not be nil") } - return &StreamDialer{proxyEndpoint: endpoint, Credentials: nil}, nil + return &StreamDialer{proxyEndpoint: endpoint, credentials: nil}, nil } type StreamDialer struct { proxyEndpoint transport.StreamEndpoint - Credentials *Credentials + credentials *Credentials } var _ transport.StreamDialer = (*StreamDialer)(nil) +func (c *StreamDialer) SetCredentials(username, password []byte) error { + if len(username) > 255 { + return errors.New("username exceeds 255 bytes") + } + if len(username) == 0 { + return errors.New("username must be at least 1 byte") + } + + if len(password) > 255 { + return errors.New("password exceeds 255 bytes") + } + if len(password) == 0 { + return errors.New("password must be at least 1 byte") + } + + c.credentials = &Credentials{username: username, password: password} + return nil +} + // DialStream implements [transport.StreamDialer].DialStream using SOCKS5. // It will send the auth method, auth credentials (if auth is chosen), and // the connect requests in one packet, to avoid an additional roundtrip. @@ -97,7 +92,7 @@ func (c *StreamDialer) DialStream(ctx context.Context, remoteAddr string) (trans var buffer [(1 + 1 + 1) + (1 + 1 + 255 + 1 + 255) + 256]byte var b []byte - if c.Credentials == nil { + if c.credentials == nil { // Method selection part: VER = 5, NMETHODS = 1, METHODS = 0 (no auth) // +----+----------+----------+ // |VER | NMETHODS | METHODS | @@ -117,10 +112,10 @@ func (c *StreamDialer) DialStream(ctx context.Context, remoteAddr string) (trans // | 1 | 1 | 1 to 255 | 1 | 1 to 255 | // +----+------+----------+------+----------+ b = append(b, 1) - b = append(b, byte(len(c.Credentials.username))) - b = append(b, c.Credentials.username...) - b = append(b, byte(len(c.Credentials.password))) - b = append(b, c.Credentials.password...) + b = append(b, byte(len(c.credentials.username))) + b = append(b, c.credentials.username...) + b = append(b, byte(len(c.credentials.password))) + b = append(b, c.credentials.password...) } // Connect request: diff --git a/transport/socks5/stream_dialer_test.go b/transport/socks5/stream_dialer_test.go index f23d3205..6352bd3b 100644 --- a/transport/socks5/stream_dialer_test.go +++ b/transport/socks5/stream_dialer_test.go @@ -221,21 +221,17 @@ func TestConnectWithAuth(t *testing.T) { // wait for server to start time.Sleep(10 * time.Millisecond) - // Create a SOCKS5 credentials - c, err := NewCredentials("testusername", "testpassword") - require.NoError(t, err) - dialer, err := NewStreamDialer(&transport.TCPEndpoint{Address: address}) require.NotNil(t, dialer) require.NoError(t, err) - dialer.Credentials = &c + err = dialer.SetCredentials([]byte("testusername"), []byte("testpassword")) + require.NoError(t, err) _, err = dialer.DialStream(context.Background(), address) require.NoError(t, err) // Try to connect with incorrect credentials - c, err = NewCredentials("testusername", "wrongpassword") + err = dialer.SetCredentials([]byte("testusername"), []byte("wrongpassword")) require.NoError(t, err) - dialer.Credentials = &c _, err = dialer.DialStream(context.Background(), address) require.Error(t, err) } From 9ad7eb28777c99f6e4a9c0a93b1dac1e9e6b8c26 Mon Sep 17 00:00:00 2001 From: amir gh Date: Fri, 22 Mar 2024 16:19:17 -0700 Subject: [PATCH 32/32] made Credentials type private --- transport/socks5/stream_dialer.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/transport/socks5/stream_dialer.go b/transport/socks5/stream_dialer.go index 0826fd77..b1839a7c 100644 --- a/transport/socks5/stream_dialer.go +++ b/transport/socks5/stream_dialer.go @@ -25,7 +25,7 @@ import ( // https://datatracker.ietf.org/doc/html/rfc1929 // Credentials can be nil, and that means no authentication. -type Credentials struct { +type credentials struct { username []byte password []byte } @@ -36,12 +36,12 @@ func NewStreamDialer(endpoint transport.StreamEndpoint) (*StreamDialer, error) { if endpoint == nil { return nil, errors.New("argument endpoint must not be nil") } - return &StreamDialer{proxyEndpoint: endpoint, credentials: nil}, nil + return &StreamDialer{proxyEndpoint: endpoint, cred: nil}, nil } type StreamDialer struct { proxyEndpoint transport.StreamEndpoint - credentials *Credentials + cred *credentials } var _ transport.StreamDialer = (*StreamDialer)(nil) @@ -61,7 +61,7 @@ func (c *StreamDialer) SetCredentials(username, password []byte) error { return errors.New("password must be at least 1 byte") } - c.credentials = &Credentials{username: username, password: password} + c.cred = &credentials{username: username, password: password} return nil } @@ -92,7 +92,7 @@ func (c *StreamDialer) DialStream(ctx context.Context, remoteAddr string) (trans var buffer [(1 + 1 + 1) + (1 + 1 + 255 + 1 + 255) + 256]byte var b []byte - if c.credentials == nil { + if c.cred == nil { // Method selection part: VER = 5, NMETHODS = 1, METHODS = 0 (no auth) // +----+----------+----------+ // |VER | NMETHODS | METHODS | @@ -112,10 +112,10 @@ func (c *StreamDialer) DialStream(ctx context.Context, remoteAddr string) (trans // | 1 | 1 | 1 to 255 | 1 | 1 to 255 | // +----+------+----------+------+----------+ b = append(b, 1) - b = append(b, byte(len(c.credentials.username))) - b = append(b, c.credentials.username...) - b = append(b, byte(len(c.credentials.password))) - b = append(b, c.credentials.password...) + b = append(b, byte(len(c.cred.username))) + b = append(b, c.cred.username...) + b = append(b, byte(len(c.cred.password))) + b = append(b, c.cred.password...) } // Connect request: