fix(ss): propagate cipher errors and decode SIP002 base64 auth
- Return err instead of nil in SS TCP/UDP connector Init when NewClientConfig fails, preventing nil pointer dereference in WrapConn - Initialize tcpClient in UDP connector for UDP-over-TCP path - Add decodeSIP002Auth to handle ss://BASE64(method:password)@host:port URL format with RawURLEncoding and StdEncoding fallback - Update buildServiceConfig and buildNodeConfig to decode SIP002 auth for ss* schemes before falling back to standard userinfo parsing - Support RawURLEncoding in parseAuthFromCmd with StdEncoding fallback - Add tests for decodeSIP002Auth, parseAuthFromCmd, and integration tests for buildNodeConfig/buildServiceConfig
This commit is contained in:
+55
-9
@@ -469,10 +469,15 @@ func buildServiceConfig(url *url.URL) ([]*config.ServiceConfig, error) {
|
||||
|
||||
var auth *config.AuthConfig
|
||||
if url.User != nil {
|
||||
auth = &config.AuthConfig{
|
||||
Username: url.User.Username(),
|
||||
if strings.HasPrefix(url.Scheme, "ss") {
|
||||
auth = decodeSIP002Auth(url)
|
||||
}
|
||||
if auth == nil {
|
||||
auth = &config.AuthConfig{
|
||||
Username: url.User.Username(),
|
||||
}
|
||||
auth.Password, _ = url.User.Password()
|
||||
}
|
||||
auth.Password, _ = url.User.Password()
|
||||
}
|
||||
|
||||
m := map[string]any{}
|
||||
@@ -614,10 +619,15 @@ func buildNodeConfig(url *url.URL, m map[string]any) (*config.NodeConfig, error)
|
||||
|
||||
var auth *config.AuthConfig
|
||||
if url.User != nil {
|
||||
auth = &config.AuthConfig{
|
||||
Username: url.User.Username(),
|
||||
if strings.HasPrefix(url.Scheme, "ss") {
|
||||
auth = decodeSIP002Auth(url)
|
||||
}
|
||||
if auth == nil {
|
||||
auth = &config.AuthConfig{
|
||||
Username: url.User.Username(),
|
||||
}
|
||||
auth.Password, _ = url.User.Password()
|
||||
}
|
||||
auth.Password, _ = url.User.Password()
|
||||
}
|
||||
|
||||
if sauth := mdutil.GetString(md, "auth"); sauth != "" && auth == nil {
|
||||
@@ -742,10 +752,46 @@ func Norm(s string) (*url.URL, error) {
|
||||
return url, nil
|
||||
}
|
||||
|
||||
func parseAuthFromCmd(sa string) (*config.AuthConfig, error) {
|
||||
v, err := base64.StdEncoding.DecodeString(sa)
|
||||
func decodeSIP002Auth(u *url.URL) *config.AuthConfig {
|
||||
if u.User == nil {
|
||||
return nil
|
||||
}
|
||||
_, hasPassword := u.User.Password()
|
||||
if hasPassword {
|
||||
return nil
|
||||
}
|
||||
username := u.User.Username()
|
||||
if username == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
decoded, err := base64.RawURLEncoding.DecodeString(username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
decoded, err = base64.StdEncoding.DecodeString(username)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
cs := string(decoded)
|
||||
n := strings.IndexByte(cs, ':')
|
||||
if n < 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &config.AuthConfig{
|
||||
Username: cs[:n],
|
||||
Password: cs[n+1:],
|
||||
}
|
||||
}
|
||||
|
||||
func parseAuthFromCmd(sa string) (*config.AuthConfig, error) {
|
||||
v, err := base64.RawURLEncoding.DecodeString(sa)
|
||||
if err != nil {
|
||||
v, err = base64.StdEncoding.DecodeString(sa)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
cs := string(v)
|
||||
n := strings.IndexByte(cs, ':')
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDecodeSIP002Auth(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
rawURL string
|
||||
wantUsername string
|
||||
wantPassword string
|
||||
wantNil bool
|
||||
}{
|
||||
{
|
||||
name: "SIP002 URL-safe base64 without padding",
|
||||
rawURL: "ss://" + base64.RawURLEncoding.EncodeToString([]byte("aes-256-gcm:password")) + "@server:8388",
|
||||
wantUsername: "aes-256-gcm",
|
||||
wantPassword: "password",
|
||||
},
|
||||
{
|
||||
name: "SIP002 standard base64 with padding",
|
||||
rawURL: "ss://" + base64.StdEncoding.EncodeToString([]byte("aes-256-gcm:pass")) + "@server:8388",
|
||||
wantUsername: "aes-256-gcm",
|
||||
wantPassword: "pass",
|
||||
},
|
||||
{
|
||||
name: "standard format with password (not SIP002)",
|
||||
rawURL: "ss://aes-256-gcm:password@server:8388",
|
||||
wantNil: true,
|
||||
},
|
||||
{
|
||||
name: "no userinfo",
|
||||
rawURL: "ss://server:8388",
|
||||
wantNil: true,
|
||||
},
|
||||
{
|
||||
name: "invalid base64",
|
||||
rawURL: "ss://not-valid!!!@server:8388",
|
||||
wantNil: true,
|
||||
},
|
||||
{
|
||||
name: "base64 without colon in decoded",
|
||||
rawURL: "ss://" + base64.RawURLEncoding.EncodeToString([]byte("nocolon")) + "@server:8388",
|
||||
wantNil: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
u, err := url.Parse(tt.rawURL)
|
||||
if err != nil {
|
||||
t.Fatalf("url.Parse(%q): %v", tt.rawURL, err)
|
||||
}
|
||||
|
||||
got := decodeSIP002Auth(u)
|
||||
if tt.wantNil {
|
||||
if got != nil {
|
||||
t.Errorf("decodeSIP002Auth() = %+v, want nil", got)
|
||||
}
|
||||
return
|
||||
}
|
||||
if got == nil {
|
||||
t.Fatal("decodeSIP002Auth() = nil, want non-nil")
|
||||
}
|
||||
if got.Username != tt.wantUsername {
|
||||
t.Errorf("Username = %q, want %q", got.Username, tt.wantUsername)
|
||||
}
|
||||
if got.Password != tt.wantPassword {
|
||||
t.Errorf("Password = %q, want %q", got.Password, tt.wantPassword)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAuthFromCmd(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantUsername string
|
||||
wantPassword string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "StdEncoding with padding",
|
||||
input: base64.StdEncoding.EncodeToString([]byte("user:pass")),
|
||||
wantUsername: "user",
|
||||
wantPassword: "pass",
|
||||
},
|
||||
{
|
||||
name: "RawURLEncoding without padding",
|
||||
input: base64.RawURLEncoding.EncodeToString([]byte("user:pass")),
|
||||
wantUsername: "user",
|
||||
wantPassword: "pass",
|
||||
},
|
||||
{
|
||||
name: "invalid base64",
|
||||
input: "not-valid!!!",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := parseAuthFromCmd(tt.input)
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Error("parseAuthFromCmd() error = nil, want error")
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("parseAuthFromCmd() error = %v", err)
|
||||
}
|
||||
if got.Username != tt.wantUsername {
|
||||
t.Errorf("Username = %q, want %q", got.Username, tt.wantUsername)
|
||||
}
|
||||
if got.Password != tt.wantPassword {
|
||||
t.Errorf("Password = %q, want %q", got.Password, tt.wantPassword)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildNodeConfigSIP002(t *testing.T) {
|
||||
encoded := base64.RawURLEncoding.EncodeToString([]byte("aes-256-gcm:password"))
|
||||
rawURL := "ss://" + encoded + "@server:8388"
|
||||
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
t.Fatalf("url.Parse: %v", err)
|
||||
}
|
||||
|
||||
node, err := buildNodeConfig(u, map[string]any{})
|
||||
if err != nil {
|
||||
t.Fatalf("buildNodeConfig: %v", err)
|
||||
}
|
||||
|
||||
if node.Connector == nil || node.Connector.Auth == nil {
|
||||
t.Fatal("node.Connector.Auth is nil")
|
||||
}
|
||||
if node.Connector.Auth.Username != "aes-256-gcm" {
|
||||
t.Errorf("Username = %q, want %q", node.Connector.Auth.Username, "aes-256-gcm")
|
||||
}
|
||||
if node.Connector.Auth.Password != "password" {
|
||||
t.Errorf("Password = %q, want %q", node.Connector.Auth.Password, "password")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildNodeConfigStandardAuth(t *testing.T) {
|
||||
rawURL := "http://user:pass@server:8080"
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
t.Fatalf("url.Parse: %v", err)
|
||||
}
|
||||
|
||||
node, err := buildNodeConfig(u, map[string]any{})
|
||||
if err != nil {
|
||||
t.Fatalf("buildNodeConfig: %v", err)
|
||||
}
|
||||
|
||||
if node.Connector == nil || node.Connector.Auth == nil {
|
||||
t.Fatal("node.Connector.Auth is nil")
|
||||
}
|
||||
if node.Connector.Auth.Username != "user" {
|
||||
t.Errorf("Username = %q, want %q", node.Connector.Auth.Username, "user")
|
||||
}
|
||||
if node.Connector.Auth.Password != "pass" {
|
||||
t.Errorf("Password = %q, want %q", node.Connector.Auth.Password, "pass")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildServiceConfigSIP002(t *testing.T) {
|
||||
encoded := base64.RawURLEncoding.EncodeToString([]byte("aes-256-gcm:password"))
|
||||
rawURL := "ss://" + encoded + "@:8388"
|
||||
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
t.Fatalf("url.Parse: %v", err)
|
||||
}
|
||||
|
||||
svcs, err := buildServiceConfig(u)
|
||||
if err != nil {
|
||||
t.Fatalf("buildServiceConfig: %v", err)
|
||||
}
|
||||
if len(svcs) == 0 {
|
||||
t.Fatal("no services returned")
|
||||
}
|
||||
|
||||
auth := svcs[0].Handler.Auth
|
||||
if auth == nil {
|
||||
t.Fatal("Handler.Auth is nil")
|
||||
}
|
||||
if auth.Username != "aes-256-gcm" {
|
||||
t.Errorf("Username = %q, want %q", auth.Username, "aes-256-gcm")
|
||||
}
|
||||
if auth.Password != "password" {
|
||||
t.Errorf("Password = %q, want %q", auth.Password, "password")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildServiceConfigStandardAuth(t *testing.T) {
|
||||
rawURL := "http://user:pass@:8080"
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
t.Fatalf("url.Parse: %v", err)
|
||||
}
|
||||
|
||||
svcs, err := buildServiceConfig(u)
|
||||
if err != nil {
|
||||
t.Fatalf("buildServiceConfig: %v", err)
|
||||
}
|
||||
if len(svcs) == 0 {
|
||||
t.Fatal("no services returned")
|
||||
}
|
||||
|
||||
auth := svcs[0].Handler.Auth
|
||||
if auth == nil {
|
||||
t.Fatal("Handler.Auth is nil")
|
||||
}
|
||||
if auth.Username != "user" {
|
||||
t.Errorf("Username = %q, want %q", auth.Username, "user")
|
||||
}
|
||||
if auth.Password != "pass" {
|
||||
t.Errorf("Password = %q, want %q", auth.Password, "pass")
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,7 @@ func (c *ssConnector) Init(md md.Metadata) (err error) {
|
||||
|
||||
clientConfig, err := utils.NewClientConfig(method, password)
|
||||
if err != nil {
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
c.client = core.NewTCPClient(clientConfig)
|
||||
|
||||
@@ -51,9 +51,10 @@ func (c *ssuConnector) Init(md md.Metadata) (err error) {
|
||||
password, _ := c.options.Auth.Password()
|
||||
clientConfig, err := utils.NewClientConfig(method, password)
|
||||
if err != nil {
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
c.client = core.NewUDPClient(clientConfig, 60)
|
||||
c.tcpClient = core.NewTCPClient(clientConfig)
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user