Skip to content

Commit e016f9e

Browse files
committed
tun, device: allocate buffers in the edge devices
Signed-off-by: Alex Valiushko <alexvaliushko@tailscale.com> Change-Id: I58908d9d3fd09441e9378a74b0ee19136a6a6964
1 parent 0c2c411 commit e016f9e

33 files changed

Lines changed: 896 additions & 219 deletions

buffer/buffer.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/* SPDX-License-Identifier: MIT
2+
*
3+
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved.
4+
*/
5+
6+
// Package buffer implements a reusable buffer abstraction.
7+
//
8+
// Wireguard-go's data processing is constrained by both the hosts API,
9+
// and the transformations performed during encapsulation:
10+
//
11+
// 1. Encryption requires tail- and headroom for extra headers and padding.
12+
// Available via winrio, and pread(2).
13+
// 2. Systems are moving towards coalesced reads for both TCP and UDP.
14+
// The read data has no gaps for individual slices.
15+
// 3. crypto.AEAD interface requires a contiguous dst []byte for Sealing.
16+
// So we can't use scatter-gather to inject the gaps.
17+
//
18+
// Until one of these three conditions is changed, the encryption strategy
19+
// is to copy on read into buffers with the required gaps.
20+
// The buffers are right-sized for the packet to avoid memory inflation.
21+
// To recycle said allocations, each buffer carries a recycle function
22+
// that routes it back to its originating pool.
23+
//
24+
// Decryption shrinks each fragment instead of growing, so buffers can pass
25+
// through the pipeline without copying till the egress coalescence.
26+
// Depending on the chosen head of the coalescence, there may or may be no room
27+
// and reallocation is a necessary fallback until we start passing
28+
// buffers in batches.
29+
30+
package buffer
31+
32+
import "fmt"
33+
34+
// Recycler holds state necessary for a correct Buffer return to its originating Source
35+
type Recycler interface {
36+
Recycle(*Buffer)
37+
}
38+
39+
// RecycleFunc adapts arbitrary closures to the Recycler interface.
40+
type RecycleFunc func(*Buffer)
41+
42+
func (f RecycleFunc) Recycle(b *Buffer) {
43+
f(b)
44+
}
45+
46+
// Buffer is a reusable slice of bytes of fixed length.
47+
// Buffer or its Data must not be retained past Release.
48+
type Buffer struct {
49+
data []byte
50+
recycler Recycler
51+
}
52+
53+
// New creates Buffer referencing the provided data and Recycler.
54+
func New(data []byte, recycler Recycler) *Buffer {
55+
return &Buffer{data: data, recycler: recycler}
56+
}
57+
58+
// Bytes returns the valid data in the Buffer.
59+
func (b *Buffer) Bytes() []byte {
60+
return b.data
61+
}
62+
63+
// SetLen sets the length of the valid data in the Buffer.
64+
// Intended to be used for truncating the valid data post read,
65+
// or extending post encryption. Does not check the capacity.
66+
func (b *Buffer) SetLen(l int) {
67+
b.data = b.data[:l]
68+
}
69+
70+
// Ensure returns a Buffer of the requested len, with the valid data from the provided Buffer.
71+
// The returned Buffer may be the same as the provided Buffer if it has sufficient capacity, or a new Buffer otherwise.
72+
//
73+
// Safe to call on a nil Buffer. Performs no nil check on the Source or bounds check on the size.
74+
func Ensure(b *Buffer, size int, src Source) *Buffer {
75+
if b == nil {
76+
return src.Get(size)
77+
}
78+
if size > cap(b.data) {
79+
bb := src.Get(size)
80+
n := copy(bb.data, b.data)
81+
if n != len(b.data) {
82+
panic(fmt.Sprintf("short copy: %d != %d", n, len(b.data)))
83+
}
84+
Release(b)
85+
return bb
86+
}
87+
b.SetLen(size)
88+
return b
89+
}
90+
91+
// Release returns Buffer to its Source for reuse.
92+
// Safe to call on a nil Buffer.
93+
func Release(b *Buffer) {
94+
if b == nil {
95+
return
96+
}
97+
b.data = b.data[:cap(b.data)]
98+
if b.recycler != nil {
99+
b.recycler.Recycle(b)
100+
}
101+
}
102+
103+
// Claim takes ownership of the Buffer at *slot, setting *slot to nil.
104+
// Returns the claimed Buffer (may be nil if slot was already nil).
105+
func Claim(slot **Buffer) *Buffer {
106+
b := *slot
107+
*slot = nil
108+
return b
109+
}
110+
111+
// ReleaseAll calls Release on each non-nil Buffer in the slice, and sets the slice elements to nil.
112+
func ReleaseAll(bs []*Buffer) {
113+
for i := range bs {
114+
Release(bs[i])
115+
bs[i] = nil
116+
}
117+
}

buffer/buffer_test.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package buffer
2+
3+
import (
4+
"testing"
5+
)
6+
7+
type countingRecycler struct {
8+
count int
9+
}
10+
11+
func (r *countingRecycler) Recycle(b *Buffer) {
12+
r.count++
13+
}
14+
15+
func TestEnsure(t *testing.T) {
16+
for _, tc := range []struct {
17+
name string
18+
buf *Buffer
19+
size int
20+
wantKept bool
21+
}{
22+
{"nil buffer", nil, 100, false},
23+
{"sufficient cap", New(make([]byte, 200), &countingRecycler{}), 100, true},
24+
{"insufficient cap", New(make([]byte, 100), &countingRecycler{}), 200, false},
25+
} {
26+
t.Run(tc.name, func(t *testing.T) {
27+
var r *countingRecycler
28+
if tc.buf != nil {
29+
r = tc.buf.recycler.(*countingRecycler)
30+
}
31+
buf := Ensure(tc.buf, tc.size, NewPoolSource())
32+
if len(buf.Bytes()) != tc.size {
33+
t.Errorf("len = %d, want %d", len(buf.Bytes()), tc.size)
34+
}
35+
if tc.wantKept != (buf == tc.buf) {
36+
t.Errorf("kept same buffer: got %v, want %v", buf == tc.buf, tc.wantKept)
37+
}
38+
if r != nil && !tc.wantKept && r.count != 1 {
39+
t.Errorf("recycler not called, count = %d, want 1", r.count)
40+
}
41+
})
42+
}
43+
}
44+
45+
func TestRelease(t *testing.T) {
46+
for _, tc := range []struct {
47+
name string
48+
buf *Buffer
49+
}{
50+
{"nil buffer", nil},
51+
{"nil recycler", New(nil, nil)},
52+
{"non-nil recycler", New(make([]byte, 10), &countingRecycler{})},
53+
} {
54+
t.Run(tc.name, func(t *testing.T) {
55+
var r *countingRecycler
56+
if tc.buf != nil {
57+
r, _ = tc.buf.recycler.(*countingRecycler)
58+
}
59+
Release(tc.buf) // must not panic
60+
if r != nil && r.count != 1 {
61+
t.Errorf("recycler not called, count = %d, want 1", r.count)
62+
}
63+
})
64+
}
65+
66+
}
67+
68+
func TestClaim(t *testing.T) {
69+
for _, tc := range []struct {
70+
name string
71+
slot *Buffer
72+
}{
73+
{"nil slot", nil},
74+
{"non-nil slot", New(make([]byte, 10), nil)},
75+
} {
76+
t.Run(tc.name, func(t *testing.T) {
77+
want := tc.slot
78+
claimed := Claim(&tc.slot)
79+
if tc.slot != nil {
80+
t.Error("slot should be nil after Claim")
81+
}
82+
if claimed != want {
83+
t.Error("claimed buffer does not match original")
84+
}
85+
})
86+
}
87+
}
88+
89+
func TestReleaseAll(t *testing.T) {
90+
for _, tc := range []struct {
91+
name string
92+
bufs []*Buffer
93+
}{
94+
{"nil slice", nil},
95+
{"empty slice", []*Buffer{}},
96+
{"slice with nil buffers", []*Buffer{nil, nil}},
97+
{"slice with non-nil buffers", []*Buffer{New(make([]byte, 10), nil), New(make([]byte, 20), nil)}},
98+
} {
99+
t.Run(tc.name, func(t *testing.T) {
100+
l := len(tc.bufs)
101+
ReleaseAll(tc.bufs) // must not panic
102+
if len(tc.bufs) != l {
103+
t.Errorf("length of bufs changed: got %d, want %d", len(tc.bufs), l)
104+
}
105+
for i, b := range tc.bufs {
106+
if b != nil {
107+
t.Errorf("bufs[%d] should be nil after ReleaseAll", i)
108+
}
109+
}
110+
})
111+
}
112+
}

buffer/constants.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/* SPDX-License-Identifier: MIT
2+
*
3+
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved.
4+
*/
5+
6+
package buffer
7+
8+
const (
9+
MaxBufferSize = MaxSegmentSize // the largest buffer that callers may request from a Source.
10+
)

buffer/constants_android.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//go:build android
2+
3+
/* SPDX-License-Identifier: MIT
4+
*
5+
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved.
6+
*/
7+
8+
package buffer
9+
10+
const (
11+
MaxSegmentSize = 2200 // largest possible Android read
12+
MaxBytesPerSource = 4096 * MaxSegmentSize
13+
)

buffer/constants_default.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//go:build !android && !ios && !windows
2+
3+
/* SPDX-License-Identifier: MIT
4+
*
5+
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved.
6+
*/
7+
8+
package buffer
9+
10+
const (
11+
MaxSegmentSize = (1 << 16) - 1 // largest possible Unix read
12+
MaxBytesPerSource = 0 // Disable and allow for infinite memory growth
13+
)

buffer/constants_ios.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
//go:build ios
2+
3+
/* SPDX-License-Identifier: MIT
4+
*
5+
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved.
6+
*/
7+
8+
package buffer
9+
10+
// Fit within memory limits for iOS's Network Extension API, which has stricter requirements.
11+
// These are vars instead of consts, because heavier network extensions might want to reduce
12+
// them further.
13+
var (
14+
MaxBytesPerSource = 1024 * MaxSegmentSize
15+
)
16+
17+
const MaxSegmentSize = 1700 // largest possible iOS read

buffer/constants_windows.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//go:build windows
2+
3+
/* SPDX-License-Identifier: MIT
4+
*
5+
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved.
6+
*/
7+
8+
package buffer
9+
10+
const (
11+
MaxSegmentSize = 2048 - 32 // largest possible Windows read
12+
MaxBytesPerSource = 0 // Disable and allow for infinite memory growth
13+
)

0 commit comments

Comments
 (0)