Compare commits

...
Author SHA1 Message Date
Frog Rocky 0895b3c31a fixes 2026-04-02 17:41:23 +02:00
Frog Rocky 3075cbdd1f test(benchmarks): add conn and conceal benchmark suites
Add benchmark harnesses for the conn and conceal packages with
deterministic crypto/rand, fake transports, and no live sockets in
timed loops.

Keep a compact default benchmark matrix for routine benchstat runs and
preserve the exhaustive matrix behind AMNEZWG_BENCH_FULL=1.
2026-03-27 12:56:45 +01:00
Frog Rocky baeb11c168 fix e2e 2026-03-25 02:45:18 +01:00
Frog Rocky a8711790df fix e2e 2026-03-25 02:45:18 +01:00
Frog Rocky b2d7894826 added end-to-end tests 2026-03-25 02:45:18 +01:00
Frog Rocky 374f91fe4c omit junk on tcp 2026-03-25 02:45:18 +01:00
Frog Rocky 8b17d27361 added stream record layer to conceal 2026-03-25 02:45:18 +01:00
Frog Rocky 6a110d4319 implemented prelude connection layer for net.Conn 2026-03-25 02:45:18 +01:00
Frog Rocky 6647dcd6bb fix of tests 2026-03-25 02:45:18 +01:00
Yaroslav Gurov 528e8aab99 fix: replace *net.UDPConn with UDPConn 2026-01-26 15:55:01 +00:00
Yaroslav Gurov e62d4ddb16 chore: masquerade udp cleanup 2026-01-21 13:58:32 +00:00
Yaroslav Gurov 526553a287 chore: framed conn size adjustments 2026-01-21 13:01:02 +00:00
Yaroslav Gurov da95c8cd6d feat: prelude conn H1-H4 compat support 2026-01-21 11:38:52 +00:00
Yaroslav Gurov c0d6e66c59 feat: compat mode with previous AWG impls of H1-H4 2026-01-20 23:22:02 +00:00
Yaroslav Gurov e69a0c191f feat: masquerade for batch conn 2026-01-20 21:22:28 +00:00
Yaroslav Gurov b91d651ae0 chore: [WIP] common awg features refactoring 2026-01-20 14:00:43 +00:00
Yaroslav Gurov 0d55ebccb3 feat: add conceal to stdbind 2026-01-19 13:59:58 +00:00
Yaroslav Gurov b7111a7841 chore: stream bind reformatting 2026-01-15 23:05:39 +00:00
Yaroslav Gurov 7772925dfb fix: handle busy port properly 2026-01-15 22:56:40 +00:00
Yaroslav Gurov 8241945a66 chore: general cleanup and refactoring
* [WIP] moved out classic AWG params to conceal conns
* refactor of conceal rules locations
* refactor of working with concealment parameters
2026-01-15 21:44:01 +00:00
Yaroslav Gurov 6b76b64b12 readme: change tips 2026-01-12 22:27:10 +00:00
Yaroslav Gurov ac6de41776 readme: reformat network section 2026-01-12 22:20:40 +00:00
Yaroslav Gurov 242e9197e5 readme: reformat tags formatting guide 2026-01-12 22:09:48 +00:00
Yaroslav Gurov 0d3e8e8a7b chore: readme changes 2026-01-12 18:27:39 +00:00
Yaroslav Gurov f116e78e23 chore: obf read/write cleanup 2026-01-12 17:21:16 +00:00
Yaroslav Gurov c8075e9e60 feat: add hex format for <dz> 2026-01-12 15:24:56 +00:00
Yaroslav Gurov 5d113165a0 feat: configurable <dz> field 2026-01-12 11:42:15 +00:00
Yaroslav Gurov 45fa148913 chore: empty default obfuscators 2026-01-09 02:25:01 +00:00
Yaroslav Gurov b25e2b6ab4 chore: longer human-readable names 2026-01-08 13:18:23 +00:00
Yaroslav Gurov 5715abd559 chore: uapi adjustments 2026-01-08 13:01:17 +00:00
Yaroslav Gurov 861b341f54 chore: stream read cleanup 2026-01-07 20:32:50 +00:00
Yaroslav Gurov 5f04c25146 feat: uapi support of fmt_in, fmt_out, and proper network fields 2026-01-07 16:24:14 +00:00
Yaroslav Gurov dc283472b5 chore: change bind sequence on windows 2026-01-07 16:22:32 +00:00
Yaroslav Gurov ec6d9a5f21 chore: multibind optimizations 2026-01-07 16:22:05 +00:00
Yaroslav Gurov 40e318c76e feat: buffer write operations for ObfuscatedConn 2025-12-20 09:17:33 +00:00
Yaroslav Gurov fecfe69b7b chore: use buffer pool in ObfuscatedConn instead of the original one buf 2025-12-20 08:52:36 +00:00
Yaroslav Gurov bb8270bcfa feat: make obf packets work in stream; initial support of obfuscated connections 2025-12-20 07:44:16 +00:00
Yaroslav Gurov 997f578a9b feat: initial support of StreamObfuscators 2025-12-19 07:51:47 +00:00
Yaroslav Gurov 497de26f78 feat: initial support of Multibind and UAPI configuration 2025-12-19 05:43:36 +00:00
Yaroslav Gurov 030539bcf7 chore: use io.ReadFull instead of plain net.Conn.Read 2025-12-18 19:26:29 +00:00
Yaroslav Gurov e19237981d feat: [exp] added size information in front of each packet 2025-12-18 16:58:47 +00:00
Yaroslav Gurov 2791bea913 chore: safe handling of new connections 2025-12-18 03:27:52 +00:00
Yaroslav Gurov 787b2f6bbd chore: initialize stream on send only 2025-12-18 00:52:26 +00:00
Yaroslav Gurov f139b29094 feat: initial fwmark support for stream connections 2025-12-18 00:52:26 +00:00
Yaroslav Gurov d42bdde6da feat: rework stream listener 2025-12-18 00:52:26 +00:00
Yaroslav Gurov f867e4dba7 feat: resolve domain immediately while on configuration stage 2025-12-18 00:52:26 +00:00
Yaroslav Gurov b7f3497a8c feat: add syncronization and protection 2025-12-18 00:52:26 +00:00
Yaroslav Gurov f0b5c51c4d feat: add tcp bind 2025-12-18 00:52:26 +00:00
49 changed files with 7167 additions and 818 deletions

No files matched your search

+187 -10
View File
@@ -89,6 +89,12 @@ Values could be specified as:
- range: `x-y`, x <= y; e.g. `123-456`
- single value `1234`
> [!TIP]
> Custom signature packets does not carry any actual data, so there is no need to specify it on both sides. General recommendation is to use it on the client side only
> [!IMPORTANT]
> If the final size of any packet exceeds system MTU, it would be fractured into fragments, which looks suspicious
### Custom signature packets
These packets are being send prior to every handshake, in the same way as Junk packets do. The sending order is `I1`, `I2`, `I3`, `I4`, `I5`. If there is no value specified, the packet is skipped.
@@ -99,16 +105,187 @@ These packets are being send prior to every handshake, in the same way as Junk p
- `I4: string`
- `I5: string`
Value is a sequence of tags specified below:
- `<b 0x[seq]>` - static bytes tag. Dumps `[seq]` as-is to the packet. `[seq]` is hex-encoded sequence which represents bytes sequence (2 hex numbers per byte) and is always even-sized
- `<r [size]>` - random bytes tag. Dumps `[size]` amount of randomly-generated bytes to the packet
- `<rd [size]>` - random digits tag. Dumps `[size]` amount of randomly-generated bytes from `[0-9]` set to the packet
- `<rc [size]>` - random chars tag. Dumps `[size]` amount of randomly-generated bytes from `[a-zA-Z] set to the packet
- `<t>` - timestamp tag. Dumps 4-bytes long current system time in UNIX format
- `<c>` - packet counter tag. Dumps 4-bytes long amount of packets sent by AWG
Value as a tag-sequence, as described in Tag format paragraph
> [!TIP]
> Custom signature packets does not carry any actual data, so there is no need to specify it on both sides. General recommendation is to use it on the client side only
### Network
**`Network: "udp" | "tcp"`**
- A network which device would use to connect to peers
- In case of `tcp`, the connection is always initiated by the peer which has `Endpoint` specified
- In case of `tcp`, the device would not listen if `ListenPort` is omitted
> [!NOTE]
> Although it is not required, `Network = tcp` would highly-likely require `FormatIn` and `FormatOut` parameters since stream connection does not have fixed-sized messages
### Message format
**`FormatIn: string of tags`**
- A format of packet which AWG device would expect on the input side instead of the original WG packet
- The packet could be a part of the stream if `<dz>` is specified
**`FormatOut: string of tags`**
- A format of packet which AWG device would generate on the output side instead of the original WG packet. The whole packet would be generated as a single piece and would be sent to the system as-is
- **Example**
```
FormatIn = <b 0x4567><dz be 4><r 12><d><t>
FormatOut = <b 0x1234><dz be 4><d>
```
> [!IMPORTANT]
> If the final size of any packet exceeds system MTU, it would be fractured into fragments, which looks suspicious
> For TCP network it is really neccessary to specify `<dz>` tag somewhere because of the stream nature - the original packet could be coalecsed with the next one or splitted into the fragments
> [!IMPORTANT]
> Always put `<d>` tag in the format.
> [!IMPORTANT]
> Peer's `FormatIn` must be the same as device's `FormatOut` for ***machines*** to understand each other formats
## Tags format
### Static bytes tag
**Description**
- Static bytes, sends and receives specified bytes as-is with predictable size
- Immutable, does not change after configuration
**Format**
```
<b 0x[seq]>
```
**Params**
- `[seq]` - *always even-sized* hex-encoded sequence of bytes (2 hex numbers per byte)
**Sample usage**
```
<b 0xd34db3ef>
<b 0x1234><dz be 2><d>
```
### Random bytes tag
**Description**
- Randomly-generated bytes of specified size
- Uses cryptographically secure generator
**Format**
```
<r [size]>
```
**Params**
- `[size]` - amount of random bytes
**Sample usage**
```
<r 21>
<r 21><dz be 2><r 1><d>
```
### Random digits tag
**Description**
- Randomly-generated digits from `[0-9]` set
- Uses cryptographically secure generator
**Format**
```
<rd [size]>
```
**Params**
- `[size]` - amount of digits
**Sample usage**
```
<rd 21>
<b 0x6e756d3a><rd 21><b 0x0d0a><dz hex 0x0d><b 0x0a><rd 1><d>
```
### Random characters tag
**Description**
- Randomly-generated digits from `[a-zA-Z]` set
- Uses cryptographically secure generator
**Format**
```
<rc [size]>
```
**Params**
- `[size]` - amount of chars
**Example**
```
<rc 21>
<b 0x6e616d653a><rc 21><b 0x0d0a><dz ascii 0x0d><b 0x0a><rc 1><d>
```
### Timestamp tag
**Description**
- Current UNIX time, int32
- Provided in big-endian
**Format**
```
<t>
```
**Sample usage**
```
<t>
<b 0x1234><t><dz be 2><d>
```
### Data tag
**Description**
- The original WireGuard packet
- Has `H1-H4` and `S1-S4` params applied
**Format**
```
<d>
```
**Sample usage**
```
<d>
<dz hex 0x0d><b 0x0a><d>
```
### Data size tag
**Description**
- Size of the remaining pard of original WireGuard packet
- `hex` and `ascii` formats are dynamic-sized and no leading zeroes
- sender appends `[end]` to the end in case of `ascii` or `hex`
**Format**
```
<dz [fmt] [size|end]>
```
**Params**
- `[fmt]` - format of the field
- `be` - fixed-size big-endian
- `le` - fixed-size little-endian
- `ascii` - decimal ascii representation
- `hex` - hexadecimal ASCII representation
- `[size]` - amount of bytes of the field
- only for `be` and `le`
- `[end]` - the symbol following the data size
- only for `ascii` and `hex`
**Sample usage**
```
<dz ascii 0x0d> e.g. `10 -> "10\r"`
<dz hex 0x0a> e.g. `10 -> "A\n"`
<dz be 4> e.g. `10 -> 0x00 0x00 0x00 0x0A`
<dz le 2> e.g. `10 -> 0x0A 0x00`
<dz 0x0d><b 0x0a><d><b 0x0d0a>
```
+581
View File
@@ -0,0 +1,581 @@
package conceal
import (
"bytes"
crand "crypto/rand"
"encoding/binary"
"io"
"net"
"os"
"sync"
"syscall"
"testing"
"time"
"golang.org/x/net/ipv4"
)
const (
benchmarkMaxPacketSize = 65535
benchmarkBatchPoolSize = 128
benchmarkUDPListenPort = 51820
benchmarkFixtureRingSize = 256
)
var (
benchmarkFramedOpts = FramedOpts{
H1: benchmarkMustHeader("777"),
H2: benchmarkMustHeader("778"),
H3: benchmarkMustHeader("779"),
H4: benchmarkMustHeader("780"),
S1: 8,
S2: 8,
S3: 8,
S4: 16,
}
benchmarkFramedCompatOpts = func() FramedOpts {
opts := benchmarkFramedOpts
opts.HeaderCompat = true
return opts
}()
benchmarkMasqueradeRules = benchmarkMustRules("<dz be 2><d>")
benchmarkPreludeOneRule = PreludeOpts{
RulesArr: [5]Rules{
benchmarkMustRules("<b 0xaabb>"),
},
}
benchmarkPreludeFiveRules = PreludeOpts{
RulesArr: [5]Rules{
benchmarkMustRules("<b 0xa1>"),
benchmarkMustRules("<b 0xa2a3>"),
benchmarkMustRules("<b 0xa4a5a6>"),
benchmarkMustRules("<b 0xa7a8a9aa>"),
benchmarkMustRules("<b 0xabacadaeaf>"),
},
}
benchmarkPreludeRulesPlusJunk = PreludeOpts{
Jc: 1,
Jmin: 3,
Jmax: 3,
RulesArr: [5]Rules{
benchmarkMustRules("<b 0xaabb>"),
},
}
benchmarkPayloads = benchmarkBuildPayloadProfiles()
benchmarkUDPAddr = &net.UDPAddr{
IP: net.IPv4(127, 0, 0, 1),
Port: benchmarkUDPListenPort,
}
benchmarkRandMu sync.Mutex
)
type benchmarkPayloadProfiles struct {
initiation []byte
response []byte
cookie []byte
transportKeepalive []byte
transportSmall []byte
transportMTU []byte
compatInitiation []byte
compatResponse []byte
compatCookie []byte
compatTransportKeepalive []byte
compatTransportSmall []byte
compatTransportMTU []byte
}
func benchmarkBuildPayloadProfiles() benchmarkPayloadProfiles {
return benchmarkPayloadProfiles{
initiation: benchmarkMakePayload(WireguardMsgInitiationSize, WireguardMsgInitiationType),
response: benchmarkMakePayload(WireguardMsgResponseSize, WireguardMsgResponseType),
cookie: benchmarkMakePayload(WireguardMsgCookieReplySize, WireguardMsgCookieReplyType),
transportKeepalive: benchmarkMakePayload(WireguardMsgTransportMinSize, WireguardMsgTransportType),
transportSmall: benchmarkMakePayload(256, WireguardMsgTransportType),
transportMTU: benchmarkMakePayload(1280, WireguardMsgTransportType),
compatInitiation: benchmarkMakePayload(WireguardMsgInitiationSize, benchmarkFramedCompatOpts.H1.start),
compatResponse: benchmarkMakePayload(WireguardMsgResponseSize, benchmarkFramedCompatOpts.H2.start),
compatCookie: benchmarkMakePayload(WireguardMsgCookieReplySize, benchmarkFramedCompatOpts.H3.start),
compatTransportKeepalive: benchmarkMakePayload(WireguardMsgTransportMinSize, benchmarkFramedCompatOpts.H4.start),
compatTransportSmall: benchmarkMakePayload(256, benchmarkFramedCompatOpts.H4.start),
compatTransportMTU: benchmarkMakePayload(1280, benchmarkFramedCompatOpts.H4.start),
}
}
func benchmarkMakePayload(size int, header uint32) []byte {
payload := make([]byte, size)
binary.LittleEndian.PutUint32(payload[:4], header)
for i := 4; i < len(payload); i++ {
payload[i] = byte(i)
}
return payload
}
func benchmarkMustRules(spec string) Rules {
rules, err := ParseRules(spec)
if err != nil {
panic(err)
}
return rules
}
func benchmarkMustHeader(spec string) *RangedHeader {
header, err := NewRangedHeader(spec)
if err != nil {
panic(err)
}
return header
}
func benchmarkNewBufferPool() *sync.Pool {
return &sync.Pool{
New: func() any {
return make([]byte, benchmarkMaxPacketSize)
},
}
}
func benchmarkNewMsgsPool() *sync.Pool {
return &sync.Pool{
New: func() any {
msgs := make([]ipv4.Message, benchmarkBatchPoolSize)
for i := range msgs {
msgs[i].Buffers = make([][]byte, 1)
msgs[i].OOB = make([]byte, 0)
}
return &msgs
},
}
}
func benchmarkMaxFramePadding(opts FramedOpts) int {
maxPadding := opts.S1
if opts.S2 > maxPadding {
maxPadding = opts.S2
}
if opts.S3 > maxPadding {
maxPadding = opts.S3
}
if opts.S4 > maxPadding {
maxPadding = opts.S4
}
return maxPadding
}
func benchmarkEncodeFramedRecord(opts FramedOpts, payload []byte) []byte {
enc, ok := newFrameEncoding(opts)
if !ok {
panic("framed benchmark encoding unavailable")
}
buf := make([]byte, len(payload)+benchmarkMaxFramePadding(opts))
n := enc.Encode(buf, payload)
if n == 0 {
panic("framed benchmark encoding failed")
}
return append([]byte(nil), buf[:n]...)
}
func benchmarkEncodeMasqueradeRecord(rules Rules, payload []byte) []byte {
pool := benchmarkNewBufferPool()
ctx := &writeContext{
FlexBuffer: WrapFlexBuffer(payload),
BufferPool: WrapBufferPool(pool),
}
tmp := pool.Get().([]byte)
defer pool.Put(tmp)
w := bytes.NewBuffer(tmp[:0])
if err := rules.Write(w, ctx); err != nil {
panic(err)
}
return append([]byte(nil), w.Bytes()...)
}
func benchmarkEncodeStreamRecords(rules Rules, records ...[]byte) []byte {
var out bytes.Buffer
for _, record := range records {
out.Write(benchmarkEncodeMasqueradeRecord(rules, record))
}
return out.Bytes()
}
func benchmarkRepeatPayload(payload []byte, count int) [][]byte {
payloads := make([][]byte, count)
for i := range payloads {
payloads[i] = payload
}
return payloads
}
func benchmarkInitiationBatch(batchSize int) [][]byte {
payloads := make([][]byte, batchSize)
if batchSize == 0 {
return payloads
}
payloads[0] = benchmarkPayloads.initiation
for i := 1; i < len(payloads); i++ {
payloads[i] = benchmarkPayloads.transportSmall
}
return payloads
}
func benchmarkTotalBytes(payloads [][]byte) int {
total := 0
for _, payload := range payloads {
total += len(payload)
}
return total
}
func benchmarkAverageBytes(payloads ...[]byte) int {
if len(payloads) == 0 {
return 0
}
total := 0
for _, payload := range payloads {
total += len(payload)
}
return total / len(payloads)
}
func benchmarkAverageInts(values ...int) int {
if len(values) == 0 {
return 0
}
total := 0
for _, value := range values {
total += value
}
return total / len(values)
}
func benchmarkFullMatrixEnabled() bool {
return os.Getenv("AMNEZWG_BENCH_FULL") != ""
}
func benchmarkRunLoop(b *testing.B, bytesPerOp int, reset func(), op func() error) {
b.Helper()
b.ReportAllocs()
if bytesPerOp > 0 {
b.SetBytes(int64(bytesPerOp))
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
if i > 0 && reset != nil {
b.StopTimer()
reset()
b.StartTimer()
}
if err := op(); err != nil {
b.Fatal(err)
}
}
}
func benchmarkRunLoopWithFixtureRing[T any](b *testing.B, bytesPerOp int, fixtures []T, reset func(T), op func(T) error) {
b.Helper()
if len(fixtures) == 0 {
b.Fatal("benchmark fixture ring must not be empty")
}
b.ReportAllocs()
if bytesPerOp > 0 {
b.SetBytes(int64(bytesPerOp))
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
if i > 0 && i%len(fixtures) == 0 && reset != nil {
b.StopTimer()
for _, fixture := range fixtures {
reset(fixture)
}
b.StartTimer()
}
if err := op(fixtures[i%len(fixtures)]); err != nil {
b.Fatal(err)
}
}
}
func benchmarkUseDeterministicRand(b *testing.B) {
b.Helper()
benchmarkRandMu.Lock()
oldReader := crand.Reader
crand.Reader = &benchmarkDeterministicReader{state: 1}
b.Cleanup(func() {
crand.Reader = oldReader
benchmarkRandMu.Unlock()
})
}
type benchmarkDeterministicReader struct {
state uint64
}
func (r *benchmarkDeterministicReader) Read(p []byte) (int, error) {
x := r.state
if x == 0 {
x = 1
}
for i := range p {
x ^= x << 13
x ^= x >> 7
x ^= x << 17
p[i] = byte(x)
}
r.state = x
return len(p), nil
}
type benchmarkStreamConn struct {
readBuf []byte
readOff int
readChunks [][]byte
readChunkAt int
readChunkOff int
}
func newBenchmarkStreamConn(readBuf []byte) *benchmarkStreamConn {
return &benchmarkStreamConn{readBuf: readBuf}
}
func newBenchmarkStreamChunksConn(readChunks [][]byte) *benchmarkStreamConn {
return &benchmarkStreamConn{readChunks: readChunks}
}
func (c *benchmarkStreamConn) ResetRead() {
c.readOff = 0
c.readChunkAt = 0
c.readChunkOff = 0
}
func (c *benchmarkStreamConn) Read(p []byte) (int, error) {
if len(c.readChunks) > 0 {
chunk := c.readChunks[c.readChunkAt]
if c.readChunkOff >= len(chunk) {
c.readChunkAt++
if c.readChunkAt == len(c.readChunks) {
c.readChunkAt = 0
}
c.readChunkOff = 0
chunk = c.readChunks[c.readChunkAt]
}
n := copy(p, chunk[c.readChunkOff:])
c.readChunkOff += n
if c.readChunkOff == len(chunk) {
c.readChunkAt++
if c.readChunkAt == len(c.readChunks) {
c.readChunkAt = 0
}
c.readChunkOff = 0
}
return n, nil
}
if len(c.readBuf) == 0 {
return 0, io.EOF
}
if c.readOff >= len(c.readBuf) {
c.readOff = 0
}
n := copy(p, c.readBuf[c.readOff:])
c.readOff += n
return n, nil
}
func (c *benchmarkStreamConn) Write(p []byte) (int, error) {
return len(p), nil
}
func (c *benchmarkStreamConn) Close() error {
return nil
}
func (c *benchmarkStreamConn) LocalAddr() net.Addr {
return benchmarkUDPAddr
}
func (c *benchmarkStreamConn) RemoteAddr() net.Addr {
return benchmarkUDPAddr
}
func (c *benchmarkStreamConn) SetDeadline(time.Time) error {
return nil
}
func (c *benchmarkStreamConn) SetReadDeadline(time.Time) error {
return nil
}
func (c *benchmarkStreamConn) SetWriteDeadline(time.Time) error {
return nil
}
type benchmarkUDPConn struct {
readPayloads [][]byte
readIndex int
}
func newBenchmarkUDPConn(readPayloads [][]byte) *benchmarkUDPConn {
return &benchmarkUDPConn{readPayloads: readPayloads}
}
func (c *benchmarkUDPConn) ResetRead() {
c.readIndex = 0
}
func (c *benchmarkUDPConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
n, _, _, udpAddr, err := c.ReadMsgUDP(p, nil)
return n, udpAddr, err
}
func (c *benchmarkUDPConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
udpAddr, _ := addr.(*net.UDPAddr)
n, _, err = c.WriteMsgUDP(p, nil, udpAddr)
return n, err
}
func (c *benchmarkUDPConn) Close() error {
return nil
}
func (c *benchmarkUDPConn) LocalAddr() net.Addr {
return benchmarkUDPAddr
}
func (c *benchmarkUDPConn) SetDeadline(time.Time) error {
return nil
}
func (c *benchmarkUDPConn) SetReadDeadline(time.Time) error {
return nil
}
func (c *benchmarkUDPConn) SetWriteDeadline(time.Time) error {
return nil
}
func (c *benchmarkUDPConn) SyscallConn() (syscall.RawConn, error) {
return nil, nil
}
func (c *benchmarkUDPConn) ReadMsgUDP(b, oob []byte) (n, oobn, flags int, addr *net.UDPAddr, err error) {
if len(c.readPayloads) == 0 {
return 0, 0, 0, nil, io.EOF
}
payload := c.readPayloads[c.readIndex]
c.readIndex++
if c.readIndex == len(c.readPayloads) {
c.readIndex = 0
}
n = copy(b, payload)
return n, 0, 0, benchmarkUDPAddr, nil
}
func (c *benchmarkUDPConn) WriteMsgUDP(b, oob []byte, addr *net.UDPAddr) (n, oobn int, err error) {
return len(b), len(oob), nil
}
type benchmarkBatchPacket struct {
payload []byte
oob []byte
addr *net.UDPAddr
}
type benchmarkBatchConn struct {
readBatches [][]benchmarkBatchPacket
readIndex int
}
func newBenchmarkBatchConn(readBatches [][]benchmarkBatchPacket) *benchmarkBatchConn {
return &benchmarkBatchConn{readBatches: readBatches}
}
func (c *benchmarkBatchConn) ResetRead() {
c.readIndex = 0
}
func (c *benchmarkBatchConn) ReadBatch(ms []ipv4.Message, flags int) (int, error) {
if len(c.readBatches) == 0 {
return 0, io.EOF
}
batch := c.readBatches[c.readIndex]
c.readIndex++
if c.readIndex == len(c.readBatches) {
c.readIndex = 0
}
for i := range batch {
packet := batch[i]
msg := &ms[i]
msg.N = copy(msg.Buffers[0], packet.payload)
if cap(msg.OOB) < len(packet.oob) {
panic("benchmark batch OOB buffer too small")
}
msg.OOB = msg.OOB[:len(packet.oob)]
copy(msg.OOB, packet.oob)
msg.NN = len(packet.oob)
msg.Addr = packet.addr
}
for i := len(batch); i < len(ms); i++ {
ms[i].N = 0
ms[i].NN = 0
ms[i].Addr = nil
ms[i].OOB = ms[i].OOB[:0]
}
return len(batch), nil
}
func (c *benchmarkBatchConn) WriteBatch(ms []ipv4.Message, flags int) (int, error) {
return len(ms), nil
}
func benchmarkBatchPackets(payloads [][]byte) []benchmarkBatchPacket {
packets := make([]benchmarkBatchPacket, len(payloads))
for i, payload := range payloads {
packets[i] = benchmarkBatchPacket{
payload: payload,
addr: benchmarkUDPAddr,
}
}
return packets
}
func benchmarkNewBatchReadMessages(count int, oobCap int) []ipv4.Message {
msgs := make([]ipv4.Message, count)
for i := range msgs {
msgs[i].Buffers = make([][]byte, 1)
msgs[i].Buffers[0] = make([]byte, benchmarkMaxPacketSize)
msgs[i].OOB = make([]byte, 0, oobCap)
}
return msgs
}
type benchmarkBatchWriteFixture struct {
msgs []ipv4.Message
payloads [][]byte
addr *net.UDPAddr
}
func newBenchmarkBatchWriteFixture(payloads [][]byte, oobCap int) *benchmarkBatchWriteFixture {
fixture := &benchmarkBatchWriteFixture{
msgs: make([]ipv4.Message, len(payloads)),
payloads: payloads,
addr: benchmarkUDPAddr,
}
for i := range fixture.msgs {
fixture.msgs[i].Buffers = make([][]byte, 1)
fixture.msgs[i].OOB = make([]byte, 0, oobCap)
}
fixture.Reset()
return fixture
}
func (f *benchmarkBatchWriteFixture) Reset() {
for i, payload := range f.payloads {
f.msgs[i].Buffers[0] = payload
f.msgs[i].N = len(payload)
f.msgs[i].NN = 0
f.msgs[i].Addr = f.addr
f.msgs[i].OOB = f.msgs[i].OOB[:0]
}
}
+901
View File
@@ -0,0 +1,901 @@
package conceal
import "testing"
func BenchmarkTCPRawConn(b *testing.B) {
benchmarkUseDeterministicRand(b)
if benchmarkFullMatrixEnabled() {
benchmarkTCPRawConnFull(b)
return
}
readPayloads := [][]byte{
benchmarkPayloads.initiation,
benchmarkPayloads.transportSmall,
}
b.Run("Read/mixed", func(b *testing.B) {
conn := newBenchmarkStreamChunksConn(readPayloads)
buf := make([]byte, benchmarkMaxPacketSize)
benchmarkRunLoop(b, benchmarkAverageBytes(readPayloads...), nil, func() error {
_, err := conn.Read(buf)
return err
})
})
writePayloads := [][]byte{
benchmarkPayloads.initiation,
benchmarkPayloads.transportSmall,
benchmarkPayloads.transportMTU,
}
b.Run("Write/mixed", func(b *testing.B) {
conn := newBenchmarkStreamConn(nil)
next := 0
benchmarkRunLoop(b, benchmarkAverageBytes(writePayloads...), nil, func() error {
payload := writePayloads[next]
next++
if next == len(writePayloads) {
next = 0
}
_, err := conn.Write(payload)
return err
})
})
}
func benchmarkTCPRawConnFull(b *testing.B) {
readCases := []struct {
name string
payload []byte
}{
{name: "Read/initiation", payload: benchmarkPayloads.initiation},
{name: "Read/transport_small", payload: benchmarkPayloads.transportSmall},
}
for _, tc := range readCases {
b.Run(tc.name, func(b *testing.B) {
conn := newBenchmarkStreamConn(tc.payload)
buf := make([]byte, benchmarkMaxPacketSize)
benchmarkRunLoop(b, len(tc.payload), nil, func() error {
_, err := conn.Read(buf)
return err
})
})
}
writeCases := []struct {
name string
payload []byte
}{
{name: "Write/initiation", payload: benchmarkPayloads.initiation},
{name: "Write/transport_small", payload: benchmarkPayloads.transportSmall},
{name: "Write/transport_mtu", payload: benchmarkPayloads.transportMTU},
}
for _, tc := range writeCases {
b.Run(tc.name, func(b *testing.B) {
conn := newBenchmarkStreamConn(nil)
benchmarkRunLoop(b, len(tc.payload), nil, func() error {
_, err := conn.Write(tc.payload)
return err
})
})
}
}
func BenchmarkTCPRecordMasquerade(b *testing.B) {
benchmarkUseDeterministicRand(b)
if benchmarkFullMatrixEnabled() {
benchmarkTCPRecordMasqueradeFull(b)
return
}
readPayloads := [][]byte{
benchmarkPayloads.initiation,
benchmarkPayloads.transportSmall,
}
readEncoded := make([][]byte, len(readPayloads))
for i, payload := range readPayloads {
readEncoded[i] = benchmarkEncodeMasqueradeRecord(benchmarkMasqueradeRules, payload)
}
b.Run("ReadRecord/mixed", func(b *testing.B) {
source := newBenchmarkStreamChunksConn(readEncoded)
pool := benchmarkNewBufferPool()
conn, ok := NewMasqueradeConn(source, pool, MasqueradeOpts{
RulesIn: benchmarkMasqueradeRules,
RulesOut: benchmarkMasqueradeRules,
})
if !ok {
b.Fatal("expected stream masquerade benchmark conn")
}
buf := make([]byte, benchmarkMaxPacketSize)
benchmarkRunLoop(b, benchmarkAverageBytes(readPayloads...), nil, func() error {
_, err := conn.ReadRecord(buf)
return err
})
})
writePayloads := [][]byte{
benchmarkPayloads.initiation,
benchmarkPayloads.transportSmall,
benchmarkPayloads.transportMTU,
}
b.Run("WriteRecord/mixed", func(b *testing.B) {
sink := newBenchmarkStreamConn(nil)
pool := benchmarkNewBufferPool()
conn, ok := NewMasqueradeConn(sink, pool, MasqueradeOpts{
RulesIn: benchmarkMasqueradeRules,
RulesOut: benchmarkMasqueradeRules,
})
if !ok {
b.Fatal("expected stream masquerade benchmark conn")
}
next := 0
benchmarkRunLoop(b, benchmarkAverageBytes(writePayloads...), nil, func() error {
payload := writePayloads[next]
next++
if next == len(writePayloads) {
next = 0
}
_, err := conn.WriteRecord(payload)
return err
})
})
}
func benchmarkTCPRecordMasqueradeFull(b *testing.B) {
readCases := []struct {
name string
payload []byte
}{
{name: "ReadRecord/initiation", payload: benchmarkPayloads.initiation},
{name: "ReadRecord/transport_small", payload: benchmarkPayloads.transportSmall},
}
for _, tc := range readCases {
b.Run(tc.name, func(b *testing.B) {
source := newBenchmarkStreamConn(benchmarkEncodeStreamRecords(benchmarkMasqueradeRules, tc.payload))
pool := benchmarkNewBufferPool()
conn, ok := NewMasqueradeConn(source, pool, MasqueradeOpts{
RulesIn: benchmarkMasqueradeRules,
RulesOut: benchmarkMasqueradeRules,
})
if !ok {
b.Fatal("expected stream masquerade benchmark conn")
}
buf := make([]byte, benchmarkMaxPacketSize)
benchmarkRunLoop(b, len(tc.payload), nil, func() error {
_, err := conn.ReadRecord(buf)
return err
})
})
}
writeCases := []struct {
name string
payload []byte
}{
{name: "WriteRecord/initiation", payload: benchmarkPayloads.initiation},
{name: "WriteRecord/transport_small", payload: benchmarkPayloads.transportSmall},
{name: "WriteRecord/transport_mtu", payload: benchmarkPayloads.transportMTU},
}
for _, tc := range writeCases {
b.Run(tc.name, func(b *testing.B) {
sink := newBenchmarkStreamConn(nil)
pool := benchmarkNewBufferPool()
conn, ok := NewMasqueradeConn(sink, pool, MasqueradeOpts{
RulesIn: benchmarkMasqueradeRules,
RulesOut: benchmarkMasqueradeRules,
})
if !ok {
b.Fatal("expected stream masquerade benchmark conn")
}
benchmarkRunLoop(b, len(tc.payload), nil, func() error {
_, err := conn.WriteRecord(tc.payload)
return err
})
})
}
}
func BenchmarkTCPFramed(b *testing.B) {
benchmarkUseDeterministicRand(b)
if benchmarkFullMatrixEnabled() {
benchmarkTCPFramedFull(b)
return
}
compatOffPayloads := [][]byte{
benchmarkPayloads.initiation,
benchmarkPayloads.response,
benchmarkPayloads.cookie,
benchmarkPayloads.transportKeepalive,
benchmarkPayloads.transportMTU,
}
compatOffEncoded := make([][]byte, len(compatOffPayloads))
for i, payload := range compatOffPayloads {
compatOffEncoded[i] = benchmarkEncodeFramedRecord(benchmarkFramedOpts, payload)
}
b.Run("Read/compat_off/mixed", func(b *testing.B) {
source := newBenchmarkStreamChunksConn(compatOffEncoded)
pool := benchmarkNewBufferPool()
conn, ok := NewFramedConn(source, pool, benchmarkFramedOpts)
if !ok {
b.Fatal("expected framed benchmark conn")
}
buf := make([]byte, benchmarkMaxPacketSize)
benchmarkRunLoop(b, benchmarkAverageBytes(compatOffPayloads...), nil, func() error {
_, err := conn.Read(buf)
return err
})
})
b.Run("Write/compat_off/mixed", func(b *testing.B) {
sink := newBenchmarkStreamConn(nil)
pool := benchmarkNewBufferPool()
conn, ok := NewFramedConn(sink, pool, benchmarkFramedOpts)
if !ok {
b.Fatal("expected framed benchmark conn")
}
next := 0
benchmarkRunLoop(b, benchmarkAverageBytes(compatOffPayloads...), nil, func() error {
payload := compatOffPayloads[next]
next++
if next == len(compatOffPayloads) {
next = 0
}
_, err := conn.Write(payload)
return err
})
})
compatOnPayloads := [][]byte{
benchmarkPayloads.compatInitiation,
benchmarkPayloads.compatTransportMTU,
}
compatOnEncoded := make([][]byte, len(compatOnPayloads))
for i, payload := range compatOnPayloads {
compatOnEncoded[i] = benchmarkEncodeFramedRecord(benchmarkFramedCompatOpts, payload)
}
b.Run("Read/compat_on/mixed", func(b *testing.B) {
source := newBenchmarkStreamChunksConn(compatOnEncoded)
pool := benchmarkNewBufferPool()
conn, ok := NewFramedConn(source, pool, benchmarkFramedCompatOpts)
if !ok {
b.Fatal("expected compat framed benchmark conn")
}
buf := make([]byte, benchmarkMaxPacketSize)
benchmarkRunLoop(b, benchmarkAverageBytes(compatOnPayloads...), nil, func() error {
_, err := conn.Read(buf)
return err
})
})
b.Run("Write/compat_on/mixed", func(b *testing.B) {
sink := newBenchmarkStreamConn(nil)
pool := benchmarkNewBufferPool()
conn, ok := NewFramedConn(sink, pool, benchmarkFramedCompatOpts)
if !ok {
b.Fatal("expected compat framed benchmark conn")
}
next := 0
benchmarkRunLoop(b, benchmarkAverageBytes(compatOnPayloads...), nil, func() error {
payload := compatOnPayloads[next]
next++
if next == len(compatOnPayloads) {
next = 0
}
_, err := conn.Write(payload)
return err
})
})
}
func benchmarkTCPFramedFull(b *testing.B) {
readCompatOff := []struct {
name string
payload []byte
}{
{name: "Read/compat_off/initiation", payload: benchmarkPayloads.initiation},
{name: "Read/compat_off/response", payload: benchmarkPayloads.response},
{name: "Read/compat_off/cookie", payload: benchmarkPayloads.cookie},
{name: "Read/compat_off/transport_keepalive", payload: benchmarkPayloads.transportKeepalive},
{name: "Read/compat_off/transport_mtu", payload: benchmarkPayloads.transportMTU},
}
for _, tc := range readCompatOff {
b.Run(tc.name, func(b *testing.B) {
source := newBenchmarkStreamConn(benchmarkEncodeFramedRecord(benchmarkFramedOpts, tc.payload))
pool := benchmarkNewBufferPool()
conn, ok := NewFramedConn(source, pool, benchmarkFramedOpts)
if !ok {
b.Fatal("expected framed benchmark conn")
}
buf := make([]byte, benchmarkMaxPacketSize)
benchmarkRunLoop(b, len(tc.payload), nil, func() error {
_, err := conn.Read(buf)
return err
})
})
}
writeCompatOff := []struct {
name string
payload []byte
}{
{name: "Write/compat_off/initiation", payload: benchmarkPayloads.initiation},
{name: "Write/compat_off/response", payload: benchmarkPayloads.response},
{name: "Write/compat_off/cookie", payload: benchmarkPayloads.cookie},
{name: "Write/compat_off/transport_keepalive", payload: benchmarkPayloads.transportKeepalive},
{name: "Write/compat_off/transport_mtu", payload: benchmarkPayloads.transportMTU},
}
for _, tc := range writeCompatOff {
b.Run(tc.name, func(b *testing.B) {
sink := newBenchmarkStreamConn(nil)
pool := benchmarkNewBufferPool()
conn, ok := NewFramedConn(sink, pool, benchmarkFramedOpts)
if !ok {
b.Fatal("expected framed benchmark conn")
}
benchmarkRunLoop(b, len(tc.payload), nil, func() error {
_, err := conn.Write(tc.payload)
return err
})
})
}
readCompatOn := []struct {
name string
payload []byte
}{
{name: "Read/compat_on/initiation", payload: benchmarkPayloads.compatInitiation},
{name: "Read/compat_on/transport_mtu", payload: benchmarkPayloads.compatTransportMTU},
}
for _, tc := range readCompatOn {
b.Run(tc.name, func(b *testing.B) {
source := newBenchmarkStreamConn(benchmarkEncodeFramedRecord(benchmarkFramedCompatOpts, tc.payload))
pool := benchmarkNewBufferPool()
conn, ok := NewFramedConn(source, pool, benchmarkFramedCompatOpts)
if !ok {
b.Fatal("expected compat framed benchmark conn")
}
buf := make([]byte, benchmarkMaxPacketSize)
benchmarkRunLoop(b, len(tc.payload), nil, func() error {
_, err := conn.Read(buf)
return err
})
})
}
writeCompatOn := []struct {
name string
payload []byte
}{
{name: "Write/compat_on/initiation", payload: benchmarkPayloads.compatInitiation},
{name: "Write/compat_on/transport_mtu", payload: benchmarkPayloads.compatTransportMTU},
}
for _, tc := range writeCompatOn {
b.Run(tc.name, func(b *testing.B) {
sink := newBenchmarkStreamConn(nil)
pool := benchmarkNewBufferPool()
conn, ok := NewFramedConn(sink, pool, benchmarkFramedCompatOpts)
if !ok {
b.Fatal("expected compat framed benchmark conn")
}
benchmarkRunLoop(b, len(tc.payload), nil, func() error {
_, err := conn.Write(tc.payload)
return err
})
})
}
}
func BenchmarkTCPPrelude(b *testing.B) {
benchmarkUseDeterministicRand(b)
if benchmarkFullMatrixEnabled() {
benchmarkTCPPreludeFull(b)
return
}
b.Run("ReadCold/mixed_decoys_then_initiation", func(b *testing.B) {
fixtures := make([]*benchmarkTCPPreludeReadFixture, benchmarkFixtureRingSize)
for i := range fixtures {
decoys := [][]byte{{0xaa}}
if i%2 == 1 {
decoys = [][]byte{{0xaa}, {0xab}, {0xac}, {0xad}, {0xae}}
}
fixtures[i] = newBenchmarkTCPPreludeReadFixture(decoys)
}
benchmarkRunLoopWithFixtureRing(b, len(benchmarkPayloads.initiation), fixtures, func(f *benchmarkTCPPreludeReadFixture) {
f.Reset()
}, func(f *benchmarkTCPPreludeReadFixture) error {
return f.Read()
})
})
b.Run("ReadHot/transport_small", func(b *testing.B) {
pool := benchmarkNewBufferPool()
source := newBenchmarkStreamConn(benchmarkEncodeStreamRecords(
benchmarkMasqueradeRules,
benchmarkEncodeFramedRecord(benchmarkFramedOpts, benchmarkPayloads.transportSmall),
))
recordConn, ok := NewMasqueradeConn(source, pool, MasqueradeOpts{
RulesIn: benchmarkMasqueradeRules,
RulesOut: benchmarkMasqueradeRules,
})
if !ok {
b.Fatal("expected record benchmark conn")
}
prelude, ok := NewPreludeConn(recordConn, pool, benchmarkFramedOpts, benchmarkPreludeOneRule)
if !ok {
b.Fatal("expected prelude benchmark conn")
}
prelude.seenValid = true
buf := make([]byte, benchmarkMaxPacketSize)
benchmarkRunLoop(b, len(benchmarkPayloads.transportSmall), nil, func() error {
_, err := prelude.Read(buf)
return err
})
})
b.Run("Write/initiation_mixed", func(b *testing.B) {
fixtures := make([]*benchmarkTCPPreludeWriteFixture, benchmarkFixtureRingSize)
for i := range fixtures {
opts := benchmarkPreludeOneRule
if i%2 == 1 {
opts = benchmarkPreludeFiveRules
}
fixtures[i] = newBenchmarkTCPPreludeWriteFixture(
opts,
benchmarkEncodeFramedRecord(benchmarkFramedOpts, benchmarkPayloads.initiation),
)
}
benchmarkRunLoopWithFixtureRing(b, len(benchmarkPayloads.initiation), fixtures, nil, func(f *benchmarkTCPPreludeWriteFixture) error {
return f.Write()
})
})
b.Run("Write/transport_small/passthrough", func(b *testing.B) {
sink := newBenchmarkStreamConn(nil)
pool := benchmarkNewBufferPool()
recordConn, ok := NewMasqueradeConn(sink, pool, MasqueradeOpts{
RulesIn: benchmarkMasqueradeRules,
RulesOut: benchmarkMasqueradeRules,
})
if !ok {
b.Fatal("expected record benchmark conn")
}
prelude, ok := NewPreludeConn(recordConn, pool, benchmarkFramedOpts, benchmarkPreludeOneRule)
if !ok {
b.Fatal("expected prelude benchmark conn")
}
payload := benchmarkEncodeFramedRecord(benchmarkFramedOpts, benchmarkPayloads.transportSmall)
benchmarkRunLoop(b, len(payload), nil, func() error {
_, err := prelude.Write(payload)
return err
})
})
}
func benchmarkTCPPreludeFull(b *testing.B) {
b.Run("ReadCold/decoy1_then_initiation", func(b *testing.B) {
pool := benchmarkNewBufferPool()
source := newBenchmarkStreamConn(benchmarkEncodeStreamRecords(
benchmarkMasqueradeRules,
[]byte{0xaa},
benchmarkEncodeFramedRecord(benchmarkFramedOpts, benchmarkPayloads.initiation),
))
recordConn, ok := NewMasqueradeConn(source, pool, MasqueradeOpts{
RulesIn: benchmarkMasqueradeRules,
RulesOut: benchmarkMasqueradeRules,
})
if !ok {
b.Fatal("expected record benchmark conn")
}
prelude, ok := NewPreludeConn(recordConn, pool, benchmarkFramedOpts, benchmarkPreludeOneRule)
if !ok {
b.Fatal("expected prelude benchmark conn")
}
buf := make([]byte, benchmarkMaxPacketSize)
reset := func() {
source.ResetRead()
prelude.seenValid = false
}
benchmarkRunLoop(b, len(benchmarkPayloads.initiation), reset, func() error {
_, err := prelude.Read(buf)
return err
})
})
b.Run("ReadCold/decoy5_then_initiation", func(b *testing.B) {
pool := benchmarkNewBufferPool()
source := newBenchmarkStreamConn(benchmarkEncodeStreamRecords(
benchmarkMasqueradeRules,
[]byte{0xaa},
[]byte{0xab},
[]byte{0xac},
[]byte{0xad},
[]byte{0xae},
benchmarkEncodeFramedRecord(benchmarkFramedOpts, benchmarkPayloads.initiation),
))
recordConn, ok := NewMasqueradeConn(source, pool, MasqueradeOpts{
RulesIn: benchmarkMasqueradeRules,
RulesOut: benchmarkMasqueradeRules,
})
if !ok {
b.Fatal("expected record benchmark conn")
}
prelude, ok := NewPreludeConn(recordConn, pool, benchmarkFramedOpts, benchmarkPreludeOneRule)
if !ok {
b.Fatal("expected prelude benchmark conn")
}
buf := make([]byte, benchmarkMaxPacketSize)
reset := func() {
source.ResetRead()
prelude.seenValid = false
}
benchmarkRunLoop(b, len(benchmarkPayloads.initiation), reset, func() error {
_, err := prelude.Read(buf)
return err
})
})
b.Run("ReadHot/transport_small", func(b *testing.B) {
pool := benchmarkNewBufferPool()
source := newBenchmarkStreamConn(benchmarkEncodeStreamRecords(
benchmarkMasqueradeRules,
benchmarkEncodeFramedRecord(benchmarkFramedOpts, benchmarkPayloads.transportSmall),
))
recordConn, ok := NewMasqueradeConn(source, pool, MasqueradeOpts{
RulesIn: benchmarkMasqueradeRules,
RulesOut: benchmarkMasqueradeRules,
})
if !ok {
b.Fatal("expected record benchmark conn")
}
prelude, ok := NewPreludeConn(recordConn, pool, benchmarkFramedOpts, benchmarkPreludeOneRule)
if !ok {
b.Fatal("expected prelude benchmark conn")
}
prelude.seenValid = true
buf := make([]byte, benchmarkMaxPacketSize)
benchmarkRunLoop(b, len(benchmarkPayloads.transportSmall), nil, func() error {
_, err := prelude.Read(buf)
return err
})
})
writeCases := []struct {
name string
payload []byte
opts PreludeOpts
}{
{
name: "Write/initiation/1_rule",
payload: benchmarkEncodeFramedRecord(benchmarkFramedOpts, benchmarkPayloads.initiation),
opts: benchmarkPreludeOneRule,
},
{
name: "Write/initiation/5_rules",
payload: benchmarkEncodeFramedRecord(benchmarkFramedOpts, benchmarkPayloads.initiation),
opts: benchmarkPreludeFiveRules,
},
{
name: "Write/transport_small/passthrough",
payload: benchmarkEncodeFramedRecord(benchmarkFramedOpts, benchmarkPayloads.transportSmall),
opts: benchmarkPreludeOneRule,
},
}
for _, tc := range writeCases {
b.Run(tc.name, func(b *testing.B) {
sink := newBenchmarkStreamConn(nil)
pool := benchmarkNewBufferPool()
recordConn, ok := NewMasqueradeConn(sink, pool, MasqueradeOpts{
RulesIn: benchmarkMasqueradeRules,
RulesOut: benchmarkMasqueradeRules,
})
if !ok {
b.Fatal("expected record benchmark conn")
}
prelude, ok := NewPreludeConn(recordConn, pool, benchmarkFramedOpts, tc.opts)
if !ok {
b.Fatal("expected prelude benchmark conn")
}
benchmarkRunLoop(b, len(tc.payload), nil, func() error {
_, err := prelude.Write(tc.payload)
return err
})
})
}
}
func BenchmarkTCPPipeline(b *testing.B) {
benchmarkUseDeterministicRand(b)
if benchmarkFullMatrixEnabled() {
benchmarkTCPPipelineFull(b)
return
}
b.Run("Read/full_pipeline/cold_initiation", func(b *testing.B) {
fixtures := make([]*benchmarkTCPPipelineReadFixture, benchmarkFixtureRingSize)
for i := range fixtures {
fixtures[i] = newBenchmarkTCPPipelineReadFixture([][]byte{{0xaa, 0xbb}})
}
benchmarkRunLoopWithFixtureRing(b, len(benchmarkPayloads.initiation), fixtures, func(f *benchmarkTCPPipelineReadFixture) {
f.Reset()
}, func(f *benchmarkTCPPipelineReadFixture) error {
return f.Read()
})
})
b.Run("Read/full_pipeline/hot_transport_small", func(b *testing.B) {
pool := benchmarkNewBufferPool()
source := newBenchmarkStreamConn(benchmarkEncodeStreamRecords(
benchmarkMasqueradeRules,
benchmarkEncodeFramedRecord(benchmarkFramedOpts, benchmarkPayloads.transportSmall),
))
recordConn, ok := NewMasqueradeConn(source, pool, MasqueradeOpts{
RulesIn: benchmarkMasqueradeRules,
RulesOut: benchmarkMasqueradeRules,
})
if !ok {
b.Fatal("expected record benchmark conn")
}
prelude, ok := NewPreludeConn(recordConn, pool, benchmarkFramedOpts, benchmarkPreludeOneRule)
if !ok {
b.Fatal("expected prelude benchmark conn")
}
prelude.seenValid = true
conn, ok := NewFramedConn(prelude, pool, benchmarkFramedOpts)
if !ok {
b.Fatal("expected pipeline benchmark conn")
}
buf := make([]byte, benchmarkMaxPacketSize)
benchmarkRunLoop(b, len(benchmarkPayloads.transportSmall), nil, func() error {
_, err := conn.Read(buf)
return err
})
})
writePayloads := [][]byte{
benchmarkPayloads.initiation,
benchmarkPayloads.transportSmall,
benchmarkPayloads.transportMTU,
}
b.Run("Write/full_pipeline/mixed", func(b *testing.B) {
sink := newBenchmarkStreamConn(nil)
pool := benchmarkNewBufferPool()
recordConn, ok := NewMasqueradeConn(sink, pool, MasqueradeOpts{
RulesIn: benchmarkMasqueradeRules,
RulesOut: benchmarkMasqueradeRules,
})
if !ok {
b.Fatal("expected record benchmark conn")
}
prelude, ok := NewPreludeConn(recordConn, pool, benchmarkFramedOpts, benchmarkPreludeOneRule)
if !ok {
b.Fatal("expected prelude benchmark conn")
}
conn, ok := NewFramedConn(prelude, pool, benchmarkFramedOpts)
if !ok {
b.Fatal("expected pipeline benchmark conn")
}
next := 0
benchmarkRunLoop(b, benchmarkAverageBytes(writePayloads...), nil, func() error {
payload := writePayloads[next]
next++
if next == len(writePayloads) {
next = 0
}
_, err := conn.Write(payload)
return err
})
})
}
func benchmarkTCPPipelineFull(b *testing.B) {
b.Run("Read/full_pipeline/cold_initiation", func(b *testing.B) {
pool := benchmarkNewBufferPool()
source := newBenchmarkStreamConn(benchmarkEncodeStreamRecords(
benchmarkMasqueradeRules,
[]byte{0xaa, 0xbb},
benchmarkEncodeFramedRecord(benchmarkFramedOpts, benchmarkPayloads.initiation),
))
recordConn, ok := NewMasqueradeConn(source, pool, MasqueradeOpts{
RulesIn: benchmarkMasqueradeRules,
RulesOut: benchmarkMasqueradeRules,
})
if !ok {
b.Fatal("expected record benchmark conn")
}
prelude, ok := NewPreludeConn(recordConn, pool, benchmarkFramedOpts, benchmarkPreludeOneRule)
if !ok {
b.Fatal("expected prelude benchmark conn")
}
conn, ok := NewFramedConn(prelude, pool, benchmarkFramedOpts)
if !ok {
b.Fatal("expected pipeline benchmark conn")
}
buf := make([]byte, benchmarkMaxPacketSize)
reset := func() {
source.ResetRead()
prelude.seenValid = false
}
benchmarkRunLoop(b, len(benchmarkPayloads.initiation), reset, func() error {
_, err := conn.Read(buf)
return err
})
})
b.Run("Read/full_pipeline/hot_transport_small", func(b *testing.B) {
pool := benchmarkNewBufferPool()
source := newBenchmarkStreamConn(benchmarkEncodeStreamRecords(
benchmarkMasqueradeRules,
benchmarkEncodeFramedRecord(benchmarkFramedOpts, benchmarkPayloads.transportSmall),
))
recordConn, ok := NewMasqueradeConn(source, pool, MasqueradeOpts{
RulesIn: benchmarkMasqueradeRules,
RulesOut: benchmarkMasqueradeRules,
})
if !ok {
b.Fatal("expected record benchmark conn")
}
prelude, ok := NewPreludeConn(recordConn, pool, benchmarkFramedOpts, benchmarkPreludeOneRule)
if !ok {
b.Fatal("expected prelude benchmark conn")
}
prelude.seenValid = true
conn, ok := NewFramedConn(prelude, pool, benchmarkFramedOpts)
if !ok {
b.Fatal("expected pipeline benchmark conn")
}
buf := make([]byte, benchmarkMaxPacketSize)
benchmarkRunLoop(b, len(benchmarkPayloads.transportSmall), nil, func() error {
_, err := conn.Read(buf)
return err
})
})
writeCases := []struct {
name string
payload []byte
}{
{name: "Write/full_pipeline/initiation", payload: benchmarkPayloads.initiation},
{name: "Write/full_pipeline/transport_small", payload: benchmarkPayloads.transportSmall},
{name: "Write/full_pipeline/transport_mtu", payload: benchmarkPayloads.transportMTU},
}
for _, tc := range writeCases {
b.Run(tc.name, func(b *testing.B) {
sink := newBenchmarkStreamConn(nil)
pool := benchmarkNewBufferPool()
recordConn, ok := NewMasqueradeConn(sink, pool, MasqueradeOpts{
RulesIn: benchmarkMasqueradeRules,
RulesOut: benchmarkMasqueradeRules,
})
if !ok {
b.Fatal("expected record benchmark conn")
}
prelude, ok := NewPreludeConn(recordConn, pool, benchmarkFramedOpts, benchmarkPreludeOneRule)
if !ok {
b.Fatal("expected prelude benchmark conn")
}
conn, ok := NewFramedConn(prelude, pool, benchmarkFramedOpts)
if !ok {
b.Fatal("expected pipeline benchmark conn")
}
benchmarkRunLoop(b, len(tc.payload), nil, func() error {
_, err := conn.Write(tc.payload)
return err
})
})
}
}
type benchmarkTCPPreludeReadFixture struct {
source *benchmarkStreamConn
prelude *PreludeConn
buf []byte
}
func newBenchmarkTCPPreludeReadFixture(decoys [][]byte) *benchmarkTCPPreludeReadFixture {
pool := benchmarkNewBufferPool()
records := make([][]byte, 0, len(decoys)+1)
records = append(records, decoys...)
records = append(records, benchmarkEncodeFramedRecord(benchmarkFramedOpts, benchmarkPayloads.initiation))
source := newBenchmarkStreamConn(benchmarkEncodeStreamRecords(benchmarkMasqueradeRules, records...))
recordConn, ok := NewMasqueradeConn(source, pool, MasqueradeOpts{
RulesIn: benchmarkMasqueradeRules,
RulesOut: benchmarkMasqueradeRules,
})
if !ok {
panic("expected record benchmark conn")
}
prelude, ok := NewPreludeConn(recordConn, pool, benchmarkFramedOpts, benchmarkPreludeOneRule)
if !ok {
panic("expected prelude benchmark conn")
}
return &benchmarkTCPPreludeReadFixture{
source: source,
prelude: prelude,
buf: make([]byte, benchmarkMaxPacketSize),
}
}
func (f *benchmarkTCPPreludeReadFixture) Reset() {
f.source.ResetRead()
f.prelude.seenValid = false
}
func (f *benchmarkTCPPreludeReadFixture) Read() error {
_, err := f.prelude.Read(f.buf)
return err
}
type benchmarkTCPPreludeWriteFixture struct {
prelude *PreludeConn
payload []byte
}
func newBenchmarkTCPPreludeWriteFixture(opts PreludeOpts, payload []byte) *benchmarkTCPPreludeWriteFixture {
sink := newBenchmarkStreamConn(nil)
pool := benchmarkNewBufferPool()
recordConn, ok := NewMasqueradeConn(sink, pool, MasqueradeOpts{
RulesIn: benchmarkMasqueradeRules,
RulesOut: benchmarkMasqueradeRules,
})
if !ok {
panic("expected record benchmark conn")
}
prelude, ok := NewPreludeConn(recordConn, pool, benchmarkFramedOpts, opts)
if !ok {
panic("expected prelude benchmark conn")
}
return &benchmarkTCPPreludeWriteFixture{
prelude: prelude,
payload: payload,
}
}
func (f *benchmarkTCPPreludeWriteFixture) Write() error {
_, err := f.prelude.Write(f.payload)
return err
}
type benchmarkTCPPipelineReadFixture struct {
source *benchmarkStreamConn
prelude *PreludeConn
conn *FramedConn
buf []byte
}
func newBenchmarkTCPPipelineReadFixture(decoys [][]byte) *benchmarkTCPPipelineReadFixture {
pool := benchmarkNewBufferPool()
records := make([][]byte, 0, len(decoys)+1)
records = append(records, decoys...)
records = append(records, benchmarkEncodeFramedRecord(benchmarkFramedOpts, benchmarkPayloads.initiation))
source := newBenchmarkStreamConn(benchmarkEncodeStreamRecords(benchmarkMasqueradeRules, records...))
recordConn, ok := NewMasqueradeConn(source, pool, MasqueradeOpts{
RulesIn: benchmarkMasqueradeRules,
RulesOut: benchmarkMasqueradeRules,
})
if !ok {
panic("expected record benchmark conn")
}
prelude, ok := NewPreludeConn(recordConn, pool, benchmarkFramedOpts, benchmarkPreludeOneRule)
if !ok {
panic("expected prelude benchmark conn")
}
conn, ok := NewFramedConn(prelude, pool, benchmarkFramedOpts)
if !ok {
panic("expected pipeline benchmark conn")
}
return &benchmarkTCPPipelineReadFixture{
source: source,
prelude: prelude,
conn: conn,
buf: make([]byte, benchmarkMaxPacketSize),
}
}
func (f *benchmarkTCPPipelineReadFixture) Reset() {
f.source.ResetRead()
f.prelude.seenValid = false
}
func (f *benchmarkTCPPipelineReadFixture) Read() error {
_, err := f.conn.Read(f.buf)
return err
}
File diff suppressed because it is too large. Load diff
+79
View File
@@ -0,0 +1,79 @@
package conceal
import "sync"
func WrapFlexBuffer(buf []byte) *FlexBuffer {
return &FlexBuffer{
buf: buf,
}
}
type FlexBuffer struct {
buf []byte
offset int
len int
}
func (b *FlexBuffer) PushTail(size int) []byte {
newLen := b.len + size
if b.offset+newLen > len(b.buf) {
return nil
}
oldLen := b.len
b.len = newLen
return b.buf[b.offset+oldLen : b.offset+newLen]
}
func (b *FlexBuffer) PullTail(size int) []byte {
newLen := b.len - size
if newLen < 0 {
return nil
}
oldLen := b.len
b.len = newLen
return b.buf[b.offset+newLen : b.offset+oldLen]
}
func (b *FlexBuffer) PullHead(size int) []byte {
if size == -1 {
size = len(b.buf)
}
newOffset := b.offset + size
if newOffset+b.len > len(b.buf) {
return nil
}
oldOffset := b.offset
b.offset = newOffset
return b.buf[oldOffset+b.len : newOffset+b.len]
}
func (b *FlexBuffer) Cap() int {
return len(b.buf)
}
func (b *FlexBuffer) Len() int {
return b.len
}
func WrapBufferPool(pool *sync.Pool) *BufferPool {
return &BufferPool{
pool: pool,
}
}
type BufferPool struct {
pool *sync.Pool
}
func (p *BufferPool) Get() []byte {
return p.pool.Get().([]byte)
}
func (p *BufferPool) Put(b []byte) {
p.pool.Put(b)
}
+23
View File
@@ -0,0 +1,23 @@
package conceal
import (
"net"
"syscall"
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
)
type UDPConn interface {
net.PacketConn
SyscallConn() (syscall.RawConn, error)
ReadMsgUDP(b, oob []byte) (n, oobn, flags int, addr *net.UDPAddr, err error)
WriteMsgUDP(b, oob []byte, addr *net.UDPAddr) (n, oobn int, err error)
}
var _ ipv4.Message = ipv6.Message{}
type BatchConn interface {
ReadBatch(ms []ipv4.Message, flags int) (n int, err error)
WriteBatch(ms []ipv4.Message, flags int) (n int, err error)
}
+391
View File
@@ -0,0 +1,391 @@
package conceal
import (
"crypto/rand"
"encoding/binary"
"net"
"sync"
"golang.org/x/net/ipv4"
)
type FramedOpts struct {
H1 *RangedHeader
H2 *RangedHeader
H3 *RangedHeader
H4 *RangedHeader
S1 int
S2 int
S3 int
S4 int
HeaderCompat bool
}
func (o *FramedOpts) HasIntersections() bool {
headers := []*RangedHeader{o.H1, o.H2, o.H3, o.H4}
for i := range len(headers) {
left := headers[i]
if left == nil {
continue
}
for j := i + 1; j < len(headers); j++ {
right := headers[j]
if right == nil {
continue
}
if left.start <= right.end && right.start <= left.end {
return true
}
}
}
return false
}
func newFrameEncoding(opts FramedOpts) (e frameEncoding, ok bool) {
e = frameEncoding{}
if opts.H1 != nil {
e.header.initial = *opts.H1
ok = true
} else {
e.header.initial = RangedHeader{WireguardMsgInitiationType, WireguardMsgInitiationType}
}
if opts.H2 != nil {
e.header.response = *opts.H2
ok = true
} else {
e.header.response = RangedHeader{WireguardMsgResponseType, WireguardMsgResponseType}
}
if opts.H3 != nil {
e.header.cookie = *opts.H3
ok = true
} else {
e.header.cookie = RangedHeader{WireguardMsgCookieReplyType, WireguardMsgCookieReplyType}
}
if opts.H4 != nil {
e.header.transport = *opts.H4
ok = true
} else {
e.header.transport = RangedHeader{WireguardMsgTransportType, WireguardMsgTransportType}
}
if opts.S1 != 0 {
e.padding.initial = opts.S1
ok = true
}
if opts.S2 != 0 {
e.padding.response = opts.S2
ok = true
}
if opts.S3 != 0 {
e.padding.cookie = opts.S3
ok = true
}
if opts.S4 != 0 {
e.padding.transport = opts.S4
ok = true
}
e.compat = opts.HeaderCompat
return e, ok
}
type frameEncoding struct {
header struct {
initial RangedHeader
response RangedHeader
cookie RangedHeader
transport RangedHeader
}
padding struct {
initial int
response int
cookie int
transport int
}
compat bool
}
type frameRecordKind uint8
const (
frameRecordInvalid frameRecordKind = iota
frameRecordInitiation
frameRecordResponse
frameRecordCookie
frameRecordTransport
)
func encodeOne(dst, src []byte, header RangedHeader, padding int) int {
rand.Read(dst[:padding])
dst = dst[padding:]
binary.LittleEndian.PutUint32(dst[:4], header.Generate())
dst = dst[4:]
n := copy(dst, src[4:])
return padding + 4 + n
}
func encodeOneCompat(dst, src []byte, padding int) int {
rand.Read(dst[:padding])
n := copy(dst[padding:], src)
return padding + n
}
func (e *frameEncoding) Encode(dst, src []byte) int {
if len(src) < 4 {
return 0
}
header := binary.LittleEndian.Uint32(src[:4])
if e.compat {
if e.header.initial.Validate(header) {
return encodeOneCompat(dst, src, e.padding.initial)
} else if e.header.response.Validate(header) {
return encodeOneCompat(dst, src, e.padding.response)
} else if e.header.cookie.Validate(header) {
return encodeOneCompat(dst, src, e.padding.cookie)
} else if e.header.transport.Validate(header) {
return encodeOneCompat(dst, src, e.padding.transport)
}
} else {
switch src[0] {
case WireguardMsgInitiationType:
return encodeOne(dst, src, e.header.initial, e.padding.initial)
case WireguardMsgResponseType:
return encodeOne(dst, src, e.header.response, e.padding.response)
case WireguardMsgCookieReplyType:
return encodeOne(dst, src, e.header.cookie, e.padding.cookie)
case WireguardMsgTransportType:
return encodeOne(dst, src, e.header.transport, e.padding.transport)
}
}
return 0
}
func decodeOneCompat(b []byte, header RangedHeader, padding int) int {
bb := b[padding:]
if !header.Validate(binary.LittleEndian.Uint32(bb[:4])) {
return 0
}
return copy(b, bb)
}
func decodeOne(b []byte, header RangedHeader, padding int, originalHeader uint32) int {
bb := b[padding:]
if !header.Validate(binary.LittleEndian.Uint32(bb[:4])) {
return 0
}
binary.LittleEndian.PutUint32(b[:4], originalHeader)
n := copy(b[4:], bb[4:])
return 4 + n
}
func (e *frameEncoding) matchesRecord(b []byte, header RangedHeader, padding, size int) bool {
if len(b) != size {
return false
}
if len(b) < padding+4 {
return false
}
return header.Validate(binary.LittleEndian.Uint32(b[padding : padding+4]))
}
func (e *frameEncoding) matchesTransportRecord(b []byte) bool {
if len(b) < WireguardMsgTransportMinSize+e.padding.transport {
return false
}
if len(b) < e.padding.transport+4 {
return false
}
return e.header.transport.Validate(binary.LittleEndian.Uint32(b[e.padding.transport : e.padding.transport+4]))
}
func (e *frameEncoding) recordKind(b []byte) frameRecordKind {
if e.matchesRecord(b, e.header.initial, e.padding.initial, WireguardMsgInitiationSize+e.padding.initial) {
return frameRecordInitiation
}
if e.matchesRecord(b, e.header.response, e.padding.response, WireguardMsgResponseSize+e.padding.response) {
return frameRecordResponse
}
if e.matchesRecord(b, e.header.cookie, e.padding.cookie, WireguardMsgCookieReplySize+e.padding.cookie) {
return frameRecordCookie
}
if e.matchesTransportRecord(b) {
return frameRecordTransport
}
return frameRecordInvalid
}
func (e *frameEncoding) IsValidRecord(b []byte) bool {
return e.recordKind(b) != frameRecordInvalid
}
func (e *frameEncoding) IsInitiationRecord(b []byte) bool {
return e.recordKind(b) == frameRecordInitiation
}
func (e *frameEncoding) Decode(b []byte) int {
switch e.recordKind(b) {
case frameRecordInitiation:
if e.compat {
return decodeOneCompat(b, e.header.initial, e.padding.initial)
}
return decodeOne(b, e.header.initial, e.padding.initial, WireguardMsgInitiationType)
case frameRecordResponse:
if e.compat {
return decodeOneCompat(b, e.header.response, e.padding.response)
}
return decodeOne(b, e.header.response, e.padding.response, WireguardMsgResponseType)
case frameRecordCookie:
if e.compat {
return decodeOneCompat(b, e.header.cookie, e.padding.cookie)
}
return decodeOne(b, e.header.cookie, e.padding.cookie, WireguardMsgCookieReplyType)
case frameRecordTransport:
if e.compat {
return decodeOneCompat(b, e.header.transport, e.padding.transport)
}
return decodeOne(b, e.header.transport, e.padding.transport, WireguardMsgTransportType)
default:
return len(b)
}
}
func NewFramedConn(conn net.Conn, pool *sync.Pool, opts FramedOpts) (c *FramedConn, ok bool) {
enc, ok := newFrameEncoding(opts)
if !ok {
return nil, false
}
return &FramedConn{
Conn: conn,
pool: WrapBufferPool(pool),
enc: enc,
}, true
}
type FramedConn struct {
net.Conn
pool *BufferPool
enc frameEncoding
}
func (c *FramedConn) Read(b []byte) (n int, err error) {
n, err = c.Conn.Read(b)
n = c.enc.Decode(b[:n])
return n, err
}
func (c *FramedConn) Write(b []byte) (n int, err error) {
t := c.pool.Get()
defer c.pool.Put(t)
n = c.enc.Encode(t, b)
diff := n - len(b)
n, err = c.Conn.Write(t[:n])
return max(n-diff, 0), err
}
func NewFramedUDPConn(conn UDPConn, pool *sync.Pool, opts FramedOpts) (c UDPConn, ok bool) {
enc, ok := newFrameEncoding(opts)
if !ok {
return nil, false
}
return &FramedUDPConn{
UDPConn: conn,
pool: WrapBufferPool(pool),
enc: enc,
}, true
}
type FramedUDPConn struct {
UDPConn
pool *BufferPool
enc frameEncoding
}
func (c *FramedUDPConn) ReadMsgUDP(b, oob []byte) (n, oobn, flags int, addr *net.UDPAddr, err error) {
n, oobn, flags, addr, err = c.UDPConn.ReadMsgUDP(b, oob)
if err != nil {
return 0, 0, 0, nil, err
}
n = c.enc.Decode(b[:n])
return n, oobn, flags, addr, err
}
func (c *FramedUDPConn) WriteMsgUDP(b, oob []byte, addr *net.UDPAddr) (n, oobn int, err error) {
t := c.pool.Get()
defer c.pool.Put(t)
n = c.enc.Encode(t, b)
diff := n - len(b)
n, oobn, err = c.UDPConn.WriteMsgUDP(t[:n], oob, addr)
return max(n-diff, 0), oobn, err
}
func NewFramedBatchConn(conn BatchConn, pool *sync.Pool, opts FramedOpts) (c BatchConn, ok bool) {
enc, ok := newFrameEncoding(opts)
if !ok {
return nil, false
}
return &FramedBatchConn{
BatchConn: conn,
pool: WrapBufferPool(pool),
enc: enc,
}, true
}
type FramedBatchConn struct {
BatchConn
pool *BufferPool
enc frameEncoding
}
func (c *FramedBatchConn) ReadBatch(ms []ipv4.Message, flags int) (n int, err error) {
n, err = c.BatchConn.ReadBatch(ms, flags)
if err != nil {
return 0, err
}
for i := range ms[:n] {
b := ms[i].Buffers[0][:ms[i].N]
ms[i].N = c.enc.Decode(b)
}
return n, nil
}
func (c *FramedBatchConn) WriteBatch(ms []ipv4.Message, flags int) (n int, err error) {
for i := range ms {
t := c.pool.Get()
defer c.pool.Put(t)
n = c.enc.Encode(t, ms[i].Buffers[0])
ms[i].Buffers[0] = t[:n]
}
// ms[i].N has incorrect N because the original data was modifier above
// however, WG does not check this field, so this is fine
return c.BatchConn.WriteBatch(ms, flags)
}
+221
View File
@@ -0,0 +1,221 @@
package conceal
import (
"bytes"
"net"
"sync"
"golang.org/x/net/ipv4"
)
type MasqueradeOpts struct {
RulesIn Rules
RulesOut Rules
}
func NewMasqueradeConn(conn net.Conn, pool *sync.Pool, opts MasqueradeOpts) (c *MasqueradeConn, ok bool) {
if opts.RulesIn == nil && opts.RulesOut == nil {
return nil, false
}
return &MasqueradeConn{
Conn: conn,
rulesIn: opts.RulesIn,
rulesOut: opts.RulesOut,
pool: WrapBufferPool(pool),
}, true
}
type MasqueradeConn struct {
net.Conn
rulesIn Rules
rulesOut Rules
pool *BufferPool
}
var _ StreamRecordConn = (*MasqueradeConn)(nil)
func (c *MasqueradeConn) CanReadRecord() bool {
return c.rulesIn != nil
}
func (c *MasqueradeConn) CanWriteRecord() bool {
return c.rulesOut != nil
}
func (c *MasqueradeConn) ReadRecord(b []byte) (n int, err error) {
if !c.CanReadRecord() {
return 0, ErrNoReadRecord
}
ctx := &readContext{
FlexBuffer: WrapFlexBuffer(b),
BufferPool: c.pool,
}
if err := c.rulesIn.Read(c.Conn, ctx); err != nil {
return 0, err
}
return ctx.Len(), nil
}
func (c *MasqueradeConn) WriteRecord(b []byte) (n int, err error) {
if !c.CanWriteRecord() {
return 0, ErrNoWriteRecord
}
ctx := &writeContext{
FlexBuffer: WrapFlexBuffer(b),
BufferPool: c.pool,
}
t := c.pool.Get()
defer c.pool.Put(t)
w := bytes.NewBuffer(t[:0])
if err := c.rulesOut.Write(w, ctx); err != nil {
return 0, err
}
if _, err := c.Conn.Write(w.Bytes()); err != nil {
return 0, err
}
return ctx.Len(), nil
}
func (c *MasqueradeConn) Read(b []byte) (n int, err error) {
if !c.CanReadRecord() {
return c.Conn.Read(b)
}
return c.ReadRecord(b)
}
func (c *MasqueradeConn) Write(b []byte) (n int, err error) {
if !c.CanWriteRecord() {
return c.Conn.Write(b)
}
return c.WriteRecord(b)
}
func NewMasqueradeUDPConn(conn UDPConn, pool *sync.Pool, opts MasqueradeOpts) (c *MasqueradeUDPConn, ok bool) {
if opts.RulesIn == nil && opts.RulesOut == nil {
return nil, false
}
return &MasqueradeUDPConn{
UDPConn: conn,
rulesIn: opts.RulesIn,
rulesOut: opts.RulesOut,
pool: WrapBufferPool(pool),
}, true
}
type MasqueradeUDPConn struct {
UDPConn
rulesIn Rules
rulesOut Rules
pool *BufferPool
}
func (c *MasqueradeUDPConn) ReadMsgUDP(b, oob []byte) (n, oobn, flags int, addr *net.UDPAddr, err error) {
n, oobn, flags, addr, err = c.UDPConn.ReadMsgUDP(b, oob)
if err != nil {
return n, oobn, flags, addr, err
}
r := bytes.NewBuffer(b[:n])
ctx := &readContext{
FlexBuffer: WrapFlexBuffer(b),
BufferPool: c.pool,
}
if err = c.rulesIn.Read(r, ctx); err != nil {
return 0, oobn, flags, addr, err
}
return ctx.Len(), oobn, flags, addr, err
}
func (c *MasqueradeUDPConn) WriteMsgUDP(b, oob []byte, addr *net.UDPAddr) (n, oobn int, err error) {
t := c.pool.Get()
defer c.pool.Put(t)
w := bytes.NewBuffer(t[:0])
ctx := &writeContext{
FlexBuffer: WrapFlexBuffer(b),
BufferPool: c.pool,
}
if err = c.rulesOut.Write(w, ctx); err != nil {
return 0, 0, err
}
return c.UDPConn.WriteMsgUDP(t[:ctx.Len()], oob, addr)
}
func NewMasqueradeBatchConn(conn BatchConn, bp *sync.Pool, opts MasqueradeOpts) (c *MasqueradeBatchConn, ok bool) {
if opts.RulesIn == nil && opts.RulesOut == nil {
return nil, false
}
return &MasqueradeBatchConn{
BatchConn: conn,
rulesIn: opts.RulesIn,
rulesOut: opts.RulesOut,
pool: WrapBufferPool(bp),
}, true
}
type MasqueradeBatchConn struct {
BatchConn
rulesIn Rules
rulesOut Rules
pool *BufferPool
}
func (c *MasqueradeBatchConn) ReadBatch(ms []ipv4.Message, flags int) (n int, err error) {
n, err = c.BatchConn.ReadBatch(ms, flags)
if err != nil {
return 0, err
}
for i := range n {
r := bytes.NewBuffer(ms[i].Buffers[0][:ms[i].N])
ctx := &readContext{
FlexBuffer: WrapFlexBuffer(ms[i].Buffers[0]),
BufferPool: c.pool,
}
if err = c.rulesIn.Read(r, ctx); err != nil {
return 0, err
}
ms[i].N = ctx.Len()
}
return n, nil
}
func (c *MasqueradeBatchConn) WriteBatch(ms []ipv4.Message, flags int) (n int, err error) {
for i := range ms {
t := c.pool.Get()
defer c.pool.Put(t)
w := bytes.NewBuffer(t[:0])
ctx := &writeContext{
FlexBuffer: WrapFlexBuffer(ms[i].Buffers[0]),
BufferPool: c.pool,
}
if err = c.rulesOut.Write(w, ctx); err != nil {
return 0, err
}
ms[i].Buffers[0] = w.Bytes()
}
return c.BatchConn.WriteBatch(ms, flags)
}
+334
View File
@@ -0,0 +1,334 @@
package conceal
import (
"bytes"
"crypto/rand"
"encoding/binary"
"math/big"
"net"
"sync"
"golang.org/x/net/ipv4"
)
type PreludeOpts struct {
Jc int
Jmin int
Jmax int
RulesArr [5]Rules
}
func (o PreludeOpts) HasDecoyRules() bool {
for _, rules := range o.RulesArr {
if rules != nil {
return true
}
}
return false
}
func (o PreludeOpts) IsEmpty() bool {
if o.HasDecoyRules() {
return false
}
return o.Jc == 0
}
func newJunkGenerator(min, max int) *junkGenerator {
return &junkGenerator{
rang: big.NewInt(int64(max - min + 1)),
offset: min,
}
}
type junkGenerator struct {
rang *big.Int
offset int
}
func (p *junkGenerator) generate(b []byte) []byte {
rndBig, _ := rand.Int(rand.Reader, p.rang)
n := int(rndBig.Int64()) + p.offset
junk := b[:n]
rand.Read(junk)
return junk
}
func NewPreludeUDPConn(
conn UDPConn,
origin UDPConn,
pool *sync.Pool,
header *RangedHeader,
opts PreludeOpts,
) (c *PreludeUDPConn, ok bool) {
if opts.IsEmpty() {
return nil, false
}
if opts.Jmin > opts.Jmax {
opts.Jmin, opts.Jmax = opts.Jmax, opts.Jmin
}
return &PreludeUDPConn{
UDPConn: conn,
origin: origin,
pool: WrapBufferPool(pool),
rulesArr: opts.RulesArr,
junkCount: opts.Jc,
junkGen: newJunkGenerator(opts.Jmin, opts.Jmax),
header: header,
}, true
}
type PreludeUDPConn struct {
UDPConn
origin UDPConn
pool *BufferPool
rulesArr [5]Rules
junkCount int
junkGen *junkGenerator
header *RangedHeader
}
func (c *PreludeUDPConn) WriteMsgUDP(b, oob []byte, addr *net.UDPAddr) (n, oobn int, err error) {
var isInit bool
if len(b) >= 4 {
typ := binary.LittleEndian.Uint32(b[:4])
if c.header != nil {
isInit = c.header.Validate(typ)
} else {
isInit = typ == WireguardMsgInitiationType
}
}
if isInit {
b := c.pool.Get()
defer c.pool.Put(b)
ctx := &writeContext{
FlexBuffer: WrapFlexBuffer(nil),
BufferPool: c.pool,
}
for _, rules := range c.rulesArr {
if rules == nil {
continue
}
w := bytes.NewBuffer(b[:0])
if err = rules.Write(w, ctx); err != nil {
return 0, 0, err
}
if _, _, err = c.origin.WriteMsgUDP(w.Bytes(), oob, addr); err != nil {
return 0, 0, err
}
}
for range c.junkCount {
junk := c.junkGen.generate(b)
if _, _, err = c.origin.WriteMsgUDP(junk, oob, addr); err != nil {
return 0, 0, err
}
}
}
return c.UDPConn.WriteMsgUDP(b, oob, addr)
}
func NewPreludeConn(
conn StreamRecordConn,
pool *sync.Pool,
framedOpts FramedOpts,
opts PreludeOpts,
) (c *PreludeConn, ok bool) {
if !opts.HasDecoyRules() || !conn.CanReadRecord() || !conn.CanWriteRecord() {
return nil, false
}
enc, _ := newFrameEncoding(framedOpts)
return &PreludeConn{
StreamRecordConn: conn,
pool: WrapBufferPool(pool),
rulesArr: opts.RulesArr,
recordEncoding: enc,
}, true
}
type PreludeConn struct {
StreamRecordConn
pool *BufferPool
rulesArr [5]Rules
recordEncoding frameEncoding
seenValid bool
}
func (c *PreludeConn) Read(b []byte) (n int, err error) {
if c.seenValid {
return c.StreamRecordConn.ReadRecord(b)
}
for {
n, err = c.StreamRecordConn.ReadRecord(b)
if err != nil {
return 0, err
}
if c.recordEncoding.IsValidRecord(b[:n]) {
c.seenValid = true
return n, nil
}
}
}
func (c *PreludeConn) Write(b []byte) (n int, err error) {
if c.recordEncoding.IsInitiationRecord(b) {
if err := c.writePreludeRecords(); err != nil {
return 0, err
}
}
return c.StreamRecordConn.WriteRecord(b)
}
func (c *PreludeConn) writePreludeRecords() (err error) {
buf := c.pool.Get()
defer c.pool.Put(buf)
ctx := &writeContext{
FlexBuffer: WrapFlexBuffer(nil),
BufferPool: c.pool,
}
for _, rules := range c.rulesArr {
if rules == nil {
continue
}
w := bytes.NewBuffer(buf[:0])
if err = rules.Write(w, ctx); err != nil {
return err
}
if _, err = c.StreamRecordConn.WriteRecord(w.Bytes()); err != nil {
return err
}
}
return nil
}
func NewPreludeBatchConn(
conn BatchConn,
origin BatchConn,
bufPool *sync.Pool,
msgsPool *sync.Pool,
header *RangedHeader,
opts PreludeOpts,
) (c *PreludeBatchConn, ok bool) {
if opts.IsEmpty() {
return nil, false
}
if opts.Jmin > opts.Jmax {
opts.Jmin, opts.Jmax = opts.Jmax, opts.Jmin
}
return &PreludeBatchConn{
BatchConn: conn,
origin: origin,
bufPool: WrapBufferPool(bufPool),
msgsPool: msgsPool,
rulesArr: opts.RulesArr,
junkCount: opts.Jc,
junkGen: newJunkGenerator(opts.Jmin, opts.Jmax),
header: header,
}, true
}
type PreludeBatchConn struct {
BatchConn
origin BatchConn
bufPool *BufferPool
msgsPool *sync.Pool
rulesArr [5]Rules
junkCount int
junkGen *junkGenerator
header *RangedHeader
}
func (c *PreludeBatchConn) WriteBatch(ms []ipv4.Message, flags int) (n int, err error) {
var initMsg *ipv4.Message
for i := range ms {
b := ms[i].Buffers[0]
var isInit bool
if len(b) >= 4 {
typ := binary.LittleEndian.Uint32(b[:4])
if c.header != nil {
isInit = c.header.Validate(typ)
} else {
isInit = typ == WireguardMsgInitiationType
}
}
if isInit {
initMsg = &ms[i]
}
}
if initMsg != nil {
ctx := &writeContext{
FlexBuffer: WrapFlexBuffer(nil),
BufferPool: c.bufPool,
}
msgs := c.msgsPool.Get().(*[]ipv4.Message)
defer c.msgsPool.Put(msgs)
i := 0
for _, rules := range c.rulesArr {
if rules == nil {
continue
}
buf := c.bufPool.Get()
defer c.bufPool.Put(buf)
w := bytes.NewBuffer(buf[:0])
if err = rules.Write(w, ctx); err != nil {
return 0, err
}
(*msgs)[i].Buffers[0] = w.Bytes()
(*msgs)[i].OOB = initMsg.OOB
(*msgs)[i].Addr = initMsg.Addr
i++
}
for range c.junkCount {
buf := c.bufPool.Get()
defer c.bufPool.Put(buf)
(*msgs)[i].Buffers[0] = c.junkGen.generate(buf)
(*msgs)[i].OOB = initMsg.OOB
(*msgs)[i].Addr = initMsg.Addr
i++
}
var start int
for {
m := (*msgs)[start:i]
n, err = c.origin.WriteBatch(m, flags)
if err != nil {
return 0, err
}
if n == len(m) {
break
}
start += n
}
}
return c.BatchConn.WriteBatch(ms, flags)
}
+13
View File
@@ -0,0 +1,13 @@
package conceal
const (
WireguardMsgInitiationType = 1
WireguardMsgResponseType = 2
WireguardMsgCookieReplyType = 3
WireguardMsgTransportType = 4
WireguardMsgInitiationSize = 148
WireguardMsgResponseSize = 92
WireguardMsgCookieReplySize = 64
WireguardMsgTransportMinSize = 32
)
+45
View File
@@ -0,0 +1,45 @@
package conceal
import (
"errors"
"strings"
)
func buildNumFormat(str string) (NumFormat, error) {
str = strings.ToLower(str)
switch str {
case "be":
return NumFormatBE, nil
case "le":
return NumFormatLE, nil
case "ascii":
return NumFormatAscii, nil
case "hex":
return NumFormatHex, nil
}
return NumFormatBE, errors.New("wrong format")
}
type NumFormat int
const (
NumFormatBE NumFormat = iota
NumFormatLE
NumFormatAscii
NumFormatHex
)
func (f NumFormat) Spec() string {
switch f {
case NumFormatBE:
return "be"
case NumFormatLE:
return "le"
case NumFormatAscii:
return "ascii"
case NumFormatHex:
return "hex"
}
return ""
}
+7 -7
View File
@@ -1,4 +1,4 @@
package device
package conceal
import (
"crypto/rand"
@@ -9,12 +9,12 @@ import (
"strings"
)
type magicHeader struct {
type RangedHeader struct {
start uint32
end uint32
}
func newMagicHeader(spec string) (*magicHeader, error) {
func NewRangedHeader(spec string) (*RangedHeader, error) {
parts := strings.Split(spec, "-")
if len(parts) < 1 || len(parts) > 2 {
return nil, errors.New("bad format")
@@ -39,24 +39,24 @@ func newMagicHeader(spec string) (*magicHeader, error) {
return nil, errors.New("wrong range specified")
}
return &magicHeader{
return &RangedHeader{
start: uint32(start),
end: uint32(end),
}, nil
}
func (h *magicHeader) GenSpec() string {
func (h *RangedHeader) GenSpec() string {
if h.start == h.end {
return fmt.Sprintf("%d", h.start)
}
return fmt.Sprintf("%d-%d", h.start, h.end)
}
func (h *magicHeader) Validate(val uint32) bool {
func (h *RangedHeader) Validate(val uint32) bool {
return h.start <= val && val <= h.end
}
func (h *magicHeader) Generate() uint32 {
func (h *RangedHeader) Generate() uint32 {
high := int64(h.end - h.start + 1)
r, _ := rand.Int(rand.Reader, big.NewInt(high))
return h.start + uint32(r.Int64())
+63
View File
@@ -0,0 +1,63 @@
package conceal
import (
"errors"
"fmt"
"strings"
)
type ruleBuilder func(val string) (Rule, error)
var ruleBuilders = map[string]ruleBuilder{
"b": buildBytesRule,
"r": buildRandRule,
"rd": buildRandDigitsRule,
"rc": buildRandCharRule,
"t": buildTimestampRule,
"dz": buildDataSizeRule,
"d": buildDataRule,
}
func ParseRules(spec string) (Rules, error) {
var (
rules Rules
errs []error
)
for i := 0; i < len(spec); {
start := strings.IndexByte(spec[i:], '<')
if start == -1 {
break
}
start += i
end := strings.IndexByte(spec[start:], '>')
if end == -1 {
return nil, errors.New("missing enclosing >")
}
end += start
key, val, _ := strings.Cut(spec[start+1:end], " ")
i = end + 1
builder, ok := ruleBuilders[key]
if !ok {
errs = append(errs, fmt.Errorf("unknown tag <%s>", key))
continue
}
rule, err := builder(val)
if err != nil {
errs = append(errs, fmt.Errorf("failed to build <%s>: %w", key, err))
continue
}
rules = append(rules, rule)
}
if len(errs) > 0 {
return nil, errors.Join(errs...)
}
return rules, nil
}
+16
View File
@@ -0,0 +1,16 @@
package conceal
import "io"
func ReadUntil(r io.Reader, b []byte, delim byte) (n int, err error) {
for n < len(b) {
if _, err = r.Read(b[n : n+1]); err != nil {
return n, err
}
if b[n] == delim {
return n, nil
}
n++
}
return n, io.ErrShortBuffer
}
+470
View File
@@ -0,0 +1,470 @@
package conceal
import (
"bytes"
"crypto/rand"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"io"
"strconv"
"strings"
"time"
"unicode"
)
var (
errInvalidData = errors.New("invalid data")
)
type readContext struct {
*FlexBuffer
*BufferPool
nextDataSize int
}
type writeContext struct {
*FlexBuffer
*BufferPool
}
type Rule interface {
Spec() string
Write(w io.Writer, ctx *writeContext) error
Read(r io.Reader, ctx *readContext) error
}
type Rules []Rule
func (r Rules) Spec() string {
var builder strings.Builder
for _, rule := range r {
builder.WriteString(rule.Spec())
}
return builder.String()
}
func (r Rules) Write(w io.Writer, ctx *writeContext) error {
for _, rule := range r {
if err := rule.Write(w, ctx); err != nil {
return err
}
}
return nil
}
func (r Rules) Read(rd io.Reader, ctx *readContext) error {
for _, rule := range r {
if err := rule.Read(rd, ctx); err != nil {
return err
}
}
return nil
}
func buildBytesRule(val string) (Rule, error) {
val = strings.TrimPrefix(val, "0x")
if len(val) == 0 {
return nil, errors.New("empty argument")
}
if len(val)%2 != 0 {
return nil, errors.New("odd amount of symbols")
}
bytes, err := hex.DecodeString(val)
if err != nil {
return nil, err
}
return &bytesRule{data: bytes}, nil
}
type bytesRule struct {
data []byte
}
func (r *bytesRule) Spec() string {
return fmt.Sprintf("<b 0x%x>", r.data)
}
func (r *bytesRule) Write(w io.Writer, ctx *writeContext) error {
_, err := w.Write(r.data)
return err
}
func (r *bytesRule) Read(rd io.Reader, ctx *readContext) error {
tmp := ctx.Get()
defer ctx.Put(tmp)
buf := tmp[:len(r.data)]
if _, err := io.ReadFull(rd, buf); err != nil {
return err
}
if !bytes.Equal(buf, r.data) {
return errInvalidData
}
return nil
}
func buildRandRule(val string) (Rule, error) {
length, err := strconv.Atoi(val)
if err != nil {
return nil, err
}
return &randRule{length: length}, nil
}
type randRule struct {
length int
}
func (r *randRule) Spec() string {
return fmt.Sprintf("<r %d>", r.length)
}
func (r *randRule) Write(w io.Writer, ctx *writeContext) error {
tmp := ctx.Get()
defer ctx.Put(tmp)
buf := tmp[:r.length]
rand.Read(buf)
_, err := w.Write(buf)
return err
}
func (r *randRule) Read(rd io.Reader, ctx *readContext) error {
tmp := ctx.Get()
defer ctx.Put(tmp)
buf := tmp[:r.length]
if _, err := io.ReadFull(rd, buf); err != nil {
return err
}
// I guess, there is no way to validate randomness
// so just return nil here like everything is fine
return nil
}
func buildRandDigitsRule(val string) (Rule, error) {
length, err := strconv.Atoi(val)
if err != nil {
return nil, err
}
return &randDigitRule{length: length}, nil
}
type randDigitRule struct {
length int
}
func (r *randDigitRule) Spec() string {
return fmt.Sprintf("<rd %d>", r.length)
}
const digits10 = "0123456789"
func (r *randDigitRule) Write(w io.Writer, ctx *writeContext) error {
tmp := ctx.Get()
defer ctx.Put(tmp)
buf := tmp[:r.length]
rand.Read(buf)
for i := range buf {
buf[i] = digits10[buf[i]%10]
}
_, err := w.Write(buf)
return err
}
func (r *randDigitRule) Read(rd io.Reader, ctx *readContext) error {
tmp := ctx.Get()
defer ctx.Put(tmp)
buf := tmp[:r.length]
if _, err := io.ReadFull(rd, buf); err != nil {
return err
}
for _, b := range buf {
if !unicode.IsDigit(rune(b)) {
return errInvalidData
}
}
return nil
}
func buildRandCharRule(val string) (Rule, error) {
length, err := strconv.Atoi(val)
if err != nil {
return nil, err
}
return &randCharRule{length: length}, nil
}
type randCharRule struct {
length int
}
func (r *randCharRule) Spec() string {
return fmt.Sprintf("<rc %d>", r.length)
}
const chars52 = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
func (r *randCharRule) Write(w io.Writer, ctx *writeContext) error {
tmp := ctx.Get()
defer ctx.Put(tmp)
buf := tmp[:r.length]
rand.Read(buf)
for i := range buf {
buf[i] = chars52[buf[i]%52]
}
_, err := w.Write(buf)
return err
}
func (r *randCharRule) Read(rd io.Reader, ctx *readContext) error {
tmp := ctx.Get()
defer ctx.Put(tmp)
buf := tmp[:r.length]
if _, err := io.ReadFull(rd, buf); err != nil {
return err
}
for _, b := range buf {
if !unicode.IsLetter(rune(b)) {
return errInvalidData
}
}
return nil
}
func buildTimestampRule(_ string) (Rule, error) {
return &timestampRule{}, nil
}
type timestampRule struct{}
func (r *timestampRule) Spec() string {
return "<t>"
}
func (r *timestampRule) Write(w io.Writer, ctx *writeContext) error {
timestamp := uint32(time.Now().Unix())
return binary.Write(w, binary.BigEndian, timestamp)
}
func (r *timestampRule) Read(rd io.Reader, ctx *readContext) error {
var timestamp uint32
if err := binary.Read(rd, binary.BigEndian, &timestamp); err != nil {
return err
}
// TODO: check timestamp?
return nil
}
func buildDataSizeRule(val string) (Rule, error) {
var (
length int = 2
format NumFormat = NumFormatBE
end byte = 0
err error
)
parts := strings.Fields(val)
if len(parts) != 2 {
return nil, errors.New("wrong amount of arguments")
}
if format, err = buildNumFormat(parts[0]); err != nil {
return nil, err
}
switch format {
case NumFormatAscii, NumFormatHex:
parts[1] = strings.TrimPrefix(parts[1], "0x")
var bytes []byte
bytes, err = hex.DecodeString(parts[1])
if err != nil {
return nil, err
}
if len(bytes) != 1 {
return nil, errors.New("too many bytes")
}
end = bytes[0]
default:
if length, err = strconv.Atoi(parts[1]); err != nil {
return nil, err
}
}
return &dataSizeRule{
length: length,
format: format,
end: end,
}, nil
}
type dataSizeRule struct {
format NumFormat
length int
end byte
}
func (r *dataSizeRule) Spec() string {
switch r.format {
case NumFormatAscii, NumFormatHex:
return fmt.Sprintf("<dz %s 0x%02x>", r.format.Spec(), r.end)
}
return fmt.Sprintf("<dz %s %d>", r.format.Spec(), r.length)
}
func (r *dataSizeRule) Write(w io.Writer, ctx *writeContext) error {
tmp := ctx.Get()
defer ctx.Put(tmp)
size := int32(ctx.Cap())
switch r.format {
case NumFormatBE:
for i := r.length - 1; i >= 0; i-- {
tmp[i] = byte(size & 0xFF)
size >>= 8
}
if _, err := w.Write(tmp[:r.length]); err != nil {
return err
}
case NumFormatLE:
for i := range r.length {
tmp[i] = byte(size & 0xFF)
size >>= 8
}
if _, err := w.Write(tmp[:r.length]); err != nil {
return err
}
case NumFormatAscii:
b := strconv.AppendInt(tmp[:0], int64(size), 10)
b = append(b, r.end)
if _, err := w.Write(b); err != nil {
return err
}
case NumFormatHex:
b := strconv.AppendInt(tmp[:0], int64(size), 16)
b = append(b, r.end)
if _, err := w.Write(b); err != nil {
return err
}
}
return nil
}
func (r *dataSizeRule) Read(rd io.Reader, ctx *readContext) error {
tmp := ctx.Get()
defer ctx.Put(tmp)
switch r.format {
case NumFormatBE:
buf := tmp[:r.length]
if _, err := io.ReadFull(rd, buf); err != nil {
return err
}
var size int
for i := range buf {
size <<= 8
size |= int(buf[i])
}
ctx.nextDataSize = size
case NumFormatLE:
buf := tmp[:r.length]
if _, err := io.ReadFull(rd, buf); err != nil {
return err
}
var size int
for i := len(buf) - 1; i >= 0; i-- {
size <<= 8
size |= int(buf[i])
}
ctx.nextDataSize = size
case NumFormatAscii:
n, err := ReadUntil(rd, tmp, r.end)
if err != nil {
return err
}
size64, err := strconv.ParseInt(string(tmp[:n]), 10, 32)
if err != nil {
return err
}
ctx.nextDataSize = int(size64)
case NumFormatHex:
n, err := ReadUntil(rd, tmp, r.end)
if err != nil {
return err
}
size64, err := strconv.ParseInt(string(tmp[:n]), 16, 32)
if err != nil {
return err
}
ctx.nextDataSize = int(size64)
}
return nil
}
func buildDataRule(val string) (Rule, error) {
return &dataRule{}, nil
}
type dataRule struct{}
func (r *dataRule) Spec() string {
return "<d>"
}
func (r *dataRule) Write(w io.Writer, ctx *writeContext) error {
buf := ctx.PullHead(-1)
if buf == nil {
return io.ErrShortBuffer
}
_, err := w.Write(buf)
return err
}
func (r *dataRule) Read(rd io.Reader, ctx *readContext) error {
buf := ctx.PushTail(ctx.nextDataSize)
if buf == nil {
return io.ErrShortBuffer
}
_, err := io.ReadFull(rd, buf)
return err
}
+19
View File
@@ -0,0 +1,19 @@
package conceal
import (
"errors"
"net"
)
var (
ErrNoReadRecord = errors.New("stream record reader unavailable")
ErrNoWriteRecord = errors.New("stream record writer unavailable")
)
type StreamRecordConn interface {
net.Conn
CanReadRecord() bool
CanWriteRecord() bool
ReadRecord(b []byte) (int, error)
WriteRecord(b []byte) (int, error)
}
+622
View File
@@ -0,0 +1,622 @@
package conn
import (
"encoding/binary"
"io"
"net"
"net/netip"
"syscall"
"testing"
"time"
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
)
const benchmarkConnMaxPacketSize = 65535
func BenchmarkConnEndpointDstToBytes(b *testing.B) {
cases := []struct {
name string
ep Endpoint
}{
{
name: "StdNetEndpoint/IPv4",
ep: &StdNetEndpoint{
AddrPort: netip.MustParseAddrPort("127.0.0.1:51820"),
},
},
{
name: "StdNetEndpoint/IPv6",
ep: &StdNetEndpoint{
AddrPort: netip.MustParseAddrPort("[2001:db8::1]:51820"),
},
},
{
name: "streamEndpoint/IPv4",
ep: &streamEndpoint{
dst: netip.MustParseAddrPort("127.0.0.1:51820"),
},
},
{
name: "streamEndpoint/IPv6",
ep: &streamEndpoint{
dst: netip.MustParseAddrPort("[2001:db8::1]:51820"),
},
},
}
for _, tc := range cases {
b.Run(tc.name, func(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = tc.ep.DstToBytes()
}
})
}
}
func BenchmarkConnCoalesceMessages(b *testing.B) {
addr := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 51820}
ep := &StdNetEndpoint{
AddrPort: addr.AddrPort(),
src: make([]byte, stickyControlSize),
}
cases := []struct {
name string
bufs [][]byte
}{
{
name: "batch8/equal_transport",
bufs: benchmarkConnEqualPayloads(8, 256),
},
{
name: "batch8/mixed_transport",
bufs: benchmarkConnMixedPayloads(8),
},
{
name: "batch64/equal_transport",
bufs: benchmarkConnEqualPayloads(64, 256),
},
{
name: "batch64/mixed_transport",
bufs: benchmarkConnMixedPayloads(64),
},
}
for _, tc := range cases {
b.Run(tc.name, func(b *testing.B) {
msgs := benchmarkConnMessages(len(tc.bufs), 2)
benchmarkConnRunLoop(b, benchmarkConnTotalBytes(tc.bufs), nil, func() error {
_ = coalesceMessages(addr, ep, tc.bufs, msgs, benchmarkConnSetGSOSize)
return nil
})
})
}
}
func BenchmarkConnSplitCoalescedMessages(b *testing.B) {
cases := []struct {
name string
build func() []ipv6.Message
firstMsgAt int
bytesPerOp int
reset func([]ipv6.Message)
}{
{
name: "gso0",
build: func() []ipv6.Message { return benchmarkConnSplitMessages(1, 256, 0, 0) },
firstMsgAt: 0,
bytesPerOp: 256,
},
{
name: "gso16_8msgs",
build: func() []ipv6.Message { return benchmarkConnSplitMessages(8, 16, 16, 7) },
firstMsgAt: 7,
bytesPerOp: 8 * 16,
reset: func(msgs []ipv6.Message) {
for i := 0; i < len(msgs)-1; i++ {
msgs[i].N = 0
msgs[i].NN = 0
}
msgs[7].N = 8 * 16
msgs[7].NN = 2
},
},
{
name: "gso128_64msgs",
build: func() []ipv6.Message { return benchmarkConnSplitMessages(64, 128, 128, 63) },
firstMsgAt: 63,
bytesPerOp: 64 * 128,
reset: func(msgs []ipv6.Message) {
for i := 0; i < len(msgs)-1; i++ {
msgs[i].N = 0
msgs[i].NN = 0
}
msgs[63].N = 64 * 128
msgs[63].NN = 2
},
},
}
for _, tc := range cases {
b.Run(tc.name, func(b *testing.B) {
if tc.reset == nil {
msgs := tc.build()
benchmarkConnRunLoop(b, tc.bytesPerOp, nil, func() error {
_, err := splitCoalescedMessages(msgs, tc.firstMsgAt, benchmarkConnGetGSOSize)
return err
})
return
}
const fixtureRingSize = 256
fixtures := make([][]ipv6.Message, fixtureRingSize)
for i := range fixtures {
fixtures[i] = tc.build()
}
b.ReportAllocs()
if tc.bytesPerOp > 0 {
b.SetBytes(int64(tc.bytesPerOp))
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
if i > 0 && i%fixtureRingSize == 0 {
b.StopTimer()
for j := range fixtures {
tc.reset(fixtures[j])
}
b.StartTimer()
}
_, err := splitCoalescedMessages(fixtures[i%fixtureRingSize], tc.firstMsgAt, benchmarkConnGetGSOSize)
if err != nil {
b.Fatal(err)
}
}
})
}
}
func BenchmarkConnStdNetBindSend(b *testing.B) {
cases := []struct {
name string
batchLen int
offload bool
}{
{name: "gso_off/batch1", batchLen: 1, offload: false},
{name: "gso_off/batch8", batchLen: 8, offload: false},
{name: "gso_on/batch8", batchLen: 8, offload: true},
{name: "gso_on/batch64", batchLen: 64, offload: true},
}
for _, tc := range cases {
b.Run(tc.name, func(b *testing.B) {
bind := NewStdNetBind().(*StdNetBind)
bind.ipv4 = &benchmarkConnUDPConn{}
bind.ipv4PC = &benchmarkConnLinuxPacketConn{}
bind.ipv4TxOffload = tc.offload
endpoint := &StdNetEndpoint{
AddrPort: netip.MustParseAddrPort("127.0.0.1:51820"),
src: make([]byte, stickyControlSize),
}
if !tc.offload {
bufs := benchmarkConnSendPayloads(tc.batchLen, 256, false)
benchmarkConnRunLoop(b, benchmarkConnTotalBytes(bufs), nil, func() error {
return bind.Send(bufs, endpoint)
})
return
}
fixtures := make([]benchmarkConnSendFixture, benchmarkConnFixtureRingSize)
for i := range fixtures {
fixtures[i] = newBenchmarkConnSendFixture(tc.batchLen, 256, true)
}
benchmarkConnRunLoopWithFixtureRing(b, benchmarkConnTotalBytes(fixtures[0].bufs), fixtures, func(f benchmarkConnSendFixture) {
benchmarkConnResetPayloadLens(f.bufs, f.lens)
}, func(f benchmarkConnSendFixture) error {
return bind.Send(f.bufs, endpoint)
})
})
}
}
func BenchmarkConnStdNetBindReceive(b *testing.B) {
cases := []struct {
name string
batchSize int
rxOffload bool
reader *benchmarkConnLinuxPacketConn
conn *benchmarkConnUDPConn
}{
{
name: "single",
batchSize: 1,
rxOffload: false,
reader: &benchmarkConnLinuxPacketConn{
readBatches: [][]benchmarkConnPacket{benchmarkConnBatchPackets(1, 256)},
},
conn: &benchmarkConnUDPConn{
readPayloads: [][]byte{benchmarkConnPayload(256)},
},
},
{
name: "batch",
batchSize: 8,
rxOffload: false,
reader: &benchmarkConnLinuxPacketConn{
readBatches: [][]benchmarkConnPacket{benchmarkConnBatchPackets(8, 256)},
},
conn: &benchmarkConnUDPConn{
readPayloads: [][]byte{benchmarkConnPayload(256)},
},
},
{
name: "batch_rx_offload",
batchSize: 64,
rxOffload: true,
reader: &benchmarkConnLinuxPacketConn{
readBatches: [][]benchmarkConnPacket{benchmarkConnCoalescedBatch(64, 256)},
},
conn: &benchmarkConnUDPConn{
readPayloads: [][]byte{benchmarkConnPayload(256)},
},
},
}
for _, tc := range cases {
b.Run(tc.name, func(b *testing.B) {
bind := NewStdNetBind().(*StdNetBind)
bufs := make([][]byte, tc.batchSize)
for i := range bufs {
bufs[i] = make([]byte, benchmarkConnMaxPacketSize)
}
sizes := make([]int, tc.batchSize)
eps := make([]Endpoint, tc.batchSize)
benchmarkConnRunLoop(b, tc.batchSize*256, nil, func() error {
_, err := bind.receiveIP(tc.reader, tc.conn, tc.rxOffload, bufs, sizes, eps)
return err
})
})
}
}
func benchmarkConnRunLoop(b *testing.B, bytesPerOp int, reset func(), op func() error) {
b.Helper()
b.ReportAllocs()
if bytesPerOp > 0 {
b.SetBytes(int64(bytesPerOp))
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
if i > 0 && reset != nil {
b.StopTimer()
reset()
b.StartTimer()
}
if err := op(); err != nil {
b.Fatal(err)
}
}
}
func benchmarkConnRunLoopWithFixtureRing[T any](b *testing.B, bytesPerOp int, fixtures []T, reset func(T), op func(T) error) {
b.Helper()
if len(fixtures) == 0 {
b.Fatal("benchmark fixture ring must not be empty")
}
b.ReportAllocs()
if bytesPerOp > 0 {
b.SetBytes(int64(bytesPerOp))
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
if i > 0 && i%len(fixtures) == 0 && reset != nil {
b.StopTimer()
for _, fixture := range fixtures {
reset(fixture)
}
b.StartTimer()
}
if err := op(fixtures[i%len(fixtures)]); err != nil {
b.Fatal(err)
}
}
}
func benchmarkConnPayload(size int) []byte {
payload := make([]byte, size)
for i := range payload {
payload[i] = byte(i)
}
return payload
}
func benchmarkConnEqualPayloads(count, size int) [][]byte {
bufs := make([][]byte, count)
for i := range bufs {
capacity := size
if i == 0 {
capacity = size * count
}
buf := make([]byte, size, capacity)
for j := range buf {
buf[j] = byte(j)
}
bufs[i] = buf
}
return bufs
}
func benchmarkConnMixedPayloads(count int) [][]byte {
bufs := make([][]byte, count)
for i := range bufs {
size := 256
if i%2 == 1 {
size = 1280
}
capacity := size
if i == 0 {
capacity = size * count
}
buf := make([]byte, size, capacity)
for j := range buf {
buf[j] = byte(j)
}
bufs[i] = buf
}
return bufs
}
func benchmarkConnSendPayloads(count, size int, offload bool) [][]byte {
bufs := make([][]byte, count)
for i := range bufs {
capacity := size
if offload && i == 0 {
capacity = size * count
}
buf := make([]byte, size, capacity)
for j := range buf {
buf[j] = byte(j)
}
bufs[i] = buf
}
return bufs
}
func benchmarkConnLengths(bufs [][]byte) []int {
lens := make([]int, len(bufs))
for i := range bufs {
lens[i] = len(bufs[i])
}
return lens
}
func benchmarkConnResetPayloadLens(bufs [][]byte, lens []int) {
for i := range bufs {
bufs[i] = bufs[i][:lens[i]]
}
}
const benchmarkConnFixtureRingSize = 256
type benchmarkConnSendFixture struct {
bufs [][]byte
lens []int
}
func newBenchmarkConnSendFixture(count, size int, offload bool) benchmarkConnSendFixture {
bufs := benchmarkConnSendPayloads(count, size, offload)
return benchmarkConnSendFixture{
bufs: bufs,
lens: benchmarkConnLengths(bufs),
}
}
func benchmarkConnTotalBytes(bufs [][]byte) int {
total := 0
for _, buf := range bufs {
total += len(buf)
}
return total
}
func benchmarkConnMessages(count int, oobCap int) []ipv6.Message {
msgs := make([]ipv6.Message, count)
for i := range msgs {
msgs[i].Buffers = make([][]byte, 1)
msgs[i].OOB = make([]byte, 0, oobCap)
}
return msgs
}
func benchmarkConnResetMessages(msgs []ipv6.Message) {
for i := range msgs {
msgs[i].Addr = nil
msgs[i].N = 0
msgs[i].NN = 0
msgs[i].OOB = msgs[i].OOB[:0]
msgs[i].Buffers[0] = nil
}
}
func benchmarkConnSetGSOSize(control *[]byte, gsoSize uint16) {
*control = (*control)[:cap(*control)]
binary.LittleEndian.PutUint16(*control, gsoSize)
}
func benchmarkConnGetGSOSize(control []byte) (int, error) {
if len(control) < 2 {
return 0, nil
}
return int(binary.LittleEndian.Uint16(control)), nil
}
func benchmarkConnSplitMessages(numMsgs, segmentSize, gsoSize, sourceIndex int) []ipv6.Message {
msgs := make([]ipv6.Message, numMsgs)
for i := range msgs {
bufSize := segmentSize
if i == sourceIndex {
bufSize = numMsgs * segmentSize
}
msgs[i].Buffers = [][]byte{make([]byte, bufSize)}
msgs[i].OOB = make([]byte, 0, 2)
}
for i := 0; i < numMsgs*segmentSize; i++ {
msgs[sourceIndex].Buffers[0][i] = byte(i)
}
msgs[sourceIndex].N = numMsgs * segmentSize
if gsoSize > 0 {
msgs[sourceIndex].OOB = msgs[sourceIndex].OOB[:2]
binary.LittleEndian.PutUint16(msgs[sourceIndex].OOB, uint16(gsoSize))
msgs[sourceIndex].NN = 2
}
return msgs
}
type benchmarkConnPacket struct {
payload []byte
oob []byte
}
func benchmarkConnBatchPackets(count, size int) []benchmarkConnPacket {
packets := make([]benchmarkConnPacket, count)
for i := range packets {
packets[i].payload = benchmarkConnPayload(size)
}
return packets
}
func benchmarkConnCoalescedBatch(count, segmentSize int) []benchmarkConnPacket {
payload := make([]byte, count*segmentSize)
for i := range payload {
payload[i] = byte(i)
}
oob := make([]byte, 2)
binary.LittleEndian.PutUint16(oob, uint16(segmentSize))
return []benchmarkConnPacket{
{
payload: payload,
oob: oob,
},
}
}
type benchmarkConnLinuxPacketConn struct {
readBatches [][]benchmarkConnPacket
readIndex int
}
func (c *benchmarkConnLinuxPacketConn) ResetRead() {
c.readIndex = 0
}
func (c *benchmarkConnLinuxPacketConn) ReadBatch(ms []ipv4.Message, flags int) (int, error) {
if len(c.readBatches) == 0 {
return 0, io.EOF
}
batch := c.readBatches[c.readIndex]
c.readIndex++
if c.readIndex == len(c.readBatches) {
c.readIndex = 0
}
for i := range batch {
msg := &ms[i]
packet := batch[i]
msg.N = copy(msg.Buffers[0], packet.payload)
if cap(msg.OOB) < len(packet.oob) {
msg.OOB = make([]byte, len(packet.oob))
}
msg.OOB = msg.OOB[:len(packet.oob)]
copy(msg.OOB, packet.oob)
msg.NN = len(packet.oob)
msg.Addr = &net.UDPAddr{
IP: net.IPv4(127, 0, 0, 1),
Port: 51820,
}
}
for i := len(batch); i < len(ms); i++ {
ms[i].N = 0
ms[i].NN = 0
ms[i].Addr = nil
ms[i].OOB = ms[i].OOB[:0]
}
return len(batch), nil
}
func (c *benchmarkConnLinuxPacketConn) WriteBatch(ms []ipv4.Message, flags int) (int, error) {
return len(ms), nil
}
type benchmarkConnUDPConn struct {
readPayloads [][]byte
readIndex int
}
func (c *benchmarkConnUDPConn) ResetRead() {
c.readIndex = 0
}
func (c *benchmarkConnUDPConn) ReadFrom(p []byte) (int, net.Addr, error) {
n, _, _, addr, err := c.ReadMsgUDP(p, nil)
return n, addr, err
}
func (c *benchmarkConnUDPConn) WriteTo(p []byte, addr net.Addr) (int, error) {
udpAddr, _ := addr.(*net.UDPAddr)
n, _, err := c.WriteMsgUDP(p, nil, udpAddr)
return n, err
}
func (c *benchmarkConnUDPConn) Close() error {
return nil
}
func (c *benchmarkConnUDPConn) LocalAddr() net.Addr {
return &net.UDPAddr{
IP: net.IPv4(127, 0, 0, 1),
Port: 51820,
}
}
func (c *benchmarkConnUDPConn) SetDeadline(time.Time) error {
return nil
}
func (c *benchmarkConnUDPConn) SetReadDeadline(time.Time) error {
return nil
}
func (c *benchmarkConnUDPConn) SetWriteDeadline(time.Time) error {
return nil
}
func (c *benchmarkConnUDPConn) SyscallConn() (syscall.RawConn, error) {
return nil, nil
}
func (c *benchmarkConnUDPConn) ReadMsgUDP(b, oob []byte) (n, oobn, flags int, addr *net.UDPAddr, err error) {
if len(c.readPayloads) == 0 {
return 0, 0, 0, nil, io.EOF
}
payload := c.readPayloads[c.readIndex]
c.readIndex++
if c.readIndex == len(c.readPayloads) {
c.readIndex = 0
}
n = copy(b, payload)
return n, 0, 0, &net.UDPAddr{
IP: net.IPv4(127, 0, 0, 1),
Port: 51820,
}, nil
}
func (c *benchmarkConnUDPConn) WriteMsgUDP(b, oob []byte, addr *net.UDPAddr) (int, int, error) {
return len(b), len(oob), nil
}
+31 -12
View File
@@ -16,12 +16,16 @@ import (
"sync"
"syscall"
"github.com/amnezia-vpn/amneziawg-go/conceal"
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
)
var (
_ Bind = (*StdNetBind)(nil)
_ Bind = (*StdNetBind)(nil)
_ Framable = (*StdNetBind)(nil)
_ Preludable = (*StdNetBind)(nil)
_ Masqueradable = (*StdNetBind)(nil)
)
// StdNetBind implements Bind for all platforms. While Windows has its own Bind
@@ -31,10 +35,10 @@ var (
// proposal in https://github.com/golang/go/issues/45886#issuecomment-1218301564.
type StdNetBind struct {
mu sync.Mutex // protects all fields except as specified
ipv4 *net.UDPConn
ipv6 *net.UDPConn
ipv4PC *ipv4.PacketConn // will be nil on non-Linux
ipv6PC *ipv6.PacketConn // will be nil on non-Linux
ipv4 UDPConn
ipv6 UDPConn
ipv4PC LinuxPacketConn // will be nil on non-Linux
ipv6PC LinuxPacketConn // will be nil on non-Linux
ipv4TxOffload bool
ipv4RxOffload bool
ipv6TxOffload bool
@@ -43,9 +47,14 @@ type StdNetBind struct {
// these two fields are not guarded by mu
udpAddrPool sync.Pool
msgsPool sync.Pool
bufPool sync.Pool
blackhole4 bool
blackhole6 bool
framedOpts conceal.FramedOpts
preludeOpts conceal.PreludeOpts
masqueradeOpts conceal.MasqueradeOpts
}
func NewStdNetBind() Bind {
@@ -70,6 +79,12 @@ func NewStdNetBind() Bind {
return &msgs
},
},
bufPool: sync.Pool{
New: func() any {
return make([]byte, 65535)
},
},
}
}
@@ -152,9 +167,9 @@ func (s *StdNetBind) Open(uport uint16) ([]ReceiveFunc, uint16, error) {
// If uport is 0, we can retry on failure.
again:
port := int(uport)
var v4conn, v6conn *net.UDPConn
var v4pc *ipv4.PacketConn
var v6pc *ipv6.PacketConn
var v4conn, v6conn UDPConn
var v4pc LinuxPacketConn
var v6pc LinuxPacketConn
v4conn, port, err = listenNet("udp4", port)
if err != nil && !errors.Is(err, syscall.EAFNOSUPPORT) {
@@ -177,8 +192,10 @@ again:
s.ipv4TxOffload, s.ipv4RxOffload = supportsUDPOffload(v4conn)
if runtime.GOOS == "linux" || runtime.GOOS == "android" {
v4pc = ipv4.NewPacketConn(v4conn)
v4pc = s.upgradePacketConn(v4pc)
s.ipv4PC = v4pc
}
v4conn = s.upgradeUDPConn(v4conn)
fns = append(fns, s.makeReceiveIPv4(v4pc, v4conn, s.ipv4RxOffload))
s.ipv4 = v4conn
}
@@ -186,8 +203,10 @@ again:
s.ipv6TxOffload, s.ipv6RxOffload = supportsUDPOffload(v6conn)
if runtime.GOOS == "linux" || runtime.GOOS == "android" {
v6pc = ipv6.NewPacketConn(v6conn)
v6pc = s.upgradePacketConn(v6pc)
s.ipv6PC = v6pc
}
v6conn = s.upgradeUDPConn(v6conn)
fns = append(fns, s.makeReceiveIPv6(v6pc, v6conn, s.ipv6RxOffload))
s.ipv6 = v6conn
}
@@ -225,7 +244,7 @@ type batchWriter interface {
func (s *StdNetBind) receiveIP(
br batchReader,
conn *net.UDPConn,
conn UDPConn,
rxOffload bool,
bufs [][]byte,
sizes []int,
@@ -277,13 +296,13 @@ func (s *StdNetBind) receiveIP(
return numMsgs, nil
}
func (s *StdNetBind) makeReceiveIPv4(pc *ipv4.PacketConn, conn *net.UDPConn, rxOffload bool) ReceiveFunc {
func (s *StdNetBind) makeReceiveIPv4(pc LinuxPacketConn, conn UDPConn, rxOffload bool) ReceiveFunc {
return func(bufs [][]byte, sizes []int, eps []Endpoint) (n int, err error) {
return s.receiveIP(pc, conn, rxOffload, bufs, sizes, eps)
}
}
func (s *StdNetBind) makeReceiveIPv6(pc *ipv6.PacketConn, conn *net.UDPConn, rxOffload bool) ReceiveFunc {
func (s *StdNetBind) makeReceiveIPv6(pc LinuxPacketConn, conn UDPConn, rxOffload bool) ReceiveFunc {
return func(bufs [][]byte, sizes []int, eps []Endpoint) (n int, err error) {
return s.receiveIP(pc, conn, rxOffload, bufs, sizes, eps)
}
@@ -409,7 +428,7 @@ retry:
return err
}
func (s *StdNetBind) send(conn *net.UDPConn, pc batchWriter, msgs []ipv6.Message) error {
func (s *StdNetBind) send(conn UDPConn, pc batchWriter, msgs []ipv6.Message) error {
var (
n int
err error
+297
View File
@@ -0,0 +1,297 @@
package conn
import (
"context"
"fmt"
"io"
"net"
"net/netip"
"strconv"
"sync"
"github.com/amnezia-vpn/amneziawg-go/conceal"
)
var (
_ Bind = (*BindStream)(nil)
_ Framable = (*BindStream)(nil)
_ Preludable = (*BindStream)(nil)
_ Masqueradable = (*BindStream)(nil)
)
type streamPacketQueue struct {
ep *streamEndpoint
buf [65535]byte
n int
}
func NewBindStream() *BindStream {
return &BindStream{
streamPacketPool: sync.Pool{
New: func() any {
return new(streamPacketQueue)
},
},
bufferPool: sync.Pool{
New: func() any {
return make([]byte, 65535)
},
},
}
}
type BindStream struct {
queue chan *streamPacketQueue
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
streamPacketPool sync.Pool
bufferPool sync.Pool
dialer net.Dialer
listenConfig net.ListenConfig
port uint16
framedOpts conceal.FramedOpts
preludeOpts conceal.PreludeOpts
masqueradeOpts conceal.MasqueradeOpts
}
func (b *BindStream) readFaucet() ReceiveFunc {
return func(packets [][]byte, sizes []int, eps []Endpoint) (n int, err error) {
streamPacket, ok := <-b.queue
if !ok {
return 0, net.ErrClosed
}
packet := streamPacket.buf[:streamPacket.n]
copy(packets[0], packet)
sizes[0] = streamPacket.n
eps[0] = streamPacket.ep
b.streamPacketPool.Put(streamPacket)
return 1, nil
}
}
func (b *BindStream) readStream(ep *streamEndpoint) {
defer b.wg.Done()
for {
sp := b.streamPacketPool.Get().(*streamPacketQueue)
n, err := ep.conn.Read(sp.buf[:])
if err != nil {
ep.Close()
return
}
sp.ep, sp.n = ep, n
b.queue <- sp
}
}
func (b *BindStream) accept(listener net.Listener) {
defer b.wg.Done()
for {
select {
case <-b.ctx.Done():
return
default:
}
conn, err := listener.Accept()
if err != nil {
// log this error somewhere
break
}
conn = b.upgradeConn(conn)
b.wg.Add(1)
go b.handleAccepted(conn)
}
}
func (b *BindStream) handleAccepted(conn net.Conn) {
defer b.wg.Done()
ap, err := netip.ParseAddrPort(conn.RemoteAddr().String())
if err != nil {
// add logs
}
ep := &streamEndpoint{
conn: conn,
dst: ap,
}
b.wg.Add(1)
go b.readStream(ep)
<-b.ctx.Done()
ep.Close()
}
func (b *BindStream) dial(ep *streamEndpoint) error {
ep.mutex.Lock()
defer ep.mutex.Unlock()
if ep.conn != nil {
return nil
}
conn, err := b.dialer.DialContext(b.ctx, "tcp", ep.DstToString())
if err != nil {
return fmt.Errorf("failed to dial context: %v", err)
}
conn = b.upgradeConn(conn)
ep.conn = conn
b.wg.Add(1)
go func() {
defer b.wg.Done()
<-b.ctx.Done()
ep.Close()
}()
b.wg.Add(1)
go b.readStream(ep)
return nil
}
func (b *BindStream) openConnections() error {
b.ctx, b.cancel = context.WithCancel(context.Background())
if b.port != 0 {
listener, err := b.listenConfig.Listen(b.ctx, "tcp", ":"+strconv.Itoa(int(b.port)))
if err != nil {
return err
}
if addr, ok := listener.Addr().(*net.TCPAddr); ok {
b.port = uint16(addr.Port)
}
b.wg.Add(1)
go func() {
defer b.wg.Done()
<-b.ctx.Done()
listener.Close()
}()
b.wg.Add(1)
go b.accept(listener)
}
return nil
}
func (b *BindStream) Open(port uint16) (fns []ReceiveFunc, actualPort uint16, err error) {
b.queue = make(chan *streamPacketQueue, 1024)
b.port = port
err = b.openConnections()
return []ReceiveFunc{b.readFaucet()}, b.port, err
}
func (b *BindStream) Send(bufs [][]byte, ep Endpoint) error {
streamEp, ok := ep.(*streamEndpoint)
if !ok {
return nil
}
select {
case <-b.ctx.Done():
return io.ErrClosedPipe
default:
}
if err := b.dial(streamEp); err != nil {
return err
}
for _, buf := range bufs {
if _, err := streamEp.conn.Write(buf); err != nil {
streamEp.Close()
return err
}
}
return nil
}
func (b *BindStream) closeConnections() {
if b.cancel != nil {
b.cancel()
}
b.wg.Wait()
}
func (b *BindStream) Close() error {
b.closeConnections()
if b.queue != nil {
close(b.queue)
}
return nil
}
func (b *BindStream) ParseEndpoint(s string) (Endpoint, error) {
tcpAddr, err := net.ResolveTCPAddr("tcp", s)
if err != nil {
return nil, fmt.Errorf("failed to resolve addr: %v", err)
}
return &streamEndpoint{
dst: tcpAddr.AddrPort(),
}, nil
}
func (b *BindStream) BatchSize() int {
return 1
}
var _ Endpoint = (*streamEndpoint)(nil)
type streamEndpoint struct {
conn net.Conn
dst netip.AddrPort
mutex sync.Mutex
}
func (e *streamEndpoint) Close() {
e.mutex.Lock()
defer e.mutex.Unlock()
if e.conn != nil {
e.conn.Close()
e.conn = nil
}
}
func (e *streamEndpoint) DstToString() string {
return e.dst.String()
}
func (e *streamEndpoint) DstToBytes() []byte {
b, _ := e.dst.MarshalBinary()
return b
}
func (e *streamEndpoint) DstIP() netip.Addr {
return e.dst.Addr()
}
func (e *streamEndpoint) ClearSrc() {
}
func (e *streamEndpoint) SrcToString() string {
return ""
}
func (e *streamEndpoint) SrcIP() netip.Addr {
return netip.Addr{}
}
-2
View File
@@ -77,8 +77,6 @@ type WinRingBind struct {
isOpen atomic.Uint32 // 0, 1, or 2
}
func NewDefaultBind() Bind { return NewWinRingBind() }
func NewWinRingBind() Bind {
if !winrio.Initialize() {
return NewStdNetBind()
+180
View File
@@ -0,0 +1,180 @@
package conn
import (
"net"
"github.com/amnezia-vpn/amneziawg-go/conceal"
)
type Framable interface {
SetFramedOpts(opts conceal.FramedOpts)
}
type Preludable interface {
SetPreludeOpts(opts conceal.PreludeOpts)
}
type Masqueradable interface {
SetMasqueradeOpts(opts conceal.MasqueradeOpts)
}
type concealStage string
const (
concealStageMasquerade concealStage = "masquerade"
concealStageRecord concealStage = "record"
concealStageFramed concealStage = "framed"
concealStagePrelude concealStage = "prelude"
)
type concealPipeline struct {
stages []concealStage
}
func (p concealPipeline) names() []string {
names := make([]string, 0, len(p.stages))
for _, stage := range p.stages {
names = append(names, string(stage))
}
return names
}
func hasFramed(opts conceal.FramedOpts) bool {
return opts.H1 != nil || opts.H2 != nil || opts.H3 != nil || opts.H4 != nil ||
opts.S1 != 0 || opts.S2 != 0 || opts.S3 != 0 || opts.S4 != 0
}
func hasMasquerade(opts conceal.MasqueradeOpts) bool {
return opts.RulesIn != nil || opts.RulesOut != nil
}
func hasBidirectionalStreamRecords(opts conceal.MasqueradeOpts) bool {
return opts.RulesIn != nil && opts.RulesOut != nil
}
func udpPreludeHeader(opts conceal.FramedOpts) *conceal.RangedHeader {
if opts.HeaderCompat {
return opts.H1
}
return nil
}
func (b *StdNetBind) udpConcealPipeline() concealPipeline {
stages := make([]concealStage, 0, 3)
if hasMasquerade(b.masqueradeOpts) {
stages = append(stages, concealStageMasquerade)
}
if hasFramed(b.framedOpts) {
stages = append(stages, concealStageFramed)
}
if !b.preludeOpts.IsEmpty() {
stages = append(stages, concealStagePrelude)
}
return concealPipeline{stages: stages}
}
func (b *StdNetBind) batchConcealPipeline() concealPipeline {
return b.udpConcealPipeline()
}
func (b *BindStream) streamConcealPipeline() concealPipeline {
stages := make([]concealStage, 0, 3)
if hasMasquerade(b.masqueradeOpts) {
stages = append(stages, concealStageRecord)
}
if b.preludeOpts.HasDecoyRules() && hasBidirectionalStreamRecords(b.masqueradeOpts) {
stages = append(stages, concealStagePrelude)
}
if hasFramed(b.framedOpts) {
stages = append(stages, concealStageFramed)
}
return concealPipeline{stages: stages}
}
func (b *StdNetBind) upgradeUDPConn(conn UDPConn) UDPConn {
origin := conn
for _, stage := range b.udpConcealPipeline().stages {
switch stage {
case concealStageMasquerade:
if masquerade, ok := conceal.NewMasqueradeUDPConn(conn, &b.bufPool, b.masqueradeOpts); ok {
conn = masquerade
}
case concealStageFramed:
if framed, ok := conceal.NewFramedUDPConn(conn, &b.bufPool, b.framedOpts); ok {
conn = framed
}
case concealStagePrelude:
if prelude, ok := conceal.NewPreludeUDPConn(conn, origin, &b.bufPool, udpPreludeHeader(b.framedOpts), b.preludeOpts); ok {
conn = prelude
}
}
}
return conn
}
func (b *StdNetBind) upgradePacketConn(conn LinuxPacketConn) LinuxPacketConn {
origin := conn
for _, stage := range b.batchConcealPipeline().stages {
switch stage {
case concealStageMasquerade:
if masquerade, ok := conceal.NewMasqueradeBatchConn(conn, &b.bufPool, b.masqueradeOpts); ok {
conn = masquerade
}
case concealStageFramed:
if framed, ok := conceal.NewFramedBatchConn(conn, &b.bufPool, b.framedOpts); ok {
conn = framed
}
case concealStagePrelude:
if prelude, ok := conceal.NewPreludeBatchConn(conn, origin, &b.bufPool, &b.msgsPool, udpPreludeHeader(b.framedOpts), b.preludeOpts); ok {
conn = prelude
}
}
}
return conn
}
func (b *StdNetBind) SetFramedOpts(opts conceal.FramedOpts) {
b.framedOpts = opts
}
func (b *StdNetBind) SetPreludeOpts(opts conceal.PreludeOpts) {
b.preludeOpts = opts
}
func (b *StdNetBind) SetMasqueradeOpts(opts conceal.MasqueradeOpts) {
b.masqueradeOpts = opts
}
func (b *BindStream) upgradeConn(conn net.Conn) net.Conn {
var recordConn conceal.StreamRecordConn
for _, stage := range b.streamConcealPipeline().stages {
switch stage {
case concealStageRecord:
if masquerade, ok := conceal.NewMasqueradeConn(conn, &b.bufferPool, b.masqueradeOpts); ok {
recordConn = masquerade
conn = masquerade
}
case concealStagePrelude:
if prelude, ok := conceal.NewPreludeConn(recordConn, &b.bufferPool, b.framedOpts, b.preludeOpts); ok {
conn = prelude
}
case concealStageFramed:
if framed, ok := conceal.NewFramedConn(conn, &b.bufferPool, b.framedOpts); ok {
conn = framed
}
}
}
return conn
}
func (b *BindStream) SetFramedOpts(opts conceal.FramedOpts) {
b.framedOpts = opts
}
func (b *BindStream) SetPreludeOpts(opts conceal.PreludeOpts) {
b.preludeOpts = opts
}
func (b *BindStream) SetMasqueradeOpts(opts conceal.MasqueradeOpts) {
b.masqueradeOpts = opts
}
+455
View File
@@ -0,0 +1,455 @@
package conn
import (
"bytes"
"encoding/binary"
"net"
"slices"
"sync"
"syscall"
"testing"
"time"
"github.com/amnezia-vpn/amneziawg-go/conceal"
)
func TestStdNetBindUDPPipelineOrder(t *testing.T) {
bind := NewStdNetBind().(*StdNetBind)
bind.SetMasqueradeOpts(conceal.MasqueradeOpts{
RulesIn: mustParseRules(t, "<dz be 2><d>"),
RulesOut: mustParseRules(t, "<dz be 2><d>"),
})
bind.SetFramedOpts(conceal.FramedOpts{H1: mustHeader(t, "777")})
bind.SetPreludeOpts(conceal.PreludeOpts{
RulesArr: [5]conceal.Rules{mustParseRules(t, "<b 0xaabb>")},
})
got := bind.udpConcealPipeline().names()
want := []string{"masquerade", "framed", "prelude"}
if !slices.Equal(got, want) {
t.Fatalf("udp pipeline = %v, want %v", got, want)
}
}
func TestStdNetBindUDPPreludeStillEmitsJunk(t *testing.T) {
bind := NewStdNetBind().(*StdNetBind)
bind.SetPreludeOpts(conceal.PreludeOpts{
Jc: 1,
Jmin: 3,
Jmax: 3,
RulesArr: [5]conceal.Rules{
mustParseRules(t, "<b 0xaabb>"),
},
})
conn := &recordingUDPConn{}
upgraded := bind.upgradeUDPConn(conn)
initiation := makeInitiationPacket()
if _, _, err := upgraded.WriteMsgUDP(initiation, nil, &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 51820}); err != nil {
t.Fatalf("udp write failed: %v", err)
}
if len(conn.writes) != 3 {
t.Fatalf("udp write count = %d, want 3", len(conn.writes))
}
if !bytes.Equal(conn.writes[0], []byte{0xaa, 0xbb}) {
t.Fatalf("udp decoy record = %x, want aabb", conn.writes[0])
}
if len(conn.writes[1]) != 3 {
t.Fatalf("udp junk length = %d, want 3", len(conn.writes[1]))
}
if !bytes.Equal(conn.writes[2], initiation) {
t.Fatalf("udp initiation payload changed")
}
}
func TestBindStreamPipelineUsesRecordLayer(t *testing.T) {
bind := newRecordAwareBind(t)
got := bind.streamConcealPipeline().names()
want := []string{"record", "prelude", "framed"}
if !slices.Equal(got, want) {
t.Fatalf("stream pipeline = %v, want %v", got, want)
}
}
func TestBindStreamPipelineOmitsPreludeWithoutBidirectionalRecords(t *testing.T) {
bind := NewBindStream()
bind.SetMasqueradeOpts(conceal.MasqueradeOpts{
RulesOut: mustParseRules(t, "<dz be 2><d>"),
})
bind.SetFramedOpts(conceal.FramedOpts{H1: mustHeader(t, "777")})
bind.SetPreludeOpts(conceal.PreludeOpts{
RulesArr: [5]conceal.Rules{mustParseRules(t, "<b 0xaabb>")},
})
got := bind.streamConcealPipeline().names()
want := []string{"record", "framed"}
if !slices.Equal(got, want) {
t.Fatalf("stream pipeline without bidirectional records = %v, want %v", got, want)
}
}
func TestBindStreamPipelineOmitsPreludeForTCPJunkOnlyConfig(t *testing.T) {
bind := NewBindStream()
bind.SetMasqueradeOpts(conceal.MasqueradeOpts{
RulesIn: mustParseRules(t, "<dz be 2><d>"),
RulesOut: mustParseRules(t, "<dz be 2><d>"),
})
bind.SetFramedOpts(conceal.FramedOpts{H1: mustHeader(t, "777")})
bind.SetPreludeOpts(conceal.PreludeOpts{
Jc: 1,
Jmin: 2,
Jmax: 2,
})
got := bind.streamConcealPipeline().names()
want := []string{"record", "framed"}
if !slices.Equal(got, want) {
t.Fatalf("stream pipeline with tcp junk only = %v, want %v", got, want)
}
}
func TestBindStreamTCPPreludeInjectsBeforeEveryInitiation(t *testing.T) {
senderRaw, receiverRaw := net.Pipe()
defer senderRaw.Close()
defer receiverRaw.Close()
bind := newRecordAwareBind(t)
sender := bind.upgradeConn(senderRaw)
receiver := mustRecordConn(t, receiverRaw, &bind.bufferPool, conceal.MasqueradeOpts{
RulesIn: mustParseRules(t, "<dz be 2><d>"),
})
initiation := makeInitiationPacket()
writeErr := make(chan error, 1)
go func() {
_, err := sender.Write(initiation)
writeErr <- err
}()
prelude := readRecord(t, receiver, 16)
if !bytes.Equal(prelude, []byte{0xaa, 0xbb}) {
t.Fatalf("prelude record = %x, want aabb", prelude)
}
firstInit := readRecord(t, receiver, len(initiation))
if gotHeader := binary.LittleEndian.Uint32(firstInit[:4]); gotHeader != 777 {
t.Fatalf("first initiation header = %d, want 777", gotHeader)
}
if err := <-writeErr; err != nil {
t.Fatalf("first write failed: %v", err)
}
writeErr = make(chan error, 1)
go func() {
_, err := sender.Write(initiation)
writeErr <- err
}()
secondPrelude := readRecord(t, receiver, 16)
if !bytes.Equal(secondPrelude, []byte{0xaa, 0xbb}) {
t.Fatalf("second prelude record = %x, want aabb", secondPrelude)
}
secondInit := readRecord(t, receiver, len(initiation))
if gotHeader := binary.LittleEndian.Uint32(secondInit[:4]); gotHeader != 777 {
t.Fatalf("second initiation header = %d, want 777", gotHeader)
}
if err := <-writeErr; err != nil {
t.Fatalf("second write failed: %v", err)
}
}
func TestBindStreamTCPPreludeSkipsNonInitiationRecords(t *testing.T) {
senderRaw, receiverRaw := net.Pipe()
defer senderRaw.Close()
defer receiverRaw.Close()
bind := newRecordAwareBind(t)
sender := bind.upgradeConn(senderRaw)
receiver := mustRecordConn(t, receiverRaw, &bind.bufferPool, conceal.MasqueradeOpts{
RulesIn: mustParseRules(t, "<dz be 2><d>"),
})
transport := makeTransportPacket()
writeErr := make(chan error, 1)
go func() {
_, err := sender.Write(transport)
writeErr <- err
}()
gotTransport := readRecord(t, receiver, len(transport))
if gotHeader := binary.LittleEndian.Uint32(gotTransport[:4]); gotHeader != 779 {
t.Fatalf("transport header = %d, want 779", gotHeader)
}
if err := <-writeErr; err != nil {
t.Fatalf("transport write failed: %v", err)
}
if err := receiver.SetReadDeadline(time.Now().Add(50 * time.Millisecond)); err != nil {
t.Fatalf("set deadline: %v", err)
}
buf := make([]byte, len(transport))
if _, err := receiver.ReadRecord(buf); err == nil {
t.Fatal("expected no extra prelude records for transport write")
}
}
func TestBindStreamTCPPreludeDropsInjectedDecoysOnRead(t *testing.T) {
senderRaw, receiverRaw := net.Pipe()
defer senderRaw.Close()
defer receiverRaw.Close()
bind := newRecordAwareBind(t)
sender := bind.upgradeConn(senderRaw)
receiver := bind.upgradeConn(receiverRaw)
initiation := makeInitiationPacket()
writeErr := make(chan error, 1)
go func() {
_, err := sender.Write(initiation)
writeErr <- err
}()
got := readPacket(t, receiver, len(initiation))
if !bytes.Equal(got, initiation) {
t.Fatalf("initiation payload changed after dropping decoys")
}
if err := <-writeErr; err != nil {
t.Fatalf("write failed: %v", err)
}
}
func TestBindStreamTCPPreludeDropsLeadingInvalidRecords(t *testing.T) {
senderRaw, receiverRaw := net.Pipe()
defer senderRaw.Close()
defer receiverRaw.Close()
bind := newRecordAwareBind(t)
receiver := bind.upgradeConn(receiverRaw)
recordWriter := mustRecordConn(t, senderRaw, &bind.bufferPool, conceal.MasqueradeOpts{
RulesOut: mustParseRules(t, "<dz be 2><d>"),
})
framedWriter, ok := conceal.NewFramedConn(recordWriter, &bind.bufferPool, bind.framedOpts)
if !ok {
t.Fatal("expected framed writer")
}
initiation := makeInitiationPacket()
writeErr := make(chan error, 1)
go func() {
if _, err := recordWriter.WriteRecord([]byte{0xde, 0xad}); err != nil {
writeErr <- err
return
}
if _, err := recordWriter.WriteRecord([]byte{0xbe, 0xef, 0x01}); err != nil {
writeErr <- err
return
}
_, err := framedWriter.Write(initiation)
writeErr <- err
}()
got := readPacket(t, receiver, len(initiation))
if !bytes.Equal(got, initiation) {
t.Fatalf("read-side did not recover after invalid leading records")
}
if err := <-writeErr; err != nil {
t.Fatalf("write failed: %v", err)
}
}
func TestBindStreamTCPPreludePassesResponseAndTransportRecords(t *testing.T) {
senderRaw, receiverRaw := net.Pipe()
defer senderRaw.Close()
defer receiverRaw.Close()
bind := newRecordAwareBind(t)
sender := bind.upgradeConn(senderRaw)
receiver := bind.upgradeConn(receiverRaw)
response := makeResponsePacket()
sendAndAssertRoundTrip(t, sender, receiver, response)
transport := makeTransportPacket()
sendAndAssertRoundTrip(t, sender, receiver, transport)
}
func newRecordAwareBind(t *testing.T) *BindStream {
t.Helper()
rules := mustParseRules(t, "<dz be 2><d>")
bind := NewBindStream()
bind.SetMasqueradeOpts(conceal.MasqueradeOpts{
RulesIn: rules,
RulesOut: rules,
})
bind.SetFramedOpts(conceal.FramedOpts{
H1: mustHeader(t, "777"),
H2: mustHeader(t, "778"),
H4: mustHeader(t, "779"),
})
bind.SetPreludeOpts(conceal.PreludeOpts{
Jc: 1,
Jmin: 2,
Jmax: 2,
RulesArr: [5]conceal.Rules{
mustParseRules(t, "<b 0xaabb>"),
},
})
return bind
}
func sendAndAssertRoundTrip(t *testing.T, sender, receiver net.Conn, packet []byte) {
t.Helper()
writeErr := make(chan error, 1)
go func() {
_, err := sender.Write(packet)
writeErr <- err
}()
got := readPacket(t, receiver, len(packet))
if !bytes.Equal(got, packet) {
t.Fatalf("round-trip payload mismatch")
}
if err := <-writeErr; err != nil {
t.Fatalf("write failed: %v", err)
}
}
func mustRecordConn(t *testing.T, conn net.Conn, pool *sync.Pool, opts conceal.MasqueradeOpts) *conceal.MasqueradeConn {
t.Helper()
recordConn, ok := conceal.NewMasqueradeConn(conn, pool, opts)
if !ok {
t.Fatal("expected record connection")
}
return recordConn
}
func mustParseRules(t *testing.T, spec string) conceal.Rules {
t.Helper()
rules, err := conceal.ParseRules(spec)
if err != nil {
t.Fatalf("parse rules %q: %v", spec, err)
}
return rules
}
func mustHeader(t *testing.T, spec string) *conceal.RangedHeader {
t.Helper()
header, err := conceal.NewRangedHeader(spec)
if err != nil {
t.Fatalf("parse header %q: %v", spec, err)
}
return header
}
func makeInitiationPacket() []byte {
packet := make([]byte, conceal.WireguardMsgInitiationSize)
binary.LittleEndian.PutUint32(packet[:4], conceal.WireguardMsgInitiationType)
for i := 4; i < len(packet); i++ {
packet[i] = byte(i)
}
return packet
}
func makeResponsePacket() []byte {
packet := make([]byte, conceal.WireguardMsgResponseSize)
binary.LittleEndian.PutUint32(packet[:4], conceal.WireguardMsgResponseType)
for i := 4; i < len(packet); i++ {
packet[i] = byte(i)
}
return packet
}
func makeTransportPacket() []byte {
packet := make([]byte, conceal.WireguardMsgTransportMinSize)
binary.LittleEndian.PutUint32(packet[:4], conceal.WireguardMsgTransportType)
for i := 4; i < len(packet); i++ {
packet[i] = byte(i)
}
return packet
}
func readPacket(t *testing.T, conn net.Conn, size int) []byte {
t.Helper()
buf := make([]byte, size)
n, err := conn.Read(buf)
if err != nil {
t.Fatalf("read packet: %v", err)
}
return slices.Clone(buf[:n])
}
func readRecord(t *testing.T, conn interface{ ReadRecord([]byte) (int, error) }, size int) []byte {
t.Helper()
buf := make([]byte, size)
n, err := conn.ReadRecord(buf)
if err != nil {
t.Fatalf("read record: %v", err)
}
return slices.Clone(buf[:n])
}
type recordingUDPConn struct {
writes [][]byte
}
func (c *recordingUDPConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
return 0, nil, net.ErrClosed
}
func (c *recordingUDPConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
c.writes = append(c.writes, bytes.Clone(p))
return len(p), nil
}
func (c *recordingUDPConn) Close() error {
return nil
}
func (c *recordingUDPConn) LocalAddr() net.Addr {
return &net.UDPAddr{}
}
func (c *recordingUDPConn) SetDeadline(t time.Time) error {
return nil
}
func (c *recordingUDPConn) SetReadDeadline(t time.Time) error {
return nil
}
func (c *recordingUDPConn) SetWriteDeadline(t time.Time) error {
return nil
}
func (c *recordingUDPConn) SyscallConn() (syscall.RawConn, error) {
return nil, nil
}
func (c *recordingUDPConn) ReadMsgUDP(b, oob []byte) (n, oobn, flags int, addr *net.UDPAddr, err error) {
return 0, 0, 0, nil, net.ErrClosed
}
func (c *recordingUDPConn) WriteMsgUDP(b, oob []byte, addr *net.UDPAddr) (n, oobn int, err error) {
c.writes = append(c.writes, bytes.Clone(b))
return len(b), len(oob), nil
}
+16
View File
@@ -9,10 +9,14 @@ package conn
import (
"errors"
"fmt"
"net"
"net/netip"
"reflect"
"runtime"
"strings"
"syscall"
"golang.org/x/net/ipv4"
)
const (
@@ -84,6 +88,18 @@ type Endpoint interface {
SrcIP() netip.Addr
}
type UDPConn interface {
net.PacketConn
SyscallConn() (syscall.RawConn, error)
ReadMsgUDP(b, oob []byte) (n, oobn, flags int, addr *net.UDPAddr, err error)
WriteMsgUDP(b, oob []byte, addr *net.UDPAddr) (n, oobn int, err error)
}
type LinuxPacketConn interface {
ReadBatch([]ipv4.Message, int) (int, error)
WriteBatch(ms []ipv4.Message, flags int) (n int, err error)
}
var (
ErrBindAlreadyOpen = errors.New("bind is already open")
ErrWrongEndpointType = errors.New("endpoint type does not correspond with bind type")
+3 -1
View File
@@ -7,4 +7,6 @@
package conn
func NewDefaultBind() Bind { return NewStdNetBind() }
func NewDefaultBind() Bind {
return NewMultibind(NewStdNetBind(), NewBindStream())
}
+7
View File
@@ -0,0 +1,7 @@
//go:build windows
package conn
func NewDefaultBind() Bind {
return NewMultibind(NewWinRingBind(), NewBindStream())
}
+1 -3
View File
@@ -8,8 +8,6 @@
package conn
import "net"
func supportsUDPOffload(_ *net.UDPConn) (txOffload, rxOffload bool) {
func supportsUDPOffload(_ UDPConn) (txOffload, rxOffload bool) {
return
}
+1 -3
View File
@@ -6,12 +6,10 @@
package conn
import (
"net"
"golang.org/x/sys/unix"
)
func supportsUDPOffload(conn *net.UDPConn) (txOffload, rxOffload bool) {
func supportsUDPOffload(conn UDPConn) (txOffload, rxOffload bool) {
rc, err := conn.SyscallConn()
if err != nil {
return
+4
View File
@@ -10,3 +10,7 @@ package conn
func (s *StdNetBind) SetMark(mark uint32) error {
return nil
}
func (b *BindStream) SetMark(mark uint32) error {
return nil
}
+35
View File
@@ -9,6 +9,7 @@ package conn
import (
"runtime"
"syscall"
"golang.org/x/sys/unix"
)
@@ -63,3 +64,37 @@ func (s *StdNetBind) SetMark(mark uint32) error {
}
return nil
}
func (b *BindStream) SetMark(mark uint32) error {
b.closeConnections()
ctrl := b.dialer.Control
b.dialer.Control = func(network, address string, c syscall.RawConn) error {
if ctrl != nil {
if err := ctrl(network, address, c); err != nil {
return err
}
}
err := c.Control(func(fd uintptr) {
unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, fwmarkIoctl, int(mark))
})
return err
}
ctrl = b.listenConfig.Control
b.listenConfig.Control = func(network, address string, c syscall.RawConn) error {
if ctrl != nil {
if err := ctrl(network, address, c); err != nil {
return err
}
}
err := c.Control(func(fd uintptr) {
unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, fwmarkIoctl, int(mark))
})
return err
}
return b.openConnections()
}
+51
View File
@@ -0,0 +1,51 @@
package conn
import (
"errors"
"sync"
)
var _ Bind = (*Multibind)(nil)
func NewMultibind(udp Bind, tcp Bind) *Multibind {
return &Multibind{
udp: udp,
tcp: tcp,
Bind: udp,
}
}
type Multibind struct {
Bind
udp Bind
tcp Bind
mutex sync.Mutex
}
func (mb *Multibind) SelectNetwork(proto string) error {
mb.mutex.Lock()
defer mb.mutex.Unlock()
if proto == "udp" {
mb.Bind = mb.udp
return nil
}
if proto == "tcp" {
mb.Bind = mb.tcp
return nil
}
return errors.New("unknown network")
}
func (mb *Multibind) Proto() string {
switch mb.Bind {
case mb.tcp:
return "tcp"
case mb.udp:
return "udp"
}
return "unknown"
}
+2 -2
View File
@@ -118,7 +118,7 @@ func (st *CookieChecker) CreateReply(
msg []byte,
recv uint32,
src []byte,
msgType uint32,
typ uint32,
) (*MessageCookieReply, error) {
st.RLock()
@@ -154,7 +154,7 @@ func (st *CookieChecker) CreateReply(
smac1 := smac2 - blake2s.Size128
reply := new(MessageCookieReply)
reply.Type = msgType
reply.Type = typ
reply.Receiver = recv
_, err := rand.Read(reply.Nonce[:])
+32 -31
View File
@@ -11,6 +11,7 @@ import (
"sync/atomic"
"time"
"github.com/amnezia-vpn/amneziawg-go/conceal"
"github.com/amnezia-vpn/amneziawg-go/conn"
"github.com/amnezia-vpn/amneziawg-go/ratelimiter"
"github.com/amnezia-vpn/amneziawg-go/rwcancel"
@@ -40,11 +41,15 @@ type Device struct {
net struct {
stopping sync.WaitGroup
sync.RWMutex
bind conn.Bind // bind interface
netlinkCancel *rwcancel.RWCancel
port uint16 // listening port
fwmark uint32 // mark value (0 = disabled)
brokenRoaming bool
bind conn.Bind // bind interface
netlinkCancel *rwcancel.RWCancel
port uint16 // listening port
fwmark uint32 // mark value (0 = disabled)
brokenRoaming bool
network string
framedOpts conceal.FramedOpts
preludeOpts conceal.PreludeOpts
masqueradeOpts conceal.MasqueradeOpts
}
staticIdentity struct {
@@ -89,28 +94,6 @@ type Device struct {
ipcMutex sync.RWMutex
closed chan struct{}
log *Logger
junk struct {
min int
max int
count int
}
headers struct {
init *magicHeader
cookie *magicHeader
response *magicHeader
transport *magicHeader
}
paddings struct {
init int
response int
cookie int
transport int
}
ipackets [5]*obfChain
}
// deviceState represents the state of a Device.
@@ -321,10 +304,8 @@ func NewDevice(tunDevice tun.Device, bind conn.Bind, logger *Logger) *Device {
device.rate.limiter.Init()
device.indexTable.Init()
device.headers.init = &magicHeader{start: MessageInitiationType, end: MessageInitiationType}
device.headers.response = &magicHeader{start: MessageResponseType, end: MessageResponseType}
device.headers.cookie = &magicHeader{start: MessageCookieReplyType, end: MessageCookieReplyType}
device.headers.transport = &magicHeader{start: MessageTransportType, end: MessageTransportType}
device.net.network = "udp"
device.net.framedOpts.HeaderCompat = true
device.PopulatePools()
@@ -515,6 +496,26 @@ func (device *Device) BindUpdate() error {
var recvFns []conn.ReceiveFunc
netc := &device.net
underlying := netc.bind
if multi, ok := netc.bind.(*conn.Multibind); ok {
if err := multi.SelectNetwork(netc.network); err != nil {
return err
}
underlying = multi.Bind
}
if framable, ok := underlying.(conn.Framable); ok {
framable.SetFramedOpts(netc.framedOpts)
}
if preludable, ok := underlying.(conn.Preludable); ok {
preludable.SetPreludeOpts(netc.preludeOpts)
}
if masqueradable, ok := underlying.(conn.Masqueradable); ok {
masqueradable.SetMasqueradeOpts(netc.masqueradeOpts)
}
recvFns, netc.port, err = netc.bind.Open(netc.port)
if err != nil {
netc.port = 0
+132
View File
@@ -12,11 +12,13 @@ import (
"fmt"
"io"
"math/rand"
"net"
"net/netip"
"os"
"os/signal"
"runtime"
"runtime/pprof"
"strconv"
"sync"
"testing"
"time"
@@ -101,6 +103,54 @@ func genConfigs(tb testing.TB, cfg ...string) (cfgs, endpointCfgs [2]string) {
return
}
func genTCPConfigs(tb testing.TB, ports [2]int, cfg ...string) (cfgs, endpointCfgs [2]string) {
var key1, key2 NoisePrivateKey
_, err := rand.Read(key1[:])
if err != nil {
tb.Errorf("unable to generate private key random bytes: %v", err)
}
_, err = rand.Read(key2[:])
if err != nil {
tb.Errorf("unable to generate private key random bytes: %v", err)
}
pub1, pub2 := key1.publicKey(), key2.publicKey()
args0 := append([]string(nil), cfg...)
args0 = append(args0, []string{
"private_key", hex.EncodeToString(key1[:]),
"listen_port", strconv.Itoa(ports[0]),
"replace_peers", "true",
"public_key", hex.EncodeToString(pub2[:]),
"protocol_version", "1",
"replace_allowed_ips", "true",
"allowed_ip", "1.0.0.2/32",
}...)
cfgs[0] = uapiCfg(args0...)
endpointCfgs[0] = uapiCfg(
"public_key", hex.EncodeToString(pub2[:]),
"endpoint", fmt.Sprintf("127.0.0.1:%d", ports[1]),
)
args1 := append([]string(nil), cfg...)
args1 = append(args1, []string{
"private_key", hex.EncodeToString(key2[:]),
"listen_port", strconv.Itoa(ports[1]),
"replace_peers", "true",
"public_key", hex.EncodeToString(pub1[:]),
"protocol_version", "1",
"replace_allowed_ips", "true",
"allowed_ip", "1.0.0.1/32",
}...)
cfgs[1] = uapiCfg(args1...)
endpointCfgs[1] = uapiCfg(
"public_key", hex.EncodeToString(pub1[:]),
"endpoint", fmt.Sprintf("127.0.0.1:%d", ports[0]),
)
return
}
// A testPair is a pair of testPeers.
type testPair [2]testPeer
@@ -213,6 +263,64 @@ func genTestPair(
return
}
func getFreeTCPPort(tb testing.TB) int {
tb.Helper()
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
tb.Fatalf("failed to allocate tcp port: %v", err)
}
defer listener.Close()
addr, ok := listener.Addr().(*net.TCPAddr)
if !ok {
tb.Fatalf("unexpected tcp addr type: %T", listener.Addr())
}
return addr.Port
}
func genTestPairTCP(tb testing.TB, extraCfg ...string) (pair testPair) {
ports := [2]int{getFreeTCPPort(tb), getFreeTCPPort(tb)}
cfg, endpointCfg := genTCPConfigs(tb, ports, extraCfg...)
var binds [2]conn.Bind
binds[0], binds[1] = conn.NewDefaultBind(), conn.NewDefaultBind()
for i := range pair {
p := &pair[i]
p.tun = tuntest.NewChannelTUN()
p.ip = netip.AddrFrom4([4]byte{1, 0, 0, byte(i + 1)})
level := LogLevelVerbose
if _, ok := tb.(*testing.B); ok && !testing.Verbose() {
level = LogLevelError
}
p.dev = NewDevice(p.tun.TUN(), binds[i], NewLogger(level, fmt.Sprintf("dev%d: ", i)))
if err := p.dev.IpcSet(cfg[i]); err != nil {
tb.Errorf("failed to configure tcp device %d: %v", i, err)
p.dev.Close()
continue
}
if err := p.dev.Up(); err != nil {
tb.Errorf("failed to bring up tcp device %d: %v", i, err)
p.dev.Close()
continue
}
}
for i := range pair {
p := &pair[i]
if err := p.dev.IpcSet(endpointCfg[i]); err != nil {
tb.Errorf("failed to configure tcp endpoint %d: %v", i, err)
p.dev.Close()
continue
}
tb.Cleanup(p.dev.Close)
}
return
}
func TestTwoDevicePing(t *testing.T) {
goroutineLeakCheck(t)
pair := genTestPair(t, true)
@@ -249,6 +357,30 @@ func TestAWGDevicePing(t *testing.T) {
})
}
func TestAWGDevicePingTCPConceal(t *testing.T) {
goroutineLeakCheck(t)
pair := genTestPairTCP(t,
"network", "tcp",
"format_in", "<b 0xfeed><dz be 2><d>",
"format_out", "<b 0xfeed><dz be 2><d>",
"i1", "<b 0xaabb>",
"i2", "<b 0xccdd>",
"s1", "15",
"s2", "18",
"s4", "25",
"h1", "123456-123500",
"h2", "67543-67550",
"h4", "32345-32350",
)
t.Run("ping 1.0.0.1", func(t *testing.T) {
pair.Send(t, Ping, nil)
})
t.Run("ping 1.0.0.2", func(t *testing.T) {
pair.Send(t, Pong, nil)
})
}
// Needs to be stopped with Ctrl-C
func TestAWGHandshakeDevicePing(t *testing.T) {
t.Skip("This test is intended to be run manually, not as part of the test suite.")
+39 -12
View File
@@ -53,11 +53,10 @@ const (
)
const (
MessageUnknownType uint32 = 0
MessageInitiationType uint32 = 1
MessageResponseType uint32 = 2
MessageCookieReplyType uint32 = 3
MessageTransportType uint32 = 4
MessageInitiationType = 1
MessageResponseType = 2
MessageCookieReplyType = 3
MessageTransportType = 4
)
const (
@@ -194,10 +193,16 @@ func (device *Device) CreateMessageInitiation(peer *Peer) (*MessageInitiation, e
handshake.mixHash(handshake.remoteStatic[:])
msgType := device.headers.init.Generate()
opts := &device.net.framedOpts
var typ uint32
if opts.HeaderCompat && opts.H1 != nil {
typ = opts.H1.Generate()
} else {
typ = MessageInitiationType
}
msg := MessageInitiation{
Type: msgType,
Type: typ,
Ephemeral: handshake.localEphemeral.publicKey(),
}
@@ -253,8 +258,15 @@ func (device *Device) ConsumeMessageInitiation(msg *MessageInitiation) *Peer {
chainKey [blake2s.Size]byte
)
if msg.Type != MessageInitiationType {
return nil
opts := &device.net.framedOpts
if opts.HeaderCompat && opts.H1 != nil {
if !opts.H1.Validate(msg.Type) {
return nil
}
} else {
if msg.Type != MessageInitiationType {
return nil
}
}
device.staticIdentity.RLock()
@@ -369,8 +381,16 @@ func (device *Device) CreateMessageResponse(peer *Peer) (*MessageResponse, error
return nil, err
}
opts := &device.net.framedOpts
var typ uint32
if opts.HeaderCompat && opts.H2 != nil {
typ = opts.H2.Generate()
} else {
typ = MessageResponseType
}
var msg MessageResponse
msg.Type = device.headers.response.Generate()
msg.Type = typ
msg.Sender = handshake.localIndex
msg.Receiver = handshake.remoteIndex
@@ -420,8 +440,15 @@ func (device *Device) CreateMessageResponse(peer *Peer) (*MessageResponse, error
}
func (device *Device) ConsumeMessageResponse(msg *MessageResponse) *Peer {
if msg.Type != MessageResponseType {
return nil
opts := &device.net.framedOpts
if opts.HeaderCompat && opts.H2 != nil {
if !opts.H2.Validate(msg.Type) {
return nil
}
} else {
if msg.Type != MessageResponseType {
return nil
}
}
// lookup handshake by receiver
-140
View File
@@ -1,140 +0,0 @@
package device
import (
"errors"
"fmt"
"strings"
)
type obfBuilder func(val string) (obf, error)
var obfBuilders = map[string]obfBuilder{
"b": newBytesObf,
"t": newTimestampObf,
"r": newRandObf,
"rc": newRandCharObf,
"rd": newRandDigitsObf,
"d": newDataObf,
"ds": newDataStringObf,
"dz": newDataSizeObf,
}
type obf interface {
Obfuscate(dst, src []byte)
Deobfuscate(dst, src []byte) bool
ObfuscatedLen(srcLen int) int
DeobfuscatedLen(srcLen int) int
}
type obfChain struct {
Spec string
obfs []obf
}
func newObfChain(spec string) (*obfChain, error) {
var (
obfs []obf
errs []error
)
remaining := spec[:]
for {
start := strings.IndexByte(remaining, '<')
if start == -1 {
break
}
end := strings.IndexByte(remaining[start:], '>')
if end == -1 {
return nil, errors.New("missing enclosing >")
}
end += start
tag := remaining[start+1 : end]
parts := strings.Fields(tag)
if len(parts) == 0 {
errs = append(errs, errors.New("empty tag"))
remaining = remaining[end+1:]
continue
}
key := parts[0]
builder, ok := obfBuilders[key]
if !ok {
errs = append(errs, fmt.Errorf("unknown tag <%s>", key))
remaining = remaining[end+1:]
continue
}
val := ""
if len(parts) > 1 {
val = parts[1]
}
o, err := builder(val)
if err != nil {
errs = append(errs, fmt.Errorf("failed to build <%s>: %w", key, err))
remaining = remaining[end+1:]
continue
}
obfs = append(obfs, o)
remaining = remaining[end+1:]
}
if len(errs) > 0 {
return nil, errors.Join(errs...)
}
return &obfChain{
Spec: spec,
obfs: obfs,
}, nil
}
func (c *obfChain) Obfuscate(dst, src []byte) {
written := 0
for _, o := range c.obfs {
obfLen := o.ObfuscatedLen(len(src))
o.Obfuscate(dst[written:written+obfLen], src)
written += obfLen
}
}
func (c *obfChain) Deobfuscate(dst, src []byte) bool {
dynamicLen := len(src) - c.ObfuscatedLen(0)
written, read := 0, 0
for _, o := range c.obfs {
deobfLen := o.DeobfuscatedLen(dynamicLen)
obfLen := o.ObfuscatedLen(deobfLen)
if !o.Deobfuscate(dst[written:written+deobfLen], src[read:read+obfLen]) {
return false
}
written += deobfLen
read += obfLen
}
return true
}
func (c *obfChain) ObfuscatedLen(n int) int {
total := 0
for _, o := range c.obfs {
total += o.ObfuscatedLen(n)
}
return total
}
func (c *obfChain) DeobfuscatedLen(n int) int {
dynamicLen := n - c.ObfuscatedLen(0)
total := 0
for _, o := range c.obfs {
total += o.DeobfuscatedLen(dynamicLen)
}
return total
}
-47
View File
@@ -1,47 +0,0 @@
package device
import (
"bytes"
"encoding/hex"
"errors"
"strings"
)
func newBytesObf(val string) (obf, error) {
val = strings.TrimPrefix(val, "0x")
if len(val) == 0 {
return nil, errors.New("empty argument")
}
if len(val)%2 != 0 {
return nil, errors.New("odd amount of symbols")
}
bytes, err := hex.DecodeString(val)
if err != nil {
return nil, err
}
return &bytesObf{data: bytes}, nil
}
type bytesObf struct {
data []byte
}
func (o *bytesObf) Obfuscate(dst, src []byte) {
copy(dst, o.data)
}
func (o *bytesObf) Deobfuscate(dst, src []byte) bool {
return bytes.Equal(o.data, src[:o.ObfuscatedLen(0)])
}
func (o *bytesObf) ObfuscatedLen(srcLen int) int {
return len(o.data)
}
func (o *bytesObf) DeobfuscatedLen(srcLen int) int {
return 0
}
-25
View File
@@ -1,25 +0,0 @@
package device
func newDataObf(val string) (obf, error) {
return &dataObf{}, nil
}
type dataObf struct {
}
func (obf *dataObf) Obfuscate(dst, src []byte) {
copy(dst, src)
}
func (obf *dataObf) Deobfuscate(dst, src []byte) bool {
copy(dst, src)
return true
}
func (o *dataObf) ObfuscatedLen(n int) int {
return n
}
func (o *dataObf) DeobfuscatedLen(n int) int {
return n
}
-38
View File
@@ -1,38 +0,0 @@
package device
import "strconv"
func newDataSizeObf(val string) (obf, error) {
length, err := strconv.Atoi(val)
if err != nil {
return nil, err
}
return &dataSizeObf{
length: length,
}, nil
}
type dataSizeObf struct {
length int
}
func (o *dataSizeObf) Obfuscate(dst, src []byte) {
srcLen := len(src)
for i := o.length - 1; i >= 0; i-- {
dst[i] = byte(srcLen & 0xFF)
srcLen >>= 8
}
}
func (o *dataSizeObf) Deobfuscate(dst, src []byte) bool {
return true
}
func (o *dataSizeObf) ObfuscatedLen(srcLen int) int {
return o.length
}
func (o *dataSizeObf) DeobfuscatedLen(srcLen int) int {
return 0
}
-29
View File
@@ -1,29 +0,0 @@
package device
import (
"encoding/base64"
)
func newDataStringObf(val string) (obf, error) {
return &dataStringObf{}, nil
}
type dataStringObf struct {
}
func (o *dataStringObf) Obfuscate(dst, src []byte) {
base64.RawStdEncoding.Encode(dst, src)
}
func (o *dataStringObf) Deobfuscate(dst, src []byte) bool {
base64.RawStdEncoding.Decode(dst, src)
return true
}
func (o *dataStringObf) ObfuscatedLen(n int) int {
return base64.RawStdEncoding.EncodedLen(n)
}
func (o *dataStringObf) DeobfuscatedLen(n int) int {
return base64.RawStdEncoding.DecodedLen(n)
}
-39
View File
@@ -1,39 +0,0 @@
package device
import (
"crypto/rand"
"strconv"
)
func newRandObf(val string) (obf, error) {
length, err := strconv.Atoi(val)
if err != nil {
return nil, err
}
return &randObf{
length: length,
}, nil
}
type randObf struct {
length int
}
func (o *randObf) Obfuscate(dst, src []byte) {
rand.Read(dst[:o.length])
}
func (o *randObf) Deobfuscate(dst, src []byte) bool {
// there is no way to validate randomness :)
// assume that it is always true
return true
}
func (o *randObf) ObfuscatedLen(n int) int {
return o.length
}
func (o *randObf) DeobfuscatedLen(n int) int {
return 0
}
-48
View File
@@ -1,48 +0,0 @@
package device
import (
"crypto/rand"
"strconv"
"unicode"
)
const chars52 = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
func newRandCharObf(val string) (obf, error) {
length, err := strconv.Atoi(val)
if err != nil {
return nil, err
}
return &randCharObf{
length: length,
}, nil
}
type randCharObf struct {
length int
}
func (o *randCharObf) Obfuscate(dst, src []byte) {
rand.Read(dst[:o.length])
for i := range dst[:o.length] {
dst[i] = chars52[dst[i]%52]
}
}
func (o *randCharObf) Deobfuscate(dst, src []byte) bool {
for _, b := range src[:o.length] {
if !unicode.IsLetter(rune(b)) {
return false
}
}
return true
}
func (o *randCharObf) ObfuscatedLen(n int) int {
return o.length
}
func (o *randCharObf) DeobfuscatedLen(n int) int {
return 0
}
-48
View File
@@ -1,48 +0,0 @@
package device
import (
"crypto/rand"
"strconv"
"unicode"
)
const digits10 = "0123456789"
func newRandDigitsObf(val string) (obf, error) {
length, err := strconv.Atoi(val)
if err != nil {
return nil, err
}
return &randDigitObf{
length: length,
}, nil
}
type randDigitObf struct {
length int
}
func (o *randDigitObf) Obfuscate(dst, src []byte) {
rand.Read(dst[:o.length])
for i := range dst[:o.length] {
dst[i] = digits10[dst[i]%10]
}
}
func (o *randDigitObf) Deobfuscate(dst, src []byte) bool {
for _, b := range src[:o.length] {
if !unicode.IsDigit(rune(b)) {
return false
}
}
return true
}
func (o *randDigitObf) ObfuscatedLen(n int) int {
return o.length
}
func (o *randDigitObf) DeobfuscatedLen(n int) int {
return 0
}
-31
View File
@@ -1,31 +0,0 @@
package device
import (
"encoding/binary"
"time"
)
func newTimestampObf(_ string) (obf, error) {
return &timestampObf{}, nil
}
type timestampObf struct{}
func (o *timestampObf) Obfuscate(dst, src []byte) {
t := uint32(time.Now().Unix())
binary.BigEndian.PutUint32(dst, t)
}
func (o *timestampObf) Deobfuscate(dst, src []byte) bool {
// replay attack check?
// requires time to be always synchronized
return true
}
func (o *timestampObf) ObfuscatedLen(n int) int {
return 4
}
func (o *timestampObf) DeobfuscatedLen(n int) int {
return 0
}
+21 -87
View File
@@ -70,10 +70,7 @@ func (peer *Peer) keepKeyFreshReceiving() {
* Every time the bind is updated a new routine is started for
* IPv4 and IPv6 (separately)
*/
func (device *Device) RoutineReceiveIncoming(
maxBatchSize int,
recv conn.ReceiveFunc,
) {
func (device *Device) RoutineReceiveIncoming(maxBatchSize int, recv conn.ReceiveFunc) {
recvName := recv.PrettyName()
defer func() {
device.log.Verbosef("Routine: receive incoming %s - stopped", recvName)
@@ -97,13 +94,13 @@ func (device *Device) RoutineReceiveIncoming(
elemsByPeer = make(map[*Peer]*QueueInboundElementsContainer, maxBatchSize)
)
for i := range maxBatchSize {
for i := range bufsArrs {
bufsArrs[i] = device.GetMessageBuffer()
bufs[i] = bufsArrs[i][:]
}
defer func() {
for i := range maxBatchSize {
for i := 0; i < maxBatchSize; i++ {
if bufsArrs[i] != nil {
device.PutMessageBuffer(bufsArrs[i])
}
@@ -136,13 +133,21 @@ func (device *Device) RoutineReceiveIncoming(
}
// check size of packet
packet := bufsArrs[i][:size]
// get message padding and type based on information from S1-S4 and H1-H4
msgType, padding := device.DeterminePacketTypeAndPadding(packet, MessageUnknownType)
if padding > 0 {
copy(packet, packet[padding:])
packet = packet[:len(packet)-padding]
packet := bufsArrs[i][:size]
msgType := binary.LittleEndian.Uint32(packet[:4])
opts := &device.net.framedOpts
if opts.HeaderCompat {
if opts.H1 != nil && opts.H1.Validate(msgType) {
msgType = MessageInitiationType
} else if opts.H2 != nil && opts.H2.Validate(msgType) {
msgType = MessageResponseType
} else if opts.H3 != nil && opts.H3.Validate(msgType) {
msgType = MessageCookieReplyType
} else if opts.H4 != nil && opts.H4.Validate(msgType) {
msgType = MessageTransportType
}
}
switch msgType {
@@ -312,14 +317,9 @@ func (device *Device) RoutineHandshake(id int) {
// consume reply
if peer := entry.peer; peer.isRunning.Load() {
device.log.Verbosef(
"Receiving cookie response from %s",
elem.endpoint.DstToString(),
)
device.log.Verbosef("Receiving cookie response from %s", elem.endpoint.DstToString())
if !peer.cookieGenerator.ConsumeReply(&reply) {
device.log.Verbosef(
"Could not decrypt invalid cookie response",
)
device.log.Verbosef("Could not decrypt invalid cookie response")
}
}
@@ -370,9 +370,6 @@ func (device *Device) RoutineHandshake(id int) {
goto skip
}
// have to reassign msgType for ranged msgType to work
msg.Type = elem.msgType
// consume initiation
peer := device.ConsumeMessageInitiation(&msg)
if peer == nil {
@@ -405,9 +402,6 @@ func (device *Device) RoutineHandshake(id int) {
goto skip
}
// have to reassign msgType for ranged msgType to work
msg.Type = elem.msgType
// consume response
peer := device.ConsumeMessageResponse(&msg)
@@ -522,17 +516,11 @@ func (peer *Peer) RoutineSequentialReceiver(maxBatchSize int) {
}
default:
device.log.Verbosef(
"Packet with invalid IP version from %v",
peer,
)
device.log.Verbosef("Packet with invalid IP version from %v", peer)
continue
}
bufs = append(
bufs,
elem.buffer[:MessageTransportOffsetContent+len(elem.packet)],
)
bufs = append(bufs, elem.buffer[:MessageTransportOffsetContent+len(elem.packet)])
}
peer.rxBytes.Add(rxBytesLen)
@@ -559,57 +547,3 @@ func (peer *Peer) RoutineSequentialReceiver(maxBatchSize int) {
device.PutInboundElementsContainer(elemsContainer)
}
}
func (device *Device) DeterminePacketTypeAndPadding(packet []byte, expectedType uint32) (uint32, int) {
size := len(packet)
if expectedType == MessageUnknownType || expectedType == MessageInitiationType {
padding := device.paddings.init
header := device.headers.init
if size == padding+MessageInitiationSize {
data := packet[padding:]
if header.Validate(binary.LittleEndian.Uint32(data)) {
return MessageInitiationType, padding
}
}
}
if expectedType == MessageUnknownType || expectedType == MessageResponseType {
padding := device.paddings.response
header := device.headers.response
if size == padding+MessageResponseSize {
data := packet[padding:]
if header.Validate(binary.LittleEndian.Uint32(data)) {
return MessageResponseType, padding
}
}
}
if expectedType == MessageUnknownType || expectedType == MessageCookieReplyType {
padding := device.paddings.cookie
header := device.headers.cookie
if size == padding+MessageCookieReplySize {
data := packet[padding:]
if header.Validate(binary.LittleEndian.Uint32(data)) {
return MessageCookieReplyType, padding
}
}
}
if expectedType == MessageUnknownType || expectedType == MessageTransportType {
padding := device.paddings.transport
header := device.headers.transport
if size >= padding+MessageTransportHeaderSize {
data := packet[padding:]
if header.Validate(binary.LittleEndian.Uint32(data)) {
return MessageTransportType, padding
}
}
}
return MessageUnknownType, 0
}
+18 -70
View File
@@ -7,10 +7,8 @@ package device
import (
"bytes"
"crypto/rand"
"encoding/binary"
"errors"
"math/big"
"net"
"os"
"sync"
@@ -126,29 +124,6 @@ func (peer *Peer) SendHandshakeInitiation(isRetry bool) error {
return err
}
var sendBuffer [][]byte
for _, ipacket := range peer.device.ipackets {
if ipacket != nil {
buf := make([]byte, ipacket.ObfuscatedLen(0))
ipacket.Obfuscate(buf, nil)
sendBuffer = append(sendBuffer, buf)
}
}
jc := peer.device.junk.count
jmin := peer.device.junk.min
jmax := peer.device.junk.max
for i := 0; i < jc; i++ {
nBig, _ := rand.Int(rand.Reader, big.NewInt(int64(jmax-jmin+1)))
n := int(nBig.Int64()) + jmin
buf := make([]byte, n)
rand.Read(buf)
sendBuffer = append(sendBuffer, buf)
}
var buf [MessageInitiationSize]byte
writer := bytes.NewBuffer(buf[:0])
binary.Write(writer, binary.LittleEndian, msg)
@@ -158,16 +133,7 @@ func (peer *Peer) SendHandshakeInitiation(isRetry bool) error {
peer.timersAnyAuthenticatedPacketTraversal()
peer.timersAnyAuthenticatedPacketSent()
if padding := peer.device.paddings.init; padding > 0 {
buf := make([]byte, padding+len(packet))
rand.Read(buf[:padding])
copy(buf[padding:], packet)
packet = buf
}
sendBuffer = append(sendBuffer, packet)
err = peer.SendBuffers(sendBuffer)
err = peer.SendBuffers([][]byte{packet})
if err != nil {
peer.device.log.Errorf("%v - Failed to send handshake initiation: %v", peer, err)
}
@@ -206,13 +172,6 @@ func (peer *Peer) SendHandshakeResponse() error {
peer.timersAnyAuthenticatedPacketTraversal()
peer.timersAnyAuthenticatedPacketSent()
if padding := peer.device.paddings.response; padding > 0 {
buf := make([]byte, padding+len(packet))
rand.Read(buf[:padding])
copy(buf[padding:], packet)
packet = buf
}
// TODO: allocation could be avoided
err = peer.SendBuffers([][]byte{packet})
if err != nil {
@@ -224,15 +183,16 @@ func (peer *Peer) SendHandshakeResponse() error {
func (device *Device) SendHandshakeCookie(initiatingElem *QueueHandshakeElement) error {
device.log.Verbosef("Sending cookie response for denied handshake message for %v", initiatingElem.endpoint.DstToString())
sender := binary.LittleEndian.Uint32(initiatingElem.packet[4:8])
msgType := device.headers.cookie.Generate()
opts := &device.net.framedOpts
var typ uint32
if opts.HeaderCompat && opts.H3 != nil {
typ = opts.H3.Generate()
} else {
typ = MessageCookieReplyType
}
reply, err := device.cookieChecker.CreateReply(
initiatingElem.packet,
sender,
initiatingElem.endpoint.DstToBytes(),
msgType,
)
sender := binary.LittleEndian.Uint32(initiatingElem.packet[4:8])
reply, err := device.cookieChecker.CreateReply(initiatingElem.packet, sender, initiatingElem.endpoint.DstToBytes(), typ)
if err != nil {
device.log.Errorf("Failed to create cookie reply: %v", err)
return err
@@ -243,13 +203,6 @@ func (device *Device) SendHandshakeCookie(initiatingElem *QueueHandshakeElement)
binary.Write(writer, binary.LittleEndian, reply)
packet := writer.Bytes()
if padding := device.paddings.cookie; padding > 0 {
buf := make([]byte, padding+len(packet))
rand.Read(buf[:padding])
copy(buf[padding:], packet)
packet = buf
}
// TODO: allocation could be avoided
device.net.bind.Send([][]byte{packet}, initiatingElem.endpoint)
return nil
@@ -515,9 +468,15 @@ func (device *Device) RoutineEncryption(id int) {
fieldReceiver := header[4:8]
fieldNonce := header[8:16]
msgType := device.headers.transport.Generate()
opts := &device.net.framedOpts
var typ uint32
if opts.HeaderCompat && opts.H4 != nil {
typ = opts.H4.Generate()
} else {
typ = MessageTransportType
}
binary.LittleEndian.PutUint32(fieldType, msgType)
binary.LittleEndian.PutUint32(fieldType, typ)
binary.LittleEndian.PutUint32(fieldReceiver, elem.keypair.remoteIndex)
binary.LittleEndian.PutUint64(fieldNonce, elem.nonce)
@@ -574,16 +533,6 @@ func (peer *Peer) RoutineSequentialSender(maxBatchSize int) {
for _, elem := range elemsContainer.elems {
if len(elem.packet) != MessageKeepaliveSize {
dataSent = true
if padding := device.paddings.transport; padding > 0 {
// elem.packet is stored at the start of elem.buffer
// with zero padding
for i := len(elem.packet) - 1; i >= 0; i-- {
elem.buffer[i+padding] = elem.buffer[i]
}
rand.Read(elem.buffer[:padding])
elem.packet = elem.buffer[:padding+len(elem.packet)]
}
}
bufs = append(bufs, elem.packet)
}
@@ -595,7 +544,6 @@ func (peer *Peer) RoutineSequentialSender(maxBatchSize int) {
if dataSent {
peer.timersDataSent()
}
for _, elem := range elemsContainer.elems {
device.PutMessageBuffer(elem.buffer)
device.PutOutboundElement(elem)
+231 -110
View File
@@ -18,6 +18,7 @@ import (
"sync"
"time"
"github.com/amnezia-vpn/amneziawg-go/conceal"
"github.com/amnezia-vpn/amneziawg-go/ipc"
)
@@ -97,56 +98,70 @@ func (device *Device) IpcGetOperation(w io.Writer) error {
sendf("fwmark=%d", device.net.fwmark)
}
if device.junk.count != 0 {
sendf("jc=%d", device.junk.count)
if device.net.preludeOpts.Jc != 0 {
sendf("jc=%d", device.net.preludeOpts.Jc)
}
if device.junk.min != 0 {
sendf("jmin=%d", device.junk.min)
if device.net.preludeOpts.Jmin != 0 {
sendf("jmin=%d", device.net.preludeOpts.Jmin)
}
if device.junk.max != 0 {
sendf("jmax=%d", device.junk.max)
if device.net.preludeOpts.Jmax != 0 {
sendf("jmax=%d", device.net.preludeOpts.Jmax)
}
if device.paddings.init != 0 {
sendf("s1=%d", device.paddings.init)
if device.net.framedOpts.S1 != 0 {
sendf("s1=%d", device.net.framedOpts.S1)
}
if device.paddings.response != 0 {
sendf("s2=%d", device.paddings.response)
if device.net.framedOpts.S2 != 0 {
sendf("s2=%d", device.net.framedOpts.S2)
}
if device.paddings.cookie != 0 {
sendf("s3=%d", device.paddings.cookie)
if device.net.framedOpts.S3 != 0 {
sendf("s3=%d", device.net.framedOpts.S3)
}
if device.paddings.transport != 0 {
sendf("s4=%d", device.paddings.transport)
if device.net.framedOpts.S4 != 0 {
sendf("s4=%d", device.net.framedOpts.S4)
}
if device.headers.init != nil {
sendf("h1=%s", device.headers.init.GenSpec())
if device.net.framedOpts.H1 != nil {
sendf("h1=%s", device.net.framedOpts.H1.GenSpec())
}
if device.headers.response != nil {
sendf("h2=%s", device.headers.response.GenSpec())
if device.net.framedOpts.H2 != nil {
sendf("h2=%s", device.net.framedOpts.H2.GenSpec())
}
if device.headers.cookie != nil {
sendf("h3=%s", device.headers.cookie.GenSpec())
if device.net.framedOpts.H3 != nil {
sendf("h3=%s", device.net.framedOpts.H3.GenSpec())
}
if device.headers.transport != nil {
sendf("h4=%s", device.headers.transport.GenSpec())
if device.net.framedOpts.H4 != nil {
sendf("h4=%s", device.net.framedOpts.H4.GenSpec())
}
for i, ipacket := range device.ipackets {
if ipacket != nil {
sendf("i%d=%s", i+1, ipacket.Spec)
for i, rules := range device.net.preludeOpts.RulesArr {
if rules != nil {
sendf("i%d=%s", i+1, rules.Spec())
}
}
if len(device.net.network) > 0 {
sendf("network=%s", device.net.network)
}
if device.net.masqueradeOpts.RulesIn != nil {
sendf("format_in=%s", device.net.masqueradeOpts.RulesIn.Spec())
}
if device.net.masqueradeOpts.RulesOut != nil {
sendf("format_out=%s", device.net.masqueradeOpts.RulesOut.Spec())
}
sendf("header_compat=%s", strconv.FormatBool(device.net.framedOpts.HeaderCompat))
for _, peer := range device.peers.keyMap {
// Serialize peer state.
peer.handshake.mutex.RLock()
@@ -197,7 +212,6 @@ func (device *Device) IpcSetOperation(r io.Reader) (err error) {
}
}()
ipcDev := new(ipcSetDevice)
peer := new(ipcSetPeer)
deviceConfig := true
@@ -206,10 +220,6 @@ func (device *Device) IpcSetOperation(r io.Reader) (err error) {
line := scanner.Text()
if line == "" {
// Blank line means terminate operation.
err := ipcDev.mergeWithDevice(device)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to merge with device: %w", err)
}
peer.handlePostConfig()
return nil
}
@@ -245,10 +255,6 @@ func (device *Device) IpcSetOperation(r io.Reader) (err error) {
return err
}
}
err = ipcDev.mergeWithDevice(device)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to merge with device: %w", err)
}
peer.handlePostConfig()
if err := scanner.Err(); err != nil {
@@ -316,7 +322,11 @@ func (device *Device) handleDeviceLine(key, value string) error {
return ipcErrorf(ipc.IpcErrorInvalid, "jc must be a positive value")
}
device.log.Verbosef("UAPI: Updating junk count")
device.junk.count = jc
device.net.preludeOpts.Jc = jc
if err := device.BindUpdate(); err != nil {
return ipcErrorf(ipc.IpcErrorPortInUse, "failed to set junk count: %w", err)
}
case "jmin":
jmin, err := strconv.Atoi(value)
@@ -326,8 +336,13 @@ func (device *Device) handleDeviceLine(key, value string) error {
if jmin <= 0 {
return ipcErrorf(ipc.IpcErrorInvalid, "jmin must be a positive value")
}
device.log.Verbosef("UAPI: Updating junk min")
device.junk.min = jmin
device.net.preludeOpts.Jmin = jmin
if err := device.BindUpdate(); err != nil {
return ipcErrorf(ipc.IpcErrorPortInUse, "failed to set junk min: %w", err)
}
case "jmax":
jmax, err := strconv.Atoi(value)
@@ -337,8 +352,13 @@ func (device *Device) handleDeviceLine(key, value string) error {
if jmax <= 0 {
return ipcErrorf(ipc.IpcErrorInvalid, "jmax must be a positive value")
}
device.log.Verbosef("UAPI: Updating junk max")
device.junk.max = jmax
device.net.preludeOpts.Jmax = jmax
if err := device.BindUpdate(); err != nil {
return ipcErrorf(ipc.IpcErrorPortInUse, "failed to set junk max: %w", err)
}
case "s1":
padding, err := strconv.Atoi(value)
@@ -348,8 +368,13 @@ func (device *Device) handleDeviceLine(key, value string) error {
if padding < 0 {
return ipcErrorf(ipc.IpcErrorInvalid, "s1 must be non-negative")
}
device.log.Verbosef("UAPI: Updating s1 padding")
device.paddings.init = padding
device.net.framedOpts.S1 = padding
if err := device.BindUpdate(); err != nil {
return ipcErrorf(ipc.IpcErrorPortInUse, "failed to set s1: %w", err)
}
case "s2":
padding, err := strconv.Atoi(value)
@@ -359,8 +384,13 @@ func (device *Device) handleDeviceLine(key, value string) error {
if padding < 0 {
return ipcErrorf(ipc.IpcErrorInvalid, "s2 must be non-negative")
}
device.log.Verbosef("UAPI: Updating s2 padding")
device.paddings.response = padding
device.net.framedOpts.S2 = padding
if err := device.BindUpdate(); err != nil {
return ipcErrorf(ipc.IpcErrorPortInUse, "failed to set s2: %w", err)
}
case "s3":
padding, err := strconv.Atoi(value)
@@ -370,8 +400,13 @@ func (device *Device) handleDeviceLine(key, value string) error {
if padding < 0 {
return ipcErrorf(ipc.IpcErrorInvalid, "s3 must be non-negative")
}
device.log.Verbosef("UAPI: Updating s3 padding")
device.paddings.cookie = padding
device.net.framedOpts.S3 = padding
if err := device.BindUpdate(); err != nil {
return ipcErrorf(ipc.IpcErrorPortInUse, "failed to set s3: %w", err)
}
case "s4":
padding, err := strconv.Atoi(value)
@@ -381,71 +416,203 @@ func (device *Device) handleDeviceLine(key, value string) error {
if padding < 0 {
return ipcErrorf(ipc.IpcErrorInvalid, "s4 must be non-negative")
}
device.log.Verbosef("UAPI: Updating s4 padding")
device.paddings.transport = padding
device.net.framedOpts.S4 = padding
if err := device.BindUpdate(); err != nil {
return ipcErrorf(ipc.IpcErrorPortInUse, "failed to set s4: %w", err)
}
case "h1":
header, err := newMagicHeader(value)
header, err := conceal.NewRangedHeader(value)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse H1: %w", err)
}
device.headers.init = header
opts := device.net.framedOpts
opts.H1 = header
if opts.HasIntersections() {
return ipcErrorf(ipc.IpcErrorInvalid, "headers must not overlap")
}
device.log.Verbosef("UAPI: Updating h1 header")
device.net.framedOpts.H1 = header
if err := device.BindUpdate(); err != nil {
return ipcErrorf(ipc.IpcErrorPortInUse, "failed to set h1: %w", err)
}
case "h2":
header, err := newMagicHeader(value)
header, err := conceal.NewRangedHeader(value)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse H2: %w", err)
}
device.headers.response = header
opts := device.net.framedOpts
opts.H2 = header
if opts.HasIntersections() {
return ipcErrorf(ipc.IpcErrorInvalid, "headers must not overlap")
}
device.log.Verbosef("UAPI: Updating h2 header")
device.net.framedOpts.H2 = header
if err := device.BindUpdate(); err != nil {
return ipcErrorf(ipc.IpcErrorPortInUse, "failed to set h2: %w", err)
}
case "h3":
header, err := newMagicHeader(value)
header, err := conceal.NewRangedHeader(value)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse H3: %w", err)
}
device.headers.cookie = header
opts := device.net.framedOpts
opts.H3 = header
if opts.HasIntersections() {
return ipcErrorf(ipc.IpcErrorInvalid, "headers must not overlap")
}
device.log.Verbosef("UAPI: Updating h3 header")
device.net.framedOpts.H3 = header
if err := device.BindUpdate(); err != nil {
return ipcErrorf(ipc.IpcErrorPortInUse, "failed to set h3: %w", err)
}
case "h4":
header, err := newMagicHeader(value)
header, err := conceal.NewRangedHeader(value)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse H4: %w", err)
}
device.headers.transport = header
opts := device.net.framedOpts
opts.H4 = header
if opts.HasIntersections() {
return ipcErrorf(ipc.IpcErrorInvalid, "headers must not overlap")
}
device.log.Verbosef("UAPI: Updating h4 header")
device.net.framedOpts.H4 = header
if err := device.BindUpdate(); err != nil {
return ipcErrorf(ipc.IpcErrorPortInUse, "failed to set h4: %w", err)
}
case "i1":
chain, err := newObfChain(value)
rules, err := conceal.ParseRules(value)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse I1: %w", err)
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse i1: %w", err)
}
device.net.preludeOpts.RulesArr[0] = rules
if err := device.BindUpdate(); err != nil {
return ipcErrorf(ipc.IpcErrorPortInUse, "failed to set i1: %w", err)
}
device.ipackets[0] = chain
case "i2":
chain, err := newObfChain(value)
rules, err := conceal.ParseRules(value)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse I2: %w", err)
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse i2: %w", err)
}
device.net.preludeOpts.RulesArr[1] = rules
if err := device.BindUpdate(); err != nil {
return ipcErrorf(ipc.IpcErrorPortInUse, "failed to set i2: %w", err)
}
device.ipackets[1] = chain
case "i3":
chain, err := newObfChain(value)
rules, err := conceal.ParseRules(value)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse I3: %w", err)
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse i3: %w", err)
}
device.net.preludeOpts.RulesArr[2] = rules
if err := device.BindUpdate(); err != nil {
return ipcErrorf(ipc.IpcErrorPortInUse, "failed to set i3: %w", err)
}
device.ipackets[2] = chain
case "i4":
chain, err := newObfChain(value)
rules, err := conceal.ParseRules(value)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse I4: %w", err)
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse i4: %w", err)
}
device.net.preludeOpts.RulesArr[3] = rules
if err := device.BindUpdate(); err != nil {
return ipcErrorf(ipc.IpcErrorPortInUse, "failed to set i4: %w", err)
}
device.ipackets[3] = chain
case "i5":
chain, err := newObfChain(value)
rules, err := conceal.ParseRules(value)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse I5: %w", err)
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse i5: %w", err)
}
device.net.preludeOpts.RulesArr[4] = rules
if err := device.BindUpdate(); err != nil {
return ipcErrorf(ipc.IpcErrorPortInUse, "failed to set i5: %w", err)
}
case "network":
device.net.Lock()
device.net.network = value
device.net.Unlock()
device.log.Verbosef("UAPI: Updating network")
if err := device.BindUpdate(); err != nil {
// TODO: change IpcErrorPortInUse to something reasonable
return ipcErrorf(ipc.IpcErrorPortInUse, "failed to set network: %w", err)
}
case "format_in":
rules, err := conceal.ParseRules(value)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse rules: %w", err)
}
device.log.Verbosef("UAPI: Updating format_in")
device.net.masqueradeOpts.RulesIn = rules
if err := device.BindUpdate(); err != nil {
// TODO: change IpcErrorPortInUse to something reasonable
return ipcErrorf(ipc.IpcErrorPortInUse, "failed to set format_in: %w", err)
}
case "format_out":
rules, err := conceal.ParseRules(value)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse rules: %w", err)
}
device.log.Verbosef("UAPI: Updating format_out")
device.net.masqueradeOpts.RulesOut = rules
if err := device.BindUpdate(); err != nil {
// TODO: change IpcErrorPortInUse to something reasonable
return ipcErrorf(ipc.IpcErrorPortInUse, "failed to set format_out: %w", err)
}
case "header_compat":
compat, err := strconv.ParseBool(value)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse header_compat: %w", err)
}
device.log.Verbosef("UAPI: Updating header_compat")
device.net.framedOpts.HeaderCompat = compat
if err := device.BindUpdate(); err != nil {
// TODO: change IpcErrorPortInUse to something reasonable
return ipcErrorf(ipc.IpcErrorPortInUse, "failed to set header_compat: %w", err)
}
device.ipackets[4] = chain
default:
return ipcErrorf(ipc.IpcErrorInvalid, "invalid UAPI device key: %v", key)
@@ -695,49 +862,3 @@ func (device *Device) IpcHandle(socket net.Conn) {
buffered.Flush()
}
}
type ipcSetDevice struct {
headers struct {
init *magicHeader
response *magicHeader
cookie *magicHeader
transport *magicHeader
}
}
func (d *ipcSetDevice) mergeWithDevice(device *Device) error {
if d.headers.init == nil {
d.headers.init = device.headers.init
}
if d.headers.response == nil {
d.headers.response = device.headers.response
}
if d.headers.cookie == nil {
d.headers.cookie = device.headers.cookie
}
if d.headers.transport == nil {
d.headers.transport = device.headers.transport
}
headers := []*magicHeader{d.headers.init, d.headers.response, d.headers.cookie, d.headers.transport}
for i := 0; i < len(headers); i++ {
for j := i + 1; j < len(headers); j++ {
left := headers[i]
right := headers[j]
if left.start <= right.end && right.start <= left.end {
return errors.New("headers must not overlap")
}
}
}
device.headers.init = d.headers.init
device.headers.response = d.headers.response
device.headers.cookie = d.headers.cookie
device.headers.transport = d.headers.transport
return nil
}
+5 -9
View File
@@ -3,11 +3,11 @@ module github.com/amnezia-vpn/amneziawg-go
go 1.24.4
require (
github.com/stretchr/testify v1.10.0
github.com/tevino/abool v1.2.0
github.com/Jigsaw-Code/outline-sdk v0.0.20
github.com/Jigsaw-Code/outline-sdk/x v0.0.8
github.com/goccy/go-yaml v1.17.1
go.uber.org/atomic v1.11.0
golang.org/x/crypto v0.42.0
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
golang.org/x/net v0.44.0
golang.org/x/sys v0.36.0
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
@@ -15,18 +15,14 @@ require (
)
require (
github.com/Jigsaw-Code/outline-sdk v0.0.20 // indirect
github.com/Jigsaw-Code/outline-sdk/x v0.0.8 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/goccy/go-yaml v1.17.1 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/shadowsocks/go-shadowsocks2 v0.1.5 // indirect
github.com/stretchr/testify v1.10.0 // indirect
golang.org/x/mobile v0.0.0-20240520174638-fa72addaaa1b // indirect
golang.org/x/mod v0.28.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/time v0.9.0 // indirect
golang.org/x/tools v0.37.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
golang.org/x/tools/go/expect v0.1.1-deprecated // indirect
)
+11 -14
View File
@@ -8,44 +8,39 @@ github.com/goccy/go-yaml v1.17.1 h1:LI34wktB2xEE3ONG/2Ar54+/HJVBriAGJ55PHls4YuY=
github.com/goccy/go-yaml v1.17.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
github.com/shadowsocks/go-shadowsocks2 v0.1.5 h1:PDSQv9y2S85Fl7VBeOMF9StzeXZyK1HakRm86CUbr28=
github.com/shadowsocks/go-shadowsocks2 v0.1.5/go.mod h1:AGGpIoek4HRno4xzyFiAtLHkOpcoznZEkAccaI/rplM=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tevino/abool v1.2.0 h1:heAkClL8H6w+mK5md9dzsuohKeXHUpY7Vw0ZCKW+huA=
github.com/tevino/abool v1.2.0/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg=
github.com/things-go/go-socks5 v0.0.5 h1:qvKaGcBkfDrUL33SchHN93srAmYGzb4CxSM2DPYufe8=
github.com/things-go/go-socks5 v0.0.5/go.mod h1:mtzInf8v5xmsBpHZVbIw2YQYhc4K0jRwzfsH64Uh0IQ=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 h1:Di6/M8l0O2lCLc6VVRWhgCiApHV8MnQurBnFSHsQtNY=
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/mobile v0.0.0-20240520174638-fa72addaaa1b h1:WX7nnnLfCEXg+FmdYZPai2XuP3VqCP1HZVMST0n9DF0=
golang.org/x/mobile v0.0.0-20240520174638-fa72addaaa1b/go.mod h1:EiXZlVfUTaAyySFVJb9rsODuiO+WXu8HrUuySb7nYFw=
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
@@ -54,10 +49,12 @@ golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM=
golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM=
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489 h1:ze1vwAdliUAr68RQ5NtufWaXaOg8WUO2OACzEV+TNdE=
+370
View File
@@ -0,0 +1,370 @@
#!/bin/bash
set -euo pipefail
# This script exercises TCP outer transport with all active TCP conceal layers.
# It is Linux-only. AWG-specific options are configured through raw UAPI, so the
# local `wg` binary is only needed for key generation helpers.
#
# Usage:
# ./netns_tcp_masquerade.sh <path to amneziawg-go> [shared-interface-fragment]
#
# The optional fragment file should contain only shared interface keys, e.g.
# Network / FormatIn / FormatOut / I1 / I2 / H1-H4 / S1-S4.
#
# The script verifies:
# - outer TCP stream contains the configured masquerade marker and decoys
# - an inner TCP payload sent over the tunnel is decrypted correctly
#
# Optional environment overrides:
# OUTER_PORT1=10000 OUTER_PORT2=20000
# FORMAT_MAGIC_HEX=feed
# I1_HEX=aabb I2_HEX=ccdd
# INNER_PORT=1111
exec 3>&1
program="${1:?usage: $0 <path to amneziawg-go>}"
shared_fragment="${2:-}"
: "${OUTER_PORT1:=10000}"
: "${OUTER_PORT2:=20000}"
: "${INNER_PORT:=1111}"
: "${FORMAT_MAGIC_HEX:=feed}"
: "${FORMAT_IN:=<b 0x${FORMAT_MAGIC_HEX}><dz be 2><d>}"
: "${FORMAT_OUT:=<b 0x${FORMAT_MAGIC_HEX}><dz be 2><d>}"
: "${I1_HEX:=aabb}"
: "${I2_HEX:=ccdd}"
: "${I1_RULES:=<b 0x${I1_HEX}>}"
: "${I2_RULES:=<b 0x${I2_HEX}>}"
: "${HEADER_COMPAT:=true}"
: "${S1:=15}"
: "${S2:=18}"
: "${S3:=20}"
: "${S4:=25}"
: "${H1:=123456-123500}"
: "${H2:=67543-67550}"
: "${H3:=223344-223350}"
: "${H4:=32345-32350}"
netns0="awg-tcp-test-$$-0"
netns1="awg-tcp-test-$$-1"
netns2="awg-tcp-test-$$-2"
tmpdir="$(mktemp -d /tmp/awg-tcp-test.XXXXXX)"
capture_file="$tmpdir/outer-tcpdump.txt"
recv_file="$tmpdir/inner-tcp.recv"
tcpdump_pid=""
server_pid=""
pretty() { echo -e "\x1b[32m\x1b[1m[+] ${1:+NS$1: }${2}\x1b[0m" >&3; }
pp() { pretty "" "$*"; "$@"; }
maybe_exec() { if [[ $BASHPID -eq $$ ]]; then "$@"; else exec "$@"; fi; }
n0() { pretty 0 "$*"; maybe_exec ip netns exec "$netns0" "$@"; }
n1() { pretty 1 "$*"; maybe_exec ip netns exec "$netns1" "$@"; }
n2() { pretty 2 "$*"; maybe_exec ip netns exec "$netns2" "$@"; }
ip0() { pretty 0 "ip $*"; ip -n "$netns0" "$@"; }
ip1() { pretty 1 "ip $*"; ip -n "$netns1" "$@"; }
ip2() { pretty 2 "ip $*"; ip -n "$netns2" "$@"; }
require_cmd() {
command -v "$1" >/dev/null 2>&1 || {
echo "missing required command: $1" >&2
exit 1
}
}
b64_to_hex() {
printf '%s' "$1" | base64 -d | xxd -p -c 256
}
wait_file() {
local ns=$1
local path=$2
local label=$3
pretty "${ns//*-}" "wait for $label"
while ! ip netns exec "$ns" test -S "$path"; do
sleep 0.1
done
}
wait_listener() {
local ns=$1
local port=$2
local label=$3
pretty "${ns//*-}" "wait for $label:$port"
while ! ss -N "$ns" -tlH "sport = $port" | grep -q LISTEN; do
sleep 0.1
done
}
render_shared_interface() {
if [[ -n $shared_fragment ]]; then
cat "$shared_fragment"
return
fi
cat <<EOF
Network = tcp
HeaderCompat = ${HEADER_COMPAT}
FormatIn = ${FORMAT_IN}
FormatOut = ${FORMAT_OUT}
I1 = ${I1_RULES}
I2 = ${I2_RULES}
S1 = ${S1}
S2 = ${S2}
S3 = ${S3}
S4 = ${S4}
H1 = ${H1}
H2 = ${H2}
H3 = ${H3}
H4 = ${H4}
EOF
}
render_shared_uapi() {
if [[ -z $shared_fragment ]]; then
cat <<EOF
network=tcp
header_compat=${HEADER_COMPAT}
format_in=${FORMAT_IN}
format_out=${FORMAT_OUT}
i1=${I1_RULES}
i2=${I2_RULES}
s1=${S1}
s2=${S2}
s3=${S3}
s4=${S4}
h1=${H1}
h2=${H2}
h3=${H3}
h4=${H4}
EOF
return
fi
awk '
function trim(s) {
gsub(/^[ \t]+|[ \t]+$/, "", s)
return s
}
BEGIN {
map["Network"] = "network"
map["FormatIn"] = "format_in"
map["FormatOut"] = "format_out"
map["HeaderCompat"] = "header_compat"
map["I1"] = "i1"
map["I2"] = "i2"
map["I3"] = "i3"
map["I4"] = "i4"
map["I5"] = "i5"
map["Jc"] = "jc"
map["Jmin"] = "jmin"
map["Jmax"] = "jmax"
map["S1"] = "s1"
map["S2"] = "s2"
map["S3"] = "s3"
map["S4"] = "s4"
map["H1"] = "h1"
map["H2"] = "h2"
map["H3"] = "h3"
map["H4"] = "h4"
}
/^[ \t]*(#|;|$)/ { next }
/^[ \t]*\[/ { next }
{
line = $0
sub(/\r$/, "", line)
split(line, parts, "=")
if (length(parts) < 2) {
next
}
key = trim(parts[1])
sub(/^[^=]*=/, "", line)
value = trim(line)
if (key in map) {
printf "%s=%s\n", map[key], value
}
}
' "$shared_fragment"
}
uapi_set() {
local iface=$1
local payload=$2
local output
output="$(
printf 'set=1\n%s\n' "$payload" |
ip netns exec "$netns0" socat - "UNIX-CONNECT:/var/run/amneziawg/${iface}.sock"
)"
if [[ "$output" != *"errno=0"* ]]; then
echo "uapi set failed for $iface" >&2
echo "$output" >&2
return 1
fi
}
render_base_uapi() {
local private_key=$1
local listen_port=$2
local peer_pub=$3
local psk=$4
local allowed_ip=$5
cat <<EOF
private_key=${private_key}
listen_port=${listen_port}
$(render_shared_uapi)
replace_peers=true
public_key=${peer_pub}
preshared_key=${psk}
protocol_version=1
replace_allowed_ips=true
allowed_ip=${allowed_ip}
EOF
}
render_endpoint_uapi() {
local peer_pub=$1
local endpoint=$2
cat <<EOF
public_key=${peer_pub}
endpoint=${endpoint}
EOF
}
cleanup() {
set +e
exec 2>/dev/null
if [[ -n $tcpdump_pid ]]; then
kill -INT "$tcpdump_pid"
wait "$tcpdump_pid"
fi
if [[ -n $server_pid ]]; then
kill "$server_pid"
wait "$server_pid"
fi
ip1 link del dev wg1
ip2 link del dev wg2
local to_kill
to_kill="$(ip netns pids "$netns0") $(ip netns pids "$netns1") $(ip netns pids "$netns2")"
[[ -n $to_kill ]] && kill $to_kill
pp ip netns del "$netns1"
pp ip netns del "$netns2"
pp ip netns del "$netns0"
rm -rf "$tmpdir"
}
trap cleanup EXIT
require_cmd ip
require_cmd ss
require_cmd wg
require_cmd base64
require_cmd xxd
require_cmd socat
require_cmd tcpdump
if [[ $EUID -ne 0 ]]; then
echo "run as root" >&2
exit 1
fi
if [[ -n $shared_fragment && ! -f $shared_fragment ]]; then
echo "shared interface fragment not found: $shared_fragment" >&2
exit 1
fi
ip netns del "$netns0" 2>/dev/null || true
ip netns del "$netns1" 2>/dev/null || true
ip netns del "$netns2" 2>/dev/null || true
pp ip netns add "$netns0"
pp ip netns add "$netns1"
pp ip netns add "$netns2"
ip0 link set up dev lo
ip1 link set up dev lo
ip2 link set up dev lo
n0 "$program" wg1
wait_file "$netns0" /var/run/amneziawg/wg1.sock "UAPI socket wg1"
ip0 link set wg1 netns "$netns1"
n0 "$program" wg2
wait_file "$netns0" /var/run/amneziawg/wg2.sock "UAPI socket wg2"
ip0 link set wg2 netns "$netns2"
key1_b64="$(pp wg genkey)"
key2_b64="$(pp wg genkey)"
pub1_b64="$(pp wg pubkey <<<"$key1_b64")"
pub2_b64="$(pp wg pubkey <<<"$key2_b64")"
psk_b64="$(pp wg genpsk)"
key1="$(b64_to_hex "$key1_b64")"
key2="$(b64_to_hex "$key2_b64")"
pub1="$(b64_to_hex "$pub1_b64")"
pub2="$(b64_to_hex "$pub2_b64")"
psk="$(b64_to_hex "$psk_b64")"
ip1 addr add 192.168.241.1/24 dev wg1
ip2 addr add 192.168.241.2/24 dev wg2
cfg1="$tmpdir/wg1.conf"
cfg2="$tmpdir/wg2.conf"
cat >"$cfg1" <<EOF
$(render_base_uapi "$key1" "$OUTER_PORT1" "$pub2" "$psk" "192.168.241.2/32")
EOF
cat >"$cfg2" <<EOF
$(render_base_uapi "$key2" "$OUTER_PORT2" "$pub1" "$psk" "192.168.241.1/32")
EOF
uapi_set wg1 "$(cat "$cfg1")"
uapi_set wg2 "$(cat "$cfg2")"
ip1 link set up dev wg1
ip2 link set up dev wg2
wait_listener "$netns0" "$OUTER_PORT1" "outer tcp listen"
wait_listener "$netns0" "$OUTER_PORT2" "outer tcp listen"
# Endpoint parsing must happen after the interface is up, otherwise it is still
# interpreted by the default UDP bind.
uapi_set wg1 "$(render_endpoint_uapi "$pub2" "127.0.0.1:${OUTER_PORT2}")"
uapi_set wg2 "$(render_endpoint_uapi "$pub1" "127.0.0.1:${OUTER_PORT1}")"
pretty 0 "capture outer tcp stream"
ip netns exec "$netns0" tcpdump -i lo -nn -s 0 -l -XX "tcp port ${OUTER_PORT1} or tcp port ${OUTER_PORT2}" >"$capture_file" 2>&1 &
tcpdump_pid=$!
sleep 1
pretty 2 "start inner tcp receiver on 192.168.241.2:${INNER_PORT}"
ip netns exec "$netns2" socat -T 10 -u "TCP-LISTEN:${INNER_PORT},bind=192.168.241.2,reuseaddr" - >"$recv_file" &
server_pid=$!
wait_listener "$netns2" "$INNER_PORT" "inner tcp listen"
payload="masked tcp over awg conceal $(date +%s)"
pretty 1 "send inner tcp payload to 192.168.241.2:${INNER_PORT}"
printf '%s' "$payload" | ip netns exec "$netns1" socat -T 10 -u - "TCP:192.168.241.2:${INNER_PORT},connect-timeout=10"
wait "$server_pid"
server_pid=""
recv_payload="$(cat "$recv_file")"
[[ "$recv_payload" == "$payload" ]]
pretty "" "inner tcp payload decrypted correctly"
sleep 1
kill -INT "$tcpdump_pid"
wait "$tcpdump_pid" || true
tcpdump_pid=""
capture_lc="$(tr '[:upper:]' '[:lower:]' <"$capture_file")"
[[ "$capture_lc" == *"$FORMAT_MAGIC_HEX"* ]]
[[ "$capture_lc" == *"$I1_HEX"* ]]
[[ "$capture_lc" == *"$I2_HEX"* ]]
pretty "" "outer tcp capture contains format magic ${FORMAT_MAGIC_HEX} and prelude decoys ${I1_HEX}/${I2_HEX}"