1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-06-07 19:59:14 -04:00

fix(network): exclude virtual ether links and prune stale ones from networkd (#2505)

The networkd backend treated any link reporting Type=ether as a wired uplink.
Podman bridges and veth pairs report Type=ether, so they were classified as
ethernet: isWired() short-circuited on Type and never consulted looksVirtual(),
which also lacked a podman prefix.

The link map was also never pruned. Links discovered at enumeration or via
signals were kept forever, so torn-down container interfaces lingered as
routable and could win the wired-uplink slot over the real NIC -- leaving the
indicator showing WiFi while a wired connection was active and default-routed.

- isWired()/isWireless() exclude virtual interfaces before consulting Type, and
  looksVirtual() now recognises podman.
- enumerateLinks() reconciles the cached map against ListLinks via syncLinks(),
  pruning links that no longer appear so dead interfaces don't accumulate.
This commit is contained in:
Graeme Foster
2026-06-01 14:45:49 +01:00
committed by GitHub
parent 304baf6f60
commit e51ceed175
2 changed files with 92 additions and 16 deletions
@@ -27,16 +27,19 @@ type linkInfo struct {
} }
func (l *linkInfo) isWired() bool { func (l *linkInfo) isWired() bool {
if looksVirtual(l.name) {
return false
}
if l.linkType != "" { if l.linkType != "" {
return l.linkType == "ether" return l.linkType == "ether"
} }
if looksVirtual(l.name) || strings.HasPrefix(l.name, "wlan") || strings.HasPrefix(l.name, "wlp") { return !strings.HasPrefix(l.name, "wlan") && !strings.HasPrefix(l.name, "wlp")
return false
}
return true
} }
func (l *linkInfo) isWireless() bool { func (l *linkInfo) isWireless() bool {
if looksVirtual(l.name) {
return false
}
if l.linkType != "" { if l.linkType != "" {
return l.linkType == "wlan" return l.linkType == "wlan"
} }
@@ -45,7 +48,7 @@ func (l *linkInfo) isWireless() bool {
func looksVirtual(name string) bool { func looksVirtual(name string) bool {
virtualPrefixes := []string{ virtualPrefixes := []string{
"lo", "docker", "veth", "virbr", "br-", "vnet", "tun", "tap", "lo", "docker", "podman", "veth", "virbr", "br-", "vnet", "tun", "tap",
"vboxnet", "vmnet", "kube", "cni", "flannel", "cali", "vboxnet", "vmnet", "kube", "cni", "flannel", "cali",
} }
for _, prefix := range virtualPrefixes { for _, prefix := range virtualPrefixes {
@@ -110,6 +113,12 @@ func (b *SystemdNetworkdBackend) Close() {
} }
} }
type enumeratedLink struct {
ifindex int32
name string
path dbus.ObjectPath
}
func (b *SystemdNetworkdBackend) enumerateLinks() error { func (b *SystemdNetworkdBackend) enumerateLinks() error {
obj := b.conn.Object(networkdBusName, b.managerPath) obj := b.conn.Object(networkdBusName, b.managerPath)
@@ -123,25 +132,48 @@ func (b *SystemdNetworkdBackend) enumerateLinks() error {
return fmt.Errorf("ListLinks: %w", err) return fmt.Errorf("ListLinks: %w", err)
} }
fresh := make([]enumeratedLink, len(links))
for i, l := range links {
fresh[i] = enumeratedLink{ifindex: l.Ifindex, name: l.Name, path: l.Path}
}
b.linksMutex.Lock() b.linksMutex.Lock()
defer b.linksMutex.Unlock() defer b.linksMutex.Unlock()
b.syncLinks(fresh)
for _, l := range links { return nil
if existing, ok := b.links[l.Name]; ok && existing.path == l.Path { }
existing.ifindex = l.Ifindex
// syncLinks reconciles the cached link map against the freshly enumerated set:
// it adds links not seen before (querying their Type once), refreshes the
// ifindex of survivors, and prunes links that no longer appear. Pruning is what
// keeps torn-down container interfaces (podman bridges, veth pairs) from
// lingering as routable and being mistaken for the wired uplink.
// Callers must hold linksMutex.
func (b *SystemdNetworkdBackend) syncLinks(fresh []enumeratedLink) {
present := make(map[string]bool, len(fresh))
for _, l := range fresh {
present[l.name] = true
if existing, ok := b.links[l.name]; ok && existing.path == l.path {
existing.ifindex = l.ifindex
continue continue
} }
info := &linkInfo{ info := &linkInfo{
ifindex: l.Ifindex, ifindex: l.ifindex,
name: l.Name, name: l.name,
path: l.Path, path: l.path,
linkType: b.fetchLinkType(l.Path), linkType: b.fetchLinkType(l.path),
} }
b.links[l.Name] = info b.links[l.name] = info
log.Debugf("networkd: enumerated link %s (ifindex=%d, path=%s, type=%q)", l.Name, l.Ifindex, l.Path, info.linkType) log.Debugf("networkd: enumerated link %s (ifindex=%d, path=%s, type=%q)", l.name, l.ifindex, l.path, info.linkType)
} }
return nil for name := range b.links {
if !present[name] {
log.Debugf("networkd: pruned stale link %s", name)
delete(b.links, name)
}
}
} }
// fetchLinkType queries networkd's Describe method and extracts the link Type // fetchLinkType queries networkd's Describe method and extracts the link Type
@@ -160,6 +160,12 @@ func TestLinkInfo_Classify(t *testing.T) {
{"loopback type", "lo", "loopback", false, false}, {"loopback type", "lo", "loopback", false, false},
{"none type (tun overlay)", "nebula.homelab", "none", false, false}, {"none type (tun overlay)", "nebula.homelab", "none", false, false},
{"none type (wireguard)", "wg0", "none", false, false}, {"none type (wireguard)", "wg0", "none", false, false},
// Virtual interfaces report Type=ether but must never be mistaken for
// the wired uplink — stale podman/veth links would otherwise poison
// ethernet detection.
{"veth ether excluded", "veth1234", "ether", false, false},
{"podman bridge ether excluded", "podman3", "ether", false, false},
{"docker bridge ether excluded", "docker0", "ether", false, false},
// Fallback path: linkType unavailable, name-prefix heuristic applies. // Fallback path: linkType unavailable, name-prefix heuristic applies.
{"fallback enp wired", "enp141s0", "", true, false}, {"fallback enp wired", "enp141s0", "", true, false},
{"fallback wlan wireless", "wlan0", "", false, true}, {"fallback wlan wireless", "wlan0", "", false, true},
@@ -205,8 +211,46 @@ func TestParseDescribeType(t *testing.T) {
} }
} }
func TestSyncLinks_PrunesRemovedLinks(t *testing.T) {
// Stale container interfaces (torn-down podman bridges, veth pairs) must
// not linger in the link map after they disappear from ListLinks — kept as
// routable, they stole the wired-uplink slot from the real ethernet NIC.
backend, _ := NewSystemdNetworkdBackend()
backend.links = map[string]*linkInfo{
"eno1": {ifindex: 2, name: "eno1", path: "/org/freedesktop/network1/link/_32", linkType: "ether", opState: "routable"},
"podman3": {ifindex: 9, name: "podman3", path: "/org/freedesktop/network1/link/_39", linkType: "ether", opState: "routable"},
"veth0": {ifindex: 10, name: "veth0", path: "/org/freedesktop/network1/link/_310", linkType: "ether", opState: "routable"},
}
backend.syncLinks([]enumeratedLink{
{ifindex: 2, name: "eno1", path: "/org/freedesktop/network1/link/_32"},
})
assert.Len(t, backend.links, 1)
assert.Contains(t, backend.links, "eno1")
assert.NotContains(t, backend.links, "podman3")
assert.NotContains(t, backend.links, "veth0")
}
func TestSyncLinks_RefreshesSurvivingLink(t *testing.T) {
// A link that survives keeps its cached Type — Describe is only queried for
// newly seen links — while picking up a refreshed ifindex.
backend, _ := NewSystemdNetworkdBackend()
backend.links = map[string]*linkInfo{
"eno1": {ifindex: 2, name: "eno1", path: "/org/freedesktop/network1/link/_32", linkType: "ether"},
}
backend.syncLinks([]enumeratedLink{
{ifindex: 7, name: "eno1", path: "/org/freedesktop/network1/link/_32"},
})
assert.Len(t, backend.links, 1)
assert.Equal(t, int32(7), backend.links["eno1"].ifindex)
assert.Equal(t, "ether", backend.links["eno1"].linkType)
}
func TestLooksVirtual(t *testing.T) { func TestLooksVirtual(t *testing.T) {
virtual := []string{"lo", "docker0", "veth123", "virbr0", "br-abc", "vnet0", "tun0", "tap0", "vboxnet0", "vmnet1", "kube-ipvs0", "cni0", "flannel.1", "cali-abc"} virtual := []string{"lo", "docker0", "veth123", "virbr0", "br-abc", "vnet0", "tun0", "tap0", "vboxnet0", "vmnet1", "kube-ipvs0", "cni0", "flannel.1", "cali-abc", "podman0", "podman3"}
for _, n := range virtual { for _, n := range virtual {
assert.True(t, looksVirtual(n), "%s should look virtual", n) assert.True(t, looksVirtual(n), "%s should look virtual", n)
} }