mirror of
https://github.com/juanfont/headscale.git
synced 2026-03-26 11:21:27 +01:00
Add hscontrol/util/zlog package with: - zf subpackage: field name constants for compile-time safety - SafeHostinfo: wrapper that redacts device fingerprinting data - SafeMapRequest: wrapper that redacts client endpoints The zf (zerolog fields) subpackage provides short constant names (e.g., zf.NodeID instead of inline "node.id" strings) ensuring consistent field naming across all log statements. Security considerations: - SafeHostinfo never logs: OSVersion, DeviceModel, DistroName - SafeMapRequest only logs endpoint counts, not actual IPs
228 lines
5.4 KiB
Go
228 lines
5.4 KiB
Go
package zlog
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/netip"
|
|
"testing"
|
|
|
|
"github.com/juanfont/headscale/hscontrol/util/zlog/zf"
|
|
"github.com/rs/zerolog"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"tailscale.com/tailcfg"
|
|
"tailscale.com/types/key"
|
|
)
|
|
|
|
func TestSafeHostinfo_MarshalZerologObject(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
hostinfo *tailcfg.Hostinfo
|
|
wantFields map[string]any
|
|
wantAbsent []string // Fields that should NOT be present
|
|
}{
|
|
{
|
|
name: "nil hostinfo",
|
|
hostinfo: nil,
|
|
wantFields: map[string]any{},
|
|
},
|
|
{
|
|
name: "basic hostinfo",
|
|
hostinfo: &tailcfg.Hostinfo{
|
|
Hostname: "myhost",
|
|
OS: "linux",
|
|
},
|
|
wantFields: map[string]any{
|
|
zf.Hostname: "myhost",
|
|
zf.OS: "linux",
|
|
},
|
|
},
|
|
{
|
|
name: "hostinfo with routes and tags",
|
|
hostinfo: &tailcfg.Hostinfo{
|
|
Hostname: "router",
|
|
OS: "linux",
|
|
RoutableIPs: []netip.Prefix{netip.MustParsePrefix("10.0.0.0/24")},
|
|
RequestTags: []string{"tag:server"},
|
|
},
|
|
wantFields: map[string]any{
|
|
zf.Hostname: "router",
|
|
zf.OS: "linux",
|
|
zf.RoutableIPCount: float64(1),
|
|
},
|
|
},
|
|
{
|
|
name: "hostinfo with netinfo",
|
|
hostinfo: &tailcfg.Hostinfo{
|
|
Hostname: "myhost",
|
|
OS: "windows",
|
|
NetInfo: &tailcfg.NetInfo{
|
|
PreferredDERP: 1,
|
|
},
|
|
},
|
|
wantFields: map[string]any{
|
|
zf.Hostname: "myhost",
|
|
zf.OS: "windows",
|
|
zf.DERP: float64(1),
|
|
},
|
|
},
|
|
{
|
|
name: "sensitive fields are NOT logged",
|
|
hostinfo: &tailcfg.Hostinfo{
|
|
Hostname: "myhost",
|
|
OS: "linux",
|
|
OSVersion: "5.15.0-generic", // Should NOT be logged
|
|
DeviceModel: "ThinkPad X1", // Should NOT be logged
|
|
IPNVersion: "1.50.0", // Should NOT be logged
|
|
},
|
|
wantFields: map[string]any{
|
|
zf.Hostname: "myhost",
|
|
zf.OS: "linux",
|
|
},
|
|
wantAbsent: []string{"os_version", "device_model", "ipn_version", "OSVersion", "DeviceModel", "IPNVersion"},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
|
|
log := zerolog.New(&buf)
|
|
|
|
log.Info().EmbedObject(Hostinfo(tt.hostinfo)).Msg("test")
|
|
|
|
var result map[string]any
|
|
|
|
err := json.Unmarshal(buf.Bytes(), &result)
|
|
require.NoError(t, err)
|
|
|
|
// Check expected fields are present
|
|
for key, wantVal := range tt.wantFields {
|
|
assert.Equal(t, wantVal, result[key], "field %s", key)
|
|
}
|
|
|
|
// Check sensitive fields are absent
|
|
for _, key := range tt.wantAbsent {
|
|
_, exists := result[key]
|
|
assert.False(t, exists, "sensitive field %s should not be logged", key)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSafeMapRequest_MarshalZerologObject(t *testing.T) {
|
|
nodeKey := key.NewNode().Public()
|
|
|
|
tests := []struct {
|
|
name string
|
|
req *tailcfg.MapRequest
|
|
wantFields map[string]any
|
|
wantAbsent []string
|
|
}{
|
|
{
|
|
name: "nil request",
|
|
req: nil,
|
|
wantFields: map[string]any{},
|
|
},
|
|
{
|
|
name: "basic request",
|
|
req: &tailcfg.MapRequest{
|
|
Stream: true,
|
|
OmitPeers: false,
|
|
Version: 100,
|
|
NodeKey: nodeKey,
|
|
},
|
|
wantFields: map[string]any{
|
|
zf.Stream: true,
|
|
zf.OmitPeers: false,
|
|
zf.Version: float64(100),
|
|
},
|
|
},
|
|
{
|
|
name: "request with endpoints - only count logged",
|
|
req: &tailcfg.MapRequest{
|
|
Stream: false,
|
|
OmitPeers: true,
|
|
Version: 100,
|
|
NodeKey: nodeKey,
|
|
Endpoints: []netip.AddrPort{
|
|
netip.MustParseAddrPort("192.168.1.100:41641"),
|
|
netip.MustParseAddrPort("10.0.0.50:41641"),
|
|
},
|
|
},
|
|
wantFields: map[string]any{
|
|
zf.Stream: false,
|
|
zf.OmitPeers: true,
|
|
zf.EndpointsCount: float64(2),
|
|
},
|
|
wantAbsent: []string{"endpoints", "Endpoints"},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
|
|
log := zerolog.New(&buf)
|
|
|
|
log.Info().EmbedObject(MapRequest(tt.req)).Msg("test")
|
|
|
|
var result map[string]any
|
|
|
|
err := json.Unmarshal(buf.Bytes(), &result)
|
|
require.NoError(t, err)
|
|
|
|
// Check expected fields are present
|
|
for key, wantVal := range tt.wantFields {
|
|
assert.Equal(t, wantVal, result[key], "field %s", key)
|
|
}
|
|
|
|
// Check node.key is a short string (not full key)
|
|
if tt.req != nil {
|
|
nodeKeyStr, ok := result[zf.NodeKey].(string)
|
|
if ok {
|
|
// Short keys are truncated, full keys are 64+ chars
|
|
assert.Less(t, len(nodeKeyStr), 20, "node key should be short form")
|
|
}
|
|
}
|
|
|
|
// Check sensitive fields are absent
|
|
for _, key := range tt.wantAbsent {
|
|
_, exists := result[key]
|
|
assert.False(t, exists, "sensitive field %s should not be logged", key)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFieldConstants(t *testing.T) {
|
|
// Verify field constants follow the expected naming pattern
|
|
fieldTests := []struct {
|
|
constant string
|
|
expected string
|
|
}{
|
|
{zf.NodeID, "node.id"},
|
|
{zf.NodeName, "node.name"},
|
|
{zf.NodeKey, "node.key"},
|
|
{zf.MachineKey, "machine.key"},
|
|
{zf.NodeTags, "node.tags"},
|
|
{zf.NodeIsTagged, "node.is_tagged"},
|
|
{zf.NodeOnline, "node.online"},
|
|
{zf.NodeExpired, "node.expired"},
|
|
{zf.UserID, "user.id"},
|
|
{zf.UserName, "user.name"},
|
|
{zf.PAKID, "pak.id"},
|
|
{zf.PAKPrefix, "pak.prefix"},
|
|
{zf.APIKeyID, "api_key.id"},
|
|
{zf.APIKeyPrefix, "api_key.prefix"},
|
|
{zf.OmitPeers, "omit_peers"},
|
|
{zf.Stream, "stream"},
|
|
}
|
|
|
|
for _, tt := range fieldTests {
|
|
t.Run(tt.expected, func(t *testing.T) {
|
|
assert.Equal(t, tt.expected, tt.constant)
|
|
})
|
|
}
|
|
}
|