Browse Source

Add Shadowsocks protocol

Miro 1 year ago
parent
commit
61ebd3e5c6
31 changed files with 2817 additions and 8 deletions
  1. 2 0
      go.mod
  2. 9 0
      go.sum
  3. 13 3
      psiphon/common/protocol/protocol.go
  4. 9 0
      psiphon/common/protocol/serverEntry.go
  5. 2 1
      psiphon/config.go
  6. 11 0
      psiphon/controller_test.go
  7. 21 1
      psiphon/dialParameters.go
  8. 24 3
      psiphon/server/config.go
  9. 11 0
      psiphon/server/server_test.go
  10. 140 0
      psiphon/server/shadowsocks.go
  11. 5 0
      psiphon/server/tunnelServer.go
  12. 63 0
      psiphon/shadowsocksConn.go
  13. 10 0
      psiphon/tunnel.go
  14. 201 0
      vendor/github.com/Jigsaw-Code/outline-sdk/LICENSE
  15. 88 0
      vendor/github.com/Jigsaw-Code/outline-sdk/internal/slicepool/slicepool.go
  16. 65 0
      vendor/github.com/Jigsaw-Code/outline-sdk/transport/address.go
  17. 47 0
      vendor/github.com/Jigsaw-Code/outline-sdk/transport/doc.go
  18. 259 0
      vendor/github.com/Jigsaw-Code/outline-sdk/transport/happyeyeballs.go
  19. 177 0
      vendor/github.com/Jigsaw-Code/outline-sdk/transport/packet.go
  20. 157 0
      vendor/github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks/cipher.go
  21. 58 0
      vendor/github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks/client_testing.go
  22. 61 0
      vendor/github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks/doc.go
  23. 82 0
      vendor/github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks/packet.go
  24. 117 0
      vendor/github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks/packet_listener.go
  25. 66 0
      vendor/github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks/salt.go
  26. 433 0
      vendor/github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks/stream.go
  27. 104 0
      vendor/github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks/stream_dialer.go
  28. 158 0
      vendor/github.com/Jigsaw-Code/outline-sdk/transport/stream.go
  29. 202 0
      vendor/github.com/shadowsocks/go-shadowsocks2/LICENSE
  30. 214 0
      vendor/github.com/shadowsocks/go-shadowsocks2/socks/socks.go
  31. 8 0
      vendor/modules.txt

+ 2 - 0
go.mod

@@ -31,6 +31,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/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
@@ -73,6 +74,7 @@ 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

+ 9 - 0
go.sum

@@ -8,6 +8,8 @@ github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIo
 github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
 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/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=
@@ -226,6 +228,8 @@ github.com/refraction-networking/obfs4 v0.1.2 h1:J842O4fGSkd2W8ogYj0KN6gqVVY+Cpq
 github.com/refraction-networking/obfs4 v0.1.2/go.mod h1:wAl/+gWiLsrcykJA3nKJHx89f5/gXGM8UKvty7+mvbM=
 github.com/refraction-networking/utls v1.3.3 h1:f/TBLX7KBciRyFH3bwupp+CE4fzoYKCirhdRcC490sw=
 github.com/refraction-networking/utls v1.3.3/go.mod h1:DlecWW1LMlMJu+9qpzzQqdHDT/C2LAe03EdpLUz/RL8=
+github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
+github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
 github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc=
 github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
 github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
@@ -233,6 +237,8 @@ github.com/ryanuber/go-glob v0.0.0-20170128012129-256dc444b735 h1:7YvPJVmEeFHR1T
 github.com/ryanuber/go-glob v0.0.0-20170128012129-256dc444b735/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
 github.com/sergeyfrolov/bsbuffer v0.0.0-20180903213811-94e85abb8507 h1:ML7ZNtcln5UBo5Wv7RIv9Xg3Pr5VuRCWLFXEwda54Y4=
 github.com/sergeyfrolov/bsbuffer v0.0.0-20180903213811-94e85abb8507/go.mod h1:DbI1gxrXI2jRGw7XGEUZQOOMd6PsnKzRrCKabvvMrwM=
+github.com/shadowsocks/go-shadowsocks2 v0.1.5 h1:PDSQv9y2S85Fl7VBeOMF9StzeXZyK1HakRm86CUbr28=
+github.com/shadowsocks/go-shadowsocks2 v0.1.5/go.mod h1:AGGpIoek4HRno4xzyFiAtLHkOpcoznZEkAccaI/rplM=
 github.com/shirou/gopsutil/v4 v4.24.5 h1:gGsArG5K6vmsh5hcFOHaPm87UD003CaDMkAOweSQjhM=
 github.com/shirou/gopsutil/v4 v4.24.5/go.mod h1:aoebb2vxetJ/yIDZISmduFvVNPHqXQ9SEJwRXxkf0RA=
 github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
@@ -292,6 +298,7 @@ go4.org/netipx v0.0.0-20230824141953-6213f710f925 h1:eeQDDVKFkx0g4Hyy8pHgmZaK0Eq
 go4.org/netipx v0.0.0-20230824141953-6213f710f925/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
 golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
@@ -338,6 +345,7 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -361,6 +369,7 @@ golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
 golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=

+ 13 - 3
psiphon/common/protocol/protocol.go

@@ -45,6 +45,7 @@ const (
 	TUNNEL_PROTOCOL_FRONTED_MEEK_QUIC_OBFUSCATED_SSH = "FRONTED-MEEK-QUIC-OSSH"
 	TUNNEL_PROTOCOL_TAPDANCE_OBFUSCATED_SSH          = "TAPDANCE-OSSH"
 	TUNNEL_PROTOCOL_CONJURE_OBFUSCATED_SSH           = "CONJURE-OSSH"
+	TUNNEL_PROTOCOL_SHADOWSOCKS_SSH                  = "SHADOWSOCKS-SSH"
 
 	FRONTING_TRANSPORT_HTTPS = "FRONTED-HTTPS"
 	FRONTING_TRANSPORT_HTTP  = "FRONTED-HTTP"
@@ -198,6 +199,7 @@ var SupportedTunnelProtocols = TunnelProtocols{
 	TUNNEL_PROTOCOL_FRONTED_MEEK_QUIC_OBFUSCATED_SSH,
 	TUNNEL_PROTOCOL_TAPDANCE_OBFUSCATED_SSH,
 	TUNNEL_PROTOCOL_CONJURE_OBFUSCATED_SSH,
+	TUNNEL_PROTOCOL_SHADOWSOCKS_SSH,
 }
 
 var DefaultDisabledTunnelProtocols = TunnelProtocols{
@@ -355,6 +357,11 @@ func TunnelProtocolUsesConjure(protocol string) bool {
 	return protocol == TUNNEL_PROTOCOL_CONJURE_OBFUSCATED_SSH
 }
 
+func TunnelProtocolUsesShadowsocks(protocol string) bool {
+	protocol = TunnelProtocolMinusInproxy(protocol)
+	return protocol == TUNNEL_PROTOCOL_SHADOWSOCKS_SSH
+}
+
 func TunnelProtocolIsResourceIntensive(protocol string) bool {
 	return TunnelProtocolUsesMeek(protocol) ||
 		TunnelProtocolUsesQUIC(protocol) ||
@@ -372,7 +379,8 @@ func TunnelProtocolIsCompatibleWithFragmentor(protocol string) bool {
 		protocol == TUNNEL_PROTOCOL_UNFRONTED_MEEK_SESSION_TICKET ||
 		protocol == TUNNEL_PROTOCOL_FRONTED_MEEK ||
 		protocol == TUNNEL_PROTOCOL_FRONTED_MEEK_HTTP ||
-		protocol == TUNNEL_PROTOCOL_CONJURE_OBFUSCATED_SSH
+		protocol == TUNNEL_PROTOCOL_CONJURE_OBFUSCATED_SSH ||
+		protocol == TUNNEL_PROTOCOL_SHADOWSOCKS_SSH
 }
 
 func TunnelProtocolIsDirect(protocol string) bool {
@@ -382,7 +390,8 @@ func TunnelProtocolIsDirect(protocol string) bool {
 		protocol == TUNNEL_PROTOCOL_UNFRONTED_MEEK ||
 		protocol == TUNNEL_PROTOCOL_UNFRONTED_MEEK_HTTPS ||
 		protocol == TUNNEL_PROTOCOL_UNFRONTED_MEEK_SESSION_TICKET ||
-		protocol == TUNNEL_PROTOCOL_QUIC_OBFUSCATED_SSH
+		protocol == TUNNEL_PROTOCOL_QUIC_OBFUSCATED_SSH ||
+		protocol == TUNNEL_PROTOCOL_SHADOWSOCKS_SSH
 }
 
 func TunnelProtocolRequiresTLS12SessionTickets(protocol string) bool {
@@ -419,7 +428,8 @@ func TunnelProtocolMayUseServerPacketManipulation(protocol string) bool {
 		protocol == TUNNEL_PROTOCOL_TLS_OBFUSCATED_SSH ||
 		protocol == TUNNEL_PROTOCOL_UNFRONTED_MEEK ||
 		protocol == TUNNEL_PROTOCOL_UNFRONTED_MEEK_HTTPS ||
-		protocol == TUNNEL_PROTOCOL_UNFRONTED_MEEK_SESSION_TICKET
+		protocol == TUNNEL_PROTOCOL_UNFRONTED_MEEK_SESSION_TICKET ||
+		protocol == TUNNEL_PROTOCOL_SHADOWSOCKS_SSH
 }
 
 func TunnelProtocolMayUseClientBPF(protocol string) bool {

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

@@ -62,6 +62,8 @@ type ServerEntry struct {
 	SshObfuscatedTapDancePort           int      `json:"sshObfuscatedTapdancePort,omitempty"`
 	SshObfuscatedConjurePort            int      `json:"sshObfuscatedConjurePort,omitempty"`
 	SshObfuscatedKey                    string   `json:"sshObfuscatedKey,omitempty"`
+	SshShadowsocksPort                  int      `json:"sshShadowsocksPort,omitempty"`
+	SshShadowsocksKey                   string   `json:"sshShadowsocksKey,omitempty"`
 	Capabilities                        []string `json:"capabilities,omitempty"`
 	Region                              string   `json:"region,omitempty"`
 	ProviderID                          string   `json:"providerID,omitempty"`
@@ -91,6 +93,7 @@ type ServerEntry struct {
 	InproxyQUICPort                     int      `json:"inproxyQUICPort,omitempty"`
 	InproxyMeekPort                     int      `json:"inproxyMeekPort,omitempty"`
 	InproxyTlsOSSHPort                  int      `json:"inproxyTlsOSSHPort,omitempty"`
+	InproxyShadowsocksPort              int      `json:"inproxyShadowsocksPort,omitempty"`
 
 	// These local fields are not expected to be present in downloaded server
 	// entries. They are added by the client to record and report stats about
@@ -750,6 +753,9 @@ func (serverEntry *ServerEntry) GetDialPortNumber(tunnelProtocol string) (int, e
 			TUNNEL_PROTOCOL_UNFRONTED_MEEK_SESSION_TICKET,
 			TUNNEL_PROTOCOL_UNFRONTED_MEEK:
 			return serverEntry.MeekServerPort, nil
+
+		case TUNNEL_PROTOCOL_SHADOWSOCKS_SSH:
+			return serverEntry.SshShadowsocksPort, nil
 		}
 
 	} else {
@@ -786,6 +792,9 @@ func (serverEntry *ServerEntry) GetDialPortNumber(tunnelProtocol string) (int, e
 			TUNNEL_PROTOCOL_UNFRONTED_MEEK_SESSION_TICKET,
 			TUNNEL_PROTOCOL_UNFRONTED_MEEK:
 			return serverEntry.InproxyMeekPort, nil
+
+		case TUNNEL_PROTOCOL_SHADOWSOCKS_SSH:
+			return serverEntry.InproxyShadowsocksPort, nil
 		}
 
 	}

+ 2 - 1
psiphon/config.go

@@ -182,7 +182,8 @@ type Config struct {
 	// include: "SSH", "OSSH", "TLS-OSSH", "UNFRONTED-MEEK-OSSH",
 	// "UNFRONTED-MEEK-HTTPS-OSSH", "UNFRONTED-MEEK-SESSION-TICKET-OSSH",
 	// "FRONTED-MEEK-OSSH", "FRONTED-MEEK-HTTP-OSSH", "QUIC-OSSH",
-	// "FRONTED-MEEK-QUIC-OSSH", "TAPDANCE-OSSH", and "CONJURE-OSSH".
+	// "FRONTED-MEEK-QUIC-OSSH", "TAPDANCE-OSSH", "CONJURE-OSSH", and
+	// "SHADOWSOCKS-SSH".
 	// For the default, an empty list, all protocols are used.
 	LimitTunnelProtocols []string
 

+ 11 - 0
psiphon/controller_test.go

@@ -155,6 +155,17 @@ func TestTLSOSSH(t *testing.T) {
 		})
 }
 
+func TestShadowsocksSSH(t *testing.T) {
+
+	t.Skipf("temporarily disabled")
+
+	controllerRun(t,
+		&controllerRunConfig{
+			protocol:                 protocol.TUNNEL_PROTOCOL_SHADOWSOCKS_SSH,
+			disableUntunneledUpgrade: true,
+		})
+}
+
 func TestUnfrontedMeek(t *testing.T) {
 	controllerRun(t,
 		&controllerRunConfig{

+ 21 - 1
psiphon/dialParameters.go

@@ -31,6 +31,8 @@ import (
 	"sync/atomic"
 	"time"
 
+	"github.com/Jigsaw-Code/outline-sdk/transport"
+	"github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks"
 	tls "github.com/Psiphon-Labs/psiphon-tls"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
@@ -1314,7 +1316,8 @@ func MakeDialParameters(
 		protocol.TUNNEL_PROTOCOL_TAPDANCE_OBFUSCATED_SSH,
 		protocol.TUNNEL_PROTOCOL_CONJURE_OBFUSCATED_SSH,
 		protocol.TUNNEL_PROTOCOL_QUIC_OBFUSCATED_SSH,
-		protocol.TUNNEL_PROTOCOL_TLS_OBFUSCATED_SSH:
+		protocol.TUNNEL_PROTOCOL_TLS_OBFUSCATED_SSH,
+		protocol.TUNNEL_PROTOCOL_SHADOWSOCKS_SSH:
 
 		dialParams.DirectDialAddress = net.JoinHostPort(serverEntry.IpAddress, dialParams.DialPortNumber)
 
@@ -1707,6 +1710,23 @@ func (dialParams *DialParameters) GetTLSOSSHConfig(config *Config) *TLSTunnelCon
 	}
 }
 
+func (dialParams *DialParameters) GetShadowsocksConfig() *ShadowsockConfig {
+
+	key, err := shadowsocks.NewEncryptionKey(shadowsocks.CHACHA20IETFPOLY1305, dialParams.ServerEntry.SshShadowsocksKey)
+	if err != nil {
+		// TODO: parse key in MakeDialParameters
+		panic(err)
+	}
+
+	return &ShadowsockConfig{
+		endpoint: &transport.TCPEndpoint{
+			Address: dialParams.DirectDialAddress,
+			// Dialer:  net.Dialer{}, // TODO: pass in custom TLS dialer?
+		},
+		key: key,
+	}
+}
+
 func (dialParams *DialParameters) GetNetworkType() string {
 	return GetNetworkType(dialParams.NetworkID)
 }

+ 24 - 3
psiphon/server/config.go

@@ -130,7 +130,7 @@ type Config struct {
 	// "SSH", "OSSH", "TLS-OSSH", "UNFRONTED-MEEK-OSSH", "UNFRONTED-MEEK-HTTPS-OSSH",
 	// "UNFRONTED-MEEK-SESSION-TICKET-OSSH", "FRONTED-MEEK-OSSH",
 	// "FRONTED-MEEK-QUIC-OSSH", "FRONTED-MEEK-HTTP-OSSH", "QUIC-OSSH",
-	// "TAPDANCE-OSSH", abd "CONJURE-OSSH".
+	// "TAPDANCE-OSSH", "CONJURE-OSSH", and "SHADOWSOCKS-SSH".
 	TunnelProtocolPorts map[string]int
 
 	// TunnelProtocolPassthroughAddresses specifies passthrough addresses to be
@@ -188,6 +188,8 @@ type Config struct {
 	// run by this server instance, which use Obfuscated SSH.
 	ObfuscatedSSHKey string
 
+	ShadowsocksKey string
+
 	// MeekCookieEncryptionPrivateKey is the NaCl private key used
 	// to decrypt meek cookie payload sent from clients. The same
 	// key is used for all meek protocols run by this server instance.
@@ -966,6 +968,17 @@ func GenerateConfig(params *GenerateConfigParams) ([]byte, []byte, []byte, []byt
 	}
 	obfuscatedSSHKey := hex.EncodeToString(obfuscatedSSHKeyBytes)
 
+	// Shadowsocks config
+
+	// TODO: assuming shadowsocks.NewEncryptionKey is deterministic for now
+	shadowsocksKey := "test1234"
+
+	// TODO: use proper secret text
+	// shadowsocksKey, err := shadowsocks.NewEncryptionKey(shadowsocks.CHACHA20IETFPOLY1305, "test1234")
+	// if err != nil {
+	// 	return nil, nil, nil, nil, nil, errors.Trace(err)
+	// }
+
 	// Meek config
 
 	var meekCookieEncryptionPublicKey, meekCookieEncryptionPrivateKey, meekObfuscatedKey string
@@ -1056,6 +1069,7 @@ func GenerateConfig(params *GenerateConfigParams) ([]byte, []byte, []byte, []byt
 		SSHServerVersion:                   sshServerVersion,
 		SSHUserName:                        sshUserName,
 		SSHPassword:                        sshPassword,
+		ShadowsocksKey:                     shadowsocksKey,
 		ObfuscatedSSHKey:                   obfuscatedSSHKey,
 		TunnelProtocolPorts:                params.TunnelProtocolPorts,
 		TunnelProtocolPassthroughAddresses: params.TunnelProtocolPassthroughAddresses,
@@ -1207,8 +1221,8 @@ func GenerateConfig(params *GenerateConfigParams) ([]byte, []byte, []byte, []byt
 	// - Only one meek port may be specified per server entry.
 	// - Neither fronted meek nor Conjuure protocols are supported here.
 
-	var sshPort, obfuscatedSSHPort, meekPort, obfuscatedSSHQUICPort, tlsOSSHPort int
-	var inproxySSHPort, inproxyOSSHPort, inproxyQUICPort, inproxyMeekPort, inproxyTlsOSSHPort int
+	var sshPort, obfuscatedSSHPort, meekPort, obfuscatedSSHQUICPort, tlsOSSHPort, shadowsocksSSHPort int
+	var inproxySSHPort, inproxyOSSHPort, inproxyQUICPort, inproxyMeekPort, inproxyTlsOSSHPort, inproxyShadowsocksPort int
 
 	for tunnelProtocol, port := range params.TunnelProtocolPorts {
 
@@ -1226,6 +1240,8 @@ func GenerateConfig(params *GenerateConfigParams) ([]byte, []byte, []byte, []byt
 				protocol.TUNNEL_PROTOCOL_UNFRONTED_MEEK_SESSION_TICKET,
 				protocol.TUNNEL_PROTOCOL_UNFRONTED_MEEK:
 				meekPort = port
+			case protocol.TUNNEL_PROTOCOL_SHADOWSOCKS_SSH:
+				shadowsocksSSHPort = port
 			}
 		} else {
 			switch protocol.TunnelProtocolMinusInproxy(tunnelProtocol) {
@@ -1241,6 +1257,8 @@ func GenerateConfig(params *GenerateConfigParams) ([]byte, []byte, []byte, []byt
 				protocol.TUNNEL_PROTOCOL_UNFRONTED_MEEK_SESSION_TICKET,
 				protocol.TUNNEL_PROTOCOL_UNFRONTED_MEEK:
 				inproxyMeekPort = port
+			case protocol.TUNNEL_PROTOCOL_SHADOWSOCKS_SSH:
+				inproxyShadowsocksPort = port
 			}
 		}
 	}
@@ -1260,6 +1278,8 @@ func GenerateConfig(params *GenerateConfigParams) ([]byte, []byte, []byte, []byt
 		SshHostKey:                          base64.RawStdEncoding.EncodeToString(sshPublicKey.Marshal()),
 		SshObfuscatedPort:                   obfuscatedSSHPort,
 		SshObfuscatedQUICPort:               obfuscatedSSHQUICPort,
+		SshShadowsocksKey:                   shadowsocksKey,
+		SshShadowsocksPort:                  shadowsocksSSHPort,
 		LimitQUICVersions:                   params.LimitQUICVersions,
 		SshObfuscatedKey:                    obfuscatedSSHKey,
 		Capabilities:                        capabilities,
@@ -1282,6 +1302,7 @@ func GenerateConfig(params *GenerateConfigParams) ([]byte, []byte, []byte, []byt
 		InproxyQUICPort:                     inproxyQUICPort,
 		InproxyMeekPort:                     inproxyMeekPort,
 		InproxyTlsOSSHPort:                  inproxyTlsOSSHPort,
+		InproxyShadowsocksPort:              inproxyShadowsocksPort,
 	}
 
 	if params.ServerEntrySignaturePublicKey != "" {

+ 11 - 0
psiphon/server/server_test.go

@@ -205,6 +205,17 @@ func TestTLSOSSH(t *testing.T) {
 		})
 }
 
+func TestShadowsocks(t *testing.T) {
+	runServer(t,
+		&runServerConfig{
+			tunnelProtocol:       "SHADOWSOCKS-SSH",
+			requireAuthorization: true,
+			doTunneledWebRequest: true,
+			doTunneledNTPRequest: true,
+			doDanglingTCPConn:    true,
+		})
+}
+
 func TestUnfrontedMeek(t *testing.T) {
 	runServer(t,
 		&runServerConfig{

+ 140 - 0
psiphon/server/shadowsocks.go

@@ -0,0 +1,140 @@
+/*
+ * Copyright (c) 2024, Psiphon Inc.
+ * All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package server
+
+import (
+	"net"
+
+	"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/shadowsocks/go-shadowsocks2/socks"
+)
+
+type ShadowsocksServer struct {
+	support  *SupportServices
+	listener net.Listener
+	key      *shadowsocks.EncryptionKey
+}
+
+func ListenShadowsocks(
+	support *SupportServices,
+	listener net.Listener,
+	ssEncryptionKey string,
+) (net.Listener, error) {
+
+	server, err := NewShadowsocksServer(support, listener, ssEncryptionKey)
+	if err != nil {
+		return nil, errors.Trace(err)
+	}
+
+	return NewShadowsocksListener(listener, server), nil
+}
+
+// NewShadowsocksServer initializes a new ShadowsocksServer.
+func NewShadowsocksServer(
+	support *SupportServices,
+	listener net.Listener,
+	ssEncryptionKey string) (*ShadowsocksServer, error) {
+
+	key, err := shadowsocks.NewEncryptionKey(shadowsocks.CHACHA20IETFPOLY1305, ssEncryptionKey)
+	if err != nil {
+		return nil, errors.TraceMsg(err, "shadowsocks.NewEncryptionKey failed")
+	}
+
+	shadowsocksServer := &ShadowsocksServer{
+		support:  support,
+		listener: listener,
+		key:      key,
+	}
+
+	return shadowsocksServer, nil
+}
+
+type ShadowsocksListener struct {
+	net.Listener
+	server *ShadowsocksServer
+}
+
+// NewShadowsocksListener initializes a new ShadowsocksListener.
+func NewShadowsocksListener(listener net.Listener, server *ShadowsocksServer) *ShadowsocksListener {
+	return &ShadowsocksListener{
+		Listener: listener,
+		server:   server,
+	}
+}
+
+func (l *ShadowsocksListener) Accept() (net.Conn, error) {
+
+	conn, err := l.Listener.Accept()
+	if err != nil {
+		return nil, errors.Trace(err)
+	}
+
+	ssr := shadowsocks.NewReader(conn, l.server.key)
+	ssw := shadowsocks.NewWriter(conn, l.server.key)
+	ssClientConn := transport.WrapConn(conn.(*net.TCPConn), ssr, ssw)
+
+	return NewShadowsocksConn(ssClientConn, l.server), nil
+}
+
+// ShadowsocksConn implements the net.Conn and common.MetricsSource interfaces.
+type ShadowsocksConn struct {
+	net.Conn
+	readTargetAddr bool // TODO: atomic?
+	server         *ShadowsocksServer
+}
+
+// NewShadowsocksConn initializes a new NewShadowsocksConn.
+func NewShadowsocksConn(conn net.Conn, server *ShadowsocksServer) *ShadowsocksConn {
+	return &ShadowsocksConn{
+		Conn:   conn,
+		server: server,
+	}
+}
+
+func (conn *ShadowsocksConn) Read(b []byte) (int, error) {
+	// First read and discard target address
+	if !conn.readTargetAddr {
+		_, err := socks.ReadAddr(conn.Conn)
+		if err != nil {
+			return 0, errors.Trace(err)
+		}
+		// TODO: check target address is what we expect
+		conn.readTargetAddr = true
+	}
+	return conn.Conn.Read(b)
+}
+
+// GetMetrics implements the common.MetricsSource interface.
+func (conn *ShadowsocksConn) GetMetrics() common.LogFields {
+
+	var logFields common.LogFields
+
+	// Relay any metrics from the underlying conn.
+	if m, ok := conn.Conn.(common.MetricsSource); ok {
+		logFields = m.GetMetrics()
+	} else {
+		logFields = make(common.LogFields)
+	}
+
+	return logFields
+}

+ 5 - 0
psiphon/server/tunnelServer.go

@@ -231,6 +231,11 @@ func (server *TunnelServer) Run() error {
 				if err != nil {
 					return errors.Trace(err)
 				}
+			} else if protocol.TunnelProtocolUsesShadowsocks(tunnelProtocol) {
+				listener, err = ListenShadowsocks(support, listener, support.Config.ShadowsocksKey)
+				if err != nil {
+					return errors.Trace(err)
+				}
 			}
 		}
 

+ 63 - 0
psiphon/shadowsocksConn.go

@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2024, Psiphon Inc.
+ * All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package psiphon
+
+import (
+	"context"
+	"net"
+
+	"github.com/Jigsaw-Code/outline-sdk/transport"
+	"github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks"
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
+)
+
+type ShadowsockConfig struct {
+	endpoint *transport.TCPEndpoint
+
+	key *shadowsocks.EncryptionKey
+}
+
+type shadowsocksConn struct {
+	net.Conn
+}
+
+func DialShadowsocksTunnel(ctx context.Context, shadowsocksConfig *ShadowsockConfig) (*shadowsocksConn, error) {
+
+	// Connects to ss server
+	// TODO: ss also supports UDP with NewPacketListener
+	d, err := shadowsocks.NewStreamDialer(shadowsocksConfig.endpoint, shadowsocksConfig.key)
+	if err != nil {
+		return nil, errors.TraceMsg(err, "failed to create StreamDialer")
+	}
+
+	// Connects to target endpoint beyond ss server. We can use a phony address
+	// here, which will be ignored on the server, and pass data through this
+	// Conn.
+	phonyTargetAddr := "phony.local:1111"
+	conn, err := d.DialStream(context.Background(), phonyTargetAddr)
+	if err != nil {
+		return nil, errors.TraceMsg(err, "StreamDialer.Dial failed")
+	}
+	// conn.SetReadDeadline(time.Now().Add(time.Second * 5))
+
+	return &shadowsocksConn{
+		Conn: conn,
+	}, nil
+}

+ 10 - 0
psiphon/tunnel.go

@@ -991,6 +991,16 @@ func dialTunnel(
 			return nil, errors.Trace(err)
 		}
 
+	} else if protocol.TunnelProtocolUsesShadowsocks(dialParams.TunnelProtocol) {
+
+		dialConn, err = DialShadowsocksTunnel(
+			ctx,
+			dialParams.GetShadowsocksConfig(),
+		)
+		if err != nil {
+			return nil, errors.Trace(err)
+		}
+
 	} else {
 
 		// Use NewTCPDialer and don't use DialTCP directly, to ensure that

+ 201 - 0
vendor/github.com/Jigsaw-Code/outline-sdk/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.

+ 88 - 0
vendor/github.com/Jigsaw-Code/outline-sdk/internal/slicepool/slicepool.go

@@ -0,0 +1,88 @@
+// 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 slicepool
+
+import (
+	"sync"
+)
+
+// Pool wraps a sync.Pool of *[]byte.  To encourage correct usage,
+// all public methods are on slicepool.LazySlice.
+//
+// All copies of a Pool refer to the same underlying pool.
+//
+// "*[]byte" is used to avoid a heap allocation when passing a
+// []byte to sync.Pool.Put, which leaks its argument to the heap.
+type Pool struct {
+	pool *sync.Pool
+	len  int
+}
+
+// MakePool returns a Pool of slices with the specified length.
+func MakePool(sliceLen int) Pool {
+	return Pool{
+		pool: &sync.Pool{
+			New: func() interface{} {
+				slice := make([]byte, sliceLen)
+				// Return a *[]byte instead of []byte ensures that
+				// the []byte is not copied, which would cause a heap
+				// allocation on every call to sync.pool.Put
+				return &slice
+			},
+		},
+		len: sliceLen,
+	}
+}
+
+func (p *Pool) get() *[]byte {
+	return p.pool.Get().(*[]byte)
+}
+
+func (p *Pool) put(b *[]byte) {
+	if len(*b) != p.len || cap(*b) != p.len {
+		panic("Buffer length mismatch")
+	}
+	p.pool.Put(b)
+}
+
+// LazySlice returns an empty LazySlice tied to this Pool.
+func (p *Pool) LazySlice() LazySlice {
+	return LazySlice{pool: p}
+}
+
+// LazySlice holds 0 or 1 slices from a particular Pool.
+type LazySlice struct {
+	slice *[]byte
+	pool  *Pool
+}
+
+// Acquire this slice from the pool and return it.
+// This slice must not already be acquired.
+func (b *LazySlice) Acquire() []byte {
+	if b.slice != nil {
+		panic("buffer already acquired")
+	}
+	b.slice = b.pool.get()
+	return *b.slice
+}
+
+// Release the buffer back to the pool, unless the box is empty.
+// The caller must discard any references to the buffer.
+func (b *LazySlice) Release() {
+	if b.slice != nil {
+		b.pool.put(b.slice)
+		b.slice = nil
+	}
+}

+ 65 - 0
vendor/github.com/Jigsaw-Code/outline-sdk/transport/address.go

@@ -0,0 +1,65 @@
+// Copyright 2023 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 transport
+
+import (
+	"fmt"
+	"net"
+)
+
+type domainAddr struct {
+	network string
+	address string
+}
+
+func (a *domainAddr) Network() string {
+	return a.network
+}
+
+func (a *domainAddr) String() string {
+	return a.address
+}
+
+var _ net.Addr = (*domainAddr)(nil)
+
+// MakeNetAddr returns a [net.Addr] based on the network and address.
+// This is a helper for code that needs to return or provide a [net.Addr].
+// The address must be in "host:port" format with the host being a domain name, IPv4 or IPv6.
+// The network must be "tcp" or "udp".
+// For IP hosts, the returned address will be of type [*net.TCPAddr] or [*net.UDPAddr], based on the network argument.
+// This is important because some of the standard library functions inspect the type of the address and might return an
+// "invalid argument" error if the type is not the correct one.
+func MakeNetAddr(network, address string) (net.Addr, error) {
+	host, port, err := net.SplitHostPort(address)
+	if err != nil {
+		return nil, err
+	}
+	portnum, err := net.LookupPort(network, port)
+	if err != nil {
+		return nil, err
+	}
+	hostIp := net.ParseIP(host)
+	if hostIp != nil {
+		switch network {
+		case "tcp":
+			return &net.TCPAddr{IP: hostIp, Port: portnum}, nil
+		case "udp":
+			return &net.UDPAddr{IP: hostIp, Port: portnum}, nil
+		default:
+			return nil, net.UnknownNetworkError(network)
+		}
+	}
+	return &domainAddr{network: network, address: net.JoinHostPort(host, fmt.Sprint(portnum))}, nil
+}

+ 47 - 0
vendor/github.com/Jigsaw-Code/outline-sdk/transport/doc.go

@@ -0,0 +1,47 @@
+// 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.
+
+/*
+Package transport has the core types to work with transport layer connections.
+
+# Connections
+
+Connections enable communication between two endpoints over an abstract transport. There are two types of connections:
+
+  - Stream connections, like TCP and the SOCK_STREAM Posix socket type. They are represented by [StreamConn] objects.
+  - Datagram connections, like UDP and the SOCK_DGRAM Posix socket type. They are represented by [net.Conn] objects.
+
+We use "Packet" instead of "Datagram" in the method and type names related to datagrams because that is the convention in the Go standard library.
+
+Each write and read on datagram connections represent a single datagram, while reads and writes on stream connections operate on byte sequences
+that may be independent of how those bytes are packaged.
+
+Stream connections offer CloseRead and CloseWrite methods, which allows for a half-closed state (like TCP).
+In general, you communicate end of data ("EOF") to the other side of the connection by calling CloseWrite (TCP will send a FIN).
+CloseRead doesn't generate packets, but it allows for releasing resources (e.g. a read loop) and to signal errors to the peer
+if more data does arrive (TCP will usually send a RST).
+
+Connections can be wrapped to create nested connections over a new transport. For example, a StreamConn could be over TCP,
+over TLS over TCP, over HTTP over TLS over TCP, over QUIC, among other options.
+
+# Dialers
+
+Dialers enable the creation of connections given a host:port address while encapsulating the underlying transport or proxy protocol.
+The [StreamDialer] and [PacketDialer] types create stream ([StreamConn]) and datagram ([net.Conn]) connections, respectively, given an address.
+
+Dialers can also be nested. For example, a TLS Stream Dialer can use a TCP dialer to create a StreamConn backed by a TCP connection,
+then create a TLS StreamConn backed by the TCP StreamConn. A SOCKS5-over-TLS Dialer could use the TLS Dialer to create the TLS StreamConn
+to the proxy before doing the SOCKS5 connection to the target address.
+*/
+package transport

+ 259 - 0
vendor/github.com/Jigsaw-Code/outline-sdk/transport/happyeyeballs.go

@@ -0,0 +1,259 @@
+// 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.
+
+package transport
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"net"
+	"net/netip"
+	"sync/atomic"
+	"time"
+)
+
+/*
+HappyEyeballsStreamDialer is a [StreamDialer] that uses [Happy Eyeballs v2] to establish a connection
+to the destination address.
+
+Happy Eyeballs v2 reduces the connection delay when compared to v1, with significant differences when one of the
+address lookups times out. V1 will wait for both the IPv4 and IPv6 lookups to return before attempting connections,
+while V2 starts connections as soon as it gets a lookup result, with a slight delay if IPv4 arrives before IPv6.
+
+Go and most platforms provide V1 only, so you will benefit from using the HappyEyeballsStreamDialer in place of the
+standard dialer, even if you are not using custom transports.
+
+[Happy Eyeballs v2]: https://datatracker.ietf.org/doc/html/rfc8305
+*/
+type HappyEyeballsStreamDialer struct {
+	// Dialer is used to establish the connection attempts. If nil, a direct TCP connection is established.
+	Dialer StreamDialer
+	// Resolve is a function to map a host name to IP addresses. See HappyEyeballsResolver.
+	Resolve HappyEyeballsResolveFunc
+}
+
+// HappyEyeballsResolveFunc performs concurrent hostname resolution for [HappyEyeballsStreamDialer].
+//
+// The function should return a channel quickly, and then send the resolution results to it
+// as they become available. HappyEyeballsStreamDialer will read the resolutions from the channel.
+// The returned channel must be closed when there are no
+// more resolutions pending, to indicate that the resolution is done. If that is not
+// done, HappyEyeballsStreamDialer will keep waiting.
+//
+// It's recommended to return a buffered channel with size equal to the number of
+// lookups, so that it will never block on write.
+// If the channel is unbuffered, you must use select when writing to the channel against
+// ctx.Done(), to make sure you don't write when HappyEyeballsStreamDialer is no longer reading.
+// Othewise your goroutine will get stuck.
+//
+// It's recommended to resolve IPv6 and IPv4 in parallel, so the connection attempts
+// are started as soon as addresses are received. That's the primary benefit of Happy
+// Eyeballs v2. If you resolve in series, and only send the addresses when both
+// resolutions are done, you will get behavior similar to Happy Eyeballs v1.
+type HappyEyeballsResolveFunc = func(ctx context.Context, hostname string) <-chan HappyEyeballsResolution
+
+// HappyEyeballsResolution represents a result of a hostname resolution.
+// Happy Eyeballs sorts the IPs in a specific way, updating the order as
+// new results are received. It's recommended to returns all IPs you receive
+// as a group, rather than one IP at a time, since a later IP may be preferred.
+type HappyEyeballsResolution struct {
+	IPs []netip.Addr
+	Err error
+}
+
+// NewParallelHappyEyeballsResolveFunc creates a [HappyEyeballsResolveFunc] that uses the given list of functions to resolve host names.
+// The given functions will all run in parallel, with results being output as they are received.
+// Typically you will pass one function for IPv6 and one for IPv4 to achieve Happy Eyballs v2 behavior.
+// It takes care of creating the channel and the parallelization and coordination between the calls.
+func NewParallelHappyEyeballsResolveFunc(resolveFuncs ...func(ctx context.Context, hostname string) ([]netip.Addr, error)) HappyEyeballsResolveFunc {
+	return func(ctx context.Context, host string) <-chan HappyEyeballsResolution {
+		// Use a buffered channel with space for both lookups, to ensure the goroutines won't
+		// block on channel write if the Happy Eyeballs algorithm is cancelled and no longer reading.
+		resultsCh := make(chan HappyEyeballsResolution, len(resolveFuncs))
+		if len(resolveFuncs) == 0 {
+			close(resultsCh)
+			return resultsCh
+		}
+
+		var pending atomic.Int32
+		pending.Store(int32(len(resolveFuncs)))
+		for _, resolve := range resolveFuncs {
+			go func(resolve func(ctx context.Context, hostname string) ([]netip.Addr, error), hostname string) {
+				ips, err := resolve(ctx, hostname)
+				resultsCh <- HappyEyeballsResolution{ips, err}
+				if pending.Add(-1) == 0 {
+					// Close results channel when no other goroutine is pending.
+					close(resultsCh)
+				}
+			}(resolve, host)
+		}
+		return resultsCh
+	}
+}
+
+var _ StreamDialer = (*HappyEyeballsStreamDialer)(nil)
+
+func (d *HappyEyeballsStreamDialer) dial(ctx context.Context, addr string) (StreamConn, error) {
+	if d.Dialer != nil {
+		return d.Dialer.DialStream(ctx, addr)
+	}
+	return (&TCPDialer{}).DialStream(ctx, addr)
+}
+
+func newClosedChan() <-chan struct{} {
+	closedCh := make(chan struct{})
+	close(closedCh)
+	return closedCh
+}
+
+// DialStream implements [StreamDialer].
+func (d *HappyEyeballsStreamDialer) DialStream(ctx context.Context, addr string) (StreamConn, error) {
+	hostname, port, err := net.SplitHostPort(addr)
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse address: %w", err)
+	}
+	if net.ParseIP(hostname) != nil {
+		// Host is already an IP address, just dial the address.
+		return d.dial(ctx, addr)
+	}
+
+	// Indicates to attempts that the dialing process is done, so they don't get stuck.
+	ctx, dialDone := context.WithCancel(ctx)
+	defer dialDone()
+
+	// HOSTNAME RESOLUTION QUERY HANDLING
+	// https://datatracker.ietf.org/doc/html/rfc8305#section-3
+	resolutionCh := d.Resolve(ctx, hostname)
+
+	// CONNECTION ATTEMPTS
+	// https://datatracker.ietf.org/doc/html/rfc8305#section-5
+	// We keep IPv4s and IPv6 separate and track the last one attempted so we can
+	// alternate the address family in the connection attempts.
+	ip4s := make([]netip.Addr, 0, 1)
+	ip6s := make([]netip.Addr, 0, 1)
+	var lastDialed netip.Addr
+	// Keep track of the lookup and dial errors separately. We prefer the dial errors
+	// when returning.
+	var lookupErr error
+	var dialErr error
+	// Channel to wait for before a new dial attempt. It starts
+	// with a closed channel that doesn't block because there's no
+	// wait initially.
+	var attemptDelayCh <-chan struct{} = newClosedChan()
+	type DialResult struct {
+		Conn StreamConn
+		Err  error
+	}
+	dialCh := make(chan DialResult)
+
+	// Channel that triggers when a new connection can be made. Starts blocked (nil)
+	// because we need IPs first.
+	var readyToDialCh <-chan struct{} = nil
+	// We keep track of pending operations (lookups and IPs to dial) so we can stop when
+	// there's no more work to wait for.
+	for opsPending := 1; opsPending > 0; {
+		if len(ip6s) == 0 && len(ip4s) == 0 {
+			// No IPs. Keep dial disabled.
+			readyToDialCh = nil
+		} else {
+			// There are IPs to dial.
+			if !lastDialed.IsValid() && len(ip6s) == 0 && resolutionCh != nil {
+				// Attempts haven't started and IPv6 lookup is not done yet. Set up Resolution Delay, as per
+				// https://datatracker.ietf.org/doc/html/rfc8305#section-8, if it hasn't been set up yet.
+				if readyToDialCh == nil {
+					resolutionDelayCtx, cancelResolutionDelay := context.WithTimeout(ctx, 50*time.Millisecond)
+					defer cancelResolutionDelay()
+					readyToDialCh = resolutionDelayCtx.Done()
+				}
+			} else {
+				// Wait for the previous attempt.
+				readyToDialCh = attemptDelayCh
+			}
+		}
+		select {
+		// Receive lookup results.
+		case lookupRes, ok := <-resolutionCh:
+			if !ok {
+				opsPending--
+				// Set to nil to make the read on lookupCh block and to signal lookup is done.
+				resolutionCh = nil
+			}
+			if lookupRes.Err != nil {
+				lookupErr = errors.Join(lookupErr, lookupRes.Err)
+				continue
+			}
+			opsPending += len(lookupRes.IPs)
+			// TODO: sort IPs as per https://datatracker.ietf.org/doc/html/rfc8305#section-4
+			for _, ip := range lookupRes.IPs {
+				if ip.Is6() {
+					ip6s = append(ip6s, ip)
+				} else {
+					ip4s = append(ip4s, ip)
+				}
+			}
+
+		// Wait for Connection Attempt Delay or attempt done.
+		// This case is disabled above when len(ip6s) == 0 && len(ip4s) == 0.
+		case <-readyToDialCh:
+			var toDial netip.Addr
+			// Alternate between IPv6 and IPv4.
+			if len(ip6s) == 0 || (lastDialed.Is6() && len(ip4s) > 0) {
+				toDial = ip4s[0]
+				ip4s = ip4s[1:]
+			} else {
+				toDial = ip6s[0]
+				ip6s = ip6s[1:]
+			}
+			// Reset Connection Attempt Delay, as per https://datatracker.ietf.org/doc/html/rfc8305#section-8
+			// We don't tie the delay context to the parent because we don't want the readyToDialCh case
+			// to trigger on the parent cancellation.
+			delayCtx, cancelDelay := context.WithTimeout(context.Background(), 250*time.Millisecond)
+			attemptDelayCh = delayCtx.Done()
+			go func(addr string, cancelDelay context.CancelFunc) {
+				// Cancel the wait if the dial return early.
+				defer cancelDelay()
+				conn, err := d.dial(ctx, addr)
+				select {
+				case <-ctx.Done():
+					if conn != nil {
+						conn.Close()
+					}
+				case dialCh <- DialResult{conn, err}:
+				}
+			}(net.JoinHostPort(toDial.String(), port), cancelDelay)
+			lastDialed = toDial
+
+		// Receive dial result.
+		case dialRes := <-dialCh:
+			opsPending--
+			if dialRes.Err != nil {
+				dialErr = errors.Join(dialErr, dialRes.Err)
+				continue
+			}
+			return dialRes.Conn, nil
+
+		// Dial has been canceled. Return.
+		case <-ctx.Done():
+			return nil, ctx.Err()
+		}
+	}
+	if dialErr != nil {
+		return nil, dialErr
+	}
+	if lookupErr != nil {
+		return nil, lookupErr
+	}
+	return nil, errors.New("address lookup returned no IPs")
+}

+ 177 - 0
vendor/github.com/Jigsaw-Code/outline-sdk/transport/packet.go

@@ -0,0 +1,177 @@
+// 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 transport
+
+import (
+	"context"
+	"fmt"
+	"net"
+)
+
+// PacketEndpoint represents an endpoint that can be used to establish packet connections (like UDP) to a fixed destination.
+type PacketEndpoint interface {
+	// ConnectPacket creates a connection bound to an endpoint, returning the connection.
+	ConnectPacket(ctx context.Context) (net.Conn, error)
+}
+
+// UDPEndpoint is a [PacketEndpoint] that connects to the specified address using UDP.
+type UDPEndpoint struct {
+	// The Dialer used to create the net.Conn on Connect().
+	Dialer net.Dialer
+	// The endpoint address ("host:port") to pass to Dial.
+	// If the host is a domain name, consider pre-resolving it to avoid resolution calls.
+	Address string
+}
+
+var _ PacketEndpoint = (*UDPEndpoint)(nil)
+
+// ConnectPacket implements [PacketEndpoint].ConnectPacket.
+func (e UDPEndpoint) ConnectPacket(ctx context.Context) (net.Conn, error) {
+	return e.Dialer.DialContext(ctx, "udp", e.Address)
+}
+
+// FuncPacketEndpoint is a [PacketEndpoint] that uses the given function to connect.
+type FuncPacketEndpoint func(ctx context.Context) (net.Conn, error)
+
+var _ PacketEndpoint = (*FuncPacketEndpoint)(nil)
+
+// ConnectPacket implements the [PacketEndpoint] interface.
+func (f FuncPacketEndpoint) ConnectPacket(ctx context.Context) (net.Conn, error) {
+	return f(ctx)
+}
+
+// PacketDialerEndpoint is a [PacketEndpoint] that connects to the given address using the specified [PacketDialer].
+type PacketDialerEndpoint struct {
+	Dialer  PacketDialer
+	Address string
+}
+
+var _ PacketEndpoint = (*PacketDialerEndpoint)(nil)
+
+// ConnectPacket implements [PacketEndpoint].ConnectPacket.
+func (e *PacketDialerEndpoint) ConnectPacket(ctx context.Context) (net.Conn, error) {
+	return e.Dialer.DialPacket(ctx, e.Address)
+}
+
+// PacketDialer provides a way to dial a destination and establish datagram connections.
+type PacketDialer interface {
+	// DialPacket connects to `addr`.
+	// `addr` has the form "host:port", where "host" can be a domain name or IP address.
+	DialPacket(ctx context.Context, addr string) (net.Conn, error)
+}
+
+// UDPDialer is a [PacketDialer] that uses the standard [net.Dialer] to dial.
+// It provides a convenient way to use a [net.Dialer] when you need a [PacketDialer].
+type UDPDialer struct {
+	Dialer net.Dialer
+}
+
+var _ PacketDialer = (*UDPDialer)(nil)
+
+// DialPacket implements [PacketDialer].DialPacket.
+func (d *UDPDialer) DialPacket(ctx context.Context, addr string) (net.Conn, error) {
+	return d.Dialer.DialContext(ctx, "udp", addr)
+}
+
+// PacketListenerDialer is a [PacketDialer] that connects to the destination using the specified [PacketListener].
+type PacketListenerDialer struct {
+	// The PacketListener that is used to create the net.PacketConn to bind on Dial. Must be non nil.
+	Listener PacketListener
+}
+
+var _ PacketDialer = (*PacketListenerDialer)(nil)
+
+type boundPacketConn struct {
+	net.PacketConn
+	remoteAddr net.Addr
+}
+
+var _ net.Conn = (*boundPacketConn)(nil)
+
+// DialPacket implements [PacketDialer].DialPacket.
+// The address is in "host:port" format and the host must be either a full IP address (not "[::]") or a domain.
+// The address must be supported by the WriteTo call of the [net.PacketConn] returned by the [PacketListener].
+// For example, a [net.UDPConn] only supports IP addresses, not domain names.
+// If the host is a domain name, consider pre-resolving it to avoid resolution calls.
+func (e PacketListenerDialer) DialPacket(ctx context.Context, address string) (net.Conn, error) {
+	netAddr, err := MakeNetAddr("udp", address)
+	if err != nil {
+		return nil, err
+	}
+	packetConn, err := e.Listener.ListenPacket(ctx)
+	if err != nil {
+		return nil, fmt.Errorf("could not create PacketConn: %w", err)
+	}
+	return &boundPacketConn{
+		PacketConn: packetConn,
+		remoteAddr: netAddr,
+	}, nil
+}
+
+// Read implements [net.Conn].Read.
+func (c *boundPacketConn) Read(packet []byte) (int, error) {
+	for {
+		n, remoteAddr, err := c.PacketConn.ReadFrom(packet)
+		if err != nil {
+			return n, err
+		}
+		if remoteAddr.String() != c.remoteAddr.String() {
+			continue
+		}
+		return n, nil
+	}
+}
+
+// Write implements [net.Conn].Write.
+func (c *boundPacketConn) Write(packet []byte) (int, error) {
+	// This may return syscall.EINVAL if remoteAddr is a name like localhost or [::].
+	n, err := c.PacketConn.WriteTo(packet, c.remoteAddr)
+	return n, err
+}
+
+// RemoteAddr implements [net.Conn].RemoteAddr.
+func (c *boundPacketConn) RemoteAddr() net.Addr {
+	return c.remoteAddr
+}
+
+// PacketListener provides a way to create a local unbound packet connection to send packets to different destinations.
+type PacketListener interface {
+	// ListenPacket creates a PacketConn that can be used to relay packets (such as UDP) through a proxy.
+	ListenPacket(ctx context.Context) (net.PacketConn, error)
+}
+
+// UDPListener is a [PacketListener] that uses the standard [net.ListenConfig].ListenPacket to listen.
+type UDPListener struct {
+	net.ListenConfig
+	// The local address to bind to, as specified in net.ListenPacket.
+	Address string
+}
+
+var _ PacketListener = (*UDPListener)(nil)
+
+// ListenPacket implements [PacketListener].ListenPacket
+func (l UDPListener) ListenPacket(ctx context.Context) (net.PacketConn, error) {
+	return l.ListenConfig.ListenPacket(ctx, "udp", l.Address)
+}
+
+// FuncPacketDialer is a [PacketDialer] that uses the given function to dial.
+type FuncPacketDialer func(ctx context.Context, addr string) (net.Conn, error)
+
+var _ PacketDialer = (*FuncPacketDialer)(nil)
+
+// DialPacket implements the [PacketDialer] interface.
+func (f FuncPacketDialer) DialPacket(ctx context.Context, addr string) (net.Conn, error) {
+	return f(ctx, addr)
+}

+ 157 - 0
vendor/github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks/cipher.go

@@ -0,0 +1,157 @@
+// 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 shadowsocks
+
+import (
+	"crypto/aes"
+	"crypto/cipher"
+	"crypto/md5"
+	"crypto/sha1"
+	"io"
+	"strings"
+
+	"golang.org/x/crypto/chacha20poly1305"
+	"golang.org/x/crypto/hkdf"
+)
+
+type cipherSpec struct {
+	newInstance func(key []byte) (cipher.AEAD, error)
+	keySize     int
+	saltSize    int
+	tagSize     int
+}
+
+// List of supported AEAD ciphers, as specified at https://shadowsocks.org/guide/aead.html
+var (
+	CHACHA20IETFPOLY1305 = "AEAD_CHACHA20_POLY1305"
+	AES256GCM            = "AEAD_AES_256_GCM"
+	AES192GCM            = "AEAD_AES_192_GCM"
+	AES128GCM            = "AEAD_AES_128_GCM"
+)
+
+var (
+	chacha20IETFPOLY1305Cipher = &cipherSpec{chacha20poly1305.New, chacha20poly1305.KeySize, 32, 16}
+	aes256GCMCipher            = &cipherSpec{newAesGCM, 32, 32, 16}
+	aes192GCMCipher            = &cipherSpec{newAesGCM, 24, 24, 16}
+	aes128GCMCipher            = &cipherSpec{newAesGCM, 16, 16, 16}
+)
+
+var supportedCiphers = [](string){CHACHA20IETFPOLY1305, AES256GCM, AES192GCM, AES128GCM}
+
+// ErrUnsupportedCipher is returned by [CypherByName] when the named cipher is not supported.
+type ErrUnsupportedCipher struct {
+	// The name of the requested [Cipher]
+	Name string
+}
+
+func (err ErrUnsupportedCipher) Error() string {
+	return "unsupported cipher " + err.Name
+}
+
+// Largest tag size among the supported ciphers. Used by the TCP buffer pool
+const maxTagSize = 16
+
+// CipherByName returns a [*Cipher] with the given name, or an error if the cipher is not supported.
+// The name must be the IETF name (as per https://www.iana.org/assignments/aead-parameters/aead-parameters.xhtml) or the
+// Shadowsocks alias from https://shadowsocks.org/guide/aead.html.
+func cipherByName(name string) (*cipherSpec, error) {
+	switch strings.ToUpper(name) {
+	case "AEAD_CHACHA20_POLY1305", "CHACHA20-IETF-POLY1305":
+		return chacha20IETFPOLY1305Cipher, nil
+	case "AEAD_AES_256_GCM", "AES-256-GCM":
+		return aes256GCMCipher, nil
+	case "AEAD_AES_192_GCM", "AES-192-GCM":
+		return aes192GCMCipher, nil
+	case "AEAD_AES_128_GCM", "AES-128-GCM":
+		return aes128GCMCipher, nil
+	default:
+		return nil, ErrUnsupportedCipher{name}
+	}
+}
+
+func newAesGCM(key []byte) (cipher.AEAD, error) {
+	blk, err := aes.NewCipher(key)
+	if err != nil {
+		return nil, err
+	}
+	return cipher.NewGCM(blk)
+}
+
+// EncryptionKey encapsulates a Shadowsocks AEAD spec and a secret
+type EncryptionKey struct {
+	cipher *cipherSpec
+	secret []byte
+}
+
+// SaltSize is the size of the salt for this Cipher
+func (c *EncryptionKey) SaltSize() int {
+	return c.cipher.saltSize
+}
+
+// TagSize is the size of the AEAD tag for this Cipher
+func (c *EncryptionKey) TagSize() int {
+	return c.cipher.tagSize
+}
+
+var subkeyInfo = []byte("ss-subkey")
+
+// NewAEAD creates the AEAD for this cipher
+func (c *EncryptionKey) NewAEAD(salt []byte) (cipher.AEAD, error) {
+	sessionKey := make([]byte, c.cipher.keySize)
+	r := hkdf.New(sha1.New, c.secret, salt, subkeyInfo)
+	if _, err := io.ReadFull(r, sessionKey); err != nil {
+		return nil, err
+	}
+	return c.cipher.newInstance(sessionKey)
+}
+
+// Function definition at https://www.openssl.org/docs/manmaster/man3/EVP_BytesToKey.html
+func simpleEVPBytesToKey(data []byte, keyLen int) ([]byte, error) {
+	var derived, di []byte
+	h := md5.New()
+	for len(derived) < keyLen {
+		_, err := h.Write(di)
+		if err != nil {
+			return nil, err
+		}
+		_, err = h.Write(data)
+		if err != nil {
+			return nil, err
+		}
+		derived = h.Sum(derived)
+		di = derived[len(derived)-h.Size():]
+		h.Reset()
+	}
+	return derived[:keyLen], nil
+}
+
+// NewEncryptionKey creates a Cipher with a cipher name and a secret.
+// The cipher name must be the IETF name (as per https://www.iana.org/assignments/aead-parameters/aead-parameters.xhtml)
+// or the Shadowsocks alias from https://shadowsocks.org/guide/aead.html.
+func NewEncryptionKey(cipherName string, secretText string) (*EncryptionKey, error) {
+	var key EncryptionKey
+	var err error
+	key.cipher, err = cipherByName(cipherName)
+	if err != nil {
+		return nil, err
+	}
+
+	// Key derivation as per https://shadowsocks.org/en/spec/AEAD-Ciphers.html
+	key.secret, err = simpleEVPBytesToKey([]byte(secretText), key.cipher.keySize)
+	if err != nil {
+		return nil, err
+	}
+	return &key, nil
+}

+ 58 - 0
vendor/github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks/client_testing.go

@@ -0,0 +1,58 @@
+// Copyright 2023 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 shadowsocks
+
+import (
+	"bytes"
+	"io"
+	"testing"
+)
+
+const (
+	testTargetAddr = "test.local:1111"
+)
+
+// Writes `payload` to `conn` and reads it into `buf`, which we take as a parameter to avoid
+// reallocations in benchmarks and memory profiles. Fails the test if the read payload does not match.
+func expectEchoPayload(conn io.ReadWriter, payload, buf []byte, t testing.TB) {
+	_, err := conn.Write(payload)
+	if err != nil {
+		t.Fatalf("Failed to write payload: %v", err)
+	}
+	n, err := conn.Read(buf)
+	if err != nil {
+		t.Fatalf("Failed to read payload: %v", err)
+	}
+	if !bytes.Equal(payload, buf[:n]) {
+		t.Fatalf("Expected output '%v'. Got '%v'", payload, buf[:n])
+	}
+}
+
+func makeTestKey(tb testing.TB) *EncryptionKey {
+	key, err := NewEncryptionKey(CHACHA20IETFPOLY1305, "testPassword")
+	if err != nil {
+		tb.Fatalf("Failed to create key: %v", err)
+	}
+	return key
+}
+
+// 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
+}

+ 61 - 0
vendor/github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks/doc.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 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 shadowsocks implements the Shadowsocks secure transport and proxy protocols.
+
+Shadowsocks is a a combination of two protocols:
+  - [Encrypted transport]: uses authenticated encryption for privacy and security. Traffic appears random to avoid detection,
+    with no distinguishable pattern or markers.
+  - [Proxy protocol]: a simplified [SOCKS5]-like protocol for routing TCP and UDP connections to various destinations.
+
+# Setting up Shadowsocks Servers
+
+When using Shadowsocks, you will need a server. There are many ways to run Shadowsocks servers. We recommend:
+
+  - [Outline Manager app]: The easiest way to create and manage Shadowsocks servers in the cloud.
+  - [outline-ss-server]: A command-line tool for advanced users offering greater configuration flexibility.
+
+# IPv6 Limitations
+
+The Shadowsocks proxy protocol lacks a mechanism for servers to signal successful connection to a destination.
+For that reason, the [StreamDialer] immediately returns a connection
+once the TCP connection to the proxy is established, but before the connection to the destination by the proxy happens.
+
+This is fine for dialed addresses that use a host name, since the name resolution will happen in the proxy, and the proxy
+will handle address selection for the client. That is usually the case for proxy apps. However in VPN apps using a "tun2socks" approach,
+the client is doing the name resolution and address selection, dialing using IP addresses. Because the dialer returns a successful connection
+regardless of the destination connectivity, this breaks the Happy Eyeballs address selection, effectively breaking IPv6 support.
+
+It's recommended that you prioritize hostname-based dialing for optimal IPv6 compatibility, and disable IPv6 if name resolution and address selection
+happens on the client side, as is the case of VPN apps.
+
+# Security Considerations
+
+Shadowsocks uses strong authenticated encryption (AEAD), standardized by the IETF. For privacy and security, this package does not support the legacy and unsafe [stream ciphers].
+
+Shadowsocks does not provide forward-secrecy. That can be accomplished by generating a new,
+completely random secret for every session, and delivering it to the client in a forward-secret way.
+With Outline, that can be done via [Dynamic Keys]: when the Dynamic Key is requested, generate a new secret.
+The response is sent over TLS, which implements forward-secrecy.
+
+[SOCKS5]: https://datatracker.ietf.org/doc/html/rfc1928
+[Outline Manager app]: https://getoutline.org/get-started/#step-1
+[outline-ss-server]: https://github.com/Jigsaw-Code/outline-ss-server?tab=readme-ov-file#how-to-run-it
+[Encrypted transport]: https://shadowsocks.org/doc/aead.html
+[Proxy protocol]: https://shadowsocks.org/doc/what-is-shadowsocks.html
+[stream ciphers]: https://shadowsocks.org/doc/stream.html
+[Dynamic Keys]: https://www.reddit.com/r/outlinevpn/wiki/index/dynamic_access_keys/
+*/
+package shadowsocks

+ 82 - 0
vendor/github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks/packet.go

@@ -0,0 +1,82 @@
+// 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 shadowsocks
+
+import (
+	"errors"
+	"io"
+)
+
+// ErrShortPacket indicates that the destination packet given to Unpack is too short.
+var ErrShortPacket = errors.New("short packet")
+
+// Assumes all ciphers have NonceSize() <= 12.
+var zeroNonce [12]byte
+
+// Pack encrypts a Shadowsocks-UDP packet and returns a slice containing the encrypted packet.
+// dst must be big enough to hold the encrypted packet.
+// If plaintext and dst overlap but are not aligned for in-place encryption, this
+// function will panic.
+func Pack(dst, plaintext []byte, key *EncryptionKey) ([]byte, error) {
+	saltSize := key.SaltSize()
+	if len(dst) < saltSize {
+		return nil, io.ErrShortBuffer
+	}
+	salt := dst[:saltSize]
+	if err := RandomSaltGenerator.GetSalt(salt); err != nil {
+		return nil, err
+	}
+
+	aead, err := key.NewAEAD(salt)
+	if err != nil {
+		return nil, err
+	}
+
+	if len(dst) < saltSize+len(plaintext)+aead.Overhead() {
+		return nil, io.ErrShortBuffer
+	}
+	return aead.Seal(salt, zeroNonce[:aead.NonceSize()], plaintext, nil), nil
+}
+
+// Unpack decrypts a Shadowsocks-UDP packet in the format [salt][cipherText][AEAD tag] and returns a slice containing
+// the decrypted payload or an error.
+// If dst is present, it is used to store the plaintext, and must have enough capacity.
+// If dst is nil, decryption proceeds in-place.
+func Unpack(dst, pkt []byte, key *EncryptionKey) ([]byte, error) {
+	saltSize := key.SaltSize()
+	if len(pkt) < saltSize {
+		return nil, ErrShortPacket
+	}
+
+	salt := pkt[:saltSize]
+	cipherTextAndTag := pkt[saltSize:]
+	if len(cipherTextAndTag) < key.TagSize() {
+		return nil, io.ErrUnexpectedEOF
+	}
+
+	if dst == nil {
+		dst = cipherTextAndTag
+	}
+	if cap(dst) < len(cipherTextAndTag)-key.TagSize() {
+		return nil, io.ErrShortBuffer
+	}
+
+	aead, err := key.NewAEAD(salt)
+	if err != nil {
+		return nil, err
+	}
+
+	return aead.Open(dst[:0], zeroNonce[:aead.NonceSize()], cipherTextAndTag, nil)
+}

+ 117 - 0
vendor/github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks/packet_listener.go

@@ -0,0 +1,117 @@
+// Copyright 2023 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 shadowsocks
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"io"
+	"net"
+
+	"github.com/Jigsaw-Code/outline-sdk/internal/slicepool"
+	"github.com/Jigsaw-Code/outline-sdk/transport"
+	"github.com/shadowsocks/go-shadowsocks2/socks"
+)
+
+// clientUDPBufferSize is the maximum supported UDP packet size in bytes.
+const clientUDPBufferSize = 16 * 1024
+
+// udpPool stores the byte slices used for storing encrypted packets.
+var udpPool = slicepool.MakePool(clientUDPBufferSize)
+
+type packetListener struct {
+	endpoint transport.PacketEndpoint
+	key      *EncryptionKey
+}
+
+var _ transport.PacketListener = (*packetListener)(nil)
+
+func NewPacketListener(endpoint transport.PacketEndpoint, key *EncryptionKey) (transport.PacketListener, error) {
+	if endpoint == nil {
+		return nil, errors.New("argument endpoint must not be nil")
+	}
+	if key == nil {
+		return nil, errors.New("argument key must not be nil")
+	}
+	return &packetListener{endpoint: endpoint, key: key}, nil
+}
+
+func (c *packetListener) ListenPacket(ctx context.Context) (net.PacketConn, error) {
+	proxyConn, err := c.endpoint.ConnectPacket(ctx)
+	if err != nil {
+		return nil, fmt.Errorf("could not connect to endpoint: %w", err)
+	}
+	conn := packetConn{Conn: proxyConn, key: c.key}
+	return &conn, nil
+}
+
+type packetConn struct {
+	net.Conn
+	key *EncryptionKey
+}
+
+var _ net.PacketConn = (*packetConn)(nil)
+
+// WriteTo encrypts `b` and writes to `addr` through the proxy.
+func (c *packetConn) WriteTo(b []byte, addr net.Addr) (int, error) {
+	socksTargetAddr := socks.ParseAddr(addr.String())
+	if socksTargetAddr == nil {
+		return 0, errors.New("failed to parse target address")
+	}
+	lazySlice := udpPool.LazySlice()
+	cipherBuf := lazySlice.Acquire()
+	defer lazySlice.Release()
+	saltSize := c.key.SaltSize()
+	// Copy the SOCKS target address and payload, reserving space for the generated salt to avoid
+	// partially overlapping the plaintext and cipher slices since `Pack` skips the salt when calling
+	// `AEAD.Seal` (see https://golang.org/pkg/crypto/cipher/#AEAD).
+	plaintextBuf := append(append(cipherBuf[saltSize:saltSize], socksTargetAddr...), b...)
+	buf, err := Pack(cipherBuf, plaintextBuf, c.key)
+	if err != nil {
+		return 0, err
+	}
+	_, err = c.Conn.Write(buf)
+	return len(b), err
+}
+
+// ReadFrom reads from the embedded PacketConn and decrypts into `b`.
+func (c *packetConn) ReadFrom(b []byte) (int, net.Addr, error) {
+	lazySlice := udpPool.LazySlice()
+	cipherBuf := lazySlice.Acquire()
+	defer lazySlice.Release()
+	n, err := c.Conn.Read(cipherBuf)
+	if err != nil {
+		return 0, nil, err
+	}
+	// Decrypt in-place.
+	buf, err := Unpack(nil, cipherBuf[:n], c.key)
+	if err != nil {
+		return 0, nil, err
+	}
+	socksSrcAddr := socks.SplitAddr(buf)
+	if socksSrcAddr == nil {
+		return 0, nil, errors.New("failed to read source address")
+	}
+	srcAddr, err := transport.MakeNetAddr("udp", socksSrcAddr.String())
+	if err != nil {
+		return 0, nil, fmt.Errorf("failed to convert incoming address: %w", err)
+	}
+	n = copy(b, buf[len(socksSrcAddr):]) // Strip the SOCKS source address
+	if len(b) < len(buf)-len(socksSrcAddr) {
+		return n, srcAddr, io.ErrShortBuffer
+	}
+	return n, srcAddr, nil
+}

+ 66 - 0
vendor/github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks/salt.go

@@ -0,0 +1,66 @@
+// 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 shadowsocks
+
+import (
+	"crypto/rand"
+	"errors"
+)
+
+// SaltGenerator generates unique salts to use in Shadowsocks connections.
+type SaltGenerator interface {
+	// Returns a new salt
+	GetSalt(salt []byte) error
+}
+
+// randomSaltGenerator generates a new random salt.
+type randomSaltGenerator struct{}
+
+// GetSalt outputs a random salt.
+func (randomSaltGenerator) GetSalt(salt []byte) error {
+	_, err := rand.Read(salt)
+	return err
+}
+
+// RandomSaltGenerator is a basic SaltGenerator.
+var RandomSaltGenerator SaltGenerator = randomSaltGenerator{}
+
+type prefixSaltGenerator struct {
+	prefix []byte
+}
+
+func (g prefixSaltGenerator) GetSalt(salt []byte) error {
+	n := copy(salt, g.prefix)
+	if n != len(g.prefix) {
+		return errors.New("prefix is too long")
+	}
+	_, err := rand.Read(salt[n:])
+	return err
+}
+
+// NewPrefixSaltGenerator returns a SaltGenerator with output including
+// the provided prefix, followed by random bytes. This is useful to change
+// how shadowsocks traffic is classified by middleboxes.
+//
+// Note: Prefixes steal entropy from the initialization vector. This weakens
+// security by increasing the likelihood that the same IV is used in two
+// different connections (which becomes likely once 2^(N/2) connections are
+// made, due to the birthday attack).  If an IV is reused, the attacker can
+// not only decrypt the ciphertext of those two connections; they can also
+// easily recover the shadowsocks key and decrypt all other connections to
+// this server.  Use with care!
+func NewPrefixSaltGenerator(prefix []byte) SaltGenerator {
+	return prefixSaltGenerator{prefix}
+}

+ 433 - 0
vendor/github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks/stream.go

@@ -0,0 +1,433 @@
+// 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 shadowsocks
+
+import (
+	"bytes"
+	"crypto/cipher"
+	"encoding/binary"
+	"fmt"
+	"io"
+	"sync"
+
+	"github.com/Jigsaw-Code/outline-sdk/internal/slicepool"
+)
+
+// payloadSizeMask is the maximum size of payload in bytes, as per https://shadowsocks.org/guide/aead.html#tcp.
+const payloadSizeMask = 0x3FFF // 16*1024 - 1
+
+// Buffer pool used for decrypting Shadowsocks streams.
+// The largest buffer we could need is for decrypting a max-length payload.
+var readBufPool = slicepool.MakePool(payloadSizeMask + maxTagSize)
+
+// Writer is an [io.Writer] that also implements [io.ReaderFrom] to
+// allow for piping the data without extra allocations and copies.
+// The LazyWrite and Flush methods allow a header to be
+// added but delayed until the first write, for concatenation.
+// All methods except Flush must be called from a single thread.
+type Writer struct {
+	// This type is single-threaded except when needFlush is true.
+	// mu protects needFlush, and also protects everything
+	// else while needFlush could be true.
+	mu sync.Mutex
+	// Indicates that a concurrent flush is currently allowed.
+	needFlush     bool
+	writer        io.Writer
+	key           *EncryptionKey
+	saltGenerator SaltGenerator
+	// Wrapper for input that arrives as a slice.
+	byteWrapper bytes.Reader
+	// Number of plaintext bytes that are currently buffered.
+	pending int
+	// These are populated by init():
+	buf  []byte
+	aead cipher.AEAD
+	// Index of the next encrypted chunk to write.
+	counter []byte
+}
+
+var (
+	_ io.Writer     = (*Writer)(nil)
+	_ io.ReaderFrom = (*Writer)(nil)
+)
+
+// NewWriter creates a [Writer] that encrypts the given [io.Writer] using
+// the shadowsocks protocol with the given encryption key.
+func NewWriter(writer io.Writer, key *EncryptionKey) *Writer {
+	return &Writer{writer: writer, key: key, saltGenerator: RandomSaltGenerator}
+}
+
+// SetSaltGenerator sets the salt generator to be used. Must be called before the first write.
+func (sw *Writer) SetSaltGenerator(saltGenerator SaltGenerator) {
+	sw.saltGenerator = saltGenerator
+}
+
+// init generates a random salt, sets up the AEAD object and writes
+// the salt to the inner Writer.
+func (sw *Writer) init() (err error) {
+	if sw.aead == nil {
+		salt := make([]byte, sw.key.SaltSize())
+		if err := sw.saltGenerator.GetSalt(salt); err != nil {
+			return fmt.Errorf("failed to generate salt: %w", err)
+		}
+		sw.aead, err = sw.key.NewAEAD(salt)
+		if err != nil {
+			return fmt.Errorf("failed to create AEAD: %w", err)
+		}
+		sw.saltGenerator = nil // No longer needed, so release reference.
+		sw.counter = make([]byte, sw.aead.NonceSize())
+		// The maximum length message is the salt (first message only), length, length tag,
+		// payload, and payload tag.
+		sizeBufSize := 2 + sw.aead.Overhead()
+		maxPayloadBufSize := payloadSizeMask + sw.aead.Overhead()
+		sw.buf = make([]byte, len(salt)+sizeBufSize+maxPayloadBufSize)
+		// Store the salt at the start of sw.buf.
+		copy(sw.buf, salt)
+	}
+	return nil
+}
+
+// encryptBlock encrypts `plaintext` in-place.  The slice must have enough capacity
+// for the tag. Returns the total ciphertext length.
+func (sw *Writer) encryptBlock(plaintext []byte) int {
+	out := sw.aead.Seal(plaintext[:0], sw.counter, plaintext, nil)
+	increment(sw.counter)
+	return len(out)
+}
+
+func (sw *Writer) Write(p []byte) (int, error) {
+	sw.byteWrapper.Reset(p)
+	n, err := sw.ReadFrom(&sw.byteWrapper)
+	return int(n), err
+}
+
+// LazyWrite queues p to be written, but doesn't send it until Flush() is
+// called, a non-lazy write is made, or the buffer is filled.
+func (sw *Writer) LazyWrite(p []byte) (int, error) {
+	if err := sw.init(); err != nil {
+		return 0, err
+	}
+
+	// Locking is needed due to potential concurrency with the Flush()
+	// for a previous call to LazyWrite().
+	sw.mu.Lock()
+	defer sw.mu.Unlock()
+
+	queued := 0
+	for {
+		n := sw.enqueue(p)
+		queued += n
+		p = p[n:]
+		if len(p) == 0 {
+			sw.needFlush = true
+			return queued, nil
+		}
+		// p didn't fit in the buffer.  Flush the buffer and try
+		// again.
+		if err := sw.flush(); err != nil {
+			return queued, err
+		}
+	}
+}
+
+// Flush sends the pending data, if any.  This method is thread-safe.
+func (sw *Writer) Flush() error {
+	sw.mu.Lock()
+	defer sw.mu.Unlock()
+	if !sw.needFlush {
+		return nil
+	}
+	return sw.flush()
+}
+
+func isZero(b []byte) bool {
+	for _, v := range b {
+		if v != 0 {
+			return false
+		}
+	}
+	return true
+}
+
+// Returns the slices of sw.buf in which to place plaintext for encryption.
+func (sw *Writer) buffers() (sizeBuf, payloadBuf []byte) {
+	// sw.buf starts with the salt.
+	saltSize := sw.key.SaltSize()
+
+	// Each Shadowsocks-TCP message consists of a fixed-length size block,
+	// followed by a variable-length payload block.
+	sizeBuf = sw.buf[saltSize : saltSize+2]
+	payloadStart := saltSize + 2 + sw.aead.Overhead()
+	payloadBuf = sw.buf[payloadStart : payloadStart+payloadSizeMask]
+	return
+}
+
+// ReadFrom implements the [io.ReaderFrom] interface.
+func (sw *Writer) ReadFrom(r io.Reader) (int64, error) {
+	if err := sw.init(); err != nil {
+		return 0, err
+	}
+	var written int64
+	var err error
+	_, payloadBuf := sw.buffers()
+
+	// Special case: one thread-safe read, if necessary
+	sw.mu.Lock()
+	if sw.needFlush {
+		pending := sw.pending
+
+		sw.mu.Unlock()
+		saltsize := sw.key.SaltSize()
+		overhead := sw.aead.Overhead()
+		// The first pending+overhead bytes of payloadBuf are potentially
+		// in use, and may be modified on the flush thread.  Data after
+		// that is safe to use on this thread.
+		readBuf := sw.buf[saltsize+2+overhead+pending+overhead:]
+		var plaintextSize int
+		plaintextSize, err = r.Read(readBuf)
+		written = int64(plaintextSize)
+		sw.mu.Lock()
+
+		sw.enqueue(readBuf[:plaintextSize])
+		if flushErr := sw.flush(); flushErr != nil {
+			err = flushErr
+		}
+		sw.needFlush = false
+	}
+	sw.mu.Unlock()
+
+	// Main transfer loop
+	for err == nil {
+		sw.pending, err = r.Read(payloadBuf)
+		written += int64(sw.pending)
+		if flushErr := sw.flush(); flushErr != nil {
+			err = flushErr
+		}
+	}
+
+	if err == io.EOF { // ignore EOF as per io.ReaderFrom contract
+		return written, nil
+	}
+	return written, fmt.Errorf("failed to read payload: %w", err)
+}
+
+// Adds as much of `plaintext` into the buffer as will fit, and increases
+// sw.pending accordingly.  Returns the number of bytes consumed.
+func (sw *Writer) enqueue(plaintext []byte) int {
+	_, payloadBuf := sw.buffers()
+	n := copy(payloadBuf[sw.pending:], plaintext)
+	sw.pending += n
+	return n
+}
+
+// Encrypts all pending data and writes it to the output.
+func (sw *Writer) flush() error {
+	if sw.pending == 0 {
+		return nil
+	}
+	// sw.buf starts with the salt.
+	saltSize := sw.key.SaltSize()
+	// Normally we ignore the salt at the beginning of sw.buf.
+	start := saltSize
+	if isZero(sw.counter) {
+		// For the first message, include the salt.  Compared to writing the salt
+		// separately, this saves one packet during TCP slow-start and potentially
+		// avoids having a distinctive size for the first packet.
+		start = 0
+	}
+
+	sizeBuf, payloadBuf := sw.buffers()
+	binary.BigEndian.PutUint16(sizeBuf, uint16(sw.pending))
+	sizeBlockSize := sw.encryptBlock(sizeBuf)
+	payloadSize := sw.encryptBlock(payloadBuf[:sw.pending])
+	_, err := sw.writer.Write(sw.buf[start : saltSize+sizeBlockSize+payloadSize])
+	sw.pending = 0
+	return err
+}
+
+// genericChunkReader is similar to io.Reader, except that it controls its own
+// buffer granularity.
+type genericChunkReader interface {
+	// ReadChunk reads the next chunk and returns its payload.  The caller must
+	// complete its use of the returned buffer before the next call.
+	// The buffer is nil iff there is an error.  io.EOF indicates a close.
+	ReadChunk() ([]byte, error)
+}
+
+type chunkReader struct {
+	reader io.Reader
+	key    *EncryptionKey
+	// These are lazily initialized:
+	aead cipher.AEAD
+	// Index of the next encrypted chunk to read.
+	counter []byte
+	// Buffer for the uint16 size and its AEAD tag.  Made in init().
+	payloadSizeBuf []byte
+	// Holds a buffer for the payload and its AEAD tag, when needed.
+	payload slicepool.LazySlice
+}
+
+// Reader is an [io.Reader] that also implements [io.WriterTo] to
+// allow for piping the data without extra allocations and copies.
+type Reader interface {
+	io.Reader
+	io.WriterTo
+}
+
+// NewReader creates a [Reader] that decrypts the given [io.Reader] using
+// the shadowsocks protocol with the given encryption key.
+func NewReader(reader io.Reader, key *EncryptionKey) Reader {
+	return &readConverter{
+		cr: &chunkReader{
+			reader:  reader,
+			key:     key,
+			payload: readBufPool.LazySlice(),
+		},
+	}
+}
+
+// init reads the salt from the inner Reader and sets up the AEAD object
+func (cr *chunkReader) init() (err error) {
+	if cr.aead == nil {
+		// For chacha20-poly1305, SaltSize is 32, NonceSize is 12 and Overhead is 16.
+		salt := make([]byte, cr.key.SaltSize())
+		if _, err := io.ReadFull(cr.reader, salt); err != nil {
+			if err != io.EOF && err != io.ErrUnexpectedEOF {
+				err = fmt.Errorf("failed to read salt: %w", err)
+			}
+			return err
+		}
+		cr.aead, err = cr.key.NewAEAD(salt)
+		if err != nil {
+			return fmt.Errorf("failed to create AEAD: %w", err)
+		}
+		cr.counter = make([]byte, cr.aead.NonceSize())
+		cr.payloadSizeBuf = make([]byte, 2+cr.aead.Overhead())
+	}
+	return nil
+}
+
+// readMessage reads, decrypts, and verifies a single AEAD ciphertext.
+// The ciphertext and tag (i.e. "overhead") must exactly fill `buf`,
+// and the decrypted message will be placed in buf[:len(buf)-overhead].
+// Returns an error only if the block could not be read.
+func (cr *chunkReader) readMessage(buf []byte) error {
+	_, err := io.ReadFull(cr.reader, buf)
+	if err != nil {
+		return err
+	}
+	_, err = cr.aead.Open(buf[:0], cr.counter, buf, nil)
+	increment(cr.counter)
+	if err != nil {
+		return fmt.Errorf("failed to decrypt: %w", err)
+	}
+	return nil
+}
+
+// ReadChunk returns the next chunk from the stream.  Callers must fully
+// consume and discard the previous chunk before calling ReadChunk again.
+func (cr *chunkReader) ReadChunk() ([]byte, error) {
+	if err := cr.init(); err != nil {
+		return nil, err
+	}
+
+	// Release the previous payload buffer.
+	cr.payload.Release()
+
+	// In Shadowsocks-AEAD, each chunk consists of two
+	// encrypted messages.  The first message contains the payload length,
+	// and the second message is the payload.  Idle read threads will
+	// block here until the next chunk.
+	if err := cr.readMessage(cr.payloadSizeBuf); err != nil {
+		if err != io.EOF && err != io.ErrUnexpectedEOF {
+			err = fmt.Errorf("failed to read payload size: %w", err)
+		}
+		return nil, err
+	}
+	size := int(binary.BigEndian.Uint16(cr.payloadSizeBuf) & payloadSizeMask)
+	sizeWithTag := size + cr.aead.Overhead()
+	payloadBuf := cr.payload.Acquire()
+	if cap(payloadBuf) < sizeWithTag {
+		// This code is unreachable if the constants are set correctly.
+		return nil, io.ErrShortBuffer
+	}
+	if err := cr.readMessage(payloadBuf[:sizeWithTag]); err != nil {
+		if err == io.EOF { // EOF is not expected mid-chunk.
+			err = io.ErrUnexpectedEOF
+		}
+		cr.payload.Release()
+		return nil, err
+	}
+	return payloadBuf[:size], nil
+}
+
+// readConverter adapts from ChunkReader, with source-controlled
+// chunk sizes, to Go-style IO.
+type readConverter struct {
+	cr       genericChunkReader
+	leftover []byte
+}
+
+func (c *readConverter) Read(b []byte) (int, error) {
+	if err := c.ensureLeftover(); err != nil {
+		return 0, err
+	}
+	n := copy(b, c.leftover)
+	c.leftover = c.leftover[n:]
+	return n, nil
+}
+
+func (c *readConverter) WriteTo(w io.Writer) (written int64, err error) {
+	for {
+		if err = c.ensureLeftover(); err != nil {
+			if err == io.EOF {
+				err = nil
+			}
+			return written, err
+		}
+		n, err := w.Write(c.leftover)
+		written += int64(n)
+		c.leftover = c.leftover[n:]
+		if err != nil {
+			return written, err
+		}
+	}
+}
+
+// Ensures that c.leftover is nonempty.  If leftover is empty, this method
+// waits for incoming data and decrypts it.
+// Returns an error only if c.leftover could not be populated.
+func (c *readConverter) ensureLeftover() error {
+	if len(c.leftover) > 0 {
+		return nil
+	}
+	c.leftover = nil
+	payload, err := c.cr.ReadChunk()
+	if err != nil {
+		return err
+	}
+	c.leftover = payload
+	return nil
+}
+
+// increment little-endian encoded unsigned integer b. Wrap around on overflow.
+func increment(b []byte) {
+	for i := range b {
+		b[i]++
+		if b[i] != 0 {
+			return
+		}
+	}
+}

+ 104 - 0
vendor/github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks/stream_dialer.go

@@ -0,0 +1,104 @@
+// Copyright 2023 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 shadowsocks
+
+import (
+	"context"
+	"errors"
+	"time"
+
+	"github.com/Jigsaw-Code/outline-sdk/transport"
+	"github.com/shadowsocks/go-shadowsocks2/socks"
+)
+
+// NewStreamDialer creates a client that routes connections to a Shadowsocks proxy listening at
+// the given StreamEndpoint, with `key` as the Shadowsocks encyption key.
+func NewStreamDialer(endpoint transport.StreamEndpoint, key *EncryptionKey) (*StreamDialer, error) {
+	if endpoint == nil {
+		return nil, errors.New("argument endpoint must not be nil")
+	}
+	if key == nil {
+		return nil, errors.New("argument key must not be nil")
+	}
+	d := StreamDialer{endpoint: endpoint, key: key, ClientDataWait: 10 * time.Millisecond}
+	return &d, nil
+}
+
+type StreamDialer struct {
+	endpoint transport.StreamEndpoint
+	key      *EncryptionKey
+
+	// SaltGenerator is used by Shadowsocks to generate the connection salts.
+	// `SaltGenerator` can be `nil`, which defaults to [shadowsocks.RandomSaltGenerator].
+	SaltGenerator SaltGenerator
+
+	// ClientDataWait specifies the amount of time to wait for client data before sending
+	// the Shadowsocks connection request to the proxy server. This value is 10 milliseconds
+	// by default.
+	//
+	// StreamDialer has an optimization to send the initial client payload along with
+	// the Shadowsocks connection request.  This saves one packet during connection, and also
+	// reduces the distinctiveness of the connection pattern.
+	//
+	// Normally, the initial payload will be sent as soon as the socket is connected,
+	// except for delays due to inter-process communication.  However, some protocols
+	// expect the server to send data first, in which case there is no client payload.
+	// We therefore use a short delay by default (10ms), longer than any reasonable IPC but shorter than
+	// typical network latency.  (In an Android emulator, the 90th percentile delay
+	// was ~1 ms.)  If no client payload is received by this time, we connect without it.
+	ClientDataWait time.Duration
+}
+
+var _ transport.StreamDialer = (*StreamDialer)(nil)
+
+// DialStream implements StreamDialer.DialStream using a Shadowsocks server.
+//
+// The Shadowsocks StreamDialer returns a connection after the connection to the proxy is established,
+// but before the connection to the target is established. That means we cannot signal "connection refused"
+// or "connection timeout" errors from the target to the application.
+//
+// This behavior breaks IPv6 Happy Eyeballs because the application IPv6 socket will connect successfully,
+// even if the proxy fails to connect to the IPv6 destination. The broken Happy Eyeballs behavior makes
+// IPv6 unusable if the proxy cannot use IPv6.
+//
+// We can't easily fix that issue because Shadowsocks, unlike SOCKS, does not have a way to indicate
+// whether the target connection is successful. Even if that was possible, we want to wait until we have
+// initial data from the application in order to send the Shadowsocks salt, SOCKS address and initial data
+// all in one packet. This makes the size of the initial packet hard to predict, avoiding packet size
+// fingerprinting. We can only get the application initial data if we return a connection first.
+func (c *StreamDialer) DialStream(ctx context.Context, remoteAddr string) (transport.StreamConn, error) {
+	socksTargetAddr := socks.ParseAddr(remoteAddr)
+	if socksTargetAddr == nil {
+		return nil, errors.New("failed to parse target address")
+	}
+	proxyConn, err := c.endpoint.ConnectStream(ctx)
+	if err != nil {
+		return nil, err
+	}
+	ssw := NewWriter(proxyConn, c.key)
+	if c.SaltGenerator != nil {
+		ssw.SetSaltGenerator(c.SaltGenerator)
+	}
+	_, err = ssw.LazyWrite(socksTargetAddr)
+	if err != nil {
+		proxyConn.Close()
+		return nil, errors.New("failed to write target address")
+	}
+	time.AfterFunc(c.ClientDataWait, func() {
+		ssw.Flush()
+	})
+	ssr := NewReader(proxyConn, c.key)
+	return transport.WrapConn(proxyConn, ssr, ssw), nil
+}

+ 158 - 0
vendor/github.com/Jigsaw-Code/outline-sdk/transport/stream.go

@@ -0,0 +1,158 @@
+// 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 transport
+
+import (
+	"context"
+	"io"
+	"net"
+)
+
+// StreamConn is a [net.Conn] that allows for closing only the reader or writer end of it, supporting half-open state.
+type StreamConn interface {
+	net.Conn
+	// Closes the Read end of the connection, allowing for the release of resources.
+	// No more reads should happen.
+	CloseRead() error
+	// Closes the Write end of the connection. An EOF or FIN signal can be
+	// sent to the connection target.
+	CloseWrite() error
+}
+
+type duplexConnAdaptor struct {
+	StreamConn
+	r io.Reader
+	w io.Writer
+}
+
+var _ StreamConn = (*duplexConnAdaptor)(nil)
+
+func (dc *duplexConnAdaptor) Read(b []byte) (int, error) {
+	return dc.r.Read(b)
+}
+func (dc *duplexConnAdaptor) WriteTo(w io.Writer) (int64, error) {
+	return io.Copy(w, dc.r)
+}
+func (dc *duplexConnAdaptor) CloseRead() error {
+	return dc.StreamConn.CloseRead()
+}
+func (dc *duplexConnAdaptor) Write(b []byte) (int, error) {
+	return dc.w.Write(b)
+}
+func (dc *duplexConnAdaptor) ReadFrom(r io.Reader) (int64, error) {
+	// Make sure we prefer ReadFrom. Otherwise io.Copy will try WriteTo first.
+	if rf, ok := dc.w.(io.ReaderFrom); ok {
+		return rf.ReadFrom(r)
+	}
+	return io.Copy(dc.w, r)
+}
+func (dc *duplexConnAdaptor) CloseWrite() error {
+	return dc.StreamConn.CloseWrite()
+}
+
+// WrapConn wraps an existing [StreamConn] with a new [io.Reader] and [io.Writer], but preserves the original
+// [StreamConn].CloseRead and [StreamConn].CloseWrite.
+func WrapConn(c StreamConn, r io.Reader, w io.Writer) StreamConn {
+	conn := c
+	// We special-case duplexConnAdaptor to avoid multiple levels of nesting.
+	if a, ok := c.(*duplexConnAdaptor); ok {
+		conn = a.StreamConn
+	}
+	return &duplexConnAdaptor{StreamConn: conn, r: r, w: w}
+}
+
+// StreamEndpoint represents an endpoint that can be used to establish stream connections (like TCP) to a fixed
+// destination.
+type StreamEndpoint interface {
+	// ConnectStream establishes a connection with the endpoint, returning the connection.
+	ConnectStream(ctx context.Context) (StreamConn, error)
+}
+
+// TCPEndpoint is a [StreamEndpoint] that connects to the specified address using the specified [StreamDialer].
+type TCPEndpoint struct {
+	// The Dialer used to create the net.Conn on Connect().
+	Dialer net.Dialer
+	// The endpoint address (host:port) to pass to Dial.
+	// If the host is a domain name, consider pre-resolving it to avoid resolution calls.
+	Address string
+}
+
+var _ StreamEndpoint = (*TCPEndpoint)(nil)
+
+// ConnectStream implements [StreamEndpoint].ConnectStream.
+func (e *TCPEndpoint) ConnectStream(ctx context.Context) (StreamConn, error) {
+	conn, err := e.Dialer.DialContext(ctx, "tcp", e.Address)
+	if err != nil {
+		return nil, err
+	}
+	return conn.(*net.TCPConn), nil
+}
+
+// FuncStreamEndpoint is a [StreamEndpoint] that uses the given function to connect.
+type FuncStreamEndpoint func(ctx context.Context) (StreamConn, error)
+
+var _ StreamEndpoint = (*FuncStreamEndpoint)(nil)
+
+// ConnectStream implements the [StreamEndpoint] interface.
+func (f FuncStreamEndpoint) ConnectStream(ctx context.Context) (StreamConn, error) {
+	return f(ctx)
+}
+
+// StreamDialerEndpoint is a [StreamEndpoint] that connects to the specified address using the specified
+// [StreamDialer].
+type StreamDialerEndpoint struct {
+	Dialer  StreamDialer
+	Address string
+}
+
+var _ StreamEndpoint = (*StreamDialerEndpoint)(nil)
+
+// ConnectStream implements [StreamEndpoint].ConnectStream.
+func (e *StreamDialerEndpoint) ConnectStream(ctx context.Context) (StreamConn, error) {
+	return e.Dialer.DialStream(ctx, e.Address)
+}
+
+// StreamDialer provides a way to dial a destination and establish stream connections.
+type StreamDialer interface {
+	// DialStream connects to `raddr`.
+	// `raddr` has the form "host:port", where "host" can be a domain name or IP address.
+	DialStream(ctx context.Context, raddr string) (StreamConn, error)
+}
+
+// TCPDialer is a [StreamDialer] that uses the standard [net.Dialer] to dial.
+// It provides a convenient way to use a [net.Dialer] when you need a [StreamDialer].
+type TCPDialer struct {
+	Dialer net.Dialer
+}
+
+var _ StreamDialer = (*TCPDialer)(nil)
+
+func (d *TCPDialer) DialStream(ctx context.Context, addr string) (StreamConn, error) {
+	conn, err := d.Dialer.DialContext(ctx, "tcp", addr)
+	if err != nil {
+		return nil, err
+	}
+	return conn.(*net.TCPConn), nil
+}
+
+// FuncStreamDialer is a [StreamDialer] that uses the given function to dial.
+type FuncStreamDialer func(ctx context.Context, addr string) (StreamConn, error)
+
+var _ StreamDialer = (*FuncStreamDialer)(nil)
+
+// DialStream implements the [StreamDialer] interface.
+func (f FuncStreamDialer) DialStream(ctx context.Context, addr string) (StreamConn, error) {
+	return f(ctx, addr)
+}

+ 202 - 0
vendor/github.com/shadowsocks/go-shadowsocks2/LICENSE

@@ -0,0 +1,202 @@
+
+                                 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.

+ 214 - 0
vendor/github.com/shadowsocks/go-shadowsocks2/socks/socks.go

@@ -0,0 +1,214 @@
+// Package socks implements essential parts of SOCKS protocol.
+package socks
+
+import (
+	"io"
+	"net"
+	"strconv"
+)
+
+// UDPEnabled is the toggle for UDP support
+var UDPEnabled = false
+
+// SOCKS request commands as defined in RFC 1928 section 4.
+const (
+	CmdConnect      = 1
+	CmdBind         = 2
+	CmdUDPAssociate = 3
+)
+
+// SOCKS address types as defined in RFC 1928 section 5.
+const (
+	AtypIPv4       = 1
+	AtypDomainName = 3
+	AtypIPv6       = 4
+)
+
+// Error represents a SOCKS error
+type Error byte
+
+func (err Error) Error() string {
+	return "SOCKS error: " + strconv.Itoa(int(err))
+}
+
+// SOCKS errors as defined in RFC 1928 section 6.
+const (
+	ErrGeneralFailure       = Error(1)
+	ErrConnectionNotAllowed = Error(2)
+	ErrNetworkUnreachable   = Error(3)
+	ErrHostUnreachable      = Error(4)
+	ErrConnectionRefused    = Error(5)
+	ErrTTLExpired           = Error(6)
+	ErrCommandNotSupported  = Error(7)
+	ErrAddressNotSupported  = Error(8)
+	InfoUDPAssociate        = Error(9)
+)
+
+// MaxAddrLen is the maximum size of SOCKS address in bytes.
+const MaxAddrLen = 1 + 1 + 255 + 2
+
+// Addr represents a SOCKS address as defined in RFC 1928 section 5.
+type Addr []byte
+
+// String serializes SOCKS address a to string form.
+func (a Addr) String() string {
+	var host, port string
+
+	switch a[0] { // address type
+	case AtypDomainName:
+		host = string(a[2 : 2+int(a[1])])
+		port = strconv.Itoa((int(a[2+int(a[1])]) << 8) | int(a[2+int(a[1])+1]))
+	case AtypIPv4:
+		host = net.IP(a[1 : 1+net.IPv4len]).String()
+		port = strconv.Itoa((int(a[1+net.IPv4len]) << 8) | int(a[1+net.IPv4len+1]))
+	case AtypIPv6:
+		host = net.IP(a[1 : 1+net.IPv6len]).String()
+		port = strconv.Itoa((int(a[1+net.IPv6len]) << 8) | int(a[1+net.IPv6len+1]))
+	}
+
+	return net.JoinHostPort(host, port)
+}
+
+func readAddr(r io.Reader, b []byte) (Addr, error) {
+	if len(b) < MaxAddrLen {
+		return nil, io.ErrShortBuffer
+	}
+	_, err := io.ReadFull(r, b[:1]) // read 1st byte for address type
+	if err != nil {
+		return nil, err
+	}
+
+	switch b[0] {
+	case AtypDomainName:
+		_, err = io.ReadFull(r, b[1:2]) // read 2nd byte for domain length
+		if err != nil {
+			return nil, err
+		}
+		_, err = io.ReadFull(r, b[2:2+int(b[1])+2])
+		return b[:1+1+int(b[1])+2], err
+	case AtypIPv4:
+		_, err = io.ReadFull(r, b[1:1+net.IPv4len+2])
+		return b[:1+net.IPv4len+2], err
+	case AtypIPv6:
+		_, err = io.ReadFull(r, b[1:1+net.IPv6len+2])
+		return b[:1+net.IPv6len+2], err
+	}
+
+	return nil, ErrAddressNotSupported
+}
+
+// ReadAddr reads just enough bytes from r to get a valid Addr.
+func ReadAddr(r io.Reader) (Addr, error) {
+	return readAddr(r, make([]byte, MaxAddrLen))
+}
+
+// SplitAddr slices a SOCKS address from beginning of b. Returns nil if failed.
+func SplitAddr(b []byte) Addr {
+	addrLen := 1
+	if len(b) < addrLen {
+		return nil
+	}
+
+	switch b[0] {
+	case AtypDomainName:
+		if len(b) < 2 {
+			return nil
+		}
+		addrLen = 1 + 1 + int(b[1]) + 2
+	case AtypIPv4:
+		addrLen = 1 + net.IPv4len + 2
+	case AtypIPv6:
+		addrLen = 1 + net.IPv6len + 2
+	default:
+		return nil
+
+	}
+
+	if len(b) < addrLen {
+		return nil
+	}
+
+	return b[:addrLen]
+}
+
+// ParseAddr parses the address in string s. Returns nil if failed.
+func ParseAddr(s string) Addr {
+	var addr Addr
+	host, port, err := net.SplitHostPort(s)
+	if err != nil {
+		return nil
+	}
+	if ip := net.ParseIP(host); ip != nil {
+		if ip4 := ip.To4(); ip4 != nil {
+			addr = make([]byte, 1+net.IPv4len+2)
+			addr[0] = AtypIPv4
+			copy(addr[1:], ip4)
+		} else {
+			addr = make([]byte, 1+net.IPv6len+2)
+			addr[0] = AtypIPv6
+			copy(addr[1:], ip)
+		}
+	} else {
+		if len(host) > 255 {
+			return nil
+		}
+		addr = make([]byte, 1+1+len(host)+2)
+		addr[0] = AtypDomainName
+		addr[1] = byte(len(host))
+		copy(addr[2:], host)
+	}
+
+	portnum, err := strconv.ParseUint(port, 10, 16)
+	if err != nil {
+		return nil
+	}
+
+	addr[len(addr)-2], addr[len(addr)-1] = byte(portnum>>8), byte(portnum)
+
+	return addr
+}
+
+// Handshake fast-tracks SOCKS initialization to get target address to connect.
+func Handshake(rw io.ReadWriter) (Addr, error) {
+	// Read RFC 1928 for request and reply structure and sizes.
+	buf := make([]byte, MaxAddrLen)
+	// read VER, NMETHODS, METHODS
+	if _, err := io.ReadFull(rw, buf[:2]); err != nil {
+		return nil, err
+	}
+	nmethods := buf[1]
+	if _, err := io.ReadFull(rw, buf[:nmethods]); err != nil {
+		return nil, err
+	}
+	// write VER METHOD
+	if _, err := rw.Write([]byte{5, 0}); err != nil {
+		return nil, err
+	}
+	// read VER CMD RSV ATYP DST.ADDR DST.PORT
+	if _, err := io.ReadFull(rw, buf[:3]); err != nil {
+		return nil, err
+	}
+	cmd := buf[1]
+	addr, err := readAddr(rw, buf)
+	if err != nil {
+		return nil, err
+	}
+	switch cmd {
+	case CmdConnect:
+		_, err = rw.Write([]byte{5, 0, 0, 1, 0, 0, 0, 0, 0, 0}) // SOCKS v5, reply succeeded
+	case CmdUDPAssociate:
+		if !UDPEnabled {
+			return nil, ErrCommandNotSupported
+		}
+		listenAddr := ParseAddr(rw.(net.Conn).LocalAddr().String())
+		_, err = rw.Write(append([]byte{5, 0, 0}, listenAddr...)) // SOCKS v5, reply succeeded
+		if err != nil {
+			return nil, ErrCommandNotSupported
+		}
+		err = InfoUDPAssociate
+	default:
+		return nil, ErrCommandNotSupported
+	}
+
+	return addr, err // skip VER, CMD, RSV fields
+}

+ 8 - 0
vendor/modules.txt

@@ -11,6 +11,11 @@ filippo.io/keygen
 # github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96
 ## explicit
 github.com/AndreasBriese/bbloom
+# github.com/Jigsaw-Code/outline-sdk v0.0.16
+## explicit; go 1.20
+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/Psiphon-Inc/rotate-safe-writer v0.0.0-20210303140923-464a7a37606e
 ## explicit
 github.com/Psiphon-Inc/rotate-safe-writer
@@ -450,6 +455,9 @@ github.com/ryanuber/go-glob
 # github.com/sergeyfrolov/bsbuffer v0.0.0-20180903213811-94e85abb8507
 ## explicit
 github.com/sergeyfrolov/bsbuffer
+# github.com/shadowsocks/go-shadowsocks2 v0.1.5
+## explicit; go 1.16
+github.com/shadowsocks/go-shadowsocks2/socks
 # github.com/shirou/gopsutil/v4 v4.24.5
 ## explicit; go 1.18
 github.com/shirou/gopsutil/v4/common