Skip to content
Open
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
6 changes: 3 additions & 3 deletions verification/api/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,12 @@ var (
// sequence and current time. Routing to the correct node can therefore happen
// based on the NodeID part of the UUID (i.e., octets 10-15).
func mintSessionID() (uuid.UUID, error) {
mid, err := machineid.ID()
nodeID, err := getNodeID()
if err != nil {
return uuid.UUID{}, err
return uuid.UUID{}, fmt.Errorf("failed to get node ID: %v", err)
}

uuid.SetNodeID([]byte(mid))
uuid.SetNodeID(nodeID)

return uuid.NewUUID()
}
Expand Down
164 changes: 164 additions & 0 deletions verification/api/nodeid.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// Copyright 2025 Contributors to the Veraison project.
// SPDX-License-Identifier: Apache-2.0

package api

import (
"crypto/rand"
"encoding/hex"
"fmt"
"io/ioutil"
"net"
"os"
"path/filepath"
"strings"

"github.com/veraison/services/log"
)

const (
nodeIDLength = 6 // bytes, as required by UUID v1
nodeIDFileName = "veraison-node-id"
)

// getNodeID returns a unique identifier for this node. It tries multiple methods
// in order of preference:
// 1. Read from a persistent node ID file (if exists)
// 2. Use MAC address from the first available non-loopback interface
// 3. Use machine-id if available (fallback for systemd systems)
// 4. Generate a random node ID and persist it
func getNodeID() ([]byte, error) {
// Try reading from our persistent node ID file
if id, err := readPersistedNodeID(); err == nil {
log.Debug("using persisted node ID")
return id, nil
}

// Try getting MAC address
if id, err := getMACBasedID(); err == nil {
log.Debug("using MAC-based node ID")
if err := persistNodeID(id); err != nil {
log.Warnf("failed to persist node ID: %v", err)
}
return id, nil
}

// Try machine-id as fallback for systemd systems
if id, err := getMachineID(); err == nil {
log.Debug("using machine-id based node ID")
if err := persistNodeID(id); err != nil {
log.Warnf("failed to persist node ID: %v", err)
}
return id, nil
}

// Generate random ID as last resort
id, err := generateRandomNodeID()
if err != nil {
return nil, fmt.Errorf("failed to generate random node ID: %v", err)
}

log.Debug("using generated random node ID")
if err := persistNodeID(id); err != nil {
log.Warnf("failed to persist node ID: %v", err)
}

return id, nil
}

// readPersistedNodeID attempts to read the node ID from a persistent file
func readPersistedNodeID() ([]byte, error) {
dir := getNodeIDDir()
path := filepath.Join(dir, nodeIDFileName)

data, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}

if len(data) != nodeIDLength*2 { // hex encoded
return nil, fmt.Errorf("invalid node ID length in file")
}

return hex.DecodeString(string(data))
}

// persistNodeID saves the node ID to a persistent file
func persistNodeID(id []byte) error {
dir := getNodeIDDir()
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}

path := filepath.Join(dir, nodeIDFileName)
return ioutil.WriteFile(path, []byte(hex.EncodeToString(id)), 0644)
}

// getMACBasedID returns a node ID based on the MAC address of the first
// available non-loopback interface
func getMACBasedID() ([]byte, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am hesitant about this.

  • The order of inteface may change (e.g. if someone adds a PCI network card to their machine).
  • In virtualized environments, MAC cannot be rilied on to be sufficiently unique.

I think might be better to remove this.

ifaces, err := net.Interfaces()
if err != nil {
return nil, err
}

for _, iface := range ifaces {
if iface.Flags&net.FlagLoopback != 0 {
continue
}
if iface.Flags&net.FlagUp == 0 {
continue
}
if len(iface.HardwareAddr) < nodeIDLength {
continue
}
return iface.HardwareAddr[:nodeIDLength], nil
}

return nil, fmt.Errorf("no suitable network interface found")
}

// getMachineID attempts to read the systemd machine-id
func getMachineID() ([]byte, error) {
files := []string{"/etc/machine-id", "/var/lib/dbus/machine-id"}
var id string

for _, file := range files {
if data, err := ioutil.ReadFile(file); err == nil {
id = strings.TrimSpace(string(data))
break
}
}

if id == "" {
return nil, fmt.Errorf("no machine-id found")
}

// Use first 6 bytes of machine-id hash
decoded, err := hex.DecodeString(id)
if err != nil {
return nil, fmt.Errorf("invalid machine-id format: %v", err)
}

return decoded[:nodeIDLength], nil
}

// generateRandomNodeID creates a random node ID
func generateRandomNodeID() ([]byte, error) {
id := make([]byte, nodeIDLength)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we already depend on github.com/google/uuid, instead of doing this, you could just do uuid.New()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok sir @setrofim will think about it

_, err := rand.Read(id)
if err != nil {
return nil, err
}
// Set multicast bit as per RFC 4122
id[0] |= 0x01
return id, nil
}

// getNodeIDDir returns the directory where the node ID file should be stored
func getNodeIDDir() string {
if dir := os.Getenv("VERAISON_NODE_ID_DIR"); dir != "" {
return dir
}
return "/var/lib/veraison"
}
71 changes: 71 additions & 0 deletions verification/api/nodeid_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright 2025 Contributors to the Veraison project.
// SPDX-License-Identifier: Apache-2.0

package api

import (
"encoding/hex"
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestGetNodeID(t *testing.T) {
// Set up a temporary directory for testing
tmpDir := t.TempDir()
os.Setenv("VERAISON_NODE_ID_DIR", tmpDir)
defer os.Unsetenv("VERAISON_NODE_ID_DIR")

// First call should generate and persist a node ID
id1, err := getNodeID()
require.NoError(t, err)
require.Len(t, id1, nodeIDLength)

// Second call should read the same persisted ID
id2, err := getNodeID()
require.NoError(t, err)
assert.Equal(t, id1, id2)

// Verify file contents
data, err := os.ReadFile(filepath.Join(tmpDir, nodeIDFileName))
require.NoError(t, err)
decoded, err := hex.DecodeString(string(data))
require.NoError(t, err)
assert.Equal(t, id1, decoded)
}

func TestGenerateRandomNodeID(t *testing.T) {
id, err := generateRandomNodeID()
require.NoError(t, err)
require.Len(t, id, nodeIDLength)
// Check multicast bit is set
assert.True(t, id[0]&0x01 == 0x01)

// Generate another to ensure they're different
id2, err := generateRandomNodeID()
require.NoError(t, err)
assert.NotEqual(t, id, id2)
}

func TestGetMACBasedID(t *testing.T) {
// This test might be skipped if no suitable interface is found
id, err := getMACBasedID()
if err != nil {
t.Skip("No suitable network interface found for testing")
}
require.Len(t, id, nodeIDLength)
}

func TestGetNodeIDDirDefault(t *testing.T) {
os.Unsetenv("VERAISON_NODE_ID_DIR")
assert.Equal(t, "/var/lib/veraison", getNodeIDDir())
}

func TestGetNodeIDDirCustom(t *testing.T) {
os.Setenv("VERAISON_NODE_ID_DIR", "/custom/path")
defer os.Unsetenv("VERAISON_NODE_ID_DIR")
assert.Equal(t, "/custom/path", getNodeIDDir())
}