mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-06-07 19:59:14 -04:00
networkd: classify links by Type instead of name prefix (#2447)
* networkd: classify links by Type instead of name prefix
The systemd-networkd backend decided wifi-vs-ethernet by checking
whether the interface name started with "wlan" or "wlp". Anything
else (that was not on a small virtual-prefix denylist) was treated
as wired ethernet. That misclassified two common cases:
* Nebula tunnels (kernel name like "nebula.homelab", Type=none,
Kind=tun) showed up as a wired ethernet device — DMS rendered an
"Ethernet connected" indicator whenever the overlay was up, even
with no physical NIC plugged in.
* Renamed wifi interfaces (e.g. systemd link files that rename
wlan0 to a friendlier name like "wifi") were also miscategorised
as ethernet, because they no longer matched wlan*/wlp*.
networkd already publishes the real link kind in the JSON returned
by the per-link Describe method ("ether", "wlan", "loopback",
"none"). Fetch it during enumerateLinks, cache it on linkInfo, and
classify against that. The old prefix logic is kept as a fallback
for the case where Describe ever fails to populate Type.
The package-level looksVirtual() helper replaces the unexported
isVirtualInterface method so the new classification helpers and
their tests can use it without needing a live backend.
Tests cover both the Type-based and fallback paths, including the
Nebula-shaped Type=none/tun case that motivated this change.
* networkd: cache linkType across signal ticks and unit-test Describe fallback
enumerateLinks runs on every PropertiesChanged signal under
/org/freedesktop/network1, which fires on carrier flap, DHCP renew, and
each address change. The previous version rebuilt every linkInfo from
scratch on each tick, including a synchronous Describe D-Bus round-trip
per link, despite the link Type being fixed at netlink creation. Preserve
existing entries when the D-Bus path matches, refreshing only ifindex,
and only call fetchLinkType on a genuinely new entry. A link torn down
and re-created at a different path still triggers a refetch.
Extract parseDescribeType from fetchLinkType so the JSON failure path —
malformed payload, missing Type field, wrong type for Type — can be
exercised without a live D-Bus connection. The classifier's fallback to
name-prefix heuristics already had coverage; this locks in that the seam
between Describe and the classifier surfaces an empty string on every
failure mode rather than misclassifying a link.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
@@ -18,10 +19,41 @@ const (
|
||||
)
|
||||
|
||||
type linkInfo struct {
|
||||
ifindex int32
|
||||
name string
|
||||
path dbus.ObjectPath
|
||||
opState string
|
||||
ifindex int32
|
||||
name string
|
||||
path dbus.ObjectPath
|
||||
opState string
|
||||
linkType string
|
||||
}
|
||||
|
||||
func (l *linkInfo) isWired() bool {
|
||||
if l.linkType != "" {
|
||||
return l.linkType == "ether"
|
||||
}
|
||||
if looksVirtual(l.name) || strings.HasPrefix(l.name, "wlan") || strings.HasPrefix(l.name, "wlp") {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (l *linkInfo) isWireless() bool {
|
||||
if l.linkType != "" {
|
||||
return l.linkType == "wlan"
|
||||
}
|
||||
return strings.HasPrefix(l.name, "wlan") || strings.HasPrefix(l.name, "wlp")
|
||||
}
|
||||
|
||||
func looksVirtual(name string) bool {
|
||||
virtualPrefixes := []string{
|
||||
"lo", "docker", "veth", "virbr", "br-", "vnet", "tun", "tap",
|
||||
"vboxnet", "vmnet", "kube", "cni", "flannel", "cali",
|
||||
}
|
||||
for _, prefix := range virtualPrefixes {
|
||||
if strings.HasPrefix(name, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type SystemdNetworkdBackend struct {
|
||||
@@ -95,17 +127,50 @@ func (b *SystemdNetworkdBackend) enumerateLinks() error {
|
||||
defer b.linksMutex.Unlock()
|
||||
|
||||
for _, l := range links {
|
||||
b.links[l.Name] = &linkInfo{
|
||||
ifindex: l.Ifindex,
|
||||
name: l.Name,
|
||||
path: l.Path,
|
||||
if existing, ok := b.links[l.Name]; ok && existing.path == l.Path {
|
||||
existing.ifindex = l.Ifindex
|
||||
continue
|
||||
}
|
||||
log.Debugf("networkd: enumerated link %s (ifindex=%d, path=%s)", l.Name, l.Ifindex, l.Path)
|
||||
info := &linkInfo{
|
||||
ifindex: l.Ifindex,
|
||||
name: l.Name,
|
||||
path: l.Path,
|
||||
linkType: b.fetchLinkType(l.Path),
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// fetchLinkType queries networkd's Describe method and extracts the link Type
|
||||
// (e.g. "ether", "wlan", "loopback", "none"). Returns empty on failure; callers
|
||||
// fall back to name-prefix heuristics in that case. The Type is fixed at link
|
||||
// creation by the kernel, so callers cache the result for the lifetime of the
|
||||
// linkInfo and only refetch when a link is re-created at a new D-Bus path.
|
||||
func (b *SystemdNetworkdBackend) fetchLinkType(path dbus.ObjectPath) string {
|
||||
linkObj := b.conn.Object(networkdBusName, path)
|
||||
var describeJSON string
|
||||
if err := linkObj.Call(networkdLinkIface+".Describe", 0).Store(&describeJSON); err != nil {
|
||||
return ""
|
||||
}
|
||||
return parseDescribeType(describeJSON)
|
||||
}
|
||||
|
||||
// parseDescribeType extracts the top-level "Type" field from a networkd
|
||||
// Describe payload. Returns empty when the JSON is malformed or the field is
|
||||
// absent, signalling callers to fall back to name-prefix heuristics.
|
||||
func parseDescribeType(describeJSON string) string {
|
||||
var parsed struct {
|
||||
Type string `json:"Type"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(describeJSON), &parsed); err != nil {
|
||||
return ""
|
||||
}
|
||||
return parsed.Type
|
||||
}
|
||||
|
||||
func (b *SystemdNetworkdBackend) updateState() error {
|
||||
b.linksMutex.RLock()
|
||||
defer b.linksMutex.RUnlock()
|
||||
@@ -113,8 +178,8 @@ func (b *SystemdNetworkdBackend) updateState() error {
|
||||
var wiredIface *linkInfo
|
||||
var wifiIface *linkInfo
|
||||
|
||||
for name, link := range b.links {
|
||||
if b.isVirtualInterface(name) {
|
||||
for _, link := range b.links {
|
||||
if !link.isWired() && !link.isWireless() {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -126,11 +191,11 @@ func (b *SystemdNetworkdBackend) updateState() error {
|
||||
}
|
||||
}
|
||||
|
||||
if strings.HasPrefix(name, "wlan") || strings.HasPrefix(name, "wlp") {
|
||||
if link.isWireless() {
|
||||
if wifiIface == nil || link.opState == "routable" || link.opState == "carrier" {
|
||||
wifiIface = link
|
||||
}
|
||||
} else if !b.isVirtualInterface(name) {
|
||||
} else if link.isWired() {
|
||||
if wiredIface == nil || link.opState == "routable" || link.opState == "carrier" {
|
||||
wiredIface = link
|
||||
}
|
||||
@@ -140,7 +205,7 @@ func (b *SystemdNetworkdBackend) updateState() error {
|
||||
var wiredConns []WiredConnection
|
||||
var ethernetDevices []EthernetDevice
|
||||
for name, link := range b.links {
|
||||
if b.isVirtualInterface(name) || strings.HasPrefix(name, "wlan") || strings.HasPrefix(name, "wlp") {
|
||||
if !link.isWired() {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -229,19 +294,6 @@ func (b *SystemdNetworkdBackend) updateState() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *SystemdNetworkdBackend) isVirtualInterface(name string) bool {
|
||||
virtualPrefixes := []string{
|
||||
"lo", "docker", "veth", "virbr", "br-", "vnet", "tun", "tap",
|
||||
"vboxnet", "vmnet", "kube", "cni", "flannel", "cali",
|
||||
}
|
||||
for _, prefix := range virtualPrefixes {
|
||||
if strings.HasPrefix(name, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (b *SystemdNetworkdBackend) getAddresses(ifname string) []string {
|
||||
iface, err := net.InterfaceByName(ifname)
|
||||
if err != nil {
|
||||
|
||||
@@ -12,7 +12,7 @@ func (b *SystemdNetworkdBackend) GetWiredConnections() ([]WiredConnection, error
|
||||
|
||||
var conns []WiredConnection
|
||||
for name, link := range b.links {
|
||||
if b.isVirtualInterface(name) || strings.HasPrefix(name, "wlan") || strings.HasPrefix(name, "wlp") {
|
||||
if !link.isWired() {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -73,8 +73,8 @@ func (b *SystemdNetworkdBackend) GetWiredNetworkDetails(id string) (*WiredNetwor
|
||||
func (b *SystemdNetworkdBackend) ConnectEthernet() error {
|
||||
b.linksMutex.RLock()
|
||||
var primaryWired *linkInfo
|
||||
for name, l := range b.links {
|
||||
if strings.HasPrefix(name, "lo") || strings.HasPrefix(name, "wlan") || strings.HasPrefix(name, "wlp") {
|
||||
for _, l := range b.links {
|
||||
if !l.isWired() {
|
||||
continue
|
||||
}
|
||||
primaryWired = l
|
||||
|
||||
@@ -145,3 +145,73 @@ func TestSystemdNetworkdBackend_DisconnectEthernetDevice(t *testing.T) {
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not supported")
|
||||
}
|
||||
|
||||
func TestLinkInfo_Classify(t *testing.T) {
|
||||
// When networkd reports a Type via Describe, classification is exact.
|
||||
cases := []struct {
|
||||
name string
|
||||
ifname string
|
||||
linkType string
|
||||
wantWired bool
|
||||
wantWifi bool
|
||||
}{
|
||||
{"ether type", "dock", "ether", true, false},
|
||||
{"wlan type", "wifi", "wlan", false, true},
|
||||
{"loopback type", "lo", "loopback", false, false},
|
||||
{"none type (tun overlay)", "nebula.homelab", "none", false, false},
|
||||
{"none type (wireguard)", "wg0", "none", false, false},
|
||||
// Fallback path: linkType unavailable, name-prefix heuristic applies.
|
||||
{"fallback enp wired", "enp141s0", "", true, false},
|
||||
{"fallback wlan wireless", "wlan0", "", false, true},
|
||||
{"fallback wlp wireless", "wlp3s0", "", false, true},
|
||||
{"fallback lo skipped", "lo", "", false, false},
|
||||
{"fallback docker skipped", "docker0", "", false, false},
|
||||
{"fallback tun skipped", "tun0", "", false, false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
l := &linkInfo{name: tc.ifname, linkType: tc.linkType}
|
||||
assert.Equal(t, tc.wantWired, l.isWired(), "isWired")
|
||||
assert.Equal(t, tc.wantWifi, l.isWireless(), "isWireless")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDescribeType(t *testing.T) {
|
||||
// parseDescribeType is the seam between networkd's Describe RPC and the
|
||||
// classifier. On any failure path it must return "" so callers fall back
|
||||
// to name-prefix heuristics rather than misclassifying the link.
|
||||
cases := []struct {
|
||||
name string
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{"ether", `{"Type":"ether","Name":"enp141s0"}`, "ether"},
|
||||
{"wlan", `{"Type":"wlan","Name":"wlan0"}`, "wlan"},
|
||||
{"loopback", `{"Type":"loopback","Name":"lo"}`, "loopback"},
|
||||
{"none with kind", `{"Type":"none","Kind":"tun","Name":"nebula.homelab"}`, "none"},
|
||||
{"empty payload", ``, ""},
|
||||
{"empty object", `{}`, ""},
|
||||
{"missing Type field", `{"Name":"wlan0","Kind":""}`, ""},
|
||||
{"explicit empty Type", `{"Type":"","Name":"wlan0"}`, ""},
|
||||
{"malformed json", `{"Type":"ether"`, ""},
|
||||
{"non-string Type", `{"Type":42}`, ""},
|
||||
{"unrelated payload", `"just a string"`, ""},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert.Equal(t, tc.want, parseDescribeType(tc.in))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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"}
|
||||
for _, n := range virtual {
|
||||
assert.True(t, looksVirtual(n), "%s should look virtual", n)
|
||||
}
|
||||
real := []string{"enp141s0", "eno1", "wlan0", "wlp3s0", "wifi", "dock", "nebula.homelab", "wg0"}
|
||||
for _, n := range real {
|
||||
assert.False(t, looksVirtual(n), "%s should not look virtual", n)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user