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:
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user