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
315 lines
11 KiB
Python
315 lines
11 KiB
Python
"""Networkd daemon handler.
|
|
|
|
Registers routes for managing systemd-networkd interface configuration
|
|
via config/network/config.json and generated .network files.
|
|
"""
|
|
|
|
import contextlib
|
|
import logging
|
|
import re
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from daemon.iface import (
|
|
GET_NETWORK_INFER_DHCP_RANGES,
|
|
GET_NETWORK_INFER_ZONES,
|
|
GET_NETWORK_INTERFACE_NAME,
|
|
GET_NETWORK_INTERFACES,
|
|
POST_NETWORK_APPLY,
|
|
POST_NETWORK_INTERFACE_NAME,
|
|
POST_NETWORK_INTERFACE_RELOAD,
|
|
POST_NETWORK_SYSCTL_SET,
|
|
)
|
|
from daemon.server import NotFoundError, registry
|
|
from lib.common import run, validate_interface_name
|
|
from lib.dnsmasq import set_upstreams
|
|
from lib.network import (
|
|
KNOWN_INTERFACE_KEYS,
|
|
collect_upstream_dns,
|
|
generate_network_files,
|
|
get_config,
|
|
infer_dhcp_ranges,
|
|
infer_zones,
|
|
parse_networkctl_status,
|
|
render_network_file,
|
|
save_config,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
PROJECT_DIR = Path(__file__).resolve().parent.parent.parent
|
|
CONFIG_DIR = PROJECT_DIR / "config" / "network"
|
|
DATA_DIR = PROJECT_DIR / "data" / "networkd"
|
|
|
|
|
|
def _copy_and_reload(iface_name: str) -> None:
|
|
"""Copy generated 99-<name>.network file to /etc/systemd/network/ and reload."""
|
|
validate_interface_name(iface_name)
|
|
src = DATA_DIR / f"99-{iface_name}.network"
|
|
dst_dir = Path("/etc/systemd/network")
|
|
run(["mkdir", "-p", str(dst_dir)], sudo=True)
|
|
dst = dst_dir / f"99-{iface_name}.network"
|
|
run(["cp", str(src), str(dst)], sudo=True)
|
|
|
|
# Remove lower-priority .network files that match this interface
|
|
# (they would override our config due to higher systemd priority)
|
|
if dst_dir.exists():
|
|
for f in dst_dir.iterdir():
|
|
if (
|
|
f.name.endswith(".network")
|
|
and f.name != dst.name
|
|
and _matches_interface(f.name, iface_name)
|
|
):
|
|
with contextlib.suppress(Exception):
|
|
run(["rm", str(f)], sudo=True)
|
|
logger.info("Removed conflicting file: %s", f.name)
|
|
|
|
run(["networkctl", "reload"], sudo=True)
|
|
run(["networkctl", "reconfigure", iface_name], sudo=True)
|
|
|
|
|
|
def _matches_interface(filename: str, iface_name: str) -> bool:
|
|
"""Check if a .network filename would match the given interface."""
|
|
base = filename.replace(".network", "")
|
|
# Strip numeric priority prefix (e.g. "50-eth1" → "eth1")
|
|
if "-" in base and base.split("-", 1)[0].isdigit():
|
|
base = base.split("-", 1)[1]
|
|
return base == iface_name
|
|
|
|
|
|
def _extract_iface_from_filename(filename: str) -> str | None:
|
|
"""Extract interface name from a .network filename (e.g. '50-eth1.network' → 'eth1')."""
|
|
base = filename.replace(".network", "")
|
|
if "-" in base and base.split("-", 1)[0].isdigit():
|
|
return base.split("-", 1)[1]
|
|
return base if base else None
|
|
|
|
|
|
def _full_reload() -> None:
|
|
"""Reload networkd for all interfaces."""
|
|
run(["networkctl", "reload"], sudo=True)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Routes
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@registry.register(GET_NETWORK_INTERFACES)
|
|
def get_interfaces(_request: Any, _body: Any) -> dict[str, Any]:
|
|
"""GET /network/interfaces — return all interface config + runtime state."""
|
|
cfg = get_config()
|
|
ifaces_cfg = cfg.get("interfaces", {})
|
|
|
|
runtime: dict[str, Any] = {}
|
|
with contextlib.suppress(Exception):
|
|
raw = run(["networkctl", "status", "--json=short", "--all"], sudo=True)
|
|
runtime = parse_networkctl_status(raw)
|
|
|
|
merged: dict[str, Any] = {}
|
|
all_names = set(ifaces_cfg.keys()) | set(runtime.keys()) - {"lo"}
|
|
for name in sorted(all_names):
|
|
merged[name] = {
|
|
"config": ifaces_cfg.get(name, {}),
|
|
"runtime": runtime.get(name, {}),
|
|
}
|
|
|
|
from lib.state import _now_iso
|
|
|
|
return {"interfaces": merged, "timestamp": _now_iso()}
|
|
|
|
|
|
@registry.register(GET_NETWORK_INTERFACE_NAME)
|
|
def get_interface(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
|
|
"""GET /network/interfaces/<name> — return config for one interface."""
|
|
if not body or "name" not in body:
|
|
raise ValueError("Interface name is required")
|
|
name = validate_interface_name(body["name"])
|
|
cfg = get_config()
|
|
ifaces = cfg.get("interfaces", {})
|
|
if name not in ifaces:
|
|
raise NotFoundError(f"Interface '{name}' not found in config")
|
|
|
|
runtime: dict[str, Any] = {}
|
|
with contextlib.suppress(Exception):
|
|
raw = run(["networkctl", "status", "--json=short", "--all"], sudo=True)
|
|
runtime = parse_networkctl_status(raw)
|
|
|
|
return {
|
|
"name": name,
|
|
"config": ifaces[name],
|
|
"runtime": runtime.get(name, {}),
|
|
}
|
|
|
|
|
|
@registry.register(POST_NETWORK_INTERFACE_NAME)
|
|
def save_interface(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
|
|
"""POST /network/interfaces/<name> — save config, render, apply."""
|
|
if not body:
|
|
raise ValueError("Request body required")
|
|
name = validate_interface_name(body.get("name", ""))
|
|
|
|
iface_cfg = {k: v for k, v in body.items() if k not in ("name",)}
|
|
|
|
unknown = set(iface_cfg.keys()) - KNOWN_INTERFACE_KEYS
|
|
if unknown:
|
|
logger.warning(
|
|
"Interface '%s': unexpected config keys %s — these will be "
|
|
"saved but not rendered to .network files",
|
|
name,
|
|
sorted(unknown),
|
|
)
|
|
|
|
with contextlib.suppress(Exception):
|
|
raw = run(["networkctl", "status", "--json=short", "--all"], sudo=True)
|
|
runtime = parse_networkctl_status(raw)
|
|
if name not in runtime:
|
|
logger.warning(
|
|
"Interface '%s' not found in networkctl "
|
|
"(config saved but networkd will ignore it)",
|
|
name,
|
|
)
|
|
|
|
cfg = get_config()
|
|
cfg.setdefault("interfaces", {})
|
|
cfg["interfaces"][name] = iface_cfg
|
|
save_config(cfg)
|
|
|
|
content = render_network_file(name, iface_cfg)
|
|
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
(DATA_DIR / f"99-{name}.network").write_text(content)
|
|
|
|
# Deploy to system. In containerized environments this may fail
|
|
# (e.g. read-only /run/sudo timestamps) — don't let that block the save.
|
|
deployed = True
|
|
try:
|
|
_copy_and_reload(name)
|
|
except Exception:
|
|
deployed = False
|
|
logger.warning(
|
|
"Interface '%s' config saved but failed to deploy to "
|
|
"systemd-networkd (sudo/system unavailable)",
|
|
name,
|
|
exc_info=True,
|
|
)
|
|
|
|
logger.info("Interface '%s' config saved (applied=%s)", name, deployed)
|
|
return {"name": name, "applied": deployed}
|
|
|
|
|
|
@registry.register(POST_NETWORK_INTERFACE_RELOAD)
|
|
def reload_interface(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
|
|
"""POST /network/interfaces/<name>/reload — reload networkd for interface."""
|
|
if not body or "name" not in body:
|
|
raise ValueError("'name' is required in request body")
|
|
name = validate_interface_name(body["name"])
|
|
|
|
with contextlib.suppress(Exception):
|
|
run(["networkctl", "reconfigure", name], sudo=True)
|
|
|
|
logger.info("Interface '%s' reloaded", name)
|
|
return {"name": name, "reloaded": True}
|
|
|
|
|
|
@registry.register(POST_NETWORK_APPLY)
|
|
def apply_all(_request: Any, _body: Any) -> dict[str, Any]:
|
|
"""POST /network/apply — apply ALL interfaces (full sync)."""
|
|
cfg = get_config()
|
|
result = generate_network_files(cfg)
|
|
generated = result.get("generated", [])
|
|
cleaned = result.get("cleaned", [])
|
|
|
|
# Remove stale/conflicting files from system dir
|
|
expected_names = {f.name for f in generated}
|
|
managed_ifaces = {
|
|
f.name.replace("99-", "").replace(".network", "") for f in generated
|
|
}
|
|
sys_dir = Path("/etc/systemd/network")
|
|
if sys_dir.exists():
|
|
for f in sys_dir.iterdir():
|
|
if f.name.endswith(".network") and f.name not in expected_names:
|
|
iface_from_file = _extract_iface_from_filename(f.name)
|
|
if iface_from_file and iface_from_file in managed_ifaces:
|
|
# Remove conflicting external configs for managed interfaces
|
|
with contextlib.suppress(Exception):
|
|
run(["rm", str(f)], sudo=True)
|
|
cleaned.append(f)
|
|
|
|
for f in generated:
|
|
dst = sys_dir / f.name
|
|
run(["mkdir", "-p", str(sys_dir)], sudo=True)
|
|
run(["cp", str(f), str(dst)], sudo=True)
|
|
|
|
_full_reload()
|
|
|
|
# TF-8: sync DNS upstreams to dnsmasq
|
|
try:
|
|
upstreams = collect_upstream_dns(cfg)
|
|
if upstreams:
|
|
set_upstreams(upstreams)
|
|
logger.info("Synced %d DNS upstreams to dnsmasq", len(upstreams))
|
|
except Exception:
|
|
logger.warning("Failed to sync DNS upstreams to dnsmasq", exc_info=True)
|
|
|
|
logger.info(
|
|
"Network config applied: %d interfaces, %d stale cleaned",
|
|
len(generated),
|
|
len(cleaned),
|
|
)
|
|
return {
|
|
"applied": len(generated),
|
|
"files": [str(p) for p in generated],
|
|
"cleaned": [str(p) for p in cleaned],
|
|
}
|
|
|
|
|
|
@registry.register(GET_NETWORK_INFER_DHCP_RANGES)
|
|
def get_infer_dhcp_ranges(_request: Any, _body: Any) -> dict[str, Any]:
|
|
"""GET /network/infer-dhcp-ranges — suggest DHCP ranges from static IPs."""
|
|
cfg = get_config()
|
|
ranges = infer_dhcp_ranges(cfg)
|
|
return {"ranges": ranges}
|
|
|
|
|
|
@registry.register(GET_NETWORK_INFER_ZONES)
|
|
def get_infer_zones(_request: Any, _body: Any) -> dict[str, Any]:
|
|
"""GET /network/infer-zones — suggest firewalld zones from interface config."""
|
|
cfg = get_config()
|
|
zones = infer_zones(cfg)
|
|
return {"zones": zones}
|
|
|
|
|
|
@registry.register(POST_NETWORK_SYSCTL_SET)
|
|
def set_sysctl(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
|
|
"""POST /sysctl/set — set a sysctl kernel parameter value.
|
|
|
|
Writes the value via `sysctl -w`, then verifies by reading it back.
|
|
|
|
Raises:
|
|
ValueError: When name or value is missing.
|
|
"""
|
|
if not body:
|
|
raise ValueError("Request body required")
|
|
name = body.get("name", "").strip()
|
|
if not name:
|
|
raise ValueError("'name' is required")
|
|
if not re.match(r"^[a-zA-Z0-9_]+(\.[a-zA-Z0-9_]+)*$", name):
|
|
raise ValueError("'name' is not a valid sysctl key")
|
|
value = str(body.get("value", "")).strip()
|
|
if not value:
|
|
raise ValueError("'value' is required")
|
|
|
|
run(["sysctl", "-w", f"{name}={value}"], sudo=True)
|
|
|
|
# Verify by reading back via /proc/sys (no sudo needed for reads, avoid
|
|
# triggering sudoers for read-only sysctl which is not whitelisted)
|
|
proc_path = Path(f"/proc/sys/{name.replace('.', '/')}")
|
|
read_value = proc_path.read_text().strip()
|
|
if read_value != value:
|
|
raise RuntimeError(
|
|
f"sysctl verify failed: set {name}={value} but read back {read_value}"
|
|
)
|
|
|
|
logger.info("sysctl %s set to %s", name, value)
|
|
return {"name": name, "value": value}
|