The current behavior stomps on the rules that programs like podman or tailscale set up for port forwarding. With this change, we split port forwardings into a separate chain, which allows us to create the ruleset once at startup and then only update the port forwardings specifically (the only dynamic part of router7’s nftables ruleset).
807 lines
22 KiB
Go
807 lines
22 KiB
Go
// Copyright 2018 Google Inc.
|
|
//
|
|
// 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.
|
|
|
|
package integration_test
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/rtr7/router7/internal/netconfig"
|
|
"github.com/vishvananda/netlink"
|
|
"github.com/vishvananda/netns"
|
|
|
|
"github.com/andreyvit/diff"
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/nftables"
|
|
)
|
|
|
|
const goldenInterfaces = `
|
|
{
|
|
"interfaces":[
|
|
{
|
|
"hardware_addr": "02:73:53:00:ca:fe",
|
|
"name": "uplink0"
|
|
},
|
|
{
|
|
"hardware_addr": "02:73:53:00:b0:0c",
|
|
"spoof_hardware_addr": "02:73:53:00:b0:aa",
|
|
"name": "lan0",
|
|
"addr": "192.168.42.1/24",
|
|
"mtu": 1492
|
|
},
|
|
{
|
|
"name": "wg0",
|
|
"addr": "fe80::1/64",
|
|
"extra_addrs": [
|
|
"10.22.100.1/24"
|
|
],
|
|
"extra_routes": [
|
|
{
|
|
"destination": "2a02:168:4a00:22::/64",
|
|
"gateway": "fe80::2"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
`
|
|
|
|
const goldenWireguard = `
|
|
{
|
|
"interfaces":[
|
|
{
|
|
"name": "wg0",
|
|
"private_key": "gBCV3afBKfW7RycmeZFMpJykvO+58KfSEIyavay90kE=",
|
|
"port": 51820,
|
|
"peers": [
|
|
{
|
|
"public_key": "ScxV5nQsUIaaOp3qdwPqRcgMkR3oR6nyi1tBLUovqBs=",
|
|
"endpoint": "192.168.42.23:12345",
|
|
"allowed_ips": [
|
|
"fe80::/64",
|
|
"10.0.137.0/24"
|
|
]
|
|
},
|
|
{
|
|
"public_key": "AVU3LodtnFaFnJmMyNNW7cUk4462lqnVULTFkjWYvRo=",
|
|
"endpoint": "[::1]:12345",
|
|
"allowed_ips": [
|
|
"10.0.0.0/8"
|
|
]
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "wg1",
|
|
"private_key": "gBCV3afBKfW7RycmeZFMpJykvO+58KfSEIyavay90kE=",
|
|
"port": 51820,
|
|
"peers": [
|
|
{
|
|
"public_key": "ScxV5nQsUIaaOp3qdwPqRcgMkR3oR6nyi1tBLUovqBs=",
|
|
"allowed_ips": [
|
|
"fe80::/64"
|
|
]
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
`
|
|
|
|
func goldenPortForwardings(additionalForwarding bool) string {
|
|
add := ""
|
|
if additionalForwarding {
|
|
add = `
|
|
{
|
|
"port": "8045",
|
|
"dest_addr": "192.168.42.22",
|
|
"dest_port": "8045"
|
|
},
|
|
`
|
|
}
|
|
return `
|
|
{
|
|
"forwardings":[
|
|
{
|
|
"port": "8080",
|
|
"dest_addr": "192.168.42.23",
|
|
"dest_port": "9999"
|
|
},
|
|
` + add + `
|
|
{
|
|
"port": "8040-8060",
|
|
"dest_addr": "192.168.42.99",
|
|
"dest_port": "8040-8060"
|
|
},
|
|
{
|
|
"proto": "udp",
|
|
"port": "53",
|
|
"dest_addr": "192.168.42.99",
|
|
"dest_port": "53"
|
|
}
|
|
]
|
|
}
|
|
`
|
|
}
|
|
|
|
func goldenNftablesRules(additionalForwarding bool) string {
|
|
add := ""
|
|
if additionalForwarding {
|
|
add = `
|
|
ip daddr != 127.0.0.0/8 ip daddr != 10.0.0.0/24 fib daddr type 2 tcp dport 8045 dnat to 192.168.42.22:8045`
|
|
}
|
|
return `table ip nat {
|
|
chain router7-portforwardings {
|
|
ip daddr != 127.0.0.0/8 ip daddr != 10.0.0.0/24 fib daddr type 2 tcp dport 8080 dnat to 192.168.42.23:9999` + add + `
|
|
ip daddr != 127.0.0.0/8 ip daddr != 10.0.0.0/24 fib daddr type 2 tcp dport 8040-8060 dnat to 192.168.42.99:8040-8060
|
|
ip daddr != 127.0.0.0/8 ip daddr != 10.0.0.0/24 fib daddr type 2 udp dport 53 dnat to 192.168.42.99:53
|
|
}
|
|
|
|
chain prerouting {
|
|
type nat hook prerouting priority 0; policy accept;
|
|
jump router7-portforwardings
|
|
}
|
|
|
|
chain postrouting {
|
|
type nat hook postrouting priority 100; policy accept;
|
|
oifname "uplink0" masquerade
|
|
iifname "lan0" oifname "lan0" ct status 0x20 masquerade
|
|
}
|
|
}
|
|
table ip filter {
|
|
counter fwded {
|
|
packets 23 bytes 42
|
|
}
|
|
|
|
counter inputc {
|
|
packets 23 bytes 42
|
|
}
|
|
|
|
counter outputc {
|
|
packets 23 bytes 42
|
|
}
|
|
|
|
chain forward {
|
|
type filter hook forward priority 0; policy accept;
|
|
oifname "uplink0" tcp flags 0x2 tcp option maxseg size set rt mtu
|
|
counter name "fwded"
|
|
}
|
|
|
|
chain input {
|
|
type filter hook input priority 0; policy accept;
|
|
counter name "inputc"
|
|
}
|
|
|
|
chain output {
|
|
type filter hook output priority 0; policy accept;
|
|
counter name "outputc"
|
|
}
|
|
}
|
|
table ip6 filter {
|
|
counter fwded {
|
|
packets 23 bytes 42
|
|
}
|
|
|
|
counter inputc {
|
|
packets 23 bytes 42
|
|
}
|
|
|
|
counter outputc {
|
|
packets 23 bytes 42
|
|
}
|
|
|
|
chain forward {
|
|
type filter hook forward priority 0; policy accept;
|
|
oifname "uplink0" tcp flags 0x2 tcp option maxseg size set rt mtu
|
|
counter name "fwded"
|
|
}
|
|
|
|
chain input {
|
|
type filter hook input priority 0; policy accept;
|
|
counter name "inputc"
|
|
}
|
|
|
|
chain output {
|
|
type filter hook output priority 0; policy accept;
|
|
counter name "outputc"
|
|
}
|
|
}`
|
|
}
|
|
|
|
const goldenDhcp4 = `
|
|
{
|
|
"valid_until":"2018-05-18T23:46:04.429895261+02:00",
|
|
"client_ip":"85.195.207.62",
|
|
"subnet_mask":"255.255.255.128",
|
|
"router":"85.195.207.1",
|
|
"dns":[
|
|
"77.109.128.2",
|
|
"213.144.129.20"
|
|
]
|
|
}
|
|
`
|
|
|
|
const goldenDhcp6 = `
|
|
{
|
|
"valid_until":"0001-01-01T00:00:00Z",
|
|
"prefixes":[
|
|
{"IP":"2a02:168:4a00::","Mask":"////////AAAAAAAAAAAAAA=="}
|
|
],
|
|
"dns":[
|
|
"2001:1620:2777:1::10",
|
|
"2001:1620:2777:2::20"
|
|
]
|
|
}
|
|
`
|
|
|
|
type wgLink struct {
|
|
ns int
|
|
}
|
|
|
|
func (w *wgLink) Type() string { return "wireguard" }
|
|
|
|
func (w *wgLink) Attrs() *netlink.LinkAttrs {
|
|
attrs := netlink.NewLinkAttrs()
|
|
attrs.Name = "wg5"
|
|
if w.ns > 0 {
|
|
attrs.Namespace = netlink.NsFd(w.ns)
|
|
}
|
|
return &attrs
|
|
}
|
|
|
|
var wireGuardAvailable = func() bool {
|
|
// The wg tool must also be available for our test to succeed:
|
|
if _, err := exec.LookPath("wg"); err != nil {
|
|
return false
|
|
}
|
|
|
|
// ns must not collide with any namespace used in the test functions: this
|
|
// function will be called by the helper process, too.
|
|
const ns = "ns4"
|
|
add := exec.Command("ip", "netns", "add", ns)
|
|
add.Stderr = os.Stderr
|
|
if err := add.Run(); err != nil {
|
|
log.Fatalf("%v: %v", add.Args, err)
|
|
}
|
|
defer exec.Command("ip", "netns", "delete", ns).Run()
|
|
|
|
nsHandle, err := netns.GetFromName(ns)
|
|
if err != nil {
|
|
log.Printf("GetFromName: %v", err)
|
|
return false
|
|
}
|
|
|
|
if err := netlink.LinkAdd(&wgLink{ns: int(nsHandle)}); err != nil {
|
|
log.Printf("netlink.LinkAdd: %v", err)
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}()
|
|
|
|
func TestNetconfig(t *testing.T) {
|
|
if os.Getenv("HELPER_PROCESS") == "1" {
|
|
tmp, err := ioutil.TempDir("", "router7")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tmp)
|
|
|
|
pf := goldenPortForwardings(os.Getenv("ADDITIONAL_PORT_FORWARDINGS") == "1")
|
|
for _, golden := range []struct {
|
|
filename, content string
|
|
}{
|
|
{"dhcp4/wire/lease.json", goldenDhcp4},
|
|
{"dhcp6/wire/lease.json", goldenDhcp6},
|
|
{"interfaces.json", goldenInterfaces},
|
|
{"portforwardings.json", pf},
|
|
} {
|
|
if err := os.MkdirAll(filepath.Join(tmp, filepath.Dir(golden.filename)), 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := ioutil.WriteFile(filepath.Join(tmp, golden.filename), []byte(golden.content), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if wireGuardAvailable {
|
|
if err := ioutil.WriteFile(filepath.Join(tmp, "wireguard.json"), []byte(goldenWireguard), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
if err := os.MkdirAll(filepath.Join(tmp, "root", "etc"), 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := os.MkdirAll(filepath.Join(tmp, "root", "tmp"), 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
netconfig.DefaultCounterObj = &nftables.CounterObj{Packets: 23, Bytes: 42}
|
|
if err := netconfig.Apply(tmp, filepath.Join(tmp, "root")); err != nil {
|
|
t.Fatalf("netconfig.Apply: %v", err)
|
|
}
|
|
|
|
// Apply twice to ensure the absence of errors when dealing with
|
|
// already-configured interfaces, addresses, routes, … (and ensure
|
|
// nftables rules are replaced, not appendend to).
|
|
netconfig.DefaultCounterObj = &nftables.CounterObj{Packets: 0, Bytes: 0}
|
|
if err := netconfig.Apply(tmp, filepath.Join(tmp, "root")); err != nil {
|
|
t.Fatalf("netconfig.Apply: %v", err)
|
|
}
|
|
|
|
b, err := ioutil.ReadFile(filepath.Join(tmp, "root", "tmp", "resolv.conf"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, want := strings.TrimSpace(string(b)), "nameserver 192.168.42.1"; got != want {
|
|
t.Errorf("/tmp/resolv.conf: got %q, want %q", got, want)
|
|
}
|
|
|
|
return
|
|
}
|
|
const ns = "ns3" // name of the network namespace to use for this test
|
|
|
|
add := exec.Command("ip", "netns", "add", ns)
|
|
add.Stderr = os.Stderr
|
|
if err := add.Run(); err != nil {
|
|
t.Fatalf("%v: %v", add.Args, err)
|
|
}
|
|
defer exec.Command("ip", "netns", "delete", ns).Run()
|
|
|
|
nsSetup := []*exec.Cmd{
|
|
exec.Command("ip", "-netns", ns, "link", "add", "dummy0", "type", "dummy"),
|
|
exec.Command("ip", "-netns", ns, "link", "add", "lan0", "type", "dummy"),
|
|
exec.Command("ip", "-netns", ns, "link", "set", "dummy0", "address", "02:73:53:00:ca:fe"),
|
|
exec.Command("ip", "-netns", ns, "link", "set", "lan0", "address", "02:73:53:00:b0:0c"),
|
|
}
|
|
|
|
for _, cmd := range nsSetup {
|
|
if err := cmd.Run(); err != nil {
|
|
t.Fatalf("%v: %v", cmd.Args, err)
|
|
}
|
|
}
|
|
|
|
cmd := exec.Command("ip", "netns", "exec", ns, os.Args[0], "-test.run=^TestNetconfig$")
|
|
cmd.Env = append(os.Environ(), "HELPER_PROCESS=1")
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
if err := cmd.Run(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
t.Run("VerifyAddresses", func(t *testing.T) {
|
|
link, err := exec.Command("ip", "-netns", ns, "link", "show", "dev", "lan0").Output()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(string(link), "link/ether 02:73:53:00:b0:aa") {
|
|
t.Errorf("lan0 MAC address is not 02:73:53:00:b0:aa")
|
|
}
|
|
if !strings.Contains(string(link), " mtu 1492 ") {
|
|
t.Errorf("lan0 MTU is not 1492 (link: %q)", string(link))
|
|
}
|
|
|
|
addrs, err := exec.Command("ip", "-netns", ns, "address", "show", "dev", "uplink0").Output()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
addrRe := regexp.MustCompile(`(?m)^\s*inet 85.195.207.62/25 brd 85.195.207.127 scope global uplink0$`)
|
|
if !addrRe.MatchString(string(addrs)) {
|
|
t.Fatalf("regexp %s does not match %s", addrRe, string(addrs))
|
|
}
|
|
|
|
addrsLan, err := exec.Command("ip", "-netns", ns, "address", "show", "dev", "lan0").Output()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
addr6Re := regexp.MustCompile(`(?m)^\s*inet6 2a02:168:4a00::1/64 scope global\s*$`)
|
|
if !addr6Re.MatchString(string(addrsLan)) {
|
|
t.Fatalf("regexp %s does not match %s", addr6Re, string(addrsLan))
|
|
}
|
|
|
|
wantRoutes := []string{
|
|
"default via 85.195.207.1 proto dhcp src 85.195.207.62 ",
|
|
"85.195.207.0/25 proto kernel scope link src 85.195.207.62 ",
|
|
"85.195.207.1 proto dhcp scope link src 85.195.207.62",
|
|
}
|
|
|
|
routes, err := ipLines("-netns", ns, "route", "show", "dev", "uplink0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if diff := cmp.Diff(wantRoutes, routes); diff != "" {
|
|
t.Fatalf("routes: diff (-want +got):\n%s", diff)
|
|
}
|
|
})
|
|
|
|
t.Run("VerifyWireguard", func(t *testing.T) {
|
|
if !wireGuardAvailable {
|
|
t.Skipf("WireGuard not available on this machine")
|
|
}
|
|
var stderr bytes.Buffer
|
|
wg := exec.Command("ip", "netns", "exec", ns, "wg", "show", "wg0")
|
|
wg.Stderr = &stderr
|
|
out, err := wg.Output()
|
|
if err != nil {
|
|
t.Fatalf("%v: %v (stderr: %v)", wg.Args, err, strings.TrimSpace(stderr.String()))
|
|
}
|
|
const want = `interface: wg0
|
|
public key: 3ck9nX4ylfXm0fq4pWJ9n8Jku4fvzIXBVe3BsCNldB8=
|
|
private key: (hidden)
|
|
listening port: 51820
|
|
|
|
peer: ScxV5nQsUIaaOp3qdwPqRcgMkR3oR6nyi1tBLUovqBs=
|
|
endpoint: 192.168.42.23:12345
|
|
allowed ips: 10.0.137.0/24, fe80::/64
|
|
|
|
peer: AVU3LodtnFaFnJmMyNNW7cUk4462lqnVULTFkjWYvRo=
|
|
endpoint: [::1]:12345
|
|
allowed ips: 10.0.0.0/8`
|
|
got := strings.TrimSpace(string(out))
|
|
// Enforce an order (it can change, or did change between kernel
|
|
// versions):
|
|
got = strings.ReplaceAll(got,
|
|
" allowed ips: fe80::/64, 10.0.137.0/24",
|
|
" allowed ips: 10.0.137.0/24, fe80::/64")
|
|
if got != want {
|
|
t.Fatalf("unexpected wg output: diff (-want +got):\n%s", diff.LineDiff(want, got))
|
|
}
|
|
|
|
out, err = exec.Command("ip", "-netns", ns, "address", "show", "dev", "wg0").Output()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
upRe := regexp.MustCompile(`wg0: <[^>]+,UP`)
|
|
if !upRe.MatchString(string(out)) {
|
|
t.Errorf("regexp %s does not match %s", upRe, string(out))
|
|
}
|
|
|
|
addr4Re := regexp.MustCompile(`(?m)^\s*inet 10.22.100.1/24 brd 10.22.100.255 scope global wg0\s*$`)
|
|
if !addr4Re.MatchString(string(out)) {
|
|
t.Errorf("regexp %s does not match %s", addr4Re, string(out))
|
|
}
|
|
|
|
addr6Re := regexp.MustCompile(`(?m)^\s*inet6 fe80::1/64 scope link\s*$`)
|
|
if !addr6Re.MatchString(string(out)) {
|
|
t.Errorf("regexp %s does not match %s", addr6Re, string(out))
|
|
}
|
|
|
|
out, err = exec.Command("ip", "-netns", ns, "-6", "route", "show", "dev", "wg0").Output()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
extraRouteRe := regexp.MustCompile(`(?m)^\s*2a02:168:4a00:22::/64 via fe80::2 metric 1024 pref medium\s*$`)
|
|
if !extraRouteRe.MatchString(string(out)) {
|
|
t.Errorf("regexp %s does not match %s", extraRouteRe, string(out))
|
|
}
|
|
|
|
})
|
|
|
|
opts := []cmp.Option{
|
|
cmp.Transformer("formatting", func(line string) string {
|
|
return strings.TrimSpace(strings.Replace(line, "dnat to", "dnat", -1))
|
|
}),
|
|
}
|
|
|
|
t.Run("VerifyNftables", func(t *testing.T) {
|
|
rules, err := ipLines("netns", "exec", ns, "nft", "--numeric", "list", "ruleset")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(rules) < 2 {
|
|
t.Fatalf("nftables rules not found")
|
|
}
|
|
|
|
got := strings.Join(rules, "\n")
|
|
if diff := cmp.Diff(goldenNftablesRules(false), got, opts...); diff != "" {
|
|
t.Fatalf("unexpected nftables rules: diff (-want +got):\n%s", diff)
|
|
}
|
|
})
|
|
|
|
cmd = exec.Command("ip", "netns", "exec", ns, os.Args[0], "-test.run=^TestNetconfig$")
|
|
cmd.Env = append(os.Environ(), "HELPER_PROCESS=1", "ADDITIONAL_PORT_FORWARDINGS=1")
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
if err := cmd.Run(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
t.Run("VerifyAdditionalNftables", func(t *testing.T) {
|
|
rules, err := ipLines("netns", "exec", ns, "nft", "--numeric", "list", "ruleset")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(rules) < 2 {
|
|
t.Fatalf("nftables rules not found")
|
|
}
|
|
|
|
if got, want := strings.Join(rules, "\n"), goldenNftablesRules(true); got != want {
|
|
t.Fatalf("unexpected nftables rules: diff (-want +got):\n%s", diff.LineDiff(want, got))
|
|
}
|
|
})
|
|
}
|
|
|
|
const goldenInterfacesBridges = `
|
|
{
|
|
"bridges":[
|
|
{
|
|
"name": "lan0",
|
|
"interface_hardware_addrs": ["02:73:53:00:b0:0c"]
|
|
}
|
|
],
|
|
"interfaces":[
|
|
{
|
|
"hardware_addr": "02:73:53:00:ca:fe",
|
|
"name": "uplink0"
|
|
},
|
|
{
|
|
"spoof_hardware_addr": "02:73:53:00:b0:aa",
|
|
"name": "lan0",
|
|
"addr": "192.168.42.1/24"
|
|
}
|
|
]
|
|
}
|
|
`
|
|
|
|
func TestNetconfigBridges(t *testing.T) {
|
|
if os.Getenv("HELPER_PROCESS") == "1" {
|
|
tmp, err := ioutil.TempDir("", "router7")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tmp)
|
|
|
|
for _, golden := range []struct {
|
|
filename, content string
|
|
}{
|
|
{"interfaces.json", goldenInterfacesBridges},
|
|
} {
|
|
if err := os.MkdirAll(filepath.Join(tmp, filepath.Dir(golden.filename)), 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := ioutil.WriteFile(filepath.Join(tmp, golden.filename), []byte(golden.content), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
if err := os.MkdirAll(filepath.Join(tmp, "root", "etc"), 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := os.MkdirAll(filepath.Join(tmp, "root", "tmp"), 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
netconfig.DefaultCounterObj = &nftables.CounterObj{Packets: 23, Bytes: 42}
|
|
if err := netconfig.Apply(tmp, filepath.Join(tmp, "root")); err != nil {
|
|
t.Fatalf("netconfig.Apply: %v", err)
|
|
}
|
|
|
|
// Apply twice to ensure the absence of errors when dealing with
|
|
// already-configured interfaces, addresses, routes, … (and ensure
|
|
// nftables rules are replaced, not appendend to).
|
|
netconfig.DefaultCounterObj = &nftables.CounterObj{Packets: 0, Bytes: 0}
|
|
if err := netconfig.Apply(tmp, filepath.Join(tmp, "root")); err != nil {
|
|
t.Fatalf("netconfig.Apply: %v", err)
|
|
}
|
|
|
|
return
|
|
}
|
|
const ns = "ns6" // name of the network namespace to use for this test
|
|
|
|
add := exec.Command("ip", "netns", "add", ns)
|
|
add.Stderr = os.Stderr
|
|
if err := add.Run(); err != nil {
|
|
t.Fatalf("%v: %v", add.Args, err)
|
|
}
|
|
defer exec.Command("ip", "netns", "delete", ns).Run()
|
|
|
|
nsSetup := []*exec.Cmd{
|
|
exec.Command("ip", "-netns", ns, "link", "add", "dummy0", "type", "dummy"),
|
|
exec.Command("ip", "-netns", ns, "link", "add", "eth0", "type", "dummy"),
|
|
exec.Command("ip", "-netns", ns, "link", "set", "dummy0", "address", "02:73:53:00:ca:fe"),
|
|
exec.Command("ip", "-netns", ns, "link", "set", "eth0", "address", "02:73:53:00:b0:0c"),
|
|
}
|
|
|
|
for _, cmd := range nsSetup {
|
|
if err := cmd.Run(); err != nil {
|
|
t.Fatalf("%v: %v", cmd.Args, err)
|
|
}
|
|
}
|
|
|
|
cmd := exec.Command("ip", "netns", "exec", ns, os.Args[0], "-test.run=^TestNetconfigBridges")
|
|
cmd.Env = append(os.Environ(), "HELPER_PROCESS=1")
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
if err := cmd.Run(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
t.Run("VerifyAddresses", func(t *testing.T) {
|
|
link, err := exec.Command("ip", "-netns", ns, "link", "show", "dev", "lan0", "type", "bridge").Output()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(string(link), "link/ether 02:73:53:00:b0:aa") {
|
|
t.Errorf("lan0 MAC address is not 02:73:53:00:b0:aa")
|
|
}
|
|
|
|
addrs, err := exec.Command("ip", "-netns", ns, "address", "show", "dev", "lan0").Output()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
addrRe := regexp.MustCompile(`(?m)^\s*inet 192.168.42.1/24 brd 192.168.42.255 scope global lan0`)
|
|
if !addrRe.MatchString(string(addrs)) {
|
|
t.Fatalf("regexp %s does not match %s", addrRe, string(addrs))
|
|
}
|
|
|
|
bridgeLinks, err := exec.Command("ip", "-netns", ns, "link", "show", "master", "lan0").Output()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(string(bridgeLinks), ": eth0: ") {
|
|
t.Errorf("lan0 bridge does not contain eth0 interface")
|
|
}
|
|
})
|
|
}
|
|
|
|
func ipLines(args ...string) ([]string, error) {
|
|
cmd := exec.Command("ip", args...)
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%v: %v", cmd.Args, err)
|
|
}
|
|
outstr := string(out)
|
|
for strings.Contains(outstr, " ") {
|
|
outstr = strings.Replace(outstr, " ", " ", -1)
|
|
}
|
|
|
|
return strings.Split(strings.TrimSpace(outstr), "\n"), nil
|
|
}
|
|
|
|
func TestDHCPv4OldAddressDeconfigured(t *testing.T) {
|
|
if os.Getenv("HELPER_PROCESS") == "1" {
|
|
tmp, err := ioutil.TempDir("", "router7")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tmp)
|
|
|
|
for _, golden := range []struct {
|
|
filename, content string
|
|
}{
|
|
{"dhcp4/wire/lease.json", goldenDhcp4},
|
|
{"interfaces.json", goldenInterfaces},
|
|
} {
|
|
if err := os.MkdirAll(filepath.Join(tmp, filepath.Dir(golden.filename)), 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := ioutil.WriteFile(filepath.Join(tmp, golden.filename), []byte(golden.content), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
if err := os.MkdirAll(filepath.Join(tmp, "root", "etc"), 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := os.MkdirAll(filepath.Join(tmp, "root", "tmp"), 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := netconfig.Apply(tmp, filepath.Join(tmp, "root")); err != nil {
|
|
t.Fatalf("netconfig.Apply: %v", err)
|
|
}
|
|
|
|
const anotherDhcp4 = `
|
|
{
|
|
"valid_until":"2018-05-18T23:46:04.429895261+02:00",
|
|
"client_ip":"85.195.199.99",
|
|
"subnet_mask":"255.255.255.128",
|
|
"router":"85.195.199.1",
|
|
"dns":[
|
|
"77.109.128.2",
|
|
"213.144.129.20"
|
|
]
|
|
}
|
|
`
|
|
if err := ioutil.WriteFile(filepath.Join(tmp, "dhcp4/wire/lease.json"), []byte(anotherDhcp4), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := netconfig.Apply(tmp, filepath.Join(tmp, "root")); err != nil {
|
|
t.Fatalf("netconfig.Apply: %v", err)
|
|
}
|
|
|
|
return
|
|
}
|
|
const ns = "ns5" // name of the network namespace to use for this test
|
|
|
|
add := exec.Command("ip", "netns", "add", ns)
|
|
add.Stderr = os.Stderr
|
|
if err := add.Run(); err != nil {
|
|
t.Fatalf("%v: %v", add.Args, err)
|
|
}
|
|
defer exec.Command("ip", "netns", "delete", ns).Run()
|
|
|
|
nsSetup := []*exec.Cmd{
|
|
exec.Command("ip", "-netns", ns, "link", "add", "dummy0", "type", "dummy"),
|
|
exec.Command("ip", "-netns", ns, "link", "add", "lan0", "type", "dummy"),
|
|
exec.Command("ip", "-netns", ns, "link", "set", "dummy0", "address", "02:73:53:00:ca:fe"),
|
|
exec.Command("ip", "-netns", ns, "link", "set", "lan0", "address", "02:73:53:00:b0:0c"),
|
|
}
|
|
|
|
for _, cmd := range nsSetup {
|
|
if err := cmd.Run(); err != nil {
|
|
t.Fatalf("%v: %v", cmd.Args, err)
|
|
}
|
|
}
|
|
|
|
cmd := exec.Command("ip", "netns", "exec", ns, os.Args[0], "-test.run=^TestDHCPv4OldAddressDeconfigured$")
|
|
cmd.Env = append(os.Environ(), "HELPER_PROCESS=1")
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
if err := cmd.Run(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
t.Run("VerifyAddresses", func(t *testing.T) {
|
|
show := exec.Command("ip", "-netns", ns, "address", "show", "dev", "uplink0")
|
|
show.Stderr = os.Stderr
|
|
addrs, err := show.Output()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
oldAddrRe := regexp.MustCompile(`(?m)^\s*inet 85.195.207.62/25 brd 85.195.207.127 scope global uplink0$`)
|
|
if oldAddrRe.MatchString(string(addrs)) {
|
|
t.Fatalf("regexp %s unexpectedly still matches %s", oldAddrRe, string(addrs))
|
|
}
|
|
|
|
addrRe := regexp.MustCompile(`(?m)^\s*inet 85.195.199.99/25 brd 85.195.199.127 scope global uplink0$`)
|
|
if !addrRe.MatchString(string(addrs)) {
|
|
t.Fatalf("regexp %s does not match %s", addrRe, string(addrs))
|
|
}
|
|
|
|
wantRoutes := []string{
|
|
"default via 85.195.199.1 proto dhcp src 85.195.199.99 ",
|
|
"85.195.199.0/25 proto kernel scope link src 85.195.199.99 ",
|
|
"85.195.199.1 proto dhcp scope link src 85.195.199.99",
|
|
}
|
|
|
|
routes, err := ipLines("-netns", ns, "route", "show", "dev", "uplink0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if diff := cmp.Diff(wantRoutes, routes); diff != "" {
|
|
t.Fatalf("routes: diff (-want +got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|