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
This commit is contained in:
parent
da8366eb8a
commit
1e77bbe7b7
9 changed files with 924 additions and 72 deletions
|
|
@ -1,10 +1,10 @@
|
|||
# Новые возможности (относительно upstream)
|
||||
# New Features (vs upstream)
|
||||
|
||||
## 1. L3 VPN туннель (Network)
|
||||
## 1. L3 VPN Tunnel (Network)
|
||||
|
||||
Полноценный IP-туннель поверх QUIC. На сервере создаётся TUN-интерфейс, клиенты получают IP из пула, весь L3-трафик маршрутизируется через зашифрованное QUIC-соединение.
|
||||
Full IP tunnel over QUIC. A TUN interface is created on the server, clients get an IP from a pool, and all L3 traffic is routed through the encrypted QUIC connection.
|
||||
|
||||
### Конфиг сервера (config.json)
|
||||
### Server config (config.json)
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
@ -21,15 +21,15 @@
|
|||
}
|
||||
```
|
||||
|
||||
Параметры:
|
||||
- `enabled` — включить туннель
|
||||
- `listen` — адрес для QUIC-слушателя (по умолч. `:5199`)
|
||||
- `token` — общий секрет для аутентификации клиентов
|
||||
- `tun.name` — имя TUN-интерфейса на сервере
|
||||
- `tun.mtu` — MTU TUN-интерфейса
|
||||
- `pool` — CIDR подсеть для выдачи IP клиентам
|
||||
Parameters:
|
||||
- `enabled` — enable the tunnel
|
||||
- `listen` — QUIC listener address (default `:5199`)
|
||||
- `token` — shared secret for client authentication
|
||||
- `tun.name` — TUN interface name on the server
|
||||
- `tun.mtu` — TUN interface MTU
|
||||
- `pool` — CIDR subnet for assigning IPs to clients
|
||||
|
||||
### Конфиг клиента (client.json)
|
||||
### Client config (client.json)
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
@ -44,19 +44,19 @@
|
|||
}
|
||||
```
|
||||
|
||||
Параметры:
|
||||
- `server` — адрес сервера (обязательно)
|
||||
- `token` — общий секрет (должен совпадать с серверным)
|
||||
- `tun.name` — имя TUN-интерфейса на клиенте
|
||||
- `tun.mtu` — MTU TUN-интерфейса
|
||||
Parameters:
|
||||
- `server` — server address (required)
|
||||
- `token` — shared secret (must match the server)
|
||||
- `tun.name` — TUN interface name on the client
|
||||
- `tun.mtu` — TUN interface MTU
|
||||
|
||||
---
|
||||
|
||||
## 2. FileMask шум (Noise)
|
||||
## 2. FileMask Noise
|
||||
|
||||
Новый слой обфускации, который маскирует трафик под скачивание зашифрованных файлов. Работает поверх существующей обфускации (Salamander/Gecko). Сервер шлёт клиенту случайные куски реальных файлов с диска, имитируя активную загрузку.
|
||||
New obfuscation layer that masks traffic as downloading encrypted files. It works on top of the existing obfuscation (Salamander/Gecko). The server sends the client random chunks of real files from disk, simulating an active download.
|
||||
|
||||
### Конфиг сервера
|
||||
### Server config
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
@ -73,23 +73,23 @@
|
|||
}
|
||||
```
|
||||
|
||||
Параметры:
|
||||
- `type` — `"filemask"` для включения; `"none"` или пустое значение — отключено
|
||||
- `filemask.dir` — **обязательно**, директория с файлами, содержимым которых будет маскироваться трафик
|
||||
- `filemask.maxRate` — максимальная скорость шума (по умолч. `"512 kbps"`)
|
||||
- `filemask.minPacketSize` — минимальный размер пакета шума
|
||||
- `filemask.maxPacketSize` — максимальный размер пакета шума
|
||||
- `filemask.idleThreshold` — таймаут бездействия клиента, после которого начинает генерироваться шум
|
||||
Parameters:
|
||||
- `type` — `"filemask"` to enable; `"none"` or empty to disable
|
||||
- `filemask.dir` — **required**, directory containing files used to mask traffic
|
||||
- `filemask.maxRate` — max noise rate (default `"512 kbps"`)
|
||||
- `filemask.minPacketSize` — minimum noise packet size
|
||||
- `filemask.maxPacketSize` — maximum noise packet size
|
||||
- `filemask.idleThreshold` — client idle timeout before noise generation starts
|
||||
|
||||
Файлы из указанной директории шифруются AES-GCM на лету и отправляются клиенту.
|
||||
Files from the specified directory are encrypted with AES-GCM on the fly and sent to the client.
|
||||
|
||||
---
|
||||
|
||||
## 3. Hysteria Outbound
|
||||
|
||||
Возможность использовать другой Hysteria сервер как upstream/выходной узел (outbound). Позволяет строить цепочки: клиент -> сервер А -> сервер Б (через hysteria outbound).
|
||||
Ability to use another Hysteria server as an upstream/outbound node. Allows building chains: client -> server A -> server B (via hysteria outbound).
|
||||
|
||||
### Конфиг сервера
|
||||
### Server config
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
@ -141,15 +141,15 @@
|
|||
}
|
||||
```
|
||||
|
||||
Поддерживаемые параметры внутри `hysteria`:
|
||||
- `server` — адрес целевого Hysteria сервера (обязательно)
|
||||
- `auth` — пароль аутентификации (обязательно)
|
||||
- `tls` — TLS настройки (SNI, insecure, pinSHA256, CA)
|
||||
- `quic` — настройки QUIC (окна приёма, таймауты)
|
||||
- `bandwidth` — лимиты пропускной способности
|
||||
- `congestion` — алгоритм контроля перегрузки (bbr/cubic/brutal)
|
||||
- `obfs` — обфускация для соединения с upstream (salamander/gecko)
|
||||
- `transport` — транспорт (udp, с опциональным port hopping)
|
||||
- `fastOpen` — включить fast open
|
||||
Supported parameters inside `hysteria`:
|
||||
- `server` — target Hysteria server address (required)
|
||||
- `auth` — authentication password (required)
|
||||
- `tls` — TLS settings (SNI, insecure, pinSHA256, CA)
|
||||
- `quic` — QUIC settings (receive windows, timeouts)
|
||||
- `bandwidth` — bandwidth limits
|
||||
- `congestion` — congestion control algorithm (bbr/cubic/brutal)
|
||||
- `obfs` — obfuscation for the upstream connection (salamander/gecko)
|
||||
- `transport` — transport (udp, with optional port hopping)
|
||||
- `fastOpen` — enable fast open
|
||||
|
||||
Используется в разделе `outbounds` наравне с `direct`, `socks5`, `http`.
|
||||
Used in the `outbounds` section alongside `direct`, `socks5`, `http`.
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ type clientConfig struct {
|
|||
Bandwidth clientConfigBandwidth `mapstructure:"bandwidth"`
|
||||
FastOpen bool `mapstructure:"fastOpen"`
|
||||
Lazy bool `mapstructure:"lazy"`
|
||||
MaxSessionDuration time.Duration `mapstructure:"maxSessionDuration"`
|
||||
SOCKS5 *socks5Config `mapstructure:"socks5"`
|
||||
HTTP *httpConfig `mapstructure:"http"`
|
||||
TCPForwarding []tcpForwardingEntry `mapstructure:"tcpForwarding"`
|
||||
|
|
@ -136,10 +137,15 @@ type clientConfigObfsGecko struct {
|
|||
MaxPacketSize int `mapstructure:"maxPacketSize"`
|
||||
}
|
||||
|
||||
type clientConfigObfsTLSMimic struct {
|
||||
Password string `mapstructure:"password"`
|
||||
}
|
||||
|
||||
type clientConfigObfs struct {
|
||||
Type string `mapstructure:"type"`
|
||||
Salamander clientConfigObfsSalamander `mapstructure:"salamander"`
|
||||
Gecko clientConfigObfsGecko `mapstructure:"gecko"`
|
||||
TLSMimic clientConfigObfsTLSMimic `mapstructure:"tlsmimic"`
|
||||
}
|
||||
|
||||
type clientConfigTLS struct {
|
||||
|
|
@ -349,6 +355,12 @@ func (c *clientConfig) wrapObfs(conn net.PacketConn) (net.PacketConn, error) {
|
|||
return nil, configError{Field: "obfs.gecko", Err: err}
|
||||
}
|
||||
return wrapped, nil
|
||||
case "tlsmimic":
|
||||
wrapped, err := obfs.WrapPacketConnTLSMimic(conn, []byte(c.Obfs.TLSMimic.Password))
|
||||
if err != nil {
|
||||
return nil, configError{Field: "obfs.tlsmimic.password", Err: err}
|
||||
}
|
||||
return wrapped, nil
|
||||
default:
|
||||
return nil, configError{Field: "obfs.type", Err: errors.New("unsupported obfuscation type")}
|
||||
}
|
||||
|
|
@ -506,6 +518,9 @@ func (c *clientConfig) URI() string {
|
|||
case "gecko":
|
||||
q.Set("obfs", "gecko")
|
||||
q.Set("obfs-password", c.Obfs.Gecko.Password)
|
||||
case "tlsmimic":
|
||||
q.Set("obfs", "tlsmimic")
|
||||
q.Set("obfs-password", c.Obfs.TLSMimic.Password)
|
||||
}
|
||||
if c.TLS.SNI != "" {
|
||||
q.Set("sni", c.TLS.SNI)
|
||||
|
|
@ -565,6 +580,8 @@ func (c *clientConfig) parseURI() bool {
|
|||
c.Obfs.Salamander.Password = q.Get("obfs-password")
|
||||
case "gecko":
|
||||
c.Obfs.Gecko.Password = q.Get("obfs-password")
|
||||
case "tlsmimic":
|
||||
c.Obfs.TLSMimic.Password = q.Get("obfs-password")
|
||||
}
|
||||
}
|
||||
if sni := q.Get("sni"); sni != "" {
|
||||
|
|
@ -805,7 +822,7 @@ func runClient(v *viper.Viper) {
|
|||
logger.Fatal("failed to parse client config", zap.Error(err))
|
||||
}
|
||||
|
||||
c, err := client.NewReconnectableClient(
|
||||
c, err := client.NewReconnectableClientWithMaxSession(
|
||||
config.Config,
|
||||
func(c client.Client, info *client.HandshakeInfo, count int) {
|
||||
connectLog(info, count)
|
||||
|
|
@ -816,7 +833,7 @@ func runClient(v *viper.Viper) {
|
|||
if count == 1 && !disableUpdateCheck {
|
||||
go runCheckUpdateClient(c)
|
||||
}
|
||||
}, config.Lazy,
|
||||
}, config.Lazy, config.MaxSessionDuration,
|
||||
)
|
||||
if err != nil {
|
||||
logger.Fatal("failed to initialize client", zap.Error(err))
|
||||
|
|
|
|||
|
|
@ -109,10 +109,17 @@ type serverConfigObfsGecko struct {
|
|||
MaxPacketSize int `mapstructure:"maxPacketSize"`
|
||||
}
|
||||
|
||||
type serverConfigObfsTLSMimic struct {
|
||||
Password string `mapstructure:"password"`
|
||||
}
|
||||
|
||||
type serverConfigObfs struct {
|
||||
Type string `mapstructure:"type"`
|
||||
Salamander serverConfigObfsSalamander `mapstructure:"salamander"`
|
||||
Gecko serverConfigObfsGecko `mapstructure:"gecko"`
|
||||
TLSMimic serverConfigObfsTLSMimic `mapstructure:"tlsmimic"`
|
||||
MinPadding int `mapstructure:"minPadding"`
|
||||
MaxPadding int `mapstructure:"maxPadding"`
|
||||
}
|
||||
|
||||
type serverConfigNoiseFileMask struct {
|
||||
|
|
@ -471,6 +478,26 @@ func (c *serverConfig) wrapObfs(conn net.PacketConn) (net.PacketConn, error) {
|
|||
return nil, configError{Field: "obfs.gecko", Err: err}
|
||||
}
|
||||
packetConn = wrapped
|
||||
case "tlsmimic":
|
||||
wrapped, err := obfs.WrapPacketConnTLSMimic(packetConn, []byte(c.Obfs.TLSMimic.Password))
|
||||
if err != nil {
|
||||
return nil, configError{Field: "obfs.tlsmimic.password", Err: err}
|
||||
}
|
||||
packetConn = wrapped
|
||||
case "auto", "salamander+tlsmimic":
|
||||
psk := c.Obfs.Salamander.Password
|
||||
if psk == "" {
|
||||
psk = c.Obfs.TLSMimic.Password
|
||||
}
|
||||
wrapped, err := obfs.WrapPacketConnMulti(packetConn, obfs.MultiObfsOptions{
|
||||
Password: psk,
|
||||
MinPadding: c.Obfs.MinPadding,
|
||||
MaxPadding: c.Obfs.MaxPadding,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, configError{Field: "obfs.multi", Err: err}
|
||||
}
|
||||
packetConn = wrapped
|
||||
default:
|
||||
return nil, configError{Field: "obfs.type", Err: errors.New("unsupported obfuscation type")}
|
||||
}
|
||||
|
|
@ -1339,6 +1366,8 @@ func serverConfigOutboundHysteriaToOutbound(c serverConfigOutboundHysteria) (out
|
|||
obfsPassword = c.Obfs.Salamander.Password
|
||||
case "gecko":
|
||||
obfsPassword = c.Obfs.Gecko.Password
|
||||
case "tlsmimic":
|
||||
obfsPassword = c.Obfs.TLSMimic.Password
|
||||
}
|
||||
cfg := &outbounds.HysteriaOutboundConfig{
|
||||
ServerAddr: c.Server,
|
||||
|
|
|
|||
|
|
@ -3,19 +3,25 @@ package client
|
|||
import (
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
coreErrs "github.com/apernet/hysteria/core/v2/errors"
|
||||
)
|
||||
|
||||
// reconnectableClientImpl is a wrapper of Client, which can reconnect when the connection is closed,
|
||||
// except when the caller explicitly calls Close() to permanently close this client.
|
||||
// reconnectableClientImpl is a wrapper of Client, which can reconnect when
|
||||
// the connection is closed (except when the caller explicitly calls Close()),
|
||||
// or periodically when maxSessionDuration > 0 to avoid long-lived sessions.
|
||||
type reconnectableClientImpl struct {
|
||||
configFunc func() (*Config, error) // called before connecting
|
||||
connectedFunc func(Client, *HandshakeInfo, int) // called when successfully connected
|
||||
configFunc func() (*Config, error)
|
||||
connectedFunc func(Client, *HandshakeInfo, int)
|
||||
client Client
|
||||
count int
|
||||
maxSessionDuration time.Duration
|
||||
|
||||
m sync.Mutex
|
||||
closed bool // permanent close
|
||||
closed bool
|
||||
recreateTimer *time.Timer
|
||||
recreateStop chan struct{}
|
||||
}
|
||||
|
||||
// NewReconnectableClient creates a reconnectable client.
|
||||
|
|
@ -23,36 +29,100 @@ type reconnectableClientImpl struct {
|
|||
// We use a function for config mainly to delay config evaluation
|
||||
// (which involves DNS resolution) until the actual connection attempt.
|
||||
func NewReconnectableClient(configFunc func() (*Config, error), connectedFunc func(Client, *HandshakeInfo, int), lazy bool) (Client, error) {
|
||||
return NewReconnectableClientWithMaxSession(configFunc, connectedFunc, lazy, 0)
|
||||
}
|
||||
|
||||
// NewReconnectableClientWithMaxSession creates a reconnectable client that
|
||||
// periodically reconnects every maxSessionDuration to avoid long-lived
|
||||
// sessions being detected by DPI. Set to 0 to disable timed recreation.
|
||||
func NewReconnectableClientWithMaxSession(configFunc func() (*Config, error), connectedFunc func(Client, *HandshakeInfo, int), lazy bool, maxSessionDuration time.Duration) (Client, error) {
|
||||
rc := &reconnectableClientImpl{
|
||||
configFunc: configFunc,
|
||||
connectedFunc: connectedFunc,
|
||||
maxSessionDuration: maxSessionDuration,
|
||||
recreateStop: make(chan struct{}),
|
||||
}
|
||||
if !lazy {
|
||||
if err := rc.reconnect(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
go rc.recreateLoop()
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
func (rc *reconnectableClientImpl) reconnect() error {
|
||||
if rc.client != nil {
|
||||
_ = rc.client.Close()
|
||||
func (rc *reconnectableClientImpl) recreateLoop() {
|
||||
for {
|
||||
rc.m.Lock()
|
||||
if rc.closed {
|
||||
rc.m.Unlock()
|
||||
return
|
||||
}
|
||||
var info *HandshakeInfo
|
||||
timer := rc.recreateTimer
|
||||
rc.m.Unlock()
|
||||
if timer == nil {
|
||||
// No timer configured, sleep until close
|
||||
<-rc.recreateStop
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-timer.C:
|
||||
_ = rc.reconnect()
|
||||
case <-rc.recreateStop:
|
||||
timer.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (rc *reconnectableClientImpl) reconnect() error {
|
||||
rc.m.Lock()
|
||||
if rc.closed {
|
||||
rc.m.Unlock()
|
||||
return coreErrs.ClosedError{}
|
||||
}
|
||||
oldClient := rc.client
|
||||
rc.client = nil
|
||||
rc.m.Unlock()
|
||||
|
||||
if oldClient != nil {
|
||||
_ = oldClient.Close()
|
||||
}
|
||||
|
||||
config, err := rc.configFunc()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rc.client, info, err = NewClient(config)
|
||||
client, info, err := NewClient(config)
|
||||
if err != nil {
|
||||
return err
|
||||
} else {
|
||||
}
|
||||
|
||||
rc.m.Lock()
|
||||
if rc.closed {
|
||||
_ = client.Close()
|
||||
rc.m.Unlock()
|
||||
return coreErrs.ClosedError{}
|
||||
}
|
||||
rc.client = client
|
||||
rc.count++
|
||||
if rc.connectedFunc != nil {
|
||||
rc.connectedFunc(rc, info, rc.count)
|
||||
rc.resetRecreateTimerLocked()
|
||||
connectedFunc := rc.connectedFunc
|
||||
rc.m.Unlock()
|
||||
|
||||
if connectedFunc != nil {
|
||||
connectedFunc(rc, info, rc.count)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rc *reconnectableClientImpl) resetRecreateTimerLocked() {
|
||||
if rc.recreateTimer != nil {
|
||||
rc.recreateTimer.Stop()
|
||||
rc.recreateTimer = nil
|
||||
}
|
||||
if rc.maxSessionDuration > 0 {
|
||||
rc.recreateTimer = time.NewTimer(rc.maxSessionDuration)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -111,10 +181,16 @@ func (rc *reconnectableClientImpl) UDP() (HyUDPConn, error) {
|
|||
|
||||
func (rc *reconnectableClientImpl) Close() error {
|
||||
rc.m.Lock()
|
||||
defer rc.m.Unlock()
|
||||
rc.closed = true
|
||||
if rc.client != nil {
|
||||
return rc.client.Close()
|
||||
if rc.recreateTimer != nil {
|
||||
rc.recreateTimer.Stop()
|
||||
rc.recreateTimer = nil
|
||||
}
|
||||
close(rc.recreateStop)
|
||||
client := rc.client
|
||||
rc.m.Unlock()
|
||||
if client != nil {
|
||||
return client.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
303
extras/obfs/multi.go
Normal file
303
extras/obfs/multi.go
Normal file
|
|
@ -0,0 +1,303 @@
|
|||
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
|
||||
}
|
||||
226
extras/obfs/multi_test.go
Normal file
226
extras/obfs/multi_test.go
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
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)
|
||||
}
|
||||
118
extras/obfs/tlsmimic.go
Normal file
118
extras/obfs/tlsmimic.go
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
package obfs
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/blake2b"
|
||||
)
|
||||
|
||||
const (
|
||||
tmPSKMinLen = 4
|
||||
tmSaltLen = 8
|
||||
tmKeyLen = blake2b.Size256
|
||||
tmHeaderSize = 5 // TLS record header: content type(1) + version(2) + length(2)
|
||||
tmContentTypeData = 0x17
|
||||
tmVersionMajor = 0x03
|
||||
tmVersionMinor = 0x03 // TLS 1.2 wire version (used in TLS 1.3 too)
|
||||
)
|
||||
|
||||
var errTLSMimicPSKTooShort = fmt.Errorf("TLS mimic PSK must be at least %d bytes", tmPSKMinLen)
|
||||
|
||||
// tlsMimicObfuscator encrypts each packet with BLAKE2b-256(PSK || salt) XOR
|
||||
// and wraps the result in a TLS 1.3 Application Data record header.
|
||||
// Wire format: [TLS record header (5 bytes)][salt (8 bytes)][XOR'd payload]
|
||||
type tlsMimicObfuscator struct {
|
||||
PSK []byte
|
||||
RandSrc *rand.Rand
|
||||
|
||||
lk sync.Mutex
|
||||
keyInput []byte
|
||||
}
|
||||
|
||||
func newTLSMimicObfuscator(psk []byte) (*tlsMimicObfuscator, error) {
|
||||
if len(psk) < tmPSKMinLen {
|
||||
return nil, errTLSMimicPSKTooShort
|
||||
}
|
||||
pskCopy := append([]byte(nil), psk...)
|
||||
keyInput := make([]byte, len(pskCopy)+tmSaltLen)
|
||||
copy(keyInput, pskCopy)
|
||||
return &tlsMimicObfuscator{
|
||||
PSK: pskCopy,
|
||||
RandSrc: rand.New(rand.NewSource(time.Now().UnixNano())),
|
||||
keyInput: keyInput,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// WrapPacketConnTLSMimic wraps conn with TLS 1.3 mimicry obfuscation.
|
||||
// Every outbound packet is encrypted with BLAKE2b-256(PSK || random salt) XOR
|
||||
// and wrapped in a TLS record header, making the traffic appear as TLS.
|
||||
func WrapPacketConnTLSMimic(conn net.PacketConn, psk []byte) (net.PacketConn, error) {
|
||||
ob, err := newTLSMimicObfuscator(psk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return wrapPacketConn(conn, ob), nil
|
||||
}
|
||||
|
||||
func (o *tlsMimicObfuscator) Obfuscate(in, out []byte) int {
|
||||
payloadLen := tmSaltLen + len(in)
|
||||
outLen := tmHeaderSize + payloadLen
|
||||
if len(out) < outLen {
|
||||
return 0
|
||||
}
|
||||
// Generate random salt into position after TLS header
|
||||
o.lk.Lock()
|
||||
_, _ = o.RandSrc.Read(out[tmHeaderSize : tmHeaderSize+tmSaltLen])
|
||||
key := o.keyLocked(out[tmHeaderSize : tmHeaderSize+tmSaltLen])
|
||||
o.lk.Unlock()
|
||||
// XOR encrypt payload
|
||||
for i, c := range in {
|
||||
out[tmHeaderSize+tmSaltLen+i] = c ^ key[i%tmKeyLen]
|
||||
}
|
||||
// TLS record header
|
||||
out[0] = tmContentTypeData
|
||||
out[1] = tmVersionMajor
|
||||
out[2] = tmVersionMinor
|
||||
binary.BigEndian.PutUint16(out[3:5], uint16(payloadLen))
|
||||
return outLen
|
||||
}
|
||||
|
||||
func (o *tlsMimicObfuscator) Deobfuscate(in, out []byte) int {
|
||||
if len(in) < tmHeaderSize+tmSaltLen {
|
||||
return 0
|
||||
}
|
||||
// Validate TLS record header
|
||||
if in[0] != tmContentTypeData {
|
||||
return 0
|
||||
}
|
||||
if in[1] != tmVersionMajor || in[2] != tmVersionMinor {
|
||||
return 0
|
||||
}
|
||||
payloadLen := int(binary.BigEndian.Uint16(in[3:5]))
|
||||
if payloadLen < tmSaltLen || tmHeaderSize+payloadLen > len(in) {
|
||||
return 0
|
||||
}
|
||||
salt := in[tmHeaderSize : tmHeaderSize+tmSaltLen]
|
||||
ciphertext := in[tmHeaderSize+tmSaltLen : tmHeaderSize+payloadLen]
|
||||
o.lk.Lock()
|
||||
key := o.keyLocked(salt)
|
||||
o.lk.Unlock()
|
||||
outLen := len(ciphertext)
|
||||
if len(out) < outLen {
|
||||
return 0
|
||||
}
|
||||
for i, c := range ciphertext {
|
||||
out[i] = c ^ key[i%tmKeyLen]
|
||||
}
|
||||
return outLen
|
||||
}
|
||||
|
||||
func (o *tlsMimicObfuscator) keyLocked(salt []byte) [tmKeyLen]byte {
|
||||
copy(o.keyInput[len(o.PSK):], salt[:tmSaltLen])
|
||||
return blake2b.Sum256(o.keyInput)
|
||||
}
|
||||
81
extras/obfs/tlsmimic_test.go
Normal file
81
extras/obfs/tlsmimic_test.go
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
package obfs
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestTLSMimicObfuscator(t *testing.T) {
|
||||
o, err := newTLSMimicObfuscator([]byte("average_password"))
|
||||
require.NoError(t, err)
|
||||
in := make([]byte, 1200)
|
||||
oOut := make([]byte, 2048)
|
||||
dOut := make([]byte, 2048)
|
||||
for i := 0; i < 1000; i++ {
|
||||
_, _ = rand.Read(in)
|
||||
n := o.Obfuscate(in, oOut)
|
||||
assert.Equal(t, tmHeaderSize+tmSaltLen+len(in), n)
|
||||
// Verify TLS header
|
||||
assert.Equal(t, byte(tmContentTypeData), oOut[0])
|
||||
assert.Equal(t, byte(tmVersionMajor), oOut[1])
|
||||
assert.Equal(t, byte(tmVersionMinor), oOut[2])
|
||||
// Decrypt
|
||||
n = o.Deobfuscate(oOut[:n], dOut)
|
||||
assert.Equal(t, len(in), n)
|
||||
assert.Equal(t, in, dOut[:n])
|
||||
}
|
||||
}
|
||||
|
||||
func TestTLSMimicPSKTooShort(t *testing.T) {
|
||||
_, err := newTLSMimicObfuscator([]byte("12"))
|
||||
assert.ErrorIs(t, err, errTLSMimicPSKTooShort)
|
||||
}
|
||||
|
||||
func TestTLSMimicRoundTripSmall(t *testing.T) {
|
||||
o, _ := newTLSMimicObfuscator([]byte("my_psk"))
|
||||
in := []byte("hello")
|
||||
oOut := make([]byte, 2048)
|
||||
dOut := make([]byte, 2048)
|
||||
n := o.Obfuscate(in, oOut)
|
||||
assert.Equal(t, tmHeaderSize+tmSaltLen+len(in), n)
|
||||
n = o.Deobfuscate(oOut[:n], dOut)
|
||||
assert.Equal(t, len(in), n)
|
||||
assert.Equal(t, in, dOut[:n])
|
||||
}
|
||||
|
||||
func TestTLSMimicRejectInvalidHeader(t *testing.T) {
|
||||
o, _ := newTLSMimicObfuscator([]byte("my_psk"))
|
||||
dOut := make([]byte, 2048)
|
||||
// Wrong content type
|
||||
buf := make([]byte, tmHeaderSize+tmSaltLen+10)
|
||||
n := o.Deobfuscate(buf, dOut)
|
||||
assert.Equal(t, 0, n)
|
||||
// Too short
|
||||
n = o.Deobfuscate(buf[:3], dOut)
|
||||
assert.Equal(t, 0, n)
|
||||
}
|
||||
|
||||
func BenchmarkTLSMimicObfuscator_Obfuscate(b *testing.B) {
|
||||
o, _ := newTLSMimicObfuscator([]byte("average_password"))
|
||||
in := make([]byte, 1200)
|
||||
_, _ = rand.Read(in)
|
||||
out := make([]byte, 2048)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
o.Obfuscate(in, out)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTLSMimicObfuscator_Deobfuscate(b *testing.B) {
|
||||
o, _ := newTLSMimicObfuscator([]byte("average_password"))
|
||||
in := make([]byte, 1200)
|
||||
_, _ = rand.Read(in)
|
||||
out := make([]byte, 2048)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
o.Deobfuscate(in, out)
|
||||
}
|
||||
}
|
||||
|
|
@ -305,6 +305,8 @@ func wrapObfs(conn net.PacketConn, obfsType, password string, minPacketSize, max
|
|||
MinPacketSize: minPacketSize,
|
||||
MaxPacketSize: maxPacketSize,
|
||||
})
|
||||
case "tlsmimic":
|
||||
return obfs.WrapPacketConnTLSMimic(conn, []byte(password))
|
||||
default:
|
||||
return nil, errors.New("unsupported obfuscation type: " + obfsType)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue