Skip to content

Commit cd9b137

Browse files
christiangdaclaude
andauthored
feat: add MACFilter for network interface classification (#6)
* feat: add MACFilter for network interface classification Add MACFilter type (Physical, All, Virtual) to control which network interfaces contribute to the machine ID. Physical-only (default) provides the most stable IDs on bare metal; All includes virtual interfaces for maximum uniqueness; Virtual-only supports container fingerprinting. Backward compatible: WithMAC() with no args behaves identically to before. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: align doc comments with go.dev/doc/comment standards - Add missing periods to all doc comment sentences - Use "reports whether" for bool-returning functions (Validate, isValidUUID, isValidSerial, isNonEmpty, isVirtualInterface) - Add [Symbol] doc links for cross-references (FormatMode, DiagnosticInfo, Provider.ID) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 84cda4c commit cd9b137

File tree

11 files changed

+281
-45
lines changed

11 files changed

+281
-45
lines changed

README.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,12 +142,36 @@ provider := machineid.New().
142142
WithCPU(). // processor ID and feature flags
143143
WithMotherboard(). // motherboard serial number
144144
WithSystemUUID(). // BIOS/UEFI system UUID
145-
WithMAC(). // physical network interface MAC addresses
145+
WithMAC(). // physical network interface MAC addresses (default filter)
146+
146147
WithDisk() // internal disk serial numbers
147148

148149
id, err := provider.ID(ctx)
149150
```
150151

152+
### MAC Address Filtering
153+
154+
Control which network interfaces are included in the machine ID using `MACFilter`:
155+
156+
```go
157+
ctx := context.Background()
158+
159+
// Physical interfaces only (default, most stable for bare-metal)
160+
id, _ := machineid.New().WithCPU().WithMAC().ID(ctx)
161+
162+
// All interfaces including virtual (VPN, Docker, bridges)
163+
id, _ = machineid.New().WithCPU().WithMAC(machineid.MACFilterAll).ID(ctx)
164+
165+
// Only virtual interfaces (useful for container-specific fingerprinting)
166+
id, _ = machineid.New().WithCPU().WithMAC(machineid.MACFilterVirtual).ID(ctx)
167+
```
168+
169+
| Filter | Interfaces Included | Best For |
170+
|---------------------|--------------------------------------------------------|--------------------------|
171+
| `MACFilterPhysical` | `en0`, `eth0`, `wlan0` (default) | Bare-metal stability |
172+
| `MACFilterAll` | Physical + virtual (`docker0`, `utun`, `bridge`, etc.) | Maximum uniqueness |
173+
| `MACFilterVirtual` | `docker0`, `utun`, `bridge0`, `veth`, `vmnet`, etc. | Container fingerprinting |
174+
151175
### Output Formats
152176

153177
All formats produce pure hexadecimal strings without dashes:
@@ -338,6 +362,12 @@ machineid -cpu -uuid -validate "b5c42832542981af58c9dc3bc241219e780ff7d276cfad05
338362
# Info-level logging (fallbacks, lifecycle events)
339363
machineid -cpu -uuid -verbose
340364

365+
# Include only physical MACs (default)
366+
machineid -mac -mac-filter physical
367+
368+
# Include all MACs (physical + virtual)
369+
machineid -all -mac-filter all
370+
341371
# Debug-level logging (command details, raw values, timing)
342372
machineid -all -debug
343373

@@ -354,6 +384,7 @@ machineid -version.long
354384
| `-motherboard` | Include motherboard serial number |
355385
| `-uuid` | Include system UUID |
356386
| `-mac` | Include network MAC addresses |
387+
| `-mac-filter F` | MAC filter: `physical` (default), `all`, or `virtual` |
357388
| `-disk` | Include disk serial numbers |
358389
| `-all` | Include all hardware identifiers |
359390
| `-vm` | VM-friendly mode (CPU + UUID only) |

cmd/machineid/main.go

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ func main() {
2222
motherboard := flag.Bool("motherboard", false, "Include motherboard serial number")
2323
uuid := flag.Bool("uuid", false, "Include system UUID")
2424
mac := flag.Bool("mac", false, "Include network MAC addresses")
25+
macFilterFlag := flag.String("mac-filter", "physical", "MAC filter: physical, all, virtual")
2526
disk := flag.Bool("disk", false, "Include disk serial numbers")
2627
all := flag.Bool("all", false, "Include all hardware identifiers")
2728
vm := flag.Bool("vm", false, "Use VM-friendly mode (CPU + UUID only)")
@@ -134,11 +135,18 @@ func main() {
134135
provider.WithSalt(*salt)
135136
}
136137

138+
mFilter, err := parseMACFilter(*macFilterFlag)
139+
if err != nil {
140+
slog.Error("invalid mac-filter", "error", err)
141+
flag.Usage()
142+
os.Exit(1)
143+
}
144+
137145
switch {
138146
case *vm:
139147
provider.VMFriendly()
140148
case *all:
141-
provider.WithCPU().WithMotherboard().WithSystemUUID().WithMAC().WithDisk()
149+
provider.WithCPU().WithMotherboard().WithSystemUUID().WithMAC(mFilter).WithDisk()
142150
default:
143151
if !*cpu && !*motherboard && !*uuid && !*mac && !*disk {
144152
// Default: CPU + Motherboard + System UUID
@@ -154,7 +162,7 @@ func main() {
154162
provider.WithSystemUUID()
155163
}
156164
if *mac {
157-
provider.WithMAC()
165+
provider.WithMAC(mFilter)
158166
}
159167
if *disk {
160168
provider.WithDisk()
@@ -213,6 +221,19 @@ func parseFormatMode(format int) (machineid.FormatMode, error) {
213221
}
214222
}
215223

224+
func parseMACFilter(value string) (machineid.MACFilter, error) {
225+
switch strings.ToLower(value) {
226+
case "physical":
227+
return machineid.MACFilterPhysical, nil
228+
case "all":
229+
return machineid.MACFilterAll, nil
230+
case "virtual":
231+
return machineid.MACFilterVirtual, nil
232+
default:
233+
return 0, fmt.Errorf("unsupported mac-filter %q; valid values are physical, all, virtual", value)
234+
}
235+
}
236+
216237
func handleValidate(ctx context.Context, provider *machineid.Provider, expectedID string, jsonOut bool) {
217238
valid, err := provider.Validate(ctx, expectedID)
218239
if err != nil {

darwin.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ func collectIdentifiers(ctx context.Context, p *Provider, diag *DiagnosticInfo)
7676

7777
if p.includeMAC {
7878
identifiers = appendIdentifiersIfValid(identifiers, func() ([]string, error) {
79-
return collectMACAddresses(logger)
79+
return collectMACAddresses(p.macFilter, logger)
8080
}, "mac:", diag, ComponentMAC, logger)
8181
}
8282

darwin_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -635,7 +635,7 @@ func TestCollectMACAddressesWithLogger(t *testing.T) {
635635
var buf bytes.Buffer
636636
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug}))
637637

638-
macs, err := collectMACAddresses(logger)
638+
macs, err := collectMACAddresses(MACFilterPhysical, logger)
639639
if err != nil {
640640
t.Logf("collectMACAddresses error (may be expected): %v", err)
641641
return

doc.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,32 @@
2929
// - [Provider.WithCPU] — processor identifier and feature flags
3030
// - [Provider.WithMotherboard] — motherboard / baseboard serial number
3131
// - [Provider.WithSystemUUID] — BIOS / UEFI system UUID
32-
// - [Provider.WithMAC] — MAC addresses of physical network interfaces
32+
// - [Provider.WithMAC] — MAC addresses of network interfaces (filterable)
3333
// - [Provider.WithDisk] — serial numbers of internal disks
3434
//
3535
// Or use [Provider.VMFriendly] to select a minimal, virtual-machine-safe
3636
// subset (CPU + System UUID).
3737
//
38+
// # MAC Address Filtering
39+
//
40+
// [Provider.WithMAC] accepts an optional [MACFilter] to control which network
41+
// interfaces contribute to the machine ID:
42+
//
43+
// - [MACFilterPhysical] — only physical interfaces (default)
44+
// - [MACFilterAll] — all non-loopback, up interfaces (physical + virtual)
45+
// - [MACFilterVirtual] — only virtual interfaces (VPN, bridge, container)
46+
//
47+
// Examples:
48+
//
49+
// // Physical interfaces only (default, most stable)
50+
// provider.WithMAC()
51+
//
52+
// // Include all interfaces
53+
// provider.WithMAC(machineid.MACFilterAll)
54+
//
55+
// // Only virtual interfaces (containers, VPNs)
56+
// provider.WithMAC(machineid.MACFilterVirtual)
57+
//
3858
// # Output Formats
3959
//
4060
// Set the output length with [Provider.WithFormat]:
@@ -159,6 +179,7 @@
159179
// machineid -cpu -uuid
160180
// machineid -all -format 32 -json
161181
// machineid -vm -salt "my-app" -diagnostics
182+
// machineid -mac -mac-filter all
162183
// machineid -cpu -uuid -verbose
163184
// machineid -all -debug
164185
// machineid -version

linux.go

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ func collectIdentifiers(ctx context.Context, p *Provider, diag *DiagnosticInfo)
3939

4040
if p.includeMAC {
4141
identifiers = appendIdentifiersIfValid(identifiers, func() ([]string, error) {
42-
return collectMACAddresses(logger)
42+
return collectMACAddresses(p.macFilter, logger)
4343
}, "mac:", diag, ComponentMAC, logger)
4444
}
4545

@@ -52,7 +52,7 @@ func collectIdentifiers(ctx context.Context, p *Provider, diag *DiagnosticInfo)
5252
return identifiers, nil
5353
}
5454

55-
// linuxCPUID retrieves CPU information from /proc/cpuinfo
55+
// linuxCPUID retrieves CPU information from /proc/cpuinfo.
5656
func linuxCPUID(logger *slog.Logger) (string, error) {
5757
const path = "/proc/cpuinfo"
5858

@@ -72,7 +72,7 @@ func linuxCPUID(logger *slog.Logger) (string, error) {
7272
return parseCPUInfo(string(data)), nil
7373
}
7474

75-
// parseCPUInfo extracts CPU information from /proc/cpuinfo content
75+
// parseCPUInfo extracts CPU information from /proc/cpuinfo content.
7676
func parseCPUInfo(content string) string {
7777
lines := strings.Split(content, "\n")
7878
var processor, vendorID, modelName, flags string
@@ -100,7 +100,7 @@ func parseCPUInfo(content string) string {
100100
return fmt.Sprintf("%s:%s:%s:%s", processor, vendorID, modelName, flags)
101101
}
102102

103-
// linuxSystemUUID retrieves system UUID from DMI
103+
// linuxSystemUUID retrieves system UUID from DMI.
104104
func linuxSystemUUID(logger *slog.Logger) (string, error) {
105105
// Try multiple locations for system UUID
106106
locations := []string{
@@ -111,7 +111,7 @@ func linuxSystemUUID(logger *slog.Logger) (string, error) {
111111
return readFirstValidFromLocations(locations, isValidUUID, logger)
112112
}
113113

114-
// linuxMotherboardSerial retrieves motherboard serial number from DMI
114+
// linuxMotherboardSerial retrieves motherboard serial number from DMI.
115115
func linuxMotherboardSerial(logger *slog.Logger) (string, error) {
116116
locations := []string{
117117
"/sys/class/dmi/id/board_serial",
@@ -121,7 +121,7 @@ func linuxMotherboardSerial(logger *slog.Logger) (string, error) {
121121
return readFirstValidFromLocations(locations, isValidSerial, logger)
122122
}
123123

124-
// linuxMachineID retrieves systemd machine ID
124+
// linuxMachineID retrieves systemd machine ID.
125125
func linuxMachineID(logger *slog.Logger) (string, error) {
126126
locations := []string{
127127
"/etc/machine-id",
@@ -131,7 +131,7 @@ func linuxMachineID(logger *slog.Logger) (string, error) {
131131
return readFirstValidFromLocations(locations, isNonEmpty, logger)
132132
}
133133

134-
// readFirstValidFromLocations reads from multiple locations until valid value found
134+
// readFirstValidFromLocations reads from multiple locations until a valid value is found.
135135
func readFirstValidFromLocations(locations []string, validator func(string) bool, logger *slog.Logger) (string, error) {
136136
for _, location := range locations {
137137
data, err := os.ReadFile(location)
@@ -156,17 +156,17 @@ func readFirstValidFromLocations(locations []string, validator func(string) bool
156156
return "", ErrNotFound
157157
}
158158

159-
// isValidUUID checks if UUID is valid (not empty or null)
159+
// isValidUUID reports whether the UUID is valid (not empty or null).
160160
func isValidUUID(uuid string) bool {
161161
return uuid != "" && uuid != "00000000-0000-0000-0000-000000000000"
162162
}
163163

164-
// isValidSerial checks if serial is valid (not empty or placeholder)
164+
// isValidSerial reports whether the serial is valid (not empty or placeholder).
165165
func isValidSerial(serial string) bool {
166166
return serial != "" && serial != biosFirmwareMessage
167167
}
168168

169-
// isNonEmpty checks if value is not empty
169+
// isNonEmpty reports whether the value is not empty.
170170
func isNonEmpty(value string) bool {
171171
return value != ""
172172
}
@@ -232,7 +232,7 @@ func linuxDiskSerialsLSBLK(ctx context.Context, executor CommandExecutor, logger
232232
return serials, nil
233233
}
234234

235-
// linuxDiskSerialsSys retrieves disk serials from /sys/block
235+
// linuxDiskSerialsSys retrieves disk serials from /sys/block.
236236
func linuxDiskSerialsSys(logger *slog.Logger) ([]string, error) {
237237
var serials []string
238238

0 commit comments

Comments
 (0)