hysteria/extras/obfs/multi_test.go
Niko Marmeladkov 1e77bbe7b7
Some checks are pending
Build master branch / build (push) Waiting to run
Tests / Test (push) Waiting to run
feat: TLS 1.3 mimicry obfuscation, multi-mode (auto + plain), session recreation
- tlsmimic: wrap QUIC packets in TLS 1.3 record headers
- multi: auto-detect Salamander/TLS-mimic/plain on the same port
- multi: random padding and jitter support for DPI evasion
- reconnect: periodic session recreation via MaxSessionDuration
- client/server: wire tlsmimic/auto/plain obfs types and config
- NEW_FEATURES: translated to English
2026-06-17 16:36:29 +03:00

226 lines
6.7 KiB
Go

package obfs
import (
"net"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// duplexPipe pairs two channels so that write(0→1) is read by read(1→0).
type duplexPipe struct {
toServer chan pkt
toClient chan pkt
addr net.Addr
}
type pkt struct {
data []byte
addr net.Addr
}
func newDuplexPipe() *duplexPipe {
return &duplexPipe{
toServer: make(chan pkt, 100),
toClient: make(chan pkt, 100),
addr: &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 9999},
}
}
func (p *duplexPipe) ReadFromServerSide(b []byte) (int, net.Addr, error) {
pkt := <-p.toServer
return copy(b, pkt.data), pkt.addr, nil
}
func (p *duplexPipe) WriteToServerSide(b []byte, addr net.Addr) (int, error) {
data := make([]byte, len(b))
copy(data, b)
p.toClient <- pkt{data: data, addr: addr}
return len(b), nil
}
func (p *duplexPipe) ReadFromClientSide(b []byte) (int, net.Addr, error) {
pkt := <-p.toClient
return copy(b, pkt.data), pkt.addr, nil
}
func (p *duplexPipe) WriteToClientSide(b []byte, addr net.Addr) (int, error) {
data := make([]byte, len(b))
copy(data, b)
p.toServer <- pkt{data: data, addr: addr}
return len(b), nil
}
// serverConn wraps duplexPipe to look like a single PacketConn from the
// server's perspective: ReadFrom reads client→server, WriteTo writes server→client.
type serverConn struct {
p *duplexPipe
}
func (s *serverConn) ReadFrom(b []byte) (int, net.Addr, error) { return s.p.ReadFromServerSide(b) }
func (s *serverConn) WriteTo(b []byte, a net.Addr) (int, error) { return s.p.WriteToServerSide(b, a) }
func (s *serverConn) Close() error { return nil }
func (s *serverConn) LocalAddr() net.Addr { return s.p.addr }
func (s *serverConn) SetDeadline(time.Time) error { return nil }
func (s *serverConn) SetReadDeadline(time.Time) error { return nil }
func (s *serverConn) SetWriteDeadline(time.Time) error { return nil }
// clientConn wraps duplexPipe from the client perspective.
type clientConn struct {
p *duplexPipe
}
func (c *clientConn) ReadFrom(b []byte) (int, net.Addr, error) { return c.p.ReadFromClientSide(b) }
func (c *clientConn) WriteTo(b []byte, a net.Addr) (int, error) { return c.p.WriteToClientSide(b, a) }
func (c *clientConn) Close() error { return nil }
func (c *clientConn) LocalAddr() net.Addr { return c.p.addr }
func (c *clientConn) SetDeadline(time.Time) error { return nil }
func (c *clientConn) SetReadDeadline(time.Time) error { return nil }
func (c *clientConn) SetWriteDeadline(time.Time) error { return nil }
func TestMultiObfsRoundTripSalamander(t *testing.T) {
d := newDuplexPipe()
server, err := WrapPacketConnMulti(&serverConn{d}, MultiObfsOptions{
Password: "test",
})
require.NoError(t, err)
defer server.Close()
client, err := WrapPacketConnSalamander(&clientConn{d}, []byte("test"))
require.NoError(t, err)
payload := []byte("hello salamander via multi")
n, err := client.WriteTo(payload, d.addr)
require.NoError(t, err)
require.Equal(t, len(payload), n)
buf := make([]byte, 2048)
n, addr, err := server.ReadFrom(buf)
require.NoError(t, err)
assert.Equal(t, payload, buf[:n])
_, err = server.WriteTo(buf[:n], addr)
require.NoError(t, err)
buf2 := make([]byte, 2048)
n, _, err = client.ReadFrom(buf2)
require.NoError(t, err)
assert.Equal(t, payload, buf2[:n])
}
func TestMultiObfsRoundTripTLSMimic(t *testing.T) {
d := newDuplexPipe()
server, err := WrapPacketConnMulti(&serverConn{d}, MultiObfsOptions{
Password: "test",
})
require.NoError(t, err)
defer server.Close()
client, err := WrapPacketConnTLSMimic(&clientConn{d}, []byte("test"))
require.NoError(t, err)
payload := []byte("hello tlsmimic via multi")
n, err := client.WriteTo(payload, d.addr)
require.NoError(t, err)
require.Equal(t, len(payload), n)
buf := make([]byte, 2048)
n, addr, err := server.ReadFrom(buf)
require.NoError(t, err)
assert.Equal(t, payload, buf[:n])
_, err = server.WriteTo(buf[:n], addr)
require.NoError(t, err)
buf2 := make([]byte, 2048)
n, _, err = client.ReadFrom(buf2)
require.NoError(t, err)
assert.Equal(t, payload, buf2[:n])
}
func TestMultiObfsRoundTripBoth(t *testing.T) {
d := newDuplexPipe()
server, err := WrapPacketConnMulti(&serverConn{d}, MultiObfsOptions{
Password: "test",
})
require.NoError(t, err)
defer server.Close()
payload1 := []byte("from salamander")
payload2 := []byte("from tlsmimic")
// Write obfuscated data directly to the server-side pipe channel
// using pre-computed obfuscated payloads.
sm, _ := newSalamanderObfuscator([]byte("test"))
tm, _ := newTLSMimicObfuscator([]byte("test"))
smOut := make([]byte, 2048)
tmOut := make([]byte, 2048)
smN := sm.Obfuscate(payload1, smOut)
tmN := tm.Obfuscate(payload2, tmOut)
// Both "clients" send through the shared pipe's client→server channel
d.toServer <- pkt{data: smOut[:smN], addr: &net.UDPAddr{IP: net.IPv4(10, 0, 0, 1), Port: 10001}}
d.toServer <- pkt{data: tmOut[:tmN], addr: &net.UDPAddr{IP: net.IPv4(10, 0, 0, 2), Port: 10002}}
buf := make([]byte, 2048)
n, addr1, err := server.ReadFrom(buf)
require.NoError(t, err)
assert.Equal(t, payload1, buf[:n])
n, addr2, err := server.ReadFrom(buf)
require.NoError(t, err)
assert.Equal(t, payload2, buf[:n])
// Server responds to each with the correct method (auto-detected)
_, err = server.WriteTo([]byte("reply1"), addr1)
require.NoError(t, err)
_, err = server.WriteTo([]byte("reply2"), addr2)
require.NoError(t, err)
// Read server responses from client side
rBuf := make([]byte, 2048)
n, _, err = d.ReadFromClientSide(rBuf)
require.NoError(t, err)
// First response to Salamander client
out := make([]byte, 2048)
nn := sm.Deobfuscate(rBuf[:n], out)
assert.Equal(t, []byte("reply1"), out[:nn])
n, _, err = d.ReadFromClientSide(rBuf)
require.NoError(t, err)
// Second response to TLS-mimic client
nn = tm.Deobfuscate(rBuf[:n], out)
assert.Equal(t, []byte("reply2"), out[:nn])
}
func TestMultiObfsRequiresPasswordForObfs(t *testing.T) {
d := newDuplexPipe()
_, err := WrapPacketConnMulti(&serverConn{d}, MultiObfsOptions{
Password: "",
})
assert.NoError(t, err)
}
func TestMultiObfsRandomPadding(t *testing.T) {
d := newDuplexPipe()
server, err := WrapPacketConnMulti(&serverConn{d}, MultiObfsOptions{
Password: "test",
MinPadding: 1000,
MaxPadding: 1200,
})
require.NoError(t, err)
defer server.Close()
_, err = server.WriteTo([]byte("small"), d.addr)
require.NoError(t, err)
// Read raw from client side to check padding
buf := make([]byte, 2048)
n, _, err := d.ReadFromClientSide(buf)
require.NoError(t, err)
assert.GreaterOrEqual(t, n, 1000)
assert.LessOrEqual(t, n, 1200)
}