- 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
226 lines
6.7 KiB
Go
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)
|
|
}
|