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) } } }