Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,12 +142,36 @@ provider := machineid.New().
WithCPU(). // processor ID and feature flags
WithMotherboard(). // motherboard serial number
WithSystemUUID(). // BIOS/UEFI system UUID
WithMAC(). // physical network interface MAC addresses
WithMAC(). // physical network interface MAC addresses (default filter)

WithDisk() // internal disk serial numbers

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

### MAC Address Filtering

Control which network interfaces are included in the machine ID using `MACFilter`:

```go
ctx := context.Background()

// Physical interfaces only (default, most stable for bare-metal)
id, _ := machineid.New().WithCPU().WithMAC().ID(ctx)

// All interfaces including virtual (VPN, Docker, bridges)
id, _ = machineid.New().WithCPU().WithMAC(machineid.MACFilterAll).ID(ctx)

// Only virtual interfaces (useful for container-specific fingerprinting)
id, _ = machineid.New().WithCPU().WithMAC(machineid.MACFilterVirtual).ID(ctx)
```

| Filter | Interfaces Included | Best For |
|---------------------|--------------------------------------------------------|--------------------------|
| `MACFilterPhysical` | `en0`, `eth0`, `wlan0` (default) | Bare-metal stability |
| `MACFilterAll` | Physical + virtual (`docker0`, `utun`, `bridge`, etc.) | Maximum uniqueness |
| `MACFilterVirtual` | `docker0`, `utun`, `bridge0`, `veth`, `vmnet`, etc. | Container fingerprinting |

### Output Formats

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

# Include only physical MACs (default)
machineid -mac -mac-filter physical

# Include all MACs (physical + virtual)
machineid -all -mac-filter all

# Debug-level logging (command details, raw values, timing)
machineid -all -debug

Expand All @@ -354,6 +384,7 @@ machineid -version.long
| `-motherboard` | Include motherboard serial number |
| `-uuid` | Include system UUID |
| `-mac` | Include network MAC addresses |
| `-mac-filter F` | MAC filter: `physical` (default), `all`, or `virtual` |
| `-disk` | Include disk serial numbers |
| `-all` | Include all hardware identifiers |
| `-vm` | VM-friendly mode (CPU + UUID only) |
Expand Down
25 changes: 23 additions & 2 deletions cmd/machineid/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ func main() {
motherboard := flag.Bool("motherboard", false, "Include motherboard serial number")
uuid := flag.Bool("uuid", false, "Include system UUID")
mac := flag.Bool("mac", false, "Include network MAC addresses")
macFilterFlag := flag.String("mac-filter", "physical", "MAC filter: physical, all, virtual")
disk := flag.Bool("disk", false, "Include disk serial numbers")
all := flag.Bool("all", false, "Include all hardware identifiers")
vm := flag.Bool("vm", false, "Use VM-friendly mode (CPU + UUID only)")
Expand Down Expand Up @@ -134,11 +135,18 @@ func main() {
provider.WithSalt(*salt)
}

mFilter, err := parseMACFilter(*macFilterFlag)
if err != nil {
slog.Error("invalid mac-filter", "error", err)
flag.Usage()
os.Exit(1)
}

switch {
case *vm:
provider.VMFriendly()
case *all:
provider.WithCPU().WithMotherboard().WithSystemUUID().WithMAC().WithDisk()
provider.WithCPU().WithMotherboard().WithSystemUUID().WithMAC(mFilter).WithDisk()
default:
if !*cpu && !*motherboard && !*uuid && !*mac && !*disk {
// Default: CPU + Motherboard + System UUID
Expand All @@ -154,7 +162,7 @@ func main() {
provider.WithSystemUUID()
}
if *mac {
provider.WithMAC()
provider.WithMAC(mFilter)
}
if *disk {
provider.WithDisk()
Expand Down Expand Up @@ -213,6 +221,19 @@ func parseFormatMode(format int) (machineid.FormatMode, error) {
}
}

func parseMACFilter(value string) (machineid.MACFilter, error) {
switch strings.ToLower(value) {
case "physical":
return machineid.MACFilterPhysical, nil
case "all":
return machineid.MACFilterAll, nil
case "virtual":
return machineid.MACFilterVirtual, nil
default:
return 0, fmt.Errorf("unsupported mac-filter %q; valid values are physical, all, virtual", value)
}
}

func handleValidate(ctx context.Context, provider *machineid.Provider, expectedID string, jsonOut bool) {
valid, err := provider.Validate(ctx, expectedID)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func collectIdentifiers(ctx context.Context, p *Provider, diag *DiagnosticInfo)

if p.includeMAC {
identifiers = appendIdentifiersIfValid(identifiers, func() ([]string, error) {
return collectMACAddresses(logger)
return collectMACAddresses(p.macFilter, logger)
}, "mac:", diag, ComponentMAC, logger)
}

Expand Down
2 changes: 1 addition & 1 deletion darwin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -635,7 +635,7 @@ func TestCollectMACAddressesWithLogger(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug}))

macs, err := collectMACAddresses(logger)
macs, err := collectMACAddresses(MACFilterPhysical, logger)
if err != nil {
t.Logf("collectMACAddresses error (may be expected): %v", err)
return
Expand Down
23 changes: 22 additions & 1 deletion doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,32 @@
// - [Provider.WithCPU] — processor identifier and feature flags
// - [Provider.WithMotherboard] — motherboard / baseboard serial number
// - [Provider.WithSystemUUID] — BIOS / UEFI system UUID
// - [Provider.WithMAC] — MAC addresses of physical network interfaces
// - [Provider.WithMAC] — MAC addresses of network interfaces (filterable)
// - [Provider.WithDisk] — serial numbers of internal disks
//
// Or use [Provider.VMFriendly] to select a minimal, virtual-machine-safe
// subset (CPU + System UUID).
//
// # MAC Address Filtering
//
// [Provider.WithMAC] accepts an optional [MACFilter] to control which network
// interfaces contribute to the machine ID:
//
// - [MACFilterPhysical] — only physical interfaces (default)
// - [MACFilterAll] — all non-loopback, up interfaces (physical + virtual)
// - [MACFilterVirtual] — only virtual interfaces (VPN, bridge, container)
//
// Examples:
//
// // Physical interfaces only (default, most stable)
// provider.WithMAC()
//
// // Include all interfaces
// provider.WithMAC(machineid.MACFilterAll)
//
// // Only virtual interfaces (containers, VPNs)
// provider.WithMAC(machineid.MACFilterVirtual)
//
// # Output Formats
//
// Set the output length with [Provider.WithFormat]:
Expand Down Expand Up @@ -159,6 +179,7 @@
// machineid -cpu -uuid
// machineid -all -format 32 -json
// machineid -vm -salt "my-app" -diagnostics
// machineid -mac -mac-filter all
// machineid -cpu -uuid -verbose
// machineid -all -debug
// machineid -version
Expand Down
22 changes: 11 additions & 11 deletions linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func collectIdentifiers(ctx context.Context, p *Provider, diag *DiagnosticInfo)

if p.includeMAC {
identifiers = appendIdentifiersIfValid(identifiers, func() ([]string, error) {
return collectMACAddresses(logger)
return collectMACAddresses(p.macFilter, logger)
}, "mac:", diag, ComponentMAC, logger)
}

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

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

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

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

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

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

// linuxMachineID retrieves systemd machine ID
// linuxMachineID retrieves systemd machine ID.
func linuxMachineID(logger *slog.Logger) (string, error) {
locations := []string{
"/etc/machine-id",
Expand All @@ -131,7 +131,7 @@ func linuxMachineID(logger *slog.Logger) (string, error) {
return readFirstValidFromLocations(locations, isNonEmpty, logger)
}

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

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

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

// isNonEmpty checks if value is not empty
// isNonEmpty reports whether the value is not empty.
func isNonEmpty(value string) bool {
return value != ""
}
Expand Down Expand Up @@ -232,7 +232,7 @@ func linuxDiskSerialsLSBLK(ctx context.Context, executor CommandExecutor, logger
return serials, nil
}

// linuxDiskSerialsSys retrieves disk serials from /sys/block
// linuxDiskSerialsSys retrieves disk serials from /sys/block.
func linuxDiskSerialsSys(logger *slog.Logger) ([]string, error) {
var serials []string

Expand Down
Loading
Loading