Files
nadir-agent/internal/modules/networking/networking_test.go
T
2026-06-22 16:06:57 +02:00

397 lines
10 KiB
Go

package networking
import (
"reflect"
"strings"
"testing"
)
func TestParseInterfaces(t *testing.T) {
// Trimmed real output from `ip -j addr` on a typical host.
input := `[
{
"ifname": "lo",
"address": "00:00:00:00:00:00",
"mtu": 65536,
"operstate": "UNKNOWN",
"addr_info": [
{"family": "inet", "local": "127.0.0.1", "prefixlen": 8},
{"family": "inet6", "local": "::1", "prefixlen": 128}
]
},
{
"ifname": "eth0",
"address": "52:54:00:12:34:56",
"mtu": 1500,
"operstate": "UP",
"addr_info": [
{"family": "inet", "local": "192.168.1.10", "prefixlen": 24},
{"family": "inet6", "local": "fe80::1", "prefixlen": 64}
]
}
]`
ifaces, err := parseInterfaces(input)
if err != nil {
t.Fatal(err)
}
if len(ifaces) != 2 {
t.Fatalf("expected 2 interfaces, got %d", len(ifaces))
}
lo := ifaces[0]
if lo.Name != "lo" || lo.State != "unknown" || lo.MTU != 65536 {
t.Errorf("lo: got %+v", lo)
}
if len(lo.IPv4) != 1 || lo.IPv4[0] != "127.0.0.1/8" {
t.Errorf("lo ipv4: got %v", lo.IPv4)
}
eth0 := ifaces[1]
if eth0.Name != "eth0" || eth0.State != "up" || eth0.MAC != "52:54:00:12:34:56" {
t.Errorf("eth0: got %+v", eth0)
}
if len(eth0.IPv4) != 1 || eth0.IPv4[0] != "192.168.1.10/24" {
t.Errorf("eth0 ipv4: got %v", eth0.IPv4)
}
if len(eth0.IPv6) != 1 || eth0.IPv6[0] != "fe80::1/64" {
t.Errorf("eth0 ipv6: got %v", eth0.IPv6)
}
}
func TestParseRoutes(t *testing.T) {
input := `[
{"dst": "default", "gateway": "192.168.1.1", "dev": "eth0", "prefsrc": "192.168.1.10", "metric": 100},
{"dst": "192.168.1.0/24", "dev": "eth0", "prefsrc": "192.168.1.10"}
]`
routes, err := parseRoutes(input)
if err != nil {
t.Fatal(err)
}
if len(routes) != 2 {
t.Fatalf("expected 2 routes, got %d", len(routes))
}
if routes[0].Destination != "default" || routes[0].Gateway != "192.168.1.1" || routes[0].Metric != 100 {
t.Errorf("route 0: got %+v", routes[0])
}
if routes[1].Destination != "192.168.1.0/24" || routes[1].Interface != "eth0" {
t.Errorf("route 1: got %+v", routes[1])
}
}
func TestParseResolv(t *testing.T) {
input := `# Generated by NetworkManager
nameserver 1.1.1.1
nameserver 8.8.8.8
; legacy comment
search example.com
nameserver 9.9.9.9
`
servers := parseResolv(input)
want := []string{"1.1.1.1", "8.8.8.8", "9.9.9.9"}
if !reflect.DeepEqual(servers, want) {
t.Errorf("parseResolv() = %v, want %v", servers, want)
}
}
func TestParseResolvEmpty(t *testing.T) {
servers := parseResolv("")
if len(servers) != 0 {
t.Errorf("expected empty, got %v", servers)
}
}
func TestParseNmcliSnapshot(t *testing.T) {
input := `ipv4.method:manual
ipv4.addresses:192.168.1.10/24
ipv4.gateway:192.168.1.1
ipv4.dns:1.1.1.1,8.8.8.8
ipv4.routes:--`
cfg := parseNmcliSnapshot(input)
if cfg.Method != "static" {
t.Errorf("method: got %q, want static", cfg.Method)
}
if cfg.Address != "192.168.1.10" || cfg.Prefix != 24 {
t.Errorf("address: got %s/%d", cfg.Address, cfg.Prefix)
}
if cfg.Gateway != "192.168.1.1" {
t.Errorf("gateway: got %q", cfg.Gateway)
}
if len(cfg.DNS) != 2 || cfg.DNS[0] != "1.1.1.1" || cfg.DNS[1] != "8.8.8.8" {
t.Errorf("dns: got %v", cfg.DNS)
}
}
func TestParseNmcliRoutes(t *testing.T) {
input := `dst=10.0.0.0/24, nh=192.168.1.1; dst=172.16.0.0/12, nh=10.0.0.1`
routes := parseNmcliRoutes(input)
if len(routes) != 2 {
t.Fatalf("expected 2 routes, got %d", len(routes))
}
if routes[0].Destination != "10.0.0.0/24" || routes[0].Gateway != "192.168.1.1" {
t.Errorf("route 0: got %v", routes[0])
}
if routes[1].Destination != "172.16.0.0/12" || routes[1].Gateway != "10.0.0.1" {
t.Errorf("route 1: got %v", routes[1])
}
}
func TestParseNmcliSnapshotDHCP(t *testing.T) {
input := `ipv4.method:auto
ipv4.addresses:--
ipv4.gateway:--
ipv4.dns:--
ipv4.routes:--`
cfg := parseNmcliSnapshot(input)
if cfg.Method != "dhcp" {
t.Errorf("method: got %q, want dhcp", cfg.Method)
}
}
func TestParseNetworkdFile(t *testing.T) {
input := `# Managed by nadir
[Match]
Name=eth0
[Network]
DHCP=no
Address=10.0.0.5/24
Gateway=10.0.0.1
DNS=1.1.1.1
DNS=8.8.8.8
[Route]
Destination=192.168.0.0/16
Gateway=10.0.0.254
`
cfg := parseNetworkdFile(input)
if cfg.Method != "static" {
t.Errorf("method: got %q, want static", cfg.Method)
}
if cfg.Address != "10.0.0.5" || cfg.Prefix != 24 {
t.Errorf("address: got %s/%d", cfg.Address, cfg.Prefix)
}
if cfg.Gateway != "10.0.0.1" {
t.Errorf("gateway: got %q", cfg.Gateway)
}
if len(cfg.DNS) != 2 {
t.Errorf("dns: got %v", cfg.DNS)
}
if len(cfg.Routes) != 1 || cfg.Routes[0].Destination != "192.168.0.0/16" || cfg.Routes[0].Gateway != "10.0.0.254" {
t.Errorf("routes: got %v", cfg.Routes)
}
}
func TestParseNetworkdFileDHCP(t *testing.T) {
input := `[Match]
Name=eth0
[Network]
DHCP=yes
`
cfg := parseNetworkdFile(input)
if cfg.Method != "dhcp" {
t.Errorf("method: got %q, want dhcp", cfg.Method)
}
}
func TestParseIfupdownStanza(t *testing.T) {
input := `# Managed by nadir
auto eth0
iface eth0 inet static
address 192.168.1.10/24
gateway 192.168.1.1
dns-nameservers 1.1.1.1 8.8.8.8
up ip route add 10.0.0.0/24 via 192.168.1.254
down ip route del 10.0.0.0/24 via 192.168.1.254
`
cfg := parseIfupdownStanza(input)
if cfg.Method != "static" {
t.Errorf("method: got %q, want static", cfg.Method)
}
if cfg.Address != "192.168.1.10" || cfg.Prefix != 24 {
t.Errorf("address: got %s/%d", cfg.Address, cfg.Prefix)
}
if cfg.Gateway != "192.168.1.1" {
t.Errorf("gateway: got %q", cfg.Gateway)
}
if len(cfg.DNS) != 2 || cfg.DNS[0] != "1.1.1.1" || cfg.DNS[1] != "8.8.8.8" {
t.Errorf("dns: got %v", cfg.DNS)
}
if len(cfg.Routes) != 1 || cfg.Routes[0].Destination != "10.0.0.0/24" || cfg.Routes[0].Gateway != "192.168.1.254" {
t.Errorf("routes: got %v", cfg.Routes)
}
}
func TestParseIfupdownStanzaDHCP(t *testing.T) {
input := `auto eth0
iface eth0 inet dhcp
`
cfg := parseIfupdownStanza(input)
if cfg.Method != "dhcp" {
t.Errorf("method: got %q, want dhcp", cfg.Method)
}
}
func TestRenderNetworkdFile(t *testing.T) {
cfg := IfaceConfig{
Method: "static",
Address: "10.0.0.5",
Prefix: 24,
Gateway: "10.0.0.1",
DNS: []string{"1.1.1.1"},
Routes: []Route{{Destination: "192.168.0.0/16", Gateway: "10.0.0.254"}},
}
out := renderNetworkdFile("eth0", cfg)
mustContain := []string{
"Name=eth0",
"DHCP=no",
"Address=10.0.0.5/24",
"Gateway=10.0.0.1",
"DNS=1.1.1.1",
"Destination=192.168.0.0/16",
}
for _, s := range mustContain {
if !strings.Contains(out, s) {
t.Errorf("renderNetworkdFile missing %q in:\n%s", s, out)
}
}
// Roundtrip test
parsed := parseNetworkdFile(out)
if !reflect.DeepEqual(parsed, cfg) {
t.Errorf("roundtrip failed: got %+v, want %+v", parsed, cfg)
}
}
func TestRenderIfupdownStanza(t *testing.T) {
cfg := IfaceConfig{
Method: "static",
Address: "192.168.1.10",
Prefix: 24,
Gateway: "192.168.1.1",
DNS: []string{"1.1.1.1", "8.8.8.8"},
Routes: []Route{{Destination: "10.0.0.0/24", Gateway: "192.168.1.254"}},
}
out := renderIfupdownStanza("eth0", cfg)
mustContain := []string{
"iface eth0 inet static",
"address 192.168.1.10/24",
"gateway 192.168.1.1",
"dns-nameservers 1.1.1.1 8.8.8.8",
"up ip route add 10.0.0.0/24 via 192.168.1.254",
"down ip route del 10.0.0.0/24 via 192.168.1.254",
}
for _, s := range mustContain {
if !strings.Contains(out, s) {
t.Errorf("renderIfupdownStanza missing %q in:\n%s", s, out)
}
}
// Roundtrip test
parsed := parseIfupdownStanza(out)
if !reflect.DeepEqual(parsed, cfg) {
t.Errorf("roundtrip failed: got %+v, want %+v", parsed, cfg)
}
}
func TestValidateIPv6(t *testing.T) {
ok := IfaceConfig{Method: "dhcp", IPv6: &IPv6Config{Method: "static", Address: "2001:db8::1", Prefix: 64, Gateway: "2001:db8::ff"}}
if err := ok.validate(); err != nil {
t.Errorf("valid ipv6 rejected: %v", err)
}
bad := []IfaceConfig{
{Method: "dhcp", IPv6: &IPv6Config{Method: "static", Address: "1.2.3.4", Prefix: 64}}, // v4 in v6 block
{Method: "dhcp", IPv6: &IPv6Config{Method: "static", Address: "2001:db8::1", Prefix: 0}},
{Method: "dhcp", IPv6: &IPv6Config{Method: "bogus"}},
}
for i, c := range bad {
if err := c.validate(); err == nil {
t.Errorf("bad ipv6 case %d accepted", i)
}
}
}
func TestParseNmcliSnapshotIPv6(t *testing.T) {
input := `ipv4.method:manual
ipv4.addresses:192.168.1.10/24
ipv6.method:manual
ipv6.addresses:2001:db8::10/64
ipv6.gateway:2001:db8::1`
cfg := parseNmcliSnapshot(input)
if cfg.IPv6 == nil {
t.Fatal("ipv6 not captured")
}
if cfg.IPv6.Method != "static" || cfg.IPv6.Address != "2001:db8::10" || cfg.IPv6.Prefix != 64 || cfg.IPv6.Gateway != "2001:db8::1" {
t.Errorf("ipv6 snapshot: %+v", cfg.IPv6)
}
}
func TestNetworkdIPv6RoundTrip(t *testing.T) {
cfg := IfaceConfig{
Method: "static",
Address: "10.0.0.5",
Prefix: 24,
IPv6: &IPv6Config{Method: "static", Address: "2001:db8::5", Prefix: 64, Gateway: "2001:db8::1"},
}
got := parseNetworkdFile(renderNetworkdFile("eth0", cfg))
if got.Address != "10.0.0.5" || got.Prefix != 24 {
t.Errorf("ipv4 lost in roundtrip: %+v", got)
}
if !reflect.DeepEqual(got.IPv6, cfg.IPv6) {
t.Errorf("ipv6 roundtrip: got %+v want %+v", got.IPv6, cfg.IPv6)
}
}
func TestIfupdownIPv6RoundTrip(t *testing.T) {
cfg := IfaceConfig{
Method: "static",
Address: "192.168.1.10",
Prefix: 24,
IPv6: &IPv6Config{Method: "static", Address: "2001:db8::10", Prefix: 64, Gateway: "2001:db8::1"},
}
got := parseIfupdownStanza(renderIfupdownStanza("eth0", cfg))
if got.Address != "192.168.1.10" || got.Prefix != 24 || got.Method != "static" {
t.Errorf("ipv4 lost in roundtrip: %+v", got)
}
if !reflect.DeepEqual(got.IPv6, cfg.IPv6) {
t.Errorf("ipv6 roundtrip: got %+v want %+v", got.IPv6, cfg.IPv6)
}
}
func TestNetworkdIPv6AutoIgnore(t *testing.T) {
auto := parseNetworkdFile(renderNetworkdFile("eth0", IfaceConfig{Method: "dhcp", IPv6: &IPv6Config{Method: "auto"}}))
if auto.IPv6 == nil || auto.IPv6.Method != "auto" {
t.Errorf("auto roundtrip: %+v", auto.IPv6)
}
ign := parseNetworkdFile(renderNetworkdFile("eth0", IfaceConfig{Method: "dhcp", IPv6: &IPv6Config{Method: "ignore"}}))
if ign.IPv6 == nil || ign.IPv6.Method != "ignore" {
t.Errorf("ignore roundtrip: %+v", ign.IPv6)
}
}
func TestSplitCIDR(t *testing.T) {
tests := []struct {
input string
wantAddr string
wantPrefix int
}{
{"192.168.1.10/24", "192.168.1.10", 24},
{"10.0.0.1/8", "10.0.0.1", 8},
{"10.0.0.1", "10.0.0.1", 0},
}
for _, tt := range tests {
addr, prefix := splitCIDR(tt.input)
if addr != tt.wantAddr || prefix != tt.wantPrefix {
t.Errorf("splitCIDR(%q) = (%q, %d), want (%q, %d)", tt.input, addr, prefix, tt.wantAddr, tt.wantPrefix)
}
}
}