Compare commits

..
Author SHA1 Message Date
FrogRocky 513511c162 outline: pass header protection key directly to UAPI 2026-09-20 10:43:13 +02:00
FrogRocky b6393b6f2d outline: simplify AWG configuration 2026-09-20 10:31:02 +02:00
FrogRocky dffb93aa29 outline: support AWG 3.1 parameters 2026-09-18 22:20:23 +02:00
Yaroslav Gurov b5928efb6c fix: disable the whole underload if DisableCookies is on
Thanks to @darkk for spotting and suggesting the fix
2026-08-28 17:48:27 +02:00
Yaroslav Gurov da11c9fadc fix: use udp window for RandomPaddingAddition 2026-08-28 17:48:05 +02:00
Yaroslav Gurov 1b86b2ae0e fix: wrong trailerLen for HandshakeCookie message
* AWG3.1-related changes indroduced a new bug with HandshakeCookie
  buffer size. Instead of adding trailerLen to the final size, it is
getting provided as a capacity param leading to the wrong behavior and
runtime panic while RandomTrailers is used
* Must be the typo :_)
2026-08-13 21:36:22 +02:00
Yaroslav Gurov 08271d00b3 feat: make PeekLookAtSocketFd available across all platforms 2026-08-13 15:55:40 +02:00
Yaroslav Gurov 75ea550a64 chore: update awg-tools 2026-08-13 13:39:15 +02:00
Yaroslav Gurov 1f50ad736e feat: awg 3.1 features
* add RandomTrailers feature which appends random amount of bytes to the
end of each packet
* add DisableCookie feature which prohibits interface to send any cookie
replies
2026-08-13 00:25:03 +02:00
Yaroslav Gurov 08d68cdae2 fix: keepalives are ignored 2026-08-05 13:59:10 +02:00
itfsdev cf9d2dd202 fix: pin Dockerfile's amneziawg-tools build to the tagged v3.0.20260730 release 2026-07-31 20:16:15 +08:00
Yaroslav Gurov 7860d60a52 fix: change HeaderCipherNonceSize to be reported in wrong S1-S4 2026-07-31 08:53:19 +02:00
Yaroslav Gurov ce7cf1034d docs: change 8 requirement to 12 in README 2026-07-31 08:53:19 +02:00
itfsdev d57d98d55b feat: update Dockerfile to point to the special commit from feat/awg3 from amneziawg-tools 2026-07-28 22:40:25 +08:00
itfsdev 0527dfa476 fix: bump Docker builder image to golang:1.25.12 2026-07-28 18:33:16 +08:00
Yaroslav Gurov 9f5d948bc7 fix: use v3 versioning 2026-07-24 15:06:35 +02:00
Yaroslav Gurov 457d920a1a feat: amneziawg 3.0
* feat: add header protection mechanism, random transport payload trailing size, and handshake timings randomization

* fix: use uint range instead of the int one

* fix: uapi typo

* fix: simultaneous padding access

* feat: prohibit Sx < 8 if headerProtection is set

* fix: do not count bytes on crypt

* fix: trailing size calculation

* fix: get rid of buffer copying on receival

* chore: rename random padding multiple

* fix: atomize junk packets

* fix: use padding from device by default

* chore: readme changes

* chore: add readme annotation about client-side params

* chore: add both side recommendation for content padding

* feat: change ContentPaddingMultiple to ContentPaddingAddition

* feat: bring chacha20 in there

* chore: use UintRange instead of magicHeader

* feat: make all timers parameters

* fix: reissue max_handshake_amount on successful handshake

* chore: add readme info about new timers

* fix: use ParseUint for UintRange

* feat: make persistent keepalive a range

* feat: use atomics everywhere where possible

* chore: readme changes

* fix: typo

* fix: minor timer fixes
2026-07-24 14:18:33 +02:00
Vinicius Fortuna c1e9bb3758 Update Outline SDK module path 2026-07-09 04:22:33 +02:00
Yaroslav Gurov 1cc94272ca fix: handle empty I1-I5 2026-06-18 00:38:58 +02:00
stereomonk 3610f21b75 doc: fix typo README.md 2026-06-17 22:22:36 +02:00
admin f4f4c99926 fix: apply S4 transport padding to keepalive packets
Keepalive packets were excluded from S4 padding because the padding
logic was nested inside the dataSent guard. The receiving side
(DeterminePacketTypeAndPadding) expects S4 padding on all transport
packets, so unpadded keepalives fail H4 header validation and are
silently dropped.

This prevents the responder from completing key confirmation —
lastHandshakeNano stays 0 until real data flows through the tunnel.
2026-05-13 11:11:21 +02:00
Yaroslav Gurov 12a012205e readme: actualize type for H1-H4 2026-03-31 17:56:17 +02:00
Yaroslav Gurov e7ef4339e7 readme: remove <c> tag from tag reference 2026-03-23 12:10:15 +01:00
80 changed files with 1682 additions and 7399 deletions

No files matched your search

+14 -8
View File
@@ -1,18 +1,24 @@
FROM golang:1.24.4 as awg
FROM golang:1.25.12 as awg
COPY . /awg
WORKDIR /awg
RUN go mod download && \
go mod verify && \
go build -ldflags '-linkmode external -extldflags "-fno-PIC -static"' -v -o /usr/bin
FROM alpine:3.19
ARG AWGTOOLS_RELEASE="1.0.20250901"
FROM alpine:3.19 as tools
ARG AWGTOOLS_COMMIT="v3.1.20260812"
RUN apk --no-cache add iproute2 iptables bash && \
cd /usr/bin/ && \
wget https://github.com/amnezia-vpn/amneziawg-tools/releases/download/v${AWGTOOLS_RELEASE}/alpine-3.19-amneziawg-tools.zip && \
unzip -j alpine-3.19-amneziawg-tools.zip && \
chmod +x /usr/bin/awg /usr/bin/awg-quick && \
RUN apk add --no-cache git build-base linux-headers && \
git clone https://github.com/amnezia-vpn/amneziawg-tools.git /amneziawg-tools && \
cd /amneziawg-tools && git checkout ${AWGTOOLS_COMMIT} && \
cd src && make
FROM alpine:3.19
RUN apk --no-cache add iproute2 iptables bash
COPY --from=tools /amneziawg-tools/src/wg /usr/bin/awg
COPY --from=tools /amneziawg-tools/src/wg-quick/linux.bash /usr/bin/awg-quick
RUN chmod +x /usr/bin/awg /usr/bin/awg-quick && \
ln -s /usr/bin/awg /usr/bin/wg && \
ln -s /usr/bin/awg-quick /usr/bin/wg-quick
COPY --from=awg /usr/bin/amneziawg-go /usr/bin/amneziawg-go
+61 -189
View File
@@ -53,9 +53,59 @@ $ make
## Configuration
### Data types and definitions
`client-side` means the param is not required to be the same on both server and client, while
`server-side` means this is mandatory the values are the same on server and on client
`range,x`, where x is the underlying type of left and right
* Format: "a-b", or "a", or "(off)"
* Example: PersistentKeepalive = 22-30
> [!NOTE]
> If there is no value specified (for any param), AWG treats it as 0
### Header protection [AWG 3+]
Header protection is the mechanism of protecting low-entropy values of packets' headers. The idea is to apply fast encryption to the specific fields which WireGuard does use for authentication and its own encryption. The cipher uses `S1-S4` crypto padding as nonce for each incoming packet.
```
[Device]
+ HeaderProtectionKey: key,string - server-side # the key to be used in header protection
```
> [!TIP]
> Use `awg genkey` to generate header protection key
> [!IMPORTANT]
> Header protection requires `S1-S4` value to be 12 at least
### Content padding [AWG 3+]
```
[Device]
+ ContentPaddingAddition: uint32,range - client-side # the range to be used as a custom padding
```
> [!TIP]
> It's important to specify content padding on both sides. However, this is not strictly required and could be omitted.
### Timings [AWG 3+]
This param could be used to customize default Wireguard's timings
```
[Device]
+ RekeyAfterTime = "uint32,range - client-side - seconds" # time, after which client tries to handshake
+ RekeyTimeout = "uint32,range - client-side - seconds" # timeout, after which handshake is repeated
+ RejectAfterTime = "uint32,range - client-side - seconds" # time, after which client forces handshake, and declines all incoming data
+ KeepaliveTimeout = "uint32,range - client-side - seconds" # time from last data sending, after which keepalive is sent
+ MaxHandshakeAttempts = "uint32,range - client-side - amount" # maximum attempts of handshake repetition
[Peer]
M PersistentKeepalive = "uint32,range - client-side - seconds" # interval of persistent keepalive
```
### Junk 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
@@ -78,10 +128,10 @@ The amount of junk packets specified in `Jc` with a random size between `Jmin` a
### Message headers
Every message in wireguard has `int32` type at the beginning of the packet. This field could be controlled by specifying the params below:
Every message in wireguard has `uint32` type at the beginning of the packet. This field could be controlled by specifying the params below:
- `H1: string` - header range of handshake initial message
- `H2: string` - header range of handshake initial message
- `H2: string` - header range of handshake response message
- `H3: string` - header range of handshake cookie message
- `H4: string` - header range of transport message
@@ -89,12 +139,6 @@ 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.
@@ -105,187 +149,15 @@ These packets are being send prior to every handshake, in the same way as Junk p
- `I4: string`
- `I5: string`
Value as a tag-sequence, as described in Tag format paragraph
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
### Network
**`Network: "udp" | "tcp"`**
- A network which device would use to connect to peers
- In case of `tcp`, the connection is always initiated by the peer which has `Endpoint` specified
- In case of `tcp`, the device would not listen if `ListenPort` is omitted
> [!NOTE]
> Although it is not required, `Network = tcp` would highly-likely require `FormatIn` and `FormatOut` parameters since stream connection does not have fixed-sized messages
### Message format
**`FormatIn: string of tags`**
- A format of packet which AWG device would expect on the input side instead of the original WG packet
- The packet could be a part of the stream if `<dz>` is specified
**`FormatOut: string of tags`**
- A format of packet which AWG device would generate on the output side instead of the original WG packet. The whole packet would be generated as a single piece and would be sent to the system as-is
- **Example**
```
FormatIn = <b 0x4567><dz be 4><r 12><d><t>
FormatOut = <b 0x1234><dz be 4><d>
```
> [!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]
> 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>
```
> If the final size of any packet exceeds system MTU, it would be fractured into fragments, which looks suspicious
-581
View File
@@ -1,581 +0,0 @@
package conceal
import (
"bytes"
crand "crypto/rand"
"encoding/binary"
"io"
"net"
"os"
"sync"
"syscall"
"testing"
"time"
"golang.org/x/net/ipv4"
)
const (
benchmarkMaxPacketSize = 65535
benchmarkBatchPoolSize = 128
benchmarkUDPListenPort = 51820
benchmarkFixtureRingSize = 256
)
var (
benchmarkFramedOpts = FramedOpts{
H1: benchmarkMustHeader("777"),
H2: benchmarkMustHeader("778"),
H3: benchmarkMustHeader("779"),
H4: benchmarkMustHeader("780"),
S1: 8,
S2: 8,
S3: 8,
S4: 16,
}
benchmarkFramedCompatOpts = func() FramedOpts {
opts := benchmarkFramedOpts
opts.HeaderCompat = true
return opts
}()
benchmarkMasqueradeRules = benchmarkMustRules("<dz be 2><d>")
benchmarkPreludeOneRule = PreludeOpts{
RulesArr: [5]Rules{
benchmarkMustRules("<b 0xaabb>"),
},
}
benchmarkPreludeFiveRules = PreludeOpts{
RulesArr: [5]Rules{
benchmarkMustRules("<b 0xa1>"),
benchmarkMustRules("<b 0xa2a3>"),
benchmarkMustRules("<b 0xa4a5a6>"),
benchmarkMustRules("<b 0xa7a8a9aa>"),
benchmarkMustRules("<b 0xabacadaeaf>"),
},
}
benchmarkPreludeRulesPlusJunk = PreludeOpts{
Jc: 1,
Jmin: 3,
Jmax: 3,
RulesArr: [5]Rules{
benchmarkMustRules("<b 0xaabb>"),
},
}
benchmarkPayloads = benchmarkBuildPayloadProfiles()
benchmarkUDPAddr = &net.UDPAddr{
IP: net.IPv4(127, 0, 0, 1),
Port: benchmarkUDPListenPort,
}
benchmarkRandMu sync.Mutex
)
type benchmarkPayloadProfiles struct {
initiation []byte
response []byte
cookie []byte
transportKeepalive []byte
transportSmall []byte
transportMTU []byte
compatInitiation []byte
compatResponse []byte
compatCookie []byte
compatTransportKeepalive []byte
compatTransportSmall []byte
compatTransportMTU []byte
}
func benchmarkBuildPayloadProfiles() benchmarkPayloadProfiles {
return benchmarkPayloadProfiles{
initiation: benchmarkMakePayload(WireguardMsgInitiationSize, WireguardMsgInitiationType),
response: benchmarkMakePayload(WireguardMsgResponseSize, WireguardMsgResponseType),
cookie: benchmarkMakePayload(WireguardMsgCookieReplySize, WireguardMsgCookieReplyType),
transportKeepalive: benchmarkMakePayload(WireguardMsgTransportMinSize, WireguardMsgTransportType),
transportSmall: benchmarkMakePayload(256, WireguardMsgTransportType),
transportMTU: benchmarkMakePayload(1280, WireguardMsgTransportType),
compatInitiation: benchmarkMakePayload(WireguardMsgInitiationSize, benchmarkFramedCompatOpts.H1.start),
compatResponse: benchmarkMakePayload(WireguardMsgResponseSize, benchmarkFramedCompatOpts.H2.start),
compatCookie: benchmarkMakePayload(WireguardMsgCookieReplySize, benchmarkFramedCompatOpts.H3.start),
compatTransportKeepalive: benchmarkMakePayload(WireguardMsgTransportMinSize, benchmarkFramedCompatOpts.H4.start),
compatTransportSmall: benchmarkMakePayload(256, benchmarkFramedCompatOpts.H4.start),
compatTransportMTU: benchmarkMakePayload(1280, benchmarkFramedCompatOpts.H4.start),
}
}
func benchmarkMakePayload(size int, header uint32) []byte {
payload := make([]byte, size)
binary.LittleEndian.PutUint32(payload[:4], header)
for i := 4; i < len(payload); i++ {
payload[i] = byte(i)
}
return payload
}
func benchmarkMustRules(spec string) Rules {
rules, err := ParseRules(spec)
if err != nil {
panic(err)
}
return rules
}
func benchmarkMustHeader(spec string) *RangedHeader {
header, err := NewRangedHeader(spec)
if err != nil {
panic(err)
}
return header
}
func benchmarkNewBufferPool() *sync.Pool {
return &sync.Pool{
New: func() any {
return make([]byte, benchmarkMaxPacketSize)
},
}
}
func benchmarkNewMsgsPool() *sync.Pool {
return &sync.Pool{
New: func() any {
msgs := make([]ipv4.Message, benchmarkBatchPoolSize)
for i := range msgs {
msgs[i].Buffers = make([][]byte, 1)
msgs[i].OOB = make([]byte, 0)
}
return &msgs
},
}
}
func benchmarkMaxFramePadding(opts FramedOpts) int {
maxPadding := opts.S1
if opts.S2 > maxPadding {
maxPadding = opts.S2
}
if opts.S3 > maxPadding {
maxPadding = opts.S3
}
if opts.S4 > maxPadding {
maxPadding = opts.S4
}
return maxPadding
}
func benchmarkEncodeFramedRecord(opts FramedOpts, payload []byte) []byte {
enc, ok := newFrameEncoding(opts)
if !ok {
panic("framed benchmark encoding unavailable")
}
buf := make([]byte, len(payload)+benchmarkMaxFramePadding(opts))
n := enc.Encode(buf, payload)
if n == 0 {
panic("framed benchmark encoding failed")
}
return append([]byte(nil), buf[:n]...)
}
func benchmarkEncodeMasqueradeRecord(rules Rules, payload []byte) []byte {
pool := benchmarkNewBufferPool()
ctx := &writeContext{
FlexBuffer: WrapFlexBuffer(payload),
BufferPool: WrapBufferPool(pool),
}
tmp := pool.Get().([]byte)
defer pool.Put(tmp)
w := bytes.NewBuffer(tmp[:0])
if err := rules.Write(w, ctx); err != nil {
panic(err)
}
return append([]byte(nil), w.Bytes()...)
}
func benchmarkEncodeStreamRecords(rules Rules, records ...[]byte) []byte {
var out bytes.Buffer
for _, record := range records {
out.Write(benchmarkEncodeMasqueradeRecord(rules, record))
}
return out.Bytes()
}
func benchmarkRepeatPayload(payload []byte, count int) [][]byte {
payloads := make([][]byte, count)
for i := range payloads {
payloads[i] = payload
}
return payloads
}
func benchmarkInitiationBatch(batchSize int) [][]byte {
payloads := make([][]byte, batchSize)
if batchSize == 0 {
return payloads
}
payloads[0] = benchmarkPayloads.initiation
for i := 1; i < len(payloads); i++ {
payloads[i] = benchmarkPayloads.transportSmall
}
return payloads
}
func benchmarkTotalBytes(payloads [][]byte) int {
total := 0
for _, payload := range payloads {
total += len(payload)
}
return total
}
func benchmarkAverageBytes(payloads ...[]byte) int {
if len(payloads) == 0 {
return 0
}
total := 0
for _, payload := range payloads {
total += len(payload)
}
return total / len(payloads)
}
func benchmarkAverageInts(values ...int) int {
if len(values) == 0 {
return 0
}
total := 0
for _, value := range values {
total += value
}
return total / len(values)
}
func benchmarkFullMatrixEnabled() bool {
return os.Getenv("AMNEZWG_BENCH_FULL") != ""
}
func benchmarkRunLoop(b *testing.B, bytesPerOp int, reset func(), op func() error) {
b.Helper()
b.ReportAllocs()
if bytesPerOp > 0 {
b.SetBytes(int64(bytesPerOp))
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
if i > 0 && reset != nil {
b.StopTimer()
reset()
b.StartTimer()
}
if err := op(); err != nil {
b.Fatal(err)
}
}
}
func benchmarkRunLoopWithFixtureRing[T any](b *testing.B, bytesPerOp int, fixtures []T, reset func(T), op func(T) error) {
b.Helper()
if len(fixtures) == 0 {
b.Fatal("benchmark fixture ring must not be empty")
}
b.ReportAllocs()
if bytesPerOp > 0 {
b.SetBytes(int64(bytesPerOp))
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
if i > 0 && i%len(fixtures) == 0 && reset != nil {
b.StopTimer()
for _, fixture := range fixtures {
reset(fixture)
}
b.StartTimer()
}
if err := op(fixtures[i%len(fixtures)]); err != nil {
b.Fatal(err)
}
}
}
func benchmarkUseDeterministicRand(b *testing.B) {
b.Helper()
benchmarkRandMu.Lock()
oldReader := crand.Reader
crand.Reader = &benchmarkDeterministicReader{state: 1}
b.Cleanup(func() {
crand.Reader = oldReader
benchmarkRandMu.Unlock()
})
}
type benchmarkDeterministicReader struct {
state uint64
}
func (r *benchmarkDeterministicReader) Read(p []byte) (int, error) {
x := r.state
if x == 0 {
x = 1
}
for i := range p {
x ^= x << 13
x ^= x >> 7
x ^= x << 17
p[i] = byte(x)
}
r.state = x
return len(p), nil
}
type benchmarkStreamConn struct {
readBuf []byte
readOff int
readChunks [][]byte
readChunkAt int
readChunkOff int
}
func newBenchmarkStreamConn(readBuf []byte) *benchmarkStreamConn {
return &benchmarkStreamConn{readBuf: readBuf}
}
func newBenchmarkStreamChunksConn(readChunks [][]byte) *benchmarkStreamConn {
return &benchmarkStreamConn{readChunks: readChunks}
}
func (c *benchmarkStreamConn) ResetRead() {
c.readOff = 0
c.readChunkAt = 0
c.readChunkOff = 0
}
func (c *benchmarkStreamConn) Read(p []byte) (int, error) {
if len(c.readChunks) > 0 {
chunk := c.readChunks[c.readChunkAt]
if c.readChunkOff >= len(chunk) {
c.readChunkAt++
if c.readChunkAt == len(c.readChunks) {
c.readChunkAt = 0
}
c.readChunkOff = 0
chunk = c.readChunks[c.readChunkAt]
}
n := copy(p, chunk[c.readChunkOff:])
c.readChunkOff += n
if c.readChunkOff == len(chunk) {
c.readChunkAt++
if c.readChunkAt == len(c.readChunks) {
c.readChunkAt = 0
}
c.readChunkOff = 0
}
return n, nil
}
if len(c.readBuf) == 0 {
return 0, io.EOF
}
if c.readOff >= len(c.readBuf) {
c.readOff = 0
}
n := copy(p, c.readBuf[c.readOff:])
c.readOff += n
return n, nil
}
func (c *benchmarkStreamConn) Write(p []byte) (int, error) {
return len(p), nil
}
func (c *benchmarkStreamConn) Close() error {
return nil
}
func (c *benchmarkStreamConn) LocalAddr() net.Addr {
return benchmarkUDPAddr
}
func (c *benchmarkStreamConn) RemoteAddr() net.Addr {
return benchmarkUDPAddr
}
func (c *benchmarkStreamConn) SetDeadline(time.Time) error {
return nil
}
func (c *benchmarkStreamConn) SetReadDeadline(time.Time) error {
return nil
}
func (c *benchmarkStreamConn) SetWriteDeadline(time.Time) error {
return nil
}
type benchmarkUDPConn struct {
readPayloads [][]byte
readIndex int
}
func newBenchmarkUDPConn(readPayloads [][]byte) *benchmarkUDPConn {
return &benchmarkUDPConn{readPayloads: readPayloads}
}
func (c *benchmarkUDPConn) ResetRead() {
c.readIndex = 0
}
func (c *benchmarkUDPConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
n, _, _, udpAddr, err := c.ReadMsgUDP(p, nil)
return n, udpAddr, err
}
func (c *benchmarkUDPConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
udpAddr, _ := addr.(*net.UDPAddr)
n, _, err = c.WriteMsgUDP(p, nil, udpAddr)
return n, err
}
func (c *benchmarkUDPConn) Close() error {
return nil
}
func (c *benchmarkUDPConn) LocalAddr() net.Addr {
return benchmarkUDPAddr
}
func (c *benchmarkUDPConn) SetDeadline(time.Time) error {
return nil
}
func (c *benchmarkUDPConn) SetReadDeadline(time.Time) error {
return nil
}
func (c *benchmarkUDPConn) SetWriteDeadline(time.Time) error {
return nil
}
func (c *benchmarkUDPConn) SyscallConn() (syscall.RawConn, error) {
return nil, nil
}
func (c *benchmarkUDPConn) ReadMsgUDP(b, oob []byte) (n, oobn, flags int, addr *net.UDPAddr, err error) {
if len(c.readPayloads) == 0 {
return 0, 0, 0, nil, io.EOF
}
payload := c.readPayloads[c.readIndex]
c.readIndex++
if c.readIndex == len(c.readPayloads) {
c.readIndex = 0
}
n = copy(b, payload)
return n, 0, 0, benchmarkUDPAddr, nil
}
func (c *benchmarkUDPConn) WriteMsgUDP(b, oob []byte, addr *net.UDPAddr) (n, oobn int, err error) {
return len(b), len(oob), nil
}
type benchmarkBatchPacket struct {
payload []byte
oob []byte
addr *net.UDPAddr
}
type benchmarkBatchConn struct {
readBatches [][]benchmarkBatchPacket
readIndex int
}
func newBenchmarkBatchConn(readBatches [][]benchmarkBatchPacket) *benchmarkBatchConn {
return &benchmarkBatchConn{readBatches: readBatches}
}
func (c *benchmarkBatchConn) ResetRead() {
c.readIndex = 0
}
func (c *benchmarkBatchConn) ReadBatch(ms []ipv4.Message, flags int) (int, error) {
if len(c.readBatches) == 0 {
return 0, io.EOF
}
batch := c.readBatches[c.readIndex]
c.readIndex++
if c.readIndex == len(c.readBatches) {
c.readIndex = 0
}
for i := range batch {
packet := batch[i]
msg := &ms[i]
msg.N = copy(msg.Buffers[0], packet.payload)
if cap(msg.OOB) < len(packet.oob) {
panic("benchmark batch OOB buffer too small")
}
msg.OOB = msg.OOB[:len(packet.oob)]
copy(msg.OOB, packet.oob)
msg.NN = len(packet.oob)
msg.Addr = packet.addr
}
for i := len(batch); i < len(ms); i++ {
ms[i].N = 0
ms[i].NN = 0
ms[i].Addr = nil
ms[i].OOB = ms[i].OOB[:0]
}
return len(batch), nil
}
func (c *benchmarkBatchConn) WriteBatch(ms []ipv4.Message, flags int) (int, error) {
return len(ms), nil
}
func benchmarkBatchPackets(payloads [][]byte) []benchmarkBatchPacket {
packets := make([]benchmarkBatchPacket, len(payloads))
for i, payload := range payloads {
packets[i] = benchmarkBatchPacket{
payload: payload,
addr: benchmarkUDPAddr,
}
}
return packets
}
func benchmarkNewBatchReadMessages(count int, oobCap int) []ipv4.Message {
msgs := make([]ipv4.Message, count)
for i := range msgs {
msgs[i].Buffers = make([][]byte, 1)
msgs[i].Buffers[0] = make([]byte, benchmarkMaxPacketSize)
msgs[i].OOB = make([]byte, 0, oobCap)
}
return msgs
}
type benchmarkBatchWriteFixture struct {
msgs []ipv4.Message
payloads [][]byte
addr *net.UDPAddr
}
func newBenchmarkBatchWriteFixture(payloads [][]byte, oobCap int) *benchmarkBatchWriteFixture {
fixture := &benchmarkBatchWriteFixture{
msgs: make([]ipv4.Message, len(payloads)),
payloads: payloads,
addr: benchmarkUDPAddr,
}
for i := range fixture.msgs {
fixture.msgs[i].Buffers = make([][]byte, 1)
fixture.msgs[i].OOB = make([]byte, 0, oobCap)
}
fixture.Reset()
return fixture
}
func (f *benchmarkBatchWriteFixture) Reset() {
for i, payload := range f.payloads {
f.msgs[i].Buffers[0] = payload
f.msgs[i].N = len(payload)
f.msgs[i].NN = 0
f.msgs[i].Addr = f.addr
f.msgs[i].OOB = f.msgs[i].OOB[:0]
}
}
-901
View File
@@ -1,901 +0,0 @@
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
@@ -1,79 +0,0 @@
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
@@ -1,23 +0,0 @@
package conceal
import (
"net"
"syscall"
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
)
type UDPConn interface {
net.PacketConn
SyscallConn() (syscall.RawConn, error)
ReadMsgUDP(b, oob []byte) (n, oobn, flags int, addr *net.UDPAddr, err error)
WriteMsgUDP(b, oob []byte, addr *net.UDPAddr) (n, oobn int, err error)
}
var _ ipv4.Message = ipv6.Message{}
type BatchConn interface {
ReadBatch(ms []ipv4.Message, flags int) (n int, err error)
WriteBatch(ms []ipv4.Message, flags int) (n int, err error)
}
-391
View File
@@ -1,391 +0,0 @@
package conceal
import (
"crypto/rand"
"encoding/binary"
"net"
"sync"
"golang.org/x/net/ipv4"
)
type FramedOpts struct {
H1 *RangedHeader
H2 *RangedHeader
H3 *RangedHeader
H4 *RangedHeader
S1 int
S2 int
S3 int
S4 int
HeaderCompat bool
}
func (o *FramedOpts) HasIntersections() bool {
headers := []*RangedHeader{o.H1, o.H2, o.H3, o.H4}
for i := range len(headers) {
left := headers[i]
if left == nil {
continue
}
for j := i + 1; j < len(headers); j++ {
right := headers[j]
if right == nil {
continue
}
if left.start <= right.end && right.start <= left.end {
return true
}
}
}
return false
}
func newFrameEncoding(opts FramedOpts) (e frameEncoding, ok bool) {
e = frameEncoding{}
if opts.H1 != nil {
e.header.initial = *opts.H1
ok = true
} else {
e.header.initial = RangedHeader{WireguardMsgInitiationType, WireguardMsgInitiationType}
}
if opts.H2 != nil {
e.header.response = *opts.H2
ok = true
} else {
e.header.response = RangedHeader{WireguardMsgResponseType, WireguardMsgResponseType}
}
if opts.H3 != nil {
e.header.cookie = *opts.H3
ok = true
} else {
e.header.cookie = RangedHeader{WireguardMsgCookieReplyType, WireguardMsgCookieReplyType}
}
if opts.H4 != nil {
e.header.transport = *opts.H4
ok = true
} else {
e.header.transport = RangedHeader{WireguardMsgTransportType, WireguardMsgTransportType}
}
if opts.S1 != 0 {
e.padding.initial = opts.S1
ok = true
}
if opts.S2 != 0 {
e.padding.response = opts.S2
ok = true
}
if opts.S3 != 0 {
e.padding.cookie = opts.S3
ok = true
}
if opts.S4 != 0 {
e.padding.transport = opts.S4
ok = true
}
e.compat = opts.HeaderCompat
return e, ok
}
type frameEncoding struct {
header struct {
initial RangedHeader
response RangedHeader
cookie RangedHeader
transport RangedHeader
}
padding struct {
initial int
response int
cookie int
transport int
}
compat bool
}
type frameRecordKind uint8
const (
frameRecordInvalid frameRecordKind = iota
frameRecordInitiation
frameRecordResponse
frameRecordCookie
frameRecordTransport
)
func encodeOne(dst, src []byte, header RangedHeader, padding int) int {
rand.Read(dst[:padding])
dst = dst[padding:]
binary.LittleEndian.PutUint32(dst[:4], header.Generate())
dst = dst[4:]
n := copy(dst, src[4:])
return padding + 4 + n
}
func encodeOneCompat(dst, src []byte, padding int) int {
rand.Read(dst[:padding])
n := copy(dst[padding:], src)
return padding + n
}
func (e *frameEncoding) Encode(dst, src []byte) int {
if len(src) < 4 {
return 0
}
header := binary.LittleEndian.Uint32(src[:4])
if e.compat {
if e.header.initial.Validate(header) {
return encodeOneCompat(dst, src, e.padding.initial)
} else if e.header.response.Validate(header) {
return encodeOneCompat(dst, src, e.padding.response)
} else if e.header.cookie.Validate(header) {
return encodeOneCompat(dst, src, e.padding.cookie)
} else if e.header.transport.Validate(header) {
return encodeOneCompat(dst, src, e.padding.transport)
}
} else {
switch src[0] {
case WireguardMsgInitiationType:
return encodeOne(dst, src, e.header.initial, e.padding.initial)
case WireguardMsgResponseType:
return encodeOne(dst, src, e.header.response, e.padding.response)
case WireguardMsgCookieReplyType:
return encodeOne(dst, src, e.header.cookie, e.padding.cookie)
case WireguardMsgTransportType:
return encodeOne(dst, src, e.header.transport, e.padding.transport)
}
}
return 0
}
func decodeOneCompat(b []byte, header RangedHeader, padding int) int {
bb := b[padding:]
if !header.Validate(binary.LittleEndian.Uint32(bb[:4])) {
return 0
}
return copy(b, bb)
}
func decodeOne(b []byte, header RangedHeader, padding int, originalHeader uint32) int {
bb := b[padding:]
if !header.Validate(binary.LittleEndian.Uint32(bb[:4])) {
return 0
}
binary.LittleEndian.PutUint32(b[:4], originalHeader)
n := copy(b[4:], bb[4:])
return 4 + n
}
func (e *frameEncoding) matchesRecord(b []byte, header RangedHeader, padding, size int) bool {
if len(b) != size {
return false
}
if len(b) < padding+4 {
return false
}
return header.Validate(binary.LittleEndian.Uint32(b[padding : padding+4]))
}
func (e *frameEncoding) matchesTransportRecord(b []byte) bool {
if len(b) < WireguardMsgTransportMinSize+e.padding.transport {
return false
}
if len(b) < e.padding.transport+4 {
return false
}
return e.header.transport.Validate(binary.LittleEndian.Uint32(b[e.padding.transport : e.padding.transport+4]))
}
func (e *frameEncoding) recordKind(b []byte) frameRecordKind {
if e.matchesRecord(b, e.header.initial, e.padding.initial, WireguardMsgInitiationSize+e.padding.initial) {
return frameRecordInitiation
}
if e.matchesRecord(b, e.header.response, e.padding.response, WireguardMsgResponseSize+e.padding.response) {
return frameRecordResponse
}
if e.matchesRecord(b, e.header.cookie, e.padding.cookie, WireguardMsgCookieReplySize+e.padding.cookie) {
return frameRecordCookie
}
if e.matchesTransportRecord(b) {
return frameRecordTransport
}
return frameRecordInvalid
}
func (e *frameEncoding) IsValidRecord(b []byte) bool {
return e.recordKind(b) != frameRecordInvalid
}
func (e *frameEncoding) IsInitiationRecord(b []byte) bool {
return e.recordKind(b) == frameRecordInitiation
}
func (e *frameEncoding) Decode(b []byte) int {
switch e.recordKind(b) {
case frameRecordInitiation:
if e.compat {
return decodeOneCompat(b, e.header.initial, e.padding.initial)
}
return decodeOne(b, e.header.initial, e.padding.initial, WireguardMsgInitiationType)
case frameRecordResponse:
if e.compat {
return decodeOneCompat(b, e.header.response, e.padding.response)
}
return decodeOne(b, e.header.response, e.padding.response, WireguardMsgResponseType)
case frameRecordCookie:
if e.compat {
return decodeOneCompat(b, e.header.cookie, e.padding.cookie)
}
return decodeOne(b, e.header.cookie, e.padding.cookie, WireguardMsgCookieReplyType)
case frameRecordTransport:
if e.compat {
return decodeOneCompat(b, e.header.transport, e.padding.transport)
}
return decodeOne(b, e.header.transport, e.padding.transport, WireguardMsgTransportType)
default:
return len(b)
}
}
func NewFramedConn(conn net.Conn, pool *sync.Pool, opts FramedOpts) (c *FramedConn, ok bool) {
enc, ok := newFrameEncoding(opts)
if !ok {
return nil, false
}
return &FramedConn{
Conn: conn,
pool: WrapBufferPool(pool),
enc: enc,
}, true
}
type FramedConn struct {
net.Conn
pool *BufferPool
enc frameEncoding
}
func (c *FramedConn) Read(b []byte) (n int, err error) {
n, err = c.Conn.Read(b)
n = c.enc.Decode(b[:n])
return n, err
}
func (c *FramedConn) Write(b []byte) (n int, err error) {
t := c.pool.Get()
defer c.pool.Put(t)
n = c.enc.Encode(t, b)
diff := n - len(b)
n, err = c.Conn.Write(t[:n])
return max(n-diff, 0), err
}
func NewFramedUDPConn(conn UDPConn, pool *sync.Pool, opts FramedOpts) (c UDPConn, ok bool) {
enc, ok := newFrameEncoding(opts)
if !ok {
return nil, false
}
return &FramedUDPConn{
UDPConn: conn,
pool: WrapBufferPool(pool),
enc: enc,
}, true
}
type FramedUDPConn struct {
UDPConn
pool *BufferPool
enc frameEncoding
}
func (c *FramedUDPConn) ReadMsgUDP(b, oob []byte) (n, oobn, flags int, addr *net.UDPAddr, err error) {
n, oobn, flags, addr, err = c.UDPConn.ReadMsgUDP(b, oob)
if err != nil {
return 0, 0, 0, nil, err
}
n = c.enc.Decode(b[:n])
return n, oobn, flags, addr, err
}
func (c *FramedUDPConn) WriteMsgUDP(b, oob []byte, addr *net.UDPAddr) (n, oobn int, err error) {
t := c.pool.Get()
defer c.pool.Put(t)
n = c.enc.Encode(t, b)
diff := n - len(b)
n, oobn, err = c.UDPConn.WriteMsgUDP(t[:n], oob, addr)
return max(n-diff, 0), oobn, err
}
func NewFramedBatchConn(conn BatchConn, pool *sync.Pool, opts FramedOpts) (c BatchConn, ok bool) {
enc, ok := newFrameEncoding(opts)
if !ok {
return nil, false
}
return &FramedBatchConn{
BatchConn: conn,
pool: WrapBufferPool(pool),
enc: enc,
}, true
}
type FramedBatchConn struct {
BatchConn
pool *BufferPool
enc frameEncoding
}
func (c *FramedBatchConn) ReadBatch(ms []ipv4.Message, flags int) (n int, err error) {
n, err = c.BatchConn.ReadBatch(ms, flags)
if err != nil {
return 0, err
}
for i := range ms[:n] {
b := ms[i].Buffers[0][:ms[i].N]
ms[i].N = c.enc.Decode(b)
}
return n, nil
}
func (c *FramedBatchConn) WriteBatch(ms []ipv4.Message, flags int) (n int, err error) {
for i := range ms {
t := c.pool.Get()
defer c.pool.Put(t)
n = c.enc.Encode(t, ms[i].Buffers[0])
ms[i].Buffers[0] = t[:n]
}
// ms[i].N has incorrect N because the original data was modifier above
// however, WG does not check this field, so this is fine
return c.BatchConn.WriteBatch(ms, flags)
}
-221
View File
@@ -1,221 +0,0 @@
package conceal
import (
"bytes"
"net"
"sync"
"golang.org/x/net/ipv4"
)
type MasqueradeOpts struct {
RulesIn Rules
RulesOut Rules
}
func NewMasqueradeConn(conn net.Conn, pool *sync.Pool, opts MasqueradeOpts) (c *MasqueradeConn, ok bool) {
if opts.RulesIn == nil && opts.RulesOut == nil {
return nil, false
}
return &MasqueradeConn{
Conn: conn,
rulesIn: opts.RulesIn,
rulesOut: opts.RulesOut,
pool: WrapBufferPool(pool),
}, true
}
type MasqueradeConn struct {
net.Conn
rulesIn Rules
rulesOut Rules
pool *BufferPool
}
var _ StreamRecordConn = (*MasqueradeConn)(nil)
func (c *MasqueradeConn) CanReadRecord() bool {
return c.rulesIn != nil
}
func (c *MasqueradeConn) CanWriteRecord() bool {
return c.rulesOut != nil
}
func (c *MasqueradeConn) ReadRecord(b []byte) (n int, err error) {
if !c.CanReadRecord() {
return 0, ErrNoReadRecord
}
ctx := &readContext{
FlexBuffer: WrapFlexBuffer(b),
BufferPool: c.pool,
}
if err := c.rulesIn.Read(c.Conn, ctx); err != nil {
return 0, err
}
return ctx.Len(), nil
}
func (c *MasqueradeConn) WriteRecord(b []byte) (n int, err error) {
if !c.CanWriteRecord() {
return 0, ErrNoWriteRecord
}
ctx := &writeContext{
FlexBuffer: WrapFlexBuffer(b),
BufferPool: c.pool,
}
t := c.pool.Get()
defer c.pool.Put(t)
w := bytes.NewBuffer(t[:0])
if err := c.rulesOut.Write(w, ctx); err != nil {
return 0, err
}
if _, err := c.Conn.Write(w.Bytes()); err != nil {
return 0, err
}
return ctx.Len(), nil
}
func (c *MasqueradeConn) Read(b []byte) (n int, err error) {
if !c.CanReadRecord() {
return c.Conn.Read(b)
}
return c.ReadRecord(b)
}
func (c *MasqueradeConn) Write(b []byte) (n int, err error) {
if !c.CanWriteRecord() {
return c.Conn.Write(b)
}
return c.WriteRecord(b)
}
func NewMasqueradeUDPConn(conn UDPConn, pool *sync.Pool, opts MasqueradeOpts) (c *MasqueradeUDPConn, ok bool) {
if opts.RulesIn == nil && opts.RulesOut == nil {
return nil, false
}
return &MasqueradeUDPConn{
UDPConn: conn,
rulesIn: opts.RulesIn,
rulesOut: opts.RulesOut,
pool: WrapBufferPool(pool),
}, true
}
type MasqueradeUDPConn struct {
UDPConn
rulesIn Rules
rulesOut Rules
pool *BufferPool
}
func (c *MasqueradeUDPConn) ReadMsgUDP(b, oob []byte) (n, oobn, flags int, addr *net.UDPAddr, err error) {
n, oobn, flags, addr, err = c.UDPConn.ReadMsgUDP(b, oob)
if err != nil {
return n, oobn, flags, addr, err
}
r := bytes.NewBuffer(b[:n])
ctx := &readContext{
FlexBuffer: WrapFlexBuffer(b),
BufferPool: c.pool,
}
if err = c.rulesIn.Read(r, ctx); err != nil {
return 0, oobn, flags, addr, err
}
return ctx.Len(), oobn, flags, addr, err
}
func (c *MasqueradeUDPConn) WriteMsgUDP(b, oob []byte, addr *net.UDPAddr) (n, oobn int, err error) {
t := c.pool.Get()
defer c.pool.Put(t)
w := bytes.NewBuffer(t[:0])
ctx := &writeContext{
FlexBuffer: WrapFlexBuffer(b),
BufferPool: c.pool,
}
if err = c.rulesOut.Write(w, ctx); err != nil {
return 0, 0, err
}
return c.UDPConn.WriteMsgUDP(t[:ctx.Len()], oob, addr)
}
func NewMasqueradeBatchConn(conn BatchConn, bp *sync.Pool, opts MasqueradeOpts) (c *MasqueradeBatchConn, ok bool) {
if opts.RulesIn == nil && opts.RulesOut == nil {
return nil, false
}
return &MasqueradeBatchConn{
BatchConn: conn,
rulesIn: opts.RulesIn,
rulesOut: opts.RulesOut,
pool: WrapBufferPool(bp),
}, true
}
type MasqueradeBatchConn struct {
BatchConn
rulesIn Rules
rulesOut Rules
pool *BufferPool
}
func (c *MasqueradeBatchConn) ReadBatch(ms []ipv4.Message, flags int) (n int, err error) {
n, err = c.BatchConn.ReadBatch(ms, flags)
if err != nil {
return 0, err
}
for i := range n {
r := bytes.NewBuffer(ms[i].Buffers[0][:ms[i].N])
ctx := &readContext{
FlexBuffer: WrapFlexBuffer(ms[i].Buffers[0]),
BufferPool: c.pool,
}
if err = c.rulesIn.Read(r, ctx); err != nil {
return 0, err
}
ms[i].N = ctx.Len()
}
return n, nil
}
func (c *MasqueradeBatchConn) WriteBatch(ms []ipv4.Message, flags int) (n int, err error) {
for i := range ms {
t := c.pool.Get()
defer c.pool.Put(t)
w := bytes.NewBuffer(t[:0])
ctx := &writeContext{
FlexBuffer: WrapFlexBuffer(ms[i].Buffers[0]),
BufferPool: c.pool,
}
if err = c.rulesOut.Write(w, ctx); err != nil {
return 0, err
}
ms[i].Buffers[0] = w.Bytes()
}
return c.BatchConn.WriteBatch(ms, flags)
}
-334
View File
@@ -1,334 +0,0 @@
package conceal
import (
"bytes"
"crypto/rand"
"encoding/binary"
"math/big"
"net"
"sync"
"golang.org/x/net/ipv4"
)
type PreludeOpts struct {
Jc int
Jmin int
Jmax int
RulesArr [5]Rules
}
func (o PreludeOpts) HasDecoyRules() bool {
for _, rules := range o.RulesArr {
if rules != nil {
return true
}
}
return false
}
func (o PreludeOpts) IsEmpty() bool {
if o.HasDecoyRules() {
return false
}
return o.Jc == 0
}
func newJunkGenerator(min, max int) *junkGenerator {
return &junkGenerator{
rang: big.NewInt(int64(max - min + 1)),
offset: min,
}
}
type junkGenerator struct {
rang *big.Int
offset int
}
func (p *junkGenerator) generate(b []byte) []byte {
rndBig, _ := rand.Int(rand.Reader, p.rang)
n := int(rndBig.Int64()) + p.offset
junk := b[:n]
rand.Read(junk)
return junk
}
func NewPreludeUDPConn(
conn UDPConn,
origin UDPConn,
pool *sync.Pool,
header *RangedHeader,
opts PreludeOpts,
) (c *PreludeUDPConn, ok bool) {
if opts.IsEmpty() {
return nil, false
}
if opts.Jmin > opts.Jmax {
opts.Jmin, opts.Jmax = opts.Jmax, opts.Jmin
}
return &PreludeUDPConn{
UDPConn: conn,
origin: origin,
pool: WrapBufferPool(pool),
rulesArr: opts.RulesArr,
junkCount: opts.Jc,
junkGen: newJunkGenerator(opts.Jmin, opts.Jmax),
header: header,
}, true
}
type PreludeUDPConn struct {
UDPConn
origin UDPConn
pool *BufferPool
rulesArr [5]Rules
junkCount int
junkGen *junkGenerator
header *RangedHeader
}
func (c *PreludeUDPConn) WriteMsgUDP(b, oob []byte, addr *net.UDPAddr) (n, oobn int, err error) {
var isInit bool
if len(b) >= 4 {
typ := binary.LittleEndian.Uint32(b[:4])
if c.header != nil {
isInit = c.header.Validate(typ)
} else {
isInit = typ == WireguardMsgInitiationType
}
}
if isInit {
b := c.pool.Get()
defer c.pool.Put(b)
ctx := &writeContext{
FlexBuffer: WrapFlexBuffer(nil),
BufferPool: c.pool,
}
for _, rules := range c.rulesArr {
if rules == nil {
continue
}
w := bytes.NewBuffer(b[:0])
if err = rules.Write(w, ctx); err != nil {
return 0, 0, err
}
if _, _, err = c.origin.WriteMsgUDP(w.Bytes(), oob, addr); err != nil {
return 0, 0, err
}
}
for range c.junkCount {
junk := c.junkGen.generate(b)
if _, _, err = c.origin.WriteMsgUDP(junk, oob, addr); err != nil {
return 0, 0, err
}
}
}
return c.UDPConn.WriteMsgUDP(b, oob, addr)
}
func NewPreludeConn(
conn StreamRecordConn,
pool *sync.Pool,
framedOpts FramedOpts,
opts PreludeOpts,
) (c *PreludeConn, ok bool) {
if !opts.HasDecoyRules() || !conn.CanReadRecord() || !conn.CanWriteRecord() {
return nil, false
}
enc, _ := newFrameEncoding(framedOpts)
return &PreludeConn{
StreamRecordConn: conn,
pool: WrapBufferPool(pool),
rulesArr: opts.RulesArr,
recordEncoding: enc,
}, true
}
type PreludeConn struct {
StreamRecordConn
pool *BufferPool
rulesArr [5]Rules
recordEncoding frameEncoding
seenValid bool
}
func (c *PreludeConn) Read(b []byte) (n int, err error) {
if c.seenValid {
return c.StreamRecordConn.ReadRecord(b)
}
for {
n, err = c.StreamRecordConn.ReadRecord(b)
if err != nil {
return 0, err
}
if c.recordEncoding.IsValidRecord(b[:n]) {
c.seenValid = true
return n, nil
}
}
}
func (c *PreludeConn) Write(b []byte) (n int, err error) {
if c.recordEncoding.IsInitiationRecord(b) {
if err := c.writePreludeRecords(); err != nil {
return 0, err
}
}
return c.StreamRecordConn.WriteRecord(b)
}
func (c *PreludeConn) writePreludeRecords() (err error) {
buf := c.pool.Get()
defer c.pool.Put(buf)
ctx := &writeContext{
FlexBuffer: WrapFlexBuffer(nil),
BufferPool: c.pool,
}
for _, rules := range c.rulesArr {
if rules == nil {
continue
}
w := bytes.NewBuffer(buf[:0])
if err = rules.Write(w, ctx); err != nil {
return err
}
if _, err = c.StreamRecordConn.WriteRecord(w.Bytes()); err != nil {
return err
}
}
return nil
}
func NewPreludeBatchConn(
conn BatchConn,
origin BatchConn,
bufPool *sync.Pool,
msgsPool *sync.Pool,
header *RangedHeader,
opts PreludeOpts,
) (c *PreludeBatchConn, ok bool) {
if opts.IsEmpty() {
return nil, false
}
if opts.Jmin > opts.Jmax {
opts.Jmin, opts.Jmax = opts.Jmax, opts.Jmin
}
return &PreludeBatchConn{
BatchConn: conn,
origin: origin,
bufPool: WrapBufferPool(bufPool),
msgsPool: msgsPool,
rulesArr: opts.RulesArr,
junkCount: opts.Jc,
junkGen: newJunkGenerator(opts.Jmin, opts.Jmax),
header: header,
}, true
}
type PreludeBatchConn struct {
BatchConn
origin BatchConn
bufPool *BufferPool
msgsPool *sync.Pool
rulesArr [5]Rules
junkCount int
junkGen *junkGenerator
header *RangedHeader
}
func (c *PreludeBatchConn) WriteBatch(ms []ipv4.Message, flags int) (n int, err error) {
var initMsg *ipv4.Message
for i := range ms {
b := ms[i].Buffers[0]
var isInit bool
if len(b) >= 4 {
typ := binary.LittleEndian.Uint32(b[:4])
if c.header != nil {
isInit = c.header.Validate(typ)
} else {
isInit = typ == WireguardMsgInitiationType
}
}
if isInit {
initMsg = &ms[i]
}
}
if initMsg != nil {
ctx := &writeContext{
FlexBuffer: WrapFlexBuffer(nil),
BufferPool: c.bufPool,
}
msgs := c.msgsPool.Get().(*[]ipv4.Message)
defer c.msgsPool.Put(msgs)
i := 0
for _, rules := range c.rulesArr {
if rules == nil {
continue
}
buf := c.bufPool.Get()
defer c.bufPool.Put(buf)
w := bytes.NewBuffer(buf[:0])
if err = rules.Write(w, ctx); err != nil {
return 0, err
}
(*msgs)[i].Buffers[0] = w.Bytes()
(*msgs)[i].OOB = initMsg.OOB
(*msgs)[i].Addr = initMsg.Addr
i++
}
for range c.junkCount {
buf := c.bufPool.Get()
defer c.bufPool.Put(buf)
(*msgs)[i].Buffers[0] = c.junkGen.generate(buf)
(*msgs)[i].OOB = initMsg.OOB
(*msgs)[i].Addr = initMsg.Addr
i++
}
var start int
for {
m := (*msgs)[start:i]
n, err = c.origin.WriteBatch(m, flags)
if err != nil {
return 0, err
}
if n == len(m) {
break
}
start += n
}
}
return c.BatchConn.WriteBatch(ms, flags)
}
-13
View File
@@ -1,13 +0,0 @@
package conceal
const (
WireguardMsgInitiationType = 1
WireguardMsgResponseType = 2
WireguardMsgCookieReplyType = 3
WireguardMsgTransportType = 4
WireguardMsgInitiationSize = 148
WireguardMsgResponseSize = 92
WireguardMsgCookieReplySize = 64
WireguardMsgTransportMinSize = 32
)
-45
View File
@@ -1,45 +0,0 @@
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 ""
}
-63
View File
@@ -1,63 +0,0 @@
package conceal
import (
"crypto/rand"
"errors"
"fmt"
"math/big"
"strconv"
"strings"
)
type RangedHeader struct {
start uint32
end uint32
}
func NewRangedHeader(spec string) (*RangedHeader, error) {
parts := strings.Split(spec, "-")
if len(parts) < 1 || len(parts) > 2 {
return nil, errors.New("bad format")
}
start, err := strconv.ParseUint(parts[0], 10, 32)
if err != nil {
return nil, fmt.Errorf("failed to parse %s: %w", parts[0], err)
}
var end uint64
if len(parts) > 1 {
end, err = strconv.ParseUint(parts[1], 10, 32)
if err != nil {
return nil, fmt.Errorf("failed to parse %s: %w", parts[1], err)
}
} else {
end = start
}
if end < start {
return nil, errors.New("wrong range specified")
}
return &RangedHeader{
start: uint32(start),
end: uint32(end),
}, nil
}
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 *RangedHeader) Validate(val uint32) bool {
return h.start <= val && val <= h.end
}
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
@@ -1,63 +0,0 @@
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
@@ -1,16 +0,0 @@
package conceal
import "io"
func ReadUntil(r io.Reader, b []byte, delim byte) (n int, err error) {
for n < len(b) {
if _, err = r.Read(b[n : n+1]); err != nil {
return n, err
}
if b[n] == delim {
return n, nil
}
n++
}
return n, io.ErrShortBuffer
}
-470
View File
@@ -1,470 +0,0 @@
package conceal
import (
"bytes"
"crypto/rand"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"io"
"strconv"
"strings"
"time"
"unicode"
)
var (
errInvalidData = errors.New("invalid data")
)
type readContext struct {
*FlexBuffer
*BufferPool
nextDataSize int
}
type writeContext struct {
*FlexBuffer
*BufferPool
}
type Rule interface {
Spec() string
Write(w io.Writer, ctx *writeContext) error
Read(r io.Reader, ctx *readContext) error
}
type Rules []Rule
func (r Rules) Spec() string {
var builder strings.Builder
for _, rule := range r {
builder.WriteString(rule.Spec())
}
return builder.String()
}
func (r Rules) Write(w io.Writer, ctx *writeContext) error {
for _, rule := range r {
if err := rule.Write(w, ctx); err != nil {
return err
}
}
return nil
}
func (r Rules) Read(rd io.Reader, ctx *readContext) error {
for _, rule := range r {
if err := rule.Read(rd, ctx); err != nil {
return err
}
}
return nil
}
func buildBytesRule(val string) (Rule, error) {
val = strings.TrimPrefix(val, "0x")
if len(val) == 0 {
return nil, errors.New("empty argument")
}
if len(val)%2 != 0 {
return nil, errors.New("odd amount of symbols")
}
bytes, err := hex.DecodeString(val)
if err != nil {
return nil, err
}
return &bytesRule{data: bytes}, nil
}
type bytesRule struct {
data []byte
}
func (r *bytesRule) Spec() string {
return fmt.Sprintf("<b 0x%x>", r.data)
}
func (r *bytesRule) Write(w io.Writer, ctx *writeContext) error {
_, err := w.Write(r.data)
return err
}
func (r *bytesRule) Read(rd io.Reader, ctx *readContext) error {
tmp := ctx.Get()
defer ctx.Put(tmp)
buf := tmp[:len(r.data)]
if _, err := io.ReadFull(rd, buf); err != nil {
return err
}
if !bytes.Equal(buf, r.data) {
return errInvalidData
}
return nil
}
func buildRandRule(val string) (Rule, error) {
length, err := strconv.Atoi(val)
if err != nil {
return nil, err
}
return &randRule{length: length}, nil
}
type randRule struct {
length int
}
func (r *randRule) Spec() string {
return fmt.Sprintf("<r %d>", r.length)
}
func (r *randRule) Write(w io.Writer, ctx *writeContext) error {
tmp := ctx.Get()
defer ctx.Put(tmp)
buf := tmp[:r.length]
rand.Read(buf)
_, err := w.Write(buf)
return err
}
func (r *randRule) Read(rd io.Reader, ctx *readContext) error {
tmp := ctx.Get()
defer ctx.Put(tmp)
buf := tmp[:r.length]
if _, err := io.ReadFull(rd, buf); err != nil {
return err
}
// I guess, there is no way to validate randomness
// so just return nil here like everything is fine
return nil
}
func buildRandDigitsRule(val string) (Rule, error) {
length, err := strconv.Atoi(val)
if err != nil {
return nil, err
}
return &randDigitRule{length: length}, nil
}
type randDigitRule struct {
length int
}
func (r *randDigitRule) Spec() string {
return fmt.Sprintf("<rd %d>", r.length)
}
const digits10 = "0123456789"
func (r *randDigitRule) Write(w io.Writer, ctx *writeContext) error {
tmp := ctx.Get()
defer ctx.Put(tmp)
buf := tmp[:r.length]
rand.Read(buf)
for i := range buf {
buf[i] = digits10[buf[i]%10]
}
_, err := w.Write(buf)
return err
}
func (r *randDigitRule) Read(rd io.Reader, ctx *readContext) error {
tmp := ctx.Get()
defer ctx.Put(tmp)
buf := tmp[:r.length]
if _, err := io.ReadFull(rd, buf); err != nil {
return err
}
for _, b := range buf {
if !unicode.IsDigit(rune(b)) {
return errInvalidData
}
}
return nil
}
func buildRandCharRule(val string) (Rule, error) {
length, err := strconv.Atoi(val)
if err != nil {
return nil, err
}
return &randCharRule{length: length}, nil
}
type randCharRule struct {
length int
}
func (r *randCharRule) Spec() string {
return fmt.Sprintf("<rc %d>", r.length)
}
const chars52 = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
func (r *randCharRule) Write(w io.Writer, ctx *writeContext) error {
tmp := ctx.Get()
defer ctx.Put(tmp)
buf := tmp[:r.length]
rand.Read(buf)
for i := range buf {
buf[i] = chars52[buf[i]%52]
}
_, err := w.Write(buf)
return err
}
func (r *randCharRule) Read(rd io.Reader, ctx *readContext) error {
tmp := ctx.Get()
defer ctx.Put(tmp)
buf := tmp[:r.length]
if _, err := io.ReadFull(rd, buf); err != nil {
return err
}
for _, b := range buf {
if !unicode.IsLetter(rune(b)) {
return errInvalidData
}
}
return nil
}
func buildTimestampRule(_ string) (Rule, error) {
return &timestampRule{}, nil
}
type timestampRule struct{}
func (r *timestampRule) Spec() string {
return "<t>"
}
func (r *timestampRule) Write(w io.Writer, ctx *writeContext) error {
timestamp := uint32(time.Now().Unix())
return binary.Write(w, binary.BigEndian, timestamp)
}
func (r *timestampRule) Read(rd io.Reader, ctx *readContext) error {
var timestamp uint32
if err := binary.Read(rd, binary.BigEndian, &timestamp); err != nil {
return err
}
// TODO: check timestamp?
return nil
}
func buildDataSizeRule(val string) (Rule, error) {
var (
length int = 2
format NumFormat = NumFormatBE
end byte = 0
err error
)
parts := strings.Fields(val)
if len(parts) != 2 {
return nil, errors.New("wrong amount of arguments")
}
if format, err = buildNumFormat(parts[0]); err != nil {
return nil, err
}
switch format {
case NumFormatAscii, NumFormatHex:
parts[1] = strings.TrimPrefix(parts[1], "0x")
var bytes []byte
bytes, err = hex.DecodeString(parts[1])
if err != nil {
return nil, err
}
if len(bytes) != 1 {
return nil, errors.New("too many bytes")
}
end = bytes[0]
default:
if length, err = strconv.Atoi(parts[1]); err != nil {
return nil, err
}
}
return &dataSizeRule{
length: length,
format: format,
end: end,
}, nil
}
type dataSizeRule struct {
format NumFormat
length int
end byte
}
func (r *dataSizeRule) Spec() string {
switch r.format {
case NumFormatAscii, NumFormatHex:
return fmt.Sprintf("<dz %s 0x%02x>", r.format.Spec(), r.end)
}
return fmt.Sprintf("<dz %s %d>", r.format.Spec(), r.length)
}
func (r *dataSizeRule) Write(w io.Writer, ctx *writeContext) error {
tmp := ctx.Get()
defer ctx.Put(tmp)
size := int32(ctx.Cap())
switch r.format {
case NumFormatBE:
for i := r.length - 1; i >= 0; i-- {
tmp[i] = byte(size & 0xFF)
size >>= 8
}
if _, err := w.Write(tmp[:r.length]); err != nil {
return err
}
case NumFormatLE:
for i := range r.length {
tmp[i] = byte(size & 0xFF)
size >>= 8
}
if _, err := w.Write(tmp[:r.length]); err != nil {
return err
}
case NumFormatAscii:
b := strconv.AppendInt(tmp[:0], int64(size), 10)
b = append(b, r.end)
if _, err := w.Write(b); err != nil {
return err
}
case NumFormatHex:
b := strconv.AppendInt(tmp[:0], int64(size), 16)
b = append(b, r.end)
if _, err := w.Write(b); err != nil {
return err
}
}
return nil
}
func (r *dataSizeRule) Read(rd io.Reader, ctx *readContext) error {
tmp := ctx.Get()
defer ctx.Put(tmp)
switch r.format {
case NumFormatBE:
buf := tmp[:r.length]
if _, err := io.ReadFull(rd, buf); err != nil {
return err
}
var size int
for i := range buf {
size <<= 8
size |= int(buf[i])
}
ctx.nextDataSize = size
case NumFormatLE:
buf := tmp[:r.length]
if _, err := io.ReadFull(rd, buf); err != nil {
return err
}
var size int
for i := len(buf) - 1; i >= 0; i-- {
size <<= 8
size |= int(buf[i])
}
ctx.nextDataSize = size
case NumFormatAscii:
n, err := ReadUntil(rd, tmp, r.end)
if err != nil {
return err
}
size64, err := strconv.ParseInt(string(tmp[:n]), 10, 32)
if err != nil {
return err
}
ctx.nextDataSize = int(size64)
case NumFormatHex:
n, err := ReadUntil(rd, tmp, r.end)
if err != nil {
return err
}
size64, err := strconv.ParseInt(string(tmp[:n]), 16, 32)
if err != nil {
return err
}
ctx.nextDataSize = int(size64)
}
return nil
}
func buildDataRule(val string) (Rule, error) {
return &dataRule{}, nil
}
type dataRule struct{}
func (r *dataRule) Spec() string {
return "<d>"
}
func (r *dataRule) Write(w io.Writer, ctx *writeContext) error {
buf := ctx.PullHead(-1)
if buf == nil {
return io.ErrShortBuffer
}
_, err := w.Write(buf)
return err
}
func (r *dataRule) Read(rd io.Reader, ctx *readContext) error {
buf := ctx.PushTail(ctx.nextDataSize)
if buf == nil {
return io.ErrShortBuffer
}
_, err := io.ReadFull(rd, buf)
return err
}
-19
View File
@@ -1,19 +0,0 @@
package conceal
import (
"errors"
"net"
)
var (
ErrNoReadRecord = errors.New("stream record reader unavailable")
ErrNoWriteRecord = errors.New("stream record writer unavailable")
)
type StreamRecordConn interface {
net.Conn
CanReadRecord() bool
CanWriteRecord() bool
ReadRecord(b []byte) (int, error)
WriteRecord(b []byte) (int, error)
}
-622
View File
@@ -1,622 +0,0 @@
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
}
+12 -31
View File
@@ -16,16 +16,12 @@ import (
"sync"
"syscall"
"github.com/amnezia-vpn/amneziawg-go/conceal"
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
)
var (
_ Bind = (*StdNetBind)(nil)
_ Framable = (*StdNetBind)(nil)
_ Preludable = (*StdNetBind)(nil)
_ Masqueradable = (*StdNetBind)(nil)
_ Bind = (*StdNetBind)(nil)
)
// StdNetBind implements Bind for all platforms. While Windows has its own Bind
@@ -35,10 +31,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 UDPConn
ipv6 UDPConn
ipv4PC LinuxPacketConn // will be nil on non-Linux
ipv6PC LinuxPacketConn // will be nil on non-Linux
ipv4 *net.UDPConn
ipv6 *net.UDPConn
ipv4PC *ipv4.PacketConn // will be nil on non-Linux
ipv6PC *ipv6.PacketConn // will be nil on non-Linux
ipv4TxOffload bool
ipv4RxOffload bool
ipv6TxOffload bool
@@ -47,14 +43,9 @@ type StdNetBind struct {
// these two fields are not guarded by mu
udpAddrPool sync.Pool
msgsPool sync.Pool
bufPool sync.Pool
blackhole4 bool
blackhole6 bool
framedOpts conceal.FramedOpts
preludeOpts conceal.PreludeOpts
masqueradeOpts conceal.MasqueradeOpts
}
func NewStdNetBind() Bind {
@@ -79,12 +70,6 @@ func NewStdNetBind() Bind {
return &msgs
},
},
bufPool: sync.Pool{
New: func() any {
return make([]byte, 65535)
},
},
}
}
@@ -167,9 +152,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 UDPConn
var v4pc LinuxPacketConn
var v6pc LinuxPacketConn
var v4conn, v6conn *net.UDPConn
var v4pc *ipv4.PacketConn
var v6pc *ipv6.PacketConn
v4conn, port, err = listenNet("udp4", port)
if err != nil && !errors.Is(err, syscall.EAFNOSUPPORT) {
@@ -192,10 +177,8 @@ 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
}
@@ -203,10 +186,8 @@ 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
}
@@ -244,7 +225,7 @@ type batchWriter interface {
func (s *StdNetBind) receiveIP(
br batchReader,
conn UDPConn,
conn *net.UDPConn,
rxOffload bool,
bufs [][]byte,
sizes []int,
@@ -296,13 +277,13 @@ func (s *StdNetBind) receiveIP(
return numMsgs, nil
}
func (s *StdNetBind) makeReceiveIPv4(pc LinuxPacketConn, conn UDPConn, rxOffload bool) ReceiveFunc {
func (s *StdNetBind) makeReceiveIPv4(pc *ipv4.PacketConn, conn *net.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 LinuxPacketConn, conn UDPConn, rxOffload bool) ReceiveFunc {
func (s *StdNetBind) makeReceiveIPv6(pc *ipv6.PacketConn, conn *net.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)
}
@@ -428,7 +409,7 @@ retry:
return err
}
func (s *StdNetBind) send(conn UDPConn, pc batchWriter, msgs []ipv6.Message) error {
func (s *StdNetBind) send(conn *net.UDPConn, pc batchWriter, msgs []ipv6.Message) error {
var (
n int
err error
-297
View File
@@ -1,297 +0,0 @@
package conn
import (
"context"
"fmt"
"io"
"net"
"net/netip"
"strconv"
"sync"
"github.com/amnezia-vpn/amneziawg-go/conceal"
)
var (
_ Bind = (*BindStream)(nil)
_ Framable = (*BindStream)(nil)
_ Preludable = (*BindStream)(nil)
_ Masqueradable = (*BindStream)(nil)
)
type streamPacketQueue struct {
ep *streamEndpoint
buf [65535]byte
n int
}
func NewBindStream() *BindStream {
return &BindStream{
streamPacketPool: sync.Pool{
New: func() any {
return new(streamPacketQueue)
},
},
bufferPool: sync.Pool{
New: func() any {
return make([]byte, 65535)
},
},
}
}
type BindStream struct {
queue chan *streamPacketQueue
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
streamPacketPool sync.Pool
bufferPool sync.Pool
dialer net.Dialer
listenConfig net.ListenConfig
port uint16
framedOpts conceal.FramedOpts
preludeOpts conceal.PreludeOpts
masqueradeOpts conceal.MasqueradeOpts
}
func (b *BindStream) readFaucet() ReceiveFunc {
return func(packets [][]byte, sizes []int, eps []Endpoint) (n int, err error) {
streamPacket, ok := <-b.queue
if !ok {
return 0, net.ErrClosed
}
packet := streamPacket.buf[:streamPacket.n]
copy(packets[0], packet)
sizes[0] = streamPacket.n
eps[0] = streamPacket.ep
b.streamPacketPool.Put(streamPacket)
return 1, nil
}
}
func (b *BindStream) readStream(ep *streamEndpoint) {
defer b.wg.Done()
for {
sp := b.streamPacketPool.Get().(*streamPacketQueue)
n, err := ep.conn.Read(sp.buf[:])
if err != nil {
ep.Close()
return
}
sp.ep, sp.n = ep, n
b.queue <- sp
}
}
func (b *BindStream) accept(listener net.Listener) {
defer b.wg.Done()
for {
select {
case <-b.ctx.Done():
return
default:
}
conn, err := listener.Accept()
if err != nil {
// log this error somewhere
break
}
conn = b.upgradeConn(conn)
b.wg.Add(1)
go b.handleAccepted(conn)
}
}
func (b *BindStream) handleAccepted(conn net.Conn) {
defer b.wg.Done()
ap, err := netip.ParseAddrPort(conn.RemoteAddr().String())
if err != nil {
// add logs
}
ep := &streamEndpoint{
conn: conn,
dst: ap,
}
b.wg.Add(1)
go b.readStream(ep)
<-b.ctx.Done()
ep.Close()
}
func (b *BindStream) dial(ep *streamEndpoint) error {
ep.mutex.Lock()
defer ep.mutex.Unlock()
if ep.conn != nil {
return nil
}
conn, err := b.dialer.DialContext(b.ctx, "tcp", ep.DstToString())
if err != nil {
return fmt.Errorf("failed to dial context: %v", err)
}
conn = b.upgradeConn(conn)
ep.conn = conn
b.wg.Add(1)
go func() {
defer b.wg.Done()
<-b.ctx.Done()
ep.Close()
}()
b.wg.Add(1)
go b.readStream(ep)
return nil
}
func (b *BindStream) openConnections() error {
b.ctx, b.cancel = context.WithCancel(context.Background())
if b.port != 0 {
listener, err := b.listenConfig.Listen(b.ctx, "tcp", ":"+strconv.Itoa(int(b.port)))
if err != nil {
return err
}
if addr, ok := listener.Addr().(*net.TCPAddr); ok {
b.port = uint16(addr.Port)
}
b.wg.Add(1)
go func() {
defer b.wg.Done()
<-b.ctx.Done()
listener.Close()
}()
b.wg.Add(1)
go b.accept(listener)
}
return nil
}
func (b *BindStream) Open(port uint16) (fns []ReceiveFunc, actualPort uint16, err error) {
b.queue = make(chan *streamPacketQueue, 1024)
b.port = port
err = b.openConnections()
return []ReceiveFunc{b.readFaucet()}, b.port, err
}
func (b *BindStream) Send(bufs [][]byte, ep Endpoint) error {
streamEp, ok := ep.(*streamEndpoint)
if !ok {
return nil
}
select {
case <-b.ctx.Done():
return io.ErrClosedPipe
default:
}
if err := b.dial(streamEp); err != nil {
return err
}
for _, buf := range bufs {
if _, err := streamEp.conn.Write(buf); err != nil {
streamEp.Close()
return err
}
}
return nil
}
func (b *BindStream) closeConnections() {
if b.cancel != nil {
b.cancel()
}
b.wg.Wait()
}
func (b *BindStream) Close() error {
b.closeConnections()
if b.queue != nil {
close(b.queue)
}
return nil
}
func (b *BindStream) ParseEndpoint(s string) (Endpoint, error) {
tcpAddr, err := net.ResolveTCPAddr("tcp", s)
if err != nil {
return nil, fmt.Errorf("failed to resolve addr: %v", err)
}
return &streamEndpoint{
dst: tcpAddr.AddrPort(),
}, nil
}
func (b *BindStream) BatchSize() int {
return 1
}
var _ Endpoint = (*streamEndpoint)(nil)
type streamEndpoint struct {
conn net.Conn
dst netip.AddrPort
mutex sync.Mutex
}
func (e *streamEndpoint) Close() {
e.mutex.Lock()
defer e.mutex.Unlock()
if e.conn != nil {
e.conn.Close()
e.conn = nil
}
}
func (e *streamEndpoint) DstToString() string {
return e.dst.String()
}
func (e *streamEndpoint) DstToBytes() []byte {
b, _ := e.dst.MarshalBinary()
return b
}
func (e *streamEndpoint) DstIP() netip.Addr {
return e.dst.Addr()
}
func (e *streamEndpoint) ClearSrc() {
}
func (e *streamEndpoint) SrcToString() string {
return ""
}
func (e *streamEndpoint) SrcIP() netip.Addr {
return netip.Addr{}
}
+21 -1
View File
@@ -17,7 +17,7 @@ import (
"golang.org/x/sys/windows"
"github.com/amnezia-vpn/amneziawg-go/conn/winrio"
"github.com/amnezia-vpn/amneziawg-go/v3/conn/winrio"
)
const (
@@ -77,6 +77,8 @@ type WinRingBind struct {
isOpen atomic.Uint32 // 0, 1, or 2
}
func NewDefaultBind() Bind { return NewWinRingBind() }
func NewWinRingBind() Bind {
if !winrio.Initialize() {
return NewStdNetBind()
@@ -580,6 +582,24 @@ func (bind *WinRingBind) BindSocketToInterface6(interfaceIndex uint32, blackhole
return nil
}
func (bind *WinRingBind) PeekLookAtSocketFd4() (fd int, err error) {
bind.mu.RLock()
defer bind.mu.RUnlock()
if bind.isOpen.Load() != 1 {
return -1, net.ErrClosed
}
return int(bind.v4.sock), nil
}
func (bind *WinRingBind) PeekLookAtSocketFd6() (fd int, err error) {
bind.mu.RLock()
defer bind.mu.RUnlock()
if bind.isOpen.Load() != 1 {
return -1, net.ErrClosed
}
return int(bind.v6.sock), nil
}
func bindSocketToInterface4(handle windows.Handle, interfaceIndex uint32) error {
const IP_UNICAST_IF = 31
/* MSDN says for IPv4 this needs to be in net byte order, so that it's like an IP address with leading zeros. */
+1 -1
View File
@@ -12,7 +12,7 @@ import (
"net/netip"
"os"
"github.com/amnezia-vpn/amneziawg-go/conn"
"github.com/amnezia-vpn/amneziawg-go/v3/conn"
)
type ChannelBind struct {
File renamed without changes.
-180
View File
@@ -1,180 +0,0 @@
package conn
import (
"net"
"github.com/amnezia-vpn/amneziawg-go/conceal"
)
type Framable interface {
SetFramedOpts(opts conceal.FramedOpts)
}
type Preludable interface {
SetPreludeOpts(opts conceal.PreludeOpts)
}
type Masqueradable interface {
SetMasqueradeOpts(opts conceal.MasqueradeOpts)
}
type concealStage string
const (
concealStageMasquerade concealStage = "masquerade"
concealStageRecord concealStage = "record"
concealStageFramed concealStage = "framed"
concealStagePrelude concealStage = "prelude"
)
type concealPipeline struct {
stages []concealStage
}
func (p concealPipeline) names() []string {
names := make([]string, 0, len(p.stages))
for _, stage := range p.stages {
names = append(names, string(stage))
}
return names
}
func hasFramed(opts conceal.FramedOpts) bool {
return opts.H1 != nil || opts.H2 != nil || opts.H3 != nil || opts.H4 != nil ||
opts.S1 != 0 || opts.S2 != 0 || opts.S3 != 0 || opts.S4 != 0
}
func hasMasquerade(opts conceal.MasqueradeOpts) bool {
return opts.RulesIn != nil || opts.RulesOut != nil
}
func hasBidirectionalStreamRecords(opts conceal.MasqueradeOpts) bool {
return opts.RulesIn != nil && opts.RulesOut != nil
}
func udpPreludeHeader(opts conceal.FramedOpts) *conceal.RangedHeader {
if opts.HeaderCompat {
return opts.H1
}
return nil
}
func (b *StdNetBind) udpConcealPipeline() concealPipeline {
stages := make([]concealStage, 0, 3)
if hasMasquerade(b.masqueradeOpts) {
stages = append(stages, concealStageMasquerade)
}
if hasFramed(b.framedOpts) {
stages = append(stages, concealStageFramed)
}
if !b.preludeOpts.IsEmpty() {
stages = append(stages, concealStagePrelude)
}
return concealPipeline{stages: stages}
}
func (b *StdNetBind) batchConcealPipeline() concealPipeline {
return b.udpConcealPipeline()
}
func (b *BindStream) streamConcealPipeline() concealPipeline {
stages := make([]concealStage, 0, 3)
if hasMasquerade(b.masqueradeOpts) {
stages = append(stages, concealStageRecord)
}
if b.preludeOpts.HasDecoyRules() && hasBidirectionalStreamRecords(b.masqueradeOpts) {
stages = append(stages, concealStagePrelude)
}
if hasFramed(b.framedOpts) {
stages = append(stages, concealStageFramed)
}
return concealPipeline{stages: stages}
}
func (b *StdNetBind) upgradeUDPConn(conn UDPConn) UDPConn {
origin := conn
for _, stage := range b.udpConcealPipeline().stages {
switch stage {
case concealStageMasquerade:
if masquerade, ok := conceal.NewMasqueradeUDPConn(conn, &b.bufPool, b.masqueradeOpts); ok {
conn = masquerade
}
case concealStageFramed:
if framed, ok := conceal.NewFramedUDPConn(conn, &b.bufPool, b.framedOpts); ok {
conn = framed
}
case concealStagePrelude:
if prelude, ok := conceal.NewPreludeUDPConn(conn, origin, &b.bufPool, udpPreludeHeader(b.framedOpts), b.preludeOpts); ok {
conn = prelude
}
}
}
return conn
}
func (b *StdNetBind) upgradePacketConn(conn LinuxPacketConn) LinuxPacketConn {
origin := conn
for _, stage := range b.batchConcealPipeline().stages {
switch stage {
case concealStageMasquerade:
if masquerade, ok := conceal.NewMasqueradeBatchConn(conn, &b.bufPool, b.masqueradeOpts); ok {
conn = masquerade
}
case concealStageFramed:
if framed, ok := conceal.NewFramedBatchConn(conn, &b.bufPool, b.framedOpts); ok {
conn = framed
}
case concealStagePrelude:
if prelude, ok := conceal.NewPreludeBatchConn(conn, origin, &b.bufPool, &b.msgsPool, udpPreludeHeader(b.framedOpts), b.preludeOpts); ok {
conn = prelude
}
}
}
return conn
}
func (b *StdNetBind) SetFramedOpts(opts conceal.FramedOpts) {
b.framedOpts = opts
}
func (b *StdNetBind) SetPreludeOpts(opts conceal.PreludeOpts) {
b.preludeOpts = opts
}
func (b *StdNetBind) SetMasqueradeOpts(opts conceal.MasqueradeOpts) {
b.masqueradeOpts = opts
}
func (b *BindStream) upgradeConn(conn net.Conn) net.Conn {
var recordConn conceal.StreamRecordConn
for _, stage := range b.streamConcealPipeline().stages {
switch stage {
case concealStageRecord:
if masquerade, ok := conceal.NewMasqueradeConn(conn, &b.bufferPool, b.masqueradeOpts); ok {
recordConn = masquerade
conn = masquerade
}
case concealStagePrelude:
if prelude, ok := conceal.NewPreludeConn(recordConn, &b.bufferPool, b.framedOpts, b.preludeOpts); ok {
conn = prelude
}
case concealStageFramed:
if framed, ok := conceal.NewFramedConn(conn, &b.bufferPool, b.framedOpts); ok {
conn = framed
}
}
}
return conn
}
func (b *BindStream) SetFramedOpts(opts conceal.FramedOpts) {
b.framedOpts = opts
}
func (b *BindStream) SetPreludeOpts(opts conceal.PreludeOpts) {
b.preludeOpts = opts
}
func (b *BindStream) SetMasqueradeOpts(opts conceal.MasqueradeOpts) {
b.masqueradeOpts = opts
}
-455
View File
@@ -1,455 +0,0 @@
package conn
import (
"bytes"
"encoding/binary"
"net"
"slices"
"sync"
"syscall"
"testing"
"time"
"github.com/amnezia-vpn/amneziawg-go/conceal"
)
func TestStdNetBindUDPPipelineOrder(t *testing.T) {
bind := NewStdNetBind().(*StdNetBind)
bind.SetMasqueradeOpts(conceal.MasqueradeOpts{
RulesIn: mustParseRules(t, "<dz be 2><d>"),
RulesOut: mustParseRules(t, "<dz be 2><d>"),
})
bind.SetFramedOpts(conceal.FramedOpts{H1: mustHeader(t, "777")})
bind.SetPreludeOpts(conceal.PreludeOpts{
RulesArr: [5]conceal.Rules{mustParseRules(t, "<b 0xaabb>")},
})
got := bind.udpConcealPipeline().names()
want := []string{"masquerade", "framed", "prelude"}
if !slices.Equal(got, want) {
t.Fatalf("udp pipeline = %v, want %v", got, want)
}
}
func TestStdNetBindUDPPreludeStillEmitsJunk(t *testing.T) {
bind := NewStdNetBind().(*StdNetBind)
bind.SetPreludeOpts(conceal.PreludeOpts{
Jc: 1,
Jmin: 3,
Jmax: 3,
RulesArr: [5]conceal.Rules{
mustParseRules(t, "<b 0xaabb>"),
},
})
conn := &recordingUDPConn{}
upgraded := bind.upgradeUDPConn(conn)
initiation := makeInitiationPacket()
if _, _, err := upgraded.WriteMsgUDP(initiation, nil, &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 51820}); err != nil {
t.Fatalf("udp write failed: %v", err)
}
if len(conn.writes) != 3 {
t.Fatalf("udp write count = %d, want 3", len(conn.writes))
}
if !bytes.Equal(conn.writes[0], []byte{0xaa, 0xbb}) {
t.Fatalf("udp decoy record = %x, want aabb", conn.writes[0])
}
if len(conn.writes[1]) != 3 {
t.Fatalf("udp junk length = %d, want 3", len(conn.writes[1]))
}
if !bytes.Equal(conn.writes[2], initiation) {
t.Fatalf("udp initiation payload changed")
}
}
func TestBindStreamPipelineUsesRecordLayer(t *testing.T) {
bind := newRecordAwareBind(t)
got := bind.streamConcealPipeline().names()
want := []string{"record", "prelude", "framed"}
if !slices.Equal(got, want) {
t.Fatalf("stream pipeline = %v, want %v", got, want)
}
}
func TestBindStreamPipelineOmitsPreludeWithoutBidirectionalRecords(t *testing.T) {
bind := NewBindStream()
bind.SetMasqueradeOpts(conceal.MasqueradeOpts{
RulesOut: mustParseRules(t, "<dz be 2><d>"),
})
bind.SetFramedOpts(conceal.FramedOpts{H1: mustHeader(t, "777")})
bind.SetPreludeOpts(conceal.PreludeOpts{
RulesArr: [5]conceal.Rules{mustParseRules(t, "<b 0xaabb>")},
})
got := bind.streamConcealPipeline().names()
want := []string{"record", "framed"}
if !slices.Equal(got, want) {
t.Fatalf("stream pipeline without bidirectional records = %v, want %v", got, want)
}
}
func TestBindStreamPipelineOmitsPreludeForTCPJunkOnlyConfig(t *testing.T) {
bind := NewBindStream()
bind.SetMasqueradeOpts(conceal.MasqueradeOpts{
RulesIn: mustParseRules(t, "<dz be 2><d>"),
RulesOut: mustParseRules(t, "<dz be 2><d>"),
})
bind.SetFramedOpts(conceal.FramedOpts{H1: mustHeader(t, "777")})
bind.SetPreludeOpts(conceal.PreludeOpts{
Jc: 1,
Jmin: 2,
Jmax: 2,
})
got := bind.streamConcealPipeline().names()
want := []string{"record", "framed"}
if !slices.Equal(got, want) {
t.Fatalf("stream pipeline with tcp junk only = %v, want %v", got, want)
}
}
func TestBindStreamTCPPreludeInjectsBeforeEveryInitiation(t *testing.T) {
senderRaw, receiverRaw := net.Pipe()
defer senderRaw.Close()
defer receiverRaw.Close()
bind := newRecordAwareBind(t)
sender := bind.upgradeConn(senderRaw)
receiver := mustRecordConn(t, receiverRaw, &bind.bufferPool, conceal.MasqueradeOpts{
RulesIn: mustParseRules(t, "<dz be 2><d>"),
})
initiation := makeInitiationPacket()
writeErr := make(chan error, 1)
go func() {
_, err := sender.Write(initiation)
writeErr <- err
}()
prelude := readRecord(t, receiver, 16)
if !bytes.Equal(prelude, []byte{0xaa, 0xbb}) {
t.Fatalf("prelude record = %x, want aabb", prelude)
}
firstInit := readRecord(t, receiver, len(initiation))
if gotHeader := binary.LittleEndian.Uint32(firstInit[:4]); gotHeader != 777 {
t.Fatalf("first initiation header = %d, want 777", gotHeader)
}
if err := <-writeErr; err != nil {
t.Fatalf("first write failed: %v", err)
}
writeErr = make(chan error, 1)
go func() {
_, err := sender.Write(initiation)
writeErr <- err
}()
secondPrelude := readRecord(t, receiver, 16)
if !bytes.Equal(secondPrelude, []byte{0xaa, 0xbb}) {
t.Fatalf("second prelude record = %x, want aabb", secondPrelude)
}
secondInit := readRecord(t, receiver, len(initiation))
if gotHeader := binary.LittleEndian.Uint32(secondInit[:4]); gotHeader != 777 {
t.Fatalf("second initiation header = %d, want 777", gotHeader)
}
if err := <-writeErr; err != nil {
t.Fatalf("second write failed: %v", err)
}
}
func TestBindStreamTCPPreludeSkipsNonInitiationRecords(t *testing.T) {
senderRaw, receiverRaw := net.Pipe()
defer senderRaw.Close()
defer receiverRaw.Close()
bind := newRecordAwareBind(t)
sender := bind.upgradeConn(senderRaw)
receiver := mustRecordConn(t, receiverRaw, &bind.bufferPool, conceal.MasqueradeOpts{
RulesIn: mustParseRules(t, "<dz be 2><d>"),
})
transport := makeTransportPacket()
writeErr := make(chan error, 1)
go func() {
_, err := sender.Write(transport)
writeErr <- err
}()
gotTransport := readRecord(t, receiver, len(transport))
if gotHeader := binary.LittleEndian.Uint32(gotTransport[:4]); gotHeader != 779 {
t.Fatalf("transport header = %d, want 779", gotHeader)
}
if err := <-writeErr; err != nil {
t.Fatalf("transport write failed: %v", err)
}
if err := receiver.SetReadDeadline(time.Now().Add(50 * time.Millisecond)); err != nil {
t.Fatalf("set deadline: %v", err)
}
buf := make([]byte, len(transport))
if _, err := receiver.ReadRecord(buf); err == nil {
t.Fatal("expected no extra prelude records for transport write")
}
}
func TestBindStreamTCPPreludeDropsInjectedDecoysOnRead(t *testing.T) {
senderRaw, receiverRaw := net.Pipe()
defer senderRaw.Close()
defer receiverRaw.Close()
bind := newRecordAwareBind(t)
sender := bind.upgradeConn(senderRaw)
receiver := bind.upgradeConn(receiverRaw)
initiation := makeInitiationPacket()
writeErr := make(chan error, 1)
go func() {
_, err := sender.Write(initiation)
writeErr <- err
}()
got := readPacket(t, receiver, len(initiation))
if !bytes.Equal(got, initiation) {
t.Fatalf("initiation payload changed after dropping decoys")
}
if err := <-writeErr; err != nil {
t.Fatalf("write failed: %v", err)
}
}
func TestBindStreamTCPPreludeDropsLeadingInvalidRecords(t *testing.T) {
senderRaw, receiverRaw := net.Pipe()
defer senderRaw.Close()
defer receiverRaw.Close()
bind := newRecordAwareBind(t)
receiver := bind.upgradeConn(receiverRaw)
recordWriter := mustRecordConn(t, senderRaw, &bind.bufferPool, conceal.MasqueradeOpts{
RulesOut: mustParseRules(t, "<dz be 2><d>"),
})
framedWriter, ok := conceal.NewFramedConn(recordWriter, &bind.bufferPool, bind.framedOpts)
if !ok {
t.Fatal("expected framed writer")
}
initiation := makeInitiationPacket()
writeErr := make(chan error, 1)
go func() {
if _, err := recordWriter.WriteRecord([]byte{0xde, 0xad}); err != nil {
writeErr <- err
return
}
if _, err := recordWriter.WriteRecord([]byte{0xbe, 0xef, 0x01}); err != nil {
writeErr <- err
return
}
_, err := framedWriter.Write(initiation)
writeErr <- err
}()
got := readPacket(t, receiver, len(initiation))
if !bytes.Equal(got, initiation) {
t.Fatalf("read-side did not recover after invalid leading records")
}
if err := <-writeErr; err != nil {
t.Fatalf("write failed: %v", err)
}
}
func TestBindStreamTCPPreludePassesResponseAndTransportRecords(t *testing.T) {
senderRaw, receiverRaw := net.Pipe()
defer senderRaw.Close()
defer receiverRaw.Close()
bind := newRecordAwareBind(t)
sender := bind.upgradeConn(senderRaw)
receiver := bind.upgradeConn(receiverRaw)
response := makeResponsePacket()
sendAndAssertRoundTrip(t, sender, receiver, response)
transport := makeTransportPacket()
sendAndAssertRoundTrip(t, sender, receiver, transport)
}
func newRecordAwareBind(t *testing.T) *BindStream {
t.Helper()
rules := mustParseRules(t, "<dz be 2><d>")
bind := NewBindStream()
bind.SetMasqueradeOpts(conceal.MasqueradeOpts{
RulesIn: rules,
RulesOut: rules,
})
bind.SetFramedOpts(conceal.FramedOpts{
H1: mustHeader(t, "777"),
H2: mustHeader(t, "778"),
H4: mustHeader(t, "779"),
})
bind.SetPreludeOpts(conceal.PreludeOpts{
Jc: 1,
Jmin: 2,
Jmax: 2,
RulesArr: [5]conceal.Rules{
mustParseRules(t, "<b 0xaabb>"),
},
})
return bind
}
func sendAndAssertRoundTrip(t *testing.T, sender, receiver net.Conn, packet []byte) {
t.Helper()
writeErr := make(chan error, 1)
go func() {
_, err := sender.Write(packet)
writeErr <- err
}()
got := readPacket(t, receiver, len(packet))
if !bytes.Equal(got, packet) {
t.Fatalf("round-trip payload mismatch")
}
if err := <-writeErr; err != nil {
t.Fatalf("write failed: %v", err)
}
}
func mustRecordConn(t *testing.T, conn net.Conn, pool *sync.Pool, opts conceal.MasqueradeOpts) *conceal.MasqueradeConn {
t.Helper()
recordConn, ok := conceal.NewMasqueradeConn(conn, pool, opts)
if !ok {
t.Fatal("expected record connection")
}
return recordConn
}
func mustParseRules(t *testing.T, spec string) conceal.Rules {
t.Helper()
rules, err := conceal.ParseRules(spec)
if err != nil {
t.Fatalf("parse rules %q: %v", spec, err)
}
return rules
}
func mustHeader(t *testing.T, spec string) *conceal.RangedHeader {
t.Helper()
header, err := conceal.NewRangedHeader(spec)
if err != nil {
t.Fatalf("parse header %q: %v", spec, err)
}
return header
}
func makeInitiationPacket() []byte {
packet := make([]byte, conceal.WireguardMsgInitiationSize)
binary.LittleEndian.PutUint32(packet[:4], conceal.WireguardMsgInitiationType)
for i := 4; i < len(packet); i++ {
packet[i] = byte(i)
}
return packet
}
func makeResponsePacket() []byte {
packet := make([]byte, conceal.WireguardMsgResponseSize)
binary.LittleEndian.PutUint32(packet[:4], conceal.WireguardMsgResponseType)
for i := 4; i < len(packet); i++ {
packet[i] = byte(i)
}
return packet
}
func makeTransportPacket() []byte {
packet := make([]byte, conceal.WireguardMsgTransportMinSize)
binary.LittleEndian.PutUint32(packet[:4], conceal.WireguardMsgTransportType)
for i := 4; i < len(packet); i++ {
packet[i] = byte(i)
}
return packet
}
func readPacket(t *testing.T, conn net.Conn, size int) []byte {
t.Helper()
buf := make([]byte, size)
n, err := conn.Read(buf)
if err != nil {
t.Fatalf("read packet: %v", err)
}
return slices.Clone(buf[:n])
}
func readRecord(t *testing.T, conn interface{ ReadRecord([]byte) (int, error) }, size int) []byte {
t.Helper()
buf := make([]byte, size)
n, err := conn.ReadRecord(buf)
if err != nil {
t.Fatalf("read record: %v", err)
}
return slices.Clone(buf[:n])
}
type recordingUDPConn struct {
writes [][]byte
}
func (c *recordingUDPConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
return 0, nil, net.ErrClosed
}
func (c *recordingUDPConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
c.writes = append(c.writes, bytes.Clone(p))
return len(p), nil
}
func (c *recordingUDPConn) Close() error {
return nil
}
func (c *recordingUDPConn) LocalAddr() net.Addr {
return &net.UDPAddr{}
}
func (c *recordingUDPConn) SetDeadline(t time.Time) error {
return nil
}
func (c *recordingUDPConn) SetReadDeadline(t time.Time) error {
return nil
}
func (c *recordingUDPConn) SetWriteDeadline(t time.Time) error {
return nil
}
func (c *recordingUDPConn) SyscallConn() (syscall.RawConn, error) {
return nil, nil
}
func (c *recordingUDPConn) ReadMsgUDP(b, oob []byte) (n, oobn, flags int, addr *net.UDPAddr, err error) {
return 0, 0, 0, nil, net.ErrClosed
}
func (c *recordingUDPConn) WriteMsgUDP(b, oob []byte, addr *net.UDPAddr) (n, oobn int, err error) {
c.writes = append(c.writes, bytes.Clone(b))
return len(b), len(oob), nil
}
-16
View File
@@ -9,14 +9,10 @@ package conn
import (
"errors"
"fmt"
"net"
"net/netip"
"reflect"
"runtime"
"strings"
"syscall"
"golang.org/x/net/ipv4"
)
const (
@@ -88,18 +84,6 @@ 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")
+1 -3
View File
@@ -7,6 +7,4 @@
package conn
func NewDefaultBind() Bind {
return NewMultibind(NewStdNetBind(), NewBindStream())
}
func NewDefaultBind() Bind { return NewStdNetBind() }
-7
View File
@@ -1,7 +0,0 @@
//go:build windows
package conn
func NewDefaultBind() Bind {
return NewMultibind(NewWinRingBind(), NewBindStream())
}
+3 -1
View File
@@ -8,6 +8,8 @@
package conn
func supportsUDPOffload(_ UDPConn) (txOffload, rxOffload bool) {
import "net"
func supportsUDPOffload(_ *net.UDPConn) (txOffload, rxOffload bool) {
return
}
+3 -1
View File
@@ -6,10 +6,12 @@
package conn
import (
"net"
"golang.org/x/sys/unix"
)
func supportsUDPOffload(conn UDPConn) (txOffload, rxOffload bool) {
func supportsUDPOffload(conn *net.UDPConn) (txOffload, rxOffload bool) {
rc, err := conn.SyscallConn()
if err != nil {
return
-4
View File
@@ -10,7 +10,3 @@ package conn
func (s *StdNetBind) SetMark(mark uint32) error {
return nil
}
func (b *BindStream) SetMark(mark uint32) error {
return nil
}
-35
View File
@@ -9,7 +9,6 @@ package conn
import (
"runtime"
"syscall"
"golang.org/x/sys/unix"
)
@@ -64,37 +63,3 @@ 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
@@ -1,51 +0,0 @@
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"
}
+1 -1
View File
@@ -8,7 +8,7 @@ package device
import (
"errors"
"github.com/amnezia-vpn/amneziawg-go/conn"
"github.com/amnezia-vpn/amneziawg-go/v3/conn"
)
type DummyDatagram struct {
+1
View File
@@ -24,6 +24,7 @@ const (
CookieRefreshTime = time.Second * 120
HandshakeInitationRate = time.Second / 50
PaddingMultiple = 16
DefaultUdpWindow = 500
)
const (
+2 -2
View File
@@ -118,7 +118,7 @@ func (st *CookieChecker) CreateReply(
msg []byte,
recv uint32,
src []byte,
typ uint32,
msgType uint32,
) (*MessageCookieReply, error) {
st.RLock()
@@ -154,7 +154,7 @@ func (st *CookieChecker) CreateReply(
smac1 := smac2 - blake2s.Size128
reply := new(MessageCookieReply)
reply.Type = typ
reply.Type = msgType
reply.Receiver = recv
_, err := rand.Read(reply.Nonce[:])
+63 -38
View File
@@ -11,11 +11,10 @@ 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"
"github.com/amnezia-vpn/amneziawg-go/tun"
"github.com/amnezia-vpn/amneziawg-go/v3/conn"
"github.com/amnezia-vpn/amneziawg-go/v3/ratelimiter"
"github.com/amnezia-vpn/amneziawg-go/v3/rwcancel"
"github.com/amnezia-vpn/amneziawg-go/v3/tun"
)
type Device struct {
@@ -41,15 +40,11 @@ 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
network string
framedOpts conceal.FramedOpts
preludeOpts conceal.PreludeOpts
masqueradeOpts conceal.MasqueradeOpts
bind conn.Bind // bind interface
netlinkCancel *rwcancel.RWCancel
port uint16 // listening port
fwmark uint32 // mark value (0 = disabled)
brokenRoaming bool
}
staticIdentity struct {
@@ -94,6 +89,46 @@ type Device struct {
ipcMutex sync.RWMutex
closed chan struct{}
log *Logger
junk struct {
min atomic.Uint32
max atomic.Uint32
count atomic.Uint32
}
headers struct {
init AtomicUintRange
cookie AtomicUintRange
response AtomicUintRange
transport AtomicUintRange
}
paddings struct {
init atomic.Uint32
response atomic.Uint32
cookie atomic.Uint32
transport atomic.Uint32
}
ipackets [5]*obfChain
headerProtection struct {
sync.RWMutex
key HeaderCipherKey
}
contentPaddingAddition AtomicUintRange
timings struct {
rekeyAfterTimeSec AtomicUintRange
rekeyTimeoutSec AtomicUintRange
rejectAfterTimeSec AtomicUintRange
keepaliveTimeoutSec AtomicUintRange
maxHandshakeAttemps AtomicUintRange
}
randomTrailers atomic.Bool
disableCookies atomic.Bool
}
// deviceState represents the state of a Device.
@@ -188,7 +223,7 @@ func (device *Device) upLocked() error {
device.peers.RLock()
for _, peer := range device.peers.keyMap {
peer.Start()
if peer.persistentKeepaliveInterval.Load() > 0 {
if !peer.persistentKeepaliveInterval.Load().IsZero() {
peer.SendKeepalive()
}
}
@@ -288,6 +323,8 @@ func (device *Device) SetPrivateKey(sk NoisePrivateKey) error {
}
func NewDevice(tunDevice tun.Device, bind conn.Bind, logger *Logger) *Device {
var rang UintRange
device := new(Device)
device.state.state.Store(uint32(deviceStateDown))
device.closed = make(chan struct{})
@@ -304,8 +341,14 @@ func NewDevice(tunDevice tun.Device, bind conn.Bind, logger *Logger) *Device {
device.rate.limiter.Init()
device.indexTable.Init()
device.net.network = "udp"
device.net.framedOpts.HeaderCompat = true
rang.FromUint32(MessageInitiationType, MessageInitiationType)
device.headers.init.Store(rang)
rang.FromUint32(MessageResponseType, MessageResponseType)
device.headers.response.Store(rang)
rang.FromUint32(MessageCookieReplyType, MessageCookieReplyType)
device.headers.cookie.Store(rang)
rang.FromUint32(MessageTransportType, MessageTransportType)
device.headers.transport.Store(rang)
device.PopulatePools()
@@ -417,10 +460,12 @@ func (device *Device) SendKeepalivesToPeersWithCurrentKeypair() {
return
}
timeout := device.keychainExpireTime()
device.peers.RLock()
for _, peer := range device.peers.keyMap {
peer.keypairs.RLock()
sendKeepalive := peer.keypairs.current != nil && !peer.keypairs.current.created.Add(RejectAfterTime).Before(time.Now())
sendKeepalive := peer.keypairs.current != nil && !peer.keypairs.current.created.Add(timeout).Before(time.Now())
peer.keypairs.RUnlock()
if sendKeepalive {
peer.SendKeepalive()
@@ -496,26 +541,6 @@ func (device *Device) BindUpdate() error {
var recvFns []conn.ReceiveFunc
netc := &device.net
underlying := netc.bind
if multi, ok := netc.bind.(*conn.Multibind); ok {
if err := multi.SelectNetwork(netc.network); err != nil {
return err
}
underlying = multi.Bind
}
if framable, ok := underlying.(conn.Framable); ok {
framable.SetFramedOpts(netc.framedOpts)
}
if preludable, ok := underlying.(conn.Preludable); ok {
preludable.SetPreludeOpts(netc.preludeOpts)
}
if masqueradable, ok := underlying.(conn.Masqueradable); ok {
masqueradable.SetMasqueradeOpts(netc.masqueradeOpts)
}
recvFns, netc.port, err = netc.bind.Open(netc.port)
if err != nil {
netc.port = 0
+4 -136
View File
@@ -12,23 +12,21 @@ import (
"fmt"
"io"
"math/rand"
"net"
"net/netip"
"os"
"os/signal"
"runtime"
"runtime/pprof"
"strconv"
"sync"
"testing"
"time"
"go.uber.org/atomic"
"github.com/amnezia-vpn/amneziawg-go/conn"
"github.com/amnezia-vpn/amneziawg-go/conn/bindtest"
"github.com/amnezia-vpn/amneziawg-go/tun"
"github.com/amnezia-vpn/amneziawg-go/tun/tuntest"
"github.com/amnezia-vpn/amneziawg-go/v3/conn"
"github.com/amnezia-vpn/amneziawg-go/v3/conn/bindtest"
"github.com/amnezia-vpn/amneziawg-go/v3/tun"
"github.com/amnezia-vpn/amneziawg-go/v3/tun/tuntest"
)
// uapiCfg returns a string that contains cfg formatted use with IpcSet.
@@ -103,54 +101,6 @@ 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
@@ -263,64 +213,6 @@ 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)
@@ -357,30 +249,6 @@ func TestAWGDevicePing(t *testing.T) {
})
}
func TestAWGDevicePingTCPConceal(t *testing.T) {
goroutineLeakCheck(t)
pair := genTestPairTCP(t,
"network", "tcp",
"format_in", "<b 0xfeed><dz be 2><d>",
"format_out", "<b 0xfeed><dz be 2><d>",
"i1", "<b 0xaabb>",
"i2", "<b 0xccdd>",
"s1", "15",
"s2", "18",
"s4", "25",
"h1", "123456-123500",
"h2", "67543-67550",
"h4", "32345-32350",
)
t.Run("ping 1.0.0.1", func(t *testing.T) {
pair.Send(t, Ping, nil)
})
t.Run("ping 1.0.0.2", func(t *testing.T) {
pair.Send(t, Pong, nil)
})
}
// Needs to be stopped with Ctrl-C
func TestAWGHandshakeDevicePing(t *testing.T) {
t.Skip("This test is intended to be run manually, not as part of the test suite.")
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"sync/atomic"
"time"
"github.com/amnezia-vpn/amneziawg-go/replay"
"github.com/amnezia-vpn/amneziawg-go/v3/replay"
)
/* Due to limitations in Go and /x/crypto there is currently
+41 -40
View File
@@ -6,16 +6,18 @@
package device
import (
"crypto/rand"
"errors"
"fmt"
"sync"
"time"
"golang.org/x/crypto/blake2s"
"golang.org/x/crypto/chacha20"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/poly1305"
"github.com/amnezia-vpn/amneziawg-go/tai64n"
"github.com/amnezia-vpn/amneziawg-go/v3/tai64n"
)
type handshakeState int
@@ -53,10 +55,11 @@ const (
)
const (
MessageInitiationType = 1
MessageResponseType = 2
MessageCookieReplyType = 3
MessageTransportType = 4
MessageUnknownType uint32 = 0
MessageInitiationType uint32 = 1
MessageResponseType uint32 = 2
MessageCookieReplyType uint32 = 3
MessageTransportType uint32 = 4
)
const (
@@ -193,16 +196,10 @@ func (device *Device) CreateMessageInitiation(peer *Peer) (*MessageInitiation, e
handshake.mixHash(handshake.remoteStatic[:])
opts := &device.net.framedOpts
var typ uint32
if opts.HeaderCompat && opts.H1 != nil {
typ = opts.H1.Generate()
} else {
typ = MessageInitiationType
}
msgType := device.headers.init.Load().PickOne()
msg := MessageInitiation{
Type: typ,
Type: msgType,
Ephemeral: handshake.localEphemeral.publicKey(),
}
@@ -258,15 +255,8 @@ func (device *Device) ConsumeMessageInitiation(msg *MessageInitiation) *Peer {
chainKey [blake2s.Size]byte
)
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
}
if msg.Type != MessageInitiationType {
return nil
}
device.staticIdentity.RLock()
@@ -381,16 +371,8 @@ 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 = typ
msg.Type = device.headers.response.Load().PickOne()
msg.Sender = handshake.localIndex
msg.Receiver = handshake.remoteIndex
@@ -440,15 +422,8 @@ func (device *Device) CreateMessageResponse(peer *Peer) (*MessageResponse, error
}
func (device *Device) ConsumeMessageResponse(msg *MessageResponse) *Peer {
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
}
if msg.Type != MessageResponseType {
return nil
}
// lookup handshake by receiver
@@ -653,3 +628,29 @@ func (peer *Peer) ReceivedWithKeypair(receivedKeypair *Keypair) bool {
keypairs.next.Store(nil)
return true
}
func (device *Device) JunkPackets() [][]byte {
var bufs [][]byte
min := device.junk.min.Load()
max := device.junk.max.Load()
for range device.junk.count.Load() {
buf := make([]byte, min+fastrandn(max-min))
rand.Read(buf)
bufs = append(bufs, buf)
}
return bufs
}
func (device *Device) HeaderProtectionCipher(salt []byte) (*chacha20.Cipher, error) {
device.headerProtection.RLock()
defer device.headerProtection.RUnlock()
if device.headerProtection.key.IsZero() {
return nil, nil
}
return chacha20.NewUnauthenticatedCipher(device.headerProtection.key[:], salt)
}
+108
View File
@@ -9,12 +9,18 @@ import (
"crypto/subtle"
"encoding/hex"
"errors"
"fmt"
"strconv"
"strings"
"sync/atomic"
)
const (
NoisePublicKeySize = 32
NoisePrivateKeySize = 32
NoisePresharedKeySize = 32
HeaderCipherKeySize = 32
HeaderCipherNonceSize = 12
)
type (
@@ -22,6 +28,7 @@ type (
NoisePrivateKey [NoisePrivateKeySize]byte
NoisePresharedKey [NoisePresharedKeySize]byte
NoiseNonce uint64 // padded to 12-bytes
HeaderCipherKey [HeaderCipherKeySize]byte
)
func loadExactHex(dst []byte, src string) error {
@@ -76,3 +83,104 @@ func (key NoisePublicKey) Equals(tar NoisePublicKey) bool {
func (key *NoisePresharedKey) FromHex(src string) error {
return loadExactHex(key[:], src)
}
func (key HeaderCipherKey) IsZero() bool {
var zero HeaderCipherKey
return key.Equals(zero)
}
func (key HeaderCipherKey) Equals(tar HeaderCipherKey) bool {
return subtle.ConstantTimeCompare(key[:], tar[:]) == 1
}
func (key *HeaderCipherKey) FromHex(src string) error {
return loadExactHex(key[:], src)
}
type UintRange uint64
func (r *UintRange) FromUint32(lo, hi uint32) {
*r = UintRange(uint64(hi)<<32 | uint64(lo))
}
func (r *UintRange) FromString(str string) error {
parts := strings.Split(str, "-")
if len(parts) < 1 || len(parts) > 2 {
return errors.New("wrong format")
}
lo, err := strconv.ParseUint(parts[0], 10, 32)
if err != nil {
return err
}
hi := lo
if len(parts) > 1 {
hi, err = strconv.ParseUint(parts[1], 10, 32)
if err != nil {
return err
}
}
if hi < lo {
return errors.New("wrong range specified")
}
r.FromUint32(uint32(lo), uint32(hi))
return nil
}
func (r UintRange) Contains(num uint32) bool {
lo, hi := uint32(r), uint32(r>>32)
return lo <= num && num <= hi
}
func (r UintRange) IsZero() bool {
return r == 0
}
func (r UintRange) PickOne() uint32 {
lo, hi := uint32(r), uint32(r>>32)
return lo + fastrandn(hi-lo+1)
}
func (r UintRange) ToString() string {
lo, hi := uint32(r), uint32(r>>32)
if lo == hi {
return fmt.Sprintf("%d", lo)
} else {
return fmt.Sprintf("%d-%d", lo, hi)
}
}
func (r UintRange) Overlap(right UintRange) bool {
l_lo, l_hi := uint32(r), uint32(r>>32)
r_lo, r_hi := uint32(right), uint32(right>>32)
return l_lo <= r_hi && r_lo <= l_hi
}
func (r UintRange) Lo() uint32 {
return uint32(r)
}
func (r UintRange) Hi() uint32 {
return uint32(r >> 32)
}
type AtomicUintRange struct {
v atomic.Uint64
}
func (a *AtomicUintRange) Load() UintRange {
return UintRange(a.v.Load())
}
func (a *AtomicUintRange) Store(r UintRange) {
a.v.Store(uint64(r))
}
func (a *AtomicUintRange) Swap(r UintRange) UintRange {
return UintRange(a.v.Swap(uint64(r)))
}
+2 -2
View File
@@ -10,8 +10,8 @@ import (
"encoding/binary"
"testing"
"github.com/amnezia-vpn/amneziawg-go/conn"
"github.com/amnezia-vpn/amneziawg-go/tun/tuntest"
"github.com/amnezia-vpn/amneziawg-go/v3/conn"
"github.com/amnezia-vpn/amneziawg-go/v3/tun/tuntest"
)
func TestCurveWrappers(t *testing.T) {
+143
View File
@@ -0,0 +1,143 @@
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...)
}
if len(obfs) == 0 {
return nil, nil
}
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
@@ -0,0 +1,47 @@
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
@@ -0,0 +1,25 @@
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
@@ -0,0 +1,38 @@
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
@@ -0,0 +1,29 @@
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
@@ -0,0 +1,39 @@
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
@@ -0,0 +1,48 @@
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
@@ -0,0 +1,48 @@
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
@@ -0,0 +1,31 @@
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
}
+11 -4
View File
@@ -12,7 +12,7 @@ import (
"sync/atomic"
"time"
"github.com/amnezia-vpn/amneziawg-go/conn"
"github.com/amnezia-vpn/amneziawg-go/v3/conn"
)
type Peer struct {
@@ -39,6 +39,7 @@ type Peer struct {
zeroKeyMaterial *Timer
persistentKeepalive *Timer
handshakeAttempts atomic.Uint32
maxHandshakeAttempts atomic.Uint32
needAnotherKeepalive atomic.Bool
sentLastMinuteHandshake atomic.Bool
}
@@ -55,7 +56,8 @@ type Peer struct {
cookieGenerator CookieGenerator
trieEntries list.List
persistentKeepaliveInterval atomic.Uint32
persistentKeepaliveInterval AtomicUintRange
udpWindow atomic.Uint32
}
func (device *Device) NewPeer(pk NoisePublicKey) (*Peer, error) {
@@ -78,6 +80,8 @@ func (device *Device) NewPeer(pk NoisePublicKey) (*Peer, error) {
// create peer
peer := new(Peer)
peer.udpWindow.Store(DefaultUdpWindow)
peer.cookieGenerator.Init(pk)
peer.device = device
peer.queue.outbound = newAutodrainingOutboundQueue(device)
@@ -192,7 +196,7 @@ func (peer *Peer) Start() {
peer.stopping.Add(2)
peer.handshake.mutex.Lock()
peer.handshake.lastSentHandshake = time.Now().Add(-(RekeyTimeout + time.Second))
peer.handshake.lastSentHandshake = time.Now().Add(-(peer.device.rekeyMinTimeout() + time.Second))
peer.handshake.mutex.Unlock()
peer.device.queue.encryption.wg.Add(1) // keep encryption queue open for our writes
@@ -242,7 +246,7 @@ func (peer *Peer) ExpireCurrentKeypairs() {
handshake.mutex.Lock()
peer.device.indexTable.Delete(handshake.localIndex)
handshake.Clear()
peer.handshake.lastSentHandshake = time.Now().Add(-(RekeyTimeout + time.Second))
peer.handshake.lastSentHandshake = time.Now().Add(-(peer.device.rekeyMinTimeout() + time.Second))
handshake.mutex.Unlock()
keypairs := &peer.keypairs
@@ -282,6 +286,9 @@ func (peer *Peer) SetEndpointFromPacket(endpoint conn.Endpoint) {
if peer.endpoint.disableRoaming {
return
}
if peer.endpoint.val != endpoint {
peer.udpWindow.Store(DefaultUdpWindow)
}
peer.endpoint.clearSrcOnTx = false
peer.endpoint.val = endpoint
}
+1 -1
View File
@@ -5,7 +5,7 @@
package device
import "github.com/amnezia-vpn/amneziawg-go/conn"
import "github.com/amnezia-vpn/amneziawg-go/v3/conn"
/* Reduce memory consumption for Android */
+1 -1
View File
@@ -7,7 +7,7 @@
package device
import "github.com/amnezia-vpn/amneziawg-go/conn"
import "github.com/amnezia-vpn/amneziawg-go/v3/conn"
const (
QueueStagedSize = conn.IdealBatchSize
+135 -25
View File
@@ -13,7 +13,7 @@ import (
"sync"
"time"
"github.com/amnezia-vpn/amneziawg-go/conn"
"github.com/amnezia-vpn/amneziawg-go/v3/conn"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
@@ -32,6 +32,7 @@ type QueueInboundElement struct {
counter uint64
keypair *Keypair
endpoint conn.Endpoint
padding uint32
}
type QueueInboundElementsContainer struct {
@@ -59,7 +60,8 @@ func (peer *Peer) keepKeyFreshReceiving() {
return
}
keypair := peer.keypairs.Current()
if keypair != nil && keypair.isInitiator && time.Since(keypair.created) > (RejectAfterTime-KeepaliveTimeout-RekeyTimeout) {
if keypair != nil && keypair.isInitiator && time.Since(keypair.created) > peer.device.keyRefreshTimeoutReceiving() {
peer.timers.sentLastMinuteHandshake.Store(true)
peer.SendHandshakeInitiation(false)
}
@@ -70,7 +72,10 @@ 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)
@@ -92,15 +97,16 @@ func (device *Device) RoutineReceiveIncoming(maxBatchSize int, recv conn.Receive
endpoints = make([]conn.Endpoint, maxBatchSize)
deathSpiral int
elemsByPeer = make(map[*Peer]*QueueInboundElementsContainer, maxBatchSize)
typeHashBuf [4]byte
)
for i := range bufsArrs {
for i := range maxBatchSize {
bufsArrs[i] = device.GetMessageBuffer()
bufs[i] = bufsArrs[i][:]
}
defer func() {
for i := 0; i < maxBatchSize; i++ {
for i := range maxBatchSize {
if bufsArrs[i] != nil {
device.PutMessageBuffer(bufsArrs[i])
}
@@ -133,21 +139,30 @@ func (device *Device) RoutineReceiveIncoming(maxBatchSize int, recv conn.Receive
}
// check size of packet
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
}
cip, err := device.HeaderProtectionCipher(packet[:HeaderCipherNonceSize])
if err != nil {
device.log.Errorf("Failed to initialize header cipher")
continue
}
typeHash := typeHashBuf[:]
clear(typeHash)
if cip != nil {
cip.XORKeyStream(typeHash, typeHash)
}
// get message padding and type based on information from S1-S4 and H1-H4
msgSize, msgType, padding := device.DeterminePacketTypeAndPadding(packet, typeHash)
packet = packet[padding:]
if msgType != MessageTransportType {
packet = packet[:msgSize]
}
if cip != nil {
applyHash(packet[:4], packet[:4], typeHash)
}
switch msgType {
@@ -161,6 +176,9 @@ func (device *Device) RoutineReceiveIncoming(maxBatchSize int, recv conn.Receive
if len(packet) < MessageTransportSize {
continue
}
if cip != nil {
cip.XORKeyStream(packet[4:MessageTransportHeaderSize], packet[4:MessageTransportHeaderSize])
}
// lookup key pair
@@ -175,7 +193,7 @@ func (device *Device) RoutineReceiveIncoming(maxBatchSize int, recv conn.Receive
// check keypair expiry
if keypair.created.Add(RejectAfterTime).Before(time.Now()) {
if keypair.created.Add(device.keychainExpireTime()).Before(time.Now()) {
continue
}
@@ -187,6 +205,7 @@ func (device *Device) RoutineReceiveIncoming(maxBatchSize int, recv conn.Receive
elem.keypair = keypair
elem.endpoint = endpoints[i]
elem.counter = 0
elem.padding = padding
elemsForPeer, ok := elemsByPeer[peer]
if !ok {
@@ -205,16 +224,25 @@ func (device *Device) RoutineReceiveIncoming(maxBatchSize int, recv conn.Receive
if len(packet) != MessageInitiationSize {
continue
}
if cip != nil {
cip.XORKeyStream(packet[4:MessageInitiationSize], packet[4:MessageInitiationSize])
}
case MessageResponseType:
if len(packet) != MessageResponseSize {
continue
}
if cip != nil {
cip.XORKeyStream(packet[4:MessageResponseSize], packet[4:MessageResponseSize])
}
case MessageCookieReplyType:
if len(packet) != MessageCookieReplySize {
continue
}
if cip != nil {
cip.XORKeyStream(packet[4:MessageCookieReplySize], packet[4:MessageCookieReplySize])
}
default:
device.log.Verbosef("Received message with unknown type")
@@ -317,9 +345,14 @@ 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",
)
}
}
@@ -336,7 +369,8 @@ func (device *Device) RoutineHandshake(id int) {
// endpoints destination address is the source of the datagram
if device.IsUnderLoad() {
disableCookies := device.disableCookies.Load()
if !disableCookies && device.IsUnderLoad() {
// verify MAC2 field
@@ -370,6 +404,9 @@ 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 {
@@ -402,6 +439,9 @@ 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)
@@ -475,7 +515,12 @@ func (peer *Peer) RoutineSequentialReceiver(maxBatchSize int) {
}
rxBytesLen += uint64(len(elem.packet) + MinMessageSize)
if len(elem.packet) == 0 {
udpWindow := elem.padding + MessageTransportHeaderSize + uint32(len(elem.packet))
if peer.udpWindow.Load() < udpWindow {
peer.udpWindow.Store(udpWindow)
}
if len(elem.packet) == 0 || elem.packet[0] == 0 {
device.log.Verbosef("%v - Receiving keepalive packet", peer)
continue
}
@@ -516,11 +561,14 @@ 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[int(elem.padding):int(elem.padding)+MessageTransportHeaderSize+len(elem.packet)])
}
peer.rxBytes.Add(rxBytesLen)
@@ -547,3 +595,65 @@ func (peer *Peer) RoutineSequentialReceiver(maxBatchSize int) {
device.PutInboundElementsContainer(elemsContainer)
}
}
func applyHash(dst, src, hash []byte) {
for i := range len(dst) {
dst[i] = src[i] ^ hash[i]
}
}
func (device *Device) DeterminePacketTypeAndPadding(packet []byte, typeHash []byte) (int, uint32, uint32) {
var headerBytes [4]byte
var padding uint32
var header UintRange
var expectedSize int
size := len(packet)
randomTrailers := device.randomTrailers.Load()
padding = device.paddings.init.Load()
header = device.headers.init.Load()
expectedSize = int(padding) + MessageInitiationSize
if size == expectedSize || randomTrailers && size > expectedSize {
applyHash(headerBytes[:], packet[padding:padding+4], typeHash)
if header.Contains(binary.LittleEndian.Uint32(headerBytes[:])) {
return MessageInitiationSize, MessageInitiationType, padding
}
}
padding = device.paddings.response.Load()
header = device.headers.response.Load()
expectedSize = int(padding) + MessageResponseSize
if size == expectedSize || randomTrailers && size > expectedSize {
applyHash(headerBytes[:], packet[padding:padding+4], typeHash)
if header.Contains(binary.LittleEndian.Uint32(headerBytes[:])) {
return MessageResponseSize, MessageResponseType, padding
}
}
padding = device.paddings.cookie.Load()
header = device.headers.cookie.Load()
expectedSize = int(padding) + MessageCookieReplySize
if size == expectedSize || randomTrailers && size > expectedSize {
applyHash(headerBytes[:], packet[padding:padding+4], typeHash)
if header.Contains(binary.LittleEndian.Uint32(headerBytes[:])) {
return MessageCookieReplySize, MessageCookieReplyType, padding
}
}
padding = device.paddings.transport.Load()
header = device.headers.transport.Load()
expectedSize = int(padding) + MessageTransportSize
if size >= expectedSize {
applyHash(headerBytes[:], packet[padding:padding+4], typeHash)
if header.Contains(binary.LittleEndian.Uint32(headerBytes[:])) {
return MessageTransportSize, MessageTransportType, padding
}
}
return 0, MessageUnknownType, 0
}
+193 -46
View File
@@ -7,15 +7,17 @@ package device
import (
"bytes"
"crypto/rand"
"encoding/binary"
"errors"
"net"
"os"
"slices"
"sync"
"time"
"github.com/amnezia-vpn/amneziawg-go/conn"
"github.com/amnezia-vpn/amneziawg-go/tun"
"github.com/amnezia-vpn/amneziawg-go/v3/conn"
"github.com/amnezia-vpn/amneziawg-go/v3/tun"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
@@ -46,11 +48,13 @@ import (
*/
type QueueOutboundElement struct {
buffer *[MaxMessageSize]byte // slice holding the packet data
packet []byte // slice of "buffer" (always!)
nonce uint64 // nonce for encryption
keypair *Keypair // keypair for encryption
peer *Peer // related peer
buffer *[MaxMessageSize]byte // slice holding the packet data
packet []byte // slice of "buffer" (always!)
nonce uint64 // nonce for encryption
keypair *Keypair // keypair for encryption
peer *Peer // related peer
padding uint32
isKeepalive bool
}
type QueueOutboundElementsContainer struct {
@@ -62,6 +66,8 @@ func (device *Device) NewOutboundElement() *QueueOutboundElement {
elem := device.GetOutboundElement()
elem.buffer = device.GetMessageBuffer()
elem.nonce = 0
elem.padding = device.paddings.transport.Load()
elem.isKeepalive = false
// keypair and peer were cleared (if necessary) by clearPointers.
return elem
}
@@ -82,6 +88,7 @@ func (elem *QueueOutboundElement) clearPointers() {
func (peer *Peer) SendKeepalive() {
if len(peer.queue.staged) == 0 && peer.isRunning.Load() {
elem := peer.device.NewOutboundElement()
elem.isKeepalive = true
elemsContainer := peer.device.GetOutboundElementsContainer()
elemsContainer.elems = append(elemsContainer.elems, elem)
select {
@@ -99,17 +106,20 @@ func (peer *Peer) SendKeepalive() {
func (peer *Peer) SendHandshakeInitiation(isRetry bool) error {
if !isRetry {
peer.timers.handshakeAttempts.Store(0)
peer.timers.maxHandshakeAttempts.Store(peer.device.maxHandshakeAttemps())
}
timeout := peer.device.rekeyMinTimeout()
peer.handshake.mutex.RLock()
if time.Since(peer.handshake.lastSentHandshake) < RekeyTimeout {
if time.Since(peer.handshake.lastSentHandshake) < timeout {
peer.handshake.mutex.RUnlock()
return nil
}
peer.handshake.mutex.RUnlock()
peer.handshake.mutex.Lock()
if time.Since(peer.handshake.lastSentHandshake) < RekeyTimeout {
if time.Since(peer.handshake.lastSentHandshake) < timeout {
peer.handshake.mutex.Unlock()
return nil
}
@@ -124,8 +134,27 @@ func (peer *Peer) SendHandshakeInitiation(isRetry bool) error {
return err
}
var buf [MessageInitiationSize]byte
writer := bytes.NewBuffer(buf[:0])
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)
}
}
sendBuffer = append(sendBuffer, peer.device.JunkPackets()...)
padding := int(peer.device.paddings.init.Load())
trailerLen := max(peer.randomTrailer(padding+MessageInitiationSize), 0)
buf := make([]byte, padding+MessageInitiationSize+trailerLen)
crypt := buf[:padding]
rand.Read(crypt)
writer := bytes.NewBuffer(buf[padding:padding])
binary.Write(writer, binary.LittleEndian, msg)
packet := writer.Bytes()
peer.cookieGenerator.AddMacs(packet)
@@ -133,7 +162,19 @@ func (peer *Peer) SendHandshakeInitiation(isRetry bool) error {
peer.timersAnyAuthenticatedPacketTraversal()
peer.timersAnyAuthenticatedPacketSent()
err = peer.SendBuffers([][]byte{packet})
cip, err := peer.device.HeaderProtectionCipher(crypt[:HeaderCipherNonceSize])
if err != nil {
return err
}
if cip != nil {
cip.XORKeyStream(packet, packet)
}
trailer := buf[padding+MessageInitiationSize:]
rand.Read(trailer)
sendBuffer = append(sendBuffer, buf)
err = peer.SendBuffers(sendBuffer)
if err != nil {
peer.device.log.Errorf("%v - Failed to send handshake initiation: %v", peer, err)
}
@@ -155,9 +196,15 @@ func (peer *Peer) SendHandshakeResponse() error {
return err
}
var buf [MessageResponseSize]byte
writer := bytes.NewBuffer(buf[:0])
padding := int(peer.device.paddings.response.Load())
trailerLen := max(peer.randomTrailer(padding+MessageResponseSize), 0)
buf := make([]byte, padding+MessageResponseSize+trailerLen)
crypt := buf[:padding]
rand.Read(crypt)
writer := bytes.NewBuffer(buf[padding:padding])
binary.Write(writer, binary.LittleEndian, response)
packet := writer.Bytes()
peer.cookieGenerator.AddMacs(packet)
@@ -172,8 +219,19 @@ func (peer *Peer) SendHandshakeResponse() error {
peer.timersAnyAuthenticatedPacketTraversal()
peer.timersAnyAuthenticatedPacketSent()
cip, err := peer.device.HeaderProtectionCipher(crypt[:HeaderCipherNonceSize])
if err != nil {
return err
}
if cip != nil {
cip.XORKeyStream(packet, packet)
}
trailer := buf[padding+MessageResponseSize:]
rand.Read(trailer)
// TODO: allocation could be avoided
err = peer.SendBuffers([][]byte{packet})
err = peer.SendBuffers([][]byte{buf})
if err != nil {
peer.device.log.Errorf("%v - Failed to send handshake response: %v", peer, err)
}
@@ -183,28 +241,45 @@ 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())
opts := &device.net.framedOpts
var typ uint32
if opts.HeaderCompat && opts.H3 != nil {
typ = opts.H3.Generate()
} else {
typ = MessageCookieReplyType
}
sender := binary.LittleEndian.Uint32(initiatingElem.packet[4:8])
reply, err := device.cookieChecker.CreateReply(initiatingElem.packet, sender, initiatingElem.endpoint.DstToBytes(), typ)
msgType := device.headers.cookie.Load().PickOne()
reply, err := device.cookieChecker.CreateReply(
initiatingElem.packet,
sender,
initiatingElem.endpoint.DstToBytes(),
msgType,
)
if err != nil {
device.log.Errorf("Failed to create cookie reply: %v", err)
return err
}
var buf [MessageCookieReplySize]byte
writer := bytes.NewBuffer(buf[:0])
padding := int(device.paddings.cookie.Load())
trailerLen := max(device.randomTrailer(padding+MessageCookieReplySize), 0)
buf := make([]byte, padding+MessageCookieReplySize+trailerLen)
crypt := buf[:padding]
rand.Read(crypt)
writer := bytes.NewBuffer(buf[padding:padding])
binary.Write(writer, binary.LittleEndian, reply)
packet := writer.Bytes()
cip, err := device.HeaderProtectionCipher(crypt[:HeaderCipherNonceSize])
if err != nil {
return err
}
if cip != nil {
cip.XORKeyStream(packet, packet)
}
trailer := buf[padding+MessageCookieReplySize:]
rand.Read(trailer)
// TODO: allocation could be avoided
device.net.bind.Send([][]byte{packet}, initiatingElem.endpoint)
device.net.bind.Send([][]byte{buf}, initiatingElem.endpoint)
return nil
}
@@ -214,7 +289,7 @@ func (peer *Peer) keepKeyFreshSending() {
return
}
nonce := keypair.sendNonce.Load()
if nonce > RekeyAfterMessages || (keypair.isInitiator && time.Since(keypair.created) > RekeyAfterTime) {
if nonce > RekeyAfterMessages || (keypair.isInitiator && time.Since(keypair.created) > peer.device.keyRefreshTimeoutSending()) {
peer.SendHandshakeInitiation(false)
}
}
@@ -236,7 +311,6 @@ func (device *Device) RoutineReadFromTUN() {
elemsByPeer = make(map[*Peer]*QueueOutboundElementsContainer, batchSize)
count = 0
sizes = make([]int, batchSize)
offset = MessageTransportHeaderSize
)
for i := range elems {
@@ -254,6 +328,9 @@ func (device *Device) RoutineReadFromTUN() {
}()
for {
padding := device.paddings.transport.Load()
offset := MessageTransportHeaderSize + int(padding)
// read packets
count, readErr = device.tun.device.Read(bufs, sizes, offset)
for i := 0; i < count; i++ {
@@ -263,6 +340,7 @@ func (device *Device) RoutineReadFromTUN() {
elem := elems[i]
elem.packet = bufs[i][offset : offset+sizes[i]]
elem.padding = padding
// lookup peer
var peer *Peer
@@ -357,7 +435,7 @@ top:
}
keypair := peer.keypairs.Current()
if keypair == nil || keypair.sendNonce.Load() >= RejectAfterMessages || time.Since(keypair.created) >= RejectAfterTime {
if keypair == nil || keypair.sendNonce.Load() >= RejectAfterMessages || time.Since(keypair.created) >= peer.device.keychainExpireTime() {
peer.SendHandshakeInitiation(false)
return
}
@@ -447,13 +525,55 @@ func calculatePaddingSize(packetSize, mtu int) int {
return paddedSize - lastUnit
}
func (peer *Peer) randomPaddingAddition(packetSize int) int {
addition := peer.device.contentPaddingAddition.Load()
if addition.IsZero() {
return -1
}
udpWindow := int(peer.udpWindow.Load())
if udpWindow < packetSize {
return 0
}
add := int(addition.PickOne())
space := udpWindow - packetSize
if add > space {
add = space
}
return add
}
func (device *Device) randomTrailer(packetSize int) int {
if !device.randomTrailers.Load() {
return -1
}
if DefaultUdpWindow < packetSize {
return 0
}
return int(fastrandn(uint32(DefaultUdpWindow - packetSize)))
}
func (peer *Peer) randomTrailer(packetSize int) int {
if !peer.device.randomTrailers.Load() {
return -1
}
udpWindow := int(peer.udpWindow.Load())
if udpWindow < packetSize {
return 0
}
return int(fastrandn(uint32(udpWindow - packetSize)))
}
/* Encrypts the elements in the queue
* and marks them for sequential consumption (by releasing the mutex)
*
* Obs. One instance per core
*/
func (device *Device) RoutineEncryption(id int) {
var paddingZeros [PaddingMultiple]byte
var nonce [chacha20poly1305.NonceSize]byte
defer device.log.Verbosef("Routine: encryption worker %d - stopped", id)
@@ -461,38 +581,63 @@ func (device *Device) RoutineEncryption(id int) {
for elemsContainer := range device.queue.encryption.c {
for _, elem := range elemsContainer.elems {
udpWindow := elem.padding + MinMessageSize + uint32(len(elem.packet))
if elem.peer.udpWindow.Load() < udpWindow {
elem.peer.udpWindow.Store(udpWindow)
}
// fill crypto padding
crypt := elem.buffer[:elem.padding]
rand.Read(crypt)
// populate header fields
header := elem.buffer[:MessageTransportHeaderSize]
header := elem.buffer[elem.padding : elem.padding+MessageTransportHeaderSize]
fieldType := header[0:4]
fieldReceiver := header[4:8]
fieldNonce := header[8:16]
opts := &device.net.framedOpts
var typ uint32
if opts.HeaderCompat && opts.H4 != nil {
typ = opts.H4.Generate()
} else {
typ = MessageTransportType
}
binary.LittleEndian.PutUint32(fieldType, typ)
binary.LittleEndian.PutUint32(fieldType, device.headers.transport.Load().PickOne())
binary.LittleEndian.PutUint32(fieldReceiver, elem.keypair.remoteIndex)
binary.LittleEndian.PutUint64(fieldNonce, elem.nonce)
// pad content to multiple of 16
paddingSize := calculatePaddingSize(len(elem.packet), int(device.tun.mtu.Load()))
elem.packet = append(elem.packet, paddingZeros[:paddingSize]...)
packetSize := len(elem.packet) + MinMessageSize + int(elem.padding)
mtu := int(device.tun.mtu.Load())
paddingSize := elem.peer.randomPaddingAddition(packetSize)
if paddingSize < 0 {
paddingSize = elem.peer.randomTrailer(packetSize)
}
if paddingSize < 0 {
// pad content to multiple of 16
paddingSize = calculatePaddingSize(len(elem.packet), mtu)
}
// append trailing zeroes
oldLen := len(elem.packet)
elem.packet = slices.Grow(elem.packet, paddingSize)
elem.packet = elem.packet[:oldLen+paddingSize]
clear(elem.packet[oldLen:])
// encrypt content and release to consumer
binary.LittleEndian.PutUint64(nonce[4:], elem.nonce)
elem.packet = elem.keypair.send.Seal(
header,
elem.buffer[:elem.padding+MessageTransportHeaderSize],
nonce[:],
elem.packet,
nil,
)
cip, err := device.HeaderProtectionCipher(crypt[:HeaderCipherNonceSize])
if err != nil {
device.log.Errorf("Routing: header obfuscation failed - packet dropped")
elem.packet = nil
continue
}
if cip != nil {
cip.XORKeyStream(header, header)
}
}
elemsContainer.Unlock()
}
@@ -531,9 +676,10 @@ func (peer *Peer) RoutineSequentialSender(maxBatchSize int) {
dataSent := false
elemsContainer.Lock()
for _, elem := range elemsContainer.elems {
if len(elem.packet) != MessageKeepaliveSize {
if !elem.isKeepalive {
dataSent = true
}
bufs = append(bufs, elem.packet)
}
@@ -544,6 +690,7 @@ func (peer *Peer) RoutineSequentialSender(maxBatchSize int) {
if dataSent {
peer.timersDataSent()
}
for _, elem := range elemsContainer.elems {
device.PutMessageBuffer(elem.buffer)
device.PutOutboundElement(elem)
+2 -2
View File
@@ -3,8 +3,8 @@
package device
import (
"github.com/amnezia-vpn/amneziawg-go/conn"
"github.com/amnezia-vpn/amneziawg-go/rwcancel"
"github.com/amnezia-vpn/amneziawg-go/v3/conn"
"github.com/amnezia-vpn/amneziawg-go/v3/rwcancel"
)
func (device *Device) startRouteListener(_ conn.Bind) (*rwcancel.RWCancel, error) {
+2 -2
View File
@@ -20,8 +20,8 @@ import (
"golang.org/x/sys/unix"
"github.com/amnezia-vpn/amneziawg-go/conn"
"github.com/amnezia-vpn/amneziawg-go/rwcancel"
"github.com/amnezia-vpn/amneziawg-go/v3/conn"
"github.com/amnezia-vpn/amneziawg-go/v3/rwcancel"
)
func (device *Device) startRouteListener(bind conn.Bind) (*rwcancel.RWCancel, error) {
+124 -27
View File
@@ -22,24 +22,25 @@ type Timer struct {
*time.Timer
modifyingLock sync.RWMutex
runningLock sync.Mutex
isPending bool
duration time.Duration
}
func (peer *Peer) NewTimer(expirationFunction func(*Peer)) *Timer {
func (peer *Peer) NewTimer(expirationFunction func(*Peer, time.Duration)) *Timer {
timer := &Timer{}
timer.Timer = time.AfterFunc(time.Hour, func() {
timer.runningLock.Lock()
defer timer.runningLock.Unlock()
timer.modifyingLock.Lock()
if !timer.isPending {
if timer.duration == 0 {
timer.modifyingLock.Unlock()
return
}
timer.isPending = false
duration := timer.duration
timer.modifyingLock.Unlock()
timer.duration = 0
expirationFunction(peer)
expirationFunction(peer, duration)
})
timer.Stop()
return timer
@@ -47,14 +48,14 @@ func (peer *Peer) NewTimer(expirationFunction func(*Peer)) *Timer {
func (timer *Timer) Mod(d time.Duration) {
timer.modifyingLock.Lock()
timer.isPending = true
timer.duration = d
timer.Reset(d)
timer.modifyingLock.Unlock()
}
func (timer *Timer) Del() {
timer.modifyingLock.Lock()
timer.isPending = false
timer.duration = 0
timer.Stop()
timer.modifyingLock.Unlock()
}
@@ -69,16 +70,18 @@ func (timer *Timer) DelSync() {
func (timer *Timer) IsPending() bool {
timer.modifyingLock.RLock()
defer timer.modifyingLock.RUnlock()
return timer.isPending
return timer.duration > 0
}
func (peer *Peer) timersActive() bool {
return peer.isRunning.Load() && peer.device != nil && peer.device.isUp()
}
func expiredRetransmitHandshake(peer *Peer) {
if peer.timers.handshakeAttempts.Load() > MaxTimerHandshakes {
peer.device.log.Verbosef("%s - Handshake did not complete after %d attempts, giving up", peer, MaxTimerHandshakes+2)
func expiredRetransmitHandshake(peer *Peer, d time.Duration) {
maxAttempts := peer.timers.maxHandshakeAttempts.Load()
if peer.timers.handshakeAttempts.Load() > maxAttempts {
peer.device.log.Verbosef("%s - Handshake did not complete after %d attempts, giving up", peer, maxAttempts+2)
if peer.timersActive() {
peer.timers.sendKeepalive.Del()
@@ -93,11 +96,11 @@ func expiredRetransmitHandshake(peer *Peer) {
* of a partial exchange.
*/
if peer.timersActive() && !peer.timers.zeroKeyMaterial.IsPending() {
peer.timers.zeroKeyMaterial.Mod(RejectAfterTime * 3)
peer.timers.zeroKeyMaterial.Mod(peer.device.keychainExpireTime() * 3)
}
} else {
peer.timers.handshakeAttempts.Add(1)
peer.device.log.Verbosef("%s - Handshake did not complete after %d seconds, retrying (try %d)", peer, int(RekeyTimeout.Seconds()), peer.timers.handshakeAttempts.Load()+1)
peer.device.log.Verbosef("%s - Handshake did not complete after %d seconds, retrying (try %d)", peer, int(d.Seconds()), peer.timers.handshakeAttempts.Load()+1)
/* We clear the endpoint address src address, in case this is the cause of trouble. */
peer.markEndpointSrcForClearing()
@@ -106,30 +109,30 @@ func expiredRetransmitHandshake(peer *Peer) {
}
}
func expiredSendKeepalive(peer *Peer) {
func expiredSendKeepalive(peer *Peer, d time.Duration) {
peer.SendKeepalive()
if peer.timers.needAnotherKeepalive.Load() {
peer.timers.needAnotherKeepalive.Store(false)
if peer.timersActive() {
peer.timers.sendKeepalive.Mod(KeepaliveTimeout)
peer.timers.sendKeepalive.Mod(peer.sendKeepaliveTimeout())
}
}
}
func expiredNewHandshake(peer *Peer) {
peer.device.log.Verbosef("%s - Retrying handshake because we stopped hearing back after %d seconds", peer, int((KeepaliveTimeout + RekeyTimeout).Seconds()))
func expiredNewHandshake(peer *Peer, d time.Duration) {
peer.device.log.Verbosef("%s - Retrying handshake because we stopped hearing back after %d seconds", peer, int(d.Seconds()))
/* We clear the endpoint address src address, in case this is the cause of trouble. */
peer.markEndpointSrcForClearing()
peer.SendHandshakeInitiation(false)
}
func expiredZeroKeyMaterial(peer *Peer) {
peer.device.log.Verbosef("%s - Removing all keys, since we haven't received a new one in %d seconds", peer, int((RejectAfterTime * 3).Seconds()))
func expiredZeroKeyMaterial(peer *Peer, d time.Duration) {
peer.device.log.Verbosef("%s - Removing all keys, since we haven't received a new one in %d seconds", peer, int(d.Seconds()))
peer.ZeroAndFlushAll()
}
func expiredPersistentKeepalive(peer *Peer) {
if peer.persistentKeepaliveInterval.Load() > 0 {
func expiredPersistentKeepalive(peer *Peer, d time.Duration) {
if !peer.persistentKeepaliveInterval.Load().IsZero() {
peer.SendKeepalive()
}
}
@@ -137,7 +140,7 @@ func expiredPersistentKeepalive(peer *Peer) {
/* Should be called after an authenticated data packet is sent. */
func (peer *Peer) timersDataSent() {
if peer.timersActive() && !peer.timers.newHandshake.IsPending() {
peer.timers.newHandshake.Mod(KeepaliveTimeout + RekeyTimeout + time.Millisecond*time.Duration(fastrandn(RekeyTimeoutJitterMaxMs)))
peer.timers.newHandshake.Mod(peer.newHandshakeTimeout() + time.Millisecond*time.Duration(fastrandn(RekeyTimeoutJitterMaxMs)))
}
}
@@ -145,7 +148,7 @@ func (peer *Peer) timersDataSent() {
func (peer *Peer) timersDataReceived() {
if peer.timersActive() {
if !peer.timers.sendKeepalive.IsPending() {
peer.timers.sendKeepalive.Mod(KeepaliveTimeout)
peer.timers.sendKeepalive.Mod(peer.sendKeepaliveTimeout())
} else {
peer.timers.needAnotherKeepalive.Store(true)
}
@@ -169,7 +172,7 @@ func (peer *Peer) timersAnyAuthenticatedPacketReceived() {
/* Should be called after a handshake initiation message is sent. */
func (peer *Peer) timersHandshakeInitiated() {
if peer.timersActive() {
peer.timers.retransmitHandshake.Mod(RekeyTimeout + time.Millisecond*time.Duration(fastrandn(RekeyTimeoutJitterMaxMs)))
peer.timers.retransmitHandshake.Mod(peer.retransmitHandshakeTimeout() + time.Millisecond*time.Duration(fastrandn(RekeyTimeoutJitterMaxMs)))
}
}
@@ -179,6 +182,7 @@ func (peer *Peer) timersHandshakeComplete() {
peer.timers.retransmitHandshake.Del()
}
peer.timers.handshakeAttempts.Store(0)
peer.timers.maxHandshakeAttempts.Store(peer.device.maxHandshakeAttemps())
peer.timers.sentLastMinuteHandshake.Store(false)
peer.lastHandshakeNano.Store(time.Now().UnixNano())
}
@@ -186,15 +190,15 @@ func (peer *Peer) timersHandshakeComplete() {
/* Should be called after an ephemeral key is created, which is before sending a handshake response or after receiving a handshake response. */
func (peer *Peer) timersSessionDerived() {
if peer.timersActive() {
peer.timers.zeroKeyMaterial.Mod(RejectAfterTime * 3)
peer.timers.zeroKeyMaterial.Mod(peer.device.keychainExpireTime() * 3)
}
}
/* Should be called before a packet with authentication -- keepalive, data, or handshake -- is sent, or after one is received. */
func (peer *Peer) timersAnyAuthenticatedPacketTraversal() {
keepalive := peer.persistentKeepaliveInterval.Load()
if keepalive > 0 && peer.timersActive() {
peer.timers.persistentKeepalive.Mod(time.Duration(keepalive) * time.Second)
if !keepalive.IsZero() && peer.timersActive() {
peer.timers.persistentKeepalive.Mod(time.Duration(keepalive.PickOne()) * time.Second)
}
}
@@ -208,6 +212,7 @@ func (peer *Peer) timersInit() {
func (peer *Peer) timersStart() {
peer.timers.handshakeAttempts.Store(0)
peer.timers.maxHandshakeAttempts.Store(peer.device.maxHandshakeAttemps())
peer.timers.sentLastMinuteHandshake.Store(false)
peer.timers.needAnotherKeepalive.Store(false)
}
@@ -219,3 +224,95 @@ func (peer *Peer) timersStop() {
peer.timers.zeroKeyMaterial.DelSync()
peer.timers.persistentKeepalive.DelSync()
}
func (peer *Peer) retransmitHandshakeTimeout() time.Duration {
timeout := RekeyTimeout
if t := peer.device.timings.rekeyTimeoutSec.Load(); !t.IsZero() {
timeout = time.Duration(t.PickOne()) * time.Second
}
return timeout
}
func (peer *Peer) sendKeepaliveTimeout() time.Duration {
timeout := KeepaliveTimeout
if t := peer.device.timings.keepaliveTimeoutSec.Load(); !t.IsZero() {
timeout = time.Duration(t.PickOne()) * time.Second
}
return timeout
}
func (peer *Peer) newHandshakeTimeout() time.Duration {
keepaliveTimeout := KeepaliveTimeout
rekeyTimeout := RekeyTimeout
if t := peer.device.timings.keepaliveTimeoutSec.Load(); !t.IsZero() {
keepaliveTimeout = time.Duration(t.Hi()) * time.Second
}
if t := peer.device.timings.rekeyTimeoutSec.Load(); !t.IsZero() {
rekeyTimeout = time.Duration(t.PickOne()) * time.Second
}
return keepaliveTimeout + rekeyTimeout
}
func (device *Device) keyRefreshTimeoutSending() time.Duration {
rekeyAfterTime := RekeyAfterTime
if t := device.timings.rekeyAfterTimeSec.Load(); !t.IsZero() {
rekeyAfterTime = time.Duration(t.PickOne()) * time.Second
}
return rekeyAfterTime
}
func (device *Device) keyRefreshTimeoutReceiving() time.Duration {
rejectAfterTime := RejectAfterTime
keepaliveTimeout := KeepaliveTimeout
rekeyTimeout := RekeyTimeout
if t := device.timings.rejectAfterTimeSec.Load(); !t.IsZero() {
rejectAfterTime = time.Duration(t.PickOne()) * time.Second
}
if t := device.timings.keepaliveTimeoutSec.Load(); !t.IsZero() {
keepaliveTimeout = time.Duration(t.Lo()) * time.Second
}
if t := device.timings.rekeyTimeoutSec.Load(); !t.IsZero() {
rekeyTimeout = time.Duration(t.Lo()) * time.Second
}
return max(0, rejectAfterTime-keepaliveTimeout-rekeyTimeout)
}
func (device *Device) keychainExpireTime() time.Duration {
rejectAfterTime := RejectAfterTime
if t := device.timings.rejectAfterTimeSec.Load(); !t.IsZero() {
rejectAfterTime = time.Duration(t.Hi()) * time.Second
}
return rejectAfterTime
}
func (device *Device) rekeyMinTimeout() time.Duration {
rekeyTimeout := RekeyTimeout
if t := device.timings.rekeyTimeoutSec.Load(); !t.IsZero() {
rekeyTimeout = time.Duration(t.Lo()) * time.Second
}
return rekeyTimeout
}
func (device *Device) maxHandshakeAttemps() uint32 {
res := uint32(MaxTimerHandshakes)
if t := device.timings.maxHandshakeAttemps.Load(); !t.IsZero() {
res = t.PickOne()
}
return res
}
+1 -1
View File
@@ -8,7 +8,7 @@ package device
import (
"fmt"
"github.com/amnezia-vpn/amneziawg-go/tun"
"github.com/amnezia-vpn/amneziawg-go/v3/tun"
)
const DefaultMTU = 1420
+273 -260
View File
@@ -18,8 +18,7 @@ import (
"sync"
"time"
"github.com/amnezia-vpn/amneziawg-go/conceal"
"github.com/amnezia-vpn/amneziawg-go/ipc"
"github.com/amnezia-vpn/amneziawg-go/v3/ipc"
)
type IPCError struct {
@@ -71,6 +70,18 @@ func (device *Device) IpcGetOperation(w io.Writer) error {
}
buf.WriteByte('\n')
}
boolf := func(prefix string, val bool) {
buf.Grow(3 + len(prefix))
buf.WriteString(prefix)
buf.WriteByte('=')
if val {
buf.WriteByte('1')
} else {
buf.WriteByte('0')
}
buf.WriteByte('\n')
}
func() {
// lock required resources
@@ -84,6 +95,9 @@ func (device *Device) IpcGetOperation(w io.Writer) error {
device.peers.RLock()
defer device.peers.RUnlock()
device.headerProtection.RLock()
defer device.headerProtection.RUnlock()
// serialize device related values
if !device.staticIdentity.privateKey.IsZero() {
@@ -98,69 +112,81 @@ func (device *Device) IpcGetOperation(w io.Writer) error {
sendf("fwmark=%d", device.net.fwmark)
}
if device.net.preludeOpts.Jc != 0 {
sendf("jc=%d", device.net.preludeOpts.Jc)
if count := device.junk.count.Load(); count != 0 {
sendf("jc=%d", count)
}
if device.net.preludeOpts.Jmin != 0 {
sendf("jmin=%d", device.net.preludeOpts.Jmin)
if min := device.junk.min.Load(); min != 0 {
sendf("jmin=%d", min)
}
if device.net.preludeOpts.Jmax != 0 {
sendf("jmax=%d", device.net.preludeOpts.Jmax)
if max := device.junk.max.Load(); max != 0 {
sendf("jmax=%d", max)
}
if device.net.framedOpts.S1 != 0 {
sendf("s1=%d", device.net.framedOpts.S1)
if padding := device.paddings.init.Load(); padding != 0 {
sendf("s1=%d", padding)
}
if device.net.framedOpts.S2 != 0 {
sendf("s2=%d", device.net.framedOpts.S2)
if padding := device.paddings.response.Load(); padding != 0 {
sendf("s2=%d", padding)
}
if device.net.framedOpts.S3 != 0 {
sendf("s3=%d", device.net.framedOpts.S3)
if padding := device.paddings.cookie.Load(); padding != 0 {
sendf("s3=%d", padding)
}
if device.net.framedOpts.S4 != 0 {
sendf("s4=%d", device.net.framedOpts.S4)
if padding := device.paddings.transport.Load(); padding != 0 {
sendf("s4=%d", padding)
}
if device.net.framedOpts.H1 != nil {
sendf("h1=%s", device.net.framedOpts.H1.GenSpec())
if header := device.headers.init.Load(); !header.IsZero() {
sendf("h1=%s", header.ToString())
}
if device.net.framedOpts.H2 != nil {
sendf("h2=%s", device.net.framedOpts.H2.GenSpec())
if header := device.headers.response.Load(); !header.IsZero() {
sendf("h2=%s", header.ToString())
}
if device.net.framedOpts.H3 != nil {
sendf("h3=%s", device.net.framedOpts.H3.GenSpec())
if header := device.headers.cookie.Load(); !header.IsZero() {
sendf("h3=%s", header.ToString())
}
if device.net.framedOpts.H4 != nil {
sendf("h4=%s", device.net.framedOpts.H4.GenSpec())
if header := device.headers.transport.Load(); !header.IsZero() {
sendf("h4=%s", header.ToString())
}
for i, rules := range device.net.preludeOpts.RulesArr {
if rules != nil {
sendf("i%d=%s", i+1, rules.Spec())
for i, ipacket := range device.ipackets {
if ipacket != nil {
sendf("i%d=%s", i+1, ipacket.Spec)
}
}
if len(device.net.network) > 0 {
sendf("network=%s", device.net.network)
if !device.headerProtection.key.IsZero() {
keyf("header_protection_key", (*[32]byte)(&device.headerProtection.key))
}
if device.net.masqueradeOpts.RulesIn != nil {
sendf("format_in=%s", device.net.masqueradeOpts.RulesIn.Spec())
if addition := device.contentPaddingAddition.Load(); !addition.IsZero() {
sendf("content_padding_addition=%s", addition.ToString())
}
if device.net.masqueradeOpts.RulesOut != nil {
sendf("format_out=%s", device.net.masqueradeOpts.RulesOut.Spec())
if timing := device.timings.rekeyAfterTimeSec.Load(); !timing.IsZero() {
sendf("rekey_after_time=%s", timing.ToString())
}
sendf("header_compat=%s", strconv.FormatBool(device.net.framedOpts.HeaderCompat))
if timing := device.timings.rekeyTimeoutSec.Load(); !timing.IsZero() {
sendf("rekey_timeout=%s", timing.ToString())
}
if timing := device.timings.rejectAfterTimeSec.Load(); !timing.IsZero() {
sendf("reject_after_time=%s", timing.ToString())
}
if timing := device.timings.keepaliveTimeoutSec.Load(); !timing.IsZero() {
sendf("keepalive_timeout=%s", timing.ToString())
}
if rang := device.timings.maxHandshakeAttemps.Load(); !rang.IsZero() {
sendf("max_handshake_attempts=%s", rang.ToString())
}
boolf("random_trailers", device.randomTrailers.Load())
boolf("disable_cookies", device.disableCookies.Load())
for _, peer := range device.peers.keyMap {
// Serialize peer state.
@@ -183,7 +209,10 @@ func (device *Device) IpcGetOperation(w io.Writer) error {
sendf("last_handshake_time_nsec=%d", nano)
sendf("tx_bytes=%d", peer.txBytes.Load())
sendf("rx_bytes=%d", peer.rxBytes.Load())
sendf("persistent_keepalive_interval=%d", peer.persistentKeepaliveInterval.Load())
if keepalive := peer.persistentKeepaliveInterval.Load(); !keepalive.IsZero() {
sendf("persistent_keepalive_interval=%s", keepalive.ToString())
}
device.allowedips.EntriesForPeer(peer, func(prefix netip.Prefix) bool {
sendf("allowed_ip=%s", prefix.String())
@@ -212,6 +241,8 @@ func (device *Device) IpcSetOperation(r io.Reader) (err error) {
}
}()
ipcDev := new(ipcSetDevice)
ipcDev.fromDevice(device)
peer := new(ipcSetPeer)
deviceConfig := true
@@ -220,6 +251,10 @@ 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
}
@@ -247,7 +282,7 @@ func (device *Device) IpcSetOperation(r io.Reader) (err error) {
var err error
if deviceConfig {
err = device.handleDeviceLine(key, value)
err = device.handleDeviceLine(ipcDev, key, value)
} else {
err = device.handlePeerLine(peer, key, value)
}
@@ -255,6 +290,10 @@ 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 {
@@ -263,7 +302,7 @@ func (device *Device) IpcSetOperation(r io.Reader) (err error) {
return nil
}
func (device *Device) handleDeviceLine(key, value string) error {
func (device *Device) handleDeviceLine(ipcDev *ipcSetDevice, key, value string) error {
switch key {
case "private_key":
var sk NoisePrivateKey
@@ -314,305 +353,195 @@ func (device *Device) handleDeviceLine(key, value string) error {
device.RemoveAllPeers()
case "jc":
jc, err := strconv.Atoi(value)
jc, err := strconv.ParseUint(value, 10, 32)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse jc: %w", err)
}
if jc <= 0 {
return ipcErrorf(ipc.IpcErrorInvalid, "jc must be a positive value")
}
device.log.Verbosef("UAPI: Updating junk count")
device.net.preludeOpts.Jc = jc
if err := device.BindUpdate(); err != nil {
return ipcErrorf(ipc.IpcErrorPortInUse, "failed to set junk count: %w", err)
}
device.log.Verbosef("UAPI: Updating junk count")
device.junk.count.Store(uint32(jc))
case "jmin":
jmin, err := strconv.Atoi(value)
jmin, err := strconv.ParseUint(value, 10, 32)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse jmin: %w", err)
}
if jmin <= 0 {
return ipcErrorf(ipc.IpcErrorInvalid, "jmin must be a positive value")
}
device.log.Verbosef("UAPI: Updating junk min")
device.net.preludeOpts.Jmin = jmin
if err := device.BindUpdate(); err != nil {
return ipcErrorf(ipc.IpcErrorPortInUse, "failed to set junk min: %w", err)
}
device.junk.min.Store(uint32(jmin))
case "jmax":
jmax, err := strconv.Atoi(value)
jmax, err := strconv.ParseUint(value, 10, 32)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse jmax: %w", err)
}
if jmax <= 0 {
return ipcErrorf(ipc.IpcErrorInvalid, "jmax must be a positive value")
}
device.log.Verbosef("UAPI: Updating junk max")
device.net.preludeOpts.Jmax = jmax
if err := device.BindUpdate(); err != nil {
return ipcErrorf(ipc.IpcErrorPortInUse, "failed to set junk max: %w", err)
}
device.junk.max.Store(uint32(jmax))
case "s1":
padding, err := strconv.Atoi(value)
padding, err := strconv.ParseUint(value, 10, 16)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse s1: %w", err)
}
if padding < 0 {
return ipcErrorf(ipc.IpcErrorInvalid, "s1 must be non-negative")
}
device.log.Verbosef("UAPI: Updating s1 padding")
device.net.framedOpts.S1 = padding
if err := device.BindUpdate(); err != nil {
return ipcErrorf(ipc.IpcErrorPortInUse, "failed to set s1: %w", err)
}
ipcDev.paddings.init = uint32(padding)
case "s2":
padding, err := strconv.Atoi(value)
padding, err := strconv.ParseUint(value, 10, 16)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse s2: %w", err)
}
if padding < 0 {
return ipcErrorf(ipc.IpcErrorInvalid, "s2 must be non-negative")
}
device.log.Verbosef("UAPI: Updating s2 padding")
device.net.framedOpts.S2 = padding
if err := device.BindUpdate(); err != nil {
return ipcErrorf(ipc.IpcErrorPortInUse, "failed to set s2: %w", err)
}
ipcDev.paddings.response = uint32(padding)
case "s3":
padding, err := strconv.Atoi(value)
padding, err := strconv.ParseUint(value, 10, 16)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse s3: %w", err)
}
if padding < 0 {
return ipcErrorf(ipc.IpcErrorInvalid, "s3 must be non-negative")
}
device.log.Verbosef("UAPI: Updating s3 padding")
device.net.framedOpts.S3 = padding
if err := device.BindUpdate(); err != nil {
return ipcErrorf(ipc.IpcErrorPortInUse, "failed to set s3: %w", err)
}
ipcDev.paddings.cookie = uint32(padding)
case "s4":
padding, err := strconv.Atoi(value)
padding, err := strconv.ParseUint(value, 10, 16)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse s4: %w", err)
}
if padding < 0 {
return ipcErrorf(ipc.IpcErrorInvalid, "s4 must be non-negative")
}
device.log.Verbosef("UAPI: Updating s4 padding")
device.net.framedOpts.S4 = padding
if err := device.BindUpdate(); err != nil {
return ipcErrorf(ipc.IpcErrorPortInUse, "failed to set s4: %w", err)
}
ipcDev.paddings.transport = uint32(padding)
case "h1":
header, err := conceal.NewRangedHeader(value)
if err != nil {
var rang UintRange
if err := rang.FromString(value); err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse H1: %w", err)
}
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)
}
ipcDev.headers.init = rang
case "h2":
header, err := conceal.NewRangedHeader(value)
if err != nil {
var rang UintRange
if err := rang.FromString(value); err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse H2: %w", err)
}
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)
}
ipcDev.headers.response = rang
case "h3":
header, err := conceal.NewRangedHeader(value)
if err != nil {
var rang UintRange
if err := rang.FromString(value); err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse H3: %w", err)
}
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)
}
ipcDev.headers.cookie = rang
case "h4":
header, err := conceal.NewRangedHeader(value)
if err != nil {
var rang UintRange
if err := rang.FromString(value); err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse H4: %w", err)
}
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)
}
ipcDev.headers.transport = rang
case "i1":
rules, err := conceal.ParseRules(value)
chain, err := newObfChain(value)
if err != nil {
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)
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse I1: %w", err)
}
device.ipackets[0] = chain
case "i2":
rules, err := conceal.ParseRules(value)
chain, err := newObfChain(value)
if err != nil {
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)
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse I2: %w", err)
}
device.ipackets[1] = chain
case "i3":
rules, err := conceal.ParseRules(value)
chain, err := newObfChain(value)
if err != nil {
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)
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse I3: %w", err)
}
device.ipackets[2] = chain
case "i4":
rules, err := conceal.ParseRules(value)
chain, err := newObfChain(value)
if err != nil {
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)
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse I4: %w", err)
}
device.ipackets[3] = chain
case "i5":
rules, err := conceal.ParseRules(value)
chain, err := newObfChain(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.ipackets[4] = chain
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)
case "header_protection_key":
var key HeaderCipherKey
err := key.FromHex(value)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse rules: %w", err)
return ipcErrorf(ipc.IpcErrorInvalid, "failed to set header_protection_key: %w", err)
}
ipcDev.headerProtectionKey = key
case "content_padding_addition":
var rang UintRange
if err := rang.FromString(value); err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse content_padding_addition: %w", err)
}
device.log.Verbosef("UAPI: Updating format_in")
device.net.masqueradeOpts.RulesIn = rules
device.log.Verbosef("UAPI: Updating content padding addition")
device.contentPaddingAddition.Store(rang)
if err := device.BindUpdate(); err != nil {
// TODO: change IpcErrorPortInUse to something reasonable
return ipcErrorf(ipc.IpcErrorPortInUse, "failed to set format_in: %w", err)
case "rekey_after_time":
var rang UintRange
if err := rang.FromString(value); err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse rekey after time: %w", err)
}
device.log.Verbosef("UAPI: Updating rekey after time")
device.timings.rekeyAfterTimeSec.Store(rang)
case "format_out":
rules, err := conceal.ParseRules(value)
case "rekey_timeout":
var rang UintRange
if err := rang.FromString(value); err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse rekey timeout: %w", err)
}
device.log.Verbosef("UAPI: Updating rekey timeout")
device.timings.rekeyTimeoutSec.Store(rang)
case "reject_after_time":
var rang UintRange
if err := rang.FromString(value); err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse reject after time: %w", err)
}
device.log.Verbosef("UAPI: Updating reject after time")
device.timings.rejectAfterTimeSec.Store(rang)
case "keepalive_timeout":
var rang UintRange
if err := rang.FromString(value); err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse keepalive timeout: %w", err)
}
device.log.Verbosef("UAPI: Updating keepalive timeout")
device.timings.keepaliveTimeoutSec.Store(rang)
case "max_handshake_attempts":
var rang UintRange
if err := rang.FromString(value); err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse max handshake attempts: %w", err)
}
device.log.Verbosef("UAPI: Updating max handshake attempts")
device.timings.maxHandshakeAttemps.Store(rang)
case "random_trailers":
val, err := strconv.ParseBool(value)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse rules: %w", err)
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse random trailers: %w", err)
}
device.log.Verbosef("UAPI: Updating random trailers")
device.randomTrailers.Store(val)
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)
case "disable_cookies":
val, 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)
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse disable cookies: %w", err)
}
device.log.Verbosef("UAPI: Updating disable cookies")
device.disableCookies.Store(val)
default:
return ipcErrorf(ipc.IpcErrorInvalid, "invalid UAPI device key: %v", key)
@@ -734,19 +663,15 @@ func (device *Device) handlePeerLine(
case "persistent_keepalive_interval":
device.log.Verbosef("%v - UAPI: Updating persistent keepalive interval", peer.Peer)
secs, err := strconv.ParseUint(value, 10, 16)
if err != nil {
return ipcErrorf(
ipc.IpcErrorInvalid,
"failed to set persistent keepalive interval: %w",
err,
)
var rang UintRange
if err := rang.FromString(value); err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to set persistent keepalive interval: %w", err)
}
old := peer.persistentKeepaliveInterval.Swap(uint32(secs))
old := peer.persistentKeepaliveInterval.Swap(rang)
// Send immediate keepalive if we're turning it on and before it wasn't on.
peer.pkaOn = old == 0 && secs != 0
peer.pkaOn = old.IsZero() && !rang.IsZero()
case "replace_allowed_ips":
device.log.Verbosef("%v - UAPI: Removing all allowedips", peer.Peer)
@@ -862,3 +787,91 @@ func (device *Device) IpcHandle(socket net.Conn) {
buffered.Flush()
}
}
type ipcSetDevice struct {
headers struct {
init UintRange
response UintRange
cookie UintRange
transport UintRange
}
paddings struct {
init uint32
response uint32
cookie uint32
transport uint32
}
headerProtectionKey HeaderCipherKey
}
func (d *ipcSetDevice) fromDevice(device *Device) {
device.headerProtection.RLock()
defer device.headerProtection.RUnlock()
d.headers.init = device.headers.init.Load()
d.headers.response = device.headers.response.Load()
d.headers.cookie = device.headers.cookie.Load()
d.headers.transport = device.headers.transport.Load()
d.paddings.init = device.paddings.init.Load()
d.paddings.response = device.paddings.response.Load()
d.paddings.cookie = device.paddings.cookie.Load()
d.paddings.transport = device.paddings.transport.Load()
d.headerProtectionKey = device.headerProtection.key
}
func (d *ipcSetDevice) mergeWithDevice(device *Device) error {
device.headerProtection.Lock()
defer device.headerProtection.Unlock()
headers := []UintRange{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.Overlap(right) {
return errors.New("headers must not overlap")
}
}
}
device.log.Verbosef("UAPI: Updating h1 padding")
device.headers.init.Store(d.headers.init)
device.log.Verbosef("UAPI: Updating h2 padding")
device.headers.response.Store(d.headers.response)
device.log.Verbosef("UAPI: Updating h3 padding")
device.headers.cookie.Store(d.headers.cookie)
device.log.Verbosef("UAPI: Updating h4 padding")
device.headers.transport.Store(d.headers.transport)
if !d.headerProtectionKey.IsZero() {
paddings := []uint32{d.paddings.init, d.paddings.response, d.paddings.cookie, d.paddings.transport}
for i, padding := range paddings {
if padding < HeaderCipherNonceSize {
return fmt.Errorf("S%d must be more then %d to use headerProtection", i, HeaderCipherNonceSize)
}
}
}
device.log.Verbosef("UAPI: Updating s1 padding")
device.paddings.init.Store(d.paddings.init)
device.log.Verbosef("UAPI: Updating s2 padding")
device.paddings.response.Store(d.paddings.response)
device.log.Verbosef("UAPI: Updating s3 padding")
device.paddings.cookie.Store(d.paddings.cookie)
device.log.Verbosef("UAPI: Updating s4 padding")
device.paddings.transport.Store(d.paddings.transport)
device.log.Verbosef("UAPI: Updating header protection key")
device.headerProtection.key = d.headerProtectionKey
return nil
}
+12 -5
View File
@@ -1,12 +1,12 @@
module github.com/amnezia-vpn/amneziawg-go
module github.com/amnezia-vpn/amneziawg-go/v3
go 1.24.4
go 1.25.0
require (
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.getoutline.org/sdk v0.0.23
golang.getoutline.org/sdk/x v0.2.0
golang.org/x/crypto v0.42.0
golang.org/x/net v0.44.0
golang.org/x/sys v0.36.0
@@ -15,14 +15,21 @@ require (
)
require (
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/onsi/ginkgo/v2 v2.12.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.48.1 // indirect
github.com/shadowsocks/go-shadowsocks2 v0.1.5 // indirect
github.com/stretchr/testify v1.10.0 // indirect
go.uber.org/mock v0.4.0 // indirect
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // 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/text v0.29.0 // indirect
golang.org/x/time v0.9.0 // indirect
golang.org/x/tools v0.37.0 // indirect
golang.org/x/tools/go/expect v0.1.1-deprecated // indirect
)
+38 -4
View File
@@ -1,35 +1,62 @@
github.com/Jigsaw-Code/outline-sdk v0.0.20 h1:4ep7MK9lFmcyPIRIbn4xrP1VKdJNsqR6+iJEOHDKnNg=
github.com/Jigsaw-Code/outline-sdk v0.0.20/go.mod h1:CFDKyGZA4zatKE4vMLe8TyQpZCyINOeRFbMAmYHxodw=
github.com/Jigsaw-Code/outline-sdk/x v0.0.8 h1:fFHFXW7CKhRiegyNSdP25S/WIiVrRnMKysHDoO/N2Xg=
github.com/Jigsaw-Code/outline-sdk/x v0.0.8/go.mod h1:zqSH7yEYIQ0pYOhrr4QnodATVb5X/eZXV4AjUp9zhvs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
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/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
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/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
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/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
github.com/onsi/ginkgo/v2 v2.12.0 h1:UIVDowFPwpg6yMUpPjGkYvf06K3RAiJXUhCxEwQVHRI=
github.com/onsi/ginkgo/v2 v2.12.0/go.mod h1:ZNEzXISYlqpb8S36iN71ifqLi3vVD1rVJGvWRCJOUpQ=
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
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/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.48.1 h1:y/8xmfWI9qmGTc+lBr4jKRUWLGSlSigv847ULJ4hYXA=
github.com/quic-go/quic-go v0.48.1/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs=
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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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/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=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
golang.getoutline.org/sdk v0.0.23 h1:UKoKCrRH3Ed6Jpg8ODYwOo6O1B1lvfIHPreMrTkSTcc=
golang.getoutline.org/sdk v0.0.23/go.mod h1:nKZlO//e/sRFk+rp8gm8EJ5RasDSyY+fGDNSd3I2iaA=
golang.getoutline.org/sdk/x v0.2.0 h1:4kuT2SgkPXktwPwT6CXuF+Jwe+COAGqiBDZbgJkS5fM=
golang.getoutline.org/sdk/x v0.2.0/go.mod h1:vyWoHW0PUsnRHdLXiDGv1Z8X48Asp2ALgwIQfJfAaNs=
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.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
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=
@@ -41,10 +68,13 @@ 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.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
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=
@@ -55,6 +85,10 @@ golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTF
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=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
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=
+1 -1
View File
@@ -20,7 +20,7 @@ import (
"testing"
"time"
"github.com/amnezia-vpn/amneziawg-go/ipc/namedpipe"
"github.com/amnezia-vpn/amneziawg-go/v3/ipc/namedpipe"
"golang.org/x/sys/windows"
)
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"net"
"os"
"github.com/amnezia-vpn/amneziawg-go/rwcancel"
"github.com/amnezia-vpn/amneziawg-go/v3/rwcancel"
"golang.org/x/sys/unix"
)
+1 -1
View File
@@ -8,7 +8,7 @@ package ipc
import (
"net"
"github.com/amnezia-vpn/amneziawg-go/ipc/namedpipe"
"github.com/amnezia-vpn/amneziawg-go/v3/ipc/namedpipe"
"golang.org/x/sys/windows"
)
+4 -4
View File
@@ -14,10 +14,10 @@ import (
"runtime"
"strconv"
"github.com/amnezia-vpn/amneziawg-go/conn"
"github.com/amnezia-vpn/amneziawg-go/device"
"github.com/amnezia-vpn/amneziawg-go/ipc"
"github.com/amnezia-vpn/amneziawg-go/tun"
"github.com/amnezia-vpn/amneziawg-go/v3/conn"
"github.com/amnezia-vpn/amneziawg-go/v3/device"
"github.com/amnezia-vpn/amneziawg-go/v3/ipc"
"github.com/amnezia-vpn/amneziawg-go/v3/tun"
"golang.org/x/sys/unix"
)
+4 -4
View File
@@ -12,11 +12,11 @@ import (
"golang.org/x/sys/windows"
"github.com/amnezia-vpn/amneziawg-go/conn"
"github.com/amnezia-vpn/amneziawg-go/device"
"github.com/amnezia-vpn/amneziawg-go/ipc"
"github.com/amnezia-vpn/amneziawg-go/v3/conn"
"github.com/amnezia-vpn/amneziawg-go/v3/device"
"github.com/amnezia-vpn/amneziawg-go/v3/ipc"
"github.com/amnezia-vpn/amneziawg-go/tun"
"github.com/amnezia-vpn/amneziawg-go/v3/tun"
)
const (
+4 -4
View File
@@ -6,10 +6,10 @@ import (
"net"
"net/netip"
"github.com/Jigsaw-Code/outline-sdk/transport"
"github.com/amnezia-vpn/amneziawg-go/conn"
"github.com/amnezia-vpn/amneziawg-go/device"
"github.com/amnezia-vpn/amneziawg-go/tun/netstack"
"github.com/amnezia-vpn/amneziawg-go/v3/conn"
"github.com/amnezia-vpn/amneziawg-go/v3/device"
"github.com/amnezia-vpn/amneziawg-go/v3/tun/netstack"
"golang.getoutline.org/sdk/transport"
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
)
+69 -24
View File
@@ -9,34 +9,43 @@ import (
"strconv"
"strings"
"github.com/Jigsaw-Code/outline-sdk/transport"
"github.com/Jigsaw-Code/outline-sdk/x/mobileproxy"
"github.com/Jigsaw-Code/outline-sdk/x/smart"
"github.com/goccy/go-yaml"
"golang.getoutline.org/sdk/transport"
"golang.getoutline.org/sdk/x/mobileproxy"
"golang.getoutline.org/sdk/x/smart"
)
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"`
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"`
HeaderProtectionKey string `yaml:"header_protection_key,omitempty"`
ContentPaddingAddition string `yaml:"content_padding_addition,omitempty"`
RekeyAfterTime string `yaml:"rekey_after_time,omitempty"`
RekeyTimeout string `yaml:"rekey_timeout,omitempty"`
RejectAfterTime string `yaml:"reject_after_time,omitempty"`
KeepaliveTimeout string `yaml:"keepalive_timeout,omitempty"`
MaxHandshakeAttempts string `yaml:"max_handshake_attempts,omitempty"`
RandomTrailers string `yaml:"random_trailers,omitempty"`
DisableCookies string `yaml:"disable_cookies,omitempty"`
Peers []PeerConfig `yaml:"peers,omitempty"`
}
type PeerConfig struct {
@@ -136,6 +145,42 @@ func genIpcString(cfg *DeviceConfig) (string, error) {
b.WriteString("\ni5=")
b.WriteString(cfg.I5)
}
if cfg.HeaderProtectionKey != "" {
b.WriteString("\nheader_protection_key=")
b.WriteString(cfg.HeaderProtectionKey)
}
if cfg.ContentPaddingAddition != "" {
b.WriteString("\ncontent_padding_addition=")
b.WriteString(cfg.ContentPaddingAddition)
}
if cfg.RekeyAfterTime != "" {
b.WriteString("\nrekey_after_time=")
b.WriteString(cfg.RekeyAfterTime)
}
if cfg.RekeyTimeout != "" {
b.WriteString("\nrekey_timeout=")
b.WriteString(cfg.RekeyTimeout)
}
if cfg.RejectAfterTime != "" {
b.WriteString("\nreject_after_time=")
b.WriteString(cfg.RejectAfterTime)
}
if cfg.KeepaliveTimeout != "" {
b.WriteString("\nkeepalive_timeout=")
b.WriteString(cfg.KeepaliveTimeout)
}
if cfg.MaxHandshakeAttempts != "" {
b.WriteString("\nmax_handshake_attempts=")
b.WriteString(cfg.MaxHandshakeAttempts)
}
if cfg.RandomTrailers != "" {
b.WriteString("\nrandom_trailers=")
b.WriteString(cfg.RandomTrailers)
}
if cfg.DisableCookies != "" {
b.WriteString("\ndisable_cookies=")
b.WriteString(cfg.DisableCookies)
}
for _, peer := range cfg.Peers {
publicKeyBytes, err := base64.StdEncoding.DecodeString(peer.PublicKey)
+3 -3
View File
@@ -3,8 +3,8 @@ package outline_test
import (
"testing"
"github.com/Jigsaw-Code/outline-sdk/x/mobileproxy"
awg "github.com/amnezia-vpn/amneziawg-go/outline"
awg "github.com/amnezia-vpn/amneziawg-go/v3/outline"
"golang.getoutline.org/sdk/x/mobileproxy"
)
const cfg = `
@@ -46,7 +46,7 @@ func Test_outlineIntegration(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if _, err = mobileproxy.RunProxy("", dialer); err != nil {
if _, err = mobileproxy.RunProxy("127.0.0.1:0", dialer); err != nil {
t.Fatal(err)
}
}
-370
View File
@@ -1,370 +0,0 @@
#!/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}"
+3 -3
View File
@@ -13,9 +13,9 @@ import (
"net/http"
"net/netip"
"github.com/amnezia-vpn/amneziawg-go/conn"
"github.com/amnezia-vpn/amneziawg-go/device"
"github.com/amnezia-vpn/amneziawg-go/tun/netstack"
"github.com/amnezia-vpn/amneziawg-go/v3/conn"
"github.com/amnezia-vpn/amneziawg-go/v3/device"
"github.com/amnezia-vpn/amneziawg-go/v3/tun/netstack"
)
func main() {
+3 -3
View File
@@ -14,9 +14,9 @@ import (
"net/http"
"net/netip"
"github.com/amnezia-vpn/amneziawg-go/conn"
"github.com/amnezia-vpn/amneziawg-go/device"
"github.com/amnezia-vpn/amneziawg-go/tun/netstack"
"github.com/amnezia-vpn/amneziawg-go/v3/conn"
"github.com/amnezia-vpn/amneziawg-go/v3/device"
"github.com/amnezia-vpn/amneziawg-go/v3/tun/netstack"
)
func main() {
+3 -3
View File
@@ -17,9 +17,9 @@ import (
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
"github.com/amnezia-vpn/amneziawg-go/conn"
"github.com/amnezia-vpn/amneziawg-go/device"
"github.com/amnezia-vpn/amneziawg-go/tun/netstack"
"github.com/amnezia-vpn/amneziawg-go/v3/conn"
"github.com/amnezia-vpn/amneziawg-go/v3/device"
"github.com/amnezia-vpn/amneziawg-go/v3/tun/netstack"
)
func main() {
+1 -1
View File
@@ -22,7 +22,7 @@ import (
"syscall"
"time"
"github.com/amnezia-vpn/amneziawg-go/tun"
"github.com/amnezia-vpn/amneziawg-go/v3/tun"
"golang.org/x/net/dns/dnsmessage"
"gvisor.dev/gvisor/pkg/buffer"
+1 -1
View File
@@ -12,7 +12,7 @@ import (
"io"
"unsafe"
"github.com/amnezia-vpn/amneziawg-go/conn"
"github.com/amnezia-vpn/amneziawg-go/v3/conn"
"golang.org/x/sys/unix"
)
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"net/netip"
"testing"
"github.com/amnezia-vpn/amneziawg-go/conn"
"github.com/amnezia-vpn/amneziawg-go/v3/conn"
"golang.org/x/sys/unix"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/header"
+2 -2
View File
@@ -17,8 +17,8 @@ import (
"time"
"unsafe"
"github.com/amnezia-vpn/amneziawg-go/conn"
"github.com/amnezia-vpn/amneziawg-go/rwcancel"
"github.com/amnezia-vpn/amneziawg-go/v3/conn"
"github.com/amnezia-vpn/amneziawg-go/v3/rwcancel"
"golang.org/x/sys/unix"
)
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"net/netip"
"os"
"github.com/amnezia-vpn/amneziawg-go/tun"
"github.com/amnezia-vpn/amneziawg-go/v3/tun"
)
func Ping(dst, src netip.Addr) []byte {