From 5797872c29840abb0dfab0855cf73057279f49e3 Mon Sep 17 00:00:00 2001 From: kenzok8 Date: Sat, 8 Aug 2026 23:54:43 +0800 Subject: [PATCH] fix(ssr): reach obfs conn through the buffered reader wrapper The shadowsocks stream layer hands its cipher and address length down to the SSR obfs conn via type assertions on the underlying conn. Since the TCP read-buffering change wrapped that conn in netproxy.BufferedReaderConn, both assertions stop matching, the obfs layer never receives a cipher and every SSR handshake fails with "outer conn did not init cipher of Obfs". Peel transparent wrappers via the existing IntrinsicConn accessor before asserting, the same escape hatch XTLS/Vision already relies on. Add tests covering the wrapped and unwrapped chains so a future wrapper cannot silently break the handshake again. --- .../shadowsocks_stream/ssr_cipher_test.go | 73 +++++++++++++++++++ protocol/shadowsocks_stream/tcp_conn.go | 16 +++- 2 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 protocol/shadowsocks_stream/ssr_cipher_test.go diff --git a/protocol/shadowsocks_stream/ssr_cipher_test.go b/protocol/shadowsocks_stream/ssr_cipher_test.go new file mode 100644 index 00000000..a59e0d3b --- /dev/null +++ b/protocol/shadowsocks_stream/ssr_cipher_test.go @@ -0,0 +1,73 @@ +package shadowsocks_stream + +import ( + "io" + "net" + "testing" + "time" + + "github.com/daeuniverse/outbound/ciphers" + "github.com/daeuniverse/outbound/netproxy" +) + +// obfsStub stands in for transport/shadowsocksr/obfs.Conn: it records the +// cipher and address length handed down by the shadowsocks stream layer. +type obfsStub struct { + netproxy.Conn + cipher *ciphers.StreamCipher + addrLen int +} + +func (o *obfsStub) SetCipher(cipher *ciphers.StreamCipher) { o.cipher = cipher } +func (o *obfsStub) SetAddrLen(addrLen int) { o.addrLen = addrLen } + +type discardConn struct{ net.Conn } + +func (discardConn) Read(p []byte) (int, error) { return 0, io.EOF } +func (discardConn) Write(p []byte) (int, error) { return len(p), nil } +func (discardConn) Close() error { return nil } +func (discardConn) SetDeadline(time.Time) error { return nil } +func (discardConn) SetReadDeadline(time.Time) error { return nil } +func (discardConn) SetWriteDeadline(t time.Time) error { + return nil +} + +func newStubChain(t *testing.T, wrap bool) (*TcpConn, *obfsStub) { + t.Helper() + cipher, err := ciphers.NewStreamCipher("aes-256-cfb", "p@ssw0rd") + if err != nil { + t.Fatal(err) + } + stub := &obfsStub{Conn: discardConn{}} + var under netproxy.Conn = stub + if wrap { + under = netproxy.NewBufferedReaderConn(stub, 0) + } + return NewTcpConn(under, cipher), stub +} + +func TestSSRObfsReceivesCipherWithoutWrapper(t *testing.T) { + conn, stub := newStubChain(t, false) + if _, err := conn.Write([]byte("hello")); err != nil { + t.Fatal(err) + } + if stub.cipher == nil { + t.Fatal("obfs conn did not receive the cipher") + } + if stub.addrLen != len("hello") { + t.Fatalf("addrLen = %d, want %d", stub.addrLen, len("hello")) + } +} + +func TestSSRObfsReceivesCipherThroughBufferedReaderConn(t *testing.T) { + conn, stub := newStubChain(t, true) + if _, err := conn.Write([]byte("hello")); err != nil { + t.Fatal(err) + } + if stub.cipher == nil { + t.Fatal("obfs conn behind BufferedReaderConn did not receive the cipher") + } + if stub.addrLen != len("hello") { + t.Fatalf("addrLen = %d, want %d", stub.addrLen, len("hello")) + } +} diff --git a/protocol/shadowsocks_stream/tcp_conn.go b/protocol/shadowsocks_stream/tcp_conn.go index 3afb724a..e4e2f1db 100644 --- a/protocol/shadowsocks_stream/tcp_conn.go +++ b/protocol/shadowsocks_stream/tcp_conn.go @@ -24,6 +24,17 @@ func NewTcpConn(c netproxy.Conn, cipher *ciphers.StreamCipher) *TcpConn { } } +// unwrapConn peels transparent wrappers such as netproxy.BufferedReaderConn so +// that SSR obfs conns stay reachable by the type assertions below. +func unwrapConn(c netproxy.Conn) netproxy.Conn { + if ic, ok := c.(interface{ IntrinsicConn() netproxy.Conn }); ok { + if inner := ic.IntrinsicConn(); inner != nil { + return inner + } + } + return c +} + func (c *TcpConn) Read(b []byte) (n int, err error) { if !c.cipher.DecryptInited() { buf := b @@ -82,12 +93,13 @@ func (c *TcpConn) Write(b []byte) (n int, err error) { b = buf // For SSR obfs. - if innerConn, ok := c.Conn.(interface { + obfsConn := unwrapConn(c.Conn) + if innerConn, ok := obfsConn.(interface { SetCipher(cipher *ciphers.StreamCipher) }); ok { innerConn.SetCipher(c.cipher) } - if innerConn, ok := c.Conn.(interface { + if innerConn, ok := obfsConn.(interface { SetAddrLen(addrLen int) }); ok { innerConn.SetAddrLen(lenToWrite)