feat: gecko obfs (experimental) (#1584)
This commit is contained in:
parent
3b64f66995
commit
c3a806b5cb
11 changed files with 1376 additions and 92 deletions
|
|
@ -113,9 +113,16 @@ type clientConfigObfsSalamander struct {
|
|||
Password string `mapstructure:"password"`
|
||||
}
|
||||
|
||||
type clientConfigObfsGecko struct {
|
||||
Password string `mapstructure:"password"`
|
||||
MinPacketSize int `mapstructure:"minPacketSize"`
|
||||
MaxPacketSize int `mapstructure:"maxPacketSize"`
|
||||
}
|
||||
|
||||
type clientConfigObfs struct {
|
||||
Type string `mapstructure:"type"`
|
||||
Salamander clientConfigObfsSalamander `mapstructure:"salamander"`
|
||||
Gecko clientConfigObfsGecko `mapstructure:"gecko"`
|
||||
}
|
||||
|
||||
type clientConfigTLS struct {
|
||||
|
|
@ -233,42 +240,46 @@ func (c *clientConfig) fillServerAddr(hyConfig *client.Config) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// fillConnFactory must be called after fillServerAddr, as we have different logic
|
||||
// for ConnFactory depending on whether we have a port hopping address.
|
||||
// fillConnFactory must be called after fillServerAddr, since the right kind
|
||||
// of inner conn (plain vs port-hopping) depends on the resolved server addr.
|
||||
func (c *clientConfig) fillConnFactory(hyConfig *client.Config) error {
|
||||
so, err := c.socketOptions()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Inner PacketConn
|
||||
var newFunc func(addr net.Addr) (net.PacketConn, error)
|
||||
hopInterval, err := c.Transport.UDP.hopIntervalConfig()
|
||||
if err != nil {
|
||||
return configError{Field: "transport.udp", Err: err}
|
||||
}
|
||||
var openInner func() (net.PacketConn, error)
|
||||
switch strings.ToLower(c.Transport.Type) {
|
||||
case "", "udp":
|
||||
if hyConfig.ServerAddr.Network() == "udphop" {
|
||||
hopAddr := hyConfig.ServerAddr.(*udphop.UDPHopAddr)
|
||||
newFunc = func(addr net.Addr) (net.PacketConn, error) {
|
||||
openInner = func() (net.PacketConn, error) {
|
||||
return udphop.NewUDPHopPacketConn(hopAddr, hopInterval, so.ListenUDP)
|
||||
}
|
||||
} else {
|
||||
newFunc = func(addr net.Addr) (net.PacketConn, error) {
|
||||
openInner = func() (net.PacketConn, error) {
|
||||
return so.ListenUDP()
|
||||
}
|
||||
}
|
||||
default:
|
||||
return configError{Field: "transport.type", Err: errors.New("unsupported transport type")}
|
||||
}
|
||||
// Obfuscation
|
||||
ob, err := c.obfuscator()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hyConfig.ConnFactory = &adaptiveConnFactory{
|
||||
NewFunc: newFunc,
|
||||
Obfuscator: ob,
|
||||
hyConfig.ConnFactory = &singleUseConnFactory{
|
||||
Open: func() (net.PacketConn, error) {
|
||||
conn, err := openInner()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wrapped, err := c.wrapObfs(conn)
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return wrapped, nil
|
||||
},
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -292,16 +303,26 @@ func (c *clientConfig) socketOptions() (*sockopts.SocketOptions, error) {
|
|||
return so, nil
|
||||
}
|
||||
|
||||
func (c *clientConfig) obfuscator() (obfs.Obfuscator, error) {
|
||||
func (c *clientConfig) wrapObfs(conn net.PacketConn) (net.PacketConn, error) {
|
||||
switch strings.ToLower(c.Obfs.Type) {
|
||||
case "", "plain":
|
||||
return nil, nil
|
||||
return conn, nil
|
||||
case "salamander":
|
||||
ob, err := obfs.NewSalamanderObfuscator([]byte(c.Obfs.Salamander.Password))
|
||||
wrapped, err := obfs.WrapPacketConnSalamander(conn, []byte(c.Obfs.Salamander.Password))
|
||||
if err != nil {
|
||||
return nil, configError{Field: "obfs.salamander.password", Err: err}
|
||||
}
|
||||
return ob, nil
|
||||
return wrapped, nil
|
||||
case "gecko":
|
||||
wrapped, err := obfs.WrapPacketConnGecko(conn, obfs.GeckoOptions{
|
||||
Password: []byte(c.Obfs.Gecko.Password),
|
||||
MinPacketSize: c.Obfs.Gecko.MinPacketSize,
|
||||
MaxPacketSize: c.Obfs.Gecko.MaxPacketSize,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, configError{Field: "obfs.gecko", Err: err}
|
||||
}
|
||||
return wrapped, nil
|
||||
default:
|
||||
return nil, configError{Field: "obfs.type", Err: errors.New("unsupported obfuscation type")}
|
||||
}
|
||||
|
|
@ -456,6 +477,9 @@ func (c *clientConfig) URI() string {
|
|||
case "salamander":
|
||||
q.Set("obfs", "salamander")
|
||||
q.Set("obfs-password", c.Obfs.Salamander.Password)
|
||||
case "gecko":
|
||||
q.Set("obfs", "gecko")
|
||||
q.Set("obfs-password", c.Obfs.Gecko.Password)
|
||||
}
|
||||
if c.TLS.SNI != "" {
|
||||
q.Set("sni", c.TLS.SNI)
|
||||
|
|
@ -513,6 +537,8 @@ func (c *clientConfig) parseURI() bool {
|
|||
switch strings.ToLower(obfsType) {
|
||||
case "salamander":
|
||||
c.Obfs.Salamander.Password = q.Get("obfs-password")
|
||||
case "gecko":
|
||||
c.Obfs.Gecko.Password = q.Get("obfs-password")
|
||||
}
|
||||
}
|
||||
if sni := q.Get("sni"); sni != "" {
|
||||
|
|
@ -680,15 +706,13 @@ func (c *clientConfig) realmConfig(addr *realm.Addr) (*client.Config, error) {
|
|||
zap.String("realm", addr.RealmID),
|
||||
zap.String("peer", result.PeerAddr.String()))
|
||||
|
||||
var finalConn net.PacketConn = baseConn
|
||||
ob, err := c.obfuscator()
|
||||
finalConn, err := c.wrapObfs(baseConn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ob != nil {
|
||||
finalConn = obfs.WrapPacketConn(baseConn, ob)
|
||||
hyConfig.ConnFactory = &singleUseConnFactory{
|
||||
Open: func() (net.PacketConn, error) { return finalConn, nil },
|
||||
}
|
||||
hyConfig.ConnFactory = &singleUseConnFactory{Conn: finalConn}
|
||||
success = true
|
||||
return hyConfig, nil
|
||||
}
|
||||
|
|
@ -1135,25 +1159,14 @@ func normalizeCertHash(hash string) string {
|
|||
return r
|
||||
}
|
||||
|
||||
type adaptiveConnFactory struct {
|
||||
NewFunc func(addr net.Addr) (net.PacketConn, error)
|
||||
Obfuscator obfs.Obfuscator // nil if no obfuscation
|
||||
}
|
||||
|
||||
func (f *adaptiveConnFactory) New(addr net.Addr) (net.PacketConn, error) {
|
||||
if f.Obfuscator == nil {
|
||||
return f.NewFunc(addr)
|
||||
} else {
|
||||
conn, err := f.NewFunc(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return obfs.WrapPacketConn(conn, f.Obfuscator), nil
|
||||
}
|
||||
}
|
||||
|
||||
// singleUseConnFactory invokes Open exactly once to produce the underlying
|
||||
// conn. It defers any allocation to the moment core/client.connect() actually
|
||||
// asks for the conn, so config-validation failures inside NewClient (e.g.
|
||||
// verifyAndFill rejecting a value) don't leak a socket. For the realm path
|
||||
// the conn is already open by the time we reach the factory, so Open just
|
||||
// returns it; for the regular path Open opens the UDP socket fresh.
|
||||
type singleUseConnFactory struct {
|
||||
Conn net.PacketConn
|
||||
Open func() (net.PacketConn, error)
|
||||
|
||||
mu sync.Mutex
|
||||
used bool
|
||||
|
|
@ -1162,11 +1175,11 @@ type singleUseConnFactory struct {
|
|||
func (f *singleUseConnFactory) New(net.Addr) (net.PacketConn, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
if f.used || f.Conn == nil {
|
||||
return nil, errors.New("realm connection already used")
|
||||
if f.used {
|
||||
return nil, errors.New("connection factory already used")
|
||||
}
|
||||
f.used = true
|
||||
return f.Conn, nil
|
||||
return f.Open()
|
||||
}
|
||||
|
||||
func addrPortStrings(addrs []netip.AddrPort) []string {
|
||||
|
|
|
|||
|
|
@ -177,6 +177,20 @@ func TestClientConfigURI(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
uri: "hysteria2://pw@geckotown.com:8443/?obfs=gecko&obfs-password=hidden",
|
||||
uriOK: true,
|
||||
config: &clientConfig{
|
||||
Server: "geckotown.com:8443",
|
||||
Auth: "pw",
|
||||
Obfs: clientConfigObfs{
|
||||
Type: "gecko",
|
||||
Gecko: clientConfigObfsGecko{
|
||||
Password: "hidden",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
uri: "invalid.bs",
|
||||
uriOK: false,
|
||||
|
|
@ -238,7 +252,7 @@ func TestSingleUseConnFactory(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
defer conn.Close()
|
||||
|
||||
f := &singleUseConnFactory{Conn: conn}
|
||||
f := &singleUseConnFactory{Open: func() (net.PacketConn, error) { return conn, nil }}
|
||||
got, err := f.New(&net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 443})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, conn, got)
|
||||
|
|
|
|||
|
|
@ -97,9 +97,16 @@ type serverConfigObfsSalamander struct {
|
|||
Password string `mapstructure:"password"`
|
||||
}
|
||||
|
||||
type serverConfigObfsGecko struct {
|
||||
Password string `mapstructure:"password"`
|
||||
MinPacketSize int `mapstructure:"minPacketSize"`
|
||||
MaxPacketSize int `mapstructure:"maxPacketSize"`
|
||||
}
|
||||
|
||||
type serverConfigObfs struct {
|
||||
Type string `mapstructure:"type"`
|
||||
Salamander serverConfigObfsSalamander `mapstructure:"salamander"`
|
||||
Gecko serverConfigObfsGecko `mapstructure:"gecko"`
|
||||
}
|
||||
|
||||
type serverConfigTLS struct {
|
||||
|
|
@ -312,30 +319,17 @@ func (c *serverConfig) fillConn(hyConfig *server.Config) error {
|
|||
return configError{Field: "listen", Err: err}
|
||||
}
|
||||
}
|
||||
switch strings.ToLower(c.Obfs.Type) {
|
||||
case "", "plain":
|
||||
hyConfig.Conn = packetConn
|
||||
hyConfig.Cleanup = cleanup
|
||||
return nil
|
||||
case "salamander":
|
||||
ob, err := obfs.NewSalamanderObfuscator([]byte(c.Obfs.Salamander.Password))
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
if cleanup != nil {
|
||||
_ = cleanup.Close()
|
||||
}
|
||||
return configError{Field: "obfs.salamander.password", Err: err}
|
||||
}
|
||||
hyConfig.Conn = obfs.WrapPacketConn(packetConn, ob)
|
||||
hyConfig.Cleanup = cleanup
|
||||
return nil
|
||||
default:
|
||||
wrapped, err := c.wrapObfs(packetConn)
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
if cleanup != nil {
|
||||
_ = cleanup.Close()
|
||||
}
|
||||
return configError{Field: "obfs.type", Err: errors.New("unsupported obfuscation type")}
|
||||
return err
|
||||
}
|
||||
hyConfig.Conn = wrapped
|
||||
hyConfig.Cleanup = cleanup
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseServerRealmAddr(listen string) (*realm.Addr, bool, error) {
|
||||
|
|
@ -371,8 +365,9 @@ func (c *serverConfig) fillRealmConn(hyConfig *server.Config, addr *realm.Addr)
|
|||
_ = conn.Close()
|
||||
return configError{Field: "realm", Err: err}
|
||||
}
|
||||
packetConn, err := c.wrapServerPacketConn(punchConn, conn)
|
||||
packetConn, err := c.wrapObfs(punchConn)
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -388,19 +383,27 @@ func (c *serverConfig) fillRealmConn(hyConfig *server.Config, addr *realm.Addr)
|
|||
return nil
|
||||
}
|
||||
|
||||
func (c *serverConfig) wrapServerPacketConn(packetConn net.PacketConn, udpConn *net.UDPConn) (net.PacketConn, error) {
|
||||
func (c *serverConfig) wrapObfs(conn net.PacketConn) (net.PacketConn, error) {
|
||||
switch strings.ToLower(c.Obfs.Type) {
|
||||
case "", "plain":
|
||||
return packetConn, nil
|
||||
return conn, nil
|
||||
case "salamander":
|
||||
ob, err := obfs.NewSalamanderObfuscator([]byte(c.Obfs.Salamander.Password))
|
||||
wrapped, err := obfs.WrapPacketConnSalamander(conn, []byte(c.Obfs.Salamander.Password))
|
||||
if err != nil {
|
||||
_ = udpConn.Close()
|
||||
return nil, configError{Field: "obfs.salamander.password", Err: err}
|
||||
}
|
||||
return obfs.WrapPacketConn(packetConn, ob), nil
|
||||
return wrapped, nil
|
||||
case "gecko":
|
||||
wrapped, err := obfs.WrapPacketConnGecko(conn, obfs.GeckoOptions{
|
||||
Password: []byte(c.Obfs.Gecko.Password),
|
||||
MinPacketSize: c.Obfs.Gecko.MinPacketSize,
|
||||
MaxPacketSize: c.Obfs.Gecko.MaxPacketSize,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, configError{Field: "obfs.gecko", Err: err}
|
||||
}
|
||||
return wrapped, nil
|
||||
default:
|
||||
_ = udpConn.Close()
|
||||
return nil, configError{Field: "obfs.type", Err: errors.New("unsupported obfuscation type")}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,10 +9,10 @@ import (
|
|||
|
||||
const udpBufferSize = 2048 // QUIC packets are at most 1500 bytes long, so 2k should be more than enough
|
||||
|
||||
// Obfuscator is the interface that wraps the Obfuscate and Deobfuscate methods.
|
||||
// Both methods return the number of bytes written to out.
|
||||
// obfuscator wraps a per-packet, length-preserving cipher.
|
||||
// Obfuscate / Deobfuscate return the number of bytes written to out.
|
||||
// If a packet is not valid, the methods should return 0.
|
||||
type Obfuscator interface {
|
||||
type obfuscator interface {
|
||||
Obfuscate(in, out []byte) int
|
||||
Deobfuscate(in, out []byte) int
|
||||
}
|
||||
|
|
@ -21,7 +21,7 @@ var _ net.PacketConn = (*obfsPacketConn)(nil)
|
|||
|
||||
type obfsPacketConn struct {
|
||||
Conn net.PacketConn
|
||||
Obfs Obfuscator
|
||||
Obfs obfuscator
|
||||
|
||||
readBuf []byte
|
||||
readMutex sync.Mutex
|
||||
|
|
@ -49,14 +49,14 @@ type obfsPacketConnUDP struct {
|
|||
UDPConn udpLikePacketConn
|
||||
}
|
||||
|
||||
// WrapPacketConn enables obfuscation on a net.PacketConn.
|
||||
// wrapPacketConn enables per-packet obfuscation on a net.PacketConn.
|
||||
// The obfuscation is transparent to the caller - the n bytes returned by
|
||||
// ReadFrom and WriteTo are the number of original bytes, not after
|
||||
// obfuscation/deobfuscation.
|
||||
func WrapPacketConn(conn net.PacketConn, obfs Obfuscator) net.PacketConn {
|
||||
func wrapPacketConn(conn net.PacketConn, ob obfuscator) net.PacketConn {
|
||||
opc := &obfsPacketConn{
|
||||
Conn: conn,
|
||||
Obfs: obfs,
|
||||
Obfs: ob,
|
||||
readBuf: make([]byte, udpBufferSize),
|
||||
writeBuf: make([]byte, udpBufferSize),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,19 +46,19 @@ func TestWrapPacketConnUsesUDPVariantForUDPConn(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
defer udp.Close()
|
||||
|
||||
wrapped := WrapPacketConn(udp, noopObfs{})
|
||||
wrapped := wrapPacketConn(udp, noopObfs{})
|
||||
_, ok := wrapped.(*obfsPacketConnUDP)
|
||||
assert.True(t, ok, "wrapping a *net.UDPConn should return *obfsPacketConnUDP")
|
||||
}
|
||||
|
||||
func TestWrapPacketConnUsesUDPVariantForUDPLikeWrapper(t *testing.T) {
|
||||
wrapped := WrapPacketConn(fakeUDPLikeConn{}, noopObfs{})
|
||||
wrapped := wrapPacketConn(fakeUDPLikeConn{}, noopObfs{})
|
||||
_, ok := wrapped.(*obfsPacketConnUDP)
|
||||
assert.True(t, ok, "wrapping a udpLikePacketConn should return *obfsPacketConnUDP")
|
||||
}
|
||||
|
||||
func TestWrapPacketConnFallsBackForPlainPacketConn(t *testing.T) {
|
||||
wrapped := WrapPacketConn(fakePlainConn{}, noopObfs{})
|
||||
wrapped := wrapPacketConn(fakePlainConn{}, noopObfs{})
|
||||
_, isUDP := wrapped.(*obfsPacketConnUDP)
|
||||
assert.False(t, isUDP, "wrapping a plain net.PacketConn should not return *obfsPacketConnUDP")
|
||||
}
|
||||
|
|
|
|||
341
extras/obfs/gecko.go
Normal file
341
extras/obfs/gecko.go
Normal file
|
|
@ -0,0 +1,341 @@
|
|||
package obfs
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Gecko adds shape obfuscation on top of Salamander: QUIC long-header
|
||||
// (handshake) packets are fragmented into randomly-sized, randomly-padded
|
||||
// chunks; short-header packets pass through untouched.
|
||||
const (
|
||||
geckoReassemblyTTL = 8 * time.Second
|
||||
geckoMaxReassembly = 4096
|
||||
geckoMaxPerSource = 8
|
||||
|
||||
geckoBufferSize = 2048
|
||||
|
||||
geckoDefaultMinPacket = 512
|
||||
geckoDefaultMaxPacket = 1200
|
||||
)
|
||||
|
||||
type GeckoOptions struct {
|
||||
Password []byte
|
||||
MinPacketSize int
|
||||
MaxPacketSize int
|
||||
}
|
||||
|
||||
func WrapPacketConnGecko(conn net.PacketConn, opts GeckoOptions) (net.PacketConn, error) {
|
||||
if len(opts.Password) == 0 {
|
||||
return nil, errors.New("gecko: password is required")
|
||||
}
|
||||
minPkt, maxPkt := opts.MinPacketSize, opts.MaxPacketSize
|
||||
if minPkt == 0 {
|
||||
minPkt = geckoDefaultMinPacket
|
||||
}
|
||||
if maxPkt == 0 {
|
||||
maxPkt = geckoDefaultMaxPacket
|
||||
}
|
||||
if minPkt <= 0 || minPkt > maxPkt || maxPkt > geckoBufferSize {
|
||||
return nil, errors.New("gecko: invalid min/max packet size")
|
||||
}
|
||||
inner, err := WrapPacketConnSalamander(conn, opts.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newGeckoPacketConn(inner, minPkt, maxPkt), nil
|
||||
}
|
||||
|
||||
type reassemblyKey struct {
|
||||
addr string
|
||||
msgID uint8
|
||||
}
|
||||
|
||||
type reassemblyEntry struct {
|
||||
chunks [][]byte
|
||||
received int
|
||||
total uint8
|
||||
deadline time.Time
|
||||
}
|
||||
|
||||
type geckoPacketConn struct {
|
||||
inner net.PacketConn
|
||||
minPkt, maxPkt int
|
||||
|
||||
msgID atomic.Uint32
|
||||
|
||||
readMu sync.Mutex
|
||||
readBuf []byte
|
||||
|
||||
mu sync.Mutex
|
||||
reassembly map[reassemblyKey]*reassemblyEntry
|
||||
perSource map[string]int
|
||||
|
||||
closeCh chan struct{}
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
func newGeckoPacketConn(inner net.PacketConn, minPkt, maxPkt int) *geckoPacketConn {
|
||||
g := &geckoPacketConn{
|
||||
inner: inner,
|
||||
minPkt: minPkt,
|
||||
maxPkt: maxPkt,
|
||||
readBuf: make([]byte, geckoBufferSize),
|
||||
reassembly: make(map[reassemblyKey]*reassemblyEntry),
|
||||
perSource: make(map[string]int),
|
||||
closeCh: make(chan struct{}),
|
||||
}
|
||||
go g.gcLoop()
|
||||
return g
|
||||
}
|
||||
|
||||
// --- Send path ---
|
||||
|
||||
func (g *geckoPacketConn) WriteTo(p []byte, addr net.Addr) (int, error) {
|
||||
if len(p) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
if p[0]&0x80 != 0 {
|
||||
// QUIC long header, do fragmentation.
|
||||
return g.writeFragmented(p, addr)
|
||||
}
|
||||
// QUIC short header (data), pass through.
|
||||
return g.inner.WriteTo(p, addr)
|
||||
}
|
||||
|
||||
func (g *geckoPacketConn) writeFragmented(p []byte, addr net.Addr) (int, error) {
|
||||
chunks := randomFragmentChunks()
|
||||
chunkSize := len(p) / chunks
|
||||
msgID := uint8(g.msgID.Add(1))
|
||||
for i := range chunks {
|
||||
start := i * chunkSize
|
||||
end := len(p)
|
||||
if i < chunks-1 {
|
||||
end = start + chunkSize
|
||||
}
|
||||
chunk := p[start:end]
|
||||
padLen := g.randomPadLen(len(chunk))
|
||||
buf := make([]byte, geckoHeaderSize+int(padLen)+len(chunk))
|
||||
n, err := encodeFrame(frameHeader{
|
||||
padLen: padLen,
|
||||
msgID: msgID,
|
||||
chunkIdx: uint8(i),
|
||||
totalChunks: uint8(chunks),
|
||||
}, chunk, buf)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if _, err := g.inner.WriteTo(buf[:n], addr); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// randomPadLen picks padding so the final UDP datagram (Salamander salt +
|
||||
// header + padding + chunk) falls within [minPkt, maxPkt]. If the chunk alone
|
||||
// already exceeds maxPkt, no padding is added.
|
||||
func (g *geckoPacketConn) randomPadLen(chunkLen int) uint16 {
|
||||
base := smSaltLen + geckoHeaderSize + chunkLen
|
||||
lo := max(g.minPkt, base)
|
||||
if lo > g.maxPkt {
|
||||
return 0
|
||||
}
|
||||
return uint16(lo - base + randIntn(g.maxPkt-lo+1))
|
||||
}
|
||||
|
||||
func randomFragmentChunks() int {
|
||||
return geckoMinFragmentChunks + randIntn(geckoMaxFragmentChunks-geckoMinFragmentChunks+1)
|
||||
}
|
||||
|
||||
// randIntn returns a uniform random int in [0, n).
|
||||
func randIntn(n int) int {
|
||||
if n <= 1 {
|
||||
return 0
|
||||
}
|
||||
var b [4]byte
|
||||
_, _ = rand.Read(b[:])
|
||||
return int(binary.BigEndian.Uint32(b[:]) % uint32(n))
|
||||
}
|
||||
|
||||
// --- Receive path ---
|
||||
|
||||
func (g *geckoPacketConn) ReadFrom(p []byte) (int, net.Addr, error) {
|
||||
g.readMu.Lock()
|
||||
defer g.readMu.Unlock()
|
||||
buf := g.readBuf
|
||||
for {
|
||||
n, addr, err := g.inner.ReadFrom(buf)
|
||||
if err != nil {
|
||||
return 0, addr, err
|
||||
}
|
||||
if n <= 0 {
|
||||
continue
|
||||
}
|
||||
// Top bit set → Gecko fragment frame; clear → short-header packet
|
||||
// or garbage, passed through for QUIC to handle.
|
||||
if buf[0]&0x80 == 0 {
|
||||
return copy(p, buf[:n]), addr, nil
|
||||
}
|
||||
h, payload, decErr := decodeFrame(buf[:n])
|
||||
if decErr != nil {
|
||||
// Malformed frame; drop silently.
|
||||
continue
|
||||
}
|
||||
out, ready := g.acceptChunk(addr, h, payload)
|
||||
if !ready {
|
||||
continue
|
||||
}
|
||||
return copy(p, out), addr, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (g *geckoPacketConn) acceptChunk(addr net.Addr, h frameHeader, payload []byte) ([]byte, bool) {
|
||||
key := reassemblyKey{addr: addr.String(), msgID: h.msgID}
|
||||
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
|
||||
e, exists := g.reassembly[key]
|
||||
if !exists {
|
||||
// Per-source cap.
|
||||
if g.perSource[key.addr] >= geckoMaxPerSource {
|
||||
return nil, false
|
||||
}
|
||||
// Global cap with eviction.
|
||||
if len(g.reassembly) >= geckoMaxReassembly {
|
||||
g.evictOldestLocked()
|
||||
}
|
||||
e = &reassemblyEntry{
|
||||
chunks: make([][]byte, h.totalChunks),
|
||||
total: h.totalChunks,
|
||||
deadline: time.Now().Add(geckoReassemblyTTL),
|
||||
}
|
||||
g.reassembly[key] = e
|
||||
g.perSource[key.addr]++
|
||||
} else if e.total != h.totalChunks {
|
||||
// Inconsistent chunk count; drop.
|
||||
return nil, false
|
||||
}
|
||||
if int(h.chunkIdx) >= len(e.chunks) || e.chunks[h.chunkIdx] != nil {
|
||||
// Bad index or duplicate; drop.
|
||||
return nil, false
|
||||
}
|
||||
cp := make([]byte, len(payload))
|
||||
copy(cp, payload)
|
||||
e.chunks[h.chunkIdx] = cp
|
||||
e.received++
|
||||
if e.received < int(e.total) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
total := 0
|
||||
for _, c := range e.chunks {
|
||||
total += len(c)
|
||||
}
|
||||
out := make([]byte, total)
|
||||
off := 0
|
||||
for _, c := range e.chunks {
|
||||
off += copy(out[off:], c)
|
||||
}
|
||||
g.dropEntryLocked(key)
|
||||
return out, true
|
||||
}
|
||||
|
||||
// --- Maintenance ---
|
||||
|
||||
func (g *geckoPacketConn) gcLoop() {
|
||||
t := time.NewTicker(geckoReassemblyTTL / 2)
|
||||
defer t.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-g.closeCh:
|
||||
return
|
||||
case now := <-t.C:
|
||||
g.gcExpired(now)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (g *geckoPacketConn) gcExpired(now time.Time) {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
for k, e := range g.reassembly {
|
||||
if now.After(e.deadline) {
|
||||
g.dropEntryLocked(k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dropEntryLocked must be called with mu held.
|
||||
func (g *geckoPacketConn) dropEntryLocked(k reassemblyKey) {
|
||||
if _, ok := g.reassembly[k]; !ok {
|
||||
return
|
||||
}
|
||||
delete(g.reassembly, k)
|
||||
g.perSource[k.addr]--
|
||||
if g.perSource[k.addr] <= 0 {
|
||||
delete(g.perSource, k.addr)
|
||||
}
|
||||
}
|
||||
|
||||
// evictOldestLocked must be called with mu held. O(n) over the map; n is
|
||||
// bounded by geckoMaxReassembly.
|
||||
func (g *geckoPacketConn) evictOldestLocked() {
|
||||
var oldestKey reassemblyKey
|
||||
var oldestDeadline time.Time
|
||||
first := true
|
||||
for k, e := range g.reassembly {
|
||||
if first || e.deadline.Before(oldestDeadline) {
|
||||
oldestKey = k
|
||||
oldestDeadline = e.deadline
|
||||
first = false
|
||||
}
|
||||
}
|
||||
if !first {
|
||||
g.dropEntryLocked(oldestKey)
|
||||
}
|
||||
}
|
||||
|
||||
// --- net.PacketConn boilerplate ---
|
||||
|
||||
func (g *geckoPacketConn) Close() error {
|
||||
g.closeOnce.Do(func() { close(g.closeCh) })
|
||||
return g.inner.Close()
|
||||
}
|
||||
|
||||
func (g *geckoPacketConn) LocalAddr() net.Addr { return g.inner.LocalAddr() }
|
||||
func (g *geckoPacketConn) SetDeadline(t time.Time) error { return g.inner.SetDeadline(t) }
|
||||
func (g *geckoPacketConn) SetReadDeadline(t time.Time) error { return g.inner.SetReadDeadline(t) }
|
||||
func (g *geckoPacketConn) SetWriteDeadline(t time.Time) error {
|
||||
return g.inner.SetWriteDeadline(t)
|
||||
}
|
||||
|
||||
// --- UDP-flavor passthrough ---
|
||||
|
||||
func (g *geckoPacketConn) SyscallConn() (syscall.RawConn, error) {
|
||||
if u, ok := g.inner.(udpLikePacketConn); ok {
|
||||
return u.SyscallConn()
|
||||
}
|
||||
return nil, errors.ErrUnsupported
|
||||
}
|
||||
|
||||
func (g *geckoPacketConn) SetReadBuffer(bytes int) error {
|
||||
if u, ok := g.inner.(udpLikePacketConn); ok {
|
||||
return u.SetReadBuffer(bytes)
|
||||
}
|
||||
return errors.ErrUnsupported
|
||||
}
|
||||
|
||||
func (g *geckoPacketConn) SetWriteBuffer(bytes int) error {
|
||||
if u, ok := g.inner.(udpLikePacketConn); ok {
|
||||
return u.SetWriteBuffer(bytes)
|
||||
}
|
||||
return errors.ErrUnsupported
|
||||
}
|
||||
86
extras/obfs/gecko_frame.go
Normal file
86
extras/obfs/gecko_frame.go
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
package obfs
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
)
|
||||
|
||||
const (
|
||||
geckoFlagFragment = 0x80
|
||||
geckoHeaderSize = 5
|
||||
|
||||
geckoMinFragmentChunks = 2
|
||||
geckoMaxFragmentChunks = 8
|
||||
)
|
||||
|
||||
var (
|
||||
errFrameTruncated = errors.New("gecko frame truncated")
|
||||
errFrameInvalid = errors.New("gecko frame invalid")
|
||||
)
|
||||
|
||||
// frameHeader is a Gecko fragment frame header.
|
||||
// Wire layout (after Salamander decryption):
|
||||
//
|
||||
// byte 0: 0x80 (fragment marker; low 7 bits reserved)
|
||||
// byte 1: msgID
|
||||
// byte 2: chunkIdx:4 | totalChunks:4
|
||||
// byte 3-4: padLen (uint16, big-endian)
|
||||
// then padLen random padding bytes, then the chunk payload
|
||||
type frameHeader struct {
|
||||
padLen uint16
|
||||
msgID uint8
|
||||
chunkIdx uint8 // < totalChunks
|
||||
totalChunks uint8 // [2, 8]
|
||||
}
|
||||
|
||||
// encodeFrame writes a frame into out, filling the padding region with random
|
||||
// bytes. out must be at least geckoHeaderSize + h.padLen + len(payload) long.
|
||||
func encodeFrame(h frameHeader, payload, out []byte) (int, error) {
|
||||
if h.totalChunks < geckoMinFragmentChunks || h.totalChunks > geckoMaxFragmentChunks {
|
||||
return 0, errFrameInvalid
|
||||
}
|
||||
if h.chunkIdx >= h.totalChunks {
|
||||
return 0, errFrameInvalid
|
||||
}
|
||||
needed := geckoHeaderSize + int(h.padLen) + len(payload)
|
||||
if len(out) < needed {
|
||||
return 0, errFrameTruncated
|
||||
}
|
||||
out[0] = geckoFlagFragment
|
||||
out[1] = h.msgID
|
||||
out[2] = h.chunkIdx<<4 | h.totalChunks&0x0f
|
||||
binary.BigEndian.PutUint16(out[3:5], h.padLen)
|
||||
if _, err := rand.Read(out[geckoHeaderSize : geckoHeaderSize+int(h.padLen)]); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
copy(out[geckoHeaderSize+int(h.padLen):], payload)
|
||||
return needed, nil
|
||||
}
|
||||
|
||||
// decodeFrame parses a frame from in. The returned payload is a sub-slice of
|
||||
// in (zero-copy) covering the bytes after the header and padding.
|
||||
func decodeFrame(in []byte) (frameHeader, []byte, error) {
|
||||
if len(in) < geckoHeaderSize {
|
||||
return frameHeader{}, nil, errFrameTruncated
|
||||
}
|
||||
if in[0]&geckoFlagFragment == 0 {
|
||||
return frameHeader{}, nil, errFrameInvalid
|
||||
}
|
||||
h := frameHeader{
|
||||
msgID: in[1],
|
||||
chunkIdx: in[2] >> 4,
|
||||
totalChunks: in[2] & 0x0f,
|
||||
padLen: binary.BigEndian.Uint16(in[3:5]),
|
||||
}
|
||||
if h.totalChunks < geckoMinFragmentChunks || h.totalChunks > geckoMaxFragmentChunks {
|
||||
return frameHeader{}, nil, errFrameInvalid
|
||||
}
|
||||
if h.chunkIdx >= h.totalChunks {
|
||||
return frameHeader{}, nil, errFrameInvalid
|
||||
}
|
||||
if geckoHeaderSize+int(h.padLen) > len(in) {
|
||||
return frameHeader{}, nil, errFrameTruncated
|
||||
}
|
||||
return h, in[geckoHeaderSize+int(h.padLen):], nil
|
||||
}
|
||||
96
extras/obfs/gecko_frame_test.go
Normal file
96
extras/obfs/gecko_frame_test.go
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
package obfs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEncodeDecodeFrame(t *testing.T) {
|
||||
payload := []byte{0xa1, 0xb2, 0xc3, 0xd4}
|
||||
for total := geckoMinFragmentChunks; total <= geckoMaxFragmentChunks; total++ {
|
||||
for idx := 0; idx < total; idx++ {
|
||||
for _, padLen := range []int{0, 1, 64, 127, 512, 1100} {
|
||||
h := frameHeader{
|
||||
padLen: uint16(padLen),
|
||||
msgID: 0xa5,
|
||||
chunkIdx: uint8(idx),
|
||||
totalChunks: uint8(total),
|
||||
}
|
||||
out := make([]byte, geckoHeaderSize+padLen+len(payload))
|
||||
n, err := encodeFrame(h, payload, out)
|
||||
if err != nil {
|
||||
t.Fatalf("encode total=%d idx=%d padLen=%d: %v", total, idx, padLen, err)
|
||||
}
|
||||
if n != len(out) {
|
||||
t.Fatalf("encode wrote %d, want %d", n, len(out))
|
||||
}
|
||||
got, body, err := decodeFrame(out)
|
||||
if err != nil {
|
||||
t.Fatalf("decode total=%d idx=%d padLen=%d: %v", total, idx, padLen, err)
|
||||
}
|
||||
if got != h {
|
||||
t.Fatalf("header mismatch: got %+v want %+v", got, h)
|
||||
}
|
||||
if !bytes.Equal(body, payload) {
|
||||
t.Fatalf("payload mismatch total=%d idx=%d padLen=%d", total, idx, padLen)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeFrameRejectsInvalid(t *testing.T) {
|
||||
payload := []byte{0xff}
|
||||
cases := []struct {
|
||||
name string
|
||||
h frameHeader
|
||||
err error
|
||||
}{
|
||||
{"totalChunks zero", frameHeader{totalChunks: 0}, errFrameInvalid},
|
||||
{"totalChunks one", frameHeader{totalChunks: 1}, errFrameInvalid},
|
||||
{"totalChunks nine", frameHeader{totalChunks: 9}, errFrameInvalid},
|
||||
{"chunkIdx out of range", frameHeader{totalChunks: 4, chunkIdx: 4}, errFrameInvalid},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
out := make([]byte, 1024)
|
||||
if _, err := encodeFrame(tc.h, payload, out); !errors.Is(err, tc.err) {
|
||||
t.Fatalf("got %v, want %v", err, tc.err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeFrameRejectsShortBuffer(t *testing.T) {
|
||||
payload := []byte{0x01, 0x02, 0x03}
|
||||
h := frameHeader{padLen: 8, totalChunks: 2, chunkIdx: 0}
|
||||
out := make([]byte, 5) // need 5 + 8 + 3 = 16
|
||||
if _, err := encodeFrame(h, payload, out); !errors.Is(err, errFrameTruncated) {
|
||||
t.Fatalf("got %v, want %v", err, errFrameTruncated)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeFrameRejectsInvalid(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
in []byte
|
||||
err error
|
||||
}{
|
||||
{"empty", []byte{}, errFrameTruncated},
|
||||
{"header truncated", []byte{0x80, 0x55, 0x22, 0x00}, errFrameTruncated},
|
||||
{"not a fragment", []byte{0x00, 0x00, 0x22, 0x00, 0x00}, errFrameInvalid},
|
||||
{"totalChunks zero", []byte{0x80, 0x00, 0x00, 0x00, 0x00}, errFrameInvalid},
|
||||
{"totalChunks one", []byte{0x80, 0x00, 0x01, 0x00, 0x00}, errFrameInvalid},
|
||||
{"totalChunks nine", []byte{0x80, 0x00, 0x09, 0x00, 0x00}, errFrameInvalid},
|
||||
{"chunkIdx == totalChunks", []byte{0x80, 0x00, 0x44, 0x00, 0x00}, errFrameInvalid},
|
||||
{"padLen overrun", []byte{0x80, 0x00, 0x02, 0x00, 0x12, 0x01, 0x02}, errFrameTruncated},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if _, _, err := decodeFrame(tc.in); !errors.Is(err, tc.err) {
|
||||
t.Fatalf("got %v, want %v", err, tc.err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
719
extras/obfs/gecko_test.go
Normal file
719
extras/obfs/gecko_test.go
Normal file
|
|
@ -0,0 +1,719 @@
|
|||
package obfs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// --- in-memory packet pipe ---
|
||||
|
||||
// memEnd is one side of a bidirectional, lossy in-memory packet pipe.
|
||||
type memEnd struct {
|
||||
addr net.Addr
|
||||
other *memEnd
|
||||
|
||||
mu sync.Mutex
|
||||
closed bool
|
||||
done chan struct{}
|
||||
inbox chan memPacket
|
||||
|
||||
writeCount atomic.Int64
|
||||
dropFn func(idx int) bool // optional: return true to drop the n-th outgoing packet
|
||||
}
|
||||
|
||||
type memPacket struct {
|
||||
src net.Addr
|
||||
data []byte
|
||||
}
|
||||
|
||||
func newMemPipe(aAddr, bAddr net.Addr) (*memEnd, *memEnd) {
|
||||
a := &memEnd{addr: aAddr, inbox: make(chan memPacket, 1024), done: make(chan struct{})}
|
||||
b := &memEnd{addr: bAddr, inbox: make(chan memPacket, 1024), done: make(chan struct{})}
|
||||
a.other = b
|
||||
b.other = a
|
||||
return a, b
|
||||
}
|
||||
|
||||
func (e *memEnd) WriteTo(p []byte, _ net.Addr) (int, error) {
|
||||
idx := int(e.writeCount.Add(1) - 1)
|
||||
if e.dropFn != nil && e.dropFn(idx) {
|
||||
return len(p), nil
|
||||
}
|
||||
cp := make([]byte, len(p))
|
||||
copy(cp, p)
|
||||
select {
|
||||
case e.other.inbox <- memPacket{src: e.addr, data: cp}:
|
||||
case <-e.other.done:
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (e *memEnd) ReadFrom(p []byte) (int, net.Addr, error) {
|
||||
select {
|
||||
case pkt := <-e.inbox:
|
||||
return copy(p, pkt.data), pkt.src, nil
|
||||
case <-e.done:
|
||||
return 0, nil, net.ErrClosed
|
||||
}
|
||||
}
|
||||
|
||||
func (e *memEnd) Close() error {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
if e.closed {
|
||||
return nil
|
||||
}
|
||||
e.closed = true
|
||||
close(e.done)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *memEnd) LocalAddr() net.Addr { return e.addr }
|
||||
func (e *memEnd) SetDeadline(time.Time) error { return nil }
|
||||
func (e *memEnd) SetReadDeadline(time.Time) error { return nil }
|
||||
func (e *memEnd) SetWriteDeadline(time.Time) error { return nil }
|
||||
|
||||
// pendingInbox returns the count of buffered, undelivered packets on this end.
|
||||
func (e *memEnd) pendingInbox() int { return len(e.inbox) }
|
||||
|
||||
// --- helpers ---
|
||||
|
||||
func mustWrapGecko(t *testing.T, conn net.PacketConn, password string) net.PacketConn {
|
||||
t.Helper()
|
||||
g, err := WrapPacketConnGecko(conn, GeckoOptions{Password: []byte(password)})
|
||||
if err != nil {
|
||||
t.Fatalf("WrapPacketConnGecko: %v", err)
|
||||
}
|
||||
return g
|
||||
}
|
||||
|
||||
func makeAddrs() (a, b net.Addr) {
|
||||
return &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 1111},
|
||||
&net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 2222}
|
||||
}
|
||||
|
||||
// quicLong / quicShort produce realistic-looking QUIC payloads of n bytes
|
||||
// with the appropriate top-bit on byte 0 to trigger / avoid fragmentation.
|
||||
func quicLong(n int) []byte {
|
||||
p := make([]byte, n)
|
||||
rand.New(rand.NewSource(1)).Read(p)
|
||||
p[0] = 0xc0
|
||||
return p
|
||||
}
|
||||
|
||||
func quicShort(n int) []byte {
|
||||
p := make([]byte, n)
|
||||
rand.New(rand.NewSource(2)).Read(p)
|
||||
p[0] = 0x40
|
||||
return p
|
||||
}
|
||||
|
||||
// --- tests ---
|
||||
|
||||
func TestGeckoRoundTripShortHeader(t *testing.T) {
|
||||
aAddr, bAddr := makeAddrs()
|
||||
a, b := newMemPipe(aAddr, bAddr)
|
||||
defer a.Close()
|
||||
defer b.Close()
|
||||
|
||||
ga := mustWrapGecko(t, a, "test")
|
||||
gb := mustWrapGecko(t, b, "test")
|
||||
defer ga.Close()
|
||||
defer gb.Close()
|
||||
|
||||
payload := quicShort(400)
|
||||
if _, err := ga.WriteTo(payload, bAddr); err != nil {
|
||||
t.Fatalf("WriteTo: %v", err)
|
||||
}
|
||||
// Short-header packets should produce exactly one wire datagram.
|
||||
if got := a.writeCount.Load(); got != 1 {
|
||||
t.Fatalf("inner writes = %d, want 1", got)
|
||||
}
|
||||
|
||||
buf := make([]byte, 4096)
|
||||
n, src, err := gb.ReadFrom(buf)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFrom: %v", err)
|
||||
}
|
||||
if !bytes.Equal(buf[:n], payload) {
|
||||
t.Fatalf("payload mismatch")
|
||||
}
|
||||
if src.String() != aAddr.String() {
|
||||
t.Fatalf("src = %v, want %v", src, aAddr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeckoRoundTripLongHeader(t *testing.T) {
|
||||
aAddr, bAddr := makeAddrs()
|
||||
a, b := newMemPipe(aAddr, bAddr)
|
||||
defer a.Close()
|
||||
defer b.Close()
|
||||
|
||||
ga := mustWrapGecko(t, a, "test")
|
||||
gb := mustWrapGecko(t, b, "test")
|
||||
defer ga.Close()
|
||||
defer gb.Close()
|
||||
|
||||
payload := quicLong(1200)
|
||||
if _, err := ga.WriteTo(payload, bAddr); err != nil {
|
||||
t.Fatalf("WriteTo: %v", err)
|
||||
}
|
||||
chunks := a.writeCount.Load()
|
||||
if chunks < geckoMinFragmentChunks || chunks > geckoMaxFragmentChunks {
|
||||
t.Fatalf("inner writes = %d, want in [%d,%d]", chunks, geckoMinFragmentChunks, geckoMaxFragmentChunks)
|
||||
}
|
||||
|
||||
buf := make([]byte, 4096)
|
||||
n, _, err := gb.ReadFrom(buf)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFrom: %v", err)
|
||||
}
|
||||
if !bytes.Equal(buf[:n], payload) {
|
||||
t.Fatalf("payload mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeckoRoundTripSmallLongHeader(t *testing.T) {
|
||||
for _, size := range []int{1, 2, 5, 10, 15, 20, 25, 27, 30, 40, 64, 128} {
|
||||
t.Run(fmt.Sprintf("size=%d", size), func(t *testing.T) {
|
||||
aAddr, bAddr := makeAddrs()
|
||||
a, b := newMemPipe(aAddr, bAddr)
|
||||
defer a.Close()
|
||||
defer b.Close()
|
||||
|
||||
ga := mustWrapGecko(t, a, "test")
|
||||
gb := mustWrapGecko(t, b, "test")
|
||||
defer ga.Close()
|
||||
defer gb.Close()
|
||||
|
||||
payload := quicLong(size)
|
||||
if _, err := ga.WriteTo(payload, bAddr); err != nil {
|
||||
t.Fatalf("WriteTo: %v", err)
|
||||
}
|
||||
|
||||
buf := make([]byte, 4096)
|
||||
n, _, err := gb.ReadFrom(buf)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFrom: %v", err)
|
||||
}
|
||||
if !bytes.Equal(buf[:n], payload) {
|
||||
t.Fatalf("payload mismatch: got %d bytes, want %d", n, size)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeckoWriteFragmentedNeverPanics(t *testing.T) {
|
||||
aAddr, bAddr := makeAddrs()
|
||||
a, b := newMemPipe(aAddr, bAddr)
|
||||
defer a.Close()
|
||||
defer b.Close()
|
||||
stop := make(chan struct{})
|
||||
defer close(stop)
|
||||
drainInbox(b, stop)
|
||||
|
||||
g := mustWrapGecko(t, a, "test").(*geckoPacketConn)
|
||||
defer g.Close()
|
||||
|
||||
for size := 1; size <= 64; size++ {
|
||||
payload := quicLong(size)
|
||||
if _, err := g.WriteTo(payload, bAddr); err != nil {
|
||||
t.Fatalf("size=%d: WriteTo: %v", size, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeckoReassemblesOutOfOrder(t *testing.T) {
|
||||
aAddr, bAddr := makeAddrs()
|
||||
a, b := newMemPipe(aAddr, bAddr)
|
||||
defer a.Close()
|
||||
defer b.Close()
|
||||
|
||||
// We need exclusive access to b's inbox to reorder. Wrap a side directly,
|
||||
// receive raw datagrams from b, shuffle them into a private pipe that
|
||||
// feeds the receiver gecko.
|
||||
ga := mustWrapGecko(t, a, "test")
|
||||
defer ga.Close()
|
||||
|
||||
payload := quicLong(900)
|
||||
if _, err := ga.WriteTo(payload, bAddr); err != nil {
|
||||
t.Fatalf("WriteTo: %v", err)
|
||||
}
|
||||
|
||||
// Drain b's inbox.
|
||||
var pkts []memPacket
|
||||
deadline := time.Now().Add(time.Second)
|
||||
for time.Now().Before(deadline) && a.writeCount.Load() > int64(len(pkts)) {
|
||||
select {
|
||||
case pkt := <-b.inbox:
|
||||
pkts = append(pkts, pkt)
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
if int64(len(pkts)) != a.writeCount.Load() {
|
||||
t.Fatalf("captured %d/%d", len(pkts), a.writeCount.Load())
|
||||
}
|
||||
|
||||
// Build a private pipe and feed packets in reverse order.
|
||||
c, d := newMemPipe(aAddr, bAddr)
|
||||
gd := mustWrapGecko(t, d, "test")
|
||||
defer gd.Close()
|
||||
defer c.Close()
|
||||
go func() {
|
||||
for i := len(pkts) - 1; i >= 0; i-- {
|
||||
d.inbox <- pkts[i]
|
||||
}
|
||||
}()
|
||||
|
||||
buf := make([]byte, 4096)
|
||||
n, _, err := gd.ReadFrom(buf)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFrom: %v", err)
|
||||
}
|
||||
if !bytes.Equal(buf[:n], payload) {
|
||||
t.Fatalf("payload mismatch after reorder")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeckoExpiresIncompleteFragment(t *testing.T) {
|
||||
aAddr, bAddr := makeAddrs()
|
||||
a, b := newMemPipe(aAddr, bAddr)
|
||||
defer a.Close()
|
||||
defer b.Close()
|
||||
|
||||
// Drop the first chunk so reassembly can never complete.
|
||||
a.dropFn = func(idx int) bool { return idx == 0 }
|
||||
|
||||
ga := mustWrapGecko(t, a, "test")
|
||||
gb := mustWrapGecko(t, b, "test").(*geckoPacketConn)
|
||||
defer ga.Close()
|
||||
defer gb.Close()
|
||||
|
||||
payload := quicLong(900)
|
||||
if _, err := ga.WriteTo(payload, bAddr); err != nil {
|
||||
t.Fatalf("WriteTo: %v", err)
|
||||
}
|
||||
|
||||
// Read all incoming packets in a goroutine; ReadFrom should never return
|
||||
// because no message is complete.
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
buf := make([]byte, 4096)
|
||||
gb.ReadFrom(buf)
|
||||
close(done)
|
||||
}()
|
||||
|
||||
// Wait until the reassembly entry exists, then fast-forward time.
|
||||
deadline := time.Now().Add(time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
gb.mu.Lock()
|
||||
n := len(gb.reassembly)
|
||||
gb.mu.Unlock()
|
||||
if n > 0 {
|
||||
break
|
||||
}
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
gb.mu.Lock()
|
||||
if len(gb.reassembly) == 0 {
|
||||
gb.mu.Unlock()
|
||||
t.Fatal("expected at least one reassembly entry")
|
||||
}
|
||||
gb.mu.Unlock()
|
||||
|
||||
gb.gcExpired(time.Now().Add(geckoReassemblyTTL + time.Second))
|
||||
|
||||
gb.mu.Lock()
|
||||
if len(gb.reassembly) != 0 {
|
||||
gb.mu.Unlock()
|
||||
t.Fatalf("reassembly map not empty after gc")
|
||||
}
|
||||
if len(gb.perSource) != 0 {
|
||||
gb.mu.Unlock()
|
||||
t.Fatalf("perSource map not empty after gc")
|
||||
}
|
||||
gb.mu.Unlock()
|
||||
|
||||
// Cleanup: closing gb unblocks the reader goroutine.
|
||||
gb.Close()
|
||||
<-done
|
||||
}
|
||||
|
||||
func TestGeckoEnforcesPerSourceCap(t *testing.T) {
|
||||
aAddr, bAddr := makeAddrs()
|
||||
a, b := newMemPipe(aAddr, bAddr)
|
||||
defer a.Close()
|
||||
defer b.Close()
|
||||
|
||||
// Drop every second chunk onward so reassembly never completes; the
|
||||
// receiver accumulates partial entries.
|
||||
a.dropFn = func(idx int) bool { return idx > 0 && idx%2 == 0 }
|
||||
|
||||
ga := mustWrapGecko(t, a, "test")
|
||||
gb := mustWrapGecko(t, b, "test").(*geckoPacketConn)
|
||||
defer ga.Close()
|
||||
defer gb.Close()
|
||||
|
||||
go func() {
|
||||
buf := make([]byte, 4096)
|
||||
for {
|
||||
if _, _, err := gb.ReadFrom(buf); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for i := 0; i < geckoMaxPerSource+5; i++ {
|
||||
if _, err := ga.WriteTo(quicLong(1200), bAddr); err != nil {
|
||||
t.Fatalf("WriteTo %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for the receiver to settle.
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
gb.mu.Lock()
|
||||
defer gb.mu.Unlock()
|
||||
count := gb.perSource[aAddr.String()]
|
||||
if count > geckoMaxPerSource {
|
||||
t.Fatalf("perSource = %d, want <= %d", count, geckoMaxPerSource)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeckoEvictsOldestOnGlobalCap(t *testing.T) {
|
||||
g := newGeckoPacketConn(nil, geckoDefaultMinPacket, geckoDefaultMaxPacket) // not actually used for I/O
|
||||
defer close(g.closeCh)
|
||||
|
||||
// Manually fill the reassembly map past the cap with entries from
|
||||
// distinct sources (so the per-source cap doesn't trigger first).
|
||||
now := time.Now()
|
||||
for i := 0; i < geckoMaxReassembly; i++ {
|
||||
key := reassemblyKey{addr: fmt.Sprintf("src-%d", i), msgID: 1}
|
||||
g.reassembly[key] = &reassemblyEntry{
|
||||
chunks: make([][]byte, 4),
|
||||
total: 4,
|
||||
deadline: now.Add(time.Duration(i) * time.Millisecond),
|
||||
}
|
||||
g.perSource[key.addr]++
|
||||
}
|
||||
|
||||
// Trigger eviction.
|
||||
g.mu.Lock()
|
||||
g.evictOldestLocked()
|
||||
g.mu.Unlock()
|
||||
|
||||
if len(g.reassembly) != geckoMaxReassembly-1 {
|
||||
t.Fatalf("after evict len = %d, want %d", len(g.reassembly), geckoMaxReassembly-1)
|
||||
}
|
||||
// The "oldest" was the one with the smallest deadline → src-0.
|
||||
if _, ok := g.reassembly[reassemblyKey{addr: "src-0", msgID: 1}]; ok {
|
||||
t.Fatal("oldest entry not evicted")
|
||||
}
|
||||
if g.perSource["src-0"] != 0 {
|
||||
t.Fatalf("perSource[src-0] = %d after eviction, want 0", g.perSource["src-0"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeckoBoundedUnderGarbageFlood(t *testing.T) {
|
||||
aAddr, bAddr := makeAddrs()
|
||||
a, b := newMemPipe(aAddr, bAddr)
|
||||
defer a.Close()
|
||||
defer b.Close()
|
||||
|
||||
gb := mustWrapGecko(t, b, "test").(*geckoPacketConn)
|
||||
defer gb.Close()
|
||||
|
||||
go func() {
|
||||
buf := make([]byte, 4096)
|
||||
for {
|
||||
if _, _, err := gb.ReadFrom(buf); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Inject 50k random datagrams from a moderate number of distinct sources.
|
||||
rng := rand.New(rand.NewSource(42))
|
||||
for i := 0; i < 50_000; i++ {
|
||||
size := 16 + rng.Intn(200)
|
||||
junk := make([]byte, size)
|
||||
rng.Read(junk)
|
||||
src := &net.UDPAddr{IP: net.IPv4(10, 0, 0, byte(i%256)), Port: 1024 + i%4096}
|
||||
select {
|
||||
case b.inbox <- memPacket{src: src, data: junk}:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("inbox blocked")
|
||||
}
|
||||
}
|
||||
|
||||
// Let the receiver drain.
|
||||
for i := 0; i < 50; i++ {
|
||||
if a.pendingInbox() == 0 && b.pendingInbox() == 0 {
|
||||
break
|
||||
}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
}
|
||||
|
||||
gb.mu.Lock()
|
||||
defer gb.mu.Unlock()
|
||||
if len(gb.reassembly) > geckoMaxReassembly {
|
||||
t.Fatalf("reassembly map size = %d, exceeds cap %d", len(gb.reassembly), geckoMaxReassembly)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeckoUDPPassthrough(t *testing.T) {
|
||||
udp, err := net.ListenUDP("udp", &net.UDPAddr{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer udp.Close()
|
||||
|
||||
g, err := WrapPacketConnGecko(udp, GeckoOptions{Password: []byte("test")})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer g.Close()
|
||||
|
||||
type udpLike interface {
|
||||
SyscallConn() (syscall.RawConn, error)
|
||||
SetReadBuffer(int) error
|
||||
SetWriteBuffer(int) error
|
||||
}
|
||||
u, ok := g.(udpLike)
|
||||
if !ok {
|
||||
t.Fatal("gecko conn does not expose UDP-flavor methods")
|
||||
}
|
||||
if rc, err := u.SyscallConn(); err != nil || rc == nil {
|
||||
t.Fatalf("SyscallConn: %v %v", rc, err)
|
||||
}
|
||||
if err := u.SetReadBuffer(1 << 20); err != nil {
|
||||
t.Fatalf("SetReadBuffer: %v", err)
|
||||
}
|
||||
if err := u.SetWriteBuffer(1 << 20); err != nil {
|
||||
t.Fatalf("SetWriteBuffer: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeckoRequiresPassword(t *testing.T) {
|
||||
if _, err := WrapPacketConnGecko(nil, GeckoOptions{}); err == nil {
|
||||
t.Fatal("expected error for missing password")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeckoRejectsInvalidPacketSize(t *testing.T) {
|
||||
cases := []GeckoOptions{
|
||||
{Password: []byte("x"), MinPacketSize: 1000, MaxPacketSize: 500}, // min > max
|
||||
{Password: []byte("x"), MinPacketSize: -1}, // min <= 0
|
||||
{Password: []byte("x"), MaxPacketSize: geckoBufferSize + 1}, // max too large
|
||||
}
|
||||
for i, opt := range cases {
|
||||
if _, err := WrapPacketConnGecko(nil, opt); err == nil {
|
||||
t.Fatalf("case %d: expected error", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestGeckoPaddingWithinBounds verifies every fragmented wire datagram is
|
||||
// padded into the configured [min, max] size band.
|
||||
func TestGeckoPaddingWithinBounds(t *testing.T) {
|
||||
const minSize, maxSize = 400, 900
|
||||
aAddr, bAddr := makeAddrs()
|
||||
a, b := newMemPipe(aAddr, bAddr)
|
||||
defer a.Close()
|
||||
defer b.Close()
|
||||
|
||||
ga, err := WrapPacketConnGecko(a, GeckoOptions{
|
||||
Password: []byte("test"),
|
||||
MinPacketSize: minSize,
|
||||
MaxPacketSize: maxSize,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("WrapPacketConnGecko: %v", err)
|
||||
}
|
||||
defer ga.Close()
|
||||
|
||||
for _, size := range []int{1, 50, 200, 600, 1200} {
|
||||
if _, err := ga.WriteTo(quicLong(size), bAddr); err != nil {
|
||||
t.Fatalf("WriteTo size=%d: %v", size, err)
|
||||
}
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case pkt := <-b.inbox:
|
||||
if len(pkt.data) < minSize || len(pkt.data) > maxSize {
|
||||
t.Fatalf("datagram size %d outside [%d, %d]", len(pkt.data), minSize, maxSize)
|
||||
}
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compile-time interface assertions.
|
||||
var (
|
||||
_ net.PacketConn = (*geckoPacketConn)(nil)
|
||||
_ udpLikePacketConn = (*geckoPacketConn)(nil)
|
||||
)
|
||||
|
||||
// Sanity: errors.ErrUnsupported is what we return when inner isn't UDP-like.
|
||||
func TestGeckoNonUDPInnerReturnsUnsupported(t *testing.T) {
|
||||
aAddr, bAddr := makeAddrs()
|
||||
a, _ := newMemPipe(aAddr, bAddr)
|
||||
defer a.Close()
|
||||
|
||||
g := mustWrapGecko(t, a, "test").(*geckoPacketConn)
|
||||
defer g.Close()
|
||||
|
||||
if _, err := g.SyscallConn(); !errors.Is(err, errors.ErrUnsupported) {
|
||||
t.Fatalf("SyscallConn err = %v, want ErrUnsupported", err)
|
||||
}
|
||||
if err := g.SetReadBuffer(1 << 20); !errors.Is(err, errors.ErrUnsupported) {
|
||||
t.Fatalf("SetReadBuffer err = %v, want ErrUnsupported", err)
|
||||
}
|
||||
if err := g.SetWriteBuffer(1 << 20); !errors.Is(err, errors.ErrUnsupported) {
|
||||
t.Fatalf("SetWriteBuffer err = %v, want ErrUnsupported", err)
|
||||
}
|
||||
}
|
||||
|
||||
// --- benchmarks ---
|
||||
|
||||
// drainInbox empties an inbox in a goroutine until done is closed; used in
|
||||
// benchmarks so a full memEnd.inbox never blocks WriteTo.
|
||||
func drainInbox(end *memEnd, done <-chan struct{}) {
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case <-end.inbox:
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func BenchmarkGeckoWriteShortHeader(b *testing.B) {
|
||||
aAddr, bAddr := makeAddrs()
|
||||
a, bEnd := newMemPipe(aAddr, bAddr)
|
||||
defer a.Close()
|
||||
defer bEnd.Close()
|
||||
stop := make(chan struct{})
|
||||
defer close(stop)
|
||||
drainInbox(bEnd, stop)
|
||||
|
||||
g, _ := WrapPacketConnGecko(a, GeckoOptions{Password: []byte("bench")})
|
||||
defer g.Close()
|
||||
|
||||
payload := quicShort(400)
|
||||
b.SetBytes(int64(len(payload)))
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if _, err := g.WriteTo(payload, bAddr); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGeckoWriteLongHeader(b *testing.B) {
|
||||
aAddr, bAddr := makeAddrs()
|
||||
a, bEnd := newMemPipe(aAddr, bAddr)
|
||||
defer a.Close()
|
||||
defer bEnd.Close()
|
||||
stop := make(chan struct{})
|
||||
defer close(stop)
|
||||
drainInbox(bEnd, stop)
|
||||
|
||||
g, _ := WrapPacketConnGecko(a, GeckoOptions{Password: []byte("bench")})
|
||||
defer g.Close()
|
||||
|
||||
payload := quicLong(1200)
|
||||
b.SetBytes(int64(len(payload)))
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if _, err := g.WriteTo(payload, bAddr); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// captureGeckoWire fragments payload through a sender Gecko conn and returns
|
||||
// the resulting wire datagrams, for use as fixed input to read benchmarks.
|
||||
func captureGeckoWire(b *testing.B, payload []byte) []memPacket {
|
||||
b.Helper()
|
||||
aAddr, bAddr := makeAddrs()
|
||||
a, bEnd := newMemPipe(aAddr, bAddr)
|
||||
defer a.Close()
|
||||
defer bEnd.Close()
|
||||
|
||||
ga, err := WrapPacketConnGecko(a, GeckoOptions{Password: []byte("bench")})
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
defer ga.Close()
|
||||
if _, err := ga.WriteTo(payload, bAddr); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
var wire []memPacket
|
||||
for {
|
||||
select {
|
||||
case pkt := <-bEnd.inbox:
|
||||
wire = append(wire, pkt)
|
||||
default:
|
||||
return wire
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGeckoReadShortHeader(b *testing.B) {
|
||||
wire := captureGeckoWire(b, quicShort(400))
|
||||
|
||||
_, recv := newMemPipe(makeAddrs())
|
||||
gb, err := WrapPacketConnGecko(recv, GeckoOptions{Password: []byte("bench")})
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
defer gb.Close()
|
||||
|
||||
buf := make([]byte, 4096)
|
||||
b.SetBytes(400)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, pkt := range wire {
|
||||
recv.inbox <- pkt
|
||||
}
|
||||
if _, _, err := gb.ReadFrom(buf); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGeckoReadLongHeader(b *testing.B) {
|
||||
wire := captureGeckoWire(b, quicLong(1200))
|
||||
|
||||
_, recv := newMemPipe(makeAddrs())
|
||||
gb, err := WrapPacketConnGecko(recv, GeckoOptions{Password: []byte("bench")})
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
defer gb.Close()
|
||||
|
||||
buf := make([]byte, 4096)
|
||||
b.SetBytes(1200)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, pkt := range wire {
|
||||
recv.inbox <- pkt
|
||||
}
|
||||
if _, _, err := gb.ReadFrom(buf); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package obfs
|
|||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
|
|
@ -15,14 +16,14 @@ const (
|
|||
smKeyLen = blake2b.Size256
|
||||
)
|
||||
|
||||
var _ Obfuscator = (*SalamanderObfuscator)(nil)
|
||||
var _ obfuscator = (*salamanderObfuscator)(nil)
|
||||
|
||||
var ErrPSKTooShort = fmt.Errorf("PSK must be at least %d bytes", smPSKMinLen)
|
||||
|
||||
// SalamanderObfuscator is an obfuscator that obfuscates each packet with
|
||||
// salamanderObfuscator is an obfuscator that obfuscates each packet with
|
||||
// the BLAKE2b-256 hash of a pre-shared key combined with a random salt.
|
||||
// Packet format: [8-byte salt][payload]
|
||||
type SalamanderObfuscator struct {
|
||||
type salamanderObfuscator struct {
|
||||
PSK []byte
|
||||
RandSrc *rand.Rand
|
||||
|
||||
|
|
@ -30,21 +31,32 @@ type SalamanderObfuscator struct {
|
|||
keyInput []byte
|
||||
}
|
||||
|
||||
func NewSalamanderObfuscator(psk []byte) (*SalamanderObfuscator, error) {
|
||||
func newSalamanderObfuscator(psk []byte) (*salamanderObfuscator, error) {
|
||||
if len(psk) < smPSKMinLen {
|
||||
return nil, ErrPSKTooShort
|
||||
}
|
||||
pskCopy := append([]byte(nil), psk...)
|
||||
keyInput := make([]byte, len(pskCopy)+smSaltLen)
|
||||
copy(keyInput, pskCopy)
|
||||
return &SalamanderObfuscator{
|
||||
return &salamanderObfuscator{
|
||||
PSK: pskCopy,
|
||||
RandSrc: rand.New(rand.NewSource(time.Now().UnixNano())),
|
||||
keyInput: keyInput,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (o *SalamanderObfuscator) Obfuscate(in, out []byte) int {
|
||||
// WrapPacketConnSalamander wraps conn with Salamander obfuscation: each
|
||||
// outbound packet is XOR'd with BLAKE2b-256(PSK || random salt) and the
|
||||
// 8-byte salt is prepended on the wire.
|
||||
func WrapPacketConnSalamander(conn net.PacketConn, psk []byte) (net.PacketConn, error) {
|
||||
ob, err := newSalamanderObfuscator(psk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return wrapPacketConn(conn, ob), nil
|
||||
}
|
||||
|
||||
func (o *salamanderObfuscator) Obfuscate(in, out []byte) int {
|
||||
outLen := len(in) + smSaltLen
|
||||
if len(out) < outLen {
|
||||
return 0
|
||||
|
|
@ -59,7 +71,7 @@ func (o *SalamanderObfuscator) Obfuscate(in, out []byte) int {
|
|||
return outLen
|
||||
}
|
||||
|
||||
func (o *SalamanderObfuscator) Deobfuscate(in, out []byte) int {
|
||||
func (o *salamanderObfuscator) Deobfuscate(in, out []byte) int {
|
||||
outLen := len(in) - smSaltLen
|
||||
if outLen <= 0 || len(out) < outLen {
|
||||
return 0
|
||||
|
|
@ -73,7 +85,7 @@ func (o *SalamanderObfuscator) Deobfuscate(in, out []byte) int {
|
|||
return outLen
|
||||
}
|
||||
|
||||
func (o *SalamanderObfuscator) keyLocked(salt []byte) [smKeyLen]byte {
|
||||
func (o *salamanderObfuscator) keyLocked(salt []byte) [smKeyLen]byte {
|
||||
copy(o.keyInput[len(o.PSK):], salt[:smSaltLen])
|
||||
return blake2b.Sum256(o.keyInput)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import (
|
|||
)
|
||||
|
||||
func BenchmarkSalamanderObfuscator_Obfuscate(b *testing.B) {
|
||||
o, _ := NewSalamanderObfuscator([]byte("average_password"))
|
||||
o, _ := newSalamanderObfuscator([]byte("average_password"))
|
||||
in := make([]byte, 1200)
|
||||
_, _ = rand.Read(in)
|
||||
out := make([]byte, 2048)
|
||||
|
|
@ -19,7 +19,7 @@ func BenchmarkSalamanderObfuscator_Obfuscate(b *testing.B) {
|
|||
}
|
||||
|
||||
func BenchmarkSalamanderObfuscator_Deobfuscate(b *testing.B) {
|
||||
o, _ := NewSalamanderObfuscator([]byte("average_password"))
|
||||
o, _ := newSalamanderObfuscator([]byte("average_password"))
|
||||
in := make([]byte, 1200)
|
||||
_, _ = rand.Read(in)
|
||||
out := make([]byte, 2048)
|
||||
|
|
@ -30,7 +30,7 @@ func BenchmarkSalamanderObfuscator_Deobfuscate(b *testing.B) {
|
|||
}
|
||||
|
||||
func TestSalamanderObfuscator(t *testing.T) {
|
||||
o, _ := NewSalamanderObfuscator([]byte("average_password"))
|
||||
o, _ := newSalamanderObfuscator([]byte("average_password"))
|
||||
in := make([]byte, 1200)
|
||||
oOut := make([]byte, 2048)
|
||||
dOut := make([]byte, 2048)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue