mirror of
https://github.com/slackhq/nebula.git
synced 2026-08-15 14:37:02 +02:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 334dd7a85d |
@@ -32,15 +32,6 @@ const tunDrainCap = 64 //256
|
|||||||
// any reallocations.
|
// any reallocations.
|
||||||
const gsoInitialPayIovs = 66
|
const gsoInitialPayIovs = 66
|
||||||
|
|
||||||
// gsoWriteBufCap is the initial per-queue coalesce scratch capacity used by
|
|
||||||
// WriteGSO to assemble [virtio_hdr || IP/TCP hdr || pays...] into a single
|
|
||||||
// contiguous buffer so we can emit the superpacket via a single write()
|
|
||||||
// instead of writev(). One worst-case TSO superpacket is bounded by the
|
|
||||||
// virtio spec at 64KiB; 128KiB gives comfortable slack for the 10-byte
|
|
||||||
// virtio header, the IP/TCP header, and any future size bumps. Grown on
|
|
||||||
// demand if a superpacket exceeds this.
|
|
||||||
const gsoWriteBufCap = tunSegBufSize
|
|
||||||
|
|
||||||
// validVnetHdr is the 10-byte virtio_net_hdr we prepend to every non-GSO TUN
|
// validVnetHdr is the 10-byte virtio_net_hdr we prepend to every non-GSO TUN
|
||||||
// write. Only flag set is VIRTIO_NET_HDR_F_DATA_VALID, which marks the skb
|
// write. Only flag set is VIRTIO_NET_HDR_F_DATA_VALID, which marks the skb
|
||||||
// CHECKSUM_UNNECESSARY so the receiving network stack skips L4 checksum
|
// CHECKSUM_UNNECESSARY so the receiving network stack skips L4 checksum
|
||||||
@@ -74,20 +65,10 @@ type Offload struct {
|
|||||||
// by WriteGSO. Separate from validVnetHdr so a concurrent non-GSO Write on
|
// by WriteGSO. Separate from validVnetHdr so a concurrent non-GSO Write on
|
||||||
// another queue never observes a half-written header.
|
// another queue never observes a half-written header.
|
||||||
gsoHdrBuf [virtioNetHdrLen]byte
|
gsoHdrBuf [virtioNetHdrLen]byte
|
||||||
// gsoIovs is a legacy writev iovec scratch. No longer used by the
|
// gsoIovs is the writev iovec scratch for WriteGSO. Sized to hold the
|
||||||
// WriteGSO path (which coalesces into gsoWriteBuf and uses a single
|
// virtio header + IP/TCP header + up to gsoInitialPayIovs payload
|
||||||
// write()) but retained for any other iovec-based path that may use it.
|
// fragments; grown on demand if a coalescer pushes more.
|
||||||
gsoIovs []unix.Iovec
|
gsoIovs []unix.Iovec
|
||||||
|
|
||||||
// gsoWriteBuf is a per-queue scratch used by WriteGSO to coalesce the
|
|
||||||
// virtio_net_hdr + IP/TCP header + payload fragments into a single
|
|
||||||
// contiguous buffer, which is then written to the TUN fd with one
|
|
||||||
// write() syscall. This mirrors wireguard-go's approach and avoids
|
|
||||||
// triggering a kernel refcount use-after-free in skb_set_owner_w /
|
|
||||||
// sock_wfree observed on Linux 4.19 TUN when scatter-gather writev is
|
|
||||||
// combined with GSO-flagged virtio_net_hdr in the tun_chr_write_iter
|
|
||||||
// path. Grown on demand if a superpacket exceeds the initial cap.
|
|
||||||
gsoWriteBuf []byte
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newOffload(fd int, shutdownFd int) (*Offload, error) {
|
func newOffload(fd int, shutdownFd int) (*Offload, error) {
|
||||||
@@ -111,7 +92,6 @@ func newOffload(fd int, shutdownFd int) (*Offload, error) {
|
|||||||
|
|
||||||
segBuf: make([]byte, tunSegBufCap),
|
segBuf: make([]byte, tunSegBufCap),
|
||||||
gsoIovs: make([]unix.Iovec, 2, 2+gsoInitialPayIovs),
|
gsoIovs: make([]unix.Iovec, 2, 2+gsoInitialPayIovs),
|
||||||
gsoWriteBuf: make([]byte, 0, gsoWriteBufCap),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
out.writeIovs[0].Base = &validVnetHdr[0]
|
out.writeIovs[0].Base = &validVnetHdr[0]
|
||||||
@@ -311,59 +291,18 @@ func (r *Offload) rawWrite(iovs []unix.Iovec) (int, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// rawWriteSingle writes buf to the TUN fd with a single write() syscall.
|
|
||||||
// Unlike rawWrite (which uses writev), this avoids the kernel
|
|
||||||
// scatter-gather path that triggers a use-after-free in
|
|
||||||
// tun_chr_write_iter → sock_alloc_send_pskb → skb_set_owner_w on Linux
|
|
||||||
// 4.19 TUN when the virtio_net_hdr requests TSO segmentation. The caller
|
|
||||||
// is responsible for including the virtio_net_hdr prefix in buf.
|
|
||||||
func (r *Offload) rawWriteSingle(buf []byte) (int, error) {
|
|
||||||
for {
|
|
||||||
n, err := unix.Write(r.fd, buf)
|
|
||||||
if err == nil {
|
|
||||||
if n < virtioNetHdrLen {
|
|
||||||
return 0, io.ErrShortWrite
|
|
||||||
}
|
|
||||||
return n - virtioNetHdrLen, nil
|
|
||||||
}
|
|
||||||
if err == unix.EAGAIN {
|
|
||||||
if werr := r.blockOnWrite(); werr != nil {
|
|
||||||
return 0, werr
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err == unix.EINTR {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err == unix.EBADF {
|
|
||||||
return 0, os.ErrClosed
|
|
||||||
}
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GSOSupported reports whether this queue was opened with IFF_VNET_HDR and
|
// GSOSupported reports whether this queue was opened with IFF_VNET_HDR and
|
||||||
// can accept WriteGSO. When false, callers should fall back to per-segment
|
// can accept WriteGSO. When false, callers should fall back to per-segment
|
||||||
// Write calls.
|
// Write calls.
|
||||||
func (r *Offload) GSOSupported() bool { return true }
|
func (r *Offload) GSOSupported() bool { return true }
|
||||||
|
|
||||||
// WriteGSO emits a TCP TSO superpacket. hdr is the IPv4/IPv6 + TCP header
|
// WriteGSO emits a TCP TSO superpacket in a single writev. hdr is the
|
||||||
// prefix (already finalized — total length, IP csum, and TCP pseudo-header
|
// IPv4/IPv6 + TCP header prefix (already finalized — total length, IP csum,
|
||||||
// partial set by the caller). pays are payload fragments whose concatenation
|
// and TCP pseudo-header partial set by the caller). pays are payload
|
||||||
// forms the full coalesced payload. gsoSize is the MSS; every segment except
|
// fragments whose concatenation forms the full coalesced payload; each
|
||||||
// possibly the last is exactly gsoSize bytes. csumStart is the byte offset
|
// slice is read-only and must stay valid until return. gsoSize is the MSS;
|
||||||
// where the TCP header begins within hdr.
|
// every segment except possibly the last is exactly gsoSize bytes.
|
||||||
//
|
// csumStart is the byte offset where the TCP header begins within hdr.
|
||||||
// Implementation note: this path coalesces [virtio_hdr || hdr || pays...]
|
|
||||||
// into a single contiguous scratch buffer (r.gsoWriteBuf) and emits it via
|
|
||||||
// one write() syscall rather than writev() with a scatter-gather iovec.
|
|
||||||
// The scatter-gather path triggered a kernel-side use-after-free on Linux
|
|
||||||
// 4.19 TUN where tun_chr_write_iter → sock_alloc_send_pskb →
|
|
||||||
// skb_set_owner_w could be invoked with a zero sk_wmem_alloc, crashing
|
|
||||||
// the router. The single-write path mirrors wireguard-go's design (see
|
|
||||||
// golang.zx2c4.com/wireguard/tun/tun_linux.go Write — it always coalesces
|
|
||||||
// GRO-merged data into a single contiguous buffer before calling
|
|
||||||
// tunFile.Write) and has no equivalent failure mode.
|
|
||||||
func (r *Offload) WriteGSO(hdr []byte, pays [][]byte, gsoSize uint16, isV6 bool, csumStart uint16) error {
|
func (r *Offload) WriteGSO(hdr []byte, pays [][]byte, gsoSize uint16, isV6 bool, csumStart uint16) error {
|
||||||
if len(hdr) == 0 || len(pays) == 0 {
|
if len(hdr) == 0 || len(pays) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -395,26 +334,24 @@ func (r *Offload) WriteGSO(hdr []byte, pays [][]byte, gsoSize uint16, isV6 bool,
|
|||||||
}
|
}
|
||||||
vhdr.encode(r.gsoHdrBuf[:])
|
vhdr.encode(r.gsoHdrBuf[:])
|
||||||
|
|
||||||
// Coalesce [virtio_hdr || hdr || pays...] into a single contiguous
|
// Build the iovec array: [virtio_hdr, hdr, pays...]. r.gsoIovs[0] is
|
||||||
// buffer. This avoids the kernel scatter-gather write path entirely.
|
// wired to gsoHdrBuf at construction and never changes.
|
||||||
need := virtioNetHdrLen + len(hdr) + totalPay
|
need := 2 + len(pays)
|
||||||
if cap(r.gsoWriteBuf) < need {
|
if cap(r.gsoIovs) < need {
|
||||||
// Grow geometrically to amortize reallocs.
|
grown := make([]unix.Iovec, need)
|
||||||
newCap := cap(r.gsoWriteBuf) * 2
|
grown[0] = r.gsoIovs[0]
|
||||||
if newCap < need {
|
r.gsoIovs = grown
|
||||||
newCap = need
|
|
||||||
}
|
|
||||||
r.gsoWriteBuf = make([]byte, 0, newCap)
|
|
||||||
} else {
|
} else {
|
||||||
r.gsoWriteBuf = r.gsoWriteBuf[:0]
|
r.gsoIovs = r.gsoIovs[:need]
|
||||||
}
|
}
|
||||||
r.gsoWriteBuf = append(r.gsoWriteBuf, r.gsoHdrBuf[:]...)
|
r.gsoIovs[1].Base = &hdr[0]
|
||||||
r.gsoWriteBuf = append(r.gsoWriteBuf, hdr...)
|
r.gsoIovs[1].SetLen(len(hdr))
|
||||||
for _, p := range pays {
|
for i, p := range pays {
|
||||||
r.gsoWriteBuf = append(r.gsoWriteBuf, p...)
|
r.gsoIovs[2+i].Base = &p[0]
|
||||||
|
r.gsoIovs[2+i].SetLen(len(p))
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := r.rawWriteSingle(r.gsoWriteBuf)
|
_, err := r.rawWrite(r.gsoIovs)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, multiqueu
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
vnetHdr := true
|
vnetHdr := true
|
||||||
name, err := tunSetIff(fd, nameStr, baseFlags|unix.IFF_VNET_HDR|unix.IFF_NAPI)
|
name, err := tunSetIff(fd, nameStr, baseFlags|unix.IFF_VNET_HDR)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = unix.Close(fd)
|
_ = unix.Close(fd)
|
||||||
vnetHdr = false
|
vnetHdr = false
|
||||||
@@ -307,7 +307,7 @@ func (t *tun) NewMultiQueueReader() error {
|
|||||||
|
|
||||||
flags := uint16(unix.IFF_TUN | unix.IFF_NO_PI | unix.IFF_MULTI_QUEUE)
|
flags := uint16(unix.IFF_TUN | unix.IFF_NO_PI | unix.IFF_MULTI_QUEUE)
|
||||||
if t.vnetHdr {
|
if t.vnetHdr {
|
||||||
flags |= unix.IFF_VNET_HDR | unix.IFF_NAPI
|
flags |= unix.IFF_VNET_HDR
|
||||||
}
|
}
|
||||||
if _, err = tunSetIff(fd, t.Device, flags); err != nil {
|
if _, err = tunSetIff(fd, t.Device, flags); err != nil {
|
||||||
_ = unix.Close(fd)
|
_ = unix.Close(fd)
|
||||||
|
|||||||
Reference in New Issue
Block a user