- 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
303 lines
6.9 KiB
Go
303 lines
6.9 KiB
Go
package obfs
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/binary"
|
|
"net"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
)
|
|
|
|
// ObfsMode represents an obfuscation method.
|
|
type ObfsMode int
|
|
|
|
const (
|
|
ObfsModeSalamander ObfsMode = iota
|
|
ObfsModeTLSMimic
|
|
ObfsModePlain
|
|
)
|
|
|
|
// MultiObfsOptions configures the multi-mode obfuscation wrapper.
|
|
type MultiObfsOptions struct {
|
|
// PSK for Salamander and TLS-mimic (they share the same key).
|
|
Password string
|
|
// PadRnd randomizes each outbound packet's total size to land in
|
|
// [MinPadding, MaxPadding]. Zero means no padding.
|
|
MinPadding int
|
|
MaxPadding int
|
|
// SendJitter adds a random delay [0, SendJitter) before each outbound
|
|
// packet to break constant-bitrate DPI patterns. Zero means no delay.
|
|
SendJitter time.Duration
|
|
}
|
|
|
|
type multiObfsPacketConn struct {
|
|
inner net.PacketConn
|
|
|
|
sm *salamanderObfuscator
|
|
tm *tlsMimicObfuscator
|
|
pad bool
|
|
minP, maxP int
|
|
sendJitter time.Duration
|
|
|
|
addrModes map[string]ObfsMode
|
|
mu sync.RWMutex
|
|
|
|
readMu sync.Mutex
|
|
readBuf []byte
|
|
writeMu sync.Mutex
|
|
writeBuf []byte
|
|
|
|
closeCh chan struct{}
|
|
closeOnce sync.Once
|
|
}
|
|
|
|
// WrapPacketConnMulti wraps conn so it accepts packets obfuscated with any of
|
|
// Salamander or TLS-mimic on the same port. Which method to use for outbound
|
|
// packets is auto-detected per remote address on the first inbound packet from
|
|
// that address.
|
|
//
|
|
// Both Salamander and TLS-mimic use the same PSK for the BLAKE2b-XOR cipher.
|
|
func WrapPacketConnMulti(conn net.PacketConn, opts MultiObfsOptions) (net.PacketConn, error) {
|
|
m := &multiObfsPacketConn{
|
|
inner: conn,
|
|
addrModes: make(map[string]ObfsMode),
|
|
readBuf: make([]byte, udpBufferSize),
|
|
writeBuf: make([]byte, udpBufferSize),
|
|
sendJitter: opts.SendJitter,
|
|
closeCh: make(chan struct{}),
|
|
}
|
|
|
|
if opts.Password != "" {
|
|
psk := []byte(opts.Password)
|
|
var err error
|
|
m.sm, err = newSalamanderObfuscator(psk)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
m.tm, err = newTLSMimicObfuscator(psk)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if opts.MinPadding > 0 && opts.MaxPadding >= opts.MinPadding {
|
|
m.pad = true
|
|
m.minP = opts.MinPadding
|
|
m.maxP = opts.MaxPadding
|
|
}
|
|
|
|
return m, nil
|
|
}
|
|
|
|
// --- Read path: try each deobfuscator in order, cache the method ---
|
|
|
|
func (m *multiObfsPacketConn) ReadFrom(p []byte) (int, net.Addr, error) {
|
|
for {
|
|
select {
|
|
case <-m.closeCh:
|
|
return 0, nil, net.ErrClosed
|
|
default:
|
|
}
|
|
|
|
m.readMu.Lock()
|
|
n, addr, err := m.inner.ReadFrom(m.readBuf)
|
|
if err != nil {
|
|
m.readMu.Unlock()
|
|
return 0, addr, err
|
|
}
|
|
if n <= 0 {
|
|
m.readMu.Unlock()
|
|
continue
|
|
}
|
|
|
|
data := m.readBuf[:n]
|
|
var plaintext []byte
|
|
var mode ObfsMode
|
|
|
|
// 1. Try TLS-mimic (detectable by leading 0x17 on the wire)
|
|
if m.tm != nil && n > 0 && data[0] == 0x17 {
|
|
buf := make([]byte, udpBufferSize)
|
|
nn := m.tm.Deobfuscate(data, buf)
|
|
if nn > 0 {
|
|
plaintext = buf[:nn]
|
|
mode = ObfsModeTLSMimic
|
|
}
|
|
}
|
|
|
|
// 2. Plain QUIC pass-through (QUIC long header: first two bits = 11)
|
|
if plaintext == nil && n > 0 && data[0]&0xC0 == 0xC0 {
|
|
buf := make([]byte, n)
|
|
copy(buf, data)
|
|
plaintext = buf
|
|
mode = ObfsModePlain
|
|
}
|
|
|
|
// 3. Try Salamander
|
|
if plaintext == nil && m.sm != nil {
|
|
buf := make([]byte, udpBufferSize)
|
|
nn := m.sm.Deobfuscate(data, buf)
|
|
if nn > 0 {
|
|
plaintext = buf[:nn]
|
|
mode = ObfsModeSalamander
|
|
}
|
|
}
|
|
|
|
// If no method worked, drop the packet.
|
|
if plaintext == nil {
|
|
m.readMu.Unlock()
|
|
continue
|
|
}
|
|
|
|
// Cache the method for this source address
|
|
if addr != nil {
|
|
m.mu.Lock()
|
|
m.addrModes[addr.String()] = mode
|
|
m.mu.Unlock()
|
|
}
|
|
|
|
nn := copy(p, plaintext)
|
|
m.readMu.Unlock()
|
|
return nn, addr, nil
|
|
}
|
|
}
|
|
|
|
// --- Write path: use cached method, default to Salamander ---
|
|
|
|
func (m *multiObfsPacketConn) WriteTo(p []byte, addr net.Addr) (int, error) {
|
|
mode := m.lookupMode(addr)
|
|
|
|
m.writeMu.Lock()
|
|
defer m.writeMu.Unlock()
|
|
|
|
var wire []byte
|
|
switch mode {
|
|
case ObfsModeTLSMimic:
|
|
if m.tm == nil {
|
|
return 0, net.ErrClosed
|
|
}
|
|
nn := m.tm.Obfuscate(p, m.writeBuf)
|
|
wire = m.writeBuf[:nn]
|
|
case ObfsModeSalamander:
|
|
if m.sm == nil {
|
|
return 0, net.ErrClosed
|
|
}
|
|
nn := m.sm.Obfuscate(p, m.writeBuf)
|
|
wire = m.writeBuf[:nn]
|
|
case ObfsModePlain:
|
|
wire = p
|
|
default:
|
|
wire = p
|
|
if m.sm != nil {
|
|
nn := m.sm.Obfuscate(p, m.writeBuf)
|
|
wire = m.writeBuf[:nn]
|
|
}
|
|
}
|
|
|
|
if m.pad {
|
|
wire = m.maybePad(wire)
|
|
}
|
|
|
|
if m.sendJitter > 0 {
|
|
time.Sleep(Jitter(m.sendJitter))
|
|
}
|
|
|
|
_, err := m.inner.WriteTo(wire, addr)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return len(p), nil
|
|
}
|
|
|
|
func (m *multiObfsPacketConn) lookupMode(addr net.Addr) ObfsMode {
|
|
if addr == nil {
|
|
return ObfsModeSalamander
|
|
}
|
|
m.mu.RLock()
|
|
mode, ok := m.addrModes[addr.String()]
|
|
m.mu.RUnlock()
|
|
if ok {
|
|
return mode
|
|
}
|
|
// Unknown address. For servers, this shouldn't happen in practice
|
|
// because the server receives a packet before sending one.
|
|
return ObfsModeSalamander
|
|
}
|
|
|
|
// maybePad appends random padding so the total packet length falls in
|
|
// [minP, maxP]. Does nothing if padding is disabled or the packet already
|
|
// exceeds maxP.
|
|
func (m *multiObfsPacketConn) maybePad(wire []byte) []byte {
|
|
base := len(wire)
|
|
if base >= m.maxP {
|
|
return wire
|
|
}
|
|
// pick a target between [max(base, minP), maxP]
|
|
target := base
|
|
if m.maxP > base {
|
|
// random in [max(base, minP), maxP]
|
|
lo := base
|
|
if m.minP > lo {
|
|
lo = m.minP
|
|
}
|
|
if lo <= m.maxP {
|
|
target = lo + randIntn(m.maxP-lo+1)
|
|
}
|
|
}
|
|
if target <= base {
|
|
return wire
|
|
}
|
|
out := make([]byte, target)
|
|
copy(out, wire)
|
|
_, _ = rand.Read(out[base:target])
|
|
return out
|
|
}
|
|
|
|
// --- net.PacketConn boilerplate ---
|
|
|
|
func (m *multiObfsPacketConn) Close() error {
|
|
m.closeOnce.Do(func() { close(m.closeCh) })
|
|
return m.inner.Close()
|
|
}
|
|
|
|
func (m *multiObfsPacketConn) LocalAddr() net.Addr { return m.inner.LocalAddr() }
|
|
|
|
func (m *multiObfsPacketConn) SetDeadline(t time.Time) error { return m.inner.SetDeadline(t) }
|
|
func (m *multiObfsPacketConn) SetReadDeadline(t time.Time) error { return m.inner.SetReadDeadline(t) }
|
|
func (m *multiObfsPacketConn) SetWriteDeadline(t time.Time) error {
|
|
return m.inner.SetWriteDeadline(t)
|
|
}
|
|
|
|
func (m *multiObfsPacketConn) SyscallConn() (syscall.RawConn, error) {
|
|
if u, ok := m.inner.(udpLikePacketConn); ok {
|
|
return u.SyscallConn()
|
|
}
|
|
return nil, syscall.EOPNOTSUPP
|
|
}
|
|
|
|
func (m *multiObfsPacketConn) SetReadBuffer(bytes int) error {
|
|
if u, ok := m.inner.(udpLikePacketConn); ok {
|
|
return u.SetReadBuffer(bytes)
|
|
}
|
|
return syscall.EOPNOTSUPP
|
|
}
|
|
|
|
func (m *multiObfsPacketConn) SetWriteBuffer(bytes int) error {
|
|
if u, ok := m.inner.(udpLikePacketConn); ok {
|
|
return u.SetWriteBuffer(bytes)
|
|
}
|
|
return syscall.EOPNOTSUPP
|
|
}
|
|
|
|
// Jitter returns a random duration in [-maxJitter, +maxJitter].
|
|
// If maxJitter is zero, returns 0 immediately.
|
|
func Jitter(maxJitter time.Duration) time.Duration {
|
|
if maxJitter <= 0 {
|
|
return 0
|
|
}
|
|
var b [8]byte
|
|
_, _ = rand.Read(b[:])
|
|
f := float64(binary.BigEndian.Uint64(b[:])>>11) / (1 << 53)
|
|
j := time.Duration(f * float64(maxJitter*2))
|
|
return j - maxJitter
|
|
}
|