From ae984d44b566d33fcd4dca76f8f66c65dea35d3d Mon Sep 17 00:00:00 2001 From: fahed dorgaa Date: Wed, 18 Jun 2025 19:16:12 +0200 Subject: [PATCH] network: add support for gvisor-tap-vsock driver and integration tests Signed-off-by: fahed dorgaa --- .github/workflows/main.yaml | 23 + cmd/rootlesskit/main.go | 36 +- docs/network.md | 48 ++ go.mod | 13 + go.sum | 60 ++- hack/benchmark-iperf3-net.sh | 11 + pkg/network/gvisortapvsock/gvisortapvsock.go | 530 +++++++++++++++++++ 7 files changed, 715 insertions(+), 6 deletions(-) create mode 100644 pkg/network/gvisortapvsock/gvisortapvsock.go diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index fe2e6680..85e81bdc 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -70,6 +70,10 @@ jobs: run: | docker run --rm --privileged rootlesskit:test-integration ./integration-net.sh pasta docker run --rm --privileged rootlesskit:test-integration ./integration-net.sh pasta --detach-netns + - name: "Integration test: Network (network driver=gvisor-tap-vsock)" + run: | + docker run --rm --privileged rootlesskit:test-integration ./integration-net.sh gvisor-tap-vsock + docker run --rm --privileged rootlesskit:test-integration ./integration-net.sh gvisor-tap-vsock --detach-netns # ===== Benchmark: Network (MTU=1500) ===== - name: "Benchmark: Network (MTU=1500, network driver=slirp4netns)" run: | @@ -101,6 +105,14 @@ jobs: run: | docker run --rm --security-opt seccomp=unconfined --security-opt apparmor=unconfined --device /dev/net/tun \ rootlesskit:test-integration ./benchmark-iperf3-net.sh pasta 1500 --detach-netns + - name: "Benchmark: Network (MTU=1500, network driver=gvisor-tap-vsock)" + run: | + docker run --rm --security-opt seccomp=unconfined --security-opt apparmor=unconfined --device /dev/net/tun \ + rootlesskit:test-integration ./benchmark-iperf3-net.sh gvisor-tap-vsock 1500 + - name: "Benchmark: Network (MTU=1500, network driver=gvisor-tap-vsock) with detach-netns" + run: | + docker run --rm --security-opt seccomp=unconfined --security-opt apparmor=unconfined --device /dev/net/tun \ + rootlesskit:test-integration ./benchmark-iperf3-net.sh gvisor-tap-vsock 1500 --detach-netns - name: "Benchmark: Network (MTU=1500, network driver=lxc-user-nic)" run: | docker run --rm --privileged \ @@ -126,6 +138,10 @@ jobs: run: | docker run --rm --security-opt seccomp=unconfined --security-opt apparmor=unconfined --device /dev/net/tun \ rootlesskit:test-integration ./benchmark-iperf3-net.sh pasta 65520 + - name: "Benchmark: Network (MTU=65520, network driver=gvisor-tap-vsock)" + run: | + docker run --rm --security-opt seccomp=unconfined --security-opt apparmor=unconfined --device /dev/net/tun \ + rootlesskit:test-integration ./benchmark-iperf3-net.sh gvisor-tap-vsock 65520 - name: "Benchmark: Network (MTU=65520, network driver=lxc-user-nic)" run: | docker run --rm --privileged \ @@ -242,3 +258,10 @@ jobs: docker exec test docker info docker exec test ./integration-docker.sh docker rm -f test + - name: "Docker Integration test: net=gvisor-tap-vsock, port-driver=builtin" + run: | + docker run -d --name test --network custom --privileged -e DOCKERD_ROOTLESS_ROOTLESSKIT_NET=gvisor-tap-vsock -e DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=builtin rootlesskit:test-integration-docker + sleep 2 + docker exec test docker info + docker exec test ./integration-docker.sh + docker rm -f test diff --git a/cmd/rootlesskit/main.go b/cmd/rootlesskit/main.go index e0bd133c..53e32f99 100644 --- a/cmd/rootlesskit/main.go +++ b/cmd/rootlesskit/main.go @@ -19,6 +19,7 @@ import ( "github.com/rootless-containers/rootlesskit/v2/pkg/child" "github.com/rootless-containers/rootlesskit/v2/pkg/common" "github.com/rootless-containers/rootlesskit/v2/pkg/copyup/tmpfssymlink" + "github.com/rootless-containers/rootlesskit/v2/pkg/network/gvisortapvsock" "github.com/rootless-containers/rootlesskit/v2/pkg/network/lxcusernic" "github.com/rootless-containers/rootlesskit/v2/pkg/network/none" "github.com/rootless-containers/rootlesskit/v2/pkg/network/pasta" @@ -97,7 +98,7 @@ See https://rootlesscontaine.rs/getting-started/common/ . }, CategoryState), Categorize(&cli.StringFlag{ Name: "net", - Usage: "network driver [host, none, pasta(experimental), slirp4netns, vpnkit, lxc-user-nic(experimental)]", + Usage: "network driver [host, none, pasta(experimental), slirp4netns, vpnkit, lxc-user-nic(experimental), gvisor-tap-vsock(experimental)]", Value: "host", }, CategoryNetwork), Categorize(&cli.StringFlag{ @@ -142,7 +143,7 @@ See https://rootlesscontaine.rs/getting-started/common/ . }, CategoryNetwork), Categorize(&cli.StringFlag{ Name: "cidr", - Usage: "CIDR for pasta and slirp4netns networks (default: 10.0.2.0/24)", + Usage: "CIDR for pasta, slirp4netns and gvisor-tap-vsock networks (default: 10.0.2.0/24)", }, CategoryNetwork), Categorize(&cli.StringFlag{ Name: "ifname", @@ -536,6 +537,35 @@ func createParentOpt(clicontext *cli.Context) (parent.Opt, error) { if err != nil { return opt, err } + case "gvisor-tap-vsock": + logrus.Warn("\"gvisor-tap-vsock\" network driver is experimental") + if disableHostLoopback { + logrus.Warn("\"--disable-host-loopback\" is not yet supported for gvisor-tap-vsock") + } + + if clicontext.String("cidr") != "" { + ipnet, err = parseCIDR(clicontext.String("cidr")) + if err != nil { + return opt, err + } + } else { + // Default CIDR for the virtual network + _, ipnet, err = net.ParseCIDR("10.0.2.0/24") + if err != nil { + return opt, err + } + } + + if clicontext.Bool("ipv6") { + // virtual network does not support IPv6 yet + // see https://github.com/containers/gvisor-tap-vsock/blob/v0.8.6/pkg/virtualnetwork/virtualnetwork.go#L102 + return opt, errors.New("--ipv6 is not supported for gvisor-tap-vsock") + } + + opt.NetworkDriver, err = gvisortapvsock.NewParentDriver(&logrusDebugWriter{label: "network/gvisor-tap-vsock"}, mtu, ipnet, ifname, disableHostLoopback, ipv6) + if err != nil { + return opt, err + } default: return opt, fmt.Errorf("unknown network mode: %s", s) } @@ -635,6 +665,8 @@ func createChildOpt(clicontext *cli.Context) (child.Opt, error) { opt.NetworkDriver = vpnkit.NewChildDriver() case "lxc-user-nic": opt.NetworkDriver = lxcusernic.NewChildDriver() + case "gvisor-tap-vsock": + opt.NetworkDriver = gvisortapvsock.NewChildDriver() default: return opt, fmt.Errorf("unknown network mode: %s", s) } diff --git a/docs/network.md b/docs/network.md index ad795845..cc647f9e 100644 --- a/docs/network.md +++ b/docs/network.md @@ -7,6 +7,7 @@ RootlessKit provides several drivers for providing network connectivity: * `--net=slirp4netns`: use [slirp4netns](https://github.com/rootless-containers/slirp4netns) (recommended) * `--net=vpnkit`: use [VPNKit](https://github.com/moby/vpnkit) * `--net=lxc-user-nic`: use `lxc-user-nic` (experimental) +* `--net=gvisor-tap-vsock`: use [gvisor-tap-vsock](https://github.com/containers/gvisor-tap-vsock) (experimental) [Benchmark: iperf3 from the child to the parent (Mar 8, 2020)](https://github.com/rootless-containers/rootlesskit/runs/492498728): @@ -207,6 +208,53 @@ You might need to reset `/var/lib/misc/dnsmasq.lxcbr0.leases` and restart the `l Currently, the MAC address is always set to a random address. +### `--net=gvisor-tap-vsock` (experimental) + +`--net=gvisor-tap-vsock` isolates the network namespace from the host and uses [gvisor-tap-vsock](https://github.com/containers/gvisor-tap-vsock) for providing usermode networking. + +Pros: +* Possible to perform network-namespaced operations, e.g. creating iptables rules, running `tcpdump` +* Supports ICMP Echo (`ping`) when `/proc/sys/net/ipv4/ping_group_range` is configured + +Cons: +* Supports only TCP, UDP, and ICMP Echo packets +* Does not support IPv6 routing (`--ipv6`) + +The network is configured as follows by default: +* IP: 10.0.2.100/24 +* Gateway: 10.0.2.1 +* DNS: 10.0.2.1 + +The network configuration can be changed by specifying custom CIDR, e.g. `--cidr=10.0.3.0/24`. + +As in `--net=slirp4netns`, specifying `--copy-up=/etc` is highly recommended unless `/etc/resolv.conf` on the host is statically configured. It is also highly recommended to specify `--disable-host-loopback`. Otherwise ports listening on 127.0.0.1 in the host are accessible as 10.0.2.1 in the RootlessKit's network namespace. + +Example session: + +```console +$ rootlesskit --net=gvisor-tap-vsock --copy-up=/etc --disable-host-loopback bash +rootlesskit$ ip a +1: lo: mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 + link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 + inet 127.0.0.1/8 scope host lo + valid_lft forever preferred_lft forever + inet6 ::1/128 scope host + valid_lft forever preferred_lft forever +2: tap0: mtu 65520 qdisc fq_codel state UP group default qlen 1000 + link/ether 1a:25:a3:a5:1d:03 brd ff:ff:ff:ff:ff:ff + inet 10.0.2.100/24 scope global tap0 + valid_lft forever preferred_lft forever + inet6 fe80::1825:a3ff:fea5:1d03/64 scope link + valid_lft forever preferred_lft forever +rootlesskit$ ip r +default via 10.0.2.1 dev tap0 +10.0.2.0/24 dev tap0 proto kernel scope link src 10.0.2.100 +rootlesskit$ cat /etc/resolv.conf +nameserver 10.0.2.1 +rootlesskit$ curl https://www.google.com +... +``` + ## IPv6 The `--ipv6` flag (since v0.14.0, EXPERIMENTAL) enables IPv6 routing for slirp4netns network driver. diff --git a/go.mod b/go.mod index d9b9aa72..d9741c74 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.23.0 require ( github.com/Masterminds/semver/v3 v3.4.0 github.com/containernetworking/plugins v1.7.1 + github.com/containers/gvisor-tap-vsock v0.8.6 github.com/gofrs/flock v0.12.1 github.com/google/uuid v1.6.0 github.com/gorilla/mux v1.8.1 @@ -19,11 +20,23 @@ require ( ) require ( + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/apparentlymart/go-cidr v1.1.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect + github.com/google/btree v1.1.2 // indirect github.com/google/go-cmp v0.7.0 // indirect + github.com/google/gopacket v1.1.19 // indirect + github.com/miekg/dns v1.1.65 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect + golang.org/x/crypto v0.38.0 // indirect + golang.org/x/mod v0.24.0 // indirect golang.org/x/net v0.38.0 // indirect + golang.org/x/sync v0.14.0 // indirect + golang.org/x/time v0.5.0 // indirect + golang.org/x/tools v0.31.0 // indirect + gvisor.dev/gvisor v0.0.0-20240916094835-a174eb65023f // indirect ) diff --git a/go.sum b/go.sum index 730bf2b2..7c388bea 100644 --- a/go.sum +++ b/go.sum @@ -1,22 +1,34 @@ github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU= +github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= github.com/containernetworking/cni v1.3.0 h1:v6EpN8RznAZj9765HhXQrtXgX+ECGebEYEmnuFjskwo= github.com/containernetworking/cni v1.3.0/go.mod h1:Bs8glZjjFfGPHMw6hQu82RUgEPNGEaBb9KS5KtNMnJ4= github.com/containernetworking/plugins v1.7.1 h1:CNAR0jviDj6FS5Vg85NTgKWLDzZPfi/lj+VJfhMDTIs= github.com/containernetworking/plugins v1.7.1/go.mod h1:xuMdjuio+a1oVQsHKjr/mgzuZ24leAsqUYRnzGoXHy0= +github.com/containers/gvisor-tap-vsock v0.8.6 h1:9SeAXK+K2o36CtrgYk6zRXbU3zrayjvkrI8b7/O6u5A= +github.com/containers/gvisor-tap-vsock v0.8.6/go.mod h1:+0mtKmm4STeSDnZe+DGnIwN4EH2f7AcWir7PwT28Ti0= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 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/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= +github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +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-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -25,16 +37,30 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 h1:q3OEI9RaN/wwcx+qgGo6ZaoJkCiDYe/gjDLfq7lQQF4= github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905/go.mod h1:VvGYjkZoJyKqlmT1yzakUs4mfKMNB0XdODP0+rdml6k= +github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= +github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= +github.com/mdlayher/packet v1.1.2 h1:3Up1NG6LZrsgDVn6X4L9Ge/iyRyxFEFD9o6Pr3Q1nQY= +github.com/mdlayher/packet v1.1.2/go.mod h1:GEu1+n9sG5VtiRE4SydOmX5GTwyyYlteZiFU+x0kew4= +github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= +github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= +github.com/miekg/dns v1.1.65 h1:0+tIPHzUW0GCge7IiK3guGP57VAw7hoPDfApjkMD1Fc= +github.com/miekg/dns v1.1.65/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck= github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= github.com/moby/vpnkit v0.6.0 h1:HEh3iQ57oigvPNbR89R14pw3difgPyFOMMD3JAoqPoY= github.com/moby/vpnkit v0.6.0/go.mod h1:CNuEpfSK4ZY/NKFWD5M79GUZcYFydh81XQ2GZnT44cQ= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= @@ -45,8 +71,8 @@ github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +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/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM= github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA= github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= @@ -57,18 +83,44 @@ github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGC github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 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= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +gvisor.dev/gvisor v0.0.0-20240916094835-a174eb65023f h1:O2w2DymsOlM/nv2pLNWCMCYOldgBBMkD7H0/prN5W2k= +gvisor.dev/gvisor v0.0.0-20240916094835-a174eb65023f/go.mod h1:sxc3Uvk/vHcd3tj7/DHVBoR5wvWT/MmRq2pj7HRJnwU= diff --git a/hack/benchmark-iperf3-net.sh b/hack/benchmark-iperf3-net.sh index d2493fa4..4d15fac4 100755 --- a/hack/benchmark-iperf3-net.sh +++ b/hack/benchmark-iperf3-net.sh @@ -48,6 +48,17 @@ function benchmark::iperf3::lxc-user-nic() { set +x } +function benchmark::iperf3::gvisor-tap-vsock() { + INFO "[benchmark:iperf3] gvisor-tap-vsock ($@)" + statedir=$(mktemp -d) + if echo "$@" | grep -q -- --detach-netns; then + IPERF3C="nsenter -n${statedir}/netns $IPERF3C" + fi + set -x + $ROOTLESSKIT --state-dir=$statedir --net=gvisor-tap-vsock $@ -- $IPERF3C 10.0.2.1 + set +x +} + function benchmark::iperf3::rootful_veth() { INFO "[benchmark:iperf3] rootful_veth ($@) for reference" # only --mtu=MTU is supposed as $@ diff --git a/pkg/network/gvisortapvsock/gvisortapvsock.go b/pkg/network/gvisortapvsock/gvisortapvsock.go new file mode 100644 index 00000000..89e03405 --- /dev/null +++ b/pkg/network/gvisortapvsock/gvisortapvsock.go @@ -0,0 +1,530 @@ +package gvisortapvsock + +import ( + "context" + "encoding/binary" + "errors" + "fmt" + "io" + "math" + "net" + "os" + "path/filepath" + "strings" + "sync" + + "github.com/containernetworking/plugins/pkg/ns" + "github.com/containers/gvisor-tap-vsock/pkg/tap" + "github.com/containers/gvisor-tap-vsock/pkg/types" + "github.com/containers/gvisor-tap-vsock/pkg/virtualnetwork" + "github.com/sirupsen/logrus" + "github.com/songgao/water" + + "github.com/rootless-containers/rootlesskit/v2/pkg/api" + "github.com/rootless-containers/rootlesskit/v2/pkg/common" + "github.com/rootless-containers/rootlesskit/v2/pkg/messages" + "github.com/rootless-containers/rootlesskit/v2/pkg/network" + "github.com/rootless-containers/rootlesskit/v2/pkg/network/iputils" + "github.com/rootless-containers/rootlesskit/v2/pkg/network/parentutils" +) + +const ( + DriverName = "gvisor-tap-vsock" + // Default buffer size for packet reading/writing + defaultBufferSize = 65536 +) + +// NewParentDriver instantiates a new parent driver +func NewParentDriver(logWriter io.Writer, mtu int, ipnet *net.IPNet, ifname string, disableHostLoopback bool, enableIPv6 bool) (network.ParentDriver, error) { + if mtu < 0 { + return nil, errors.New("got negative mtu") + } + if mtu == 0 { + mtu = 65520 + } + + if ifname == "" { + ifname = "tap0" + } + + return &parentDriver{ + logWriter: logWriter, + mtu: mtu, + ipnet: ipnet, + ifname: ifname, + disableHostLoopback: disableHostLoopback, + enableIPv6: enableIPv6, + }, nil +} + +type parentDriver struct { + logWriter io.Writer + mtu int + ipnet *net.IPNet + ifname string + disableHostLoopback bool + enableIPv6 bool + infoMu sync.RWMutex + info func() *api.NetworkDriverInfo + + // Store the virtual network and its network switch for later use + vn *virtualnetwork.VirtualNetwork + networkSwitch *tap.Switch + vnMu sync.RWMutex + + // Socket for communication with the child namespace + socketPath string + listener net.Listener + ctx context.Context + cancel context.CancelFunc +} + +func (d *parentDriver) Info(ctx context.Context) (*api.NetworkDriverInfo, error) { + d.infoMu.RLock() + infoFn := d.info + d.infoMu.RUnlock() + if infoFn == nil { + return &api.NetworkDriverInfo{ + Driver: DriverName, + }, nil + } + + return infoFn(), nil +} + +func (d *parentDriver) MTU() int { + return d.mtu +} + +// setupNetworkConfig sets up the basic network configuration +func (d *parentDriver) setupNetworkConfig() (ip string, gateway string, netmask int, err error) { + + var ipAddr net.IP + var gw net.IP + + ipAddr, err = iputils.AddIPInt(d.ipnet.IP, 100) + if err != nil { + return "", "", 0, err + } + ip = ipAddr.String() + + gw, err = iputils.AddIPInt(d.ipnet.IP, 1) + if err != nil { + return "", "", 0, err + } + gateway = gw.String() + netmask, _ = d.ipnet.Mask.Size() + + return ip, gateway, netmask, nil +} + +// setupVirtualNetwork creates and configures the virtual network +func (d *parentDriver) setupVirtualNetwork(gateway string) (*virtualnetwork.VirtualNetwork, error) { + config := &types.Configuration{ + MTU: d.mtu, + Subnet: d.ipnet.String(), + GatewayIP: gateway, + // This MAC address is a locally administered address (LAA) as indicated by the second least significant + // bit of the first byte (5a). Using a fixed MAC address ensures consistent behavior across restarts + // and allows for easier debugging and identification of the gateway interface. + GatewayMacAddress: "5a:94:ef:e4:0c:dd", + DHCPStaticLeases: map[string]string{}, + } + + if !d.disableHostLoopback { + // Configure NAT to allow access to host's localhost (127.0.0.1) + // Map 10.0.2.1 (default gateway) to host's 127.0.0.1 + config.NAT = map[string]string{ + "10.0.2.1": "127.0.0.1", + } + } + + vn, err := virtualnetwork.New(config) + if err != nil { + return nil, fmt.Errorf("creating virtual network: %w", err) + } + + return vn, nil +} + +// setupDNSServers configures the DNS servers for the network +func (d *parentDriver) setupDNSServers() ([]string, error) { + dns := make([]string, 0, 2) + + // Add IPv4 DNS server + // dns server is bind to the gateway IP address + // https://github.com/containers/gvisor-tap-vsock/blob/main/pkg/types/configuration.go#L29 + dnsIP, err := iputils.AddIPInt(d.ipnet.IP, 1) + if err != nil { + return nil, err + } + dns = append(dns, dnsIP.String()) + + return dns, nil +} + +// prepareNetworkMessage creates the network message with all configuration details +func (d *parentDriver) prepareNetworkMessage(tap string, ip string, netmask int, gateway string) (*messages.ParentInitNetworkDriverCompleted, error) { + dnsServers, err := d.setupDNSServers() + if err != nil { + return nil, err + } + + netmsg := messages.ParentInitNetworkDriverCompleted{ + Dev: tap, + DNS: dnsServers, + MTU: d.mtu, + IP: ip, + Netmask: netmask, + Gateway: gateway, + } + + return &netmsg, nil +} + +// updateDriverInfo updates the driver info with network configuration +func (d *parentDriver) updateDriverInfo(netmsg *messages.ParentInitNetworkDriverCompleted) { + apiDNS := make([]net.IP, 0, len(netmsg.DNS)) + for _, nameserver := range netmsg.DNS { + apiDNS = append(apiDNS, net.ParseIP(nameserver)) + } + + d.infoMu.Lock() + d.info = func() *api.NetworkDriverInfo { + return &api.NetworkDriverInfo{ + Driver: DriverName, + DNS: apiDNS, + ChildIP: net.ParseIP(netmsg.IP), + DynamicChildIP: false, + } + } + d.infoMu.Unlock() +} + +// setupGvisortapvsockDir creates the directory for gvisor-tap-vsock files +func (d *parentDriver) setupGvisortapvsockDir(stateDir string) (string, error) { + gvisortapvsockDir := filepath.Join(stateDir, "gvisortapvsock") + if err := os.MkdirAll(gvisortapvsockDir, 0700); err != nil { + return "", fmt.Errorf("creating gvisortapvsock directory: %w", err) + } + return gvisortapvsockDir, nil +} + +// setupSocket creates a Unix socket for communication with the child namespace +func (d *parentDriver) setupSocket(gvisortapvsockDir string) error { + d.socketPath = filepath.Join(gvisortapvsockDir, "tap.sock") + + if err := os.RemoveAll(d.socketPath); err != nil { + return fmt.Errorf("removing existing socket: %w", err) + } + + listener, err := net.Listen("unix", d.socketPath) + if err != nil { + return fmt.Errorf("creating unix socket: %w", err) + } + + d.listener = listener + d.ctx, d.cancel = context.WithCancel(context.Background()) + + go d.acceptConnections() + + return nil +} + +// acceptConnections accepts connections from the child namespace +func (d *parentDriver) acceptConnections() { + for { + select { + case <-d.ctx.Done(): + return + default: + conn, err := d.listener.Accept() + if err != nil { + // Check if the error is due to the listener being closed, which is expected during cleanup + if errors.Is(err, io.EOF) || strings.Contains(err.Error(), "use of closed network connection") { + logrus.Debugf("listener closed, stopping accept loop") + return + } + logrus.Errorf("accepting connection: %v", err) + continue + } + + // Handle the connection + go d.handleConnection(conn) + } + } +} + +// handleConnection handles a connection from the child namespace +func (d *parentDriver) handleConnection(conn net.Conn) { + defer conn.Close() + + d.vnMu.RLock() + vn := d.vn + d.vnMu.RUnlock() + + if vn == nil { + logrus.Error("virtual network not initialized") + return + } + + // Use the AcceptStdio function to stream packets between the Unix socket and the virtual network + // This will handle the connection and forward packets in both directions + logrus.Debugf("forwarding packets between Unix socket and virtual network using VfkitProtocol") + if err := vn.AcceptStdio(d.ctx, conn); err != nil { + logrus.Debugf(err.Error()) + if errors.Is(err, io.EOF) { + // This is expected when the child process exits + logrus.Debugf("child process exited, connection closed: %v", err) + } else { + logrus.Errorf("accepting connection with VfkitProtocol: %v", err) + } + } +} + +// createCleanupFunc creates a cleanup function for the virtual network +func (d *parentDriver) createCleanupFunc(vn *virtualnetwork.VirtualNetwork) func() error { + return func() error { + logrus.Debugf("closing gvisor-tap-vsock virtual network") + // The VirtualNetwork struct doesn't have an explicit Close method, + // but we'll keep a reference to it to prevent garbage collection + _ = vn + + // Close the socket + if d.cancel != nil { + d.cancel() + } + if d.listener != nil { + if err := d.listener.Close(); err != nil { + logrus.Errorf("closing listener: %v", err) + } + } + + logrus.Debugf("closed gvisor-tap-vsock virtual network") + return nil + } +} + +func (d *parentDriver) ConfigureNetwork(childPID int, stateDir, detachedNetNSPath string) (*messages.ParentInitNetworkDriverCompleted, func() error, error) { + tap := d.ifname + var cleanups []func() error + + // Set up the tap device + if err := parentutils.PrepareTap(childPID, detachedNetNSPath, tap); err != nil { + return nil, common.Seq(cleanups), fmt.Errorf("setting up tap %s: %w", tap, err) + } + + // Set up network configuration + ip, gateway, netmask, err := d.setupNetworkConfig() + if err != nil { + return nil, common.Seq(cleanups), err + } + + // Create a directory for the gvisor-tap-vsock files + gvisortapvsockDir, err := d.setupGvisortapvsockDir(stateDir) + if err != nil { + return nil, common.Seq(cleanups), err + } + + // Set up the Unix socket + if err := d.setupSocket(gvisortapvsockDir); err != nil { + return nil, common.Seq(cleanups), fmt.Errorf("setting up socket: %w", err) + } + + // Set up virtual network + vn, err := d.setupVirtualNetwork(gateway) + if err != nil { + return nil, common.Seq(cleanups), err + } + + // Store the virtual network for later use + d.vnMu.Lock() + d.vn = vn + d.vnMu.Unlock() + + // Add cleanup function + cleanups = append(cleanups, d.createCleanupFunc(vn)) + + // Prepare network message + netmsg, err := d.prepareNetworkMessage(tap, ip, netmask, gateway) + if err != nil { + return nil, common.Seq(cleanups), err + } + + // Add the socket path to the network driver opaque + if netmsg.NetworkDriverOpaque == nil { + netmsg.NetworkDriverOpaque = make(map[string]string) + } + netmsg.NetworkDriverOpaque["socketPath"] = d.socketPath + + // Update driver info + d.updateDriverInfo(netmsg) + + return netmsg, common.Seq(cleanups), nil +} + +func NewChildDriver() network.ChildDriver { + return &childDriver{} +} + +type childDriver struct { + tap *water.Interface + conn net.Conn + socketPath string + bufferPool *sync.Pool +} + +func (d *childDriver) ChildDriverInfo() (*network.ChildDriverInfo, error) { + return &network.ChildDriverInfo{ + ConfiguresInterface: false, + }, nil +} + +func (d *childDriver) ConfigureNetworkChild(netmsg *messages.ParentInitNetworkDriverCompleted, detachedNetNSPath string) (string, error) { + tapName := netmsg.Dev + if tapName == "" { + return "", errors.New("could not determine the preconfigured tap") + } + + d.socketPath = netmsg.NetworkDriverOpaque["socketPath"] + if d.socketPath == "" { + return "", errors.New("socket path not provided") + } + + // Initialize buffer pool + d.bufferPool = &sync.Pool{ + New: func() interface{} { + return make([]byte, defaultBufferSize) + }, + } + + fn := func(_ ns.NetNS) error { + // Open the tap device + var err error + d.tap, err = water.New(water.Config{ + DeviceType: water.TAP, + PlatformSpecificParams: water.PlatformSpecificParams{ + Name: tapName, + }, + }) + if err != nil { + return fmt.Errorf("opening tap device %s: %w", tapName, err) + } + + var conn net.Conn + conn, err = net.Dial("unix", d.socketPath) + if err != nil { + return fmt.Errorf("connecting to socket %s: %w", d.socketPath, err) + } + + d.conn = conn + + // Start forwarding goroutines for direct streaming between tap and socket + go d.forwardTapToSocket() + go d.forwardSocketToTap() + + return nil + } + + if detachedNetNSPath == "" { + if err := fn(nil); err != nil { + return "", err + } + } else { + if err := ns.WithNetNSPath(detachedNetNSPath, fn); err != nil { + return "", err + } + } + + // tap is created, opened, and "up". + // IP stuff and MTU are not configured by the parent here, + // and they are up to the child. + return tapName, nil +} + +// forwardTapToSocket forwards packets from the tap device to the Unix socket +func (d *childDriver) forwardTapToSocket() { + size := make([]byte, 2) + + for { + // Get buffer from pool + bufInterface := d.bufferPool.Get() + buf := bufInterface.([]byte) + + n, err := d.tap.Read(buf) + if err != nil { + // Return buffer to pool before exiting + d.bufferPool.Put(buf) + if err != io.EOF { + logrus.Errorf("reading from tap: %v", err) + } + return + } + + if n < 0 || n > math.MaxUint16 { + // Return buffer to pool and continue + d.bufferPool.Put(buf) + logrus.Errorf("invalid frame length: %d", n) + continue + } + + // Encode size as 16-bit little-endian + binary.LittleEndian.PutUint16(size, uint16(n)) + + // Write size+packet to socket + if _, err := d.conn.Write(append(size, buf[:n]...)); err != nil { + // Return buffer to pool before exiting + d.bufferPool.Put(buf) + if err != io.EOF { + logrus.Errorf("writing to socket: %v", err) + } + return + } + + // Return buffer to pool for reuse + d.bufferPool.Put(buf) + } +} + +// forwardSocketToTap forwards packets from the Unix socket to the tap device +func (d *childDriver) forwardSocketToTap() { + sizeBuf := make([]byte, 2) + + for { + _, err := io.ReadFull(d.conn, sizeBuf) + if err != nil { + if err != io.EOF { + logrus.Errorf("reading size from socket: %v", err) + } + return + } + + size := int(binary.LittleEndian.Uint16(sizeBuf)) + + // Get buffer from pool + bufInterface := d.bufferPool.Get() + buf := bufInterface.([]byte) + + _, err = io.ReadFull(d.conn, buf[:size]) + if err != nil { + // Return buffer to pool before exiting + d.bufferPool.Put(buf) + if err != io.EOF { + logrus.Errorf("reading packet from socket: %v", err) + } + return + } + + if _, err := d.tap.Write(buf[:size]); err != nil { + // Return buffer to pool before exiting + d.bufferPool.Put(buf) + if err != io.EOF { + logrus.Errorf("writing to tap: %v", err) + } + return + } + + // Return buffer to pool for reuse + d.bufferPool.Put(buf) + } +}