Skip to content

Commit

Permalink
Merge pull request #152 from amircybersec/connectivity-error-amir
Browse files Browse the repository at this point in the history
Sanitize transport config and include in report (Alt Approach)
  • Loading branch information
amircybersec authored Jan 13, 2024
2 parents 7294484 + e05ff4b commit bb5a759
Show file tree
Hide file tree
Showing 4 changed files with 219 additions and 3 deletions.
42 changes: 41 additions & 1 deletion x/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func parseConfigPart(oneDialerConfig string) (*url.URL, error) {
if oneDialerConfig == "" {
return nil, errors.New("empty config part")
}
// Make it "<scheme>:" it it's only "<scheme>" to parse as a URL.
// Make it "<scheme>:" if it's only "<scheme>" to parse as a URL.
if !strings.Contains(oneDialerConfig, ":") {
oneDialerConfig += ":"
}
Expand Down Expand Up @@ -177,3 +177,43 @@ func NewPacketListener(transportConfig string) (transport.PacketListener, error)
return nil, fmt.Errorf("config scheme '%v' is not supported", url.Scheme)
}
}

func SanitizeConfig(transportConfig string) (string, error) {
// Do nothing if the config is empty
if transportConfig == "" {
return "", nil
}
// Split the string into parts
parts := strings.Split(transportConfig, "|")

// Iterate through each part
for i, part := range parts {
u, err := parseConfigPart(part)
if err != nil {
return "", fmt.Errorf("failed to parse config part: %w", err)
}
scheme := strings.ToLower(u.Scheme)
switch scheme {
case "ss":
parts[i], _ = sanitizeShadowsocksURL(u)
case "socks5":
parts[i], _ = sanitizeSocks5URL(u)
case "override", "split", "tls", "tlsfrag":
// No sanitization needed
parts[i] = u.String()
default:
parts[i] = scheme + "://UNKNOWN"
}
}
// Join the parts back into a string
return strings.Join(parts, "|"), nil
}

func sanitizeSocks5URL(u *url.URL) (string, error) {
const redactedPlaceholder = "REDACTED"
if u.User != nil {
u.User = url.User(redactedPlaceholder)
return u.String(), nil
}
return u.String(), nil
}
104 changes: 104 additions & 0 deletions x/config/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package config

import (
"encoding/base64"
"testing"

"github.com/stretchr/testify/require"
)

func TestSanitizeConfig(t *testing.T) {
// Test that empty config is accepted.
_, err := SanitizeConfig("")
require.NoError(t, err)

// Test that a invalid cypher is rejected.
sanitizedConfig, err := SanitizeConfig("split:5|ss://[email protected]:1234?prefix=HTTP%2F1.1%20")
require.NoError(t, err)
require.Equal(t, "split:5|ss://ERROR", sanitizedConfig)

// Test that a valid config is accepted and user info is redacted.
sanitizedConfig, err = SanitizeConfig("split:5|ss://Y2hhY2hhMjAtaWV0Zi1wb2x5MTMwNTpLeTUyN2duU3FEVFB3R0JpQ1RxUnlT@example.com:1234?prefix=HTTP%2F1.1%20")
require.NoError(t, err)
require.Equal(t, "split:5|ss://[email protected]:1234?prefix=HTTP%2F1.1%20", sanitizedConfig)

// Test sanitizer with unknown transport.
sanitizedConfig, err = SanitizeConfig("split:5|vless://[email protected]:443?path=%2Fvless&security=tls&encryption=none&alpn=h2&host=sub.hello.com&fp=chrome&type=ws&sni=sub.hello.com#vless-ws-tls-cdn")
require.NoError(t, err)
require.Equal(t, "split:5|vless://UNKNOWN", sanitizedConfig)

// Test sanitizer with transport that don't have user info.
sanitizedConfig, err = SanitizeConfig("split:5|tlsfrag:5")
require.NoError(t, err)
require.Equal(t, "split:5|tlsfrag:5", sanitizedConfig)

// Test sanitization on an unknown transport.
sanitizedConfig, err = SanitizeConfig("transport://hjdbfjhbqfjheqrf")
require.NoError(t, err)
require.Equal(t, "transport://UNKNOWN", sanitizedConfig)

// Test that an invalid config is rejected.
_, err = SanitizeConfig("::hghg")
require.Error(t, err)
}

func TestShowsocksLagacyBase64URL(t *testing.T) {
encoded := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString([]byte("aes-256-gcm:[email protected]:1234?prefix=HTTP%2F1.1%20"))
u, err := parseConfigPart("ss://" + string(encoded) + "#outline-123")
require.NoError(t, err)
config, err := parseShadowsocksLegacyBase64URL(u)
require.Equal(t, "example.com:1234", config.serverAddress)
require.Equal(t, "HTTP/1.1 ", string(config.prefix))
require.NoError(t, err)
}

func TestParseShadowsocksURL(t *testing.T) {
encoded := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString([]byte("aes-256-gcm:[email protected]:1234?prefix=HTTP%2F1.1%20"))
u, err := parseConfigPart("ss://" + string(encoded) + "#outline-123")
require.NoError(t, err)
config, err := parseShadowsocksURL(u)
require.Equal(t, "example.com:1234", config.serverAddress)
require.Equal(t, "HTTP/1.1 ", string(config.prefix))
require.NoError(t, err)

encoded = base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString([]byte("aes-256-gcm:1234567"))
u, err = parseConfigPart("ss://" + string(encoded) + "@example.com:1234?prefix=HTTP%2F1.1%20" + "#outline-123")
require.NoError(t, err)
config, err = parseShadowsocksURL(u)
require.Equal(t, "example.com:1234", config.serverAddress)
require.Equal(t, "HTTP/1.1 ", string(config.prefix))
require.NoError(t, err)
}

func TestSocks5URLSanitization(t *testing.T) {
configString := "socks5://myuser:[email protected]:1080"
sanitizedConfig, err := SanitizeConfig(configString)
require.NoError(t, err)
require.Equal(t, "socks5://[email protected]:1080", sanitizedConfig)
}

func TestParseShadowsocksSIP002URLUnsuccessful(t *testing.T) {
encoded := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString([]byte("aes-256-gcm:[email protected]:1234?prefix=HTTP%2F1.1%20"))
u, err := parseConfigPart("ss://" + string(encoded) + "#outline-123")
require.NoError(t, err)
_, err = parseShadowsocksSIP002URL(u)
require.Error(t, err)
}

func TestParseShadowsocksSIP002URLUnsupportedCypher(t *testing.T) {
configString := "ss://Y2hhY2hhMjAtaWV0Zi1wb2x5MTMwnTpLeTUyN2duU3FEVFB3R0JpQ1RxUnlT@example.com:1234?prefix=HTTP%2F1.1%20"
u, err := parseConfigPart(configString)
require.NoError(t, err)
_, err = parseShadowsocksSIP002URL(u)
require.Error(t, err)
}

func TestParseShadowsocksSIP002URLSuccessful(t *testing.T) {
configString := "ss://Y2hhY2hhMjAtaWV0Zi1wb2x5MTMwNTpLeTUyN2duU3FEVFB3R0JpQ1RxUnlT@example.com:1234?prefix=HTTP%2F1.1%20"
u, err := parseConfigPart(configString)
require.NoError(t, err)
config, err := parseShadowsocksSIP002URL(u)
require.NoError(t, err)
require.Equal(t, "example.com:1234", config.serverAddress)
require.Equal(t, "HTTP/1.1 ", string(config.prefix))
}
68 changes: 68 additions & 0 deletions x/config/shadowsocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,65 @@ type shadowsocksConfig struct {
}

func parseShadowsocksURL(url *url.URL) (*shadowsocksConfig, error) {
// attempt to decode as SIP002 URI format and
// fall back to legacy base64 format if decoding fails
config, err := parseShadowsocksSIP002URL(url)
if err == nil {
return config, nil
}
return parseShadowsocksLegacyBase64URL(url)
}

// parseShadowsocksLegacyBase64URL parses URL based on legacy base64 format:
// https://shadowsocks.org/doc/configs.html#uri-and-qr-code
func parseShadowsocksLegacyBase64URL(url *url.URL) (*shadowsocksConfig, error) {
config := &shadowsocksConfig{}
if url.Host == "" {
return nil, errors.New("host not specified")
}
decoded, err := base64.URLEncoding.WithPadding(base64.NoPadding).DecodeString(url.Host)
if err != nil {
// If decoding fails, return the original url with error
return nil, fmt.Errorf("failed to decode host string [%v]: %w", url.String(), err)
}
var fragment string
if url.Fragment != "" {
fragment = "#" + url.Fragment
} else {
fragment = ""
}
newURL, err := url.Parse(strings.ToLower(url.Scheme) + "://" + string(decoded) + fragment)
if err != nil {
// if parsing fails, return the original url with error
return nil, fmt.Errorf("failed to parse config part: %w", err)
}
// extend this check to see if decoded string contains contains other valid fields
if newURL.User == nil {
return nil, fmt.Errorf("invalid user info: %w", err)
}
cipherInfoBytes := newURL.User.String()
cipherName, secret, found := strings.Cut(string(cipherInfoBytes), ":")
if !found {
return nil, errors.New("invalid cipher info: no ':' separator")
}
config.serverAddress = newURL.Host
config.cryptoKey, err = shadowsocks.NewEncryptionKey(cipherName, secret)
if err != nil {
return nil, fmt.Errorf("failed to create cipher: %w", err)
}
prefixStr := newURL.Query().Get("prefix")
if len(prefixStr) > 0 {
config.prefix, err = parseStringPrefix(prefixStr)
if err != nil {
return nil, fmt.Errorf("failed to parse prefix: %w", err)
}
}
return config, nil
}

// parseShadowsocksSIP002URL parses URL based on SIP002 format:
// https://shadowsocks.org/doc/sip002.html
func parseShadowsocksSIP002URL(url *url.URL) (*shadowsocksConfig, error) {
config := &shadowsocksConfig{}
if url.Host == "" {
return nil, errors.New("host not specified")
Expand Down Expand Up @@ -110,3 +169,12 @@ func parseStringPrefix(utf8Str string) ([]byte, error) {
}
return rawBytes, nil
}

func sanitizeShadowsocksURL(u *url.URL) (string, error) {
const redactedPlaceholder = "REDACTED"
config, err := parseShadowsocksURL(u)
if err != nil {
return "ss://ERROR", err
}
return "ss://" + redactedPlaceholder + "@" + config.serverAddress + "?prefix=" + url.PathEscape(string(config.prefix)), nil
}
8 changes: 6 additions & 2 deletions x/examples/test-connectivity/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ type connectivityReport struct {
Resolver string `json:"resolver"`
Proto string `json:"proto"`
// TODO(fortuna): add sanitized transport config.
// Transport string `json:"transport"`
Transport string `json:"transport"`

// Observations
Time time.Time `json:"time"`
Expand Down Expand Up @@ -193,12 +193,16 @@ func main() {
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: config.SanitizedConfig(*transportFlag),
Transport: sanitizedConfig,
DurationMs: testDuration.Milliseconds(),
Error: makeErrorRecord(result),
}
Expand Down

0 comments on commit bb5a759

Please sign in to comment.