318d7169f7
Replace fragile text-based parsing of ● 1: lo
Link File: n/a
Network File: n/a
State: carrier (unmanaged)
Online state: unknown
Type: loopback
Hardware Address: 00:00:00:00:00:00
MTU: 65536
QDisc: noqueue
IPv6 Address Generation Mode: eui64
Number of Queues (Tx/Rx): 1/1
Address: 127.0.0.1
::1
May 30 22:43:27 vacuum-wall systemd-networkd[315]: lo: Link UP
May 30 22:43:27 vacuum-wall systemd-networkd[315]: lo: Gained carrier
● 77: eth0
Link File: /usr/lib/systemd/network/99-default.link
Network File: /etc/systemd/network/eth0.network
State: routable (configured)
Online state: online
Type: ether
Kind: veth
Driver: veth
Hardware Address: 8e:63:52:6b:ea:e8
MTU: 1500 (min: 68, max: 65535)
QDisc: noqueue
IPv6 Address Generation Mode: eui64
Number of Queues (Tx/Rx): 8/8
Auto negotiation: no
Speed: 10Gbps
Duplex: full
Port: tp
Address: 192.168.1.5 (DHCPv4 via 192.168.1.1)
2600:4040:a6c1:4a00:8c63:52ff:fe6b:eae8
fe80::8c63:52ff:fe6b:eae8
Gateway: 192.168.1.1
fe80::3ebd:c5ff:fe2b:bd99
DNS: 192.168.1.1
2600:4040:a6c1:4a00::1
Search Domains: myfiosgateway.com
Activation Policy: up
Required For Online: yes
DHCPv4 Client ID: 8e:63:52:6b:ea:e8
DHCPv6 Client IAID: 0xf3d61521
DHCPv6 Client DUID: DUID-EN/Vendor:0000ab11b94215a519e8ca54
May 30 22:43:27 vacuum-wall systemd-networkd[315]: eth0: Link UP
May 30 22:43:27 vacuum-wall systemd-networkd[315]: eth0: Gained carrier
May 30 22:43:27 vacuum-wall systemd-networkd[315]: eth0: Configuring with /etc/systemd/network/eth0.network.
May 30 22:43:27 vacuum-wall systemd-networkd[315]: eth0: Gained IPv6LL
May 30 22:43:27 vacuum-wall systemd-networkd[315]: eth0: DHCPv4 address 192.168.1.5/24, gateway 192.168.1.1 acquired from 192.168.1.1
● 79: eth1
Link File: /usr/lib/systemd/network/99-default.link
Network File: /etc/systemd/network/50-eth1.network
State: routable (configured)
Online state: online
Type: ether
Kind: veth
Driver: veth
Hardware Address: 8e:63:52:6b:ea:8e
MTU: 1500 (min: 68, max: 65535)
QDisc: noqueue
IPv6 Address Generation Mode: eui64
Number of Queues (Tx/Rx): 8/8
Auto negotiation: no
Speed: 10Gbps
Duplex: full
Port: tp
Address: 10.4.20.1
fd42:a304:c836:2a7f:8c63:52ff:fe6b:ea8e
fe80::8c63:52ff:fe6b:ea8e
Gateway: fe80::1266:6aff:fe76:bc5b
DNS: fd42:a304:c836:2a7f::1
Activation Policy: up
Required For Online: yes
DHCPv6 Client IAID: 0x1da7c7a5
DHCPv6 Client DUID: DUID-EN/Vendor:0000ab11b94215a519e8ca54
Jun 01 03:48:43 vacuum-wall systemd-networkd[315]: eth1: Reconfiguring with /etc/systemd/network/50-eth1.network.
Jun 01 04:05:47 vacuum-wall systemd-networkd[315]: eth1: Reconfiguring with /etc/systemd/network/99-eth1.network.
Jun 01 04:05:47 vacuum-wall systemd-networkd[315]: eth1: DHCPv6 lease lost
Jun 01 04:05:47 vacuum-wall systemd-networkd[315]: eth1: Reconfiguring with /etc/systemd/network/99-eth1.network.
Jun 01 04:06:51 vacuum-wall systemd-networkd[315]: eth1: Reconfiguring with /etc/systemd/network/99-eth1.network.
Jun 01 04:06:51 vacuum-wall systemd-networkd[315]: eth1: DHCPv6 lease lost
Jun 01 04:06:51 vacuum-wall systemd-networkd[315]: eth1: Reconfiguring with /etc/systemd/network/99-eth1.network.
Jun 01 04:09:35 vacuum-wall systemd-networkd[315]: eth1: Reconfiguring with /etc/systemd/network/50-eth1.network.
Jun 01 04:09:35 vacuum-wall systemd-networkd[315]: eth1: DHCPv6 lease lost
Jun 01 04:09:35 vacuum-wall systemd-networkd[315]: eth1: Reconfiguring with /etc/systemd/network/50-eth1.network. with structured JSON parsing using {"Interfaces":[{"Index":1,"Name":"lo","Type":"loopback","Flags":65609,"FlagsString":"up,loopback,running,lower-up","KernelOperationalState":0,"KernelOperationalStateString":"unknown","MTU":65536,"MinimumMTU":0,"MaximumMTU":4294967295,"AdministrativeState":"unmanaged","OperationalState":"carrier","CarrierState":"carrier","AddressState":"off","IPv4AddressState":"off","IPv6AddressState":"off","OnlineState":null,"Addresses":[{"Family":2,"Address":[127,0,0,1],"PrefixLength":8,"ConfigSource":"foreign","Scope":254,"ScopeString":"host","Flags":128,"FlagsString":"permanent","ConfigState":"configured"},{"Family":10,"Address":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],"PrefixLength":128,"ConfigSource":"foreign","Scope":254,"ScopeString":"host","Flags":128,"FlagsString":"permanent","ConfigState":"configured"}],"Routes":[{"Family":10,"Destination":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],"DestinationPrefixLength":128,"TOS":0,"Scope":0,"Protocol":2,"Type":2,"Priority":0,"Table":255,"Flags":0,"ConfigSource":"foreign","ScopeString":"global","ProtocolString":"kernel","TypeString":"local","TableString":"local","Preference":0,"FlagsString":"","ConfigState":"configured"},{"Family":2,"Destination":[127,255,255,255],"DestinationPrefixLength":32,"PreferredSource":[127,0,0,1],"TOS":0,"Scope":253,"Protocol":2,"Type":3,"Priority":0,"Table":255,"Flags":0,"ConfigSource":"foreign","ScopeString":"link","ProtocolString":"kernel","TypeString":"broadcast","TableString":"local","Preference":0,"FlagsString":"","ConfigState":"configured"},{"Family":2,"Destination":[127,0,0,1],"DestinationPrefixLength":32,"PreferredSource":[127,0,0,1],"TOS":0,"Scope":254,"Protocol":2,"Type":2,"Priority":0,"Table":255,"Flags":0,"ConfigSource":"foreign","ScopeString":"host","ProtocolString":"kernel","TypeString":"local","TableString":"local","Preference":0,"FlagsString":"","ConfigState":"configured"},{"Family":2,"Destination":[127,0,0,0],"DestinationPrefixLength":8,"PreferredSource":[127,0,0,1],"TOS":0,"Scope":254,"Protocol":2,"Type":2,"Priority":0,"Table":255,"Flags":0,"ConfigSource":"foreign","ScopeString":"host","ProtocolString":"kernel","TypeString":"local","TableString":"local","Preference":0,"FlagsString":"","ConfigState":"configured"}]},{"Index":77,"Name":"eth0","Kind":"veth","Type":"ether","Driver":"veth","Flags":69699,"FlagsString":"up,broadcast,running,multicast,lower-up","KernelOperationalState":6,"KernelOperationalStateString":"up","MTU":1500,"MinimumMTU":68,"MaximumMTU":65535,"HardwareAddress":[142,99,82,107,234,232],"BroadcastAddress":[255,255,255,255,255,255],"IPv6LinkLocalAddress":[254,128,0,0,0,0,0,0,140,99,82,255,254,107,234,232],"AdministrativeState":"configured","OperationalState":"routable","CarrierState":"carrier","AddressState":"routable","IPv4AddressState":"routable","IPv6AddressState":"routable","OnlineState":"online","NetworkFile":"/etc/systemd/network/eth0.network","NetworkFileDropins":[],"RequiredForOnline":true,"RequiredOperationalStateForOnline":[null,null],"RequiredFamilyForOnline":"any","ActivationPolicy":"up","DNS":[{"Family":2,"Address":[192,168,1,1],"ConfigSource":"DHCPv4","ConfigProvider":[192,168,1,1]},{"Family":10,"Address":[38,0,64,64,166,193,74,0,0,0,0,0,0,0,0,1],"ConfigSource":"NDisc","ConfigProvider":[254,128,0,0,0,0,0,0,62,189,197,255,254,43,189,153]}],"SearchDomains":[{"Domain":"myfiosgateway.com","ConfigSource":"DHCPv4","ConfigProvider":[192,168,1,1]}],"DNSSettings":[{"LLMNR":"yes","ConfigSource":"static"},{"MDNS":"no","ConfigSource":"static"}],"Addresses":[{"Family":2,"Address":[192,168,1,5],"PrefixLength":24,"ConfigSource":"DHCPv4","ConfigProvider":[192,168,1,1],"Broadcast":[192,168,1,255],"Scope":0,"ScopeString":"global","Flags":0,"FlagsString":null,"PreferredLifetimeUSec":1734964293205,"PreferredLifetimeUsec":1734964293205,"ValidLifetimeUSec":1734964293205,"ValidLifetimeUsec":1734964293205,"ConfigState":"configured"},{"Family":10,"Address":[254,128,0,0,0,0,0,0,140,99,82,255,254,107,234,232],"PrefixLength":64,"ConfigSource":"foreign","Scope":253,"ScopeString":"link","Flags":128,"FlagsString":"permanent","ConfigState":"configured"},{"Family":10,"Address":[38,0,64,64,166,193,74,0,140,99,82,255,254,107,234,232],"PrefixLength":64,"ConfigSource":"NDisc","ConfigProvider":[254,128,0,0,0,0,0,0,62,189,197,255,254,43,189,153],"Scope":0,"ScopeString":"global","Flags":768,"FlagsString":"manage-temporary-address,no-prefixroute","PreferredLifetimeUSec":1677495569859,"PreferredLifetimeUsec":1677495569859,"ValidLifetimeUSec":1677495569859,"ValidLifetimeUsec":1677495569859,"ConfigState":"configured"}],"NextHops":[{"ID":1635324079,"Family":10,"ConfigSource":"NDisc","ConfigProvider":[254,128,0,0,0,0,0,0,62,189,197,255,254,43,189,153],"Gateway":[254,128,0,0,0,0,0,0,62,189,197,255,254,43,189,153],"Flags":0,"FlagsString":"","Protocol":9,"ProtocolString":"9","Blackhole":false,"ConfigState":"configured"}],"Routes":[{"Family":2,"Destination":[192,168,1,0],"DestinationPrefixLength":24,"PreferredSource":[192,168,1,5],"TOS":0,"Scope":253,"Protocol":2,"Type":1,"Priority":1024,"Table":254,"Flags":0,"ConfigSource":"foreign","ScopeString":"link","ProtocolString":"kernel","TypeString":"unicast","TableString":"main","Preference":0,"FlagsString":"","ConfigState":"configured"},{"Family":2,"Destination":[192,168,1,255],"DestinationPrefixLength":32,"PreferredSource":[192,168,1,5],"TOS":0,"Scope":253,"Protocol":2,"Type":3,"Priority":0,"Table":255,"Flags":0,"ConfigSource":"foreign","ScopeString":"link","ProtocolString":"kernel","TypeString":"broadcast","TableString":"local","Preference":0,"FlagsString":"","ConfigState":"configured"},{"Family":10,"Destination":[38,0,64,64,166,193,74,0,140,99,82,255,254,107,234,232],"DestinationPrefixLength":128,"TOS":0,"Scope":0,"Protocol":2,"Type":2,"Priority":0,"Table":255,"Flags":0,"ConfigSource":"foreign","ScopeString":"global","ProtocolString":"kernel","TypeString":"local","TableString":"local","Preference":0,"FlagsString":"","ConfigState":"configured"},{"Family":10,"Destination":[38,0,64,64,166,193,74,0,0,0,0,0,0,0,0,0],"DestinationPrefixLength":64,"TOS":0,"Scope":0,"Protocol":9,"Type":1,"Priority":1024,"Table":254,"Flags":0,"ConfigSource":"NDisc","ConfigProvider":[254,128,0,0,0,0,0,0,62,189,197,255,254,43,189,153],"ScopeString":"global","ProtocolString":"9","TypeString":"unicast","TableString":"main","Preference":0,"FlagsString":"","LifetimeUSec":1677495568830,"ConfigState":"configured"},{"Family":10,"Destination":[254,128,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"DestinationPrefixLength":64,"TOS":0,"Scope":0,"Protocol":2,"Type":1,"Priority":256,"Table":254,"Flags":0,"ConfigSource":"foreign","ScopeString":"global","ProtocolString":"kernel","TypeString":"unicast","TableString":"main","Preference":0,"FlagsString":"","ConfigState":"configured"},{"Family":2,"Destination":[0,0,0,0],"DestinationPrefixLength":0,"Gateway":[192,168,1,1],"PreferredSource":[192,168,1,5],"TOS":0,"Scope":0,"Protocol":16,"Type":1,"Priority":1024,"Table":254,"Flags":0,"ConfigSource":"DHCPv4","ConfigProvider":[192,168,1,1],"ScopeString":"global","ProtocolString":"16","TypeString":"unicast","TableString":"main","Preference":0,"FlagsString":"","ConfigState":"configured"},{"Family":2,"Destination":[192,168,1,1],"DestinationPrefixLength":32,"PreferredSource":[192,168,1,5],"TOS":0,"Scope":253,"Protocol":16,"Type":1,"Priority":1024,"Table":254,"Flags":0,"ConfigSource":"DHCPv4","ConfigProvider":[192,168,1,1],"ScopeString":"link","ProtocolString":"16","TypeString":"unicast","TableString":"main","Preference":0,"FlagsString":"","ConfigState":"configured"},{"Family":2,"Destination":[192,168,1,5],"DestinationPrefixLength":32,"PreferredSource":[192,168,1,5],"TOS":0,"Scope":254,"Protocol":2,"Type":2,"Priority":0,"Table":255,"Flags":0,"ConfigSource":"foreign","ScopeString":"host","ProtocolString":"kernel","TypeString":"local","TableString":"local","Preference":0,"FlagsString":"","ConfigState":"configured"},{"Family":10,"Destination":[255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"DestinationPrefixLength":8,"TOS":0,"Scope":0,"Protocol":2,"Type":5,"Priority":256,"Table":255,"Flags":0,"ConfigSource":"foreign","ScopeString":"global","ProtocolString":"kernel","TypeString":"multicast","TableString":"local","Preference":0,"FlagsString":"","ConfigState":"configured"},{"Family":10,"Destination":[254,128,0,0,0,0,0,0,140,99,82,255,254,107,234,232],"DestinationPrefixLength":128,"TOS":0,"Scope":0,"Protocol":2,"Type":2,"Priority":0,"Table":255,"Flags":0,"ConfigSource":"foreign","ScopeString":"global","ProtocolString":"kernel","TypeString":"local","TableString":"local","Preference":0,"FlagsString":"","ConfigState":"configured"}],"DHCPv4Client":{"Lease":{"LeaseTimestampUSec":1648564292241,"Timeout1USec":1691764292241,"Timeout2USec":1724164292241},"ClientIdentifier":[1,142,99,82,107,234,232]},"DHCPv6Client":{"Lease":{"LeaseTimestampUSec":958440922609},"DUID":[0,2,0,0,171,17,185,66,21,165,25,232,202,84]}},{"Index":79,"Name":"eth1","Kind":"veth","Type":"ether","Driver":"veth","Flags":69699,"FlagsString":"up,broadcast,running,multicast,lower-up","KernelOperationalState":6,"KernelOperationalStateString":"up","MTU":1500,"MinimumMTU":68,"MaximumMTU":65535,"HardwareAddress":[142,99,82,107,234,142],"BroadcastAddress":[255,255,255,255,255,255],"IPv6LinkLocalAddress":[254,128,0,0,0,0,0,0,140,99,82,255,254,107,234,142],"AdministrativeState":"configured","OperationalState":"routable","CarrierState":"carrier","AddressState":"routable","IPv4AddressState":"routable","IPv6AddressState":"routable","OnlineState":"online","NetworkFile":"/etc/systemd/network/50-eth1.network","NetworkFileDropins":[],"RequiredForOnline":true,"RequiredOperationalStateForOnline":[null,null],"RequiredFamilyForOnline":"any","ActivationPolicy":"up","DNS":[{"Family":10,"Address":[253,66,163,4,200,54,42,127,0,0,0,0,0,0,0,1],"ConfigSource":"DHCPv6","ConfigProvider":[254,128,0,0,0,0,0,0,18,102,106,255,254,118,188,91]},{"Family":10,"Address":[253,66,163,4,200,54,42,127,0,0,0,0,0,0,0,1],"ConfigSource":"NDisc","ConfigProvider":[254,128,0,0,0,0,0,0,18,102,106,255,254,118,188,91]}],"DNSSettings":[{"LLMNR":"yes","ConfigSource":"static"},{"MDNS":"no","ConfigSource":"static"}],"Addresses":[{"Family":10,"Address":[254,128,0,0,0,0,0,0,140,99,82,255,254,107,234,142],"PrefixLength":64,"ConfigSource":"foreign","Scope":253,"ScopeString":"link","Flags":128,"FlagsString":"permanent","ConfigState":"configured"},{"Family":2,"Address":[10,4,20,1],"PrefixLength":24,"ConfigSource":"static","Broadcast":[10,4,20,255],"Scope":0,"ScopeString":"global","Flags":128,"FlagsString":"permanent","ConfigState":"configured"},{"Family":10,"Address":[253,66,163,4,200,54,42,127,140,99,82,255,254,107,234,142],"PrefixLength":64,"ConfigSource":"NDisc","ConfigProvider":[254,128,0,0,0,0,0,0,18,102,106,255,254,118,188,91],"Scope":0,"ScopeString":"global","Flags":896,"FlagsString":"permanent,manage-temporary-address,no-prefixroute","ConfigState":"configured"}],"NextHops":[{"ID":3144860678,"Family":10,"ConfigSource":"NDisc","ConfigProvider":[254,128,0,0,0,0,0,0,18,102,106,255,254,118,188,91],"Gateway":[254,128,0,0,0,0,0,0,18,102,106,255,254,118,188,91],"Flags":0,"FlagsString":"","Protocol":9,"ProtocolString":"9","Blackhole":false,"ConfigState":"configured"}],"Routes":[{"Family":2,"Destination":[10,4,20,255],"DestinationPrefixLength":32,"PreferredSource":[10,4,20,1],"TOS":0,"Scope":253,"Protocol":2,"Type":3,"Priority":0,"Table":255,"Flags":0,"ConfigSource":"foreign","ScopeString":"link","ProtocolString":"kernel","TypeString":"broadcast","TableString":"local","Preference":0,"FlagsString":"","ConfigState":"configured"},{"Family":10,"Destination":[254,128,0,0,0,0,0,0,140,99,82,255,254,107,234,142],"DestinationPrefixLength":128,"TOS":0,"Scope":0,"Protocol":2,"Type":2,"Priority":0,"Table":255,"Flags":0,"ConfigSource":"foreign","ScopeString":"global","ProtocolString":"kernel","TypeString":"local","TableString":"local","Preference":0,"FlagsString":"","ConfigState":"configured"},{"Family":10,"Destination":[253,66,163,4,200,54,42,127,0,0,0,0,0,0,0,0],"DestinationPrefixLength":64,"TOS":0,"Scope":0,"Protocol":9,"Type":1,"Priority":1024,"Table":254,"Flags":0,"ConfigSource":"NDisc","ConfigProvider":[254,128,0,0,0,0,0,0,18,102,106,255,254,118,188,91],"ScopeString":"global","ProtocolString":"9","TypeString":"unicast","TableString":"main","Preference":0,"FlagsString":"","ConfigState":"configured"},{"Family":10,"Destination":[253,66,163,4,200,54,42,127,140,99,82,255,254,107,234,142],"DestinationPrefixLength":128,"TOS":0,"Scope":0,"Protocol":2,"Type":2,"Priority":0,"Table":255,"Flags":0,"ConfigSource":"foreign","ScopeString":"global","ProtocolString":"kernel","TypeString":"local","TableString":"local","Preference":0,"FlagsString":"","ConfigState":"configured"},{"Family":2,"Destination":[10,4,20,1],"DestinationPrefixLength":32,"PreferredSource":[10,4,20,1],"TOS":0,"Scope":254,"Protocol":2,"Type":2,"Priority":0,"Table":255,"Flags":0,"ConfigSource":"foreign","ScopeString":"host","ProtocolString":"kernel","TypeString":"local","TableString":"local","Preference":0,"FlagsString":"","ConfigState":"configured"},{"Family":2,"Destination":[10,4,20,0],"DestinationPrefixLength":24,"PreferredSource":[10,4,20,1],"TOS":0,"Scope":253,"Protocol":2,"Type":1,"Priority":0,"Table":254,"Flags":0,"ConfigSource":"foreign","ScopeString":"link","ProtocolString":"kernel","TypeString":"unicast","TableString":"main","Preference":0,"FlagsString":"","ConfigState":"configured"}],"DHCPv6Client":{"Lease":{"LeaseTimestampUSec":1585104388996},"DUID":[0,2,0,0,171,17,185,66,21,165,25,232,202,84]}}],"Routes":[{"Family":10,"Destination":[38,0,64,64,166,193,74,0,0,0,0,0,0,0,0,0],"DestinationPrefixLength":56,"TOS":0,"Scope":0,"Protocol":9,"Type":1,"Priority":512,"Table":254,"Flags":0,"NextHopID":1635324079,"ConfigSource":"NDisc","ConfigProvider":[254,128,0,0,0,0,0,0,62,189,197,255,254,43,189,153],"ScopeString":"global","ProtocolString":"9","TypeString":"unicast","TableString":"main","Preference":1,"FlagsString":"","LifetimeUSec":1677495568830,"ConfigState":"configured"},{"Family":10,"Destination":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"DestinationPrefixLength":0,"TOS":0,"Scope":0,"Protocol":9,"Type":1,"Priority":1024,"Table":254,"Flags":0,"NextHopID":3144860678,"ConfigSource":"NDisc","ConfigProvider":[254,128,0,0,0,0,0,0,18,102,106,255,254,118,188,91],"ScopeString":"global","ProtocolString":"9","TypeString":"unicast","TableString":"main","Preference":0,"FlagsString":"","LifetimeUSec":1671685709586,"ConfigState":"configured"},{"Family":10,"Destination":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"DestinationPrefixLength":0,"TOS":0,"Scope":0,"Protocol":9,"Type":1,"Priority":1024,"Table":254,"Flags":0,"NextHopID":1635324079,"ConfigSource":"NDisc","ConfigProvider":[254,128,0,0,0,0,0,0,62,189,197,255,254,43,189,153],"ScopeString":"global","ProtocolString":"9","TypeString":"unicast","TableString":"main","Preference":0,"FlagsString":"","LifetimeUSec":1671195568830,"ConfigState":"configured"}],"RoutingPolicyRules":[{"Family":10,"Protocol":2,"ProtocolString":"kernel","TOS":0,"Type":1,"TypeString":"table","IPProtocol":0,"IPProtocolString":"ip","Priority":0,"FirewallMark":0,"FirewallMask":0,"Table":255,"TableString":"local","Invert":false,"ConfigSource":"foreign","ConfigState":"configured"},{"Family":10,"Protocol":2,"ProtocolString":"kernel","TOS":0,"Type":1,"TypeString":"table","IPProtocol":0,"IPProtocolString":"ip","Priority":32766,"FirewallMark":0,"FirewallMask":0,"Table":254,"TableString":"main","Invert":false,"ConfigSource":"foreign","ConfigState":"configured"},{"Family":2,"Protocol":2,"ProtocolString":"kernel","TOS":0,"Type":1,"TypeString":"table","IPProtocol":0,"IPProtocolString":"ip","Priority":32767,"FirewallMark":0,"FirewallMask":0,"Table":253,"TableString":"default","Invert":false,"ConfigSource":"foreign","ConfigState":"configured"},{"Family":2,"Protocol":2,"ProtocolString":"kernel","TOS":0,"Type":1,"TypeString":"table","IPProtocol":0,"IPProtocolString":"ip","Priority":0,"FirewallMark":0,"FirewallMask":0,"Table":255,"TableString":"local","Invert":false,"ConfigSource":"foreign","ConfigState":"configured"},{"Family":2,"Protocol":2,"ProtocolString":"kernel","TOS":0,"Type":1,"TypeString":"table","IPProtocol":0,"IPProtocolString":"ip","Priority":32766,"FirewallMark":0,"FirewallMask":0,"Table":254,"TableString":"main","Invert":false,"ConfigSource":"foreign","ConfigState":"configured"}]}. This provides more reliable and maintainable runtime state extraction.
lib/network.py:
- Rewrite parse_networkctl_status() to parse JSON instead of text lines
- Add _bytes_to_ip() helper for converting address byte arrays to IP strings
- Extract addresses, gateway (from Routes), DNS, MAC, and state from structured JSON
- Add proper error handling for malformed JSON input
daemon/handlers/network.py:
- Update all 3 callers (get_interfaces, get_interface, save_interface) to use --json=short
- Fix get_interfaces to include runtime-only interfaces by unioning config and runtime names (minus lo), rather than only iterating config-defined interfaces
lib/state.py:
- Update _collect_networkd to use --json=short flag
tests/test_network.py, tests/test_network_integration.py:
- Update all test fixtures from text output to matching JSON structure
1007 lines
34 KiB
Python
1007 lines
34 KiB
Python
"""Tests for lib.network module — networkd config, rendering, and parsing."""
|
|
|
|
import json
|
|
|
|
import pytest
|
|
|
|
from lib import network as _net
|
|
|
|
|
|
@pytest.fixture
|
|
def tmp_network(tmp_path):
|
|
orig_config = _net.CONFIG_FILE
|
|
orig_data = _net.DATA_DIR
|
|
_net.CONFIG_FILE = tmp_path / "config" / "network" / "config.json"
|
|
_net.DATA_DIR = tmp_path / "data" / "networkd"
|
|
yield tmp_path
|
|
_net.CONFIG_FILE = orig_config
|
|
_net.DATA_DIR = orig_data
|
|
|
|
|
|
# =================================================================
|
|
# get_config / save_config
|
|
# =================================================================
|
|
|
|
|
|
class TestGetConfig:
|
|
def test_returns_default_when_missing(self, tmp_network):
|
|
cfg = _net.get_config()
|
|
assert isinstance(cfg, dict)
|
|
assert "interfaces" in cfg
|
|
|
|
def test_creates_config_file(self, tmp_network):
|
|
cfg = _net.get_config()
|
|
assert _net.CONFIG_FILE.exists()
|
|
assert cfg["interfaces"] == {}
|
|
|
|
|
|
class TestSaveConfig:
|
|
def test_save_and_read(self, tmp_network):
|
|
cfg = {"interfaces": {"eth0": {"addresses": ["10.0.0.1/24"]}}}
|
|
_net.save_config(cfg)
|
|
loaded = _net.get_config()
|
|
assert loaded["interfaces"]["eth0"]["addresses"] == ["10.0.0.1/24"]
|
|
|
|
|
|
# =================================================================
|
|
# render_network_file
|
|
# =================================================================
|
|
|
|
|
|
class TestRenderNetworkFile:
|
|
def test_minimal(self):
|
|
content = _net.render_network_file("eth0", {})
|
|
assert "[Match]" in content
|
|
assert "Name=eth0" in content
|
|
assert "[Network]" in content
|
|
|
|
def test_with_addresses_bare_strings(self):
|
|
"""Bare string addresses (legacy compat)."""
|
|
entry = {"addresses": ["192.168.1.1/24", "192.168.2.1/24"]}
|
|
content = _net.render_network_file("eth0", entry)
|
|
assert "Address=192.168.1.1/24" in content
|
|
assert "Address=192.168.2.1/24" in content
|
|
|
|
def test_with_addresses_as_dicts(self):
|
|
"""Dict addresses with label, scope, etc."""
|
|
entry = {
|
|
"addresses": [
|
|
{"address": "10.0.0.1/24", "label": "eth0:0"},
|
|
{"address": "10.0.0.2/24", "scope": "host"},
|
|
]
|
|
}
|
|
content = _net.render_network_file("eth0", entry)
|
|
assert "[Address]" in content
|
|
assert "[Address#1]" in content
|
|
assert "Address=10.0.0.1/24" in content
|
|
assert "Label=eth0:0" in content
|
|
assert "Address=10.0.0.2/24" in content
|
|
assert "Scope=host" in content
|
|
|
|
def test_with_gateway(self):
|
|
content = _net.render_network_file("eth0", {"gateway": "192.168.1.254"})
|
|
assert "Gateway=192.168.1.254" in content
|
|
|
|
def test_with_ipv6_gateway(self):
|
|
content = _net.render_network_file("eth0", {"ipv6_gateway": "fe80::1"})
|
|
assert "IPv6Gateway=fe80::1" in content
|
|
|
|
def test_with_dns(self):
|
|
content = _net.render_network_file("eth0", {"dns": ["8.8.8.8", "8.8.4.4"]})
|
|
assert "DNS=8.8.8.8" in content
|
|
assert "DNS=8.8.4.4" in content
|
|
|
|
def test_with_ipv6_dns(self):
|
|
content = _net.render_network_file(
|
|
"eth0", {"ipv6_dns": ["2001:4860:4860::8888"]}
|
|
)
|
|
assert "IPv6DNS=2001:4860:4860::8888" in content
|
|
|
|
def test_with_domains(self):
|
|
content = _net.render_network_file(
|
|
"eth0", {"domains": ["example.com", "internal"]}
|
|
)
|
|
assert "Domains=example.com" in content
|
|
assert "Domains=internal" in content
|
|
|
|
def test_dns_default_route(self):
|
|
content = _net.render_network_file("eth0", {"dns_default_route": True})
|
|
assert "DNSDefaultRoute=yes" in content
|
|
|
|
def test_with_routes(self):
|
|
entry = {
|
|
"routes": [
|
|
{"destination": "10.0.0.0/8", "gateway": "192.168.1.254"},
|
|
{"destination": "172.16.0.0/12", "gateway": "10.0.0.254"},
|
|
]
|
|
}
|
|
content = _net.render_network_file("eth0", entry)
|
|
assert "[Route]" in content
|
|
assert "[Route#1]" in content
|
|
assert "[Route1]" not in content
|
|
assert "Destination=10.0.0.0/8" in content
|
|
assert "Gateway=10.0.0.254" in content
|
|
|
|
def test_route_with_extended_keys(self):
|
|
"""Route with metric, table, scope, etc."""
|
|
entry = {
|
|
"routes": [
|
|
{
|
|
"destination": "10.0.0.0/8",
|
|
"gateway": "192.168.1.254",
|
|
"metric": 100,
|
|
"table": 100,
|
|
"scope": "link",
|
|
}
|
|
]
|
|
}
|
|
content = _net.render_network_file("eth0", entry)
|
|
assert "Metric=100" in content
|
|
assert "Table=100" in content
|
|
assert "Scope=link" in content
|
|
|
|
def test_link_section(self):
|
|
entry = {
|
|
"link": {
|
|
"mtu_bytes": 9000,
|
|
"mac_address": "00:11:22:33:44:55",
|
|
"arp": True,
|
|
"multicast": False,
|
|
"activation_policy": "manual",
|
|
"required_for_online": True,
|
|
}
|
|
}
|
|
content = _net.render_network_file("eth0", entry)
|
|
assert "[Link]" in content
|
|
assert "MTUBytes=9000" in content
|
|
assert "MACAddress=00:11:22:33:44:55" in content
|
|
assert "ARP=yes" in content
|
|
assert "Multicast=no" in content
|
|
assert "ActivationPolicy=manual" in content
|
|
assert "RequiredForOnline=yes" in content
|
|
|
|
def test_link_unmanaged(self):
|
|
entry = {"link": {"unmanaged": True}}
|
|
content = _net.render_network_file("eth0", entry)
|
|
assert "Unmanaged=yes" in content
|
|
|
|
def test_dhcp_mode(self):
|
|
content = _net.render_network_file("eth0", {"dhcp": "ipv4"})
|
|
assert "DHCP=ipv4" in content
|
|
|
|
def test_address_with_extended_keys(self):
|
|
entry = {
|
|
"addresses": [
|
|
{
|
|
"address": "10.0.0.1/24",
|
|
"label": "eth0:0",
|
|
"scope": "host",
|
|
"route_metric": 50,
|
|
"duplicate_address_detection": "enabled",
|
|
"manage_temporary_address": False,
|
|
"add_prefix_route": True,
|
|
}
|
|
]
|
|
}
|
|
content = _net.render_network_file("eth0", entry)
|
|
assert "Address=10.0.0.1/24" in content
|
|
assert "Label=eth0:0" in content
|
|
assert "Scope=host" in content
|
|
assert "RouteMetric=50" in content
|
|
assert "DuplicateAddressDetection=enabled" in content
|
|
assert "ManageTemporaryAddress=no" in content
|
|
assert "AddPrefixRoute=yes" in content
|
|
|
|
def test_dhcpv4_section(self):
|
|
entry = {
|
|
"dhcp_client": {
|
|
"hostname": "myhost",
|
|
"rapid_commit": True,
|
|
"use_dns": True,
|
|
}
|
|
}
|
|
content = _net.render_network_file("eth0", entry)
|
|
assert "[DHCPv4]" in content
|
|
assert "Hostname=myhost" in content
|
|
assert "RapidCommit=yes" in content
|
|
assert "UseDNS=yes" in content
|
|
|
|
def test_dhcpv6_section(self):
|
|
entry = {
|
|
"dhcp_client": {
|
|
"send_hostname": True,
|
|
"hostname": "myhost",
|
|
"use_dns": False,
|
|
}
|
|
}
|
|
content = _net.render_network_file("eth0", entry)
|
|
assert "[DHCPv6]" in content
|
|
assert "SendHostname=yes" in content
|
|
assert "Hostname=myhost" in content
|
|
assert "UseDNS=no" in content
|
|
|
|
def test_dhcpv4_with_send_option(self):
|
|
entry = {
|
|
"dhcp_client": {
|
|
"send_option": [
|
|
{"code": "5", "value": "10"},
|
|
"10 20",
|
|
],
|
|
"user_class": ["class1", "class2"],
|
|
}
|
|
}
|
|
content = _net.render_network_file("eth0", entry)
|
|
assert "SendOption=5 10" in content
|
|
assert "SendOption=10 20" in content
|
|
assert "UserClass=class1" in content
|
|
assert "UserClass=class2" in content
|
|
|
|
def test_no_link_section_when_empty(self):
|
|
content = _net.render_network_file("eth0", {})
|
|
assert "[Link]" not in content
|
|
|
|
def test_no_dhcp_section_when_empty(self):
|
|
content = _net.render_network_file("eth0", {})
|
|
assert "[DHCPv4]" not in content
|
|
assert "[DHCPv6]" not in content
|
|
|
|
def test_full_entry(self):
|
|
entry = {
|
|
"addresses": [{"address": "10.0.0.1/24"}],
|
|
"gateway": "10.0.0.254",
|
|
"dns": ["1.1.1.1", "1.0.0.1"],
|
|
"routes": [{"destination": "192.168.0.0/16", "gateway": "10.0.0.254"}],
|
|
}
|
|
content = _net.render_network_file("eth0", entry)
|
|
assert "Address=10.0.0.1/24" in content
|
|
assert "Gateway=10.0.0.254" in content
|
|
assert "DNS=1.1.1.1" in content
|
|
assert "DNS=1.0.0.1" in content
|
|
assert "[Route]" in content
|
|
|
|
def test_ipv6_addresses(self):
|
|
entry = {
|
|
"addresses": ["10.0.0.1/24"],
|
|
"ipv6_addresses": [
|
|
{"address": "fd00::1/64"},
|
|
{"address": "fd00::2/64", "scope": "link"},
|
|
],
|
|
}
|
|
content = _net.render_network_file("eth0", entry)
|
|
# IPv6 addresses get offset indices
|
|
assert "[Address#1]" in content
|
|
assert "[Address#2]" in content
|
|
assert "Address=fd00::1/64" in content
|
|
|
|
|
|
# =================================================================
|
|
# parse_networkctl_status
|
|
# =================================================================
|
|
|
|
|
|
class TestParseNetworkctlStatus:
|
|
def test_empty_output(self):
|
|
assert _net.parse_networkctl_status("") == {}
|
|
|
|
def test_single_interface(self):
|
|
output = json.dumps(
|
|
{
|
|
"Interfaces": [
|
|
{
|
|
"Name": "eth0",
|
|
"Type": "ether",
|
|
"AdministrativeState": "configured",
|
|
"OperationalState": "routable",
|
|
"Addresses": [
|
|
{
|
|
"Family": 2,
|
|
"Address": [192, 168, 1, 1],
|
|
"PrefixLength": 24,
|
|
}
|
|
],
|
|
"DNS": [
|
|
{"Family": 2, "Address": [8, 8, 8, 8]},
|
|
{"Family": 2, "Address": [8, 8, 4, 4]},
|
|
],
|
|
"Routes": [
|
|
{
|
|
"Family": 2,
|
|
"Destination": [0, 0, 0, 0],
|
|
"DestinationPrefixLength": 0,
|
|
"Gateway": [192, 168, 1, 254],
|
|
}
|
|
],
|
|
}
|
|
]
|
|
}
|
|
)
|
|
result = _net.parse_networkctl_status(output)
|
|
assert "eth0" in result
|
|
iface = result["eth0"]
|
|
assert "192.168.1.1/24" in iface["addresses"]
|
|
assert iface["gateway"] == "192.168.1.254"
|
|
assert "8.8.8.8" in iface["dns"]
|
|
assert "8.8.4.4" in iface["dns"]
|
|
|
|
def test_unmanaged(self):
|
|
output = json.dumps(
|
|
{
|
|
"Interfaces": [
|
|
{
|
|
"Name": "lo",
|
|
"Type": "loopback",
|
|
"AdministrativeState": "unmanaged",
|
|
"OperationalState": "carrier",
|
|
"Addresses": [],
|
|
}
|
|
]
|
|
}
|
|
)
|
|
result = _net.parse_networkctl_status(output)
|
|
assert "lo" in result
|
|
|
|
def test_no_addresses(self):
|
|
output = json.dumps(
|
|
{
|
|
"Interfaces": [
|
|
{
|
|
"Name": "eth0",
|
|
"Type": "ether",
|
|
"AdministrativeState": "degraded",
|
|
"OperationalState": "degraded",
|
|
"Addresses": [],
|
|
}
|
|
]
|
|
}
|
|
)
|
|
result = _net.parse_networkctl_status(output)
|
|
assert "eth0" in result
|
|
assert result["eth0"]["addresses"] == []
|
|
|
|
def test_multiple_interfaces(self):
|
|
output = json.dumps(
|
|
{
|
|
"Interfaces": [
|
|
{
|
|
"Name": "eth0",
|
|
"Type": "ether",
|
|
"AdministrativeState": "configured",
|
|
"OperationalState": "routable",
|
|
"Addresses": [
|
|
{
|
|
"Family": 2,
|
|
"Address": [192, 168, 1, 1],
|
|
"PrefixLength": 24,
|
|
}
|
|
],
|
|
},
|
|
{
|
|
"Name": "eth1",
|
|
"Type": "ether",
|
|
"AdministrativeState": "configured",
|
|
"OperationalState": "routable",
|
|
"Addresses": [
|
|
{"Family": 2, "Address": [10, 0, 0, 1], "PrefixLength": 24}
|
|
],
|
|
},
|
|
]
|
|
}
|
|
)
|
|
result = _net.parse_networkctl_status(output)
|
|
assert "eth0" in result
|
|
assert "eth1" in result
|
|
|
|
|
|
# =================================================================
|
|
# generate_network_files — TF-6: numeric prefixes + stale cleanup
|
|
# =================================================================
|
|
|
|
|
|
class TestGenerateNetworkFiles:
|
|
def test_generates_files_with_prefix(self, tmp_network):
|
|
cfg = {
|
|
"interfaces": {
|
|
"eth0": {"addresses": ["10.0.0.1/24"], "gateway": "10.0.0.254"},
|
|
"eth1": {"addresses": ["192.168.1.1/24"]},
|
|
}
|
|
}
|
|
_net.save_config(cfg)
|
|
result = _net.generate_network_files(cfg)
|
|
assert "generated" in result
|
|
assert "cleaned" in result
|
|
paths = result["generated"]
|
|
assert len(paths) == 2
|
|
# Check 99- prefix
|
|
assert (tmp_network / "data" / "networkd" / "99-eth0.network").exists()
|
|
content = (tmp_network / "data" / "networkd" / "99-eth0.network").read_text()
|
|
assert "Name=eth0" in content
|
|
assert "Gateway=10.0.0.254" in content
|
|
|
|
def test_empty_interfaces(self, tmp_network):
|
|
cfg = {"interfaces": {}}
|
|
_net.save_config(cfg)
|
|
result = _net.generate_network_files(cfg)
|
|
assert result["generated"] == []
|
|
assert result["cleaned"] == []
|
|
|
|
def test_non_dict_entry_skipped(self, tmp_network):
|
|
cfg = {"interfaces": {"bad": "not-a-dict"}}
|
|
_net.save_config(cfg)
|
|
result = _net.generate_network_files(cfg)
|
|
assert result["generated"] == []
|
|
|
|
def test_removes_stale_files(self, tmp_network):
|
|
"""Stale files from old bare-name format are cleaned up."""
|
|
data_dir = tmp_network / "data" / "networkd"
|
|
data_dir.mkdir(parents=True)
|
|
# Simulate old files
|
|
(data_dir / "old-eth0.network").write_text("[Match]\nName=old-eth0\n")
|
|
(data_dir / "99-old-eth0.network").write_text("[Match]\nName=old-eth0\n")
|
|
|
|
cfg = {"interfaces": {"eth0": {"addresses": ["10.0.0.1/24"]}}}
|
|
_net.save_config(cfg)
|
|
result = _net.generate_network_files(cfg)
|
|
|
|
assert len(result["generated"]) == 1
|
|
assert len(result["cleaned"]) == 2
|
|
# Old files are gone
|
|
assert not (data_dir / "old-eth0.network").exists()
|
|
assert not (data_dir / "99-old-eth0.network").exists()
|
|
# New file exists
|
|
assert (data_dir / "99-eth0.network").exists()
|
|
|
|
def test_cleanup_only_when_no_new_interfaces(self, tmp_network):
|
|
"""Only stale cleanup, no new files."""
|
|
data_dir = tmp_network / "data" / "networkd"
|
|
data_dir.mkdir(parents=True)
|
|
(data_dir / "stale.network").write_text("[Match]\n")
|
|
|
|
cfg = {"interfaces": {}}
|
|
_net.save_config(cfg)
|
|
result = _net.generate_network_files(cfg)
|
|
assert result["generated"] == []
|
|
assert len(result["cleaned"]) == 1
|
|
assert not (data_dir / "stale.network").exists()
|
|
|
|
|
|
# =================================================================
|
|
# TF-11: Extended DHCPv4 tests
|
|
# =================================================================
|
|
|
|
|
|
class TestDHCPv4Extended:
|
|
def test_dhcpv4_all_keys(self):
|
|
entry = {
|
|
"dhcp_client": {
|
|
"hostname": "myhost",
|
|
"duid_type": "llt",
|
|
"duid_raw_data": "01:02:03",
|
|
"iaid": "04:05:06:07",
|
|
"client_identifier": "aa:bb:cc",
|
|
"rapid_commit": True,
|
|
"anonymize": True,
|
|
"use_dns": False,
|
|
"use_ntp": True,
|
|
"use_sip": False,
|
|
"use_captive_portal": True,
|
|
"use_mtu": False,
|
|
"use_hostname": True,
|
|
"use_domains": "route",
|
|
"use_routes": False,
|
|
"route_metric": 200,
|
|
"send_decline": True,
|
|
"net_label": "mynet",
|
|
"nft_set": "myset",
|
|
"ip_service_type": "lowdelay",
|
|
"socket_priority": 10,
|
|
"bootp": False,
|
|
"label": "mylabel",
|
|
"max_attempts": 5,
|
|
"listen_port": 68,
|
|
"server_port": 67,
|
|
"mud_url": "https://example.com/mud.json",
|
|
"boot_filename": "boot.img",
|
|
"send_option": [
|
|
{"code": "5", "value": "10"},
|
|
"10 20",
|
|
],
|
|
"send_vendor_option": [
|
|
{"code": "1", "vendor_code": "2", "value": "3"},
|
|
"4 5 6",
|
|
],
|
|
"user_class": ["class1", "class2"],
|
|
"vendor_class_identifier": "vendor1",
|
|
"request_options": "1 3 6",
|
|
}
|
|
}
|
|
content = _net.render_network_file("eth0", entry)
|
|
assert "[DHCPv4]" in content
|
|
assert "Hostname=myhost" in content
|
|
assert "DUIDType=llt" in content
|
|
assert "DUIDRawData=01:02:03" in content
|
|
assert "IAID=04:05:06:07" in content
|
|
assert "ClientIdentifier=aa:bb:cc" in content
|
|
assert "RapidCommit=yes" in content
|
|
assert "Anonymize=yes" in content
|
|
assert "UseDNS=no" in content
|
|
assert "UseNTP=yes" in content
|
|
assert "UseSIP=no" in content
|
|
assert "UseCaptivePortal=yes" in content
|
|
assert "UseMTU=no" in content
|
|
assert "UseHostname=yes" in content
|
|
assert "UseDomains=route" in content
|
|
assert "UseRoutes=no" in content
|
|
assert "RouteMetric=200" in content
|
|
assert "SendDecline=yes" in content
|
|
assert "NetLabel=mynet" in content
|
|
assert "NFTSet=myset" in content
|
|
assert "IPServiceType=lowdelay" in content
|
|
assert "SocketPriority=10" in content
|
|
assert "BOOTP=no" in content
|
|
assert "Label=mylabel" in content
|
|
assert "MaxAttempts=5" in content
|
|
assert "ListenPort=68" in content
|
|
assert "ServerPort=67" in content
|
|
assert "MUDURL=https://example.com/mud.json" in content
|
|
assert "BootFilename=boot.img" in content
|
|
assert "SendOption=5 10" in content
|
|
assert "SendOption=10 20" in content
|
|
assert "SendVendorOption=1 2 3" in content
|
|
assert "SendVendorOption=4 5 6" in content
|
|
assert "UserClass=class1" in content
|
|
assert "UserClass=class2" in content
|
|
assert "VendorClassIdentifier=vendor1" in content
|
|
assert "RequestOptions=1 3 6" in content
|
|
|
|
def test_dhcpv4_only_when_dhcp_ipv4(self):
|
|
entry = {
|
|
"dhcp": "ipv4",
|
|
"dhcp_client": {"hostname": "test"},
|
|
}
|
|
content = _net.render_network_file("eth0", entry)
|
|
assert "[DHCPv4]" in content
|
|
assert "[DHCPv6]" not in content
|
|
|
|
def test_dhcpv4_only_when_dhcp_ipv6(self):
|
|
entry = {
|
|
"dhcp": "ipv6",
|
|
"dhcp_client": {"hostname": "test"},
|
|
}
|
|
content = _net.render_network_file("eth0", entry)
|
|
assert "[DHCPv4]" not in content
|
|
assert "[DHCPv6]" in content
|
|
|
|
def test_dhcpv4_and_v6_when_dhcp_yes(self):
|
|
entry = {
|
|
"dhcp": "yes",
|
|
"dhcp_client": {"hostname": "test"},
|
|
}
|
|
content = _net.render_network_file("eth0", entry)
|
|
assert "[DHCPv4]" in content
|
|
assert "[DHCPv6]" in content
|
|
|
|
|
|
# =================================================================
|
|
# TF-11: Extended DHCPv6 tests
|
|
# =================================================================
|
|
|
|
|
|
class TestDHCPv6Extended:
|
|
def test_dhcpv6_all_keys(self):
|
|
entry = {
|
|
"dhcp": "ipv6",
|
|
"dhcp_client": {
|
|
"send_hostname": True,
|
|
"hostname": "myhost",
|
|
"duid": "01:02",
|
|
"duid_type": "llt",
|
|
"duid_raw_data": "aa:bb",
|
|
"iaid": "01:02:03:04",
|
|
"anonymize": True,
|
|
"rapid_commit": "attempt-only",
|
|
"prefix_delegation_hint": "2001:db8::/48",
|
|
"unassigned_subnet_policy": "/64",
|
|
"use_address": True,
|
|
"use_captive_portal": False,
|
|
"use_delegated_prefix": True,
|
|
"use_dns": True,
|
|
"use_ntp": False,
|
|
"use_sip": True,
|
|
"use_dnr": False,
|
|
"use_hostname": True,
|
|
"use_domains": "route",
|
|
"send_release": True,
|
|
"net_label": "vlan6",
|
|
"nft_set": "ipv6set",
|
|
"without_ra": "ipv6",
|
|
"send_option": [
|
|
{"code": "1", "value": "2"},
|
|
"3 4",
|
|
],
|
|
"send_vendor_option": [
|
|
{"code": "10", "vendor_code": "20", "value": "30"},
|
|
"40 50 60",
|
|
],
|
|
"user_class": ["v6class"],
|
|
"vendor_class": ["v6vendor"],
|
|
},
|
|
}
|
|
content = _net.render_network_file("eth0", entry)
|
|
assert "[DHCPv6]" in content
|
|
assert "SendHostname=yes" in content
|
|
assert "Hostname=myhost" in content
|
|
assert "DUID=01:02" in content
|
|
assert "DUIDType=llt" in content
|
|
assert "DUIDRawData=aa:bb" in content
|
|
assert "IAID=01:02:03:04" in content
|
|
assert "Anonymize=yes" in content
|
|
assert "RapidCommit=attempt-only" in content
|
|
assert "PrefixDelegationHint=2001:db8::/48" in content
|
|
assert "UnassignedSubnetPolicy=/64" in content
|
|
assert "UseAddress=yes" in content
|
|
assert "UseCaptivePortal=no" in content
|
|
assert "UseDelegatedPrefix=yes" in content
|
|
assert "UseDNS=yes" in content
|
|
assert "UseNTP=no" in content
|
|
assert "UseSIP=yes" in content
|
|
assert "UseDNR=no" in content
|
|
assert "UseHostname=yes" in content
|
|
assert "UseDomains=route" in content
|
|
assert "SendRelease=yes" in content
|
|
assert "NetLabel=vlan6" in content
|
|
assert "NFTSet=ipv6set" in content
|
|
assert "WithoutRA=ipv6" in content
|
|
assert "SendOption=1 2" in content
|
|
assert "SendOption=3 4" in content
|
|
assert "SendVendorOption=10 20 30" in content
|
|
assert "SendVendorOption=40 50 60" in content
|
|
assert "UserClass=v6class" in content
|
|
assert "VendorClass=v6vendor" in content
|
|
|
|
|
|
# =================================================================
|
|
# TF-11: Extended Route tests
|
|
# =================================================================
|
|
|
|
|
|
class TestRouteExtended:
|
|
def test_route_all_keys(self):
|
|
entry = {
|
|
"routes": [
|
|
{
|
|
"destination": "10.0.0.0/8",
|
|
"gateway": "192.168.1.254",
|
|
"metric": 100,
|
|
"table": 100,
|
|
"type": "unicast",
|
|
"scope": "link",
|
|
"gateway_on_link": True,
|
|
"ipv6_preference": "medium",
|
|
"initial_congestion_window": 10,
|
|
"initial_advertised_receive_window": 60,
|
|
"quick_ack": True,
|
|
"fast_open_no_cookie": False,
|
|
"mtu_bytes": 1400,
|
|
"protocol": "static",
|
|
"next_hop": 1,
|
|
"multi_path_route": ["10.0.0.2", "10.0.0.3"],
|
|
},
|
|
{
|
|
"destination": "172.16.0.0/12",
|
|
"gateway": "10.0.0.254",
|
|
"metric": 200,
|
|
},
|
|
]
|
|
}
|
|
content = _net.render_network_file("eth0", entry)
|
|
assert "[Route]" in content
|
|
assert "[Route#1]" in content
|
|
assert "Destination=10.0.0.0/8" in content
|
|
assert "Gateway=192.168.1.254" in content
|
|
assert "Metric=100" in content
|
|
assert "Table=100" in content
|
|
assert "Type=unicast" in content
|
|
assert "Scope=link" in content
|
|
assert "GatewayOnLink=yes" in content
|
|
assert "IPv6Preference=medium" in content
|
|
assert "InitialCongestionWindow=10" in content
|
|
assert "InitialAdvertisedReceiveWindow=60" in content
|
|
assert "QuickAck=yes" in content
|
|
assert "FastOpenNoCookie=no" in content
|
|
assert "MTUBytes=1400" in content
|
|
assert "Protocol=static" in content
|
|
assert "NextHop=1" in content
|
|
assert "MultiPathRoute=10.0.0.2" in content
|
|
assert "MultiPathRoute=10.0.0.3" in content
|
|
assert "Destination=172.16.0.0/12" in content
|
|
assert "Metric=200" in content
|
|
assert "Gateway=10.0.0.254" in content
|
|
|
|
def test_route_integer_table(self):
|
|
entry = {
|
|
"routes": [
|
|
{"destination": "0.0.0.0/0", "gateway": "10.0.0.1", "table": "main"}
|
|
]
|
|
}
|
|
content = _net.render_network_file("eth0", entry)
|
|
assert "Table=main" in content
|
|
|
|
|
|
# =================================================================
|
|
# TF-11: collect_upstream_dns tests
|
|
# =================================================================
|
|
|
|
|
|
class TestCollectUpstreamDNS:
|
|
def test_collects_public_dns(self):
|
|
cfg = {
|
|
"interfaces": {
|
|
"eth0": {
|
|
"dns": ["8.8.8.8", "1.1.1.1"],
|
|
}
|
|
}
|
|
}
|
|
result = _net.collect_upstream_dns(cfg)
|
|
assert "8.8.8.8" in result
|
|
assert "1.1.1.1" in result
|
|
|
|
def test_filters_local_dns(self):
|
|
cfg = {
|
|
"interfaces": {
|
|
"eth0": {
|
|
"dns": ["8.8.8.8", "192.168.1.1", "10.0.0.1"],
|
|
}
|
|
}
|
|
}
|
|
result = _net.collect_upstream_dns(cfg)
|
|
assert "8.8.8.8" in result
|
|
assert "192.168.1.1" not in result
|
|
assert "10.0.0.1" not in result
|
|
|
|
def test_filters_ipv6_local_dns(self):
|
|
cfg = {
|
|
"interfaces": {
|
|
"eth0": {
|
|
"dns": ["8.8.8.8"],
|
|
"ipv6_dns": ["2001:4860:4860::8888", "fe80::1"],
|
|
}
|
|
}
|
|
}
|
|
result = _net.collect_upstream_dns(cfg)
|
|
assert "8.8.8.8" in result
|
|
assert "2001:4860:4860::8888" in result
|
|
assert "fe80::1" not in result
|
|
|
|
def test_deduplicates(self):
|
|
cfg = {
|
|
"interfaces": {
|
|
"eth0": {"dns": ["8.8.8.8"]},
|
|
"eth1": {"dns": ["8.8.8.8", "1.1.1.1"]},
|
|
}
|
|
}
|
|
result = _net.collect_upstream_dns(cfg)
|
|
assert result.count("8.8.8.8") == 1
|
|
assert "1.1.1.1" in result
|
|
|
|
def test_empty_config(self):
|
|
result = _net.collect_upstream_dns({"interfaces": {}})
|
|
assert result == []
|
|
|
|
def test_skips_non_dict_entries(self):
|
|
cfg = {"interfaces": {"bad": "not-a-dict"}}
|
|
result = _net.collect_upstream_dns(cfg)
|
|
assert result == []
|
|
|
|
def test_filters_loopback(self):
|
|
cfg = {
|
|
"interfaces": {
|
|
"eth0": {
|
|
"dns": ["8.8.8.8", "127.0.0.1", "::1"],
|
|
"ipv6_dns": ["::1", "2001:4860:4860::8844"],
|
|
}
|
|
}
|
|
}
|
|
result = _net.collect_upstream_dns(cfg)
|
|
assert "127.0.0.1" not in result
|
|
assert "::1" not in result
|
|
assert "8.8.8.8" in result
|
|
assert "2001:4860:4860::8844" in result
|
|
|
|
def test_filters_link_local(self):
|
|
cfg = {
|
|
"interfaces": {
|
|
"eth0": {
|
|
"dns": ["169.254.1.1", "8.8.4.4"],
|
|
}
|
|
}
|
|
}
|
|
result = _net.collect_upstream_dns(cfg)
|
|
assert "169.254.1.1" not in result
|
|
assert "8.8.4.4" in result
|
|
|
|
def test_filters_unicast_local(self):
|
|
cfg = {
|
|
"interfaces": {
|
|
"eth0": {
|
|
"ipv6_dns": ["fc00::1", "fd00::1", "2607:f8b0:4004:800::200e"],
|
|
}
|
|
}
|
|
}
|
|
result = _net.collect_upstream_dns(cfg)
|
|
assert "fc00::1" not in result
|
|
assert "fd00::1" not in result
|
|
assert "2607:f8b0:4004:800::200e" in result
|
|
|
|
|
|
# =================================================================
|
|
# TF-11: infer_dhcp_ranges tests
|
|
# =================================================================
|
|
|
|
|
|
class TestInferDhcpRanges:
|
|
def test_basic_subnet(self):
|
|
cfg = {
|
|
"interfaces": {
|
|
"eth0": {
|
|
"addresses": [{"address": "192.168.1.1/24"}],
|
|
}
|
|
}
|
|
}
|
|
result = _net.infer_dhcp_ranges(cfg)
|
|
assert "eth0" in result
|
|
r = result["eth0"]
|
|
assert r["prefix"] == 24
|
|
assert r["start"] == "192.168.1.100"
|
|
assert r["end"] == "192.168.1.200"
|
|
|
|
def test_bare_string_address(self):
|
|
cfg = {
|
|
"interfaces": {
|
|
"eth0": {
|
|
"addresses": ["192.168.1.1/24"],
|
|
}
|
|
}
|
|
}
|
|
result = _net.infer_dhcp_ranges(cfg)
|
|
assert "eth0" in result
|
|
|
|
def test_skips_ipv6_only(self):
|
|
cfg = {
|
|
"interfaces": {
|
|
"eth0": {
|
|
"addresses": [{"address": "fd00::1/64"}],
|
|
}
|
|
}
|
|
}
|
|
result = _net.infer_dhcp_ranges(cfg)
|
|
assert "eth0" not in result
|
|
|
|
def test_skips_non_network_address(self):
|
|
cfg = {
|
|
"interfaces": {
|
|
"eth0": {
|
|
"addresses": [{"address": "192.168.1.1"}],
|
|
}
|
|
}
|
|
}
|
|
result = _net.infer_dhcp_ranges(cfg)
|
|
assert "eth0" not in result
|
|
|
|
def test_empty_config(self):
|
|
result = _net.infer_dhcp_ranges({"interfaces": {}})
|
|
assert result == {}
|
|
|
|
def test_small_subnet_skipped(self):
|
|
cfg = {
|
|
"interfaces": {
|
|
"eth0": {
|
|
"addresses": [{"address": "192.168.1.0/31"}],
|
|
}
|
|
}
|
|
}
|
|
result = _net.infer_dhcp_ranges(cfg)
|
|
assert "eth0" not in result
|
|
|
|
def test_larger_subnet(self):
|
|
cfg = {
|
|
"interfaces": {
|
|
"eth0": {
|
|
"addresses": [{"address": "10.0.0.1/16"}],
|
|
}
|
|
}
|
|
}
|
|
result = _net.infer_dhcp_ranges(cfg)
|
|
assert "eth0" in result
|
|
r = result["eth0"]
|
|
assert r["prefix"] == 16
|
|
assert r["subnet"] == "10.0.0.0"
|
|
|
|
def test_skips_non_dict_entry(self):
|
|
cfg = {"interfaces": {"bad": "not-a-dict"}}
|
|
result = _net.infer_dhcp_ranges(cfg)
|
|
assert result == {}
|
|
|
|
|
|
# =================================================================
|
|
# TF-11: infer_zones tests
|
|
# =================================================================
|
|
|
|
|
|
class TestInferZones:
|
|
def test_wireguard_iface_is_wan(self):
|
|
cfg = {
|
|
"interfaces": {
|
|
"wg0": {},
|
|
}
|
|
}
|
|
result = _net.infer_zones(cfg)
|
|
assert result["wg0"] == "wan"
|
|
|
|
def test_dhcp_iface_is_wan(self):
|
|
cfg = {
|
|
"interfaces": {
|
|
"eth0": {"dhcp": "ipv4"},
|
|
}
|
|
}
|
|
result = _net.infer_zones(cfg)
|
|
assert result["eth0"] == "wan"
|
|
|
|
def test_dhcp_yes_is_wan(self):
|
|
cfg = {
|
|
"interfaces": {
|
|
"eth0": {"dhcp": "yes"},
|
|
}
|
|
}
|
|
result = _net.infer_zones(cfg)
|
|
assert result["eth0"] == "wan"
|
|
|
|
def test_routed_iface_is_management(self):
|
|
cfg = {
|
|
"interfaces": {
|
|
"eth0": {
|
|
"addresses": [{"address": "10.0.0.1/24"}],
|
|
"routes": [{"destination": "0.0.0.0/0", "gateway": "10.0.0.254"}],
|
|
}
|
|
}
|
|
}
|
|
result = _net.infer_zones(cfg)
|
|
assert result["eth0"] == "management"
|
|
|
|
def test_default_is_lan(self):
|
|
cfg = {
|
|
"interfaces": {
|
|
"eth1": {
|
|
"addresses": [{"address": "192.168.1.1/24"}],
|
|
}
|
|
}
|
|
}
|
|
result = _net.infer_zones(cfg)
|
|
assert result["eth1"] == "lan"
|
|
|
|
def test_empty_config(self):
|
|
result = _net.infer_zones({"interfaces": {}})
|
|
assert result == {}
|
|
|
|
def test_skips_non_dict_entry(self):
|
|
cfg = {"interfaces": {"bad": "not-a-dict"}}
|
|
result = _net.infer_zones(cfg)
|
|
assert result == {}
|
|
|
|
def test_multiple_interfaces_mixed(self):
|
|
cfg = {
|
|
"interfaces": {
|
|
"wg0": {},
|
|
"eth0": {"dhcp": "ipv4"},
|
|
"eth1": {"addresses": [{"address": "192.168.1.1/24"}]},
|
|
"eth2": {
|
|
"addresses": [{"address": "10.0.0.1/24"}],
|
|
"routes": [{"destination": "0.0.0.0/0", "gateway": "10.0.0.254"}],
|
|
},
|
|
}
|
|
}
|
|
result = _net.infer_zones(cfg)
|
|
assert result["wg0"] == "wan"
|
|
assert result["eth0"] == "wan"
|
|
assert result["eth1"] == "lan"
|
|
assert result["eth2"] == "management"
|