hysteria/extras/obfs/multi.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

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
}