Skip to content

Commit 55464f0

Browse files
committed
tun, conn, device: allocate buffers in the I/O devices
Previously crypto device maintained a batch of preallocated to the MaxMessageSize buffers that the I/O only needs to read into. This change inverts the buffer ownership. A (wrapped) nil pointer is passed into I/O. Device expects a backing array to be allocated, and a slice of read data cut to offset+size returned from the read within the same wrapper. The wrapper is defined in buffer.Buffer, and carries the backing reference and an optional Recycler implementation to return the backing for reuse. I/O is encouraged to implement a buffer management solution that works best for its host OS. A shared sync.Pool is provided as a default option. Updates tailscale/corp#36989 Signed-off-by: Alex Valiushko <alexvaliushko@tailscale.com> Change-Id: I58908d9d3fd09441e9378a74b0ee19136a6a6964
1 parent 20486f7 commit 55464f0

37 files changed

Lines changed: 449 additions & 225 deletions

conn/bind_std.go

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"sync"
1717
"syscall"
1818

19+
"github.com/tailscale/wireguard-go/device/buffer"
1920
"golang.org/x/net/ipv4"
2021
"golang.org/x/net/ipv6"
2122
)
@@ -233,13 +234,13 @@ func (s *StdNetBind) receiveIP(
233234
br batchReader,
234235
conn *net.UDPConn,
235236
rxOffload bool,
236-
bufs [][]byte,
237-
sizes []int,
237+
bufs []buffer.Buffer,
238238
eps []Endpoint,
239239
) (n int, err error) {
240240
msgs := s.getMessages()
241+
buffer.EnsureAllocated(bufs)
241242
for i := range bufs {
242-
(*msgs)[i].Buffers[0] = bufs[i]
243+
(*msgs)[i].Buffers[0] = bufs[i].Data
243244
(*msgs)[i].OOB = (*msgs)[i].OOB[:cap((*msgs)[i].OOB)]
244245
}
245246
defer s.putMessages(msgs)
@@ -271,8 +272,8 @@ func (s *StdNetBind) receiveIP(
271272
}
272273
for i := 0; i < numMsgs; i++ {
273274
msg := &(*msgs)[i]
274-
sizes[i] = msg.N
275-
if sizes[i] == 0 {
275+
bufs[i].Data = bufs[i].Data[:msg.N]
276+
if len(bufs[i].Data) == 0 {
276277
continue
277278
}
278279
addrPort := msg.Addr.(*net.UDPAddr).AddrPort()
@@ -284,14 +285,14 @@ func (s *StdNetBind) receiveIP(
284285
}
285286

286287
func (s *StdNetBind) makeReceiveIPv4(pc *ipv4.PacketConn, conn *net.UDPConn, rxOffload bool) ReceiveFunc {
287-
return func(bufs [][]byte, sizes []int, eps []Endpoint) (n int, err error) {
288-
return s.receiveIP(pc, conn, rxOffload, bufs, sizes, eps)
288+
return func(bufs []buffer.Buffer, eps []Endpoint) (n int, err error) {
289+
return s.receiveIP(pc, conn, rxOffload, bufs, eps)
289290
}
290291
}
291292

292293
func (s *StdNetBind) makeReceiveIPv6(pc *ipv6.PacketConn, conn *net.UDPConn, rxOffload bool) ReceiveFunc {
293-
return func(bufs [][]byte, sizes []int, eps []Endpoint) (n int, err error) {
294-
return s.receiveIP(pc, conn, rxOffload, bufs, sizes, eps)
294+
return func(bufs []buffer.Buffer, eps []Endpoint) (n int, err error) {
295+
return s.receiveIP(pc, conn, rxOffload, bufs, eps)
295296
}
296297
}
297298

conn/bind_std_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"net"
66
"testing"
77

8+
"github.com/tailscale/wireguard-go/device/buffer"
89
"golang.org/x/net/ipv6"
910
)
1011

@@ -15,15 +16,14 @@ func TestStdNetBindReceiveFuncAfterClose(t *testing.T) {
1516
t.Fatal(err)
1617
}
1718
bind.Close()
18-
bufs := make([][]byte, 1)
19-
bufs[0] = make([]byte, 1)
20-
sizes := make([]int, 1)
19+
bufs := make([]buffer.Buffer, 1)
20+
bufs[0] = buffer.Buffer{Data: make([]byte, 1)}
2121
eps := make([]Endpoint, 1)
2222
for _, fn := range fns {
2323
// The ReceiveFuncs must not access conn-related fields on StdNetBind
2424
// unguarded. Close() nils the conn-related fields resulting in a panic
2525
// if they violate the mutex.
26-
fn(bufs, sizes, eps)
26+
fn(bufs, eps)
2727
}
2828
}
2929

conn/bind_windows.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"golang.org/x/sys/windows"
1919

2020
"github.com/tailscale/wireguard-go/conn/winrio"
21+
"github.com/tailscale/wireguard-go/device/buffer"
2122
)
2223

2324
const (
@@ -416,20 +417,22 @@ retry:
416417
return n, &ep, nil
417418
}
418419

419-
func (bind *WinRingBind) receiveIPv4(bufs [][]byte, sizes []int, eps []Endpoint) (int, error) {
420+
func (bind *WinRingBind) receiveIPv4(bufs []buffer.Buffer, eps []Endpoint) (int, error) {
420421
bind.mu.RLock()
421422
defer bind.mu.RUnlock()
422-
n, ep, err := bind.v4.Receive(bufs[0], &bind.isOpen)
423-
sizes[0] = n
423+
buffer.EnsureAllocated(bufs[:1])
424+
n, ep, err := bind.v4.Receive(bufs[0].Data, &bind.isOpen)
425+
bufs[0].Data = bufs[0].Data[:n]
424426
eps[0] = ep
425427
return 1, err
426428
}
427429

428-
func (bind *WinRingBind) receiveIPv6(bufs [][]byte, sizes []int, eps []Endpoint) (int, error) {
430+
func (bind *WinRingBind) receiveIPv6(bufs []buffer.Buffer, eps []Endpoint) (int, error) {
429431
bind.mu.RLock()
430432
defer bind.mu.RUnlock()
431-
n, ep, err := bind.v6.Receive(bufs[0], &bind.isOpen)
432-
sizes[0] = n
433+
buffer.EnsureAllocated(bufs[:1])
434+
n, ep, err := bind.v6.Receive(bufs[0].Data, &bind.isOpen)
435+
bufs[0].Data = bufs[0].Data[:n]
433436
eps[0] = ep
434437
return 1, err
435438
}

conn/bindtest/bindtest.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"os"
1414

1515
"github.com/tailscale/wireguard-go/conn"
16+
"github.com/tailscale/wireguard-go/device/buffer"
1617
)
1718

1819
type ChannelBind struct {
@@ -94,13 +95,14 @@ func (c *ChannelBind) BatchSize() int { return 1 }
9495
func (c *ChannelBind) SetMark(mark uint32) error { return nil }
9596

9697
func (c *ChannelBind) makeReceiveFunc(ch chan []byte) conn.ReceiveFunc {
97-
return func(bufs [][]byte, sizes []int, eps []conn.Endpoint) (n int, err error) {
98+
return func(bufs []buffer.Buffer, eps []conn.Endpoint) (n int, err error) {
9899
select {
99100
case <-c.closeSignal:
100101
return 0, net.ErrClosed
101102
case rx := <-ch:
102-
copied := copy(bufs[0], rx)
103-
sizes[0] = copied
103+
buffer.EnsureAllocated(bufs[:1])
104+
n := copy(bufs[0].Data, rx)
105+
bufs[0].Data = bufs[0].Data[:n]
104106
eps[0] = c.target6
105107
return 1, nil
106108
}

conn/conn.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,20 @@ import (
1313
"reflect"
1414
"runtime"
1515
"strings"
16+
17+
"github.com/tailscale/wireguard-go/device/buffer"
1618
)
1719

1820
const (
1921
IdealBatchSize = 128 // maximum number of packets handled per read and write
2022
)
2123

22-
// A ReceiveFunc receives at least one packet from the network and writes them
23-
// into packets. On a successful read it returns the number of elements of
24-
// sizes, packets, and endpoints that should be evaluated. Some elements of
25-
// sizes may be zero, and callers should ignore them. Callers must pass a sizes
26-
// and eps slice with a length greater than or equal to the length of packets.
27-
// These lengths must not exceed the length of the associated Bind.BatchSize().
28-
type ReceiveFunc func(packets [][]byte, sizes []int, eps []Endpoint) (n int, err error)
24+
// A ReceiveFunc receives at least one packet from the network into bufs.
25+
// On a successful read it returns the number of elements of bufs and eps
26+
// that should be evaluated. Callers must pass an eps slice with a length
27+
// greater than or equal to the length of bufs. These lengths must not
28+
// exceed the length of the associated Bind.BatchSize().
29+
type ReceiveFunc func(bufs []buffer.Buffer, eps []Endpoint) (n int, err error)
2930

3031
// A Bind listens on a port for both IPv6 and IPv4 UDP traffic.
3132
//

conn/conn_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ package conn
77

88
import (
99
"testing"
10+
11+
"github.com/tailscale/wireguard-go/device/buffer"
1012
)
1113

1214
func TestPrettyName(t *testing.T) {
1315
var (
14-
recvFunc ReceiveFunc = func(bufs [][]byte, sizes []int, eps []Endpoint) (n int, err error) { return }
16+
recvFunc ReceiveFunc = func(bufs []buffer.Buffer, eps []Endpoint) (n int, err error) { return }
1517
)
1618

1719
const want = "TestPrettyName"

device/buffer/buffer.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/* SPDX-License-Identifier: MIT
2+
*
3+
* Copyright (C) 2017-2023 WireGuard LLC. All Rights Reserved.
4+
*/
5+
6+
// Package buffer provides pooled packet buffers for the I/O pipeline.
7+
// Each [Buffer] carries one packet and a recycle function that returns
8+
// its backing storage to the originating pool on [Release].
9+
package buffer
10+
11+
import "unsafe"
12+
13+
// RecycleHandle is the opaque reference to the [Buffer] backing arrays.
14+
type RecycleHandle unsafe.Pointer
15+
16+
// Recycler returns a backing array for reuse.
17+
// The argument is the Backing of the [Buffer] being released.
18+
type Recycler interface {
19+
Recycle(RecycleHandle)
20+
}
21+
22+
// RecycleFunc is a function adapter for Recycler.
23+
type RecycleFunc func(RecycleHandle)
24+
25+
func (f RecycleFunc) Recycle(ptr RecycleHandle) { f(ptr) }
26+
27+
// Buffer is the packet envelope. Meant to be a value type,
28+
// allocated once per goroutine and reused across read cycles.
29+
type Buffer struct {
30+
// Recycle returns the backing array to its pool.
31+
// Nil for unmanaged/external Buffers.
32+
Recycler Recycler
33+
34+
// Backing is the raw pointer to the byte array,
35+
// anonymized to allow varying array and recycler implementations.
36+
// Zero for external/unmanaged Buffers.
37+
Backing RecycleHandle
38+
39+
// Data holds the bounded packet data, sliced from the backing array,
40+
// and may be re-sliced by the caller. Do not append() on this slice.
41+
// Nil for uninitialized Buffers.
42+
Data []byte
43+
}
44+
45+
// Release returns the backing data to its pool and zeros the Buffer.
46+
func (b *Buffer) Release() {
47+
if b.Recycler != nil {
48+
b.Recycler.Recycle(b.Backing)
49+
}
50+
*b = Buffer{}
51+
}
52+
53+
// Claim transfers ownership: returns a copy of the Buffer and zeros the source.
54+
func (b *Buffer) Claim() Buffer {
55+
c := *b
56+
*b = Buffer{}
57+
return c
58+
}
59+
60+
// ReleaseAll releases each Buffer in the slice.
61+
func ReleaseAll(bufs []Buffer) {
62+
for i := range bufs {
63+
bufs[i].Release()
64+
}
65+
}

device/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 = MaxReadSize // the largest buffer that I/O may attempt to read or write.
10+
)

device/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+
MaxReadSize = 2200
12+
MaxPooledBuffers = 4096
13+
)

device/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+
MaxReadSize = (1 << 16) - 1
12+
MaxPooledBuffers = 0 // Disable and allow for infinite memory growth
13+
)

0 commit comments

Comments
 (0)