Miro 1 год назад
Родитель
Сommit
27c5d5fa68
49 измененных файлов с 3233 добавлено и 197 удалено
  1. 5 4
      go.mod
  2. 10 6
      go.sum
  3. 19 1
      psiphon/common/parameters/parameters.go
  4. 5 1
      psiphon/common/protocol/packed.go
  5. 1 0
      psiphon/common/protocol/serverEntry.go
  6. 34 0
      psiphon/config.go
  7. 49 0
      psiphon/dialParameters.go
  8. 1 0
      psiphon/server/api.go
  9. 0 1
      psiphon/server/config.go
  10. 32 13
      psiphon/server/server_test.go
  11. 88 8
      psiphon/server/shadowsocks.go
  12. 6 0
      psiphon/serverApi.go
  13. 51 2
      psiphon/shadowsocksConn.go
  14. 201 0
      vendor/github.com/Jigsaw-Code/outline-ss-server/LICENSE
  15. 46 0
      vendor/github.com/Jigsaw-Code/outline-ss-server/net/error.go
  16. 63 0
      vendor/github.com/Jigsaw-Code/outline-ss-server/net/private_net.go
  17. 57 0
      vendor/github.com/Jigsaw-Code/outline-ss-server/net/stream.go
  18. 35 0
      vendor/github.com/Jigsaw-Code/outline-ss-server/service/PROBES.md
  19. 118 0
      vendor/github.com/Jigsaw-Code/outline-ss-server/service/cipher_list.go
  20. 58 0
      vendor/github.com/Jigsaw-Code/outline-ss-server/service/cipher_list_testing.go
  21. 400 0
      vendor/github.com/Jigsaw-Code/outline-ss-server/service/listeners.go
  22. 26 0
      vendor/github.com/Jigsaw-Code/outline-ss-server/service/logger.go
  23. 69 0
      vendor/github.com/Jigsaw-Code/outline-ss-server/service/metrics/metrics.go
  24. 117 0
      vendor/github.com/Jigsaw-Code/outline-ss-server/service/replay.go
  25. 120 0
      vendor/github.com/Jigsaw-Code/outline-ss-server/service/server_salt.go
  26. 189 0
      vendor/github.com/Jigsaw-Code/outline-ss-server/service/shadowsocks.go
  27. 33 0
      vendor/github.com/Jigsaw-Code/outline-ss-server/service/socketopts_linux.go
  28. 395 0
      vendor/github.com/Jigsaw-Code/outline-ss-server/service/tcp.go
  29. 40 0
      vendor/github.com/Jigsaw-Code/outline-ss-server/service/tcp_linux.go
  30. 38 0
      vendor/github.com/Jigsaw-Code/outline-ss-server/service/tcp_other.go
  31. 497 0
      vendor/github.com/Jigsaw-Code/outline-ss-server/service/udp.go
  32. 61 0
      vendor/github.com/Jigsaw-Code/outline-ss-server/service/udp_linux.go
  33. 30 0
      vendor/github.com/Jigsaw-Code/outline-ss-server/service/udp_other.go
  34. 3 0
      vendor/golang.org/x/sync/errgroup/errgroup.go
  35. 0 102
      vendor/golang.org/x/sys/execabs/execabs.go
  36. 0 17
      vendor/golang.org/x/sys/execabs/execabs_go118.go
  37. 0 20
      vendor/golang.org/x/sys/execabs/execabs_go119.go
  38. 1 1
      vendor/golang.org/x/tools/go/packages/external.go
  39. 1 1
      vendor/golang.org/x/tools/go/packages/golist.go
  40. 2 6
      vendor/golang.org/x/tools/go/packages/packages.go
  41. 1 2
      vendor/golang.org/x/tools/internal/gocommand/invoke.go
  42. 0 8
      vendor/golang.org/x/tools/internal/packagesinternal/packages.go
  43. 172 0
      vendor/golang.org/x/tools/internal/versions/gover.go
  44. 19 0
      vendor/golang.org/x/tools/internal/versions/types.go
  45. 20 0
      vendor/golang.org/x/tools/internal/versions/types_go121.go
  46. 24 0
      vendor/golang.org/x/tools/internal/versions/types_go122.go
  47. 49 0
      vendor/golang.org/x/tools/internal/versions/versions_go121.go
  48. 38 0
      vendor/golang.org/x/tools/internal/versions/versions_go122.go
  49. 9 4
      vendor/modules.txt

+ 5 - 4
go.mod

@@ -32,6 +32,7 @@ replace github.com/pion/webrtc/v3 => ./replace/webrtc
 require (
 	filippo.io/edwards25519 v1.1.0
 	github.com/Jigsaw-Code/outline-sdk v0.0.16
+	github.com/Jigsaw-Code/outline-ss-server v1.8.0
 	github.com/Psiphon-Inc/rotate-safe-writer v0.0.0-20210303140923-464a7a37606e
 	github.com/Psiphon-Labs/bolt v0.0.0-20200624191537-23cedaef7ad7
 	github.com/Psiphon-Labs/consistent v0.0.0-20240322131436-20aaa4e05737
@@ -74,7 +75,6 @@ require (
 	github.com/refraction-networking/conjure v0.7.11-0.20240130155008-c8df96195ab2
 	github.com/refraction-networking/gotapdance v1.7.10
 	github.com/ryanuber/go-glob v0.0.0-20170128012129-256dc444b735
-	github.com/shadowsocks/go-shadowsocks2 v0.1.5
 	github.com/shirou/gopsutil/v4 v4.24.5
 	github.com/sirupsen/logrus v1.9.3
 	github.com/stretchr/testify v1.9.0
@@ -83,7 +83,7 @@ require (
 	github.com/wlynxg/anet v0.0.1
 	golang.org/x/crypto v0.22.0
 	golang.org/x/net v0.24.0
-	golang.org/x/sync v0.5.0
+	golang.org/x/sync v0.6.0
 	golang.org/x/sys v0.20.0
 	golang.org/x/term v0.19.0
 	golang.org/x/time v0.5.0
@@ -138,6 +138,7 @@ require (
 	github.com/refraction-networking/obfs4 v0.1.2 // indirect
 	github.com/refraction-networking/utls v1.3.3 // indirect
 	github.com/sergeyfrolov/bsbuffer v0.0.0-20180903213811-94e85abb8507 // indirect
+	github.com/shadowsocks/go-shadowsocks2 v0.1.5 // indirect
 	github.com/shoenig/go-m1cpu v0.1.6 // indirect
 	github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 // indirect
 	github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 // indirect
@@ -151,10 +152,10 @@ require (
 	go.uber.org/mock v0.4.0 // indirect
 	go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect
 	go4.org/netipx v0.0.0-20230824141953-6213f710f925 // indirect
-	golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
+	golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e // indirect
 	golang.org/x/mod v0.14.0 // indirect
 	golang.org/x/text v0.14.0 // indirect
-	golang.org/x/tools v0.15.0 // indirect
+	golang.org/x/tools v0.16.0 // indirect
 	google.golang.org/protobuf v1.31.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )

+ 10 - 6
go.sum

@@ -10,6 +10,8 @@ github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8
 github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
 github.com/Jigsaw-Code/outline-sdk v0.0.16 h1:WbHmv80FKDIpzEmR3GehTbq5CibYTLvcxIIpMMILiEs=
 github.com/Jigsaw-Code/outline-sdk v0.0.16/go.mod h1:e1oQZbSdLJBBuHgfeQsgEkvkuyIePPwstUeZRGq0KO8=
+github.com/Jigsaw-Code/outline-ss-server v1.8.0 h1:6h7CZsyl1vQLz3nvxmL9FbhDug4QxJ1YTxm534eye1E=
+github.com/Jigsaw-Code/outline-ss-server v1.8.0/go.mod h1:slnHH3OZsQmZx/DRKhxvvaGE/8+n3Lkd6363h1ev71E=
 github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
 github.com/Psiphon-Inc/rotate-safe-writer v0.0.0-20210303140923-464a7a37606e h1:NPfqIbzmijrl0VclX2t8eO5EPBhqe47LLGKpRrcVjXk=
@@ -162,6 +164,8 @@ github.com/onsi/ginkgo/v2 v2.12.0 h1:UIVDowFPwpg6yMUpPjGkYvf06K3RAiJXUhCxEwQVHRI
 github.com/onsi/ginkgo/v2 v2.12.0/go.mod h1:ZNEzXISYlqpb8S36iN71ifqLi3vVD1rVJGvWRCJOUpQ=
 github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
 github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
+github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
+github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
 github.com/oschwald/geoip2-golang v1.9.0 h1:uvD3O6fXAXs+usU+UGExshpdP13GAqp4GBrzN7IgKZc=
 github.com/oschwald/geoip2-golang v1.9.0/go.mod h1:BHK6TvDyATVQhKNbQBdrj9eAvuwOMi2zSFXizL3K81Y=
 github.com/oschwald/maxminddb-golang v1.12.0 h1:9FnTOD0YOhP7DGxGsq4glzpGy5+w7pq50AS6wALUMYs=
@@ -307,8 +311,8 @@ golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98y
 golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
 golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
 golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
-golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
-golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
+golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e h1:723BNChdd0c2Wk6WOE320qGBiPtYx0F0Bbm1kriShfE=
+golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
 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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@@ -336,8 +340,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
-golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
+golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -398,8 +402,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
 golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/tools v0.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8=
-golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk=
+golang.org/x/tools v0.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM=
+golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

+ 19 - 1
psiphon/common/parameters/parameters.go

@@ -380,6 +380,10 @@ const (
 	OSSHPrefixSplitMaxDelay                            = "OSSHPrefixSplitMaxDelay"
 	OSSHPrefixEnableFragmentor                         = "OSSHPrefixEnableFragmentor"
 	ServerOSSHPrefixSpecs                              = "ServerOSSHPrefixSpecs"
+	ShadowsocksPrefixSpecs                             = "ShadowsocksPrefixSpecs"
+	ShadowsocksPrefixScopedSpecNames                   = "ShadowsocksPrefixScopedSpecNames"
+	ShadowsocksPrefixProbability                       = "ShadowsocksPrefixProbability"
+	ReplayShadowsocksPrefix                            = "ReplayShadowsocksPrefix"
 	TLSTunnelObfuscatedPSKProbability                  = "TLSTunnelObfuscatedPSKProbability"
 	TLSTunnelTrafficShapingProbability                 = "TLSTunnelTrafficShapingProbability"
 	TLSTunnelMinTLSPadding                             = "TLSTunnelMinTLSPadding"
@@ -748,6 +752,7 @@ var defaultParameters = map[string]struct {
 	ReplayHTTPTransformerParameters:        {value: true},
 	ReplayOSSHSeedTransformerParameters:    {value: true},
 	ReplayOSSHPrefix:                       {value: true},
+	ReplayShadowsocksPrefix:                {value: true},
 	ReplayTLSFragmentClientHello:           {value: true},
 	ReplayInproxyWebRTC:                    {value: true},
 	ReplayInproxySTUN:                      {value: true},
@@ -914,6 +919,10 @@ var defaultParameters = map[string]struct {
 	OSSHPrefixEnableFragmentor: {value: false},
 	ServerOSSHPrefixSpecs:      {value: transforms.Specs{}, flags: serverSideOnly},
 
+	ShadowsocksPrefixSpecs:           {value: transforms.Specs{}},
+	ShadowsocksPrefixScopedSpecNames: {value: transforms.ScopedSpecNames{}},
+	ShadowsocksPrefixProbability:     {value: 0.0, minimum: 0.0},
+
 	// TLSTunnelMinTLSPadding/TLSTunnelMaxTLSPadding are subject to TLS server limitations.
 
 	TLSTunnelObfuscatedPSKProbability:  {value: 0.5, minimum: 0.0},
@@ -1275,6 +1284,13 @@ func (p *Parameters) Set(
 	}
 	osshPrefixSpecs, _ := osshPrefixSpecsValue.(transforms.Specs)
 
+	shadowsocksPrefixSpecsValue, err := getAppliedValue(
+		ShadowsocksPrefixSpecs, parameters, applyParameters)
+	if err != nil {
+		return nil, errors.Trace(err)
+	}
+	shadowsocksPrefixSpecs, _ := shadowsocksPrefixSpecsValue.(transforms.Specs)
+
 	// Special case: in-proxy broker public keys in InproxyBrokerSpecs must
 	// appear in InproxyAllBrokerPublicKeys; and inproxy common compartment
 	// IDs must appear in InproxyAllCommonCompartmentIDs. This check is
@@ -1503,7 +1519,7 @@ func (p *Parameters) Set(
 				}
 
 				prefixMode := false
-				if name == OSSHPrefixSpecs || name == ServerOSSHPrefixSpecs {
+				if name == OSSHPrefixSpecs || name == ServerOSSHPrefixSpecs || name == ShadowsocksPrefixSpecs {
 					prefixMode = true
 				}
 				err := v.Validate(prefixMode)
@@ -1528,6 +1544,8 @@ func (p *Parameters) Set(
 					specs = obfuscatedQuicNonceTransformSpecs
 				} else if name == OSSHPrefixScopedSpecNames {
 					specs = osshPrefixSpecs
+				} else if name == ShadowsocksPrefixScopedSpecNames {
+					specs = shadowsocksPrefixSpecs
 				}
 
 				err := v.Validate(specs)

+ 5 - 1
psiphon/common/protocol/packed.go

@@ -827,7 +827,11 @@ func init() {
 		{165, "inproxy_broker_is_reuse", intConverter},
 		{166, "inproxy_webrtc_use_media_streams", intConverter},
 
-		// Next key value = 167
+		// Specs: server.baseDialParams
+
+		{167, "shadowsocks_prefix", nil},
+
+		// Next key value = 168
 	}
 
 	for _, spec := range packedAPIParameterSpecs {

+ 1 - 0
psiphon/common/protocol/serverEntry.go

@@ -86,6 +86,7 @@ type ServerEntry struct {
 	DisableObfuscatedQUICTransforms     bool     `json:"disableObfuscatedQUICTransforms,omitempty"`
 	DisableOSSHTransforms               bool     `json:"disableOSSHTransforms,omitempty"`
 	DisableOSSHPrefix                   bool     `json:"disableOSSHPrefix,omitempty"`
+	DisableShadowsocksPrefix            bool     `json:"disableShadowsocksPrefix,omitempty"`
 	InproxySessionPublicKey             string   `json:"inproxySessionPublicKey,omitempty"`
 	InproxySessionRootObfuscationSecret string   `json:"inproxySessionRootObfuscationSecret,omitempty"`
 	InproxySSHPort                      int      `json:"inproxySSHPort,omitempty"`

+ 34 - 0
psiphon/config.go

@@ -977,6 +977,11 @@ type Config struct {
 	OSSHPrefixSplitMaxDelayMilliseconds *int
 	OSSHPrefixEnableFragmentor          *bool
 
+	// ShadowsocksPrefix parameters are for testing purposes only.
+	ShadowsocksPrefixSpecs           transforms.Specs
+	ShadowsocksPrefixScopedSpecNames transforms.ScopedSpecNames
+	ShadowsocksPrefixProbability     *float64
+
 	// TLSTunnelTrafficShapingProbability and associated fields are for testing.
 	TLSTunnelObfuscatedPSKProbability  *float64
 	TLSTunnelTrafficShapingProbability *float64
@@ -2497,6 +2502,18 @@ func (config *Config) makeConfigParameters() map[string]interface{} {
 		applyParameters[parameters.OSSHPrefixEnableFragmentor] = *config.OSSHPrefixEnableFragmentor
 	}
 
+	if config.ShadowsocksPrefixSpecs != nil {
+		applyParameters[parameters.ShadowsocksPrefixSpecs] = config.ShadowsocksPrefixSpecs
+	}
+
+	if config.ShadowsocksPrefixScopedSpecNames != nil {
+		applyParameters[parameters.ShadowsocksPrefixScopedSpecNames] = config.ShadowsocksPrefixScopedSpecNames
+	}
+
+	if config.ShadowsocksPrefixProbability != nil {
+		applyParameters[parameters.ShadowsocksPrefixProbability] = *config.ShadowsocksPrefixProbability
+	}
+
 	if config.TLSTunnelObfuscatedPSKProbability != nil {
 		applyParameters[parameters.TLSTunnelObfuscatedPSKProbability] = *config.TLSTunnelObfuscatedPSKProbability
 	}
@@ -3466,6 +3483,23 @@ func (config *Config) setDialParametersHash() {
 		binary.Write(hash, binary.LittleEndian, *config.OSSHPrefixEnableFragmentor)
 	}
 
+	if config.ShadowsocksPrefixSpecs != nil {
+		hash.Write([]byte("ShadowsocksPrefixSpecs"))
+		encodedShadowsocksPrefixSpecs, _ := json.Marshal(config.ShadowsocksPrefixSpecs)
+		hash.Write(encodedShadowsocksPrefixSpecs)
+	}
+
+	if config.ShadowsocksPrefixScopedSpecNames != nil {
+		hash.Write([]byte("ShadowsocksPrefixScopedSpecNames"))
+		encodedShadowsocksPrefixScopedSpecNames, _ := json.Marshal(config.ShadowsocksPrefixScopedSpecNames)
+		hash.Write(encodedShadowsocksPrefixScopedSpecNames)
+	}
+
+	if config.ShadowsocksPrefixProbability != nil {
+		hash.Write([]byte("ShadowsocksPrefixProbability"))
+		binary.Write(hash, binary.LittleEndian, *config.ShadowsocksPrefixProbability)
+	}
+
 	if config.TLSTunnelObfuscatedPSKProbability != nil {
 		hash.Write([]byte("TLSTunnelObfuscatedPSKProbability"))
 		binary.Write(hash, binary.LittleEndian, *config.TLSTunnelObfuscatedPSKProbability)

+ 49 - 0
psiphon/dialParameters.go

@@ -98,6 +98,8 @@ type DialParameters struct {
 	OSSHPrefixSpec        *obfuscator.OSSHPrefixSpec
 	OSSHPrefixSplitConfig *obfuscator.OSSHPrefixSplitConfig
 
+	ShadowsocksPrefixSpec *ShadowsocksPrefixSpec
+
 	FragmentorSeed *prng.Seed
 
 	FrontingProviderID string
@@ -249,6 +251,7 @@ func MakeDialParameters(
 	replayHTTPTransformerParameters := p.Bool(parameters.ReplayHTTPTransformerParameters)
 	replayOSSHSeedTransformerParameters := p.Bool(parameters.ReplayOSSHSeedTransformerParameters)
 	replayOSSHPrefix := p.Bool(parameters.ReplayOSSHPrefix)
+	replayShadowsocksPrefix := p.Bool(parameters.ReplayShadowsocksPrefix)
 	replayInproxySTUN := p.Bool(parameters.ReplayInproxySTUN)
 	replayInproxyWebRTC := p.Bool(parameters.ReplayInproxyWebRTC)
 
@@ -1132,6 +1135,24 @@ func MakeDialParameters(
 
 	}
 
+	if serverEntry.DisableShadowsocksPrefix {
+
+		dialParams.ShadowsocksPrefixSpec = nil
+
+	} else if !isReplay || !replayShadowsocksPrefix {
+
+		prefixSpec, err := makeShadowsocksPrefixSpecParameters(p)
+		if err != nil {
+			return nil, errors.Trace(err)
+		}
+
+		if prefixSpec.Spec != nil {
+			dialParams.ShadowsocksPrefixSpec = prefixSpec
+		} else {
+			dialParams.ShadowsocksPrefixSpec = nil
+		}
+	}
+
 	if protocol.TunnelProtocolUsesMeekHTTP(dialParams.TunnelProtocol) {
 
 		if serverEntry.DisableHTTPTransforms {
@@ -1712,6 +1733,7 @@ func (dialParams *DialParameters) GetShadowsocksConfig() *ShadowsockConfig {
 	return &ShadowsockConfig{
 		dialAddr: dialParams.DirectDialAddress,
 		key:      dialParams.ServerEntry.SshShadowsocksKey,
+		prefix:   dialParams.ShadowsocksPrefixSpec,
 	}
 }
 
@@ -2258,6 +2280,33 @@ func makeOSSHPrefixSplitConfig(p parameters.ParametersAccessor) (*obfuscator.OSS
 	}, nil
 }
 
+func makeShadowsocksPrefixSpecParameters(
+	p parameters.ParametersAccessor) (*ShadowsocksPrefixSpec, error) {
+
+	if !p.WeightedCoinFlip(parameters.ShadowsocksPrefixProbability) {
+		return &ShadowsocksPrefixSpec{}, nil
+	}
+
+	specs := p.ProtocolTransformSpecs(parameters.ShadowsocksPrefixSpecs)
+	scopedSpecNames := p.ProtocolTransformScopedSpecNames(parameters.ShadowsocksPrefixScopedSpecNames)
+
+	name, spec := specs.Select(transforms.SCOPE_ANY, scopedSpecNames)
+
+	if spec == nil {
+		return &ShadowsocksPrefixSpec{}, nil
+	} else {
+		seed, err := prng.NewSeed()
+		if err != nil {
+			return nil, errors.Trace(err)
+		}
+		return &ShadowsocksPrefixSpec{
+			Name: name,
+			Spec: spec,
+			Seed: seed,
+		}, nil
+	}
+}
+
 func selectConjureTransport(
 	p parameters.ParametersAccessor) string {
 

+ 1 - 0
psiphon/server/api.go

@@ -1127,6 +1127,7 @@ var baseDialParams = []requestParamSpec{
 	{"http_transform", isAnyString, requestParamOptional},
 	{"seed_transform", isAnyString, requestParamOptional},
 	{"ossh_prefix", isAnyString, requestParamOptional},
+	{"shadowsocks_prefix", isAnyString, requestParamOptional},
 	{"tls_fragmented", isBooleanFlag, requestParamOptional | requestParamLogFlagAsBool},
 	{"tls_padding", isIntString, requestParamOptional | requestParamLogStringAsInt},
 	{"tls_ossh_sni_server_name", isDomain, requestParamOptional},

+ 0 - 1
psiphon/server/config.go

@@ -973,7 +973,6 @@ func GenerateConfig(params *GenerateConfigParams) ([]byte, []byte, []byte, []byt
 
 	// Shadowsocks config
 
-	// TODO: double check there are enough bytes of entropy
 	shadowsocksKeyBytes, err := common.MakeSecureRandomBytes(SHADOWSOCKS_KEY_BYTE_LENGTH)
 	if err != nil {
 		return nil, nil, nil, nil, nil, errors.Trace(err)

+ 32 - 13
psiphon/server/server_test.go

@@ -213,6 +213,7 @@ func TestShadowsocks(t *testing.T) {
 			doTunneledWebRequest: true,
 			doTunneledNTPRequest: true,
 			doDanglingTCPConn:    true,
+			applyPrefix:          true,
 		})
 }
 
@@ -1478,18 +1479,30 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
 
 		if runConfig.applyPrefix {
 
-			applyParameters[parameters.OSSHPrefixSpecs] = transforms.Specs{
-				"TEST": {{"", "\x00{200}"}},
-			}
-			applyParameters[parameters.OSSHPrefixScopedSpecNames] = transforms.ScopedSpecNames{
-				"": {"TEST"},
-			}
-			applyParameters[parameters.OSSHPrefixProbability] = 1.0
-			applyParameters[parameters.OSSHPrefixSplitMinDelay] = "10ms"
-			applyParameters[parameters.OSSHPrefixSplitMaxDelay] = "20ms"
+			if protocol.TunnelProtocolIsObfuscatedSSH(runConfig.tunnelProtocol) {
+
+				applyParameters[parameters.OSSHPrefixSpecs] = transforms.Specs{
+					"TEST": {{"", "\x00{200}"}},
+				}
+				applyParameters[parameters.OSSHPrefixScopedSpecNames] = transforms.ScopedSpecNames{
+					"": {"TEST"},
+				}
+				applyParameters[parameters.OSSHPrefixProbability] = 1.0
+				applyParameters[parameters.OSSHPrefixSplitMinDelay] = "10ms"
+				applyParameters[parameters.OSSHPrefixSplitMaxDelay] = "20ms"
 
-			applyParameters[parameters.OSSHPrefixEnableFragmentor] = runConfig.forceFragmenting
+				applyParameters[parameters.OSSHPrefixEnableFragmentor] = runConfig.forceFragmenting
 
+			} else if protocol.TunnelProtocolUsesShadowsocks(runConfig.tunnelProtocol) {
+
+				applyParameters[parameters.ShadowsocksPrefixSpecs] = transforms.Specs{
+					"TEST": {{"", "\x00{16}"}},
+				}
+				applyParameters[parameters.ShadowsocksPrefixScopedSpecNames] = transforms.ScopedSpecNames{
+					"": {"TEST"},
+				}
+				applyParameters[parameters.ShadowsocksPrefixProbability] = 1.0
+			}
 		}
 
 		if runConfig.forceFragmenting {
@@ -2016,7 +2029,7 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
 	checkPruneServerEntriesTest(t, runConfig, testDataDirName, pruneServerEntryTestCases)
 
 	// Inspect OSSH prefix flows, if applicable.
-	if runConfig.inspectFlows && runConfig.applyPrefix {
+	if runConfig.inspectFlows && runConfig.applyPrefix && protocol.TunnelProtocolIsObfuscatedSSH(runConfig.tunnelProtocol) {
 
 		flows := <-flowInspectorProxy.ch
 		serverFlows := flows[0]
@@ -2674,8 +2687,14 @@ func checkExpectedServerTunnelLogFields(
 
 	if runConfig.applyPrefix {
 
-		if fields["ossh_prefix"] == nil || fmt.Sprintf("%s", fields["ossh_prefix"]) == "" {
-			return fmt.Errorf("missing expected field 'ossh_prefix'")
+		if protocol.TunnelProtocolIsObfuscatedSSH(runConfig.tunnelProtocol) {
+			if fields["ossh_prefix"] == nil || fmt.Sprintf("%s", fields["ossh_prefix"]) == "" {
+				return fmt.Errorf("missing expected field 'ossh_prefix'")
+			}
+		} else if protocol.TunnelProtocolUsesShadowsocks(runConfig.tunnelProtocol) {
+			if fields["shadowsocks_prefix"] == nil || fmt.Sprintf("%s", fields["shadowsocks_prefix"]) == "" {
+				return fmt.Errorf("missing expected field 'shadowsocks_prefix'")
+			}
 		}
 	}
 

+ 88 - 8
psiphon/server/shadowsocks.go

@@ -20,10 +20,14 @@
 package server
 
 import (
+	"bytes"
+	"fmt"
+	"io"
 	"net"
 
 	"github.com/Jigsaw-Code/outline-sdk/transport"
 	"github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks"
+	"github.com/Jigsaw-Code/outline-ss-server/service"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
 )
@@ -31,9 +35,11 @@ import (
 // ShadowsocksServer tunnels TCP traffic (in the case of Psiphon, SSH traffic)
 // over Shadowsocks.
 type ShadowsocksServer struct {
-	support  *SupportServices
-	listener net.Listener
-	key      *shadowsocks.EncryptionKey
+	support       *SupportServices
+	listener      net.Listener
+	key           *shadowsocks.EncryptionKey
+	saltGenerator service.ServerSaltGenerator
+	replayCache   service.ReplayCache
 }
 
 // ListenShadowsocks returns the listener of a new ShadowsocksServer.
@@ -57,16 +63,22 @@ func NewShadowsocksServer(
 	listener net.Listener,
 	ssEncryptionKey string) (*ShadowsocksServer, error) {
 
-	// TODO: consider using other AEAD ciphers; client cipher needs to match.
+	// Note: client must use the same cipher.
 	key, err := shadowsocks.NewEncryptionKey(shadowsocks.CHACHA20IETFPOLY1305, ssEncryptionKey)
 	if err != nil {
 		return nil, errors.TraceMsg(err, "shadowsocks.NewEncryptionKey failed")
 	}
 
+	// Note: see comment for service.MaxCapacity for a description of
+	// the expected false positive rate.
+	replayHistory := service.MaxCapacity
+
 	shadowsocksServer := &ShadowsocksServer{
-		support:  support,
-		listener: listener,
-		key:      key,
+		support:       support,
+		listener:      listener,
+		key:           key,
+		saltGenerator: service.NewServerSaltGenerator(ssEncryptionKey),
+		replayCache:   service.NewReplayCache(replayHistory),
 	}
 
 	return shadowsocksServer, nil
@@ -94,13 +106,81 @@ func (l *ShadowsocksListener) Accept() (net.Conn, error) {
 		return nil, errors.Trace(err)
 	}
 
-	ssr := shadowsocks.NewReader(conn, l.server.key)
+	salt, reader, err := l.readSalt(conn)
+	if err != nil {
+		return nil, errors.TraceMsg(err, "failed to read salt")
+	}
+
+	// TODO: code mostly copied from [1]; use NewShadowsocksStreamAuthenticator instead?
+	//
+	// [1] https://github.com/Jigsaw-Code/outline-ss-server/blob/fa651d3e87cc0a94104babb3ae85253471a22ebc/service/tcp.go#L138
+
+	// Hardcode key ID because all clients use the same cipher per server,
+	// which is fine because the underlying SSH connection protects the
+	// confidentiality and integrity of client traffic between the client and
+	// server.
+	keyID := "1"
+
+	isServerSalt := l.server.saltGenerator.IsServerSalt(salt)
+
+	if isServerSalt || !l.server.replayCache.Add(keyID, salt) {
+
+		go drainConn(conn)
+
+		if isServerSalt {
+			return nil, errors.TraceNew("server replay detected")
+		}
+		return nil, errors.TraceNew("client replay detected")
+	}
+
+	ssr := shadowsocks.NewReader(reader, l.server.key)
 	ssw := shadowsocks.NewWriter(conn, l.server.key)
+	ssw.SetSaltGenerator(l.server.saltGenerator)
 	ssClientConn := transport.WrapConn(conn.(*net.TCPConn), ssr, ssw)
 
 	return NewShadowsocksConn(ssClientConn, l.server), nil
 }
 
+func drainConn(conn net.Conn) {
+	_, _ = io.Copy(io.Discard, conn)
+	conn.Close()
+}
+
+func (l *ShadowsocksListener) readSalt(conn net.Conn) ([]byte, io.Reader, error) {
+
+	type result struct {
+		salt []byte
+		err  error
+	}
+
+	resultChannel := make(chan result)
+
+	go func() {
+		saltSize := l.server.key.SaltSize()
+		salt := make([]byte, saltSize)
+		if n, err := io.ReadFull(conn, salt); err != nil {
+			resultChannel <- result{
+				err: fmt.Errorf("reading conn failed after %d bytes: %w", n, err),
+			}
+			return
+		}
+
+		resultChannel <- result{
+			salt: salt,
+		}
+	}()
+
+	select {
+	case result := <-resultChannel:
+		if result.err != nil {
+			return nil, nil, result.err
+		}
+		return result.salt, io.MultiReader(bytes.NewReader(result.salt), conn), nil
+	case <-l.server.support.TunnelServer.shutdownBroadcast:
+		return nil, nil, errors.TraceNew("shutdown broadcast")
+	}
+}
+
 // ShadowsocksConn implements the net.Conn and common.MetricsSource interfaces.
 type ShadowsocksConn struct {
 	net.Conn

+ 6 - 0
psiphon/serverApi.go

@@ -1317,6 +1317,12 @@ func getBaseAPIParameters(
 			}
 		}
 
+		if dialParams.ShadowsocksPrefixSpec != nil {
+			if dialParams.ShadowsocksPrefixSpec.Spec != nil {
+				params["shadowsocks_prefix"] = dialParams.ShadowsocksPrefixSpec.Name
+			}
+		}
+
 		if dialParams.DialConnMetrics != nil {
 			metrics := dialParams.DialConnMetrics.GetMetrics()
 			for name, value := range metrics {

+ 51 - 2
psiphon/shadowsocksConn.go

@@ -25,13 +25,17 @@ import (
 
 	"github.com/Jigsaw-Code/outline-sdk/transport"
 	"github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks"
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng"
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/transforms"
 )
 
 // ShadowsockConfig specifies the behavior of a shadowsocksConn.
 type ShadowsockConfig struct {
 	dialAddr string
 	key      string
+	prefix   *ShadowsocksPrefixSpec
 }
 
 // shadowsocksConn is a network connection that tunnels net.Conn flows over Shadowsocks.
@@ -45,7 +49,7 @@ func DialShadowsocksTunnel(
 	shadowsocksConfig *ShadowsockConfig,
 	dialConfig *DialConfig) (*shadowsocksConn, error) {
 
-	// TODO: consider using other AEAD ciphers; server cipher needs to match.
+	// Note: server must use the same cipher.
 	key, err := shadowsocks.NewEncryptionKey(shadowsocks.CHACHA20IETFPOLY1305, shadowsocksConfig.key)
 	if err != nil {
 		return nil, errors.Trace(err)
@@ -57,9 +61,28 @@ func DialShadowsocksTunnel(
 	}
 
 	// Based on shadowsocks.DialStream
-	// TODO: explicitly set SaltGenerator?
 	ssw := shadowsocks.NewWriter(conn, key)
 	ssr := shadowsocks.NewReader(conn, key)
+
+	if shadowsocksConfig.prefix != nil {
+
+		prefix, err := makePrefix(shadowsocksConfig.prefix)
+		if err != nil {
+			return nil, errors.Trace(err)
+		}
+
+		// Prefixes must be <= 16 bytes as longer prefixes risk salt collisions,
+		// which can compromise the security of the connection [1][2].
+		// [1] https://developers.google.com/outline/docs/guides/service-providers/prefixing
+		// [2] See comment for shadowsocks.NewPrefixSaltGenerator
+		//
+		// TODO: consider logging a warning or returning an error if
+		// len(prefix) > 16.
+		if len(prefix) <= 16 {
+			ssw.SetSaltGenerator(shadowsocks.NewPrefixSaltGenerator(prefix))
+		}
+	}
+
 	// TODO: is this cast correct/safe?
 	ssConn := transport.WrapConn(conn.(*TCPConn).Conn.(*net.TCPConn), ssr, ssw)
 
@@ -67,3 +90,29 @@ func DialShadowsocksTunnel(
 		Conn: ssConn,
 	}, nil
 }
+
+func (conn *shadowsocksConn) IsClosed() bool {
+	closer, ok := conn.Conn.(common.Closer)
+	if !ok {
+		return false
+	}
+	return closer.IsClosed()
+}
+
+type ShadowsocksPrefixSpec struct {
+	Name string
+	Spec transforms.Spec
+	Seed *prng.Seed
+}
+
+func makePrefix(spec *ShadowsocksPrefixSpec) ([]byte, error) {
+
+	minLength := 0
+
+	prefix, _, err := spec.Spec.ApplyPrefix(spec.Seed, minLength)
+	if err != nil {
+		return nil, errors.Trace(err)
+	}
+
+	return prefix, nil
+}

+ 201 - 0
vendor/github.com/Jigsaw-Code/outline-ss-server/LICENSE

@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 46 - 0
vendor/github.com/Jigsaw-Code/outline-ss-server/net/error.go

@@ -0,0 +1,46 @@
+// Copyright 2019 Jigsaw Operations LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package net
+
+type ConnectionError struct {
+	// TODO: create status enums and move to metrics.go
+	Status  string
+	Message string
+	Cause   error
+}
+
+func NewConnectionError(status, message string, cause error) *ConnectionError {
+	return &ConnectionError{Status: status, Message: message, Cause: cause}
+}
+
+func (e *ConnectionError) Error() string {
+	if e == nil {
+		return "<nil>"
+	}
+	msg := e.Message
+	if len(e.Status) > 0 {
+		msg += " [" + e.Status + "]"
+	}
+	if e.Cause != nil {
+		msg += ": " + e.Cause.Error()
+	}
+	return msg
+}
+
+func (e *ConnectionError) Unwrap() error {
+	return e.Cause
+}
+
+var _ error = (*ConnectionError)(nil)

+ 63 - 0
vendor/github.com/Jigsaw-Code/outline-ss-server/net/private_net.go

@@ -0,0 +1,63 @@
+// Copyright 2019 Jigsaw Operations LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package net
+
+import (
+	"fmt"
+	"net"
+)
+
+var privateNetworks []*net.IPNet
+
+func init() {
+	for _, cidr := range []string{
+		// RFC 1918: private IPv4 networks
+		"10.0.0.0/8",
+		"172.16.0.0/12",
+		"192.168.0.0/16",
+		// RFC 4193: IPv6 ULAs
+		"fc00::/7",
+		// RFC 6598: reserved prefix for CGNAT
+		"100.64.0.0/10",
+	} {
+		_, subnet, _ := net.ParseCIDR(cidr)
+		privateNetworks = append(privateNetworks, subnet)
+	}
+}
+
+// IsPrivateAddress returns whether an IP address belongs to the LAN.
+func IsPrivateAddress(ip net.IP) bool {
+	for _, network := range privateNetworks {
+		if network.Contains(ip) {
+			return true
+		}
+	}
+	return false
+}
+
+// TargetIPValidator is a type alias for checking if an IP is allowed.
+type TargetIPValidator = func(net.IP) error
+
+// RequirePublicIP returns an error if the destination IP is not a
+// standard public IP.
+func RequirePublicIP(ip net.IP) error {
+	if !ip.IsGlobalUnicast() {
+		return NewConnectionError("ERR_ADDRESS_INVALID", fmt.Sprintf("Address is not global unicast: %s", ip.String()), nil)
+	}
+	if IsPrivateAddress(ip) {
+		return NewConnectionError("ERR_ADDRESS_PRIVATE", fmt.Sprintf("Address is private: %s", ip.String()), nil)
+	}
+	return nil
+}

+ 57 - 0
vendor/github.com/Jigsaw-Code/outline-ss-server/net/stream.go

@@ -0,0 +1,57 @@
+// Copyright 2019 Jigsaw Operations LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package net
+
+import (
+	"io"
+
+	"github.com/Jigsaw-Code/outline-sdk/transport"
+)
+
+type DuplexConn = transport.StreamConn
+
+func copyOneWay(leftConn, rightConn DuplexConn) (int64, error) {
+	n, err := io.Copy(leftConn, rightConn)
+	// Send FIN to indicate EOF
+	leftConn.CloseWrite()
+	// Release reader resources
+	rightConn.CloseRead()
+	return n, err
+}
+
+// Relay copies between left and right bidirectionally. Returns number of
+// bytes copied from right to left, from left to right, and any error occurred.
+// Relay allows for half-closed connections: if one side is done writing, it can
+// still read all remaining data from its peer.
+func Relay(leftConn, rightConn DuplexConn) (int64, int64, error) {
+	type res struct {
+		N   int64
+		Err error
+	}
+	ch := make(chan res)
+
+	go func() {
+		n, err := copyOneWay(rightConn, leftConn)
+		ch <- res{n, err}
+	}()
+
+	n, err := copyOneWay(leftConn, rightConn)
+	rs := <-ch
+
+	if err == nil {
+		err = rs.Err
+	}
+	return n, rs.N, err
+}

+ 35 - 0
vendor/github.com/Jigsaw-Code/outline-ss-server/service/PROBES.md

@@ -0,0 +1,35 @@
+# Outline Shadowsocks Probing and Replay Defenses
+
+## Attacks
+
+To ensure that proxied connections have not been modified in transit, the Outline implementation of Shadowsocks only supports modern [AEAD cipher suites](https://shadowsocks.org/en/spec/AEAD-Ciphers.html).  This protects users from a wide range of potential attacks.  However, even with [AEAD's authenticity guarantees](https://en.wikipedia.org/wiki/Authenticated_encryption), there are still ways for an attacker to abuse the Shadowsocks protocol.
+
+One category of attacks are "probing" attacks, in which the adversary sends test data to the proxy in order to confirm that it is actually a Shadowsocks proxy.  This is a violation of the Shadowsocks security design, which is intended to ensure that only an authenticated user can identify the proxy.  For example, one [probing attack against Shadowsocks](https://scholar.google.com/scholar?cluster=8542824533765048218) sends different numbers of random bytes to a target server, and identifies how many bytes the server reads before detecting an error and closing the connection.  This number can be distinctive, identifying the server software.
+
+Another [reported](https://gfw.report/blog/gfw_shadowsocks/) category of attacks are "replay" attacks, in which an adversary records a conversation between a Shadowsocks client and server, then replays the contents of that connection.  The contents are valid Shadowsocks AEAD data, so the proxy will forward the connection to the specified destination, as usual.  In some cases, this can cause a duplicated action (e.g. uploading a file twice with HTTP POST).  However, modern secure protocols such as HTTPS are not replayable, so this will normally have no ill effect.
+
+A greater concern for Outline is the use of replays in probing attacks to identify Shadowsocks proxies.  By sending modified and unmodified replays, an attacker might be able to confirm that a server is in fact a Shadowsocks proxy, by observing distinctive behaviors.
+
+## Outline's defenses
+
+Outline contains several defenses against probing and replay attacks.
+
+### Invalid probe data
+
+If Outline detects that the initial data is invalid, it will continue to read data (exactly as if it were valid), but will not reply, and will not close the connection until a timeout.  This leaves the attacker with minimal information about the server.
+
+### Client replays
+
+When client replay protection is enabled, every incoming valid handshake is reduced to a 32-bit checksum and stored in a hash table.  When the table is full, it is archived and replaced with a fresh one, ensuring that the recent history is always in memory.  Using 32-bit checksums results in a false-positive detection rate of 1 in 4 billion for each entry in the history.  At the maximum history size (two sets of 20,000 checksums each), that results in a false-positive failure rate of 1 in 100,000 sockets ... still far lower than the error rate expected from network unreliability.
+
+This feature is on by default in Outline.  Admins who are using outline-ss-server directly can enable this feature by adding "--replay_history 10000" to their outline-ss-server invocation.  This costs approximately 20 bytes of memory per checksum.
+
+### Server replays
+
+Shadowsocks uses the same Key Derivation Function for both upstream and downstream flows, so in principle an attacker could record data sent from the server to the client, and use it in a "reflected replay" attack as simulated client->server data.  The data would appear to be valid and authenticated to the server, but the connection would most likely fail when attempting to parse the destination address header, perhaps leading to a distinctive failure behavior.
+
+To avoid this class of attacks, outline-ss-server uses an [HMAC](https://en.wikipedia.org/wiki/HMAC) with a 32-bit tag to mark all server handshakes, and checks for the presence of this tag in all incoming handshakes.  If the tag is present, the connection is a reflected replay, with a false positive probability of 1 in 4 billion.
+
+## Metrics
+
+Outline provides server operators with metrics on a variety of aspects of server activity, including any detected attacks.  To observe attacks detected by your server, look at the `tcp_probes` histogram vector in Prometheus.  The `status` field will be `"ERR_CIPHER"` (indicating invalid probe data), `"ERR_REPLAY_CLIENT"`, or `"ERR_REPLAY_SERVER"`, depending on the kind of attack your server observed.  You can also see approximately how many bytes were sent before giving up.

+ 118 - 0
vendor/github.com/Jigsaw-Code/outline-ss-server/service/cipher_list.go

@@ -0,0 +1,118 @@
+// Copyright 2018 Jigsaw Operations LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package service
+
+import (
+	"container/list"
+	"net/netip"
+	"sync"
+
+	"github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks"
+)
+
+// Don't add a tag if it would reduce the salt entropy below this amount.
+const minSaltEntropy = 16
+
+// CipherEntry holds a Cipher with an identifier.
+// The public fields are constant, but lastClientIP is mutable under cipherList.mu.
+type CipherEntry struct {
+	ID            string
+	CryptoKey     *shadowsocks.EncryptionKey
+	SaltGenerator ServerSaltGenerator
+	lastClientIP  netip.Addr
+}
+
+// MakeCipherEntry constructs a CipherEntry.
+func MakeCipherEntry(id string, cryptoKey *shadowsocks.EncryptionKey, secret string) CipherEntry {
+	var saltGenerator ServerSaltGenerator
+	if cryptoKey.SaltSize()-serverSaltMarkLen >= minSaltEntropy {
+		// Mark salts with a tag for reverse replay protection.
+		saltGenerator = NewServerSaltGenerator(secret)
+	} else {
+		// Adding a tag would leave too little randomness to protect
+		// against accidental salt reuse, so don't mark the salts.
+		saltGenerator = RandomServerSaltGenerator
+	}
+	return CipherEntry{
+		ID:            id,
+		CryptoKey:     cryptoKey,
+		SaltGenerator: saltGenerator,
+	}
+}
+
+// CipherList is a thread-safe collection of CipherEntry elements that allows for
+// snapshotting and moving to front.
+type CipherList interface {
+	// Returns a snapshot of the cipher list optimized for this client IP
+	SnapshotForClientIP(clientIP netip.Addr) []*list.Element
+	MarkUsedByClientIP(e *list.Element, clientIP netip.Addr)
+	// Update replaces the current contents of the CipherList with `contents`,
+	// which is a List of *CipherEntry.  Update takes ownership of `contents`,
+	// which must not be read or written after this call.
+	Update(contents *list.List)
+}
+
+type cipherList struct {
+	CipherList
+	list *list.List
+	mu   sync.RWMutex
+}
+
+// NewCipherList creates an empty CipherList
+func NewCipherList() CipherList {
+	return &cipherList{list: list.New()}
+}
+
+func matchesIP(e *list.Element, clientIP netip.Addr) bool {
+	c := e.Value.(*CipherEntry)
+	return clientIP != netip.Addr{} && clientIP == c.lastClientIP
+}
+
+func (cl *cipherList) SnapshotForClientIP(clientIP netip.Addr) []*list.Element {
+	cl.mu.RLock()
+	defer cl.mu.RUnlock()
+	cipherArray := make([]*list.Element, cl.list.Len())
+	i := 0
+	// First pass: put all ciphers with matching last known IP at the front.
+	for e := cl.list.Front(); e != nil; e = e.Next() {
+		if matchesIP(e, clientIP) {
+			cipherArray[i] = e
+			i++
+		}
+	}
+	// Second pass: include all remaining ciphers in recency order.
+	for e := cl.list.Front(); e != nil; e = e.Next() {
+		if !matchesIP(e, clientIP) {
+			cipherArray[i] = e
+			i++
+		}
+	}
+	return cipherArray
+}
+
+func (cl *cipherList) MarkUsedByClientIP(e *list.Element, clientIP netip.Addr) {
+	cl.mu.Lock()
+	defer cl.mu.Unlock()
+	cl.list.MoveToFront(e)
+
+	c := e.Value.(*CipherEntry)
+	c.lastClientIP = clientIP
+}
+
+func (cl *cipherList) Update(src *list.List) {
+	cl.mu.Lock()
+	cl.list = src
+	cl.mu.Unlock()
+}

+ 58 - 0
vendor/github.com/Jigsaw-Code/outline-ss-server/service/cipher_list_testing.go

@@ -0,0 +1,58 @@
+// Copyright 2018 Jigsaw Operations LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package service
+
+import (
+	"container/list"
+	"fmt"
+
+	"github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks"
+)
+
+// MakeTestCiphers creates a CipherList containing one fresh AEAD cipher
+// for each secret in `secrets`.
+func MakeTestCiphers(secrets []string) (CipherList, error) {
+	l := list.New()
+	for i := 0; i < len(secrets); i++ {
+		cipherID := fmt.Sprintf("id-%v", i)
+		cipher, err := shadowsocks.NewEncryptionKey(shadowsocks.CHACHA20IETFPOLY1305, secrets[i])
+		if err != nil {
+			return nil, fmt.Errorf("failed to create cipher %v: %w", i, err)
+		}
+		entry := MakeCipherEntry(cipherID, cipher, secrets[i])
+		l.PushBack(&entry)
+	}
+	cipherList := NewCipherList()
+	cipherList.Update(l)
+	return cipherList, nil
+}
+
+// makeTestPayload returns a slice of `size` arbitrary bytes.
+func makeTestPayload(size int) []byte {
+	payload := make([]byte, size)
+	for i := 0; i < size; i++ {
+		payload[i] = byte(i)
+	}
+	return payload
+}
+
+// makeTestSecrets returns a slice of `n` test passwords.  Not secure!
+func makeTestSecrets(n int) []string {
+	secrets := make([]string, n)
+	for i := 0; i < n; i++ {
+		secrets[i] = fmt.Sprintf("secret-%v", i)
+	}
+	return secrets
+}

+ 400 - 0
vendor/github.com/Jigsaw-Code/outline-ss-server/service/listeners.go

@@ -0,0 +1,400 @@
+// Copyright 2024 The Outline Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package service
+
+import (
+	"errors"
+	"fmt"
+	"io"
+	"net"
+	"sync"
+
+	"github.com/Jigsaw-Code/outline-sdk/transport"
+)
+
+// The implementations of listeners for different network types are not
+// interchangeable. The type of listener depends on the network type.
+type Listener = io.Closer
+
+// StreamListener is a network listener for stream-oriented protocols that
+// accepts [transport.StreamConn] connections.
+type StreamListener interface {
+	// Accept waits for and returns the next connection to the listener.
+	AcceptStream() (transport.StreamConn, error)
+
+	// Close closes the listener.
+	// Any blocked Accept operations will be unblocked and return errors. This
+	// stops the current listener from accepting new connections without closing
+	// the underlying socket. Only when the last user of the underlying socket
+	// closes it, do we actually close it.
+	Close() error
+
+	// Addr returns the listener's network address.
+	Addr() net.Addr
+}
+
+type TCPListener struct {
+	ln *net.TCPListener
+}
+
+var _ StreamListener = (*TCPListener)(nil)
+
+func (t *TCPListener) AcceptStream() (transport.StreamConn, error) {
+	return t.ln.AcceptTCP()
+}
+
+func (t *TCPListener) Close() error {
+	return t.ln.Close()
+}
+
+func (t *TCPListener) Addr() net.Addr {
+	return t.ln.Addr()
+}
+
+type OnCloseFunc func() error
+
+type acceptResponse struct {
+	conn transport.StreamConn
+	err  error
+}
+
+type virtualStreamListener struct {
+	mu          sync.Mutex // Mutex to protect access to the channels
+	addr        net.Addr
+	acceptCh    <-chan acceptResponse
+	closeCh     chan struct{}
+	onCloseFunc OnCloseFunc
+}
+
+var _ StreamListener = (*virtualStreamListener)(nil)
+
+func (sl *virtualStreamListener) AcceptStream() (transport.StreamConn, error) {
+	sl.mu.Lock()
+	acceptCh := sl.acceptCh
+	sl.mu.Unlock()
+
+	select {
+	case acceptResponse, ok := <-acceptCh:
+		if !ok {
+			return nil, net.ErrClosed
+		}
+		return acceptResponse.conn, acceptResponse.err
+	case <-sl.closeCh:
+		return nil, net.ErrClosed
+	}
+}
+
+func (sl *virtualStreamListener) Close() error {
+	sl.mu.Lock()
+	defer sl.mu.Unlock()
+
+	if sl.acceptCh == nil {
+		return nil
+	}
+	sl.acceptCh = nil
+	close(sl.closeCh)
+	if sl.onCloseFunc != nil {
+		onCloseFunc := sl.onCloseFunc
+		sl.onCloseFunc = nil
+		return onCloseFunc()
+	}
+	return nil
+}
+
+func (sl *virtualStreamListener) Addr() net.Addr {
+	return sl.addr
+}
+
+type readRequest struct {
+	buffer []byte
+	respCh chan struct {
+		n    int
+		addr net.Addr
+		err  error
+	}
+}
+
+type virtualPacketConn struct {
+	net.PacketConn
+	readCh chan readRequest
+
+	mu          sync.Mutex // Mutex to protect against race conditions when closing the connection.
+	closeCh     chan struct{}
+	onCloseFunc OnCloseFunc
+}
+
+func (pc *virtualPacketConn) ReadFrom(p []byte) (int, net.Addr, error) {
+	respCh := make(chan struct {
+		n    int
+		addr net.Addr
+		err  error
+	}, 1)
+
+	select {
+	case pc.readCh <- readRequest{
+		buffer: p,
+		respCh: respCh,
+	}:
+	case <-pc.closeCh:
+		return 0, nil, net.ErrClosed
+	}
+
+	resp := <-respCh
+	return resp.n, resp.addr, resp.err
+}
+
+// Close closes the virtualPacketConn. It must be called once, and only once,
+// per virtualPacketConn.
+func (pc *virtualPacketConn) Close() error {
+	pc.mu.Lock()
+	defer pc.mu.Unlock()
+
+	close(pc.closeCh)
+	if pc.onCloseFunc != nil {
+		onCloseFunc := pc.onCloseFunc
+		pc.onCloseFunc = nil
+		return onCloseFunc()
+	}
+	return nil
+}
+
+// MultiListener manages shared listeners.
+type MultiListener[T Listener] interface {
+	// Acquire creates a new listener from the shared listener. Listeners can overlap
+	// one another (e.g. during config changes the new config is started before the
+	// old config is destroyed), which is done by creating virtual listeners that wrap
+	// the shared listener. These virtual listeners do not actually close the
+	// underlying socket until all uses of the shared listener have been closed.
+	Acquire() (T, error)
+}
+
+type multiStreamListener struct {
+	mu          sync.Mutex
+	addr        string
+	ln          StreamListener
+	count       uint32
+	acceptCh    chan acceptResponse
+	onCloseFunc OnCloseFunc
+}
+
+// NewMultiStreamListener creates a new stream-based [MultiListener].
+func NewMultiStreamListener(addr string, onCloseFunc OnCloseFunc) MultiListener[StreamListener] {
+	return &multiStreamListener{
+		addr:        addr,
+		onCloseFunc: onCloseFunc,
+	}
+}
+
+func (m *multiStreamListener) Acquire() (StreamListener, error) {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	if m.ln == nil {
+		tcpAddr, err := net.ResolveTCPAddr("tcp", m.addr)
+		if err != nil {
+			return nil, err
+		}
+		ln, err := net.ListenTCP("tcp", tcpAddr)
+		if err != nil {
+			return nil, err
+		}
+		m.ln = &TCPListener{ln}
+		m.acceptCh = make(chan acceptResponse)
+		go func() {
+			for {
+				m.mu.Lock()
+				ln := m.ln
+				m.mu.Unlock()
+
+				if ln == nil {
+					return
+				}
+				conn, err := ln.AcceptStream()
+				if errors.Is(err, net.ErrClosed) {
+					close(m.acceptCh)
+					return
+				}
+				m.acceptCh <- acceptResponse{conn, err}
+			}
+		}()
+	}
+
+	m.count++
+	return &virtualStreamListener{
+		addr:     m.ln.Addr(),
+		acceptCh: m.acceptCh,
+		closeCh:  make(chan struct{}),
+		onCloseFunc: func() error {
+			m.mu.Lock()
+			defer m.mu.Unlock()
+			m.count--
+			if m.count == 0 {
+				m.ln.Close()
+				m.ln = nil
+				if m.onCloseFunc != nil {
+					onCloseFunc := m.onCloseFunc
+					m.onCloseFunc = nil
+					return onCloseFunc()
+				}
+			}
+			return nil
+		},
+	}, nil
+}
+
+type multiPacketListener struct {
+	mu          sync.Mutex
+	addr        string
+	pc          net.PacketConn
+	count       uint32
+	readCh      chan readRequest
+	doneCh      chan struct{}
+	onCloseFunc OnCloseFunc
+}
+
+// NewMultiPacketListener creates a new packet-based [MultiListener].
+func NewMultiPacketListener(addr string, onCloseFunc OnCloseFunc) MultiListener[net.PacketConn] {
+	return &multiPacketListener{
+		addr:        addr,
+		onCloseFunc: onCloseFunc,
+	}
+}
+
+func (m *multiPacketListener) Acquire() (net.PacketConn, error) {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	if m.pc == nil {
+		pc, err := net.ListenPacket("udp", m.addr)
+		if err != nil {
+			return nil, err
+		}
+		m.pc = pc
+		m.readCh = make(chan readRequest)
+		m.doneCh = make(chan struct{})
+		go func() {
+			buffer := make([]byte, serverUDPBufferSize)
+			for {
+				n, addr, err := m.pc.ReadFrom(buffer)
+				pkt := buffer[:n]
+				select {
+				case req := <-m.readCh:
+					n := copy(req.buffer, pkt)
+					req.respCh <- struct {
+						n    int
+						addr net.Addr
+						err  error
+					}{n, addr, err}
+				case <-m.doneCh:
+					return
+				}
+			}
+		}()
+	}
+
+	m.count++
+	return &virtualPacketConn{
+		PacketConn: m.pc,
+		readCh:     m.readCh,
+		closeCh:    make(chan struct{}),
+		onCloseFunc: func() error {
+			m.mu.Lock()
+			defer m.mu.Unlock()
+			m.count--
+			if m.count == 0 {
+				close(m.doneCh)
+				m.pc.Close()
+				if m.onCloseFunc != nil {
+					onCloseFunc := m.onCloseFunc
+					m.onCloseFunc = nil
+					return onCloseFunc()
+				}
+			}
+			return nil
+		},
+	}, nil
+}
+
+// ListenerManager holds the state of shared listeners.
+type ListenerManager interface {
+	// ListenStream creates a new stream listener for a given address.
+	ListenStream(addr string) (StreamListener, error)
+
+	// ListenPacket creates a new packet listener for a given address.
+	ListenPacket(addr string) (net.PacketConn, error)
+}
+
+type listenerManager struct {
+	streamListeners map[string]MultiListener[StreamListener]
+	packetListeners map[string]MultiListener[net.PacketConn]
+	mu              sync.Mutex
+}
+
+// NewListenerManager creates a new [ListenerManger].
+func NewListenerManager() ListenerManager {
+	return &listenerManager{
+		streamListeners: make(map[string]MultiListener[StreamListener]),
+		packetListeners: make(map[string]MultiListener[net.PacketConn]),
+	}
+}
+
+func (m *listenerManager) ListenStream(addr string) (StreamListener, error) {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	streamLn, exists := m.streamListeners[addr]
+	if !exists {
+		streamLn = NewMultiStreamListener(
+			addr,
+			func() error {
+				m.mu.Lock()
+				delete(m.streamListeners, addr)
+				m.mu.Unlock()
+				return nil
+			},
+		)
+		m.streamListeners[addr] = streamLn
+	}
+	ln, err := streamLn.Acquire()
+	if err != nil {
+		return nil, fmt.Errorf("unable to create stream listener for %s: %v", addr, err)
+	}
+	return ln, nil
+}
+
+func (m *listenerManager) ListenPacket(addr string) (net.PacketConn, error) {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	packetLn, exists := m.packetListeners[addr]
+	if !exists {
+		packetLn = NewMultiPacketListener(
+			addr,
+			func() error {
+				m.mu.Lock()
+				delete(m.packetListeners, addr)
+				m.mu.Unlock()
+				return nil
+			},
+		)
+		m.packetListeners[addr] = packetLn
+	}
+
+	ln, err := packetLn.Acquire()
+	if err != nil {
+		return nil, fmt.Errorf("unable to create packet listener for %s: %v", addr, err)
+	}
+	return ln, nil
+}

+ 26 - 0
vendor/github.com/Jigsaw-Code/outline-ss-server/service/logger.go

@@ -0,0 +1,26 @@
+// Copyright 2018 Jigsaw Operations LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package service
+
+import (
+	"io"
+	"log/slog"
+	"math"
+)
+
+func noopLogger() *slog.Logger {
+	// TODO: Use built-in no-op log level when available: https://go.dev/issue/62005
+	return slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{Level: slog.Level(math.MaxInt)}))
+}

+ 69 - 0
vendor/github.com/Jigsaw-Code/outline-ss-server/service/metrics/metrics.go

@@ -0,0 +1,69 @@
+// Copyright 2018 Jigsaw Operations LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package metrics
+
+import (
+	"io"
+
+	"github.com/Jigsaw-Code/outline-sdk/transport"
+)
+
+type ProxyMetrics struct {
+	ClientProxy int64
+	ProxyTarget int64
+	TargetProxy int64
+	ProxyClient int64
+}
+
+type measuredConn struct {
+	transport.StreamConn
+	io.WriterTo
+	readCount *int64
+	io.ReaderFrom
+	writeCount *int64
+}
+
+func (c *measuredConn) Read(b []byte) (int, error) {
+	n, err := c.StreamConn.Read(b)
+	*c.readCount += int64(n)
+	return n, err
+}
+
+func (c *measuredConn) WriteTo(w io.Writer) (int64, error) {
+	n, err := io.Copy(w, c.StreamConn)
+	*c.readCount += n
+	return n, err
+}
+
+func (c *measuredConn) Write(b []byte) (int, error) {
+	n, err := c.StreamConn.Write(b)
+	*c.writeCount += int64(n)
+	return n, err
+}
+
+func (c *measuredConn) ReadFrom(r io.Reader) (n int64, err error) {
+	if rf, ok := c.StreamConn.(io.ReaderFrom); ok {
+		// Prefer ReadFrom if we are calling ReadFrom. Otherwise io.Copy will try WriteTo first.
+		n, err = rf.ReadFrom(r)
+	} else {
+		n, err = io.Copy(c.StreamConn, r)
+	}
+	*c.writeCount += n
+	return n, err
+}
+
+func MeasureConn(conn transport.StreamConn, bytesSent, bytesReceived *int64) transport.StreamConn {
+	return &measuredConn{StreamConn: conn, writeCount: bytesSent, readCount: bytesReceived}
+}

+ 117 - 0
vendor/github.com/Jigsaw-Code/outline-ss-server/service/replay.go

@@ -0,0 +1,117 @@
+// Copyright 2020 Jigsaw Operations LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package service
+
+import (
+	"encoding/binary"
+	"errors"
+	"sync"
+)
+
+// MaxCapacity is the largest allowed size of ReplayCache.
+//
+// Capacities in excess of 20,000 are not recommended, due to the false
+// positive rate of up to 2 * capacity / 2^32 = 1 / 100,000.  If larger
+// capacities are desired, the key type should be changed to uint64.
+const MaxCapacity = 20_000
+
+type empty struct{}
+
+// ReplayCache allows us to check whether a handshake salt was used within
+// the last `capacity` handshakes.  It requires approximately 20*capacity
+// bytes of memory (as measured by BenchmarkReplayCache_Creation).
+//
+// The nil and zero values represent a cache with capacity 0, i.e. no cache.
+type ReplayCache struct {
+	mutex    sync.Mutex
+	capacity int
+	active   map[uint32]empty
+	archive  map[uint32]empty
+}
+
+// NewReplayCache returns a fresh ReplayCache that promises to remember at least
+// the most recent `capacity` handshakes.
+func NewReplayCache(capacity int) ReplayCache {
+	if capacity > MaxCapacity {
+		panic("ReplayCache capacity would result in too many false positives")
+	}
+	return ReplayCache{
+		capacity: capacity,
+		active:   make(map[uint32]empty, capacity),
+		// `archive` is read-only and initially empty.
+	}
+}
+
+// Trivially reduces the key and salt to a uint32, avoiding collisions
+// in case of salts with a shared prefix or suffix.  Salts are normally
+// random, but in principle a client might use a counter instead, so
+// using only the prefix or suffix is not sufficient.  Including the key
+// ID in the hash avoids accidental collisions when the same salt is used
+// by different access keys, as might happen in the case of a counter.
+//
+// Secure hashing is not required, because only authenticated handshakes
+// are added to the cache.  A hostile client could produce colliding salts,
+// but this would not impact other users.  Each map uses a new random hash
+// function, so it is not trivial for a hostile client to mount an
+// algorithmic complexity attack with nearly-colliding hashes:
+// https://dave.cheney.net/2018/05/29/how-the-go-runtime-implements-maps-efficiently-without-generics
+func preHash(id string, salt []byte) uint32 {
+	buf := [4]byte{}
+	for i := 0; i < len(id); i++ {
+		buf[i&0x3] ^= id[i]
+	}
+	for i, v := range salt {
+		buf[i&0x3] ^= v
+	}
+	return binary.BigEndian.Uint32(buf[:])
+}
+
+// Add a handshake with this key ID and salt to the cache.
+// Returns false if it is already present.
+func (c *ReplayCache) Add(id string, salt []byte) bool {
+	if c == nil || c.capacity == 0 {
+		// Cache is disabled, so every salt is new.
+		return true
+	}
+	hash := preHash(id, salt)
+	c.mutex.Lock()
+	defer c.mutex.Unlock()
+	if _, ok := c.active[hash]; ok {
+		// Fast replay: `salt` is already in the active set.
+		return false
+	}
+	_, inArchive := c.archive[hash]
+	if len(c.active) >= c.capacity {
+		// Discard the archive and move active to archive.
+		c.archive = c.active
+		c.active = make(map[uint32]empty, c.capacity)
+	}
+	c.active[hash] = empty{}
+	return !inArchive
+}
+
+// Resize adjusts the capacity of the ReplayCache.
+func (c *ReplayCache) Resize(capacity int) error {
+	if capacity > MaxCapacity {
+		return errors.New("ReplayCache capacity would result in too many false positives")
+	}
+	c.mutex.Lock()
+	defer c.mutex.Unlock()
+	c.capacity = capacity
+	// NOTE: The active handshakes and archive lists are not explicitly shrunk.
+	// Their sizes will naturally adjust as new handshakes are added and the cache
+	// adheres to the updated capacity.
+	return nil
+}

+ 120 - 0
vendor/github.com/Jigsaw-Code/outline-ss-server/service/server_salt.go

@@ -0,0 +1,120 @@
+// Copyright 2020 Jigsaw Operations LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package service
+
+import (
+	"bytes"
+	"crypto"
+	"crypto/hmac"
+	"crypto/rand"
+	"fmt"
+	"io"
+
+	ss "github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks"
+	"golang.org/x/crypto/hkdf"
+)
+
+// ServerSaltGenerator offers the ability to check if a salt was marked as
+// server-originated.
+type ServerSaltGenerator interface {
+	ss.SaltGenerator
+	// IsServerSalt returns true if the salt was created by this generator
+	// and is marked as server-originated.
+	IsServerSalt(salt []byte) bool
+}
+
+// randomServerSaltGenerator generates a new random salt.
+type randomServerSaltGenerator struct{}
+
+// GetSalt outputs a random salt.
+func (randomServerSaltGenerator) GetSalt(salt []byte) error {
+	_, err := rand.Read(salt)
+	return err
+}
+
+func (randomServerSaltGenerator) IsServerSalt(salt []byte) bool {
+	return false
+}
+
+// RandomServerSaltGenerator is a basic ServerSaltGenerator.
+var RandomServerSaltGenerator ServerSaltGenerator = randomServerSaltGenerator{}
+
+// serverSaltGenerator generates unique salts that are secretly marked.
+type serverSaltGenerator struct {
+	key []byte
+}
+
+// serverSaltMarkLen is the number of bytes of salt to use as a marker.
+// Increasing this value reduces the false positive rate, but increases
+// the likelihood of salt collisions.
+const serverSaltMarkLen = 4 // Must be less than or equal to SHA1.Size()
+
+// Constant to identify this marking scheme.
+var serverSaltLabel = []byte("outline-server-salt")
+
+// NewServerSaltGenerator returns a SaltGenerator whose output is apparently
+// random, but is secretly marked as being issued by the server.
+// This is useful to prevent the server from accepting its own output in a
+// reflection attack.
+func NewServerSaltGenerator(secret string) ServerSaltGenerator {
+	// Shadowsocks already uses HKDF-SHA1 to derive the AEAD key, so we use
+	// the same derivation with a different "info" to generate our HMAC key.
+	keySource := hkdf.New(crypto.SHA1.New, []byte(secret), nil, serverSaltLabel)
+	// The key can be any size, but matching the block size is most efficient.
+	key := make([]byte, crypto.SHA1.Size())
+	io.ReadFull(keySource, key)
+	return serverSaltGenerator{key}
+}
+
+func (sg serverSaltGenerator) splitSalt(salt []byte) (prefix, mark []byte, err error) {
+	prefixLen := len(salt) - serverSaltMarkLen
+	if prefixLen < 0 {
+		return nil, nil, fmt.Errorf("salt is too short: %d < %d", len(salt), serverSaltMarkLen)
+	}
+	return salt[:prefixLen], salt[prefixLen:], nil
+}
+
+// getTag takes in a salt prefix and returns the tag.
+func (sg serverSaltGenerator) getTag(prefix []byte) []byte {
+	// Use HMAC-SHA1, even though SHA1 is broken, because HMAC-SHA1 is still
+	// secure, and we're already using HKDF-SHA1.
+	hmac := hmac.New(crypto.SHA1.New, sg.key)
+	hmac.Write(prefix) // Hash.Write never returns an error.
+	return hmac.Sum(nil)
+}
+
+// GetSalt returns an apparently random salt that can be identified
+// as server-originated by anyone who knows the Shadowsocks key.
+func (sg serverSaltGenerator) GetSalt(salt []byte) error {
+	prefix, mark, err := sg.splitSalt(salt)
+	if err != nil {
+		return err
+	}
+	if _, err := rand.Read(prefix); err != nil {
+		return err
+	}
+	tag := sg.getTag(prefix)
+	copy(mark, tag)
+	return nil
+}
+
+func (sg serverSaltGenerator) IsServerSalt(salt []byte) bool {
+	prefix, mark, err := sg.splitSalt(salt)
+	if err != nil {
+		return false
+	}
+	tag := sg.getTag(prefix)
+	return bytes.Equal(tag[:serverSaltMarkLen], mark)
+}

+ 189 - 0
vendor/github.com/Jigsaw-Code/outline-ss-server/service/shadowsocks.go

@@ -0,0 +1,189 @@
+// Copyright 2024 The Outline Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package service
+
+import (
+	"context"
+	"log/slog"
+	"net"
+	"time"
+
+	"github.com/Jigsaw-Code/outline-sdk/transport"
+
+	onet "github.com/Jigsaw-Code/outline-ss-server/net"
+)
+
+const (
+	// 59 seconds is most common timeout for servers that do not respond to invalid requests
+	tcpReadTimeout time.Duration = 59 * time.Second
+
+	// A UDP NAT timeout of at least 5 minutes is recommended in RFC 4787 Section 4.3.
+	defaultNatTimeout time.Duration = 5 * time.Minute
+)
+
+// ShadowsocksConnMetrics is used to report Shadowsocks related metrics on connections.
+type ShadowsocksConnMetrics interface {
+	AddCipherSearch(accessKeyFound bool, timeToCipher time.Duration)
+}
+
+type ServiceMetrics interface {
+	UDPMetrics
+	AddOpenTCPConnection(conn net.Conn) TCPConnMetrics
+	AddCipherSearch(proto string, accessKeyFound bool, timeToCipher time.Duration)
+}
+
+type Service interface {
+	HandleStream(ctx context.Context, conn transport.StreamConn)
+	HandlePacket(conn net.PacketConn)
+}
+
+// Option is a Shadowsocks service constructor option.
+type Option func(s *ssService)
+
+type ssService struct {
+	logger            *slog.Logger
+	metrics           ServiceMetrics
+	ciphers           CipherList
+	natTimeout        time.Duration
+	targetIPValidator onet.TargetIPValidator
+	replayCache       *ReplayCache
+
+	streamDialer   transport.StreamDialer
+	sh             StreamHandler
+	packetListener transport.PacketListener
+	ph             PacketHandler
+}
+
+// NewShadowsocksService creates a new Shadowsocks service.
+func NewShadowsocksService(opts ...Option) (Service, error) {
+	s := &ssService{}
+
+	for _, opt := range opts {
+		opt(s)
+	}
+
+	// If no NAT timeout is provided via options, use the recommended default.
+	if s.natTimeout == 0 {
+		s.natTimeout = defaultNatTimeout
+	}
+	// If no logger is provided via options, use a noop logger.
+	if s.logger == nil {
+		s.logger = noopLogger()
+	}
+
+	// TODO: Register initial data metrics at zero.
+	s.sh = NewStreamHandler(
+		NewShadowsocksStreamAuthenticator(s.ciphers, s.replayCache, &ssConnMetrics{ServiceMetrics: s.metrics, proto: "tcp"}, s.logger),
+		tcpReadTimeout,
+	)
+	if s.streamDialer != nil {
+		s.sh.SetTargetDialer(s.streamDialer)
+	}
+	s.sh.SetLogger(s.logger)
+
+	s.ph = NewPacketHandler(s.natTimeout, s.ciphers, s.metrics, &ssConnMetrics{ServiceMetrics: s.metrics, proto: "udp"})
+	if s.packetListener != nil {
+		s.ph.SetTargetPacketListener(s.packetListener)
+	}
+	s.ph.SetLogger(s.logger)
+
+	return s, nil
+}
+
+// WithLogger can be used to provide a custom log target. If not provided,
+// the service uses a noop logger (i.e., no logging).
+func WithLogger(l *slog.Logger) Option {
+	return func(s *ssService) {
+		s.logger = l
+	}
+}
+
+// WithCiphers option function.
+func WithCiphers(ciphers CipherList) Option {
+	return func(s *ssService) {
+		s.ciphers = ciphers
+	}
+}
+
+// WithMetrics option function.
+func WithMetrics(metrics ServiceMetrics) Option {
+	return func(s *ssService) {
+		s.metrics = metrics
+	}
+}
+
+// WithReplayCache option function.
+func WithReplayCache(replayCache *ReplayCache) Option {
+	return func(s *ssService) {
+		s.replayCache = replayCache
+	}
+}
+
+// WithNatTimeout option function.
+func WithNatTimeout(natTimeout time.Duration) Option {
+	return func(s *ssService) {
+		s.natTimeout = natTimeout
+	}
+}
+
+// WithStreamDialer option function.
+func WithStreamDialer(dialer transport.StreamDialer) Option {
+	return func(s *ssService) {
+		s.streamDialer = dialer
+	}
+}
+
+// WithPacketListener option function.
+func WithPacketListener(listener transport.PacketListener) Option {
+	return func(s *ssService) {
+		s.packetListener = listener
+	}
+}
+
+// HandleStream handles a Shadowsocks stream-based connection.
+func (s *ssService) HandleStream(ctx context.Context, conn transport.StreamConn) {
+	var connMetrics TCPConnMetrics
+	if s.metrics != nil {
+		connMetrics = s.metrics.AddOpenTCPConnection(conn)
+	}
+	s.sh.Handle(ctx, conn, connMetrics)
+}
+
+// HandlePacket handles a Shadowsocks packet connection.
+func (s *ssService) HandlePacket(conn net.PacketConn) {
+	s.ph.Handle(conn)
+}
+
+type ssConnMetrics struct {
+	ServiceMetrics
+	proto string
+}
+
+var _ ShadowsocksConnMetrics = (*ssConnMetrics)(nil)
+
+func (cm *ssConnMetrics) AddCipherSearch(accessKeyFound bool, timeToCipher time.Duration) {
+	if cm.ServiceMetrics != nil {
+		cm.ServiceMetrics.AddCipherSearch(cm.proto, accessKeyFound, timeToCipher)
+	}
+}
+
+// NoOpShadowsocksConnMetrics is a [ShadowsocksConnMetrics] that doesn't do anything. Useful in tests
+// or if you don't want to track metrics.
+type NoOpShadowsocksConnMetrics struct{}
+
+var _ ShadowsocksConnMetrics = (*NoOpShadowsocksConnMetrics)(nil)
+
+func (m *NoOpShadowsocksConnMetrics) AddCipherSearch(accessKeyFound bool, timeToCipher time.Duration) {
+}

+ 33 - 0
vendor/github.com/Jigsaw-Code/outline-ss-server/service/socketopts_linux.go

@@ -0,0 +1,33 @@
+// Copyright 2024 Jigsaw Operations LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build linux
+
+package service
+
+import (
+	"os"
+	"syscall"
+)
+
+func SetFwmark(rc syscall.RawConn, fwmark uint) error {
+	var err error
+	rc.Control(func(fd uintptr) {
+		err = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, int(fwmark))
+	})
+	if err != nil {
+		return os.NewSyscallError("failed to set fwmark for socket", err)
+	}
+	return nil
+}

+ 395 - 0
vendor/github.com/Jigsaw-Code/outline-ss-server/service/tcp.go

@@ -0,0 +1,395 @@
+// Copyright 2018 Jigsaw Operations LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package service
+
+import (
+	"bytes"
+	"container/list"
+	"context"
+	"errors"
+	"fmt"
+	"io"
+	"log/slog"
+	"net"
+	"net/netip"
+	"sync"
+	"time"
+
+	"github.com/Jigsaw-Code/outline-sdk/transport"
+	"github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks"
+	"github.com/shadowsocks/go-shadowsocks2/socks"
+
+	onet "github.com/Jigsaw-Code/outline-ss-server/net"
+	"github.com/Jigsaw-Code/outline-ss-server/service/metrics"
+)
+
+// TCPConnMetrics is used to report metrics on TCP connections.
+type TCPConnMetrics interface {
+	AddAuthenticated(accessKey string)
+	AddClosed(status string, data metrics.ProxyMetrics, duration time.Duration)
+	AddProbe(status, drainResult string, clientProxyBytes int64)
+}
+
+func remoteIP(conn net.Conn) netip.Addr {
+	addr := conn.RemoteAddr()
+	if addr == nil {
+		return netip.Addr{}
+	}
+	if tcpaddr, ok := addr.(*net.TCPAddr); ok {
+		return tcpaddr.AddrPort().Addr()
+	}
+	addrPort, err := netip.ParseAddrPort(addr.String())
+	if err == nil {
+		return addrPort.Addr()
+	}
+	return netip.Addr{}
+}
+
+// Wrapper for slog.Debug during TCP access key searches.
+func debugTCP(l *slog.Logger, template string, cipherID string, attr slog.Attr) {
+	// This is an optimization to reduce unnecessary allocations due to an interaction
+	// between Go's inlining/escape analysis and varargs functions like slog.Debug.
+	if l.Enabled(nil, slog.LevelDebug) {
+		l.LogAttrs(nil, slog.LevelDebug, fmt.Sprintf("TCP: %s", template), slog.String("ID", cipherID), attr)
+	}
+}
+
+// bytesForKeyFinding is the number of bytes to read for finding the AccessKey.
+// Is must satisfy provided >= bytesForKeyFinding >= required for every cipher in the list.
+// provided = saltSize + 2 + 2 * cipher.TagSize, the minimum number of bytes we will see in a valid connection
+// required = saltSize + 2 + cipher.TagSize, the number of bytes needed to authenticate the connection.
+const bytesForKeyFinding = 50
+
+func findAccessKey(clientReader io.Reader, clientIP netip.Addr, cipherList CipherList, l *slog.Logger) (*CipherEntry, io.Reader, []byte, time.Duration, error) {
+	// We snapshot the list because it may be modified while we use it.
+	ciphers := cipherList.SnapshotForClientIP(clientIP)
+	firstBytes := make([]byte, bytesForKeyFinding)
+	if n, err := io.ReadFull(clientReader, firstBytes); err != nil {
+		return nil, clientReader, nil, 0, fmt.Errorf("reading header failed after %d bytes: %w", n, err)
+	}
+
+	findStartTime := time.Now()
+	entry, elt := findEntry(firstBytes, ciphers, l)
+	timeToCipher := time.Since(findStartTime)
+	if entry == nil {
+		// TODO: Ban and log client IPs with too many failures too quick to protect against DoS.
+		return nil, clientReader, nil, timeToCipher, fmt.Errorf("could not find valid TCP cipher")
+	}
+
+	// Move the active cipher to the front, so that the search is quicker next time.
+	cipherList.MarkUsedByClientIP(elt, clientIP)
+	salt := firstBytes[:entry.CryptoKey.SaltSize()]
+	return entry, io.MultiReader(bytes.NewReader(firstBytes), clientReader), salt, timeToCipher, nil
+}
+
+// Implements a trial decryption search.  This assumes that all ciphers are AEAD.
+func findEntry(firstBytes []byte, ciphers []*list.Element, l *slog.Logger) (*CipherEntry, *list.Element) {
+	// To hold the decrypted chunk length.
+	chunkLenBuf := [2]byte{}
+	for ci, elt := range ciphers {
+		entry := elt.Value.(*CipherEntry)
+		cryptoKey := entry.CryptoKey
+		_, err := shadowsocks.Unpack(chunkLenBuf[:0], firstBytes[:cryptoKey.SaltSize()+2+cryptoKey.TagSize()], cryptoKey)
+		if err != nil {
+			debugTCP(l, "Failed to decrypt length.", entry.ID, slog.Any("err", err))
+			continue
+		}
+		debugTCP(l, "Found cipher.", entry.ID, slog.Int("index", ci))
+		return entry, elt
+	}
+	return nil, nil
+}
+
+type StreamAuthenticateFunc func(clientConn transport.StreamConn) (string, transport.StreamConn, *onet.ConnectionError)
+
+// NewShadowsocksStreamAuthenticator creates a stream authenticator that uses Shadowsocks.
+// TODO(fortuna): Offer alternative transports.
+func NewShadowsocksStreamAuthenticator(ciphers CipherList, replayCache *ReplayCache, metrics ShadowsocksConnMetrics, l *slog.Logger) StreamAuthenticateFunc {
+	if metrics == nil {
+		metrics = &NoOpShadowsocksConnMetrics{}
+	}
+	if l == nil {
+		l = noopLogger()
+	}
+	return func(clientConn transport.StreamConn) (string, transport.StreamConn, *onet.ConnectionError) {
+		// Find the cipher and acess key id.
+		cipherEntry, clientReader, clientSalt, timeToCipher, keyErr := findAccessKey(clientConn, remoteIP(clientConn), ciphers, l)
+		metrics.AddCipherSearch(keyErr == nil, timeToCipher)
+		if keyErr != nil {
+			const status = "ERR_CIPHER"
+			return "", nil, onet.NewConnectionError(status, "Failed to find a valid cipher", keyErr)
+		}
+		var id string
+		if cipherEntry != nil {
+			id = cipherEntry.ID
+		}
+
+		// Check if the connection is a replay.
+		isServerSalt := cipherEntry.SaltGenerator.IsServerSalt(clientSalt)
+		// Only check the cache if findAccessKey succeeded and the salt is unrecognized.
+		if isServerSalt || !replayCache.Add(cipherEntry.ID, clientSalt) {
+			var status string
+			if isServerSalt {
+				status = "ERR_REPLAY_SERVER"
+			} else {
+				status = "ERR_REPLAY_CLIENT"
+			}
+			return id, nil, onet.NewConnectionError(status, "Replay detected", nil)
+		}
+
+		ssr := shadowsocks.NewReader(clientReader, cipherEntry.CryptoKey)
+		ssw := shadowsocks.NewWriter(clientConn, cipherEntry.CryptoKey)
+		ssw.SetSaltGenerator(cipherEntry.SaltGenerator)
+		return id, transport.WrapConn(clientConn, ssr, ssw), nil
+	}
+}
+
+type streamHandler struct {
+	logger       *slog.Logger
+	listenerId   string
+	readTimeout  time.Duration
+	authenticate StreamAuthenticateFunc
+	dialer       transport.StreamDialer
+}
+
+// NewStreamHandler creates a StreamHandler
+func NewStreamHandler(authenticate StreamAuthenticateFunc, timeout time.Duration) StreamHandler {
+	return &streamHandler{
+		logger:       noopLogger(),
+		readTimeout:  timeout,
+		authenticate: authenticate,
+		dialer:       MakeValidatingTCPStreamDialer(onet.RequirePublicIP, 0),
+	}
+}
+
+// StreamHandler is a handler that handles stream connections.
+type StreamHandler interface {
+	Handle(ctx context.Context, conn transport.StreamConn, connMetrics TCPConnMetrics)
+	// SetLogger sets the logger used to log messages. Uses a no-op logger if nil.
+	SetLogger(l *slog.Logger)
+	// SetTargetDialer sets the [transport.StreamDialer] to be used to connect to target addresses.
+	SetTargetDialer(dialer transport.StreamDialer)
+}
+
+func (s *streamHandler) SetLogger(l *slog.Logger) {
+	if l == nil {
+		l = noopLogger()
+	}
+	s.logger = l
+}
+
+func (s *streamHandler) SetTargetDialer(dialer transport.StreamDialer) {
+	s.dialer = dialer
+}
+
+func ensureConnectionError(err error, fallbackStatus string, fallbackMsg string) *onet.ConnectionError {
+	if err == nil {
+		return nil
+	}
+	var connErr *onet.ConnectionError
+	if errors.As(err, &connErr) {
+		return connErr
+	} else {
+		return onet.NewConnectionError(fallbackStatus, fallbackMsg, err)
+	}
+}
+
+type StreamAcceptFunc func() (transport.StreamConn, error)
+
+func WrapStreamAcceptFunc[T transport.StreamConn](f func() (T, error)) StreamAcceptFunc {
+	return func() (transport.StreamConn, error) {
+		return f()
+	}
+}
+
+type StreamHandleFunc func(ctx context.Context, conn transport.StreamConn)
+
+// StreamServe repeatedly calls `accept` to obtain connections and `handle` to handle them until
+// accept() returns [ErrClosed]. When that happens, all connection handlers will be notified
+// via their [context.Context]. StreamServe will return after all pending handlers return.
+func StreamServe(accept StreamAcceptFunc, handle StreamHandleFunc) {
+	var running sync.WaitGroup
+	defer running.Wait()
+	ctx, contextCancel := context.WithCancel(context.Background())
+	defer contextCancel()
+	for {
+		clientConn, err := accept()
+		if err != nil {
+			if errors.Is(err, net.ErrClosed) {
+				break
+			}
+			slog.Warn("Accept failed. Continuing to listen.", "err", err)
+			continue
+		}
+
+		running.Add(1)
+		go func() {
+			defer running.Done()
+			defer clientConn.Close()
+			defer func() {
+				if r := recover(); r != nil {
+					slog.Warn("Panic in TCP handler. Continuing to listen.", "err", r)
+				}
+			}()
+			handle(ctx, clientConn)
+		}()
+	}
+}
+
+func (h *streamHandler) Handle(ctx context.Context, clientConn transport.StreamConn, connMetrics TCPConnMetrics) {
+	if connMetrics == nil {
+		connMetrics = &NoOpTCPConnMetrics{}
+	}
+	var proxyMetrics metrics.ProxyMetrics
+	measuredClientConn := metrics.MeasureConn(clientConn, &proxyMetrics.ProxyClient, &proxyMetrics.ClientProxy)
+	connStart := time.Now()
+
+	connError := h.handleConnection(ctx, measuredClientConn, connMetrics, &proxyMetrics)
+
+	connDuration := time.Since(connStart)
+	status := "OK"
+	if connError != nil {
+		status = connError.Status
+		h.logger.LogAttrs(nil, slog.LevelDebug, "TCP: Error", slog.String("msg", connError.Message), slog.Any("cause", connError.Cause))
+	}
+	connMetrics.AddClosed(status, proxyMetrics, connDuration)
+	measuredClientConn.Close() // Closing after the metrics are added aids integration testing.
+	h.logger.LogAttrs(nil, slog.LevelDebug, "TCP: Done.", slog.String("status", status), slog.Duration("duration", connDuration))
+}
+
+func getProxyRequest(clientConn transport.StreamConn) (string, error) {
+	// TODO(fortuna): Use Shadowsocks proxy, HTTP CONNECT or SOCKS5 based on first byte:
+	// case 1, 3 or 4: Shadowsocks (address type)
+	// case 5: SOCKS5 (protocol version)
+	// case "C": HTTP CONNECT (first char of method)
+	tgtAddr, err := socks.ReadAddr(clientConn)
+	if err != nil {
+		return "", err
+	}
+	return tgtAddr.String(), nil
+}
+
+func proxyConnection(l *slog.Logger, ctx context.Context, dialer transport.StreamDialer, tgtAddr string, clientConn transport.StreamConn) *onet.ConnectionError {
+	tgtConn, dialErr := dialer.DialStream(ctx, tgtAddr)
+	if dialErr != nil {
+		// We don't drain so dial errors and invalid addresses are communicated quickly.
+		return ensureConnectionError(dialErr, "ERR_CONNECT", "Failed to connect to target")
+	}
+	defer tgtConn.Close()
+	l.LogAttrs(nil, slog.LevelDebug, "Proxy connection.", slog.String("client", clientConn.RemoteAddr().String()), slog.String("target", tgtConn.RemoteAddr().String()))
+
+	fromClientErrCh := make(chan error)
+	go func() {
+		_, fromClientErr := io.Copy(tgtConn, clientConn)
+		if fromClientErr != nil {
+			// Drain to prevent a close in the case of a cipher error.
+			io.Copy(io.Discard, clientConn)
+		}
+		clientConn.CloseRead()
+		// Send FIN to target.
+		// We must do this after the drain is completed, otherwise the target will close its
+		// connection with the proxy, which will, in turn, close the connection with the client.
+		tgtConn.CloseWrite()
+		fromClientErrCh <- fromClientErr
+	}()
+	_, fromTargetErr := io.Copy(clientConn, tgtConn)
+	// Send FIN to client.
+	clientConn.CloseWrite()
+	tgtConn.CloseRead()
+
+	fromClientErr := <-fromClientErrCh
+	if fromClientErr != nil {
+		return onet.NewConnectionError("ERR_RELAY_CLIENT", "Failed to relay traffic from client", fromClientErr)
+	}
+	if fromTargetErr != nil {
+		return onet.NewConnectionError("ERR_RELAY_TARGET", "Failed to relay traffic from target", fromTargetErr)
+	}
+	return nil
+}
+
+func (h *streamHandler) handleConnection(ctx context.Context, outerConn transport.StreamConn, connMetrics TCPConnMetrics, proxyMetrics *metrics.ProxyMetrics) *onet.ConnectionError {
+	// Set a deadline to receive the address to the target.
+	readDeadline := time.Now().Add(h.readTimeout)
+	if deadline, ok := ctx.Deadline(); ok {
+		outerConn.SetDeadline(deadline)
+		if deadline.Before(readDeadline) {
+			readDeadline = deadline
+		}
+	}
+	outerConn.SetReadDeadline(readDeadline)
+
+	id, innerConn, authErr := h.authenticate(outerConn)
+	if authErr != nil {
+		// Drain to protect against probing attacks.
+		h.absorbProbe(outerConn, connMetrics, authErr.Status, proxyMetrics)
+		return authErr
+	}
+	connMetrics.AddAuthenticated(id)
+
+	// Read target address and dial it.
+	tgtAddr, err := getProxyRequest(innerConn)
+	// Clear the deadline for the target address
+	outerConn.SetReadDeadline(time.Time{})
+	if err != nil {
+		// Drain to prevent a close on cipher error.
+		io.Copy(io.Discard, outerConn)
+		return onet.NewConnectionError("ERR_READ_ADDRESS", "Failed to get target address", err)
+	}
+
+	dialer := transport.FuncStreamDialer(func(ctx context.Context, addr string) (transport.StreamConn, error) {
+		tgtConn, err := h.dialer.DialStream(ctx, tgtAddr)
+		if err != nil {
+			return nil, err
+		}
+		tgtConn = metrics.MeasureConn(tgtConn, &proxyMetrics.ProxyTarget, &proxyMetrics.TargetProxy)
+		return tgtConn, nil
+	})
+	return proxyConnection(h.logger, ctx, dialer, tgtAddr, innerConn)
+}
+
+// Keep the connection open until we hit the authentication deadline to protect against probing attacks
+// `proxyMetrics` is a pointer because its value is being mutated by `clientConn`.
+func (h *streamHandler) absorbProbe(clientConn io.ReadCloser, connMetrics TCPConnMetrics, status string, proxyMetrics *metrics.ProxyMetrics) {
+	// This line updates proxyMetrics.ClientProxy before it's used in AddTCPProbe.
+	_, drainErr := io.Copy(io.Discard, clientConn) // drain socket
+	drainResult := drainErrToString(drainErr)
+	h.logger.LogAttrs(nil, slog.LevelDebug, "Drain error.", slog.Any("err", drainErr), slog.String("result", drainResult))
+	connMetrics.AddProbe(status, drainResult, proxyMetrics.ClientProxy)
+}
+
+func drainErrToString(drainErr error) string {
+	netErr, ok := drainErr.(net.Error)
+	switch {
+	case drainErr == nil:
+		return "eof"
+	case ok && netErr.Timeout():
+		return "timeout"
+	default:
+		return "other"
+	}
+}
+
+// NoOpTCPConnMetrics is a [TCPConnMetrics] that doesn't do anything. Useful in tests
+// or if you don't want to track metrics.
+type NoOpTCPConnMetrics struct{}
+
+var _ TCPConnMetrics = (*NoOpTCPConnMetrics)(nil)
+
+func (m *NoOpTCPConnMetrics) AddAuthenticated(accessKey string) {}
+
+func (m *NoOpTCPConnMetrics) AddClosed(status string, data metrics.ProxyMetrics, duration time.Duration) {
+}
+
+func (m *NoOpTCPConnMetrics) AddProbe(status, drainResult string, clientProxyBytes int64) {}

+ 40 - 0
vendor/github.com/Jigsaw-Code/outline-ss-server/service/tcp_linux.go

@@ -0,0 +1,40 @@
+// Copyright 2024 Jigsaw Operations LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build linux
+
+package service
+
+import (
+	"net"
+	"syscall"
+
+	"github.com/Jigsaw-Code/outline-sdk/transport"
+
+	onet "github.com/Jigsaw-Code/outline-ss-server/net"
+)
+
+// fwmark can be used in conjunction with other Linux networking features like cgroups, network namespaces, and TC (Traffic Control) for sophisticated network management.
+// Value of 0 disables fwmark (SO_MARK) (Linux Only)
+func MakeValidatingTCPStreamDialer(targetIPValidator onet.TargetIPValidator, fwmark uint) transport.StreamDialer {
+	return &transport.TCPDialer{Dialer: net.Dialer{Control: func(network, address string, c syscall.RawConn) error {
+		if fwmark > 0 {
+			if err := SetFwmark(c, fwmark); err != nil {
+				return err
+			}
+		}
+		ip, _, _ := net.SplitHostPort(address)
+		return targetIPValidator(net.ParseIP(ip))
+	}}}
+}

+ 38 - 0
vendor/github.com/Jigsaw-Code/outline-ss-server/service/tcp_other.go

@@ -0,0 +1,38 @@
+// Copyright 2024 Jigsaw Operations LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build !linux
+
+package service
+
+import (
+	"net"
+	"syscall"
+
+	"github.com/Jigsaw-Code/outline-sdk/transport"
+
+	onet "github.com/Jigsaw-Code/outline-ss-server/net"
+)
+
+// fwmark can be used in conjunction with other Linux networking features like cgroups, network namespaces, and TC (Traffic Control) for sophisticated network management.
+// Value of 0 disables fwmark (SO_MARK) (Linux Only)
+func MakeValidatingTCPStreamDialer(targetIPValidator onet.TargetIPValidator, fwmark uint) transport.StreamDialer {
+	if fwmark != 0 {
+		panic("fwmark is linux-specific feature and should be 0")
+	}
+	return &transport.TCPDialer{Dialer: net.Dialer{Control: func(network, address string, c syscall.RawConn) error {
+		ip, _, _ := net.SplitHostPort(address)
+		return targetIPValidator(net.ParseIP(ip))
+	}}}
+}

+ 497 - 0
vendor/github.com/Jigsaw-Code/outline-ss-server/service/udp.go

@@ -0,0 +1,497 @@
+// Copyright 2018 Jigsaw Operations LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package service
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"log/slog"
+	"net"
+	"net/netip"
+	"runtime/debug"
+	"sync"
+	"time"
+
+	"github.com/Jigsaw-Code/outline-sdk/transport"
+	"github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks"
+	"github.com/shadowsocks/go-shadowsocks2/socks"
+
+	onet "github.com/Jigsaw-Code/outline-ss-server/net"
+)
+
+// UDPConnMetrics is used to report metrics on UDP connections.
+type UDPConnMetrics interface {
+	AddPacketFromClient(status string, clientProxyBytes, proxyTargetBytes int64)
+	AddPacketFromTarget(status string, targetProxyBytes, proxyClientBytes int64)
+	RemoveNatEntry()
+}
+
+type UDPMetrics interface {
+	AddUDPNatEntry(clientAddr net.Addr, accessKey string) UDPConnMetrics
+}
+
+// Max UDP buffer size for the server code.
+const serverUDPBufferSize = 64 * 1024
+
+// Wrapper for slog.Debug during UDP proxying.
+func debugUDP(l *slog.Logger, template string, cipherID string, attr slog.Attr) {
+	// This is an optimization to reduce unnecessary allocations due to an interaction
+	// between Go's inlining/escape analysis and varargs functions like slog.Debug.
+	if l.Enabled(nil, slog.LevelDebug) {
+		l.LogAttrs(nil, slog.LevelDebug, fmt.Sprintf("UDP: %s", template), slog.String("ID", cipherID), attr)
+	}
+}
+
+func debugUDPAddr(l *slog.Logger, template string, addr net.Addr, attr slog.Attr) {
+	if l.Enabled(nil, slog.LevelDebug) {
+		l.LogAttrs(nil, slog.LevelDebug, fmt.Sprintf("UDP: %s", template), slog.String("address", addr.String()), attr)
+	}
+}
+
+// Decrypts src into dst. It tries each cipher until it finds one that authenticates
+// correctly. dst and src must not overlap.
+func findAccessKeyUDP(clientIP netip.Addr, dst, src []byte, cipherList CipherList, l *slog.Logger) ([]byte, string, *shadowsocks.EncryptionKey, error) {
+	// Try each cipher until we find one that authenticates successfully. This assumes that all ciphers are AEAD.
+	// We snapshot the list because it may be modified while we use it.
+	snapshot := cipherList.SnapshotForClientIP(clientIP)
+	for ci, entry := range snapshot {
+		id, cryptoKey := entry.Value.(*CipherEntry).ID, entry.Value.(*CipherEntry).CryptoKey
+		buf, err := shadowsocks.Unpack(dst, src, cryptoKey)
+		if err != nil {
+			debugUDP(l, "Failed to unpack.", id, slog.Any("err", err))
+			continue
+		}
+		debugUDP(l, "Found cipher.", id, slog.Int("index", ci))
+		// Move the active cipher to the front, so that the search is quicker next time.
+		cipherList.MarkUsedByClientIP(entry, clientIP)
+		return buf, id, cryptoKey, nil
+	}
+	return nil, "", nil, errors.New("could not find valid UDP cipher")
+}
+
+type packetHandler struct {
+	logger            *slog.Logger
+	natTimeout        time.Duration
+	ciphers           CipherList
+	m                 UDPMetrics
+	ssm               ShadowsocksConnMetrics
+	targetIPValidator onet.TargetIPValidator
+	targetListener    transport.PacketListener
+}
+
+// NewPacketHandler creates a PacketHandler
+func NewPacketHandler(natTimeout time.Duration, cipherList CipherList, m UDPMetrics, ssMetrics ShadowsocksConnMetrics) PacketHandler {
+	if m == nil {
+		m = &NoOpUDPMetrics{}
+	}
+	if ssMetrics == nil {
+		ssMetrics = &NoOpShadowsocksConnMetrics{}
+	}
+	return &packetHandler{
+		logger:            noopLogger(),
+		natTimeout:        natTimeout,
+		ciphers:           cipherList,
+		m:                 m,
+		ssm:               ssMetrics,
+		targetIPValidator: onet.RequirePublicIP,
+		targetListener:    MakeTargetUDPListener(0),
+	}
+}
+
+// PacketHandler is a running UDP shadowsocks proxy that can be stopped.
+type PacketHandler interface {
+	// SetLogger sets the logger used to log messages. Uses a no-op logger if nil.
+	SetLogger(l *slog.Logger)
+	// SetTargetIPValidator sets the function to be used to validate the target IP addresses.
+	SetTargetIPValidator(targetIPValidator onet.TargetIPValidator)
+	// SetTargetPacketListener sets the packet listener to use for target connections.
+	SetTargetPacketListener(targetListener transport.PacketListener)
+	// Handle returns after clientConn closes and all the sub goroutines return.
+	Handle(clientConn net.PacketConn)
+}
+
+func (h *packetHandler) SetLogger(l *slog.Logger) {
+	if l == nil {
+		l = noopLogger()
+	}
+	h.logger = l
+}
+
+func (h *packetHandler) SetTargetIPValidator(targetIPValidator onet.TargetIPValidator) {
+	h.targetIPValidator = targetIPValidator
+}
+
+func (h *packetHandler) SetTargetPacketListener(targetListener transport.PacketListener) {
+	h.targetListener = targetListener
+}
+
+// Listen on addr for encrypted packets and basically do UDP NAT.
+// We take the ciphers as a pointer because it gets replaced on config updates.
+func (h *packetHandler) Handle(clientConn net.PacketConn) {
+	nm := newNATmap(h.natTimeout, h.m, h.logger)
+	defer nm.Close()
+	cipherBuf := make([]byte, serverUDPBufferSize)
+	textBuf := make([]byte, serverUDPBufferSize)
+
+	for {
+		clientProxyBytes, clientAddr, err := clientConn.ReadFrom(cipherBuf)
+		if errors.Is(err, net.ErrClosed) {
+			break
+		}
+
+		var proxyTargetBytes int
+		var targetConn *natconn
+
+		connError := func() (connError *onet.ConnectionError) {
+			defer func() {
+				if r := recover(); r != nil {
+					slog.Error("Panic in UDP loop: %v. Continuing to listen.", r)
+					debug.PrintStack()
+				}
+			}()
+
+			// Error from ReadFrom
+			if err != nil {
+				return onet.NewConnectionError("ERR_READ", "Failed to read from client", err)
+			}
+			defer slog.LogAttrs(nil, slog.LevelDebug, "UDP: Done", slog.String("address", clientAddr.String()))
+			debugUDPAddr(h.logger, "Outbound packet.", clientAddr, slog.Int("bytes", clientProxyBytes))
+
+			cipherData := cipherBuf[:clientProxyBytes]
+			var payload []byte
+			var tgtUDPAddr *net.UDPAddr
+			targetConn = nm.Get(clientAddr.String())
+			if targetConn == nil {
+				ip := clientAddr.(*net.UDPAddr).AddrPort().Addr()
+				var textData []byte
+				var cryptoKey *shadowsocks.EncryptionKey
+				unpackStart := time.Now()
+				textData, keyID, cryptoKey, err := findAccessKeyUDP(ip, textBuf, cipherData, h.ciphers, h.logger)
+				timeToCipher := time.Since(unpackStart)
+				h.ssm.AddCipherSearch(err == nil, timeToCipher)
+
+				if err != nil {
+					return onet.NewConnectionError("ERR_CIPHER", "Failed to unpack initial packet", err)
+				}
+
+				var onetErr *onet.ConnectionError
+				if payload, tgtUDPAddr, onetErr = h.validatePacket(textData); onetErr != nil {
+					return onetErr
+				}
+
+				udpConn, err := h.targetListener.ListenPacket(context.Background())
+				if err != nil {
+					return onet.NewConnectionError("ERR_CREATE_SOCKET", "Failed to create a `PacketConn`", err)
+				}
+
+				targetConn = nm.Add(clientAddr, clientConn, cryptoKey, udpConn, keyID)
+			} else {
+				unpackStart := time.Now()
+				textData, err := shadowsocks.Unpack(nil, cipherData, targetConn.cryptoKey)
+				timeToCipher := time.Since(unpackStart)
+				h.ssm.AddCipherSearch(err == nil, timeToCipher)
+
+				if err != nil {
+					return onet.NewConnectionError("ERR_CIPHER", "Failed to unpack data from client", err)
+				}
+
+				var onetErr *onet.ConnectionError
+				if payload, tgtUDPAddr, onetErr = h.validatePacket(textData); onetErr != nil {
+					return onetErr
+				}
+			}
+
+			debugUDPAddr(h.logger, "Proxy exit.", clientAddr, slog.Any("target", targetConn.LocalAddr()))
+			proxyTargetBytes, err = targetConn.WriteTo(payload, tgtUDPAddr) // accept only UDPAddr despite the signature
+			if err != nil {
+				return onet.NewConnectionError("ERR_WRITE", "Failed to write to target", err)
+			}
+			return nil
+		}()
+
+		status := "OK"
+		if connError != nil {
+			slog.LogAttrs(nil, slog.LevelDebug, "UDP: Error", slog.String("msg", connError.Message), slog.Any("cause", connError.Cause))
+			status = connError.Status
+		}
+		if targetConn != nil {
+			targetConn.metrics.AddPacketFromClient(status, int64(clientProxyBytes), int64(proxyTargetBytes))
+		}
+	}
+}
+
+// Given the decrypted contents of a UDP packet, return
+// the payload and the destination address, or an error if
+// this packet cannot or should not be forwarded.
+func (h *packetHandler) validatePacket(textData []byte) ([]byte, *net.UDPAddr, *onet.ConnectionError) {
+	tgtAddr := socks.SplitAddr(textData)
+	if tgtAddr == nil {
+		return nil, nil, onet.NewConnectionError("ERR_READ_ADDRESS", "Failed to get target address", nil)
+	}
+
+	tgtUDPAddr, err := net.ResolveUDPAddr("udp", tgtAddr.String())
+	if err != nil {
+		return nil, nil, onet.NewConnectionError("ERR_RESOLVE_ADDRESS", fmt.Sprintf("Failed to resolve target address %v", tgtAddr), err)
+	}
+	if err := h.targetIPValidator(tgtUDPAddr.IP); err != nil {
+		return nil, nil, ensureConnectionError(err, "ERR_ADDRESS_INVALID", "invalid address")
+	}
+
+	payload := textData[len(tgtAddr):]
+	return payload, tgtUDPAddr, nil
+}
+
+func isDNS(addr net.Addr) bool {
+	_, port, _ := net.SplitHostPort(addr.String())
+	return port == "53"
+}
+
+type natconn struct {
+	net.PacketConn
+	cryptoKey *shadowsocks.EncryptionKey
+	metrics   UDPConnMetrics
+	// NAT timeout to apply for non-DNS packets.
+	defaultTimeout time.Duration
+	// Current read deadline of PacketConn.  Used to avoid decreasing the
+	// deadline.  Initially zero.
+	readDeadline time.Time
+	// If the connection has only sent one DNS query, it will close
+	// if it receives a DNS response.
+	fastClose sync.Once
+}
+
+func (c *natconn) onWrite(addr net.Addr) {
+	// Fast close is only allowed if there has been exactly one write,
+	// and it was a DNS query.
+	isDNS := isDNS(addr)
+	isFirstWrite := c.readDeadline.IsZero()
+	if !isDNS || !isFirstWrite {
+		// Disable fast close.  (Idempotent.)
+		c.fastClose.Do(func() {})
+	}
+
+	timeout := c.defaultTimeout
+	if isDNS {
+		// Shorten timeout as required by RFC 5452 Section 10.
+		timeout = 17 * time.Second
+	}
+
+	newDeadline := time.Now().Add(timeout)
+	if newDeadline.After(c.readDeadline) {
+		c.readDeadline = newDeadline
+		c.SetReadDeadline(newDeadline)
+	}
+}
+
+func (c *natconn) onRead(addr net.Addr) {
+	c.fastClose.Do(func() {
+		if isDNS(addr) {
+			// The next ReadFrom() should time out immediately.
+			c.SetReadDeadline(time.Now())
+		}
+	})
+}
+
+func (c *natconn) WriteTo(buf []byte, dst net.Addr) (int, error) {
+	c.onWrite(dst)
+	return c.PacketConn.WriteTo(buf, dst)
+}
+
+func (c *natconn) ReadFrom(buf []byte) (int, net.Addr, error) {
+	n, addr, err := c.PacketConn.ReadFrom(buf)
+	if err == nil {
+		c.onRead(addr)
+	}
+	return n, addr, err
+}
+
+// Packet NAT table
+type natmap struct {
+	sync.RWMutex
+	keyConn map[string]*natconn
+	logger  *slog.Logger
+	timeout time.Duration
+	metrics UDPMetrics
+}
+
+func newNATmap(timeout time.Duration, sm UDPMetrics, l *slog.Logger) *natmap {
+	m := &natmap{logger: l, metrics: sm}
+	m.keyConn = make(map[string]*natconn)
+	m.timeout = timeout
+	return m
+}
+
+func (m *natmap) Get(key string) *natconn {
+	m.RLock()
+	defer m.RUnlock()
+	return m.keyConn[key]
+}
+
+func (m *natmap) set(key string, pc net.PacketConn, cryptoKey *shadowsocks.EncryptionKey, connMetrics UDPConnMetrics) *natconn {
+	entry := &natconn{
+		PacketConn:     pc,
+		cryptoKey:      cryptoKey,
+		metrics:        connMetrics,
+		defaultTimeout: m.timeout,
+	}
+
+	m.Lock()
+	defer m.Unlock()
+
+	m.keyConn[key] = entry
+	return entry
+}
+
+func (m *natmap) del(key string) net.PacketConn {
+	m.Lock()
+	defer m.Unlock()
+
+	entry, ok := m.keyConn[key]
+	if ok {
+		delete(m.keyConn, key)
+		return entry
+	}
+	return nil
+}
+
+func (m *natmap) Add(clientAddr net.Addr, clientConn net.PacketConn, cryptoKey *shadowsocks.EncryptionKey, targetConn net.PacketConn, keyID string) *natconn {
+	connMetrics := m.metrics.AddUDPNatEntry(clientAddr, keyID)
+	entry := m.set(clientAddr.String(), targetConn, cryptoKey, connMetrics)
+
+	go func() {
+		timedCopy(clientAddr, clientConn, entry, m.logger)
+		connMetrics.RemoveNatEntry()
+		if pc := m.del(clientAddr.String()); pc != nil {
+			pc.Close()
+		}
+	}()
+	return entry
+}
+
+func (m *natmap) Close() error {
+	m.Lock()
+	defer m.Unlock()
+
+	var err error
+	now := time.Now()
+	for _, pc := range m.keyConn {
+		if e := pc.SetReadDeadline(now); e != nil {
+			err = e
+		}
+	}
+	return err
+}
+
+// Get the maximum length of the shadowsocks address header by parsing
+// and serializing an IPv6 address from the example range.
+var maxAddrLen int = len(socks.ParseAddr("[2001:db8::1]:12345"))
+
+// copy from target to client until read timeout
+func timedCopy(clientAddr net.Addr, clientConn net.PacketConn, targetConn *natconn, l *slog.Logger) {
+	// pkt is used for in-place encryption of downstream UDP packets, with the layout
+	// [padding?][salt][address][body][tag][extra]
+	// Padding is only used if the address is IPv4.
+	pkt := make([]byte, serverUDPBufferSize)
+
+	saltSize := targetConn.cryptoKey.SaltSize()
+	// Leave enough room at the beginning of the packet for a max-length header (i.e. IPv6).
+	bodyStart := saltSize + maxAddrLen
+
+	expired := false
+	for {
+		var bodyLen, proxyClientBytes int
+		connError := func() (connError *onet.ConnectionError) {
+			var (
+				raddr net.Addr
+				err   error
+			)
+			// `readBuf` receives the plaintext body in `pkt`:
+			// [padding?][salt][address][body][tag][unused]
+			// |--     bodyStart     --|[      readBuf    ]
+			readBuf := pkt[bodyStart:]
+			bodyLen, raddr, err = targetConn.ReadFrom(readBuf)
+			if err != nil {
+				if netErr, ok := err.(net.Error); ok {
+					if netErr.Timeout() {
+						expired = true
+						return nil
+					}
+				}
+				return onet.NewConnectionError("ERR_READ", "Failed to read from target", err)
+			}
+
+			debugUDPAddr(l, "Got response.", clientAddr, slog.Any("target", raddr))
+			srcAddr := socks.ParseAddr(raddr.String())
+			addrStart := bodyStart - len(srcAddr)
+			// `plainTextBuf` concatenates the SOCKS address and body:
+			// [padding?][salt][address][body][tag][unused]
+			// |-- addrStart -|[plaintextBuf ]
+			plaintextBuf := pkt[addrStart : bodyStart+bodyLen]
+			copy(plaintextBuf, srcAddr)
+
+			// saltStart is 0 if raddr is IPv6.
+			saltStart := addrStart - saltSize
+			// `packBuf` adds space for the salt and tag.
+			// `buf` shows the space that was used.
+			// [padding?][salt][address][body][tag][unused]
+			//           [            packBuf             ]
+			//           [          buf           ]
+			packBuf := pkt[saltStart:]
+			buf, err := shadowsocks.Pack(packBuf, plaintextBuf, targetConn.cryptoKey) // Encrypt in-place
+			if err != nil {
+				return onet.NewConnectionError("ERR_PACK", "Failed to pack data to client", err)
+			}
+			proxyClientBytes, err = clientConn.WriteTo(buf, clientAddr)
+			if err != nil {
+				return onet.NewConnectionError("ERR_WRITE", "Failed to write to client", err)
+			}
+			return nil
+		}()
+		status := "OK"
+		if connError != nil {
+			slog.LogAttrs(nil, slog.LevelDebug, "UDP: Error", slog.String("msg", connError.Message), slog.Any("cause", connError.Cause))
+			status = connError.Status
+		}
+		if expired {
+			break
+		}
+		targetConn.metrics.AddPacketFromTarget(status, int64(bodyLen), int64(proxyClientBytes))
+	}
+}
+
+// NoOpUDPConnMetrics is a [UDPConnMetrics] that doesn't do anything. Useful in tests
+// or if you don't want to track metrics.
+type NoOpUDPConnMetrics struct{}
+
+var _ UDPConnMetrics = (*NoOpUDPConnMetrics)(nil)
+
+func (m *NoOpUDPConnMetrics) AddPacketFromClient(status string, clientProxyBytes, proxyTargetBytes int64) {
+}
+
+func (m *NoOpUDPConnMetrics) AddPacketFromTarget(status string, targetProxyBytes, proxyClientBytes int64) {
+}
+
+func (m *NoOpUDPConnMetrics) RemoveNatEntry() {}
+
+// NoOpUDPMetrics is a [UDPMetrics] that doesn't do anything. Useful in tests
+// or if you don't want to track metrics.
+type NoOpUDPMetrics struct{}
+
+var _ UDPMetrics = (*NoOpUDPMetrics)(nil)
+
+func (m *NoOpUDPMetrics) AddUDPNatEntry(clientAddr net.Addr, accessKey string) UDPConnMetrics {
+	return &NoOpUDPConnMetrics{}
+}

+ 61 - 0
vendor/github.com/Jigsaw-Code/outline-ss-server/service/udp_linux.go

@@ -0,0 +1,61 @@
+// Copyright 2024 Jigsaw Operations LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in comlniance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by aplnicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or imlnied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build linux
+
+package service
+
+import (
+	"context"
+	"fmt"
+	"net"
+
+	"github.com/Jigsaw-Code/outline-sdk/transport"
+)
+
+type udpListener struct {
+	// fwmark can be used in conjunction with other Linux networking features like cgroups, network
+	// namespaces, and TC (Traffic Control) for sophisticated network management.
+	// Value of 0 disables fwmark (SO_MARK) (Linux only)
+	fwmark uint
+}
+
+// NewPacketListener creates a new PacketListener that listens on UDP
+// and optionally sets a firewall mark on the socket (Linux only).
+func MakeTargetUDPListener(fwmark uint) transport.PacketListener {
+	return &udpListener{fwmark: fwmark}
+}
+
+func (ln *udpListener) ListenPacket(ctx context.Context) (net.PacketConn, error) {
+	conn, err := net.ListenUDP("udp", nil)
+	if err != nil {
+		return nil, fmt.Errorf("Failed to create UDP socket: %w", err)
+	}
+
+	if ln.fwmark > 0 {
+		rawConn, err := conn.SyscallConn()
+		if err != nil {
+			conn.Close()
+			return nil, fmt.Errorf("failed to get UDP raw connection: %w", err)
+		}
+
+		err = SetFwmark(rawConn, ln.fwmark)
+		if err != nil {
+			conn.Close()
+			return nil, fmt.Errorf("Failed to set `fwmark`: %w", err)
+
+		}
+	}
+	return conn, nil
+}

+ 30 - 0
vendor/github.com/Jigsaw-Code/outline-ss-server/service/udp_other.go

@@ -0,0 +1,30 @@
+// Copyright 2024 Jigsaw Operations LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build !linux
+
+package service
+
+import (
+	"github.com/Jigsaw-Code/outline-sdk/transport"
+)
+
+// fwmark can be used in conjunction with other Linux networking features like cgroups, network namespaces, and TC (Traffic Control) for sophisticated network management.
+// Value of 0 disables fwmark (SO_MARK)
+func MakeTargetUDPListener(fwmark uint) transport.PacketListener {
+	if fwmark != 0 {
+		panic("fwmark is linux-specific feature and should be 0")
+	}
+	return &transport.UDPListener{Address: ""}
+}

+ 3 - 0
vendor/golang.org/x/sync/errgroup/errgroup.go

@@ -4,6 +4,9 @@
 
 // Package errgroup provides synchronization, error propagation, and Context
 // cancelation for groups of goroutines working on subtasks of a common task.
+//
+// [errgroup.Group] is related to [sync.WaitGroup] but adds handling of tasks
+// returning errors.
 package errgroup
 
 import (

+ 0 - 102
vendor/golang.org/x/sys/execabs/execabs.go

@@ -1,102 +0,0 @@
-// Copyright 2020 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-// Package execabs is a drop-in replacement for os/exec
-// that requires PATH lookups to find absolute paths.
-// That is, execabs.Command("cmd") runs the same PATH lookup
-// as exec.Command("cmd"), but if the result is a path
-// which is relative, the Run and Start methods will report
-// an error instead of running the executable.
-//
-// See https://blog.golang.org/path-security for more information
-// about when it may be necessary or appropriate to use this package.
-package execabs
-
-import (
-	"context"
-	"fmt"
-	"os/exec"
-	"path/filepath"
-	"reflect"
-	"unsafe"
-)
-
-// ErrNotFound is the error resulting if a path search failed to find an executable file.
-// It is an alias for exec.ErrNotFound.
-var ErrNotFound = exec.ErrNotFound
-
-// Cmd represents an external command being prepared or run.
-// It is an alias for exec.Cmd.
-type Cmd = exec.Cmd
-
-// Error is returned by LookPath when it fails to classify a file as an executable.
-// It is an alias for exec.Error.
-type Error = exec.Error
-
-// An ExitError reports an unsuccessful exit by a command.
-// It is an alias for exec.ExitError.
-type ExitError = exec.ExitError
-
-func relError(file, path string) error {
-	return fmt.Errorf("%s resolves to executable in current directory (.%c%s)", file, filepath.Separator, path)
-}
-
-// LookPath searches for an executable named file in the directories
-// named by the PATH environment variable. If file contains a slash,
-// it is tried directly and the PATH is not consulted. The result will be
-// an absolute path.
-//
-// LookPath differs from exec.LookPath in its handling of PATH lookups,
-// which are used for file names without slashes. If exec.LookPath's
-// PATH lookup would have returned an executable from the current directory,
-// LookPath instead returns an error.
-func LookPath(file string) (string, error) {
-	path, err := exec.LookPath(file)
-	if err != nil && !isGo119ErrDot(err) {
-		return "", err
-	}
-	if filepath.Base(file) == file && !filepath.IsAbs(path) {
-		return "", relError(file, path)
-	}
-	return path, nil
-}
-
-func fixCmd(name string, cmd *exec.Cmd) {
-	if filepath.Base(name) == name && !filepath.IsAbs(cmd.Path) && !isGo119ErrFieldSet(cmd) {
-		// exec.Command was called with a bare binary name and
-		// exec.LookPath returned a path which is not absolute.
-		// Set cmd.lookPathErr and clear cmd.Path so that it
-		// cannot be run.
-		lookPathErr := (*error)(unsafe.Pointer(reflect.ValueOf(cmd).Elem().FieldByName("lookPathErr").Addr().Pointer()))
-		if *lookPathErr == nil {
-			*lookPathErr = relError(name, cmd.Path)
-		}
-		cmd.Path = ""
-	}
-}
-
-// CommandContext is like Command but includes a context.
-//
-// The provided context is used to kill the process (by calling os.Process.Kill)
-// if the context becomes done before the command completes on its own.
-func CommandContext(ctx context.Context, name string, arg ...string) *exec.Cmd {
-	cmd := exec.CommandContext(ctx, name, arg...)
-	fixCmd(name, cmd)
-	return cmd
-
-}
-
-// Command returns the Cmd struct to execute the named program with the given arguments.
-// See exec.Command for most details.
-//
-// Command differs from exec.Command in its handling of PATH lookups,
-// which are used when the program name contains no slashes.
-// If exec.Command would have returned an exec.Cmd configured to run an
-// executable from the current directory, Command instead
-// returns an exec.Cmd that will return an error from Start or Run.
-func Command(name string, arg ...string) *exec.Cmd {
-	cmd := exec.Command(name, arg...)
-	fixCmd(name, cmd)
-	return cmd
-}

+ 0 - 17
vendor/golang.org/x/sys/execabs/execabs_go118.go

@@ -1,17 +0,0 @@
-// Copyright 2022 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-//go:build !go1.19
-
-package execabs
-
-import "os/exec"
-
-func isGo119ErrDot(err error) bool {
-	return false
-}
-
-func isGo119ErrFieldSet(cmd *exec.Cmd) bool {
-	return false
-}

+ 0 - 20
vendor/golang.org/x/sys/execabs/execabs_go119.go

@@ -1,20 +0,0 @@
-// Copyright 2022 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-//go:build go1.19
-
-package execabs
-
-import (
-	"errors"
-	"os/exec"
-)
-
-func isGo119ErrDot(err error) bool {
-	return errors.Is(err, exec.ErrDot)
-}
-
-func isGo119ErrFieldSet(cmd *exec.Cmd) bool {
-	return cmd.Err != nil
-}

+ 1 - 1
vendor/golang.org/x/tools/go/packages/external.go

@@ -12,8 +12,8 @@ import (
 	"bytes"
 	"encoding/json"
 	"fmt"
-	exec "golang.org/x/sys/execabs"
 	"os"
+	"os/exec"
 	"strings"
 )
 

+ 1 - 1
vendor/golang.org/x/tools/go/packages/golist.go

@@ -11,6 +11,7 @@ import (
 	"fmt"
 	"log"
 	"os"
+	"os/exec"
 	"path"
 	"path/filepath"
 	"reflect"
@@ -20,7 +21,6 @@ import (
 	"sync"
 	"unicode"
 
-	exec "golang.org/x/sys/execabs"
 	"golang.org/x/tools/go/internal/packagesdriver"
 	"golang.org/x/tools/internal/gocommand"
 	"golang.org/x/tools/internal/packagesinternal"

+ 2 - 6
vendor/golang.org/x/tools/go/packages/packages.go

@@ -29,6 +29,7 @@ import (
 	"golang.org/x/tools/internal/packagesinternal"
 	"golang.org/x/tools/internal/typeparams"
 	"golang.org/x/tools/internal/typesinternal"
+	"golang.org/x/tools/internal/versions"
 )
 
 // A LoadMode controls the amount of detail to return when loading.
@@ -432,12 +433,6 @@ func init() {
 	packagesinternal.GetDepsErrors = func(p interface{}) []*packagesinternal.PackageError {
 		return p.(*Package).depsErrors
 	}
-	packagesinternal.GetGoCmdRunner = func(config interface{}) *gocommand.Runner {
-		return config.(*Config).gocmdRunner
-	}
-	packagesinternal.SetGoCmdRunner = func(config interface{}, runner *gocommand.Runner) {
-		config.(*Config).gocmdRunner = runner
-	}
 	packagesinternal.SetModFile = func(config interface{}, value string) {
 		config.(*Config).modFile = value
 	}
@@ -1024,6 +1019,7 @@ func (ld *loader) loadPackage(lpkg *loaderPackage) {
 		Selections: make(map[*ast.SelectorExpr]*types.Selection),
 	}
 	typeparams.InitInstanceInfo(lpkg.TypesInfo)
+	versions.InitFileVersions(lpkg.TypesInfo)
 	lpkg.TypesSizes = ld.sizes
 
 	importer := importerFunc(func(path string) (*types.Package, error) {

+ 1 - 2
vendor/golang.org/x/tools/internal/gocommand/invoke.go

@@ -13,6 +13,7 @@ import (
 	"io"
 	"log"
 	"os"
+	"os/exec"
 	"reflect"
 	"regexp"
 	"runtime"
@@ -21,8 +22,6 @@ import (
 	"sync"
 	"time"
 
-	exec "golang.org/x/sys/execabs"
-
 	"golang.org/x/tools/internal/event"
 	"golang.org/x/tools/internal/event/keys"
 	"golang.org/x/tools/internal/event/label"

+ 0 - 8
vendor/golang.org/x/tools/internal/packagesinternal/packages.go

@@ -5,10 +5,6 @@
 // Package packagesinternal exposes internal-only fields from go/packages.
 package packagesinternal
 
-import (
-	"golang.org/x/tools/internal/gocommand"
-)
-
 var GetForTest = func(p interface{}) string { return "" }
 var GetDepsErrors = func(p interface{}) []*PackageError { return nil }
 
@@ -18,10 +14,6 @@ type PackageError struct {
 	Err         string   // the error itself
 }
 
-var GetGoCmdRunner = func(config interface{}) *gocommand.Runner { return nil }
-
-var SetGoCmdRunner = func(config interface{}, runner *gocommand.Runner) {}
-
 var TypecheckCgo int
 var DepsErrors int // must be set as a LoadMode to call GetDepsErrors
 var ForTest int    // must be set as a LoadMode to call GetForTest

+ 172 - 0
vendor/golang.org/x/tools/internal/versions/gover.go

@@ -0,0 +1,172 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This is a fork of internal/gover for use by x/tools until
+// go1.21 and earlier are no longer supported by x/tools.
+
+package versions
+
+import "strings"
+
+// A gover is a parsed Go gover: major[.Minor[.Patch]][kind[pre]]
+// The numbers are the original decimal strings to avoid integer overflows
+// and since there is very little actual math. (Probably overflow doesn't matter in practice,
+// but at the time this code was written, there was an existing test that used
+// go1.99999999999, which does not fit in an int on 32-bit platforms.
+// The "big decimal" representation avoids the problem entirely.)
+type gover struct {
+	major string // decimal
+	minor string // decimal or ""
+	patch string // decimal or ""
+	kind  string // "", "alpha", "beta", "rc"
+	pre   string // decimal or ""
+}
+
+// compare returns -1, 0, or +1 depending on whether
+// x < y, x == y, or x > y, interpreted as toolchain versions.
+// The versions x and y must not begin with a "go" prefix: just "1.21" not "go1.21".
+// Malformed versions compare less than well-formed versions and equal to each other.
+// The language version "1.21" compares less than the release candidate and eventual releases "1.21rc1" and "1.21.0".
+func compare(x, y string) int {
+	vx := parse(x)
+	vy := parse(y)
+
+	if c := cmpInt(vx.major, vy.major); c != 0 {
+		return c
+	}
+	if c := cmpInt(vx.minor, vy.minor); c != 0 {
+		return c
+	}
+	if c := cmpInt(vx.patch, vy.patch); c != 0 {
+		return c
+	}
+	if c := strings.Compare(vx.kind, vy.kind); c != 0 { // "" < alpha < beta < rc
+		return c
+	}
+	if c := cmpInt(vx.pre, vy.pre); c != 0 {
+		return c
+	}
+	return 0
+}
+
+// lang returns the Go language version. For example, lang("1.2.3") == "1.2".
+func lang(x string) string {
+	v := parse(x)
+	if v.minor == "" || v.major == "1" && v.minor == "0" {
+		return v.major
+	}
+	return v.major + "." + v.minor
+}
+
+// isValid reports whether the version x is valid.
+func isValid(x string) bool {
+	return parse(x) != gover{}
+}
+
+// parse parses the Go version string x into a version.
+// It returns the zero version if x is malformed.
+func parse(x string) gover {
+	var v gover
+
+	// Parse major version.
+	var ok bool
+	v.major, x, ok = cutInt(x)
+	if !ok {
+		return gover{}
+	}
+	if x == "" {
+		// Interpret "1" as "1.0.0".
+		v.minor = "0"
+		v.patch = "0"
+		return v
+	}
+
+	// Parse . before minor version.
+	if x[0] != '.' {
+		return gover{}
+	}
+
+	// Parse minor version.
+	v.minor, x, ok = cutInt(x[1:])
+	if !ok {
+		return gover{}
+	}
+	if x == "" {
+		// Patch missing is same as "0" for older versions.
+		// Starting in Go 1.21, patch missing is different from explicit .0.
+		if cmpInt(v.minor, "21") < 0 {
+			v.patch = "0"
+		}
+		return v
+	}
+
+	// Parse patch if present.
+	if x[0] == '.' {
+		v.patch, x, ok = cutInt(x[1:])
+		if !ok || x != "" {
+			// Note that we are disallowing prereleases (alpha, beta, rc) for patch releases here (x != "").
+			// Allowing them would be a bit confusing because we already have:
+			//	1.21 < 1.21rc1
+			// But a prerelease of a patch would have the opposite effect:
+			//	1.21.3rc1 < 1.21.3
+			// We've never needed them before, so let's not start now.
+			return gover{}
+		}
+		return v
+	}
+
+	// Parse prerelease.
+	i := 0
+	for i < len(x) && (x[i] < '0' || '9' < x[i]) {
+		if x[i] < 'a' || 'z' < x[i] {
+			return gover{}
+		}
+		i++
+	}
+	if i == 0 {
+		return gover{}
+	}
+	v.kind, x = x[:i], x[i:]
+	if x == "" {
+		return v
+	}
+	v.pre, x, ok = cutInt(x)
+	if !ok || x != "" {
+		return gover{}
+	}
+
+	return v
+}
+
+// cutInt scans the leading decimal number at the start of x to an integer
+// and returns that value and the rest of the string.
+func cutInt(x string) (n, rest string, ok bool) {
+	i := 0
+	for i < len(x) && '0' <= x[i] && x[i] <= '9' {
+		i++
+	}
+	if i == 0 || x[0] == '0' && i != 1 { // no digits or unnecessary leading zero
+		return "", "", false
+	}
+	return x[:i], x[i:], true
+}
+
+// cmpInt returns cmp.Compare(x, y) interpreting x and y as decimal numbers.
+// (Copied from golang.org/x/mod/semver's compareInt.)
+func cmpInt(x, y string) int {
+	if x == y {
+		return 0
+	}
+	if len(x) < len(y) {
+		return -1
+	}
+	if len(x) > len(y) {
+		return +1
+	}
+	if x < y {
+		return -1
+	} else {
+		return +1
+	}
+}

+ 19 - 0
vendor/golang.org/x/tools/internal/versions/types.go

@@ -0,0 +1,19 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package versions
+
+import (
+	"go/types"
+)
+
+// GoVersion returns the Go version of the type package.
+// It returns zero if no version can be determined.
+func GoVersion(pkg *types.Package) string {
+	// TODO(taking): x/tools can call GoVersion() [from 1.21] after 1.25.
+	if pkg, ok := any(pkg).(interface{ GoVersion() string }); ok {
+		return pkg.GoVersion()
+	}
+	return ""
+}

+ 20 - 0
vendor/golang.org/x/tools/internal/versions/types_go121.go

@@ -0,0 +1,20 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build !go1.22
+// +build !go1.22
+
+package versions
+
+import (
+	"go/ast"
+	"go/types"
+)
+
+// FileVersions always reports the a file's Go version as the
+// zero version at this Go version.
+func FileVersions(info *types.Info, file *ast.File) string { return "" }
+
+// InitFileVersions is a noop at this Go version.
+func InitFileVersions(*types.Info) {}

+ 24 - 0
vendor/golang.org/x/tools/internal/versions/types_go122.go

@@ -0,0 +1,24 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build go1.22
+// +build go1.22
+
+package versions
+
+import (
+	"go/ast"
+	"go/types"
+)
+
+// FileVersions maps a file to the file's semantic Go version.
+// The reported version is the zero version if a version cannot be determined.
+func FileVersions(info *types.Info, file *ast.File) string {
+	return info.FileVersions[file]
+}
+
+// InitFileVersions initializes info to record Go versions for Go files.
+func InitFileVersions(info *types.Info) {
+	info.FileVersions = make(map[*ast.File]string)
+}

+ 49 - 0
vendor/golang.org/x/tools/internal/versions/versions_go121.go

@@ -0,0 +1,49 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build !go1.22
+// +build !go1.22
+
+package versions
+
+// Lang returns the Go language version for version x.
+// If x is not a valid version, Lang returns the empty string.
+// For example:
+//
+//	Lang("go1.21rc2") = "go1.21"
+//	Lang("go1.21.2") = "go1.21"
+//	Lang("go1.21") = "go1.21"
+//	Lang("go1") = "go1"
+//	Lang("bad") = ""
+//	Lang("1.21") = ""
+func Lang(x string) string {
+	v := lang(stripGo(x))
+	if v == "" {
+		return ""
+	}
+	return x[:2+len(v)] // "go"+v without allocation
+}
+
+// Compare returns -1, 0, or +1 depending on whether
+// x < y, x == y, or x > y, interpreted as Go versions.
+// The versions x and y must begin with a "go" prefix: "go1.21" not "1.21".
+// Invalid versions, including the empty string, compare less than
+// valid versions and equal to each other.
+// The language version "go1.21" compares less than the
+// release candidate and eventual releases "go1.21rc1" and "go1.21.0".
+// Custom toolchain suffixes are ignored during comparison:
+// "go1.21.0" and "go1.21.0-bigcorp" are equal.
+func Compare(x, y string) int { return compare(stripGo(x), stripGo(y)) }
+
+// IsValid reports whether the version x is valid.
+func IsValid(x string) bool { return isValid(stripGo(x)) }
+
+// stripGo converts from a "go1.21" version to a "1.21" version.
+// If v does not start with "go", stripGo returns the empty string (a known invalid version).
+func stripGo(v string) string {
+	if len(v) < 2 || v[:2] != "go" {
+		return ""
+	}
+	return v[2:]
+}

+ 38 - 0
vendor/golang.org/x/tools/internal/versions/versions_go122.go

@@ -0,0 +1,38 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build go1.22
+// +build go1.22
+
+package versions
+
+import (
+	"go/version"
+)
+
+// Lang returns the Go language version for version x.
+// If x is not a valid version, Lang returns the empty string.
+// For example:
+//
+//	Lang("go1.21rc2") = "go1.21"
+//	Lang("go1.21.2") = "go1.21"
+//	Lang("go1.21") = "go1.21"
+//	Lang("go1") = "go1"
+//	Lang("bad") = ""
+//	Lang("1.21") = ""
+func Lang(x string) string { return version.Lang(x) }
+
+// Compare returns -1, 0, or +1 depending on whether
+// x < y, x == y, or x > y, interpreted as Go versions.
+// The versions x and y must begin with a "go" prefix: "go1.21" not "1.21".
+// Invalid versions, including the empty string, compare less than
+// valid versions and equal to each other.
+// The language version "go1.21" compares less than the
+// release candidate and eventual releases "go1.21rc1" and "go1.21.0".
+// Custom toolchain suffixes are ignored during comparison:
+// "go1.21.0" and "go1.21.0-bigcorp" are equal.
+func Compare(x, y string) int { return version.Compare(x, y) }
+
+// IsValid reports whether the version x is valid.
+func IsValid(x string) bool { return version.IsValid(x) }

+ 9 - 4
vendor/modules.txt

@@ -16,6 +16,11 @@ github.com/AndreasBriese/bbloom
 github.com/Jigsaw-Code/outline-sdk/internal/slicepool
 github.com/Jigsaw-Code/outline-sdk/transport
 github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks
+# github.com/Jigsaw-Code/outline-ss-server v1.8.0
+## explicit; go 1.21
+github.com/Jigsaw-Code/outline-ss-server/net
+github.com/Jigsaw-Code/outline-ss-server/service
+github.com/Jigsaw-Code/outline-ss-server/service/metrics
 # github.com/Psiphon-Inc/rotate-safe-writer v0.0.0-20210303140923-464a7a37606e
 ## explicit
 github.com/Psiphon-Inc/rotate-safe-writer
@@ -541,7 +546,7 @@ golang.org/x/crypto/nacl/box
 golang.org/x/crypto/nacl/secretbox
 golang.org/x/crypto/salsa20/salsa
 golang.org/x/crypto/sha3
-# golang.org/x/exp v0.0.0-20230905200255-921286631fa9
+# golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e
 ## explicit; go 1.20
 golang.org/x/exp/constraints
 golang.org/x/exp/rand
@@ -566,13 +571,12 @@ golang.org/x/net/ipv6
 golang.org/x/net/proxy
 golang.org/x/net/route
 golang.org/x/net/trace
-# golang.org/x/sync v0.5.0
+# golang.org/x/sync v0.6.0
 ## explicit; go 1.18
 golang.org/x/sync/errgroup
 # golang.org/x/sys v0.20.0
 ## explicit; go 1.18
 golang.org/x/sys/cpu
-golang.org/x/sys/execabs
 golang.org/x/sys/plan9
 golang.org/x/sys/unix
 golang.org/x/sys/windows
@@ -591,7 +595,7 @@ golang.org/x/text/unicode/norm
 # golang.org/x/time v0.5.0
 ## explicit; go 1.18
 golang.org/x/time/rate
-# golang.org/x/tools v0.15.0
+# golang.org/x/tools v0.16.0
 ## explicit; go 1.18
 golang.org/x/tools/go/ast/inspector
 golang.org/x/tools/go/gcexportdata
@@ -610,6 +614,7 @@ golang.org/x/tools/internal/pkgbits
 golang.org/x/tools/internal/tokeninternal
 golang.org/x/tools/internal/typeparams
 golang.org/x/tools/internal/typesinternal
+golang.org/x/tools/internal/versions
 # golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b
 ## explicit; go 1.20
 golang.zx2c4.com/wireguard/replay