-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathdevice.go
More file actions
232 lines (200 loc) · 6.99 KB
/
Copy pathdevice.go
File metadata and controls
232 lines (200 loc) · 6.99 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
package tuntap
import (
"fmt"
"net"
"os"
"time"
"unsafe"
"github.com/hetznercloud/virtio-go/virtio"
"golang.org/x/sys/unix"
)
// Documentation:
// https://docs.kernel.org/networking/tuntap.html
// Also worth a read:
// https://blog.cloudflare.com/virtual-networking-101-understanding-tap/
// Device represents a TUN/TAP device.
type Device struct {
name string
ifindex uint32
mac net.HardwareAddr
file *os.File
}
// NewDevice creates a new TUN/TAP device, brings it up, and returns a [Device]
// instance providing access to it.
//
// There are multiple options that can be passed to this constructor to
// influence device creation:
// - [WithName]
// - [WithDeviceType]
// - [WithVirtioNetHdr]
// - [WithInterfaceFlags]
//
// Remember to call [Device.Close] after use to free up resources.
func NewDevice(options ...Option) (_ *Device, err error) {
opts := optionDefaults
opts.apply(options)
if err = opts.validate(); err != nil {
return nil, fmt.Errorf("invalid options: %w", err)
}
// Get a file descriptor. The device will exist as long as we keep this
// file descriptor open.
fd, err := unix.Open("/dev/net/tun", os.O_RDWR, 0o666)
if err != nil {
return nil, fmt.Errorf("access tuntap driver: %w", err)
}
// Create an interface request. When the name is empty, the kernel will
// auto-select one.
ifreq, err := unix.NewIfreq(opts.name)
if err != nil {
_ = unix.Close(fd)
return nil, fmt.Errorf("new ifreq: %w", err)
}
// Create the new device.
ifreq.SetUint16(opts.ifreqFlags())
if err = unix.IoctlIfreq(fd, unix.TUNSETIFF, ifreq); err != nil {
_ = unix.Close(fd)
return nil, fmt.Errorf("create device: %w", err)
}
dev := Device{
// The TUNSETIFF ioctl writes the actual name that was chosen for the
// device back to the request, so use that.
name: ifreq.Name(),
}
// Make the file descriptor of the device non-blocking. This enables us to
// cancel reads after a timeout when no packets are arriving.
// This, and the call to NewFile has to happen after creating the device:
// https://github.com/golang/go/issues/30426#issuecomment-470330742
// NewFile will recognize that the file descriptor is non-blocking and will
// configure polling for it.
if err = unix.SetNonblock(fd, true); err != nil {
_ = unix.Close(fd)
return nil, fmt.Errorf("make file descriptor non-blocking: %w", err)
}
// By wrapping the file descriptor as an os.File, we not only have a
// convenient way to read and write, but also register a finalizer that
// closes the file descriptor when it's being garbage collected.
dev.file = os.NewFile(uintptr(fd), dev.name)
// Make sure the device is removed when one of the following initialization
// steps fails.
defer func() {
if err != nil {
_ = dev.Close()
}
}()
if opts.virtioNetHdr {
// Tell the device which size we use for our virtio_net_hdr.
err = unix.IoctlSetPointerInt(fd, unix.TUNSETVNETHDRSZ, virtio.NetHdrSize)
if err != nil {
return nil, fmt.Errorf("set vnethdr size: %w", err)
}
}
// Tell the device which offloads are supported.
err = unix.IoctlSetInt(fd, unix.TUNSETOFFLOAD, opts.offloads)
if err != nil {
return nil, fmt.Errorf("set offloads: %w", err)
}
// For the following ioctls we need just any AF_INET socket, so create one.
inet, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0)
if err != nil {
return nil, fmt.Errorf("open inet socket: %w", err)
}
defer func() { _ = unix.Close(inet) }()
// Set the interface flags to bring it up.
ifreq.SetUint16(unix.IFF_UP | opts.interfaceFlags)
if err = unix.IoctlIfreq(inet, unix.SIOCSIFFLAGS, ifreq); err != nil {
return nil, fmt.Errorf("set interface flags: %w", err)
}
// Get the interface index.
if err = unix.IoctlIfreq(inet, unix.SIOCGIFINDEX, ifreq); err != nil {
return nil, fmt.Errorf("get interface index: %w", err)
}
dev.ifindex = ifreq.Uint32()
// Get the MAC address.
// This ioctl writes a sockaddr into the data ifru section of the interface
// request struct. The MAC address is in the beginning of the
// sockaddr.sa_data section.
if err = unix.IoctlIfreq(inet, unix.SIOCGIFHWADDR, ifreq); err != nil {
return nil, fmt.Errorf("get mac address: %w", err)
}
dev.mac = unsafe.Slice((*byte)(unsafe.Pointer(ifreq)), 32)[16+2 : 16+8]
return &dev, nil
}
// Close closes the file descriptor behind this device. This will cause the
// TUN/TAP device to be removed.
func (dev *Device) Close() error {
if err := dev.file.Close(); err != nil {
return fmt.Errorf("close file descriptor: %w", err)
}
dev.file = nil
return nil
}
// Name returns the name of this device.
func (dev *Device) Name() string {
dev.ensureInitialized()
return dev.name
}
// Ifindex returns the interface index of this device.
func (dev *Device) Ifindex() uint32 {
dev.ensureInitialized()
return dev.ifindex
}
// MAC returns the hardware address of this device.
func (dev *Device) MAC() net.HardwareAddr {
dev.ensureInitialized()
return dev.mac
}
// File returns the [os.File] that is used to communicate with this device.
// If you access it directly, please be careful to not interfere with this
// implementation.
func (dev *Device) File() *os.File {
dev.ensureInitialized()
return dev.file
}
// WritePacket writes the given packet to the TUN/TAP device.
// When the [WithVirtioNetHdr] option was enabled, then the caller is
// responsible to prepend the packet with a [virtio.NetHdr].
func (dev *Device) WritePacket(packet []byte) error {
dev.ensureInitialized()
_, err := dev.file.Write(packet)
if err != nil {
return fmt.Errorf("write %d bytes: %w", len(packet), err)
}
return nil
}
// ReadPacket reads the next available packet from the TUN/TAP device into the
// given buffer. Make sure that the buffer is large enough, otherwise only a
// part of the packet may be read. The number of read bytes will be returned.
//
// When the [WithVirtioNetHdr] option was enabled, then the read packet will be
// prepended with a [virtio.NetHdr]. The caller is responsible to handle it
// accordingly.
//
// A timeout can be given to limit the time this operation blocks. If no packet
// arrives within the given timeout, the read is canceled and an error that
// wraps [os.ErrDeadlineExceeded] is returned. Pass a timeout of zero to make
// this operation block infinitely.
func (dev *Device) ReadPacket(buf []byte, timeout time.Duration) (int, error) {
dev.ensureInitialized()
// Make sure the read times out. This only works for files that support
// polling (see above).
// When no timeout is desired, passing the zero time removes the deadline.
var deadline time.Time
if timeout > 0 {
deadline = time.Now().Add(timeout)
}
if err := dev.file.SetReadDeadline(deadline); err != nil {
return 0, fmt.Errorf("set deadline: %w", err)
}
n, err := dev.file.Read(buf)
if err != nil {
return n, fmt.Errorf("read up to %d bytes: %w", len(buf), err)
}
return n, nil
}
// ensureInitialized is used as a guard to prevent methods to be called on an
// uninitialized instance.
func (dev *Device) ensureInitialized() {
if dev.file == nil {
panic("device is not initialized")
}
}