Compare commits

...
Author SHA1 Message Date
Frog Rocky 948c555794 fix: store prelude state on endpoints 2026-06-01 16:30:56 +02:00
Frog Rocky 756d3dd6bd feat: send prelude once per endpoint session 2026-05-23 17:32:06 +02:00
Frog Rocky 9965d6d761 fix(conn): route UDP conceal format errors to fallback
Add tuntest coverage for TCP/UDP TUN packets through UDP and TCP masquerade
pipelines. Cover TCP and UDP fallback on the device path, and preserve
format errors from UDP datagram decoding so invalid packets can be rerouted.
2026-05-11 16:59:04 +02:00
Frog Rocky f8b2d2ba5a feat: add conceal fallback port proxying
Add fallback_port support so invalid TCP/UDP conceal traffic is proxied to a local fallback service instead of being dropped.

Also add typed conceal format errors, preserve consumed bytes for TCP fallback replay, wire fallback config through UAPI/binds/Outline, and cover TCP, UDP, and UAPI behavior with tests.
2026-05-11 16:59:04 +02:00
Frog Rocky 8d9bf0ed57 fixes 2026-04-27 13:08:28 +02:00
Frog Rocky 9e06d7e934 fix(conn): preserve batching in ConcealBind.Send
Batch active UDP conceal output before forwarding to the inner bind so
StdNetBind can still use WriteBatch/sendmmsg and UDP GSO coalescing.

Preserve the no-op fast path by forwarding the original input batch
directly when the conceal pipeline is inactive. Clone prelude datagrams
before retaining them, keep encoded datagrams in pooled buffers until
their send completes, and defensively cap flushes by inner BatchSize.

Add regression tests for active conceal batching, exact flattened wire
order with prelude/junk datagrams, and no-op batch forwarding.
2026-04-27 13:08:28 +02:00
Frog Rocky 4d8f90b9af fix(tests): compat fixes 2026-04-27 13:08:28 +02:00
Frog Rocky 4b261410f2 fix(conn): move UDP conceal to the bind layer for Windows UDP
Move UDP conceal above the UDP bind so conceal-enabled Windows UDP can run
over WinRingBind instead of forcing a StdNetBind fallback.

Add a bind-level UDP conceal middleware that:
- encodes outbound WireGuard packets with framed and masquerade rules
- injects prelude decoys and junk before initiation packets
- wraps ReceiveFuncs to decode inbound datagrams and drop invalid decoy,
  junk, and prelude packets before they reach device

Extract transport-agnostic UDP packet helpers in conceal so framing,
masquerade, and prelude logic is shared across WinRingBind and StdNetBind.

Keep the TCP conceal path unchanged in BindStream, remove the old Windows
fast/fallback UDP selector, and add focused bind-level conceal tests.
2026-04-27 13:08:28 +02:00
Frog Rocky c025f7d12b fix(tests): compatibility fixes 2026-04-27 13:08:28 +02:00
Frog Rocky 1dbdb6da67 fix(tests): compatibility fixes 2026-04-27 13:08:28 +02:00
Frog Rocky 9c92c7fbfb fix(tests): compatibility fixes 2026-04-27 13:08:28 +02:00
Frog Rocky 03767cd6c2 fix(tests): compatibility fixes 2026-04-27 13:08:28 +02:00
Frog Rocky 8d2de9eb72 fix(tests): compatibility fixes 2026-04-27 13:08:28 +02:00
Frog Rocky 68e519c963 fix(tests): compatibility fixes 2026-04-27 13:08:28 +02:00
Frog Rocky 1376958db2 fix(tests): compatibility fixes 2026-04-27 13:08:28 +02:00
Frog Rocky a018084aa2 fix(tests): added smoke tests for win 2026-04-27 13:08:28 +02:00
Frog Rocky b09fac60b2 fix(tests): aligning tests for windows 2026-04-27 13:08:28 +02:00
Frog Rocky 3e1536dd74 fix(conn): honor Windows UDP conceal settings in the default bind
Route the Windows UDP path through a conceal-aware wrapper that keeps the
WinRing fast path for plain UDP and falls back to StdNetBind when framed,
prelude, or masquerade layers are enabled.

Normalize UDP endpoints across both paths and add tests for bind
selection, endpoint stability, receive normalization, and conceal option
propagation.
2026-04-27 13:08:28 +02:00
Frog Rocky c8f66f9438 fixes 2026-04-04 01:08:08 +02:00
Frog Rocky e8b61fbc03 fixes 2026-04-04 01:08:08 +02:00
Frog Rocky f4cdedf40a perf(conceal): optimize record and prelude hot paths
Replace hot-path bytes.NewBuffer usage with slice-backed readers and
writers, and build FlexBuffer-backed read/write contexts by value.

Remove defer from the targeted per-message and per-batch write paths,
return pooled buffers explicitly after downstream writes complete, and
keep TCP/UDP prelude behavior unchanged.

Add focused regression tests for stream, UDP, and batch masquerade and
prelude writes, and fix write-side bookkeeping to emit the actual
encoded bytes.
2026-04-04 01:08:08 +02:00
Frog Rocky c65f05a009 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-04-04 01:08:08 +02: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
68 changed files with 11257 additions and 854 deletions

No files matched your search

+199 -14
View File
@@ -54,14 +54,15 @@ $ make
## Configuration
> [!NOTE]
> If there is no value specified (for any param), AWG treats it as 0
> If there is no value specified (for any param), AWG treats it as 0, except `PreludeResendInterval`, which defaults to 120 seconds.
### Junk packets
### Prelude packets
The amount of junk packets specified in `Jc` with a random size between `Jmin` and `Jmax` would be generated and sent prior every handshake
Junk packets and custom signature packets are generated once at the beginning of an outbound endpoint session. They can be forced again by `PreludeResendInterval`.
- `Jc: int`, recommended range is 4-12
- `Jmin: int` <= `Jmax:int`
- `PreludeResendInterval: int`, seconds; default is 120, explicit 0 disables timer-based resend
> [!TIP]
> Junk packets do 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
@@ -89,9 +90,15 @@ 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.
These packets are sent as part of the prelude, 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.
- `I1: string`
- `I2: string`
@@ -99,16 +106,194 @@ 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
### Fallback
**`FallbackPort: int`**
- A local fallback service port in range `1-65535`.
- If configured and incoming data does not match the configured AWG/conceal formats, AWG proxies that TCP stream or UDP sender to a loopback service on `FallbackPort` and relays the response back to the original sender.
- Absence means fallback is disabled.
### 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 := newSliceWriter(tmp)
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)
}
+410
View File
@@ -0,0 +1,410 @@
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, error) {
switch e.recordKind(b) {
case frameRecordInitiation:
if e.compat {
return decodeOneCompat(b, e.header.initial, e.padding.initial), nil
}
return decodeOne(b, e.header.initial, e.padding.initial, WireguardMsgInitiationType), nil
case frameRecordResponse:
if e.compat {
return decodeOneCompat(b, e.header.response, e.padding.response), nil
}
return decodeOne(b, e.header.response, e.padding.response, WireguardMsgResponseType), nil
case frameRecordCookie:
if e.compat {
return decodeOneCompat(b, e.header.cookie, e.padding.cookie), nil
}
return decodeOne(b, e.header.cookie, e.padding.cookie, WireguardMsgCookieReplyType), nil
case frameRecordTransport:
if e.compat {
return decodeOneCompat(b, e.header.transport, e.padding.transport), nil
}
return decodeOne(b, e.header.transport, e.padding.transport, WireguardMsgTransportType), nil
default:
return 0, NewFormatError(b, errInvalidData)
}
}
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)
if n > 0 {
var decodeErr error
n, decodeErr = c.enc.Decode(b[:n])
if decodeErr != nil {
return 0, decodeErr
}
}
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, err = 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, err = c.enc.Decode(b)
if err != nil {
return 0, err
}
}
return n, nil
}
func (c *FramedBatchConn) WriteBatch(ms []ipv4.Message, flags int) (n int, err error) {
var inline [128][]byte
pooled := inline[:0]
if len(ms) > len(inline) {
pooled = make([][]byte, 0, len(ms))
}
for i := range ms {
t := c.pool.Get()
pooled = append(pooled, 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
n, err = c.BatchConn.WriteBatch(ms, flags)
for _, buf := range pooled {
c.pool.Put(buf)
}
return n, err
}
+215
View File
@@ -0,0 +1,215 @@
package conceal
import (
"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()
w := newSliceWriter(t)
if err := c.rulesOut.Write(&w, &ctx); err != nil {
c.pool.Put(t)
return 0, err
}
if _, err := c.Conn.Write(w.Bytes()); err != nil {
c.pool.Put(t)
return 0, err
}
c.pool.Put(t)
return len(b), 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) {
enc, ok := newMasqueradeEncoding(WrapBufferPool(pool), opts)
if !ok {
return nil, false
}
return &MasqueradeUDPConn{
UDPConn: conn,
pool: enc.pool,
enc: enc,
}, true
}
type MasqueradeUDPConn struct {
UDPConn
pool *BufferPool
enc masqueradeEncoding
}
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
}
n, err = c.enc.DecodeInPlace(b, n)
if err != nil {
return 0, oobn, flags, addr, err
}
return n, oobn, flags, addr, err
}
func (c *MasqueradeUDPConn) WriteMsgUDP(b, oob []byte, addr *net.UDPAddr) (n, oobn int, err error) {
t := c.pool.Get()
w := newSliceWriter(t)
n, err = c.enc.Encode(w.buf[:cap(w.buf)], b)
if err != nil {
c.pool.Put(t)
return 0, 0, err
}
n, oobn, err = c.UDPConn.WriteMsgUDP(t[:n], oob, addr)
c.pool.Put(t)
if err != nil {
return 0, oobn, err
}
return len(b), oobn, nil
}
func NewMasqueradeBatchConn(conn BatchConn, bp *sync.Pool, opts MasqueradeOpts) (c *MasqueradeBatchConn, ok bool) {
enc, ok := newMasqueradeEncoding(WrapBufferPool(bp), opts)
if !ok {
return nil, false
}
return &MasqueradeBatchConn{
BatchConn: conn,
pool: enc.pool,
enc: enc,
}, true
}
type MasqueradeBatchConn struct {
BatchConn
pool *BufferPool
enc masqueradeEncoding
}
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 {
ms[i].N, err = c.enc.DecodeInPlace(ms[i].Buffers[0], ms[i].N)
if err != nil {
return 0, err
}
}
return n, nil
}
func (c *MasqueradeBatchConn) WriteBatch(ms []ipv4.Message, flags int) (n int, err error) {
var inline [128][]byte
pooled := inline[:0]
if len(ms) > len(inline) {
pooled = make([][]byte, 0, len(ms))
}
for i := range ms {
t := c.pool.Get()
pooled = append(pooled, t)
n, err := c.enc.Encode(t, ms[i].Buffers[0])
if err != nil {
for _, buf := range pooled {
c.pool.Put(buf)
}
return 0, err
}
ms[i].Buffers[0] = t[:n]
}
n, err = c.BatchConn.WriteBatch(ms, flags)
for _, buf := range pooled {
c.pool.Put(buf)
}
return n, err
}
+408
View File
@@ -0,0 +1,408 @@
package conceal
import (
"bytes"
"encoding/binary"
"errors"
"net"
"sync"
"syscall"
"testing"
"time"
"golang.org/x/net/ipv4"
)
func TestMasqueradeConnWriteRecordEncodesPayload(t *testing.T) {
rules := mustTestRules(t, "<dz be 2><d>")
raw := &recordingConn{}
pool := newTestBufferPool()
conn, ok := NewMasqueradeConn(raw, pool, MasqueradeOpts{RulesOut: rules})
if !ok {
t.Fatal("expected masquerade conn")
}
payload := []byte{0x01, 0x02, 0x03}
n, err := conn.WriteRecord(payload)
if err != nil {
t.Fatalf("WriteRecord failed: %v", err)
}
if n != len(payload) {
t.Fatalf("WriteRecord n = %d, want %d", n, len(payload))
}
want := []byte{0x00, 0x03, 0x01, 0x02, 0x03}
if !bytes.Equal(raw.writes[0], want) {
t.Fatalf("WriteRecord bytes = %x, want %x", raw.writes[0], want)
}
}
func TestMasqueradeUDPConnWriteMsgUDPEncodesPayload(t *testing.T) {
rules := mustTestRules(t, "<dz be 2><d>")
raw := &recordingUDPConn{}
pool := newTestBufferPool()
conn, ok := NewMasqueradeUDPConn(raw, pool, MasqueradeOpts{RulesOut: rules})
if !ok {
t.Fatal("expected masquerade udp conn")
}
payload := []byte{0x01, 0x02, 0x03}
n, oobn, err := conn.WriteMsgUDP(payload, []byte{0xaa}, &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 51820})
if err != nil {
t.Fatalf("WriteMsgUDP failed: %v", err)
}
if n != len(payload) {
t.Fatalf("WriteMsgUDP n = %d, want %d", n, len(payload))
}
if oobn != 1 {
t.Fatalf("WriteMsgUDP oobn = %d, want 1", oobn)
}
want := []byte{0x00, 0x03, 0x01, 0x02, 0x03}
if len(raw.writes) != 1 {
t.Fatalf("write count = %d, want 1", len(raw.writes))
}
if !bytes.Equal(raw.writes[0], want) {
t.Fatalf("WriteMsgUDP bytes = %x, want %x", raw.writes[0], want)
}
}
func TestMasqueradeBatchConnWriteBatchEncodesEachMessage(t *testing.T) {
rules := mustTestRules(t, "<dz be 2><d>")
raw := &recordingBatchConn{}
pool := newTestBufferPool()
conn, ok := NewMasqueradeBatchConn(raw, pool, MasqueradeOpts{RulesOut: rules})
if !ok {
t.Fatal("expected masquerade batch conn")
}
msgs := []ipv4.Message{
{Buffers: net.Buffers{[]byte{0x01, 0x02}}},
{Buffers: net.Buffers{[]byte{0x03}}},
}
n, err := conn.WriteBatch(msgs, 0)
if err != nil {
t.Fatalf("WriteBatch failed: %v", err)
}
if n != len(msgs) {
t.Fatalf("WriteBatch n = %d, want %d", n, len(msgs))
}
if len(raw.batches) != 1 {
t.Fatalf("batch count = %d, want 1", len(raw.batches))
}
want := [][]byte{
{0x00, 0x02, 0x01, 0x02},
{0x00, 0x01, 0x03},
}
for i, got := range raw.batches[0] {
if !bytes.Equal(got.data, want[i]) {
t.Fatalf("batch msg %d = %x, want %x", i, got.data, want[i])
}
}
}
func TestFramedBatchConnReadBatchDecodesEachMessage(t *testing.T) {
initiation := make([]byte, WireguardMsgInitiationSize)
copy(initiation[:4], []byte{0x88, 0x77, 0x66, 0x55})
initiation[4], initiation[5] = 0xaa, 0xbb
transport := make([]byte, WireguardMsgTransportMinSize)
copy(transport[:4], []byte{0x44, 0x33, 0x22, 0x11})
transport[4], transport[5] = 0xcc, 0xdd
raw := &recordingBatchConn{
readBatches: [][]recordedBatchMessage{
{
{data: initiation},
{data: transport},
},
},
}
pool := newTestBufferPool()
opts := FramedOpts{
H1: &RangedHeader{0x55667788, 0x55667788},
H4: &RangedHeader{0x11223344, 0x11223344},
}
conn, ok := NewFramedBatchConn(raw, pool, opts)
if !ok {
t.Fatal("expected framed batch conn")
}
msgs := []ipv4.Message{
{Buffers: net.Buffers{make([]byte, WireguardMsgInitiationSize)}},
{Buffers: net.Buffers{make([]byte, WireguardMsgTransportMinSize)}},
}
n, err := conn.ReadBatch(msgs, 0)
if err != nil {
t.Fatalf("ReadBatch failed: %v", err)
}
if n != len(msgs) {
t.Fatalf("ReadBatch n = %d, want %d", n, len(msgs))
}
want := [][]byte{
make([]byte, WireguardMsgInitiationSize),
make([]byte, WireguardMsgTransportMinSize),
}
binary.LittleEndian.PutUint32(want[0][:4], WireguardMsgInitiationType)
binary.LittleEndian.PutUint32(want[1][:4], WireguardMsgTransportType)
want[0][4], want[0][5] = 0xaa, 0xbb
want[1][4], want[1][5] = 0xcc, 0xdd
for i := range msgs {
if msgs[i].N != len(want[i]) {
t.Fatalf("msg %d N = %d, want %d", i, msgs[i].N, len(want[i]))
}
if !bytes.Equal(msgs[i].Buffers[0][:msgs[i].N], want[i]) {
t.Fatalf("msg %d = %x, want %x", i, msgs[i].Buffers[0][:msgs[i].N], want[i])
}
}
}
func TestFramedBatchConnWriteBatchEncodesEachMessage(t *testing.T) {
raw := &recordingBatchConn{}
pool := newTestBufferPool()
opts := FramedOpts{
H1: &RangedHeader{0x55667788, 0x55667788},
H4: &RangedHeader{0x11223344, 0x11223344},
}
conn, ok := NewFramedBatchConn(raw, pool, opts)
if !ok {
t.Fatal("expected framed batch conn")
}
msgs := []ipv4.Message{
{Buffers: net.Buffers{[]byte{WireguardMsgInitiationType, 0x00, 0x00, 0x00, 0xaa, 0xbb}}},
{Buffers: net.Buffers{[]byte{WireguardMsgTransportType, 0x00, 0x00, 0x00, 0xcc, 0xdd}}},
}
n, err := conn.WriteBatch(msgs, 0)
if err != nil {
t.Fatalf("WriteBatch failed: %v", err)
}
if n != len(msgs) {
t.Fatalf("WriteBatch n = %d, want %d", n, len(msgs))
}
if len(raw.batches) != 1 {
t.Fatalf("batch count = %d, want 1", len(raw.batches))
}
want := [][]byte{
{0x88, 0x77, 0x66, 0x55, 0xaa, 0xbb},
{0x44, 0x33, 0x22, 0x11, 0xcc, 0xdd},
}
for i, got := range raw.batches[0] {
if !bytes.Equal(got.data, want[i]) {
t.Fatalf("batch msg %d = %x, want %x", i, got.data, want[i])
}
}
}
func TestPreludeBatchConnWriteBatchEmitsPreludeBeforeInitiation(t *testing.T) {
raw := &recordingBatchConn{}
pool := newTestBufferPool()
msgsPool := newTestMsgsPool()
rules := mustTestRules(t, "<b 0xaabb>")
conn, ok := NewPreludeBatchConn(raw, raw, pool, msgsPool, nil, PreludeOpts{
Jc: 1,
Jmin: 3,
Jmax: 3,
RulesArr: [5]Rules{
rules,
},
})
if !ok {
t.Fatal("expected prelude batch conn")
}
initiation := make([]byte, 8)
binary.LittleEndian.PutUint32(initiation[:4], WireguardMsgInitiationType)
msgs := []ipv4.Message{
{
Buffers: net.Buffers{initiation},
OOB: []byte{0x44},
Addr: &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 51820},
},
}
n, err := conn.WriteBatch(msgs, 0)
if err != nil {
t.Fatalf("WriteBatch failed: %v", err)
}
if n != len(msgs) {
t.Fatalf("WriteBatch n = %d, want %d", n, len(msgs))
}
if len(raw.batches) != 2 {
t.Fatalf("batch count = %d, want 2", len(raw.batches))
}
preludeBatch := raw.batches[0]
if len(preludeBatch) != 2 {
t.Fatalf("prelude batch len = %d, want 2", len(preludeBatch))
}
if !bytes.Equal(preludeBatch[0].data, []byte{0xaa, 0xbb}) {
t.Fatalf("prelude decoy = %x, want aabb", preludeBatch[0].data)
}
if len(preludeBatch[1].data) != 3 {
t.Fatalf("junk len = %d, want 3", len(preludeBatch[1].data))
}
if !bytes.Equal(raw.batches[1][0].data, initiation) {
t.Fatalf("main batch payload changed")
}
}
func mustTestRules(t *testing.T, spec string) Rules {
t.Helper()
rules, err := ParseRules(spec)
if err != nil {
t.Fatalf("ParseRules(%q): %v", spec, err)
}
return rules
}
func newTestBufferPool() *sync.Pool {
return &sync.Pool{
New: func() any {
return make([]byte, 256)
},
}
}
func newTestMsgsPool() *sync.Pool {
return &sync.Pool{
New: func() any {
msgs := make([]ipv4.Message, 8)
for i := range msgs {
msgs[i].Buffers = make(net.Buffers, 1)
}
return &msgs
},
}
}
type recordingConn struct {
writes [][]byte
}
func (c *recordingConn) Read(_ []byte) (int, error) {
return 0, errors.New("not implemented")
}
func (c *recordingConn) Write(b []byte) (int, error) {
c.writes = append(c.writes, append([]byte(nil), b...))
return len(b), nil
}
func (c *recordingConn) Close() error {
return nil
}
func (c *recordingConn) LocalAddr() net.Addr {
return &net.TCPAddr{}
}
func (c *recordingConn) RemoteAddr() net.Addr {
return &net.TCPAddr{}
}
func (c *recordingConn) SetDeadline(time.Time) error {
return nil
}
func (c *recordingConn) SetReadDeadline(time.Time) error {
return nil
}
func (c *recordingConn) SetWriteDeadline(time.Time) error {
return nil
}
type recordingUDPConn struct {
writes [][]byte
}
func (c *recordingUDPConn) ReadFrom([]byte) (int, net.Addr, error) {
return 0, nil, errors.New("not implemented")
}
func (c *recordingUDPConn) WriteTo([]byte, net.Addr) (int, error) {
return 0, errors.New("not implemented")
}
func (c *recordingUDPConn) Close() error {
return nil
}
func (c *recordingUDPConn) LocalAddr() net.Addr {
return &net.UDPAddr{}
}
func (c *recordingUDPConn) SetDeadline(time.Time) error {
return nil
}
func (c *recordingUDPConn) SetReadDeadline(time.Time) error {
return nil
}
func (c *recordingUDPConn) SetWriteDeadline(time.Time) error {
return nil
}
func (c *recordingUDPConn) ReadMsgUDP(_, _ []byte) (int, int, int, *net.UDPAddr, error) {
return 0, 0, 0, nil, errors.New("not implemented")
}
func (c *recordingUDPConn) WriteMsgUDP(b, _ []byte, _ *net.UDPAddr) (int, int, error) {
c.writes = append(c.writes, append([]byte(nil), b...))
return len(b), 1, nil
}
func (c *recordingUDPConn) SyscallConn() (syscall.RawConn, error) {
return nil, errors.New("not implemented")
}
type recordedBatchMessage struct {
data []byte
}
type recordingBatchConn struct {
batches [][]recordedBatchMessage
readBatches [][]recordedBatchMessage
readIndex int
}
func (c *recordingBatchConn) ReadBatch(ms []ipv4.Message, flags int) (int, error) {
if len(c.readBatches) == 0 {
return 0, errors.New("not implemented")
}
batch := c.readBatches[c.readIndex]
c.readIndex++
if c.readIndex == len(c.readBatches) {
c.readIndex = 0
}
for i := range batch {
msg := &ms[i]
msg.N = copy(msg.Buffers[0], batch[i].data)
}
for i := len(batch); i < len(ms); i++ {
ms[i].N = 0
}
return len(batch), nil
}
func (c *recordingBatchConn) WriteBatch(ms []ipv4.Message, flags int) (int, error) {
batch := make([]recordedBatchMessage, len(ms))
for i := range ms {
batch[i] = recordedBatchMessage{
data: append([]byte(nil), ms[i].Buffers[0]...),
}
}
c.batches = append(c.batches, batch)
return len(ms), nil
}
+419
View File
@@ -0,0 +1,419 @@
package conceal
import (
"crypto/rand"
"math/big"
"net"
"sync"
"time"
"golang.org/x/net/ipv4"
)
const DefaultPreludeResendInterval = 120 * time.Second
type PreludeOpts struct {
Jc int
Jmin int
Jmax int
ResendInterval time.Duration
RulesArr [5]Rules
}
type PreludeState struct {
mu sync.Mutex
sent bool
lastSent time.Time
}
func (s *PreludeState) ClaimSend(now time.Time, resendInterval time.Duration) bool {
if s == nil {
return false
}
s.mu.Lock()
defer s.mu.Unlock()
if !s.sent {
s.sent = true
s.lastSent = now
return true
}
if resendInterval <= 0 || now.Sub(s.lastSent) < resendInterval {
return false
}
s.lastSent = now
return true
}
func (s *PreludeState) Reset() {
if s == nil {
return
}
s.mu.Lock()
s.sent = false
s.lastSent = time.Time{}
s.mu.Unlock()
}
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,
_ *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),
resendInterval: opts.ResendInterval,
}, true
}
type PreludeUDPConn struct {
UDPConn
origin UDPConn
pool *BufferPool
rulesArr [5]Rules
junkCount int
junkGen *junkGenerator
state PreludeState
resendInterval time.Duration
}
func (c *PreludeUDPConn) WriteMsgUDP(b, oob []byte, addr *net.UDPAddr) (n, oobn int, err error) {
state := &c.state
if state.ClaimSend(time.Now(), c.resendInterval) {
buf := c.pool.Get()
ctx := writeContext{
FlexBuffer: WrapFlexBuffer(nil),
BufferPool: c.pool,
}
w := newSliceWriter(buf)
for _, rules := range c.rulesArr {
if rules == nil {
continue
}
w.Reset(buf)
if err = rules.Write(&w, &ctx); err != nil {
c.pool.Put(buf)
state.Reset()
return 0, 0, err
}
if _, _, err = c.origin.WriteMsgUDP(w.Bytes(), oob, addr); err != nil {
c.pool.Put(buf)
state.Reset()
return 0, 0, err
}
}
for range c.junkCount {
junk := c.junkGen.generate(buf)
if _, _, err = c.origin.WriteMsgUDP(junk, oob, addr); err != nil {
c.pool.Put(buf)
state.Reset()
return 0, 0, err
}
}
c.pool.Put(buf)
}
return c.UDPConn.WriteMsgUDP(b, oob, addr)
}
func NewPreludeConn(
conn StreamRecordConn,
pool *sync.Pool,
framedOpts FramedOpts,
opts PreludeOpts,
) (c *PreludeConn, ok bool) {
return NewPreludeConnWithState(conn, pool, framedOpts, opts, nil, false)
}
func NewPreludeConnWithState(
conn StreamRecordConn,
pool *sync.Pool,
framedOpts FramedOpts,
opts PreludeOpts,
state *PreludeState,
emitOutbound bool,
) (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,
resendInterval: opts.ResendInterval,
state: state,
emitOutbound: emitOutbound,
recordEncoding: enc,
}, true
}
type PreludeConn struct {
StreamRecordConn
pool *BufferPool
rulesArr [5]Rules
resendInterval time.Duration
state *PreludeState
emitOutbound bool
recordEncoding frameEncoding
seenValid bool
}
func (c *PreludeConn) Read(b []byte) (n int, err error) {
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
}
if c.isPreludeRecord(b[:n]) {
continue
}
return 0, NewFormatError(b[:n], errInvalidData)
}
}
func (c *PreludeConn) isPreludeRecord(b []byte) bool {
for _, rules := range c.rulesArr {
if rules.Match(b, c.pool) {
return true
}
}
return false
}
func (c *PreludeConn) Write(b []byte) (n int, err error) {
if c.emitOutbound && c.state != nil && c.state.ClaimSend(time.Now(), c.resendInterval) {
if err := c.writePreludeRecords(); err != nil {
c.state.Reset()
return 0, err
}
}
return c.StreamRecordConn.WriteRecord(b)
}
func (c *PreludeConn) writePreludeRecords() (err error) {
buf := c.pool.Get()
ctx := writeContext{
FlexBuffer: WrapFlexBuffer(nil),
BufferPool: c.pool,
}
w := newSliceWriter(buf)
for _, rules := range c.rulesArr {
if rules == nil {
continue
}
w.Reset(buf)
if err = rules.Write(&w, &ctx); err != nil {
c.pool.Put(buf)
return err
}
if _, err = c.StreamRecordConn.WriteRecord(w.Bytes()); err != nil {
c.pool.Put(buf)
return err
}
}
c.pool.Put(buf)
return nil
}
func NewPreludeBatchConn(
conn BatchConn,
origin BatchConn,
bufPool *sync.Pool,
msgsPool *sync.Pool,
_ *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),
resendInterval: opts.ResendInterval,
}, true
}
type PreludeBatchConn struct {
BatchConn
origin BatchConn
bufPool *BufferPool
msgsPool *sync.Pool
rulesArr [5]Rules
junkCount int
junkGen *junkGenerator
state PreludeState
resendInterval time.Duration
}
func (c *PreludeBatchConn) WriteBatch(ms []ipv4.Message, flags int) (n int, err error) {
if len(ms) == 0 {
return c.BatchConn.WriteBatch(ms, flags)
}
preludeMsg := &ms[0]
state := &c.state
if state.ClaimSend(time.Now(), c.resendInterval) {
ctx := writeContext{
FlexBuffer: WrapFlexBuffer(nil),
BufferPool: c.bufPool,
}
msgs := c.msgsPool.Get().(*[]ipv4.Message)
count := c.junkCount
for _, rules := range c.rulesArr {
if rules != nil {
count++
}
}
var inline [32][]byte
pooled := inline[:0]
if count > len(inline) {
pooled = make([][]byte, 0, count)
}
i := 0
for _, rules := range c.rulesArr {
if rules == nil {
continue
}
buf := c.bufPool.Get()
pooled = append(pooled, buf)
w := newSliceWriter(buf)
if err = rules.Write(&w, &ctx); err != nil {
for _, pooledBuf := range pooled {
c.bufPool.Put(pooledBuf)
}
c.msgsPool.Put(msgs)
state.Reset()
return 0, err
}
(*msgs)[i].Buffers[0] = w.Bytes()
(*msgs)[i].OOB = preludeMsg.OOB
(*msgs)[i].Addr = preludeMsg.Addr
i++
}
for range c.junkCount {
buf := c.bufPool.Get()
pooled = append(pooled, buf)
(*msgs)[i].Buffers[0] = c.junkGen.generate(buf)
(*msgs)[i].OOB = preludeMsg.OOB
(*msgs)[i].Addr = preludeMsg.Addr
i++
}
var start int
for {
m := (*msgs)[start:i]
n, err = c.origin.WriteBatch(m, flags)
if err != nil {
for _, pooledBuf := range pooled {
c.bufPool.Put(pooledBuf)
}
c.msgsPool.Put(msgs)
state.Reset()
return 0, err
}
if n == len(m) {
break
}
start += n
}
for _, pooledBuf := range pooled {
c.bufPool.Put(pooledBuf)
}
c.msgsPool.Put(msgs)
}
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 (
"bytes"
"errors"
)
var ErrFormat = errors.New("conceal format error")
type FormatError struct {
Data []byte
Err error
}
func NewFormatError(data []byte, err error) *FormatError {
return &FormatError{
Data: bytes.Clone(data),
Err: err,
}
}
func (e *FormatError) Error() string {
if e == nil {
return ErrFormat.Error()
}
if e.Err == nil {
return ErrFormat.Error()
}
return ErrFormat.Error() + ": " + e.Err.Error()
}
func (e *FormatError) Unwrap() []error {
if e == nil || e.Err == nil {
return []error{ErrFormat}
}
return []error{ErrFormat, e.Err}
}
func FormatErrorData(err error) []byte {
var formatErr *FormatError
if errors.As(err, &formatErr) {
return bytes.Clone(formatErr.Data)
}
return nil
}
+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 ""
}
+44
View File
@@ -0,0 +1,44 @@
package conceal
import (
"bytes"
"errors"
"sync"
"testing"
)
func TestMasqueradeConnReadRecordReturnsFormatErrorWithData(t *testing.T) {
rules, err := ParseRules("<b 0xfeed><dz be 2><d>")
if err != nil {
t.Fatalf("parse rules: %v", err)
}
pool := sync.Pool{
New: func() any {
return make([]byte, 65535)
},
}
conn, ok := NewMasqueradeConn(newBenchmarkStreamConn([]byte("GET /")), &pool, MasqueradeOpts{
RulesIn: rules,
})
if !ok {
t.Fatal("expected masquerade connection")
}
buf := make([]byte, 64)
n, err := conn.ReadRecord(buf)
if !errors.Is(err, ErrFormat) {
t.Fatalf("ReadRecord error = %v, want ErrFormat", err)
}
if n != 0 {
t.Fatalf("ReadRecord n = %d, want 0", n)
}
var formatErr *FormatError
if !errors.As(err, &formatErr) {
t.Fatalf("ReadRecord error type = %T, want *FormatError", err)
}
if !bytes.Equal(formatErr.Data, []byte("GE")) {
t.Fatalf("format error data = %q, want %q", formatErr.Data, "GE")
}
}
+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
}
+535
View File
@@ -0,0 +1,535 @@
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
formatData []byte
}
func (ctx *readContext) rememberRead(b []byte) {
ctx.formatData = append(ctx.formatData, b...)
}
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 {
formatDataStart := len(ctx.formatData)
for _, rule := range r {
if err := rule.Read(rd, ctx); err != nil {
if errors.Is(err, ErrFormat) {
return err
}
if errors.Is(err, errInvalidData) {
return NewFormatError(ctx.formatData[formatDataStart:], err)
}
return err
}
}
return nil
}
func (r Rules) Match(b []byte, pool *BufferPool) bool {
if r == nil {
return false
}
tmp := pool.Get()
defer pool.Put(tmp)
reader := newSliceReader(b)
ctx := readContext{
FlexBuffer: WrapFlexBuffer(tmp),
BufferPool: pool,
}
if err := r.Read(&reader, &ctx); err != nil {
return false
}
return len(reader.buf) == 0
}
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)]
n, err := io.ReadFull(rd, buf)
ctx.rememberRead(buf[:n])
if 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]
n, err := io.ReadFull(rd, buf)
ctx.rememberRead(buf[:n])
if 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]
n, err := io.ReadFull(rd, buf)
ctx.rememberRead(buf[:n])
if 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]
n, err := io.ReadFull(rd, buf)
ctx.rememberRead(buf[:n])
if 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 {
tmp := ctx.Get()
defer ctx.Put(tmp)
buf := tmp[:4]
n, err := io.ReadFull(rd, buf)
ctx.rememberRead(buf[:n])
if err != nil {
return err
}
_ = binary.BigEndian.Uint32(buf)
// 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]
n, err := io.ReadFull(rd, buf)
ctx.rememberRead(buf[:n])
if err != nil {
if errors.Is(err, io.ErrShortBuffer) {
return errInvalidData
}
return err
}
var size int
for i := range buf {
size <<= 8
size |= int(buf[i])
}
ctx.nextDataSize = size
case NumFormatLE:
buf := tmp[:r.length]
n, err := io.ReadFull(rd, buf)
ctx.rememberRead(buf[:n])
if err != nil {
if errors.Is(err, io.ErrShortBuffer) {
return errInvalidData
}
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 {
ctx.rememberRead(tmp[:n+1])
} else {
ctx.rememberRead(tmp[:n])
}
if err != nil {
return err
}
size64, err := strconv.ParseInt(string(tmp[:n]), 10, 32)
if err != nil {
return fmt.Errorf("%w: %v", errInvalidData, err)
}
ctx.nextDataSize = int(size64)
case NumFormatHex:
n, err := ReadUntil(rd, tmp, r.end)
if err == nil {
ctx.rememberRead(tmp[:n+1])
} else {
ctx.rememberRead(tmp[:n])
}
if err != nil {
return err
}
size64, err := strconv.ParseInt(string(tmp[:n]), 16, 32)
if err != nil {
return fmt.Errorf("%w: %v", errInvalidData, 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 errInvalidData
}
n, err := io.ReadFull(rd, buf)
ctx.rememberRead(buf[:n])
return err
}
+42
View File
@@ -0,0 +1,42 @@
package conceal
import "io"
type sliceReader struct {
buf []byte
}
func newSliceReader(buf []byte) sliceReader {
return sliceReader{buf: buf}
}
func (r *sliceReader) Read(p []byte) (n int, err error) {
if len(r.buf) == 0 {
return 0, io.EOF
}
n = copy(p, r.buf)
r.buf = r.buf[n:]
return n, nil
}
type sliceWriter struct {
buf []byte
}
func newSliceWriter(buf []byte) sliceWriter {
return sliceWriter{buf: buf[:0]}
}
func (w *sliceWriter) Reset(buf []byte) {
w.buf = buf[:0]
}
func (w *sliceWriter) Write(p []byte) (n int, err error) {
w.buf = append(w.buf, p...)
return len(p), nil
}
func (w *sliceWriter) Bytes() []byte {
return w.buf
}
+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)
}
+296
View File
@@ -0,0 +1,296 @@
package conceal
import (
"encoding/binary"
"errors"
"sync"
"time"
)
// UDPDatagramPipeline applies UDP conceal transforms at datagram boundaries.
// It is transport-agnostic and can therefore be reused by different UDP binds.
type UDPDatagramPipeline struct {
pool *BufferPool
classifier packetClassifier
framing frameEncoding
framingActive bool
masquerade masqueradeEncoding
masqueradeActive bool
preludeActive bool
preludeResendInterval time.Duration
rulesArr [5]Rules
junkCount int
junkGen *junkGenerator
}
func NewUDPDatagramPipeline(
pool *sync.Pool,
framedOpts FramedOpts,
preludeOpts PreludeOpts,
masqueradeOpts MasqueradeOpts,
) *UDPDatagramPipeline {
if preludeOpts.Jmin > preludeOpts.Jmax {
preludeOpts.Jmin, preludeOpts.Jmax = preludeOpts.Jmax, preludeOpts.Jmin
}
pipeline := &UDPDatagramPipeline{
pool: WrapBufferPool(pool),
classifier: newPacketClassifier(framedOpts),
preludeActive: !preludeOpts.IsEmpty(),
preludeResendInterval: preludeOpts.ResendInterval,
rulesArr: preludeOpts.RulesArr,
junkCount: preludeOpts.Jc,
}
if pipeline.junkCount > 0 {
pipeline.junkGen = newJunkGenerator(preludeOpts.Jmin, preludeOpts.Jmax)
}
pipeline.framing, pipeline.framingActive = newFrameEncoding(framedOpts)
pipeline.masquerade, pipeline.masqueradeActive = newMasqueradeEncoding(pipeline.pool, masqueradeOpts)
return pipeline
}
func (p *UDPDatagramPipeline) Active() bool {
return p.preludeActive || p.framingActive || p.masqueradeActive
}
func (p *UDPDatagramPipeline) PreludeActive() bool {
return p.preludeActive
}
func (p *UDPDatagramPipeline) PreludeResendInterval() time.Duration {
return p.preludeResendInterval
}
func (p *UDPDatagramPipeline) Encode(dst, src []byte) (int, error) {
data := src
if p.framingActive && p.masqueradeActive {
tmp := p.pool.Get()
n := p.framing.Encode(tmp, src)
if n == 0 {
n = copy(tmp, src)
}
data = tmp[:n]
defer p.pool.Put(tmp)
}
if p.framingActive && !p.masqueradeActive {
n := p.framing.Encode(dst, src)
if n == 0 {
n = copy(dst, src)
}
return n, nil
}
if p.masqueradeActive {
return p.masquerade.Encode(dst, data)
}
return copy(dst, data), nil
}
func (p *UDPDatagramPipeline) DecodeInPlace(buf []byte, n int) (int, bool) {
n, err := p.DecodeInPlaceErr(buf, n)
return n, err == nil
}
func (p *UDPDatagramPipeline) DecodeInPlaceErr(buf []byte, n int) (int, error) {
if p.masqueradeActive {
var err error
n, err = p.masquerade.DecodeInPlace(buf, n)
if err != nil {
if !errors.Is(err, ErrFormat) {
err = NewFormatError(buf[:n], err)
}
return 0, err
}
}
if p.framingActive {
var err error
n, err = p.framing.Decode(buf[:n])
if err != nil {
return 0, err
}
}
if !p.classifier.IsValid(buf[:n]) {
return 0, NewFormatError(buf[:n], errInvalidData)
}
return n, nil
}
func (p *UDPDatagramPipeline) EmitPrelude(emit func([]byte) error) error {
if !p.preludeActive {
return nil
}
buf := p.pool.Get()
defer p.pool.Put(buf)
ctx := writeContext{
FlexBuffer: WrapFlexBuffer(nil),
BufferPool: p.pool,
}
w := newSliceWriter(buf)
for _, rules := range p.rulesArr {
if rules == nil {
continue
}
w.Reset(buf)
if err := rules.Write(&w, &ctx); err != nil {
return err
}
if err := emit(w.Bytes()); err != nil {
return err
}
}
for range p.junkCount {
if err := emit(p.junkGen.generate(buf)); err != nil {
return err
}
}
return nil
}
type packetClassifier struct {
header struct {
initial RangedHeader
response RangedHeader
cookie RangedHeader
transport RangedHeader
}
}
func newPacketClassifier(opts FramedOpts) packetClassifier {
classifier := packetClassifier{}
if opts.HeaderCompat && opts.H1 != nil {
classifier.header.initial = *opts.H1
} else {
classifier.header.initial = RangedHeader{WireguardMsgInitiationType, WireguardMsgInitiationType}
}
if opts.HeaderCompat && opts.H2 != nil {
classifier.header.response = *opts.H2
} else {
classifier.header.response = RangedHeader{WireguardMsgResponseType, WireguardMsgResponseType}
}
if opts.HeaderCompat && opts.H3 != nil {
classifier.header.cookie = *opts.H3
} else {
classifier.header.cookie = RangedHeader{WireguardMsgCookieReplyType, WireguardMsgCookieReplyType}
}
if opts.HeaderCompat && opts.H4 != nil {
classifier.header.transport = *opts.H4
} else {
classifier.header.transport = RangedHeader{WireguardMsgTransportType, WireguardMsgTransportType}
}
return classifier
}
func (c packetClassifier) packetKind(packet []byte) frameRecordKind {
if len(packet) < 4 {
return frameRecordInvalid
}
header := binary.LittleEndian.Uint32(packet[:4])
switch {
case len(packet) == WireguardMsgInitiationSize && c.header.initial.Validate(header):
return frameRecordInitiation
case len(packet) == WireguardMsgResponseSize && c.header.response.Validate(header):
return frameRecordResponse
case len(packet) == WireguardMsgCookieReplySize && c.header.cookie.Validate(header):
return frameRecordCookie
case len(packet) >= WireguardMsgTransportMinSize && c.header.transport.Validate(header):
return frameRecordTransport
default:
return frameRecordInvalid
}
}
func (c packetClassifier) IsValid(packet []byte) bool {
return c.packetKind(packet) != frameRecordInvalid
}
func (c packetClassifier) IsInitiation(packet []byte) bool {
return c.packetKind(packet) == frameRecordInitiation
}
func (c packetClassifier) MatchesInitiationHeader(packet []byte) bool {
if len(packet) < 4 {
return false
}
return c.header.initial.Validate(binary.LittleEndian.Uint32(packet[:4]))
}
type masqueradeEncoding struct {
rulesIn Rules
rulesOut Rules
pool *BufferPool
}
func newMasqueradeEncoding(pool *BufferPool, opts MasqueradeOpts) (masqueradeEncoding, bool) {
if opts.RulesIn == nil && opts.RulesOut == nil {
return masqueradeEncoding{}, false
}
return masqueradeEncoding{
rulesIn: opts.RulesIn,
rulesOut: opts.RulesOut,
pool: pool,
}, true
}
func (e masqueradeEncoding) DecodeInPlace(buf []byte, n int) (int, error) {
if e.rulesIn == nil {
return n, nil
}
r := newSliceReader(buf[:n])
ctx := readContext{
FlexBuffer: WrapFlexBuffer(buf),
BufferPool: e.pool,
}
if err := e.rulesIn.Read(&r, &ctx); err != nil {
return 0, err
}
return ctx.Len(), nil
}
func (e masqueradeEncoding) Encode(dst, src []byte) (int, error) {
if e.rulesOut == nil {
return copy(dst, src), nil
}
ctx := writeContext{
FlexBuffer: WrapFlexBuffer(src),
BufferPool: e.pool,
}
w := newSliceWriter(dst)
if err := e.rulesOut.Write(&w, &ctx); err != nil {
return 0, err
}
return len(w.Bytes()), nil
}
+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
}
+57 -17
View File
@@ -16,12 +16,17 @@ 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)
_ Fallbackable = (*StdNetBind)(nil)
)
// StdNetBind implements Bind for all platforms. While Windows has its own Bind
@@ -31,10 +36,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 +48,15 @@ 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
fallbackPort uint16
}
func NewStdNetBind() Bind {
@@ -70,6 +81,12 @@ func NewStdNetBind() Bind {
return &msgs
},
},
bufPool: sync.Pool{
New: func() any {
return make([]byte, 65535)
},
},
}
}
@@ -79,7 +96,8 @@ type StdNetEndpoint struct {
// src is the current sticky source address and interface index, if
// supported. Typically this is a PKTINFO structure from/for control
// messages, see unix.PKTINFO for an example.
src []byte
src []byte
prelude conceal.PreludeState
}
var (
@@ -102,6 +120,15 @@ func (e *StdNetEndpoint) ClearSrc() {
// Truncate src, no need to reallocate.
e.src = e.src[:0]
}
e.ResetPreludeState()
}
func (e *StdNetEndpoint) PreludeState() *conceal.PreludeState {
return &e.prelude
}
func (e *StdNetEndpoint) ResetPreludeState() {
e.prelude.Reset()
}
func (e *StdNetEndpoint) DstIP() netip.Addr {
@@ -152,9 +179,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 +204,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 +215,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,20 +256,21 @@ type batchWriter interface {
func (s *StdNetBind) receiveIP(
br batchReader,
conn *net.UDPConn,
conn UDPConn,
rxOffload bool,
bufs [][]byte,
sizes []int,
eps []Endpoint,
) (n int, err error) {
msgs := s.getMessages()
for i := range bufs {
(*msgs)[i].Buffers[0] = bufs[i]
(*msgs)[i].OOB = (*msgs)[i].OOB[:cap((*msgs)[i].OOB)]
}
defer s.putMessages(msgs)
var numMsgs int
if runtime.GOOS == "linux" || runtime.GOOS == "android" {
for i := range bufs {
(*msgs)[i].Buffers[0] = bufs[i]
(*msgs)[i].OOB = (*msgs)[i].OOB[:cap((*msgs)[i].OOB)]
}
if rxOffload {
readAt := len(*msgs) - (IdealBatchSize / udpSegmentMaxDatagrams)
numMsgs, err = br.ReadBatch((*msgs)[readAt:], 0)
@@ -257,6 +289,8 @@ func (s *StdNetBind) receiveIP(
}
} else {
msg := &(*msgs)[0]
msg.Buffers[0] = bufs[0]
msg.OOB = msg.OOB[:cap(msg.OOB)]
msg.N, msg.NN, _, msg.Addr, err = conn.ReadMsgUDP(msg.Buffers[0], msg.OOB)
if err != nil {
return 0, err
@@ -277,13 +311,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)
}
@@ -304,11 +338,17 @@ func (s *StdNetBind) Close() error {
var err1, err2 error
if s.ipv4 != nil {
if closer, ok := s.ipv4PC.(fallbackSessionCloser); ok {
closer.closeFallbackSessions()
}
err1 = s.ipv4.Close()
s.ipv4 = nil
s.ipv4PC = nil
}
if s.ipv6 != nil {
if closer, ok := s.ipv6PC.(fallbackSessionCloser); ok {
closer.closeFallbackSessions()
}
err2 = s.ipv6.Close()
s.ipv6 = nil
s.ipv6PC = nil
@@ -409,7 +449,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
+327
View File
@@ -0,0 +1,327 @@
package conn
import (
"context"
"errors"
"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)
_ Fallbackable = (*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
fallbackPort uint16
}
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 {
b.streamPacketPool.Put(sp)
if b.fallbackPort != 0 && errors.Is(err, conceal.ErrFormat) {
b.proxyStreamFallback(ep, conceal.FormatErrorData(err))
return
}
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:
}
rawConn, err := listener.Accept()
if err != nil {
// log this error somewhere
break
}
conn := b.upgradeConn(rawConn, nil, false)
b.wg.Add(1)
go b.handleAccepted(rawConn, conn)
}
}
func (b *BindStream) handleAccepted(rawConn, conn net.Conn) {
defer b.wg.Done()
ap, err := netip.ParseAddrPort(conn.RemoteAddr().String())
if err != nil {
// add logs
}
ep := &streamEndpoint{
conn: conn,
rawConn: rawConn,
dst: ap,
outboundPrelude: false,
}
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
}
rawConn, err := b.dialer.DialContext(b.ctx, "tcp", ep.DstToString())
if err != nil {
return fmt.Errorf("failed to dial context: %v", err)
}
ep.ResetPreludeState()
conn := b.upgradeConn(rawConn, ep.PreludeState(), ep.outboundPrelude)
ep.conn = conn
ep.rawConn = rawConn
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(),
outboundPrelude: true,
}, nil
}
func (b *BindStream) BatchSize() int {
return 1
}
var _ Endpoint = (*streamEndpoint)(nil)
type streamEndpoint struct {
conn net.Conn
rawConn net.Conn
dst netip.AddrPort
prelude conceal.PreludeState
outboundPrelude bool
mutex sync.Mutex
}
func (e *streamEndpoint) Close() {
e.mutex.Lock()
defer e.mutex.Unlock()
if e.conn != nil {
e.conn.Close()
e.conn = nil
}
if e.rawConn != nil {
e.rawConn.Close()
e.rawConn = nil
}
e.ResetPreludeState()
}
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() {
e.ResetPreludeState()
}
func (e *streamEndpoint) PreludeState() *conceal.PreludeState {
return &e.prelude
}
func (e *streamEndpoint) ResetPreludeState() {
e.prelude.Reset()
}
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()
+206
View File
@@ -0,0 +1,206 @@
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 Fallbackable interface {
SetFallbackPort(port uint16)
}
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 udpConcealPipeline(
framedOpts conceal.FramedOpts,
preludeOpts conceal.PreludeOpts,
masqueradeOpts conceal.MasqueradeOpts,
) concealPipeline {
stages := make([]concealStage, 0, 3)
if hasMasquerade(masqueradeOpts) {
stages = append(stages, concealStageMasquerade)
}
if hasFramed(framedOpts) {
stages = append(stages, concealStageFramed)
}
if !preludeOpts.IsEmpty() {
stages = append(stages, concealStagePrelude)
}
return concealPipeline{stages: stages}
}
func (b *StdNetBind) udpConcealPipeline() concealPipeline {
return udpConcealPipeline(b.framedOpts, b.preludeOpts, b.masqueradeOpts)
}
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
}
}
}
if b.fallbackPort != 0 {
conn = newFallbackUDPConn(conn, origin, b.fallbackPort)
}
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
}
}
}
if b.fallbackPort != 0 {
conn = newFallbackBatchConn(conn, origin, b.fallbackPort)
}
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 *StdNetBind) SetFallbackPort(port uint16) {
b.fallbackPort = port
}
func (b *BindStream) upgradeConn(conn net.Conn, preludeState *conceal.PreludeState, emitPrelude bool) 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.NewPreludeConnWithState(recordConn, &b.bufferPool, b.framedOpts, b.preludeOpts, preludeState, emitPrelude); 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
}
func (b *BindStream) SetFallbackPort(port uint16) {
b.fallbackPort = port
}
+451
View File
@@ -0,0 +1,451 @@
package conn
import (
"bytes"
"encoding/binary"
"errors"
"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 TestBindStreamTCPPreludeInjectsOncePerSession(t *testing.T) {
senderRaw, receiverRaw := net.Pipe()
defer senderRaw.Close()
defer receiverRaw.Close()
bind := newRecordAwareBind(t)
sender := upgradeTestStreamConn(bind, senderRaw, true)
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
}()
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 TestBindStreamTCPPreludeInjectsBeforeFirstOutboundRecord(t *testing.T) {
senderRaw, receiverRaw := net.Pipe()
defer senderRaw.Close()
defer receiverRaw.Close()
bind := newRecordAwareBind(t)
sender := upgradeTestStreamConn(bind, senderRaw, true)
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
}()
prelude := readRecord(t, receiver, 16)
if !bytes.Equal(prelude, []byte{0xaa, 0xbb}) {
t.Fatalf("prelude record = %x, want aabb", prelude)
}
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 := upgradeTestStreamConn(bind, senderRaw, true)
receiver := upgradeTestStreamConn(bind, receiverRaw, false)
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 TestBindStreamTCPPreludeRejectsLeadingUnknownRecords(t *testing.T) {
senderRaw, receiverRaw := net.Pipe()
defer senderRaw.Close()
defer receiverRaw.Close()
bind := newRecordAwareBind(t)
receiver := upgradeTestStreamConn(bind, receiverRaw, false)
recordWriter := mustRecordConn(t, senderRaw, &bind.bufferPool, conceal.MasqueradeOpts{
RulesOut: mustParseRules(t, "<dz be 2><d>"),
})
writeErr := make(chan error, 1)
go func() {
_, err := recordWriter.WriteRecord([]byte{0xde, 0xad})
writeErr <- err
}()
buf := make([]byte, 16)
if _, err := receiver.Read(buf); !errors.Is(err, conceal.ErrFormat) {
t.Fatalf("receiver error = %v, want ErrFormat", err)
}
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 := upgradeTestStreamConn(bind, senderRaw, false)
receiver := upgradeTestStreamConn(bind, receiverRaw, false)
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 upgradeTestStreamConn(bind *BindStream, conn net.Conn, emitPrelude bool) net.Conn {
var state *conceal.PreludeState
if emitPrelude {
state = new(conceal.PreludeState)
}
return bind.upgradeConn(conn, state, emitPrelude)
}
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(NewConcealBind(NewStdNetBind()), NewBindStream())
}
+7
View File
@@ -0,0 +1,7 @@
//go:build windows
package conn
func NewDefaultBind() Bind {
return NewMultibind(NewConcealBind(NewWinRingBind()), NewBindStream())
}
+336
View File
@@ -0,0 +1,336 @@
package conn
import (
"errors"
"io"
"net"
"strconv"
"sync"
"github.com/amnezia-vpn/amneziawg-go/conceal"
"golang.org/x/net/ipv4"
)
func fallbackTCPAddress(addr net.Addr, port uint16) string {
host := "127.0.0.1"
if tcpAddr, ok := addr.(*net.TCPAddr); ok && len(tcpAddr.IP) > 0 && tcpAddr.IP.To4() == nil {
host = "::1"
}
return net.JoinHostPort(host, strconv.Itoa(int(port)))
}
func fallbackUDPAddress(addr net.Addr, port uint16) *net.UDPAddr {
ip := net.IPv4(127, 0, 0, 1)
if udpAddr, ok := addr.(*net.UDPAddr); ok && len(udpAddr.IP) > 0 && udpAddr.IP.To4() == nil {
ip = net.IPv6loopback
}
return &net.UDPAddr{IP: ip, Port: int(port)}
}
func fallbackUDPAddressForEndpoint(ep Endpoint, port uint16) *net.UDPAddr {
ip := net.IPv4(127, 0, 0, 1)
if ep != nil && ep.DstIP().Is6() {
ip = net.IPv6loopback
}
return &net.UDPAddr{IP: ip, Port: int(port)}
}
func (b *BindStream) proxyStreamFallback(ep *streamEndpoint, first []byte) {
rawConn := ep.rawConn
if rawConn == nil {
rawConn = ep.conn
}
if rawConn == nil {
return
}
fallbackConn, err := b.dialer.DialContext(b.ctx, "tcp", fallbackTCPAddress(rawConn.RemoteAddr(), b.fallbackPort))
if err != nil {
ep.Close()
return
}
if len(first) > 0 {
if _, err := fallbackConn.Write(first); err != nil {
fallbackConn.Close()
ep.Close()
return
}
}
b.wg.Add(1)
go func() {
defer b.wg.Done()
_, _ = io.Copy(rawConn, fallbackConn)
rawConn.Close()
fallbackConn.Close()
}()
_, _ = io.Copy(fallbackConn, rawConn)
rawConn.Close()
fallbackConn.Close()
}
type fallbackUDPConn struct {
UDPConn
origin UDPConn
port uint16
mu sync.Mutex
sessions map[string]*fallbackUDPSession
}
func newFallbackUDPConn(conn, origin UDPConn, port uint16) UDPConn {
return &fallbackUDPConn{
UDPConn: conn,
origin: origin,
port: port,
sessions: make(map[string]*fallbackUDPSession),
}
}
func (c *fallbackUDPConn) ReadMsgUDP(b, oob []byte) (n, oobn, flags int, addr *net.UDPAddr, err error) {
for {
n, oobn, flags, addr, err = c.UDPConn.ReadMsgUDP(b, oob)
if err == nil {
return n, oobn, flags, addr, nil
}
if !errors.Is(err, conceal.ErrFormat) || addr == nil {
return n, oobn, flags, addr, err
}
if data := conceal.FormatErrorData(err); len(data) > 0 {
_ = c.forward(addr, data)
}
}
}
func (c *fallbackUDPConn) Close() error {
c.mu.Lock()
for key, session := range c.sessions {
session.close()
delete(c.sessions, key)
}
c.mu.Unlock()
return c.UDPConn.Close()
}
func (c *fallbackUDPConn) forward(remote *net.UDPAddr, data []byte) error {
session, err := c.session(remote)
if err != nil {
return err
}
_, err = session.conn.Write(data)
return err
}
func (c *fallbackUDPConn) session(remote *net.UDPAddr) (*fallbackUDPSession, error) {
key := remote.String()
c.mu.Lock()
if session := c.sessions[key]; session != nil {
c.mu.Unlock()
return session, nil
}
c.mu.Unlock()
fallbackConn, err := net.DialUDP("udp", nil, fallbackUDPAddress(remote, c.port))
if err != nil {
return nil, err
}
session := &fallbackUDPSession{
parent: c,
remote: &net.UDPAddr{
IP: append(net.IP(nil), remote.IP...),
Port: remote.Port,
Zone: remote.Zone,
},
conn: fallbackConn,
}
c.mu.Lock()
if existing := c.sessions[key]; existing != nil {
c.mu.Unlock()
fallbackConn.Close()
return existing, nil
}
c.sessions[key] = session
c.mu.Unlock()
go session.relay()
return session, nil
}
func (c *fallbackUDPConn) removeSession(remote string) {
c.mu.Lock()
delete(c.sessions, remote)
c.mu.Unlock()
}
type fallbackUDPSession struct {
parent *fallbackUDPConn
remote *net.UDPAddr
conn *net.UDPConn
once sync.Once
}
func (s *fallbackUDPSession) relay() {
defer s.parent.removeSession(s.remote.String())
buf := make([]byte, 65535)
for {
n, err := s.conn.Read(buf)
if err != nil {
return
}
_, _, _ = s.parent.origin.WriteMsgUDP(buf[:n], nil, s.remote)
}
}
func (s *fallbackUDPSession) close() {
s.once.Do(func() {
s.conn.Close()
})
}
type fallbackBatchConn struct {
LinuxPacketConn
origin LinuxPacketConn
port uint16
mu sync.Mutex
sessions map[string]*fallbackBatchSession
}
type fallbackSessionCloser interface {
closeFallbackSessions()
}
func newFallbackBatchConn(conn, origin LinuxPacketConn, port uint16) LinuxPacketConn {
return &fallbackBatchConn{
LinuxPacketConn: conn,
origin: origin,
port: port,
sessions: make(map[string]*fallbackBatchSession),
}
}
func (c *fallbackBatchConn) ReadBatch(ms []ipv4.Message, flags int) (n int, err error) {
for {
n, err = c.LinuxPacketConn.ReadBatch(ms, flags)
if err == nil {
return n, nil
}
if !errors.Is(err, conceal.ErrFormat) {
return n, err
}
data := conceal.FormatErrorData(err)
remote := firstBatchUDPAddr(ms)
if len(data) == 0 || remote == nil {
return n, err
}
_ = c.forward(remote, data)
}
}
func (c *fallbackBatchConn) closeFallbackSessions() {
c.mu.Lock()
for key, session := range c.sessions {
session.close()
delete(c.sessions, key)
}
c.mu.Unlock()
}
func (c *fallbackBatchConn) forward(remote *net.UDPAddr, data []byte) error {
session, err := c.session(remote)
if err != nil {
return err
}
_, err = session.conn.Write(data)
return err
}
func (c *fallbackBatchConn) session(remote *net.UDPAddr) (*fallbackBatchSession, error) {
key := remote.String()
c.mu.Lock()
if session := c.sessions[key]; session != nil {
c.mu.Unlock()
return session, nil
}
c.mu.Unlock()
fallbackConn, err := net.DialUDP("udp", nil, fallbackUDPAddress(remote, c.port))
if err != nil {
return nil, err
}
session := &fallbackBatchSession{
parent: c,
remote: &net.UDPAddr{
IP: append(net.IP(nil), remote.IP...),
Port: remote.Port,
Zone: remote.Zone,
},
conn: fallbackConn,
}
c.mu.Lock()
if existing := c.sessions[key]; existing != nil {
c.mu.Unlock()
fallbackConn.Close()
return existing, nil
}
c.sessions[key] = session
c.mu.Unlock()
go session.relay()
return session, nil
}
func (c *fallbackBatchConn) removeSession(remote string) {
c.mu.Lock()
delete(c.sessions, remote)
c.mu.Unlock()
}
type fallbackBatchSession struct {
parent *fallbackBatchConn
remote *net.UDPAddr
conn *net.UDPConn
once sync.Once
}
func (s *fallbackBatchSession) relay() {
defer s.parent.removeSession(s.remote.String())
buf := make([]byte, 65535)
for {
n, err := s.conn.Read(buf)
if err != nil {
return
}
msg := ipv4.Message{
Buffers: [][]byte{buf[:n]},
Addr: s.remote,
}
_, _ = s.parent.origin.WriteBatch([]ipv4.Message{msg}, 0)
}
}
func (s *fallbackBatchSession) close() {
s.once.Do(func() {
s.conn.Close()
})
}
func firstBatchUDPAddr(ms []ipv4.Message) *net.UDPAddr {
for i := range ms {
if ms[i].N == 0 || ms[i].Addr == nil {
continue
}
if addr, ok := ms[i].Addr.(*net.UDPAddr); ok {
return addr
}
}
return nil
}
+198
View File
@@ -0,0 +1,198 @@
package conn
import (
"bytes"
"io"
"net"
"strconv"
"sync"
"testing"
"time"
"github.com/amnezia-vpn/amneziawg-go/conceal"
)
func TestBindStreamFallbackProxiesFormatErrorToTCPPort(t *testing.T) {
fallbackListener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen fallback: %v", err)
}
defer fallbackListener.Close()
fallbackPort := fallbackListener.Addr().(*net.TCPAddr).Port
probe := []byte("GET / HTTP/1.1\r\n")
response := []byte("HTTP/1.1 200 OK\r\n\r\n")
received := make(chan []byte, 1)
served := make(chan error, 1)
go func() {
conn, err := fallbackListener.Accept()
if err != nil {
served <- err
return
}
defer conn.Close()
buf := make([]byte, len(probe))
if _, err := io.ReadFull(conn, buf); err != nil {
served <- err
return
}
received <- bytes.Clone(buf)
_, err = conn.Write(response)
served <- err
}()
bind := NewBindStream()
bind.SetMasqueradeOpts(conceal.MasqueradeOpts{
RulesIn: mustParseRules(t, "<b 0xfeed><dz be 2><d>"),
RulesOut: mustParseRules(t, "<b 0xfeed><dz be 2><d>"),
})
bind.SetFallbackPort(uint16(fallbackPort))
listenPort := freeTCPPort(t)
_, actualPort, err := bind.Open(listenPort)
if err != nil {
t.Fatalf("open bind stream: %v", err)
}
defer bind.Close()
client, err := net.Dial("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(int(actualPort))))
if err != nil {
t.Fatalf("dial bind stream: %v", err)
}
defer client.Close()
if err := client.SetDeadline(time.Now().Add(2 * time.Second)); err != nil {
t.Fatalf("set deadline: %v", err)
}
if _, err := client.Write(probe[:2]); err != nil {
t.Fatalf("write initial probe bytes: %v", err)
}
if _, err := client.Write(probe[2:]); err != nil {
t.Fatalf("write remaining probe bytes: %v", err)
}
gotResponse := make([]byte, len(response))
if _, err := io.ReadFull(client, gotResponse); err != nil {
t.Fatalf("read fallback response: %v", err)
}
if !bytes.Equal(gotResponse, response) {
t.Fatalf("fallback response = %q, want %q", gotResponse, response)
}
select {
case gotProbe := <-received:
if !bytes.Equal(gotProbe, probe) {
t.Fatalf("fallback received = %q, want %q", gotProbe, probe)
}
case <-time.After(2 * time.Second):
t.Fatal("fallback service did not receive probe")
}
if err := <-served; err != nil {
t.Fatalf("fallback service failed: %v", err)
}
}
func TestFallbackUDPConnProxiesFormatErrorToUDPPort(t *testing.T) {
fallbackServer, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)})
if err != nil {
t.Fatalf("listen fallback udp: %v", err)
}
defer fallbackServer.Close()
probe := []byte("GE")
response := []byte("udp-ok")
received := make(chan []byte, 1)
served := make(chan error, 1)
go func() {
buf := make([]byte, 64)
n, addr, err := fallbackServer.ReadFromUDP(buf)
if err != nil {
served <- err
return
}
received <- bytes.Clone(buf[:n])
_, err = fallbackServer.WriteToUDP(response, addr)
served <- err
}()
raw, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)})
if err != nil {
t.Fatalf("listen awg udp: %v", err)
}
defer raw.Close()
pool := sync.Pool{
New: func() any {
return make([]byte, 65535)
},
}
masquerade, ok := conceal.NewMasqueradeUDPConn(raw, &pool, conceal.MasqueradeOpts{
RulesIn: mustParseRules(t, "<b 0xfeed><dz be 2><d>"),
})
if !ok {
t.Fatal("expected masquerade udp conn")
}
fallback := newFallbackUDPConn(masquerade, raw, uint16(fallbackServer.LocalAddr().(*net.UDPAddr).Port))
readDone := make(chan error, 1)
go func() {
buf := make([]byte, 64)
_, _, _, _, err := fallback.ReadMsgUDP(buf, nil)
readDone <- err
}()
client, err := net.DialUDP("udp4", nil, raw.LocalAddr().(*net.UDPAddr))
if err != nil {
t.Fatalf("dial awg udp: %v", err)
}
defer client.Close()
if err := client.SetDeadline(time.Now().Add(2 * time.Second)); err != nil {
t.Fatalf("set udp deadline: %v", err)
}
if _, err := client.Write(probe); err != nil {
t.Fatalf("write udp probe: %v", err)
}
gotResponse := make([]byte, len(response))
if _, err := io.ReadFull(client, gotResponse); err != nil {
t.Fatalf("read udp fallback response: %v", err)
}
if !bytes.Equal(gotResponse, response) {
t.Fatalf("udp fallback response = %q, want %q", gotResponse, response)
}
select {
case gotProbe := <-received:
if !bytes.Equal(gotProbe, probe) {
t.Fatalf("udp fallback received = %q, want %q", gotProbe, probe)
}
case <-time.After(2 * time.Second):
t.Fatal("udp fallback service did not receive probe")
}
if err := <-served; err != nil {
t.Fatalf("udp fallback service failed: %v", err)
}
raw.Close()
select {
case <-readDone:
case <-time.After(2 * time.Second):
t.Fatal("fallback ReadMsgUDP did not exit after close")
}
}
func freeTCPPort(t *testing.T) uint16 {
t.Helper()
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("allocate tcp port: %v", err)
}
defer listener.Close()
return uint16(listener.Addr().(*net.TCPAddr).Port)
}
+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"
}
+62
View File
@@ -0,0 +1,62 @@
package conn
import "github.com/amnezia-vpn/amneziawg-go/conceal"
type PreludeEndpoint interface {
Endpoint
PreludeState() *conceal.PreludeState
ResetPreludeState()
}
type wrappedEndpoint interface {
Endpoint
UnwrapEndpoint() Endpoint
}
type preludeEndpoint struct {
Endpoint
prelude conceal.PreludeState
}
func wrapPreludeEndpoint(ep Endpoint) Endpoint {
if ep == nil {
return nil
}
if _, ok := ep.(PreludeEndpoint); ok {
return ep
}
if _, ok := ep.(wrappedEndpoint); ok {
return ep
}
return &preludeEndpoint{Endpoint: ep}
}
func unwrapEndpoint(ep Endpoint) Endpoint {
for {
wrapped, ok := ep.(wrappedEndpoint)
if !ok {
return ep
}
ep = wrapped.UnwrapEndpoint()
}
}
func (e *preludeEndpoint) UnwrapEndpoint() Endpoint {
return e.Endpoint
}
func (e *preludeEndpoint) ClearSrc() {
e.Endpoint.ClearSrc()
e.ResetPreludeState()
}
func (e *preludeEndpoint) PreludeState() *conceal.PreludeState {
return &e.prelude
}
func (e *preludeEndpoint) ResetPreludeState() {
e.prelude.Reset()
}
var _ PreludeEndpoint = (*preludeEndpoint)(nil)
var _ wrappedEndpoint = (*preludeEndpoint)(nil)
+412
View File
@@ -0,0 +1,412 @@
package conn
import (
"bytes"
"errors"
"net"
"sync"
"sync/atomic"
"time"
"github.com/amnezia-vpn/amneziawg-go/conceal"
)
var (
_ Bind = (*ConcealBind)(nil)
_ Framable = (*ConcealBind)(nil)
_ Preludable = (*ConcealBind)(nil)
_ Masqueradable = (*ConcealBind)(nil)
_ Fallbackable = (*ConcealBind)(nil)
)
type ConcealBind struct {
inner Bind
mu sync.Mutex
bufPool sync.Pool
framedOpts conceal.FramedOpts
preludeOpts conceal.PreludeOpts
masqueradeOpts conceal.MasqueradeOpts
fallbackPort uint16
fallbackSessions map[string]*concealBindFallbackSession
pipeline atomic.Pointer[conceal.UDPDatagramPipeline]
}
func NewConcealBind(inner Bind) *ConcealBind {
bind := &ConcealBind{
inner: inner,
bufPool: sync.Pool{
New: func() any {
return make([]byte, 65535)
},
},
}
bind.rebuildPipelineLocked()
return bind
}
func (b *ConcealBind) rebuildPipelineLocked() {
b.pipeline.Store(conceal.NewUDPDatagramPipeline(&b.bufPool, b.framedOpts, b.preludeOpts, b.masqueradeOpts))
}
func (b *ConcealBind) currentPipeline() *conceal.UDPDatagramPipeline {
return b.pipeline.Load()
}
func (b *ConcealBind) udpConcealPipeline() concealPipeline {
b.mu.Lock()
defer b.mu.Unlock()
return udpConcealPipeline(b.framedOpts, b.preludeOpts, b.masqueradeOpts)
}
func (b *ConcealBind) Open(port uint16) ([]ReceiveFunc, uint16, error) {
recvFns, actualPort, err := b.inner.Open(port)
if err != nil {
return nil, 0, err
}
wrapped := make([]ReceiveFunc, len(recvFns))
for i, fn := range recvFns {
wrapped[i] = b.wrapReceiveFunc(fn)
}
return wrapped, actualPort, nil
}
func (b *ConcealBind) wrapReceiveFunc(fn ReceiveFunc) ReceiveFunc {
return func(packets [][]byte, sizes []int, eps []Endpoint) (int, error) {
n, err := fn(packets, sizes, eps)
if err != nil {
return 0, err
}
if n == 0 {
return 0, nil
}
pipeline := b.currentPipeline()
if pipeline == nil || !pipeline.Active() {
wrapPreludeEndpoints(eps[:n])
return n, nil
}
for i := 0; i < n; i++ {
if sizes[i] == 0 || eps[i] == nil {
sizes[i] = 0
eps[i] = nil
continue
}
size, err := pipeline.DecodeInPlaceErr(packets[i], sizes[i])
if err != nil {
if errors.Is(err, conceal.ErrFormat) {
data := conceal.FormatErrorData(err)
if len(data) == 0 {
data = bytes.Clone(packets[i][:sizes[i]])
}
_ = b.forwardFallbackUDP(eps[i], data)
}
sizes[i] = 0
eps[i] = nil
continue
}
sizes[i] = size
eps[i] = wrapPreludeEndpoint(eps[i])
}
return n, nil
}
}
func wrapPreludeEndpoints(eps []Endpoint) {
for i, ep := range eps {
eps[i] = wrapPreludeEndpoint(ep)
}
}
func (b *ConcealBind) Close() error {
b.mu.Lock()
b.closeFallbackSessionsLocked()
b.mu.Unlock()
return b.inner.Close()
}
func (b *ConcealBind) SetMark(mark uint32) error {
return b.inner.SetMark(mark)
}
func (b *ConcealBind) preludeState(ep Endpoint) *conceal.PreludeState {
if ep == nil {
return nil
}
if preludeEP, ok := ep.(PreludeEndpoint); ok {
return preludeEP.PreludeState()
}
return nil
}
func (b *ConcealBind) resetPreludeState(ep Endpoint) {
if ep == nil {
return
}
if preludeEP, ok := ep.(PreludeEndpoint); ok {
preludeEP.ResetPreludeState()
}
}
func (b *ConcealBind) Send(bufs [][]byte, ep Endpoint) error {
if len(bufs) == 0 {
return nil
}
pipeline := b.currentPipeline()
if pipeline == nil || !pipeline.Active() {
return b.inner.Send(bufs, unwrapEndpoint(ep))
}
batchSize := b.inner.BatchSize()
if batchSize < 1 {
batchSize = 1
}
batch := make([][]byte, 0, batchSize)
retained := make([][]byte, 0, batchSize)
putRetained := func() {
for i, buf := range retained {
b.bufPool.Put(buf)
retained[i] = nil
}
retained = retained[:0]
}
clearBatch := func() {
for i := range batch {
batch[i] = nil
}
batch = batch[:0]
}
defer func() {
putRetained()
clearBatch()
}()
flush := func() error {
if len(batch) == 0 {
return nil
}
err := b.inner.Send(batch, unwrapEndpoint(ep))
putRetained()
clearBatch()
return err
}
appendPacket := func(packet []byte, retainedBuf []byte) error {
if len(batch) == batchSize {
if err := flush(); err != nil {
if retainedBuf != nil {
b.bufPool.Put(retainedBuf)
}
return err
}
}
batch = append(batch, packet)
if retainedBuf != nil {
retained = append(retained, retainedBuf)
}
if len(batch) == batchSize {
return flush()
}
return nil
}
preludeState := b.preludeState(ep)
if pipeline.PreludeActive() && preludeState != nil && preludeState.ClaimSend(time.Now(), pipeline.PreludeResendInterval()) {
if err := pipeline.EmitPrelude(func(packet []byte) error {
return appendPacket(bytes.Clone(packet), nil)
}); err != nil {
b.resetPreludeState(ep)
return err
}
}
for _, buf := range bufs {
encoded := b.bufPool.Get().([]byte)
n, err := pipeline.Encode(encoded, buf)
if err != nil {
b.bufPool.Put(encoded)
return err
}
if err := appendPacket(encoded[:n], encoded); err != nil {
return err
}
}
return flush()
}
func (b *ConcealBind) ParseEndpoint(s string) (Endpoint, error) {
ep, err := b.inner.ParseEndpoint(s)
if err != nil {
return nil, err
}
ep = wrapPreludeEndpoint(ep)
b.resetPreludeState(ep)
return ep, nil
}
func (b *ConcealBind) BatchSize() int {
return b.inner.BatchSize()
}
func (b *ConcealBind) SetFramedOpts(opts conceal.FramedOpts) {
b.mu.Lock()
defer b.mu.Unlock()
b.framedOpts = opts
b.rebuildPipelineLocked()
}
func (b *ConcealBind) SetPreludeOpts(opts conceal.PreludeOpts) {
b.mu.Lock()
defer b.mu.Unlock()
b.preludeOpts = opts
b.rebuildPipelineLocked()
}
func (b *ConcealBind) SetMasqueradeOpts(opts conceal.MasqueradeOpts) {
b.mu.Lock()
defer b.mu.Unlock()
b.masqueradeOpts = opts
b.rebuildPipelineLocked()
}
func (b *ConcealBind) SetFallbackPort(port uint16) {
b.mu.Lock()
if b.fallbackPort != port {
b.closeFallbackSessionsLocked()
}
b.fallbackPort = port
b.mu.Unlock()
if fallbackable, ok := b.inner.(Fallbackable); ok {
fallbackable.SetFallbackPort(port)
}
}
func (b *ConcealBind) forwardFallbackUDP(ep Endpoint, data []byte) error {
if ep == nil || len(data) == 0 {
return nil
}
port := b.currentFallbackPort()
if port == 0 {
return nil
}
session, err := b.fallbackSession(ep, port)
if err != nil {
return err
}
_, err = session.conn.Write(data)
return err
}
func (b *ConcealBind) currentFallbackPort() uint16 {
b.mu.Lock()
defer b.mu.Unlock()
return b.fallbackPort
}
func (b *ConcealBind) fallbackSession(ep Endpoint, port uint16) (*concealBindFallbackSession, error) {
key := ep.DstToString()
b.mu.Lock()
if session := b.fallbackSessions[key]; session != nil {
b.mu.Unlock()
return session, nil
}
b.mu.Unlock()
fallbackConn, err := net.DialUDP("udp", nil, fallbackUDPAddressForEndpoint(ep, port))
if err != nil {
return nil, err
}
session := &concealBindFallbackSession{
parent: b,
key: key,
ep: ep,
conn: fallbackConn,
}
b.mu.Lock()
if b.fallbackPort != port || b.fallbackPort == 0 {
b.mu.Unlock()
fallbackConn.Close()
return nil, net.ErrClosed
}
if b.fallbackSessions == nil {
b.fallbackSessions = make(map[string]*concealBindFallbackSession)
}
if existing := b.fallbackSessions[key]; existing != nil {
b.mu.Unlock()
fallbackConn.Close()
return existing, nil
}
b.fallbackSessions[key] = session
b.mu.Unlock()
go session.relay()
return session, nil
}
func (b *ConcealBind) removeFallbackSession(key string, session *concealBindFallbackSession) {
b.mu.Lock()
if b.fallbackSessions[key] == session {
delete(b.fallbackSessions, key)
}
b.mu.Unlock()
}
func (b *ConcealBind) closeFallbackSessionsLocked() {
for key, session := range b.fallbackSessions {
session.close()
delete(b.fallbackSessions, key)
}
}
type concealBindFallbackSession struct {
parent *ConcealBind
key string
ep Endpoint
conn *net.UDPConn
once sync.Once
}
func (s *concealBindFallbackSession) relay() {
defer s.parent.removeFallbackSession(s.key, s)
buf := make([]byte, 65535)
for {
n, err := s.conn.Read(buf)
if err != nil {
return
}
if err := s.parent.inner.Send([][]byte{buf[:n]}, unwrapEndpoint(s.ep)); err != nil {
return
}
}
}
func (s *concealBindFallbackSession) close() {
s.once.Do(func() {
s.conn.Close()
})
}
+23
View File
@@ -0,0 +1,23 @@
//go:build android
package conn
import "errors"
var _ PeekLookAtSocketFd = (*ConcealBind)(nil)
func (b *ConcealBind) PeekLookAtSocketFd4() (fd int, err error) {
peek, ok := b.inner.(PeekLookAtSocketFd)
if !ok {
return -1, errors.New("peek look at socket fd unsupported")
}
return peek.PeekLookAtSocketFd4()
}
func (b *ConcealBind) PeekLookAtSocketFd6() (fd int, err error) {
peek, ok := b.inner.(PeekLookAtSocketFd)
if !ok {
return -1, errors.New("peek look at socket fd unsupported")
}
return peek.PeekLookAtSocketFd6()
}
+813
View File
@@ -0,0 +1,813 @@
package conn
import (
"bytes"
"encoding/binary"
"net"
"net/netip"
"slices"
"testing"
"time"
"github.com/amnezia-vpn/amneziawg-go/conceal"
)
func TestConcealBindUDPPipelineOrder(t *testing.T) {
bind := NewConcealBind(&fakePacketBind{batchSize: 4})
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 TestConcealBindNoOpWithoutOpts(t *testing.T) {
inner := &fakePacketBind{
batchSize: 3,
recvBatches: []fakeRecvBatch{
{
packets: [][]byte{makeInitiationPacket(), makeTransportPacket()},
eps: []Endpoint{
&fakePacketEndpoint{addr: netip.MustParseAddrPort("127.0.0.1:51820")},
&fakePacketEndpoint{addr: netip.MustParseAddrPort("127.0.0.1:51820")},
},
},
},
}
bind := NewConcealBind(inner)
endpoint, err := bind.ParseEndpoint("127.0.0.1:51820")
if err != nil {
t.Fatalf("parse endpoint: %v", err)
}
initiation := makeInitiationPacket()
transport := makeTransportPacket()
if err := bind.Send([][]byte{initiation, transport}, endpoint); err != nil {
t.Fatalf("send: %v", err)
}
if len(inner.sendCalls) != 1 {
t.Fatalf("send calls = %d, want 1", len(inner.sendCalls))
}
if got := sendCallBatchLengths(inner.sendCalls); !slices.Equal(got, []int{2}) {
t.Fatalf("send batch lengths = %v, want [2]", got)
}
if got := inner.sendCalls[0].packets; !slices.EqualFunc(got, [][]byte{initiation, transport}, bytes.Equal) {
t.Fatalf("sent packets changed on no-op path")
}
if got := bind.BatchSize(); got != 3 {
t.Fatalf("batch size = %d, want 3", got)
}
fns, _, err := bind.Open(0)
if err != nil {
t.Fatalf("open: %v", err)
}
bufs := [][]byte{make([]byte, 256), make([]byte, 256), make([]byte, 256)}
sizes := make([]int, 3)
eps := make([]Endpoint, 3)
n, err := fns[0](bufs, sizes, eps)
if err != nil {
t.Fatalf("receive: %v", err)
}
if n != 2 {
t.Fatalf("received count = %d, want 2", n)
}
if !bytes.Equal(bufs[0][:sizes[0]], initiation) {
t.Fatalf("initiation changed on no-op path")
}
if !bytes.Equal(bufs[1][:sizes[1]], transport) {
t.Fatalf("transport changed on no-op path")
}
}
func TestConcealBindNoOpUnwrapsNonPreludeEndpoint(t *testing.T) {
inner := &rawPacketBind{
fakePacketBind: fakePacketBind{batchSize: 3},
}
bind := NewConcealBind(inner)
endpoint, err := bind.ParseEndpoint("127.0.0.1:51820")
if err != nil {
t.Fatalf("parse endpoint: %v", err)
}
if _, ok := endpoint.(PreludeEndpoint); !ok {
t.Fatalf("parsed endpoint does not carry prelude state")
}
transport := makeTransportPacket()
if err := bind.Send([][]byte{transport}, endpoint); err != nil {
t.Fatalf("send: %v", err)
}
if inner.badEndpoint {
t.Fatalf("inner bind received wrapped endpoint, want raw endpoint")
}
}
func TestConcealBindSendBatchesActivePipeline(t *testing.T) {
inner := &fakePacketBind{batchSize: 3}
bind := NewConcealBind(inner)
bind.SetFramedOpts(conceal.FramedOpts{
H1: mustHeader(t, "777"),
H2: mustHeader(t, "778"),
H4: mustHeader(t, "779"),
})
endpoint, err := bind.ParseEndpoint("127.0.0.1:51820")
if err != nil {
t.Fatalf("parse endpoint: %v", err)
}
initiation := makeInitiationPacket()
transport := makeTransportPacket()
response := makeResponsePacket()
if err := bind.Send([][]byte{initiation, transport, response}, endpoint); err != nil {
t.Fatalf("send: %v", err)
}
if got := sendCallBatchLengths(inner.sendCalls); !slices.Equal(got, []int{3}) {
t.Fatalf("send batch lengths = %v, want [3]", got)
}
wirePackets := flattenSendCalls(inner.sendCalls)
if len(wirePackets) != 3 {
t.Fatalf("wire packet count = %d, want 3", len(wirePackets))
}
if got := wireHeader(t, wirePackets[0]); got != 777 {
t.Fatalf("wire packet 0 header = %d, want 777", got)
}
if got := wireHeader(t, wirePackets[1]); got != 779 {
t.Fatalf("wire packet 1 header = %d, want 779", got)
}
if got := wireHeader(t, wirePackets[2]); got != 778 {
t.Fatalf("wire packet 2 header = %d, want 778", got)
}
}
func TestConcealBindSendBatchesPreludeInWireOrder(t *testing.T) {
inner := &fakePacketBind{batchSize: 3}
bind := NewConcealBind(inner)
bind.SetFramedOpts(conceal.FramedOpts{
H1: mustHeader(t, "777"),
H4: mustHeader(t, "779"),
})
bind.SetPreludeOpts(conceal.PreludeOpts{
Jc: 1,
Jmin: 3,
Jmax: 3,
RulesArr: [5]conceal.Rules{
mustParseRules(t, "<b 0xaabb>"),
},
})
endpoint, err := bind.ParseEndpoint("127.0.0.1:51820")
if err != nil {
t.Fatalf("parse endpoint: %v", err)
}
firstInitiation := makeInitiationPacket()
transport := makeTransportPacket()
secondInitiation := makeInitiationPacket()
if err := bind.Send([][]byte{firstInitiation, transport, secondInitiation}, endpoint); err != nil {
t.Fatalf("send: %v", err)
}
if got := sendCallBatchLengths(inner.sendCalls); !slices.Equal(got, []int{3, 2}) {
t.Fatalf("send batch lengths = %v, want [3 2]", got)
}
wirePackets := flattenSendCalls(inner.sendCalls)
if len(wirePackets) != 5 {
t.Fatalf("wire packet count = %d, want 5", len(wirePackets))
}
if !bytes.Equal(wirePackets[0], []byte{0xaa, 0xbb}) {
t.Fatalf("wire packet 0 prelude = %x, want aabb", wirePackets[0])
}
if len(wirePackets[1]) != 3 {
t.Fatalf("wire packet 1 junk len = %d, want 3", len(wirePackets[1]))
}
if got := wireHeader(t, wirePackets[2]); got != 777 {
t.Fatalf("wire packet 2 header = %d, want 777", got)
}
if got := wireHeader(t, wirePackets[3]); got != 779 {
t.Fatalf("wire packet 3 header = %d, want 779", got)
}
if got := wireHeader(t, wirePackets[4]); got != 777 {
t.Fatalf("wire packet 4 header = %d, want 777", got)
}
}
func TestConcealBindPreludeResendIntervalZeroDisablesForcedResend(t *testing.T) {
inner := &fakePacketBind{batchSize: 8}
bind := NewConcealBind(inner)
bind.SetFramedOpts(conceal.FramedOpts{H4: mustHeader(t, "779")})
bind.SetPreludeOpts(conceal.PreludeOpts{
ResendInterval: 0,
RulesArr: [5]conceal.Rules{
mustParseRules(t, "<b 0xaabb>"),
},
})
endpoint, err := bind.ParseEndpoint("127.0.0.1:51820")
if err != nil {
t.Fatalf("parse endpoint: %v", err)
}
transport := makeTransportPacket()
if err := bind.Send([][]byte{transport}, endpoint); err != nil {
t.Fatalf("first send: %v", err)
}
time.Sleep(time.Millisecond)
if err := bind.Send([][]byte{transport}, endpoint); err != nil {
t.Fatalf("second send: %v", err)
}
wirePackets := flattenSendCalls(inner.sendCalls)
if len(wirePackets) != 3 {
t.Fatalf("wire packet count = %d, want 3", len(wirePackets))
}
if !bytes.Equal(wirePackets[0], []byte{0xaa, 0xbb}) {
t.Fatalf("wire prelude = %x, want aabb", wirePackets[0])
}
if got := wireHeader(t, wirePackets[1]); got != 779 {
t.Fatalf("first transport header = %d, want 779", got)
}
if got := wireHeader(t, wirePackets[2]); got != 779 {
t.Fatalf("second transport header = %d, want 779", got)
}
}
func TestConcealBindPreludePositiveResendIntervalForcesResend(t *testing.T) {
inner := &fakePacketBind{batchSize: 8}
bind := NewConcealBind(inner)
bind.SetFramedOpts(conceal.FramedOpts{H4: mustHeader(t, "779")})
bind.SetPreludeOpts(conceal.PreludeOpts{
ResendInterval: time.Nanosecond,
RulesArr: [5]conceal.Rules{
mustParseRules(t, "<b 0xaabb>"),
},
})
endpoint, err := bind.ParseEndpoint("127.0.0.1:51820")
if err != nil {
t.Fatalf("parse endpoint: %v", err)
}
transport := makeTransportPacket()
if err := bind.Send([][]byte{transport}, endpoint); err != nil {
t.Fatalf("first send: %v", err)
}
time.Sleep(time.Millisecond)
if err := bind.Send([][]byte{transport}, endpoint); err != nil {
t.Fatalf("second send: %v", err)
}
wirePackets := flattenSendCalls(inner.sendCalls)
if len(wirePackets) != 4 {
t.Fatalf("wire packet count = %d, want 4", len(wirePackets))
}
if !bytes.Equal(wirePackets[0], []byte{0xaa, 0xbb}) {
t.Fatalf("first prelude = %x, want aabb", wirePackets[0])
}
if got := wireHeader(t, wirePackets[1]); got != 779 {
t.Fatalf("first transport header = %d, want 779", got)
}
if !bytes.Equal(wirePackets[2], []byte{0xaa, 0xbb}) {
t.Fatalf("second prelude = %x, want aabb", wirePackets[2])
}
if got := wireHeader(t, wirePackets[3]); got != 779 {
t.Fatalf("second transport header = %d, want 779", got)
}
}
func TestConcealBindWrapsNonPreludeEndpointAndUnwrapsForInnerBind(t *testing.T) {
inner := &rawPacketBind{
fakePacketBind: fakePacketBind{batchSize: 8},
}
bind := NewConcealBind(inner)
bind.SetFramedOpts(conceal.FramedOpts{H4: mustHeader(t, "779")})
bind.SetPreludeOpts(conceal.PreludeOpts{
ResendInterval: 0,
RulesArr: [5]conceal.Rules{
mustParseRules(t, "<b 0xaabb>"),
},
})
endpoint, err := bind.ParseEndpoint("127.0.0.1:51820")
if err != nil {
t.Fatalf("parse endpoint: %v", err)
}
if _, ok := endpoint.(PreludeEndpoint); !ok {
t.Fatalf("parsed endpoint does not carry prelude state")
}
transport := makeTransportPacket()
if err := bind.Send([][]byte{transport}, endpoint); err != nil {
t.Fatalf("first send: %v", err)
}
if err := bind.Send([][]byte{transport}, endpoint); err != nil {
t.Fatalf("second send: %v", err)
}
if inner.badEndpoint {
t.Fatalf("inner bind received wrapped endpoint, want raw endpoint")
}
wirePackets := flattenSendCalls(inner.sendCalls)
if len(wirePackets) != 3 {
t.Fatalf("wire packet count = %d, want 3", len(wirePackets))
}
if !bytes.Equal(wirePackets[0], []byte{0xaa, 0xbb}) {
t.Fatalf("wire prelude = %x, want aabb", wirePackets[0])
}
for i, call := range inner.sendCalls {
if _, ok := call.endpoint.(*rawPacketEndpoint); !ok {
t.Fatalf("send call %d endpoint = %T, want *rawPacketEndpoint", i, call.endpoint)
}
}
}
func TestConcealBindSendAndReceive(t *testing.T) {
senderInner := &fakePacketBind{batchSize: 4}
sender := newTestConcealBind(t, senderInner)
endpoint, err := sender.ParseEndpoint("127.0.0.1:51820")
if err != nil {
t.Fatalf("parse endpoint: %v", err)
}
initiation := makeInitiationPacket()
transport := makeTransportPacket()
if err := sender.Send([][]byte{initiation, transport}, endpoint); err != nil {
t.Fatalf("send: %v", err)
}
wirePackets := flattenSendCalls(senderInner.sendCalls)
if len(wirePackets) != 4 {
t.Fatalf("wire packet count = %d, want 4", len(wirePackets))
}
if !bytes.Equal(wirePackets[0], []byte{0xaa, 0xbb}) {
t.Fatalf("wire prelude decoy = %x, want aabb", wirePackets[0])
}
if len(wirePackets[1]) != 3 {
t.Fatalf("wire junk len = %d, want 3", len(wirePackets[1]))
}
receiverInner := &fakePacketBind{
batchSize: 4,
recvBatches: []fakeRecvBatch{
{
packets: wirePackets,
eps: []Endpoint{
endpoint,
endpoint,
endpoint,
endpoint,
},
},
},
}
receiver := newTestConcealBind(t, receiverInner)
fns, _, err := receiver.Open(0)
if err != nil {
t.Fatalf("open: %v", err)
}
bufs := [][]byte{
make([]byte, 256),
make([]byte, 256),
make([]byte, 256),
make([]byte, 256),
}
sizes := make([]int, 4)
eps := make([]Endpoint, 4)
n, err := fns[0](bufs, sizes, eps)
if err != nil {
t.Fatalf("receive: %v", err)
}
if n != 4 {
t.Fatalf("received count = %d, want 4", n)
}
if got := sizes[:n]; !slices.Equal(got, []int{0, 0, len(initiation), len(transport)}) {
t.Fatalf("sizes = %v, want [0 0 %d %d]", got, len(initiation), len(transport))
}
if eps[0] != nil || eps[1] != nil {
t.Fatalf("dropped endpoints = %v %v, want nil nil", eps[0], eps[1])
}
if !bytes.Equal(bufs[2][:sizes[2]], initiation) {
t.Fatalf("decoded initiation mismatch")
}
if !bytes.Equal(bufs[3][:sizes[3]], transport) {
t.Fatalf("decoded transport mismatch")
}
if got := eps[2].DstToString(); got != endpoint.DstToString() {
t.Fatalf("endpoint[2] = %q, want %q", got, endpoint.DstToString())
}
if got := eps[3].DstToString(); got != endpoint.DstToString() {
t.Fatalf("endpoint[3] = %q, want %q", got, endpoint.DstToString())
}
}
func TestConcealBindReceivePreservesIndicesForMixedBatch(t *testing.T) {
senderInner := &fakePacketBind{batchSize: 4}
sender := newTestConcealBind(t, senderInner)
endpoint, err := sender.ParseEndpoint("127.0.0.1:51820")
if err != nil {
t.Fatalf("parse endpoint: %v", err)
}
initiation := makeInitiationPacket()
if err := sender.Send([][]byte{initiation}, endpoint); err != nil {
t.Fatalf("send: %v", err)
}
wirePackets := flattenSendCalls(senderInner.sendCalls)
if len(wirePackets) != 3 {
t.Fatalf("wire packet count = %d, want 3", len(wirePackets))
}
receiverInner := &fakePacketBind{
batchSize: 4,
recvBatches: []fakeRecvBatch{
{
packets: [][]byte{wirePackets[0], wirePackets[2]},
eps: []Endpoint{
endpoint,
endpoint,
},
},
},
}
receiver := newTestConcealBind(t, receiverInner)
fns, _, err := receiver.Open(0)
if err != nil {
t.Fatalf("open: %v", err)
}
bufs := [][]byte{
make([]byte, 256),
make([]byte, 256),
}
sizes := make([]int, 2)
eps := make([]Endpoint, 2)
n, err := fns[0](bufs, sizes, eps)
if err != nil {
t.Fatalf("receive: %v", err)
}
if n != 2 {
t.Fatalf("received count = %d, want 2", n)
}
if got := sizes[:n]; !slices.Equal(got, []int{0, len(initiation)}) {
t.Fatalf("sizes = %v, want [0 %d]", got, len(initiation))
}
if eps[0] != nil {
t.Fatalf("endpoint[0] = %v, want nil", eps[0])
}
if !bytes.Equal(bufs[0][:len(wirePackets[0])], wirePackets[0]) {
t.Fatalf("slot 0 packet bytes changed unexpectedly")
}
if !bytes.Equal(bufs[1][:sizes[1]], initiation) {
t.Fatalf("decoded initiation was not left in slot 1")
}
if got := eps[1].DstToString(); got != endpoint.DstToString() {
t.Fatalf("endpoint[1] = %q, want %q", got, endpoint.DstToString())
}
}
func TestConcealBindReceiveInvalidOnlyBatchReturnsZeroSizesWithoutRetry(t *testing.T) {
senderInner := &fakePacketBind{batchSize: 4}
sender := newTestConcealBind(t, senderInner)
endpoint, err := sender.ParseEndpoint("127.0.0.1:51820")
if err != nil {
t.Fatalf("parse endpoint: %v", err)
}
initiation := makeInitiationPacket()
if err := sender.Send([][]byte{initiation}, endpoint); err != nil {
t.Fatalf("send: %v", err)
}
wirePackets := flattenSendCalls(senderInner.sendCalls)
if len(wirePackets) != 3 {
t.Fatalf("wire packet count = %d, want 3", len(wirePackets))
}
receiverInner := &fakePacketBind{
batchSize: 4,
recvBatches: []fakeRecvBatch{
{
packets: wirePackets[:2],
eps: []Endpoint{
endpoint,
endpoint,
},
},
{
packets: wirePackets[2:],
eps: []Endpoint{
endpoint,
},
},
},
}
receiver := newTestConcealBind(t, receiverInner)
fns, _, err := receiver.Open(0)
if err != nil {
t.Fatalf("open: %v", err)
}
bufs := [][]byte{
make([]byte, 256),
make([]byte, 256),
make([]byte, 256),
make([]byte, 256),
}
sizes := make([]int, 4)
eps := make([]Endpoint, 4)
n, err := fns[0](bufs, sizes, eps)
if err != nil {
t.Fatalf("receive: %v", err)
}
if n != 2 {
t.Fatalf("received count = %d, want 2", n)
}
if receiverInner.recvCalls != 1 {
t.Fatalf("inner receive calls = %d, want 1", receiverInner.recvCalls)
}
if got := sizes[:n]; !slices.Equal(got, []int{0, 0}) {
t.Fatalf("sizes = %v, want [0 0]", got)
}
if eps[0] != nil || eps[1] != nil {
t.Fatalf("dropped endpoints = %v %v, want nil nil", eps[0], eps[1])
}
n, err = fns[0](bufs, sizes, eps)
if err != nil {
t.Fatalf("second receive: %v", err)
}
if n != 1 {
t.Fatalf("second received count = %d, want 1", n)
}
if !bytes.Equal(bufs[0][:sizes[0]], initiation) {
t.Fatalf("decoded initiation mismatch on second receive")
}
if got := eps[0].DstToString(); got != endpoint.DstToString() {
t.Fatalf("endpoint[0] = %q, want %q", got, endpoint.DstToString())
}
}
func TestConcealBindMasqueradePassThroughWithoutRulesOut(t *testing.T) {
inner := &fakePacketBind{batchSize: 2}
bind := NewConcealBind(inner)
bind.SetMasqueradeOpts(conceal.MasqueradeOpts{
RulesIn: mustParseRules(t, "<dz be 2><d>"),
})
endpoint, err := bind.ParseEndpoint("127.0.0.1:51820")
if err != nil {
t.Fatalf("parse endpoint: %v", err)
}
transport := makeTransportPacket()
if err := bind.Send([][]byte{transport}, endpoint); err != nil {
t.Fatalf("send: %v", err)
}
wirePackets := flattenSendCalls(inner.sendCalls)
if len(wirePackets) != 1 {
t.Fatalf("wire packet count = %d, want 1", len(wirePackets))
}
if !bytes.Equal(wirePackets[0], transport) {
t.Fatalf("half-duplex masquerade changed outbound packet")
}
}
func newTestConcealBind(t *testing.T, inner *fakePacketBind) *ConcealBind {
t.Helper()
bind := NewConcealBind(inner)
rules := mustParseRules(t, "<dz be 2><d>")
bind.SetMasqueradeOpts(conceal.MasqueradeOpts{
RulesIn: rules,
RulesOut: rules,
})
bind.SetFramedOpts(conceal.FramedOpts{
H1: mustHeader(t, "777"),
H4: mustHeader(t, "779"),
})
bind.SetPreludeOpts(conceal.PreludeOpts{
Jc: 1,
Jmin: 3,
Jmax: 3,
RulesArr: [5]conceal.Rules{
mustParseRules(t, "<b 0xaabb>"),
},
})
return bind
}
type fakePacketBind struct {
batchSize int
recvBatches []fakeRecvBatch
sendCalls []fakeSendCall
recvCalls int
openCalls int
closeCalls int
}
type fakeRecvBatch struct {
packets [][]byte
eps []Endpoint
err error
}
type fakeSendCall struct {
packets [][]byte
endpoint Endpoint
}
func (b *fakePacketBind) Open(port uint16) ([]ReceiveFunc, uint16, error) {
b.openCalls++
idx := 0
return []ReceiveFunc{
func(packets [][]byte, sizes []int, eps []Endpoint) (int, error) {
b.recvCalls++
if idx >= len(b.recvBatches) {
return 0, net.ErrClosed
}
batch := b.recvBatches[idx]
idx++
for i, payload := range batch.packets {
copy(packets[i], payload)
sizes[i] = len(payload)
eps[i] = batch.eps[i]
}
return len(batch.packets), batch.err
},
}, port, nil
}
func (b *fakePacketBind) Close() error {
b.closeCalls++
return nil
}
func (b *fakePacketBind) SetMark(mark uint32) error {
return nil
}
func (b *fakePacketBind) Send(bufs [][]byte, ep Endpoint) error {
packets := make([][]byte, 0, len(bufs))
for _, buf := range bufs {
packets = append(packets, bytes.Clone(buf))
}
b.sendCalls = append(b.sendCalls, fakeSendCall{
packets: packets,
endpoint: ep,
})
return nil
}
func (b *fakePacketBind) ParseEndpoint(s string) (Endpoint, error) {
addr, err := netip.ParseAddrPort(s)
if err != nil {
return nil, err
}
return &fakePacketEndpoint{addr: addr}, nil
}
func (b *fakePacketBind) BatchSize() int {
return b.batchSize
}
type fakePacketEndpoint struct {
addr netip.AddrPort
prelude conceal.PreludeState
}
func (e *fakePacketEndpoint) ClearSrc() {
e.ResetPreludeState()
}
func (e *fakePacketEndpoint) PreludeState() *conceal.PreludeState {
return &e.prelude
}
func (e *fakePacketEndpoint) ResetPreludeState() {
e.prelude.Reset()
}
func (e *fakePacketEndpoint) SrcToString() string {
return ""
}
func (e *fakePacketEndpoint) DstToString() string {
return e.addr.String()
}
func (e *fakePacketEndpoint) DstToBytes() []byte {
out, _ := e.addr.MarshalBinary()
return out
}
func (e *fakePacketEndpoint) DstIP() netip.Addr {
return e.addr.Addr()
}
func (e *fakePacketEndpoint) SrcIP() netip.Addr {
return netip.Addr{}
}
type rawPacketBind struct {
fakePacketBind
badEndpoint bool
}
func (b *rawPacketBind) Send(bufs [][]byte, ep Endpoint) error {
if _, ok := ep.(*rawPacketEndpoint); !ok {
b.badEndpoint = true
}
return b.fakePacketBind.Send(bufs, ep)
}
func (b *rawPacketBind) ParseEndpoint(s string) (Endpoint, error) {
addr, err := netip.ParseAddrPort(s)
if err != nil {
return nil, err
}
return &rawPacketEndpoint{addr: addr}, nil
}
type rawPacketEndpoint struct {
addr netip.AddrPort
}
func (e *rawPacketEndpoint) ClearSrc() {}
func (e *rawPacketEndpoint) SrcToString() string {
return ""
}
func (e *rawPacketEndpoint) DstToString() string {
return e.addr.String()
}
func (e *rawPacketEndpoint) DstToBytes() []byte {
out, _ := e.addr.MarshalBinary()
return out
}
func (e *rawPacketEndpoint) DstIP() netip.Addr {
return e.addr.Addr()
}
func (e *rawPacketEndpoint) SrcIP() netip.Addr {
return netip.Addr{}
}
func flattenSendCalls(calls []fakeSendCall) [][]byte {
var out [][]byte
for _, call := range calls {
out = append(out, call.packets...)
}
return out
}
func sendCallBatchLengths(calls []fakeSendCall) []int {
lengths := make([]int, 0, len(calls))
for _, call := range calls {
lengths = append(lengths, len(call.packets))
}
return lengths
}
func wireHeader(t *testing.T, packet []byte) uint32 {
t.Helper()
if len(packet) < 4 {
t.Fatalf("wire packet len = %d, want at least 4", len(packet))
}
return binary.LittleEndian.Uint32(packet[:4])
}
var _ Bind = (*fakePacketBind)(nil)
var _ Bind = (*rawPacketBind)(nil)
var _ Endpoint = (*fakePacketEndpoint)(nil)
var _ Endpoint = (*rawPacketEndpoint)(nil)
var _ PreludeEndpoint = (*fakePacketEndpoint)(nil)
+21
View File
@@ -0,0 +1,21 @@
//go:build windows
package conn
var _ BindSocketToInterface = (*ConcealBind)(nil)
func (b *ConcealBind) BindSocketToInterface4(interfaceIndex uint32, blackhole bool) error {
bindable, ok := b.inner.(BindSocketToInterface)
if !ok {
return nil
}
return bindable.BindSocketToInterface4(interfaceIndex, blackhole)
}
func (b *ConcealBind) BindSocketToInterface6(interfaceIndex uint32, blackhole bool) error {
bindable, ok := b.inner.(BindSocketToInterface)
if !ok {
return nil
}
return bindable.BindSocketToInterface6(interfaceIndex, blackhole)
}
+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[:])
+38 -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,16 @@ 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
fallbackPort uint16
}
staticIdentity struct {
@@ -89,28 +95,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 +305,9 @@ 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.net.preludeOpts.ResendInterval = conceal.DefaultPreludeResendInterval
device.PopulatePools()
@@ -515,6 +498,30 @@ 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)
}
if fallbackable, ok := underlying.(conn.Fallbackable); ok {
fallbackable.SetFallbackPort(netc.fallbackPort)
}
recvFns, netc.port, err = netc.bind.Open(netc.port)
if err != nil {
netc.port = 0
+345 -1
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
@@ -129,6 +179,16 @@ func (pair *testPair) Send(
tb testing.TB,
ping SendDirection,
done chan struct{},
) {
tb.Helper()
pair.SendPacket(tb, ping, tuntest.Ping, done)
}
func (pair *testPair) SendPacket(
tb testing.TB,
ping SendDirection,
packet func(dst, src netip.Addr) []byte,
done chan struct{},
) {
tb.Helper()
p0, p1 := pair[0], pair[1]
@@ -137,7 +197,7 @@ func (pair *testPair) Send(
p0, p1 = p1, p0
}
msg := tuntest.Ping(p0.ip, p1.ip)
msg := packet(p0.ip, p1.ip)
p1.tun.Outbound <- msg
timer := time.NewTimer(6 * time.Second)
defer timer.Stop()
@@ -213,6 +273,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 +367,232 @@ 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)
})
}
func TestAWGDeviceUDPMasqueradeTransportsTUNTCPAndUDP(t *testing.T) {
goroutineLeakCheck(t)
pair := genTestPair(t, true,
"format_in", "<b 0xfeed><dz be 2><d>",
"format_out", "<b 0xfeed><dz be 2><d>",
)
for _, tc := range []struct {
name string
packet func(dst, src netip.Addr) []byte
}{
{name: "udp", packet: tuntest.UDP},
{name: "tcp", packet: tuntest.TCP},
} {
t.Run(tc.name+"_ping", func(t *testing.T) {
pair.SendPacket(t, Ping, tc.packet, nil)
})
t.Run(tc.name+"_pong", func(t *testing.T) {
pair.SendPacket(t, Pong, tc.packet, nil)
})
}
}
func TestAWGDeviceTCPMasqueradeTransportsTUNTCPAndUDP(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>",
)
for _, tc := range []struct {
name string
packet func(dst, src netip.Addr) []byte
}{
{name: "udp", packet: tuntest.UDP},
{name: "tcp", packet: tuntest.TCP},
} {
t.Run(tc.name+"_ping", func(t *testing.T) {
pair.SendPacket(t, Ping, tc.packet, nil)
})
t.Run(tc.name+"_pong", func(t *testing.T) {
pair.SendPacket(t, Pong, tc.packet, nil)
})
}
}
func TestAWGDeviceTCPFallbackWithTUNDevice(t *testing.T) {
fallbackListener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen fallback: %v", err)
}
defer fallbackListener.Close()
probe := []byte("GET / HTTP/1.1\r\n")
response := []byte("HTTP/1.1 200 OK\r\n\r\n")
received := make(chan []byte, 1)
served := make(chan error, 1)
go func() {
conn, err := fallbackListener.Accept()
if err != nil {
served <- err
return
}
defer conn.Close()
buf := make([]byte, len(probe))
if _, err := io.ReadFull(conn, buf); err != nil {
served <- err
return
}
received <- bytes.Clone(buf)
_, err = conn.Write(response)
served <- err
}()
device := newFallbackTestDevice(t,
"tcp",
getFreeTCPPort(t),
fallbackListener.Addr().(*net.TCPAddr).Port,
)
client, err := net.Dial("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(int(device.net.port))))
if err != nil {
t.Fatalf("dial device: %v", err)
}
defer client.Close()
if err := client.SetDeadline(time.Now().Add(2 * time.Second)); err != nil {
t.Fatalf("set client deadline: %v", err)
}
if _, err := client.Write(probe); err != nil {
t.Fatalf("write probe: %v", err)
}
got := make([]byte, len(response))
if _, err := io.ReadFull(client, got); err != nil {
t.Fatalf("read fallback response: %v", err)
}
if !bytes.Equal(got, response) {
t.Fatalf("fallback response = %q, want %q", got, response)
}
client.Close()
assertFallbackReceived(t, received, served, probe)
}
func TestAWGDeviceUDPFallbackWithTUNDevice(t *testing.T) {
fallbackConn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)})
if err != nil {
t.Fatalf("listen fallback udp: %v", err)
}
defer fallbackConn.Close()
probe := []byte("GE")
response := []byte("udp-ok")
received := make(chan []byte, 1)
served := make(chan error, 1)
go func() {
buf := make([]byte, 64)
n, addr, err := fallbackConn.ReadFromUDP(buf)
if err != nil {
served <- err
return
}
received <- bytes.Clone(buf[:n])
_, err = fallbackConn.WriteToUDP(response, addr)
served <- err
}()
device := newFallbackTestDevice(t, "udp", 0, fallbackConn.LocalAddr().(*net.UDPAddr).Port)
client, err := net.DialUDP("udp4", nil, &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: int(device.net.port)})
if err != nil {
t.Fatalf("dial device udp: %v", err)
}
defer client.Close()
if err := client.SetDeadline(time.Now().Add(2 * time.Second)); err != nil {
t.Fatalf("set udp client deadline: %v", err)
}
if _, err := client.Write(probe); err != nil {
t.Fatalf("write udp probe: %v", err)
}
got := make([]byte, len(response))
if _, err := io.ReadFull(client, got); err != nil {
t.Fatalf("read udp fallback response: %v", err)
}
if !bytes.Equal(got, response) {
t.Fatalf("udp fallback response = %q, want %q", got, response)
}
assertFallbackReceived(t, received, served, probe)
}
func newFallbackTestDevice(t *testing.T, network string, listenPort, fallbackPort int) *Device {
t.Helper()
var key NoisePrivateKey
if _, err := rand.Read(key[:]); err != nil {
t.Fatalf("generate private key: %v", err)
}
tunDevice := tuntest.NewChannelTUN()
device := NewDevice(tunDevice.TUN(), conn.NewDefaultBind(), NewLogger(LogLevelError, "fallback-test: "))
cfg := uapiCfg(
"private_key", hex.EncodeToString(key[:]),
"listen_port", strconv.Itoa(listenPort),
"network", network,
"format_in", "<b 0xfeed><dz be 2><d>",
"fallback_port", strconv.Itoa(fallbackPort),
)
if err := device.IpcSet(cfg); err != nil {
device.Close()
t.Fatalf("configure fallback device: %v", err)
}
if err := device.Up(); err != nil {
device.Close()
t.Fatalf("bring up fallback device: %v", err)
}
t.Cleanup(device.Close)
return device
}
func assertFallbackReceived(t *testing.T, received <-chan []byte, served <-chan error, want []byte) {
t.Helper()
select {
case got := <-received:
if !bytes.Equal(got, want) {
t.Fatalf("fallback received = %q, want %q", got, want)
}
case <-time.After(2 * time.Second):
t.Fatal("fallback service did not receive probe")
}
if err := <-served; err != nil {
t.Fatalf("fallback service failed: %v", err)
}
}
// 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)
+266 -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,76 @@ 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)
sendf("prelude_resend_interval=%d", int64(device.net.preludeOpts.ResendInterval/time.Second))
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())
}
if device.net.fallbackPort != 0 {
sendf("fallback_port=%d", device.net.fallbackPort)
}
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 +218,6 @@ func (device *Device) IpcSetOperation(r io.Reader) (err error) {
}
}()
ipcDev := new(ipcSetDevice)
peer := new(ipcSetPeer)
deviceConfig := true
@@ -206,10 +226,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 +261,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 +328,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 +342,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 +358,26 @@ 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 "prelude_resend_interval":
secs, err := strconv.ParseUint(value, 10, 32)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse prelude_resend_interval: %w", err)
}
device.log.Verbosef("UAPI: Updating prelude resend interval")
device.net.preludeOpts.ResendInterval = time.Duration(secs) * time.Second
if err := device.BindUpdate(); err != nil {
return ipcErrorf(ipc.IpcErrorPortInUse, "failed to set prelude resend interval: %w", err)
}
case "s1":
padding, err := strconv.Atoi(value)
@@ -348,8 +387,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 +403,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 +419,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 +435,219 @@ 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)
}
case "fallback_port":
port, err := strconv.ParseUint(value, 10, 16)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse fallback_port: %w", err)
}
if port == 0 {
return ipcErrorf(ipc.IpcErrorInvalid, "fallback_port must be in range 1-65535")
}
device.log.Verbosef("UAPI: Updating fallback_port")
device.net.fallbackPort = uint16(port)
if err := device.BindUpdate(); err != nil {
return ipcErrorf(ipc.IpcErrorPortInUse, "failed to set fallback_port: %w", err)
}
device.ipackets[4] = chain
default:
return ipcErrorf(ipc.IpcErrorInvalid, "invalid UAPI device key: %v", key)
@@ -695,49 +897,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
}
+68
View File
@@ -0,0 +1,68 @@
package device
import (
"bytes"
"strings"
"testing"
"github.com/amnezia-vpn/amneziawg-go/conn/bindtest"
"github.com/amnezia-vpn/amneziawg-go/tun/tuntest"
)
func TestDeviceFallbackPortUAPI(t *testing.T) {
tunDevice := tuntest.NewChannelTUN()
binds := bindtest.NewChannelBinds()
device := NewDevice(tunDevice.TUN(), binds[0], NewLogger(LogLevelError, ""))
defer device.Close()
if err := device.IpcSet(uapiCfg("fallback_port", "8082")); err != nil {
t.Fatalf("set fallback_port: %v", err)
}
var got bytes.Buffer
if err := device.IpcGetOperation(&got); err != nil {
t.Fatalf("get uapi: %v", err)
}
if !strings.Contains(got.String(), "fallback_port=8082\n") {
t.Fatalf("IpcGetOperation output missing fallback_port: %q", got.String())
}
for _, value := range []string{"0", "65536"} {
if err := device.IpcSet(uapiCfg("fallback_port", value)); err == nil {
t.Fatalf("fallback_port=%s accepted, want error", value)
}
}
}
func TestDevicePreludeResendIntervalUAPI(t *testing.T) {
tunDevice := tuntest.NewChannelTUN()
binds := bindtest.NewChannelBinds()
device := NewDevice(tunDevice.TUN(), binds[0], NewLogger(LogLevelError, ""))
defer device.Close()
var got bytes.Buffer
if err := device.IpcGetOperation(&got); err != nil {
t.Fatalf("get default uapi: %v", err)
}
if !strings.Contains(got.String(), "prelude_resend_interval=120\n") {
t.Fatalf("IpcGetOperation output missing default prelude_resend_interval: %q", got.String())
}
if err := device.IpcSet(uapiCfg("prelude_resend_interval", "0")); err != nil {
t.Fatalf("set prelude_resend_interval=0: %v", err)
}
got.Reset()
if err := device.IpcGetOperation(&got); err != nil {
t.Fatalf("get updated uapi: %v", err)
}
if !strings.Contains(got.String(), "prelude_resend_interval=0\n") {
t.Fatalf("IpcGetOperation output missing disabled prelude_resend_interval: %q", got.String())
}
for _, value := range []string{"-1", "abc"} {
if err := device.IpcSet(uapiCfg("prelude_resend_interval", value)); err == nil {
t.Fatalf("prelude_resend_interval=%s accepted, want error", value)
}
}
}
+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=
+13 -1
View File
@@ -6,6 +6,7 @@
package ipc
import (
"errors"
"net"
"github.com/amnezia-vpn/amneziawg-go/ipc/namedpipe"
@@ -50,6 +51,7 @@ func (l *UAPIListener) Addr() net.Addr {
}
var UAPISecurityDescriptor *windows.SECURITY_DESCRIPTOR
var UAPIFallbackSecurityDescriptor *windows.SECURITY_DESCRIPTOR
func init() {
var err error
@@ -57,12 +59,22 @@ func init() {
if err != nil {
panic(err)
}
UAPIFallbackSecurityDescriptor, err = windows.SecurityDescriptorFromString("D:P(A;;GA;;;SY)(A;;GA;;;BA)S:(ML;;NWNRNX;;;HI)")
if err != nil {
panic(err)
}
}
func UAPIListen(name string) (net.Listener, error) {
path := `\\.\pipe\ProtectedPrefix\Administrators\AmneziaWG\` + name
listener, err := (&namedpipe.ListenConfig{
SecurityDescriptor: UAPISecurityDescriptor,
}).Listen(`\\.\pipe\ProtectedPrefix\Administrators\AmneziaWG\` + name)
}).Listen(path)
if errors.Is(err, windows.ERROR_INVALID_OWNER) {
listener, err = (&namedpipe.ListenConfig{
SecurityDescriptor: UAPIFallbackSecurityDescriptor,
}).Listen(path)
}
if err != nil {
return nil, err
}
+31 -21
View File
@@ -16,27 +16,29 @@ import (
)
type DeviceConfig struct {
PrivateKey string `yaml:"private_key"`
Address []string `yaml:"address"`
Dns []string `yaml:"dns"`
Mtu int `yaml:"mtu,omitempty"`
Jc int `yaml:"jc,omitempty"`
Jmin int `yaml:"jmin,omitempty"`
Jmax int `yaml:"jmax,omitempty"`
S1 int `yaml:"s1,omitempty"`
S2 int `yaml:"s2,omitempty"`
S3 int `yaml:"s3,omitempty"`
S4 int `yaml:"s4,omitempty"`
H1 string `yaml:"h1,omitempty"`
H2 string `yaml:"h2,omitempty"`
H3 string `yaml:"h3,omitempty"`
H4 string `yaml:"h4,omitempty"`
I1 string `yaml:"i1,omitempty"`
I2 string `yaml:"i2,omitempty"`
I3 string `yaml:"i3,omitempty"`
I4 string `yaml:"i4,omitempty"`
I5 string `yaml:"i5,omitempty"`
Peers []PeerConfig `yaml:"peers,omitempty"`
PrivateKey string `yaml:"private_key"`
Address []string `yaml:"address"`
Dns []string `yaml:"dns"`
Mtu int `yaml:"mtu,omitempty"`
Jc int `yaml:"jc,omitempty"`
Jmin int `yaml:"jmin,omitempty"`
Jmax int `yaml:"jmax,omitempty"`
PreludeResendInterval *int `yaml:"prelude_resend_interval,omitempty"`
S1 int `yaml:"s1,omitempty"`
S2 int `yaml:"s2,omitempty"`
S3 int `yaml:"s3,omitempty"`
S4 int `yaml:"s4,omitempty"`
H1 string `yaml:"h1,omitempty"`
H2 string `yaml:"h2,omitempty"`
H3 string `yaml:"h3,omitempty"`
H4 string `yaml:"h4,omitempty"`
I1 string `yaml:"i1,omitempty"`
I2 string `yaml:"i2,omitempty"`
I3 string `yaml:"i3,omitempty"`
I4 string `yaml:"i4,omitempty"`
I5 string `yaml:"i5,omitempty"`
FallbackPort int `yaml:"fallback_port,omitempty"`
Peers []PeerConfig `yaml:"peers,omitempty"`
}
type PeerConfig struct {
@@ -84,6 +86,10 @@ func genIpcString(cfg *DeviceConfig) (string, error) {
b.WriteString("\njmax=")
b.WriteString(strconv.Itoa(cfg.Jmax))
}
if cfg.PreludeResendInterval != nil {
b.WriteString("\nprelude_resend_interval=")
b.WriteString(strconv.Itoa(*cfg.PreludeResendInterval))
}
if cfg.S1 != 0 {
b.WriteString("\ns1=")
b.WriteString(strconv.Itoa(cfg.S1))
@@ -136,6 +142,10 @@ func genIpcString(cfg *DeviceConfig) (string, error) {
b.WriteString("\ni5=")
b.WriteString(cfg.I5)
}
if cfg.FallbackPort != 0 {
b.WriteString("\nfallback_port=")
b.WriteString(strconv.Itoa(cfg.FallbackPort))
}
for _, peer := range cfg.Peers {
publicKeyBytes, err := base64.StdEncoding.DecodeString(peer.PublicKey)
+62
View File
@@ -0,0 +1,62 @@
package main
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"flag"
"log"
"os"
"golang.org/x/crypto/curve25519"
)
type keypairOutput struct {
Private string `json:"private"`
Public string `json:"public"`
}
type pskOutput struct {
PSK string `json:"psk"`
}
func main() {
mode := flag.String("mode", "keypair", "keypair or psk")
flag.Parse()
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
switch *mode {
case "keypair":
private := make([]byte, 32)
if _, err := rand.Read(private); err != nil {
log.Fatalf("read private key: %v", err)
}
private[0] &= 248
private[31] &= 127
private[31] |= 64
public, err := curve25519.X25519(private, curve25519.Basepoint)
if err != nil {
log.Fatalf("derive public key: %v", err)
}
if err := enc.Encode(keypairOutput{
Private: hex.EncodeToString(private),
Public: hex.EncodeToString(public),
}); err != nil {
log.Fatalf("encode keypair: %v", err)
}
case "psk":
psk := make([]byte, 32)
if _, err := rand.Read(psk); err != nil {
log.Fatalf("read psk: %v", err)
}
if err := enc.Encode(pskOutput{PSK: hex.EncodeToString(psk)}); err != nil {
log.Fatalf("encode psk: %v", err)
}
default:
log.Fatalf("unsupported mode %q", *mode)
}
}
+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}"
+645
View File
@@ -0,0 +1,645 @@
<#
Single-VM Windows conceal smoke harness.
What it validates:
- two local Windows peers can start and handshake with UDP or TCP outer transport
- configured conceal markers appear in outer capture
- UAPI reports the requested network mode and non-zero handshake/traffic counters
What it does not validate:
- multi-host routing, NAT, roaming, or full netns-style isolation semantics
#>
[CmdletBinding()]
param(
[ValidateSet("udp", "tcp", "both")]
[string]$Mode = "both",
[string]$ProgramPath = ".\amneziawg-go.exe",
[string]$GoCommand = "go",
[string]$OutDir = ".\_artifacts\windows-single-vm-conceal-smoke",
[int]$HandshakeTimeoutSec = 25
)
$ErrorActionPreference = "Stop"
Set-StrictMode -Version Latest
$RepoRoot = Split-Path -Parent $PSScriptRoot
$KeygenPath = ".\tests\cmd\awg-keygen"
if ([System.IO.Path]::IsPathRooted($ProgramPath)) {
$ProgramPath = [System.IO.Path]::GetFullPath($ProgramPath)
}
else {
$ProgramPath = [System.IO.Path]::GetFullPath((Join-Path $RepoRoot $ProgramPath))
}
if ([System.IO.Path]::IsPathRooted($OutDir)) {
$OutDir = [System.IO.Path]::GetFullPath($OutDir)
}
else {
$OutDir = [System.IO.Path]::GetFullPath((Join-Path $RepoRoot $OutDir))
}
function Resolve-WintunDllPath {
$candidates = @(
(Join-Path (Split-Path -Parent $ProgramPath) "wintun.dll"),
(Join-Path $env:WINDIR "System32\wintun.dll")
)
foreach ($candidate in $candidates) {
if ($candidate -and (Test-Path $candidate)) {
return [System.IO.Path]::GetFullPath($candidate)
}
}
throw @"
Required dependency wintun.dll was not found.
Expected one of:
- $(Join-Path (Split-Path -Parent $ProgramPath) "wintun.dll")
- $(Join-Path $env:WINDIR "System32\wintun.dll")
Place the architecture-matching Wintun DLL next to $ProgramPath and rerun the script.
"@
}
function Assert-Administrator {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal($identity)
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw "This script must be run from an elevated PowerShell session."
}
}
function New-Directory([string]$Path) {
New-Item -ItemType Directory -Force -Path $Path | Out-Null
}
function Write-Phase([string]$Message) {
Write-Host "[$(Get-Date -Format 'HH:mm:ss')] $Message"
}
function Reset-Directory([string]$Path) {
if (Test-Path $Path) {
Remove-Item -Path $Path -Recurse -Force
}
New-Directory $Path
}
function Invoke-JsonCommand([string]$FilePath, [string[]]$Arguments) {
Push-Location $RepoRoot
try {
$output = & $FilePath @Arguments
if ($LASTEXITCODE -ne 0) {
throw "Command failed: $FilePath $($Arguments -join ' ')"
}
return $output | ConvertFrom-Json
}
finally {
Pop-Location
}
}
function New-Keypair {
return Invoke-JsonCommand $GoCommand @("run", $KeygenPath, "-mode", "keypair")
}
function New-Psk {
return Invoke-JsonCommand $GoCommand @("run", $KeygenPath, "-mode", "psk")
}
function Open-UapiPipe([string]$InterfaceName, [int]$TimeoutMs) {
$pipeName = "ProtectedPrefix\Administrators\AmneziaWG\$InterfaceName"
$client = New-Object System.IO.Pipes.NamedPipeClientStream(
".",
$pipeName,
[System.IO.Pipes.PipeDirection]::InOut,
[System.IO.Pipes.PipeOptions]::None,
[System.Security.Principal.TokenImpersonationLevel]::Impersonation
)
$client.Connect($TimeoutMs)
return $client
}
function Read-UapiResponse([System.IO.StreamReader]$Reader) {
$lines = New-Object System.Collections.Generic.List[string]
$sawErrno = $false
while ($true) {
$line = $Reader.ReadLine()
if ($null -eq $line) {
break
}
$lines.Add($line)
if ($line -match '^errno=') {
$sawErrno = $true
continue
}
if ($sawErrno -and $line -eq "") {
break
}
}
if (-not $sawErrno) {
throw "Incomplete UAPI response: missing errno line."
}
return $lines
}
function Invoke-UapiOperation([string]$InterfaceName, [string]$Operation, [string]$Body) {
$pipe = Open-UapiPipe -InterfaceName $InterfaceName -TimeoutMs 5000
try {
$writer = New-Object System.IO.StreamWriter($pipe, [System.Text.Encoding]::ASCII, 1024, $true)
$writer.NewLine = "`n"
$writer.AutoFlush = $true
$reader = New-Object System.IO.StreamReader($pipe, [System.Text.Encoding]::ASCII, $false, 1024, $true)
switch ($Operation) {
"set" {
if (-not $Body.EndsWith("`n")) {
$Body += "`n"
}
$writer.Write("set=1`n")
$writer.Write($Body)
$writer.Write("`n")
}
"get" {
$writer.Write("get=1`n`n")
}
default {
throw "Unsupported UAPI operation '$Operation'."
}
}
$lines = Read-UapiResponse -Reader $reader
$errnoLine = $lines | Where-Object { $_ -match '^errno=' } | Select-Object -Last 1
$errno = [int]($errnoLine -replace '^errno=', '')
if ($errno -ne 0) {
throw "UAPI $Operation failed for $InterfaceName with errno=$errno"
}
return (($lines | Where-Object { $_ -notmatch '^errno=' -and $_ -ne "" }) -join "`n")
}
finally {
$pipe.Dispose()
}
}
function Get-LogTail([string]$Path, [int]$MaxLines = 40) {
if (-not (Test-Path $Path)) {
return ""
}
return ((Get-Content -Path $Path -Tail $MaxLines -ErrorAction SilentlyContinue) -join "`n")
}
function Get-LogText([string]$Path) {
if (-not (Test-Path $Path)) {
return ""
}
return (Get-Content -Path $Path -Raw -ErrorAction SilentlyContinue)
}
function Wait-UapiReady([pscustomobject]$ProcessInfo, [string]$InterfaceName, [int]$TimeoutSec) {
Write-Phase "Waiting for UAPI pipe for $InterfaceName"
$deadline = (Get-Date).AddSeconds($TimeoutSec)
while ((Get-Date) -lt $deadline) {
if ($ProcessInfo.Process.HasExited) {
$stderrTail = Get-LogTail -Path $ProcessInfo.StderrPath
$stdoutTail = Get-LogTail -Path $ProcessInfo.StdoutPath
throw @"
Process for $InterfaceName exited before the UAPI pipe was ready.
ExitCode: $($ProcessInfo.Process.ExitCode)
stderr:
$stderrTail
stdout:
$stdoutTail
"@
}
try {
$pipe = Open-UapiPipe -InterfaceName $InterfaceName -TimeoutMs 500
$pipe.Dispose()
Write-Phase "UAPI pipe ready for $InterfaceName"
return
}
catch {
Start-Sleep -Milliseconds 200
}
}
throw "Timed out waiting for UAPI pipe for $InterfaceName"
}
function Convert-LinesToMap([string]$Text) {
$map = @{}
foreach ($line in ($Text -split "`r?`n")) {
if ($line -notmatch '^(?<k>[^=]+)=(?<v>.*)$') {
continue
}
$key = $matches.k
$value = $matches.v
if ($map.ContainsKey($key)) {
if ($map[$key] -is [System.Collections.IList]) {
$map[$key].Add($value) | Out-Null
}
else {
$list = New-Object System.Collections.ArrayList
$list.Add($map[$key]) | Out-Null
$list.Add($value) | Out-Null
$map[$key] = $list
}
}
else {
$map[$key] = $value
}
}
return $map
}
function Wait-Handshake([string]$InterfaceName, [int]$TimeoutSec) {
Write-Phase "Waiting for handshake on $InterfaceName"
$deadline = (Get-Date).AddSeconds($TimeoutSec)
$lastReport = [DateTime]::MinValue
while ((Get-Date) -lt $deadline) {
$state = Convert-LinesToMap (Invoke-UapiOperation -InterfaceName $InterfaceName -Operation "get" -Body "")
$hs = 0
$tx = 0
$rx = 0
$network = ""
if ($state.ContainsKey("last_handshake_time_sec")) {
$hs = [int64]$state["last_handshake_time_sec"]
}
if ($state.ContainsKey("tx_bytes")) {
$tx = [int64]$state["tx_bytes"]
}
if ($state.ContainsKey("rx_bytes")) {
$rx = [int64]$state["rx_bytes"]
}
if ($state.ContainsKey("network")) {
$network = $state["network"]
}
if ($hs -gt 0 -and $tx -gt 0 -and $rx -gt 0) {
Write-Phase "Handshake ready on $InterfaceName (network=$network, hs=$hs, tx=$tx, rx=$rx)"
return $state
}
$now = Get-Date
if (($now - $lastReport).TotalSeconds -ge 2) {
Write-Phase "Still waiting on $InterfaceName (network=$network, hs=$hs, tx=$tx, rx=$rx)"
$lastReport = $now
}
Start-Sleep -Milliseconds 500
}
throw "Timed out waiting for handshake on $InterfaceName"
}
function Get-LittleEndianUint32Bytes([uint32]$Value) {
return [System.BitConverter]::GetBytes($Value)
}
function Convert-HexToBytes([string]$Hex) {
$clean = $Hex.ToLowerInvariant()
if ($clean.StartsWith("0x")) {
$clean = $clean.Substring(2)
}
if (($clean.Length % 2) -ne 0) {
throw "Hex string must have even length: $Hex"
}
$bytes = New-Object byte[] ($clean.Length / 2)
for ($i = 0; $i -lt $bytes.Length; $i++) {
$bytes[$i] = [Convert]::ToByte($clean.Substring($i * 2, 2), 16)
}
return $bytes
}
function Test-ByteSequenceInArray([byte[]]$Haystack, [byte[]]$Needle) {
if ($Needle.Length -eq 0 -or $Haystack.Length -lt $Needle.Length) {
return $false
}
for ($i = 0; $i -le ($Haystack.Length - $Needle.Length); $i++) {
$matched = $true
for ($j = 0; $j -lt $Needle.Length; $j++) {
if ($Haystack[$i + $j] -ne $Needle[$j]) {
$matched = $false
break
}
}
if ($matched) {
return $true
}
}
return $false
}
function Assert-CaptureContains([byte[]]$CaptureBytes, [string]$Label, [byte[]]$Needle) {
if (-not (Test-ByteSequenceInArray -Haystack $CaptureBytes -Needle $Needle)) {
$hex = ([System.BitConverter]::ToString($Needle)).Replace("-", "").ToLowerInvariant()
throw "Capture does not contain expected $Label marker: $hex"
}
}
function Test-CaptureContains([byte[]]$CaptureBytes, [byte[]]$Needle) {
return Test-ByteSequenceInArray -Haystack $CaptureBytes -Needle $Needle
}
function Assert-StateValue([hashtable]$State, [string]$Key, [string]$ExpectedValue, [string]$InterfaceName) {
if (-not $State.ContainsKey($Key)) {
throw "UAPI state for $InterfaceName does not include key '$Key'"
}
if ([string]$State[$Key] -ne $ExpectedValue) {
throw "UAPI state for $InterfaceName has $Key=$($State[$Key]), expected $ExpectedValue"
}
}
function Invoke-Pktmon([string[]]$Arguments) {
& pktmon.exe @Arguments | Out-Null
if ($LASTEXITCODE -ne 0) {
throw "pktmon failed: pktmon.exe $($Arguments -join ' ')"
}
}
function Start-Capture([string]$EtlPath, [int]$PortA, [int]$PortB, [string]$TransportProtocol) {
Write-Phase "Starting pktmon capture for $TransportProtocol on ports $PortA/$PortB"
try { & pktmon.exe stop | Out-Null } catch {}
try { & pktmon.exe filter remove | Out-Null } catch {}
Invoke-Pktmon @("filter", "add", "conceal-smoke", "-t", $TransportProtocol.ToUpperInvariant(), "-p", "$PortA", "$PortB")
Invoke-Pktmon @("start", "--capture", "--pkt-size", "256", "--comp", "all", "--file-name", $EtlPath)
}
function Stop-Capture([string]$EtlPath, [string]$PcapPath) {
Write-Phase "Stopping pktmon capture"
try {
Invoke-Pktmon @("stop")
}
finally {
try { & pktmon.exe filter remove | Out-Null } catch {}
}
Write-Phase "Converting ETL to PCAPNG"
Invoke-Pktmon @("etl2pcap", $EtlPath, "--out", $PcapPath)
}
function Start-AwgProcess([string]$InterfaceName, [string]$ModeOutDir) {
$stdout = Join-Path $ModeOutDir "$InterfaceName.stdout.log"
$stderr = Join-Path $ModeOutDir "$InterfaceName.stderr.log"
$process = Start-Process -FilePath $ProgramPath -ArgumentList @($InterfaceName) -WorkingDirectory $RepoRoot -PassThru -WindowStyle Hidden -RedirectStandardOutput $stdout -RedirectStandardError $stderr
return [PSCustomObject]@{
InterfaceName = $InterfaceName
Process = $process
StdoutPath = $stdout
StderrPath = $stderr
}
}
function Stop-AwgProcess([pscustomobject]$ProcessInfo) {
if ($null -eq $ProcessInfo) {
return
}
try {
if (-not $ProcessInfo.Process.HasExited) {
Stop-Process -Id $ProcessInfo.Process.Id -Force
}
}
catch {}
}
function New-ModeProfile([string]$SelectedMode) {
if ($SelectedMode -eq "udp") {
return @{
Mode = "udp"
PortA = 31000
PortB = 32000
FormatHex = "feedfacedeadbeef"
I1Hex = "aabbccddeeff0011"
I2Hex = "2233445566778899"
H1 = [uint32]305419896
H2 = [uint32]2596069104
H4 = [uint32]267242409
FormatSpec = "<b 0xfeedfacedeadbeef><dz be 2><d>"
I1Spec = "<b 0xaabbccddeeff0011>"
I2Spec = "<b 0x2233445566778899>"
DeviceArgs = @(
"network", "udp",
"header_compat", "true",
"format_in", "<b 0xfeedfacedeadbeef><dz be 2><d>",
"format_out", "<b 0xfeedfacedeadbeef><dz be 2><d>",
"i1", "<b 0xaabbccddeeff0011>",
"i2", "<b 0x2233445566778899>",
"jc", "1",
"jmin", "8",
"jmax", "8",
"s1", "15",
"s2", "18",
"s4", "25",
"h1", "305419896",
"h2", "2596069104",
"h4", "267242409"
)
}
}
return @{
Mode = "tcp"
PortA = 41000
PortB = 42000
FormatHex = "beefcafebad0f00d"
I1Hex = "1122334455667788"
I2Hex = "99aabbccddeeff00"
H1 = [uint32]286331153
H2 = [uint32]572662306
H4 = [uint32]1145324612
FormatSpec = "<b 0xbeefcafebad0f00d><dz be 2><d>"
I1Spec = "<b 0x1122334455667788>"
I2Spec = "<b 0x99aabbccddeeff00>"
DeviceArgs = @(
"network", "tcp",
"header_compat", "true",
"format_in", "<b 0xbeefcafebad0f00d><dz be 2><d>",
"format_out", "<b 0xbeefcafebad0f00d><dz be 2><d>",
"i1", "<b 0x1122334455667788>",
"i2", "<b 0x99aabbccddeeff00>",
"s1", "15",
"s2", "18",
"s4", "25",
"h1", "286331153",
"h2", "572662306",
"h4", "1145324612"
)
}
}
function New-UapiConfig([string[]]$Pairs) {
if (($Pairs.Length % 2) -ne 0) {
throw "Expected an even number of key/value entries."
}
$builder = New-Object System.Text.StringBuilder
for ($i = 0; $i -lt $Pairs.Length; $i += 2) {
[void]$builder.Append($Pairs[$i]).Append('=').Append($Pairs[$i + 1]).Append("`n")
}
return $builder.ToString()
}
function Invoke-ConcealSmoke([string]$SelectedMode) {
Write-Phase "Starting conceal smoke for mode=$SelectedMode"
$profile = New-ModeProfile -SelectedMode $SelectedMode
$modeOutDir = Join-Path $OutDir $SelectedMode
Reset-Directory $modeOutDir
$ifaceA = "awg-smoke-$SelectedMode-a"
$ifaceB = "awg-smoke-$SelectedMode-b"
$captureEtl = Join-Path $modeOutDir "$SelectedMode.etl"
$capturePcap = Join-Path $modeOutDir "$SelectedMode.pcapng"
$procA = $null
$procB = $null
try {
Write-Phase "Generating key material for mode=$SelectedMode"
$keyA = New-Keypair
$keyB = New-Keypair
$psk = New-Psk
Write-Phase "Starting local peers $ifaceA and $ifaceB"
$procA = Start-AwgProcess -InterfaceName $ifaceA -ModeOutDir $modeOutDir
$procB = Start-AwgProcess -InterfaceName $ifaceB -ModeOutDir $modeOutDir
Wait-UapiReady -ProcessInfo $procA -InterfaceName $ifaceA -TimeoutSec 20
Wait-UapiReady -ProcessInfo $procB -InterfaceName $ifaceB -TimeoutSec 20
$deviceConfigA = @(
"private_key", $keyA.private,
"listen_port", "$($profile.PortA)",
"replace_peers", "true"
) + $profile.DeviceArgs
$peerConfigA = @(
"public_key", $keyB.public,
"preshared_key", $psk.psk,
"protocol_version", "1",
"replace_allowed_ips", "true",
"allowed_ip", "10.99.0.2/32",
"endpoint", "127.0.0.1:$($profile.PortB)"
)
$deviceConfigB = @(
"private_key", $keyB.private,
"listen_port", "$($profile.PortB)",
"replace_peers", "true"
) + $profile.DeviceArgs
$peerConfigB = @(
"public_key", $keyA.public,
"preshared_key", $psk.psk,
"protocol_version", "1",
"replace_allowed_ips", "true",
"allowed_ip", "10.99.0.1/32",
"endpoint", "127.0.0.1:$($profile.PortA)"
)
Write-Phase "Applying device config for $ifaceA"
Invoke-UapiOperation -InterfaceName $ifaceA -Operation "set" -Body (New-UapiConfig -Pairs $deviceConfigA) | Out-Null
Write-Phase "Applying device config for $ifaceB"
Invoke-UapiOperation -InterfaceName $ifaceB -Operation "set" -Body (New-UapiConfig -Pairs $deviceConfigB) | Out-Null
Write-Phase "Applying peer config for $ifaceA"
Invoke-UapiOperation -InterfaceName $ifaceA -Operation "set" -Body (New-UapiConfig -Pairs $peerConfigA) | Out-Null
Write-Phase "Applying peer config for $ifaceB"
Invoke-UapiOperation -InterfaceName $ifaceB -Operation "set" -Body (New-UapiConfig -Pairs $peerConfigB) | Out-Null
Start-Capture -EtlPath $captureEtl -PortA $profile.PortA -PortB $profile.PortB -TransportProtocol $SelectedMode
try {
$kickA = New-UapiConfig -Pairs @("public_key", $keyB.public, "persistent_keepalive_interval", "1")
Write-Phase "Triggering keepalive on $ifaceA"
Invoke-UapiOperation -InterfaceName $ifaceA -Operation "set" -Body $kickA | Out-Null
$stateA = Wait-Handshake -InterfaceName $ifaceA -TimeoutSec $HandshakeTimeoutSec
$stateB = Wait-Handshake -InterfaceName $ifaceB -TimeoutSec $HandshakeTimeoutSec
Start-Sleep -Seconds 2
if ($stateA["network"] -ne $SelectedMode) {
throw "UAPI state for $ifaceA reports network=$($stateA["network"]), expected $SelectedMode"
}
if ($stateB["network"] -ne $SelectedMode) {
throw "UAPI state for $ifaceB reports network=$($stateB["network"]), expected $SelectedMode"
}
Assert-StateValue -State $stateA -Key "format_in" -ExpectedValue $profile.FormatSpec -InterfaceName $ifaceA
Assert-StateValue -State $stateA -Key "format_out" -ExpectedValue $profile.FormatSpec -InterfaceName $ifaceA
Assert-StateValue -State $stateA -Key "header_compat" -ExpectedValue "true" -InterfaceName $ifaceA
Assert-StateValue -State $stateA -Key "i1" -ExpectedValue $profile.I1Spec -InterfaceName $ifaceA
Assert-StateValue -State $stateA -Key "i2" -ExpectedValue $profile.I2Spec -InterfaceName $ifaceA
Assert-StateValue -State $stateB -Key "format_in" -ExpectedValue $profile.FormatSpec -InterfaceName $ifaceB
Assert-StateValue -State $stateB -Key "format_out" -ExpectedValue $profile.FormatSpec -InterfaceName $ifaceB
Assert-StateValue -State $stateB -Key "header_compat" -ExpectedValue "true" -InterfaceName $ifaceB
Assert-StateValue -State $stateB -Key "i1" -ExpectedValue $profile.I1Spec -InterfaceName $ifaceB
Assert-StateValue -State $stateB -Key "i2" -ExpectedValue $profile.I2Spec -InterfaceName $ifaceB
}
finally {
Stop-Capture -EtlPath $captureEtl -PcapPath $capturePcap
}
$stdoutA = Get-LogText -Path $procA.StdoutPath
$stdoutB = Get-LogText -Path $procB.StdoutPath
if ($SelectedMode -eq "udp") {
if (($stdoutA -match 'Failed to receive .* packet: invalid data') -or
($stdoutB -match 'Failed to receive .* packet: invalid data')) {
throw "Unexpected UDP invalid-data log entry observed. UDP decoy/junk datagrams should now be filtered at the bind layer before device logging."
}
}
Write-Phase "Validating capture markers for mode=$SelectedMode"
$captureBytes = [System.IO.File]::ReadAllBytes($capturePcap)
$captureMarkers = @{
format = Test-CaptureContains -CaptureBytes $captureBytes -Needle (Convert-HexToBytes $profile.FormatHex)
i1 = Test-CaptureContains -CaptureBytes $captureBytes -Needle (Convert-HexToBytes $profile.I1Hex)
i2 = Test-CaptureContains -CaptureBytes $captureBytes -Needle (Convert-HexToBytes $profile.I2Hex)
h1 = Test-CaptureContains -CaptureBytes $captureBytes -Needle (Get-LittleEndianUint32Bytes $profile.H1)
h2 = Test-CaptureContains -CaptureBytes $captureBytes -Needle (Get-LittleEndianUint32Bytes $profile.H2)
h4 = Test-CaptureContains -CaptureBytes $captureBytes -Needle (Get-LittleEndianUint32Bytes $profile.H4)
}
if (-not ($captureMarkers.Values -contains $true)) {
Write-Warning "pktmon capture did not expose any conceal payload markers for mode=$SelectedMode. Handshake and UAPI validation still passed."
}
[PSCustomObject]@{
mode = $SelectedMode
interface_a = $ifaceA
interface_b = $ifaceB
pcapng = $capturePcap
logs = $modeOutDir
capture_markers = $captureMarkers
}
}
finally {
Write-Phase "Stopping local peers for mode=$SelectedMode"
Stop-AwgProcess -Process $procA
Stop-AwgProcess -Process $procB
}
}
Assert-Administrator
New-Directory $OutDir
if (-not (Test-Path $ProgramPath)) {
throw "Program path does not exist: $ProgramPath"
}
$wintunDllPath = Resolve-WintunDllPath
Write-Host "Using Wintun DLL at $wintunDllPath"
$modes = if ($Mode -eq "both") { @("udp", "tcp") } else { @($Mode) }
$results = @()
foreach ($selectedMode in $modes) {
$results += Invoke-ConcealSmoke -SelectedMode $selectedMode
}
$summaryPath = Join-Path $OutDir "summary.json"
$results | ConvertTo-Json -Depth 4 | Set-Content -Encoding UTF8 $summaryPath
$results | Format-Table -AutoSize | Out-String | Write-Host
Write-Host "Summary written to $summaryPath"
+4 -4
View File
@@ -5,10 +5,10 @@ import (
"fmt"
"math/rand"
"testing"
"golang.org/x/sys/unix"
)
const tcpProtocolNumber = 6
func checksumRef(b []byte, initial uint16) uint16 {
ac := uint64(initial)
@@ -58,9 +58,9 @@ func TestPseudoHeaderChecksum(t *testing.T) {
rng.Read(srcAddr)
rng.Read(dstAddr)
rng.Read(buf)
phSum := pseudoHeaderChecksumNoFold(unix.IPPROTO_TCP, srcAddr, dstAddr, uint16(length))
phSum := pseudoHeaderChecksumNoFold(tcpProtocolNumber, srcAddr, dstAddr, uint16(length))
csum := checksum(buf, phSum)
phSumRef := pseudoHeaderChecksumRefNoFold(unix.IPPROTO_TCP, srcAddr, dstAddr, uint16(length))
phSumRef := pseudoHeaderChecksumRefNoFold(tcpProtocolNumber, srcAddr, dstAddr, uint16(length))
csumRef := checksumRef(buf, phSumRef)
if csum != csumRef {
t.Error("Expected checksumRef", csumRef, "got", csum)
+60
View File
@@ -25,6 +25,48 @@ func Ping(dst, src netip.Addr) []byte {
return genICMPv4(payload, dst, src)
}
func UDP(dst, src netip.Addr) []byte {
payload := []byte("tuntest-udp")
const (
udpProtocolNumber = 17
ipv4Size = 20
udpSize = 8
)
pkt := make([]byte, ipv4Size+udpSize+len(payload))
udp := pkt[ipv4Size : ipv4Size+udpSize]
binary.BigEndian.PutUint16(udp[0:], 1337)
binary.BigEndian.PutUint16(udp[2:], 7331)
binary.BigEndian.PutUint16(udp[4:], uint16(udpSize+len(payload)))
copy(pkt[ipv4Size+udpSize:], payload)
fillIPv4Header(pkt[:ipv4Size], dst, src, udpProtocolNumber, len(pkt))
return pkt
}
func TCP(dst, src netip.Addr) []byte {
payload := []byte("tuntest-tcp")
const (
tcpProtocolNumber = 6
ipv4Size = 20
tcpSize = 20
)
pkt := make([]byte, ipv4Size+tcpSize+len(payload))
tcp := pkt[ipv4Size : ipv4Size+tcpSize]
binary.BigEndian.PutUint16(tcp[0:], 1337)
binary.BigEndian.PutUint16(tcp[2:], 7331)
binary.BigEndian.PutUint32(tcp[4:], 1)
binary.BigEndian.PutUint32(tcp[8:], 1)
tcp[12] = tcpSize / 4 << 4
tcp[13] = 0x18 // PSH|ACK
binary.BigEndian.PutUint16(tcp[14:], 65535)
copy(pkt[ipv4Size+tcpSize:], payload)
fillIPv4Header(pkt[:ipv4Size], dst, src, tcpProtocolNumber, len(pkt))
return pkt
}
// Checksum is the "internet checksum" from https://tools.ietf.org/html/rfc1071.
func checksum(buf []byte, initial uint16) uint16 {
v := uint32(initial)
@@ -79,6 +121,24 @@ func genICMPv4(payload []byte, dst, src netip.Addr) []byte {
return pkt
}
func fillIPv4Header(ip []byte, dst, src netip.Addr, protocol, length int) {
const (
ipv4Size = 20
ipv4TotalLenOffset = 2
ipv4ChecksumOffset = 10
ttl = 65
)
ip[0] = (4 << 4) | (ipv4Size / 4)
binary.BigEndian.PutUint16(ip[ipv4TotalLenOffset:], uint16(length))
ip[8] = ttl
ip[9] = byte(protocol)
copy(ip[12:], src.AsSlice())
copy(ip[16:], dst.AsSlice())
chksum := ^checksum(ip[:], 0)
binary.BigEndian.PutUint16(ip[ipv4ChecksumOffset:], chksum)
}
type ChannelTUN struct {
Inbound chan []byte // incoming packets, closed on TUN close
Outbound chan []byte // outbound packets, blocks forever on TUN close