runsc: workaround for scraping nftables

This feature is flag-gated because it can increase runsc startup time by
spawning several child processes.

Testing is done via shell script rather than the usually-preferred Go Docker
API because `docker network create` installs a bunch of iptables rules, whereas
the analagous API calls do not.

PiperOrigin-RevId: 582848436
This commit is contained in:
Kevin Krakauer
2023-11-15 16:53:23 -08:00
committed by gVisor bot
parent 9fd832da02
commit e7e8d0f971
7 changed files with 172 additions and 3 deletions
+2
View File
@@ -331,6 +331,8 @@ iptables-tests: load-iptables $(RUNTIME_BIN)
@#$(call test,--test_env=RUNTIME=runc //test/iptables:iptables_test)
@$(call install_runtime,$(RUNTIME),--net-raw)
@$(call test_runtime,$(RUNTIME),--test_env=TEST_NET_RAW=true //test/iptables:iptables_test)
@$(call install_runtime,$(RUNTIME)-nftables,--net-raw --reproduce-nftables)
@$(call test_runtime,$(RUNTIME)-nftables, --test_output=all //test/iptables:nftables_test --test_arg=$(RUNTIME)-nftables)
.PHONY: iptables-tests
packetdrill-tests: load-packetdrill $(RUNTIME_BIN)
+3
View File
@@ -1,2 +1,5 @@
FROM ubuntu:mantic
# The iptables package installs both iptables-legacy and iptables-nft, with
# iptables symlinked to the latter.
RUN apt update && apt install -y iptables
+4
View File
@@ -334,6 +334,10 @@ type Config struct {
// ReproduceNAT, when true, tells runsc to scrape the host network
// namespace's NAT iptables and reproduce it inside the sandbox.
ReproduceNAT bool `flag:"reproduce-nat"`
// ReproduceNftables attempts to scrape nftables routing rules if
// present, and reproduce them in the sandbox.
ReproduceNftables bool `flag:"reproduce-nftables"`
}
func (c *Config) validate() error {
+1
View File
@@ -121,6 +121,7 @@ func RegisterFlags(flagSet *flag.FlagSet) {
flagSet.Bool("buffer-pooling", true, "enable allocation of buffers from a shared pool instead of the heap.")
flagSet.Bool("EXPERIMENTAL-afxdp", false, "EXPERIMENTAL. Use an AF_XDP socket to receive packets.")
flagSet.Bool("reproduce-nat", false, "Scrape the host netns NAT table and reproduce it in the sandbox.")
flagSet.Bool("reproduce-nftables", false, "Attempt to scrape and reproduce nftable rules inside the sandbox. Overrides reproduce-nat when true.")
// Flags that control sandbox runtime behavior: accelerator related.
flagSet.Bool("nvproxy", false, "EXPERIMENTAL: enable support for Nvidia GPUs")
+103 -3
View File
@@ -19,6 +19,7 @@ import (
"fmt"
"net"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
@@ -320,12 +321,19 @@ func createInterfacesAndRoutesFromNS(conn *urpc.Client, nsPath string, conf *con
}
// Pass the host's NAT table if requested.
if conf.ReproduceNAT {
args.NATBlob = true
f, err := writeNATBlob()
if conf.ReproduceNftables || conf.ReproduceNAT {
var f *os.File
if conf.ReproduceNftables {
log.Infof("reproing nftables")
f, err = checkNftables()
} else if conf.ReproduceNAT {
log.Infof("reproing legacy tables")
f, err = writeNATBlob()
}
if err != nil {
return fmt.Errorf("failed to write NAT blob: %v", err)
}
args.NATBlob = true
args.FilePayload.Files = append(args.FilePayload.Files, f)
}
@@ -597,3 +605,95 @@ func removeAddress(source netlink.Link, ipAndMask string) error {
}
return netlink.AddrDel(source, addr)
}
// The below is a work around to generate iptables-legacy rules on machines
// that use iptables-nftables. The logic goes something like this:
//
// start
// |
// v no
// are legacy tables empty? -----> scrape rules -----> done <----+
// | ^ |
// | yes | |
// v yes | |
// are nft tables empty? -------------------------------+ |
// | |
// | no |
// v |
// pipe iptables-nft-save -t nat to iptables-legacy-restore |
// scrape rules |
// delete iptables-legacy rules |
// | |
// +---------------------------------------------------+
//
// If we fail at some point (e.g. to find a binary), we just try to scrape the
// legacy rules.
const emptyNatRules = `-P PREROUTING ACCEPT
-P INPUT ACCEPT
-P OUTPUT ACCEPT
-P POSTROUTING ACCEPT
`
func checkNftables() (*os.File, error) {
// Use iptables (not iptables-save) to test table emptiness because it
// gives predictable results: no counters and no comments.
// Is the legacy table empty?
if out, err := exec.Command("iptables-legacy", "-t", "nat", "-S").Output(); err != nil || string(out) != emptyNatRules {
return writeNATBlob()
}
// Is the nftables table empty?
if out, err := exec.Command("iptables-nft", "-t", "nat", "-S").Output(); err != nil || string(out) == emptyNatRules {
return nil, fmt.Errorf("no rules to scrape: %v", err)
}
// Get the current (empty) legacy rules.
currLegacy, err := exec.Command("iptables-legacy-save", "-t", "nat").Output()
if err != nil {
return nil, fmt.Errorf("failed to save existing rules with error (%v) and output: %s", err, currLegacy)
}
// Restore empty legacy rules.
defer func() {
cmd := exec.Command("iptables-legacy-restore")
stdin, err := cmd.StdinPipe()
if err != nil {
log.Warningf("failed to get stdin pipe: %v", err)
return
}
go func() {
defer stdin.Close()
stdin.Write(currLegacy)
}()
if out, err := cmd.CombinedOutput(); err != nil {
log.Warningf("failed to restore iptables error (%v) with output: %s", err, out)
}
}()
// Pipe the output of iptables-nft-save to iptables-legacy-restore.
nftOut, err := exec.Command("iptables-nft-save", "-t", "nat").Output()
if err != nil {
return nil, fmt.Errorf("failed to run iptables-nft-save: %v", err)
}
cmd := exec.Command("iptables-legacy-restore")
stdin, err := cmd.StdinPipe()
if err != nil {
return nil, fmt.Errorf("failed to get stdin pipe: %v", err)
}
go func() {
defer stdin.Close()
stdin.Write(nftOut)
}()
if out, err := cmd.CombinedOutput(); err != nil {
return nil, fmt.Errorf("failed to restore iptables error (%v) with output: %s", err, out)
}
return writeNATBlob()
}
+10
View File
@@ -42,3 +42,13 @@ go_test(
"//pkg/test/testutil",
],
)
sh_test(
name = "nftables_test",
srcs = ["nftables_test.sh"],
tags = [
"local",
"manual",
],
visibility = ["//:sandbox"],
)
+49
View File
@@ -0,0 +1,49 @@
#!/bin/bash
# Copyright 2023 The gVisor Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
set -euxo pipefail
# expected_regex is generated by running `iptables -t nat -S` inside a runc
# Docker container connected to a custom network. Custom networks cause Docker
# to install DNS routing rules.
expected_regex='-P PREROUTING ACCEPT
-P INPUT ACCEPT
-P OUTPUT ACCEPT
-P POSTROUTING ACCEPT
-N DOCKER_OUTPUT
-N DOCKER_POSTROUTING
-A OUTPUT -d 127.0.0.11/32 -j DOCKER_OUTPUT
-A POSTROUTING -d 127.0.0.11/32 -j DOCKER_POSTROUTING
-A DOCKER_OUTPUT -d 127.0.0.11/32 -p tcp -m tcp --dport 53 -j DNAT --to-destination 127.0.0.11:[0-9]+
-A DOCKER_OUTPUT -d 127.0.0.11/32 -p udp -m udp --dport 53 -j DNAT --to-destination 127.0.0.11:[0-9]+
-A DOCKER_POSTROUTING -s 127.0.0.11/32 -p tcp -m tcp --sport [0-9]+ -j SNAT --to-source :53
-A DOCKER_POSTROUTING -s 127.0.0.11/32 -p udp -m udp --sport [0-9]+ -j SNAT --to-source :53'
# The runtime name is the first and only argument.
runtime="$1"
# The image passed to docker run uses iptables-nft by default, so the above
# rules can't be simply scraped and passed to gVisor. We test that those rules
# are correctly translated to iptables-legacy rules.
net_name="nftables-test-net-$(shuf -i 0-99999999 -n 1)"
docker network create "$net_name"
trap "docker network rm \"$net_name\"" EXIT
got=$(docker run --network="$net_name" --rm --runtime "$runtime" --privileged gvisor.dev/images/iptables iptables-legacy -t nat -S)
if ! [[ "$got" =~ $expected_regex ]]; then
echo "Got incorrect rules: got on the left, want on the right"
diff <(echo "$got") <(echo "$expected_regex")
exit 1
fi