feat: improve MAC address selection with virtual NIC fallback

- Extract selectMAC for testable interface selection logic
- Prefer physical NICs, fall back to virtual MACs (e.g. Docker)
- Add comprehensive unit tests for all MAC selection scenarios
This commit is contained in:
audanye-sudo
2026-03-28 23:38:55 +08:00
parent d0ab1ff641
commit 7fdabb6230
2 changed files with 108 additions and 15 deletions
+81 -1
View File
@@ -1,6 +1,7 @@
package security
import (
"net"
"os"
"path/filepath"
"testing"
@@ -111,7 +112,7 @@ func TestGetMACAddress_ReturnsNonEmpty(t *testing.T) {
t.Parallel()
mac, err := GetMACAddress()
if err != nil {
t.Skipf("no physical NIC available: %v", err)
t.Skipf("no NIC available: %v", err)
}
if mac == "" {
t.Fatal("expected non-empty MAC address")
@@ -122,6 +123,85 @@ func TestGetMACAddress_ReturnsNonEmpty(t *testing.T) {
}
}
func TestSelectMAC_PhysicalPreferred(t *testing.T) {
t.Parallel()
ifaces := []net.Interface{
{Index: 1, Name: "lo", Flags: net.FlagLoopback, HardwareAddr: nil},
{Index: 2, Name: "eth0", Flags: net.FlagUp, HardwareAddr: net.HardwareAddr{0x02, 0x42, 0xac, 0x11, 0x00, 0x02}}, // Docker virtual
{Index: 3, Name: "enp0s3", Flags: net.FlagUp, HardwareAddr: net.HardwareAddr{0x14, 0x98, 0x77, 0xab, 0xcd, 0xef}}, // physical
}
mac, err := selectMAC(ifaces)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if mac != "14:98:77:ab:cd:ef" {
t.Fatalf("expected physical MAC, got %s", mac)
}
}
func TestSelectMAC_VirtualFallback(t *testing.T) {
t.Parallel()
// Docker container scenario: only loopback + Docker veth
ifaces := []net.Interface{
{Index: 1, Name: "lo", Flags: net.FlagLoopback, HardwareAddr: nil},
{Index: 2, Name: "eth0", Flags: net.FlagUp, HardwareAddr: net.HardwareAddr{0x02, 0x42, 0xac, 0x11, 0x00, 0x02}},
}
mac, err := selectMAC(ifaces)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if mac != "02:42:ac:11:00:02" {
t.Fatalf("expected Docker virtual MAC, got %s", mac)
}
}
func TestSelectMAC_MultipleVirtualPicksFirst(t *testing.T) {
t.Parallel()
ifaces := []net.Interface{
{Index: 1, Name: "lo", Flags: net.FlagLoopback},
{Index: 2, Name: "eth0", Flags: net.FlagUp, HardwareAddr: net.HardwareAddr{0x02, 0x42, 0xbb, 0x00, 0x00, 0x01}},
{Index: 3, Name: "eth1", Flags: net.FlagUp, HardwareAddr: net.HardwareAddr{0x02, 0x42, 0xaa, 0x00, 0x00, 0x01}},
}
mac, err := selectMAC(ifaces)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Lexicographically: 02:42:aa:... < 02:42:bb:...
if mac != "02:42:aa:00:00:01" {
t.Fatalf("expected lexicographically first virtual MAC, got %s", mac)
}
}
func TestSelectMAC_NoInterfaces(t *testing.T) {
t.Parallel()
_, err := selectMAC(nil)
if err == nil {
t.Fatal("expected error for empty interface list")
}
}
func TestSelectMAC_OnlyLoopback(t *testing.T) {
t.Parallel()
ifaces := []net.Interface{
{Index: 1, Name: "lo", Flags: net.FlagLoopback, HardwareAddr: net.HardwareAddr{0x00, 0x00, 0x00, 0x00, 0x00, 0x00}},
}
_, err := selectMAC(ifaces)
if err == nil {
t.Fatal("expected error when only loopback exists")
}
}
func TestSelectMAC_NoHardwareAddr(t *testing.T) {
t.Parallel()
ifaces := []net.Interface{
{Index: 1, Name: "tun0", Flags: net.FlagUp, HardwareAddr: nil},
}
_, err := selectMAC(ifaces)
if err == nil {
t.Fatal("expected error for interfaces without hardware address")
}
}
// ─── storage.go ────────────────────────────────────────────────────────
func TestDeleteEncryptedData(t *testing.T) {
+27 -14
View File
@@ -43,14 +43,10 @@ func isVirtualMAC(mac string) bool {
return false
}
// GetMACAddress returns the first physical (non-loopback, non-virtual) MAC
// address, sorted lexicographically for determinism.
func GetMACAddress() (string, error) {
ifaces, err := net.Interfaces()
if err != nil {
return "", fmt.Errorf("enumerating network interfaces: %w", err)
}
var candidates []string
// selectMAC picks the best MAC from the given interface list.
// Physical NICs are preferred; virtual MACs are used as fallback.
func selectMAC(ifaces []net.Interface) (string, error) {
var physical, virtual []string
for _, iface := range ifaces {
if iface.Flags&net.FlagLoopback != 0 {
continue
@@ -61,13 +57,30 @@ func GetMACAddress() (string, error) {
}
s := strings.ToLower(hw.String())
if isVirtualMAC(s) {
continue
virtual = append(virtual, s)
} else {
physical = append(physical, s)
}
candidates = append(candidates, s)
}
if len(candidates) == 0 {
return "", fmt.Errorf("no physical NIC MAC address found")
if len(physical) > 0 {
slices.Sort(physical)
return physical[0], nil
}
slices.Sort(candidates)
return candidates[0], nil
if len(virtual) > 0 {
slices.Sort(virtual)
return virtual[0], nil
}
return "", fmt.Errorf("no NIC MAC address found (no network interfaces available)")
}
// GetMACAddress returns the first physical (non-loopback, non-virtual) MAC
// address, sorted lexicographically for determinism. If no physical NIC is
// found (e.g. inside a Docker container), it falls back to the first
// non-loopback virtual MAC address so that token encryption still works.
func GetMACAddress() (string, error) {
ifaces, err := net.Interfaces()
if err != nil {
return "", fmt.Errorf("enumerating network interfaces: %w", err)
}
return selectMAC(ifaces)
}