mirror of
https://github.com/juanfont/headscale.git
synced 2025-11-01 21:47:45 +09:00
This PR addresses some consistency issues that was introduced or discovered with the nodestore. nodestore: Now returns the node that is being put or updated when it is finished. This closes a race condition where when we read it back, we do not necessarily get the node with the given change and it ensures we get all the other updates from that batch write. auth: Authentication paths have been unified and simplified. It removes a lot of bad branches and ensures we only do the minimal work. A comprehensive auth test set has been created so we do not have to run integration tests to validate auth and it has allowed us to generate test cases for all the branches we currently know of. integration: added a lot more tooling and checks to validate that nodes reach the expected state when they come up and down. Standardised between the different auth models. A lot of this is to support or detect issues in the changes to nodestore (races) and auth (inconsistencies after login and reaching correct state) This PR was assisted, particularly tests, by claude code.
1189 lines
31 KiB
Go
1189 lines
31 KiB
Go
package util
|
|
|
|
import (
|
|
"errors"
|
|
"net/netip"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"tailscale.com/tailcfg"
|
|
)
|
|
|
|
func TestTailscaleVersionNewerOrEqual(t *testing.T) {
|
|
type args struct {
|
|
minimum string
|
|
toCheck string
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
want bool
|
|
}{
|
|
{
|
|
name: "is-equal",
|
|
args: args{
|
|
minimum: "1.56",
|
|
toCheck: "1.56",
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "is-newer-head",
|
|
args: args{
|
|
minimum: "1.56",
|
|
toCheck: "head",
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "is-newer-unstable",
|
|
args: args{
|
|
minimum: "1.56",
|
|
toCheck: "unstable",
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "is-newer-patch",
|
|
args: args{
|
|
minimum: "1.56.1",
|
|
toCheck: "1.56.1",
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "is-older-patch-same-minor",
|
|
args: args{
|
|
minimum: "1.56.1",
|
|
toCheck: "1.56.0",
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "is-older-unstable",
|
|
args: args{
|
|
minimum: "1.56",
|
|
toCheck: "1.55",
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "is-older-one-stable",
|
|
args: args{
|
|
minimum: "1.56",
|
|
toCheck: "1.54",
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "is-older-five-stable",
|
|
args: args{
|
|
minimum: "1.56",
|
|
toCheck: "1.46",
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "is-older-patch",
|
|
args: args{
|
|
minimum: "1.56",
|
|
toCheck: "1.48.1",
|
|
},
|
|
want: false,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := TailscaleVersionNewerOrEqual(tt.args.minimum, tt.args.toCheck); got != tt.want {
|
|
t.Errorf("TailscaleVersionNewerThan() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseLoginURLFromCLILogin(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
output string
|
|
wantURL string
|
|
wantErr string
|
|
}{
|
|
{
|
|
name: "valid https URL",
|
|
output: `
|
|
To authenticate, visit:
|
|
|
|
https://headscale.example.com/register/3oYCOZYA2zZmGB4PQ7aHBaMi
|
|
|
|
Success.`,
|
|
wantURL: "https://headscale.example.com/register/3oYCOZYA2zZmGB4PQ7aHBaMi",
|
|
wantErr: "",
|
|
},
|
|
{
|
|
name: "valid http URL",
|
|
output: `
|
|
To authenticate, visit:
|
|
|
|
http://headscale.example.com/register/3oYCOZYA2zZmGB4PQ7aHBaMi
|
|
|
|
Success.`,
|
|
wantURL: "http://headscale.example.com/register/3oYCOZYA2zZmGB4PQ7aHBaMi",
|
|
wantErr: "",
|
|
},
|
|
{
|
|
name: "no URL",
|
|
output: `
|
|
To authenticate, visit:
|
|
|
|
Success.`,
|
|
wantURL: "",
|
|
wantErr: "no URL found",
|
|
},
|
|
{
|
|
name: "multiple URLs",
|
|
output: `
|
|
To authenticate, visit:
|
|
|
|
https://headscale.example.com/register/3oYCOZYA2zZmGB4PQ7aHBaMi
|
|
|
|
To authenticate, visit:
|
|
|
|
http://headscale.example.com/register/dv1l2k5FackOYl-7-V3mSd_E
|
|
|
|
Success.`,
|
|
wantURL: "",
|
|
wantErr: "multiple URLs found: https://headscale.example.com/register/3oYCOZYA2zZmGB4PQ7aHBaMi and http://headscale.example.com/register/dv1l2k5FackOYl-7-V3mSd_E",
|
|
},
|
|
{
|
|
name: "invalid URL",
|
|
output: `
|
|
To authenticate, visit:
|
|
|
|
invalid-url
|
|
|
|
Success.`,
|
|
wantURL: "",
|
|
wantErr: "no URL found",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
gotURL, err := ParseLoginURLFromCLILogin(tt.output)
|
|
if tt.wantErr != "" {
|
|
if err == nil || err.Error() != tt.wantErr {
|
|
t.Errorf("ParseLoginURLFromCLILogin() error = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Errorf("ParseLoginURLFromCLILogin() error = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
if gotURL.String() != tt.wantURL {
|
|
t.Errorf("ParseLoginURLFromCLILogin() = %v, want %v", gotURL, tt.wantURL)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseTraceroute(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
want Traceroute
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "simple successful traceroute",
|
|
input: `traceroute to 172.24.0.3 (172.24.0.3), 30 hops max, 46 byte packets
|
|
1 ts-head-hk0urr.headscale.net (100.64.0.1) 1.135 ms 0.922 ms 0.619 ms
|
|
2 172.24.0.3 (172.24.0.3) 0.593 ms 0.549 ms 0.522 ms`,
|
|
want: Traceroute{
|
|
Hostname: "172.24.0.3",
|
|
IP: netip.MustParseAddr("172.24.0.3"),
|
|
Route: []TraceroutePath{
|
|
{
|
|
Hop: 1,
|
|
Hostname: "ts-head-hk0urr.headscale.net",
|
|
IP: netip.MustParseAddr("100.64.0.1"),
|
|
Latencies: []time.Duration{
|
|
1135 * time.Microsecond,
|
|
922 * time.Microsecond,
|
|
619 * time.Microsecond,
|
|
},
|
|
},
|
|
{
|
|
Hop: 2,
|
|
Hostname: "172.24.0.3",
|
|
IP: netip.MustParseAddr("172.24.0.3"),
|
|
Latencies: []time.Duration{
|
|
593 * time.Microsecond,
|
|
549 * time.Microsecond,
|
|
522 * time.Microsecond,
|
|
},
|
|
},
|
|
},
|
|
Success: true,
|
|
Err: nil,
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "traceroute with timeouts",
|
|
input: `traceroute to 8.8.8.8 (8.8.8.8), 30 hops max, 60 byte packets
|
|
1 router.local (192.168.1.1) 1.234 ms 1.123 ms 1.121 ms
|
|
2 * * *
|
|
3 isp-gateway.net (10.0.0.1) 15.678 ms 14.789 ms 15.432 ms
|
|
4 8.8.8.8 (8.8.8.8) 20.123 ms 19.876 ms 20.345 ms`,
|
|
want: Traceroute{
|
|
Hostname: "8.8.8.8",
|
|
IP: netip.MustParseAddr("8.8.8.8"),
|
|
Route: []TraceroutePath{
|
|
{
|
|
Hop: 1,
|
|
Hostname: "router.local",
|
|
IP: netip.MustParseAddr("192.168.1.1"),
|
|
Latencies: []time.Duration{
|
|
1234 * time.Microsecond,
|
|
1123 * time.Microsecond,
|
|
1121 * time.Microsecond,
|
|
},
|
|
},
|
|
{
|
|
Hop: 2,
|
|
Hostname: "*",
|
|
},
|
|
{
|
|
Hop: 3,
|
|
Hostname: "isp-gateway.net",
|
|
IP: netip.MustParseAddr("10.0.0.1"),
|
|
Latencies: []time.Duration{
|
|
15678 * time.Microsecond,
|
|
14789 * time.Microsecond,
|
|
15432 * time.Microsecond,
|
|
},
|
|
},
|
|
{
|
|
Hop: 4,
|
|
Hostname: "8.8.8.8",
|
|
IP: netip.MustParseAddr("8.8.8.8"),
|
|
Latencies: []time.Duration{
|
|
20123 * time.Microsecond,
|
|
19876 * time.Microsecond,
|
|
20345 * time.Microsecond,
|
|
},
|
|
},
|
|
},
|
|
Success: true,
|
|
Err: nil,
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "unsuccessful traceroute",
|
|
input: `traceroute to 10.0.0.99 (10.0.0.99), 5 hops max, 60 byte packets
|
|
1 router.local (192.168.1.1) 1.234 ms 1.123 ms 1.121 ms
|
|
2 * * *
|
|
3 * * *
|
|
4 * * *
|
|
5 * * *`,
|
|
want: Traceroute{
|
|
Hostname: "10.0.0.99",
|
|
IP: netip.MustParseAddr("10.0.0.99"),
|
|
Route: []TraceroutePath{
|
|
{
|
|
Hop: 1,
|
|
Hostname: "router.local",
|
|
IP: netip.MustParseAddr("192.168.1.1"),
|
|
Latencies: []time.Duration{
|
|
1234 * time.Microsecond,
|
|
1123 * time.Microsecond,
|
|
1121 * time.Microsecond,
|
|
},
|
|
},
|
|
{
|
|
Hop: 2,
|
|
Hostname: "*",
|
|
},
|
|
{
|
|
Hop: 3,
|
|
Hostname: "*",
|
|
},
|
|
{
|
|
Hop: 4,
|
|
Hostname: "*",
|
|
},
|
|
{
|
|
Hop: 5,
|
|
Hostname: "*",
|
|
},
|
|
},
|
|
Success: false,
|
|
Err: errors.New("traceroute did not reach target"),
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "empty input",
|
|
input: "",
|
|
want: Traceroute{},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "invalid header",
|
|
input: "not a valid traceroute output",
|
|
want: Traceroute{},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "windows tracert format",
|
|
input: `Tracing route to google.com [8.8.8.8]
|
|
over a maximum of 30 hops:
|
|
|
|
1 <1 ms <1 ms <1 ms router.local [192.168.1.1]
|
|
2 5 ms 4 ms 5 ms 10.0.0.1
|
|
3 * * * Request timed out.
|
|
4 20 ms 19 ms 21 ms 8.8.8.8`,
|
|
want: Traceroute{
|
|
Hostname: "google.com",
|
|
IP: netip.MustParseAddr("8.8.8.8"),
|
|
Route: []TraceroutePath{
|
|
{
|
|
Hop: 1,
|
|
Hostname: "router.local",
|
|
IP: netip.MustParseAddr("192.168.1.1"),
|
|
Latencies: []time.Duration{
|
|
1 * time.Millisecond,
|
|
1 * time.Millisecond,
|
|
1 * time.Millisecond,
|
|
},
|
|
},
|
|
{
|
|
Hop: 2,
|
|
Hostname: "10.0.0.1",
|
|
IP: netip.MustParseAddr("10.0.0.1"),
|
|
Latencies: []time.Duration{
|
|
5 * time.Millisecond,
|
|
4 * time.Millisecond,
|
|
5 * time.Millisecond,
|
|
},
|
|
},
|
|
{
|
|
Hop: 3,
|
|
Hostname: "*",
|
|
},
|
|
{
|
|
Hop: 4,
|
|
Hostname: "8.8.8.8",
|
|
IP: netip.MustParseAddr("8.8.8.8"),
|
|
Latencies: []time.Duration{
|
|
20 * time.Millisecond,
|
|
19 * time.Millisecond,
|
|
21 * time.Millisecond,
|
|
},
|
|
},
|
|
},
|
|
Success: true,
|
|
Err: nil,
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "mixed latency formats",
|
|
input: `traceroute to 192.168.1.1 (192.168.1.1), 30 hops max, 60 byte packets
|
|
1 gateway (192.168.1.1) 0.5 ms * 0.4 ms`,
|
|
want: Traceroute{
|
|
Hostname: "192.168.1.1",
|
|
IP: netip.MustParseAddr("192.168.1.1"),
|
|
Route: []TraceroutePath{
|
|
{
|
|
Hop: 1,
|
|
Hostname: "gateway",
|
|
IP: netip.MustParseAddr("192.168.1.1"),
|
|
Latencies: []time.Duration{
|
|
500 * time.Microsecond,
|
|
400 * time.Microsecond,
|
|
},
|
|
},
|
|
},
|
|
Success: true,
|
|
Err: nil,
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "only one latency value",
|
|
input: `traceroute to 10.0.0.1 (10.0.0.1), 30 hops max, 60 byte packets
|
|
1 10.0.0.1 (10.0.0.1) 1.5 ms`,
|
|
want: Traceroute{
|
|
Hostname: "10.0.0.1",
|
|
IP: netip.MustParseAddr("10.0.0.1"),
|
|
Route: []TraceroutePath{
|
|
{
|
|
Hop: 1,
|
|
Hostname: "10.0.0.1",
|
|
IP: netip.MustParseAddr("10.0.0.1"),
|
|
Latencies: []time.Duration{
|
|
1500 * time.Microsecond,
|
|
},
|
|
},
|
|
},
|
|
Success: true,
|
|
Err: nil,
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "backward compatibility - original format with 3 latencies",
|
|
input: `traceroute to 172.24.0.3 (172.24.0.3), 30 hops max, 46 byte packets
|
|
1 ts-head-hk0urr.headscale.net (100.64.0.1) 1.135 ms 0.922 ms 0.619 ms
|
|
2 172.24.0.3 (172.24.0.3) 0.593 ms 0.549 ms 0.522 ms`,
|
|
want: Traceroute{
|
|
Hostname: "172.24.0.3",
|
|
IP: netip.MustParseAddr("172.24.0.3"),
|
|
Route: []TraceroutePath{
|
|
{
|
|
Hop: 1,
|
|
Hostname: "ts-head-hk0urr.headscale.net",
|
|
IP: netip.MustParseAddr("100.64.0.1"),
|
|
Latencies: []time.Duration{
|
|
1135 * time.Microsecond,
|
|
922 * time.Microsecond,
|
|
619 * time.Microsecond,
|
|
},
|
|
},
|
|
{
|
|
Hop: 2,
|
|
Hostname: "172.24.0.3",
|
|
IP: netip.MustParseAddr("172.24.0.3"),
|
|
Latencies: []time.Duration{
|
|
593 * time.Microsecond,
|
|
549 * time.Microsecond,
|
|
522 * time.Microsecond,
|
|
},
|
|
},
|
|
},
|
|
Success: true,
|
|
Err: nil,
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "two latencies only - common on packet loss",
|
|
input: `traceroute to 8.8.8.8 (8.8.8.8), 30 hops max, 60 byte packets
|
|
1 gateway (192.168.1.1) 1.2 ms 1.1 ms`,
|
|
want: Traceroute{
|
|
Hostname: "8.8.8.8",
|
|
IP: netip.MustParseAddr("8.8.8.8"),
|
|
Route: []TraceroutePath{
|
|
{
|
|
Hop: 1,
|
|
Hostname: "gateway",
|
|
IP: netip.MustParseAddr("192.168.1.1"),
|
|
Latencies: []time.Duration{
|
|
1200 * time.Microsecond,
|
|
1100 * time.Microsecond,
|
|
},
|
|
},
|
|
},
|
|
Success: false,
|
|
Err: errors.New("traceroute did not reach target"),
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "hostname without parentheses - some traceroute versions",
|
|
input: `traceroute to 8.8.8.8 (8.8.8.8), 30 hops max, 60 byte packets
|
|
1 192.168.1.1 1.2 ms 1.1 ms 1.0 ms
|
|
2 8.8.8.8 20.1 ms 19.9 ms 20.2 ms`,
|
|
want: Traceroute{
|
|
Hostname: "8.8.8.8",
|
|
IP: netip.MustParseAddr("8.8.8.8"),
|
|
Route: []TraceroutePath{
|
|
{
|
|
Hop: 1,
|
|
Hostname: "192.168.1.1",
|
|
IP: netip.MustParseAddr("192.168.1.1"),
|
|
Latencies: []time.Duration{
|
|
1200 * time.Microsecond,
|
|
1100 * time.Microsecond,
|
|
1000 * time.Microsecond,
|
|
},
|
|
},
|
|
{
|
|
Hop: 2,
|
|
Hostname: "8.8.8.8",
|
|
IP: netip.MustParseAddr("8.8.8.8"),
|
|
Latencies: []time.Duration{
|
|
20100 * time.Microsecond,
|
|
19900 * time.Microsecond,
|
|
20200 * time.Microsecond,
|
|
},
|
|
},
|
|
},
|
|
Success: true,
|
|
Err: nil,
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "ipv6 traceroute",
|
|
input: `traceroute to 2001:4860:4860::8888 (2001:4860:4860::8888), 30 hops max, 80 byte packets
|
|
1 2001:db8::1 (2001:db8::1) 1.123 ms 1.045 ms 0.987 ms
|
|
2 2001:4860:4860::8888 (2001:4860:4860::8888) 15.234 ms 14.876 ms 15.123 ms`,
|
|
want: Traceroute{
|
|
Hostname: "2001:4860:4860::8888",
|
|
IP: netip.MustParseAddr("2001:4860:4860::8888"),
|
|
Route: []TraceroutePath{
|
|
{
|
|
Hop: 1,
|
|
Hostname: "2001:db8::1",
|
|
IP: netip.MustParseAddr("2001:db8::1"),
|
|
Latencies: []time.Duration{
|
|
1123 * time.Microsecond,
|
|
1045 * time.Microsecond,
|
|
987 * time.Microsecond,
|
|
},
|
|
},
|
|
{
|
|
Hop: 2,
|
|
Hostname: "2001:4860:4860::8888",
|
|
IP: netip.MustParseAddr("2001:4860:4860::8888"),
|
|
Latencies: []time.Duration{
|
|
15234 * time.Microsecond,
|
|
14876 * time.Microsecond,
|
|
15123 * time.Microsecond,
|
|
},
|
|
},
|
|
},
|
|
Success: true,
|
|
Err: nil,
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "macos traceroute with extra spacing",
|
|
input: `traceroute to google.com (8.8.8.8), 64 hops max, 52 byte packets
|
|
1 router.home (192.168.1.1) 2.345 ms 1.234 ms 1.567 ms
|
|
2 * * *
|
|
3 isp-gw.net (10.1.1.1) 15.234 ms 14.567 ms 15.890 ms
|
|
4 google.com (8.8.8.8) 20.123 ms 19.456 ms 20.789 ms`,
|
|
want: Traceroute{
|
|
Hostname: "google.com",
|
|
IP: netip.MustParseAddr("8.8.8.8"),
|
|
Route: []TraceroutePath{
|
|
{
|
|
Hop: 1,
|
|
Hostname: "router.home",
|
|
IP: netip.MustParseAddr("192.168.1.1"),
|
|
Latencies: []time.Duration{
|
|
2345 * time.Microsecond,
|
|
1234 * time.Microsecond,
|
|
1567 * time.Microsecond,
|
|
},
|
|
},
|
|
{
|
|
Hop: 2,
|
|
Hostname: "*",
|
|
},
|
|
{
|
|
Hop: 3,
|
|
Hostname: "isp-gw.net",
|
|
IP: netip.MustParseAddr("10.1.1.1"),
|
|
Latencies: []time.Duration{
|
|
15234 * time.Microsecond,
|
|
14567 * time.Microsecond,
|
|
15890 * time.Microsecond,
|
|
},
|
|
},
|
|
{
|
|
Hop: 4,
|
|
Hostname: "google.com",
|
|
IP: netip.MustParseAddr("8.8.8.8"),
|
|
Latencies: []time.Duration{
|
|
20123 * time.Microsecond,
|
|
19456 * time.Microsecond,
|
|
20789 * time.Microsecond,
|
|
},
|
|
},
|
|
},
|
|
Success: true,
|
|
Err: nil,
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "busybox traceroute minimal format",
|
|
input: `traceroute to 10.0.0.1 (10.0.0.1), 30 hops max, 38 byte packets
|
|
1 10.0.0.1 (10.0.0.1) 1.234 ms 1.123 ms 1.456 ms`,
|
|
want: Traceroute{
|
|
Hostname: "10.0.0.1",
|
|
IP: netip.MustParseAddr("10.0.0.1"),
|
|
Route: []TraceroutePath{
|
|
{
|
|
Hop: 1,
|
|
Hostname: "10.0.0.1",
|
|
IP: netip.MustParseAddr("10.0.0.1"),
|
|
Latencies: []time.Duration{
|
|
1234 * time.Microsecond,
|
|
1123 * time.Microsecond,
|
|
1456 * time.Microsecond,
|
|
},
|
|
},
|
|
},
|
|
Success: true,
|
|
Err: nil,
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "linux traceroute with dns failure fallback to IP",
|
|
input: `traceroute to example.com (93.184.216.34), 30 hops max, 60 byte packets
|
|
1 192.168.1.1 (192.168.1.1) 1.234 ms 1.123 ms 1.098 ms
|
|
2 10.0.0.1 (10.0.0.1) 5.678 ms 5.432 ms 5.321 ms
|
|
3 93.184.216.34 (93.184.216.34) 20.123 ms 19.876 ms 20.234 ms`,
|
|
want: Traceroute{
|
|
Hostname: "example.com",
|
|
IP: netip.MustParseAddr("93.184.216.34"),
|
|
Route: []TraceroutePath{
|
|
{
|
|
Hop: 1,
|
|
Hostname: "192.168.1.1",
|
|
IP: netip.MustParseAddr("192.168.1.1"),
|
|
Latencies: []time.Duration{
|
|
1234 * time.Microsecond,
|
|
1123 * time.Microsecond,
|
|
1098 * time.Microsecond,
|
|
},
|
|
},
|
|
{
|
|
Hop: 2,
|
|
Hostname: "10.0.0.1",
|
|
IP: netip.MustParseAddr("10.0.0.1"),
|
|
Latencies: []time.Duration{
|
|
5678 * time.Microsecond,
|
|
5432 * time.Microsecond,
|
|
5321 * time.Microsecond,
|
|
},
|
|
},
|
|
{
|
|
Hop: 3,
|
|
Hostname: "93.184.216.34",
|
|
IP: netip.MustParseAddr("93.184.216.34"),
|
|
Latencies: []time.Duration{
|
|
20123 * time.Microsecond,
|
|
19876 * time.Microsecond,
|
|
20234 * time.Microsecond,
|
|
},
|
|
},
|
|
},
|
|
Success: true,
|
|
Err: nil,
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "alpine linux traceroute with ms variations",
|
|
input: `traceroute to 1.1.1.1 (1.1.1.1), 30 hops max, 46 byte packets
|
|
1 gateway (192.168.0.1) 0.456ms 0.389ms 0.412ms
|
|
2 1.1.1.1 (1.1.1.1) 8.234ms 7.987ms 8.123ms`,
|
|
want: Traceroute{
|
|
Hostname: "1.1.1.1",
|
|
IP: netip.MustParseAddr("1.1.1.1"),
|
|
Route: []TraceroutePath{
|
|
{
|
|
Hop: 1,
|
|
Hostname: "gateway",
|
|
IP: netip.MustParseAddr("192.168.0.1"),
|
|
Latencies: []time.Duration{
|
|
456 * time.Microsecond,
|
|
389 * time.Microsecond,
|
|
412 * time.Microsecond,
|
|
},
|
|
},
|
|
{
|
|
Hop: 2,
|
|
Hostname: "1.1.1.1",
|
|
IP: netip.MustParseAddr("1.1.1.1"),
|
|
Latencies: []time.Duration{
|
|
8234 * time.Microsecond,
|
|
7987 * time.Microsecond,
|
|
8123 * time.Microsecond,
|
|
},
|
|
},
|
|
},
|
|
Success: true,
|
|
Err: nil,
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "mixed asterisk and latency values",
|
|
input: `traceroute to 8.8.8.8 (8.8.8.8), 30 hops max, 60 byte packets
|
|
1 gateway (192.168.1.1) * 1.234 ms 1.123 ms
|
|
2 10.0.0.1 (10.0.0.1) 5.678 ms * 5.432 ms
|
|
3 8.8.8.8 (8.8.8.8) 20.123 ms 19.876 ms *`,
|
|
want: Traceroute{
|
|
Hostname: "8.8.8.8",
|
|
IP: netip.MustParseAddr("8.8.8.8"),
|
|
Route: []TraceroutePath{
|
|
{
|
|
Hop: 1,
|
|
Hostname: "gateway",
|
|
IP: netip.MustParseAddr("192.168.1.1"),
|
|
Latencies: []time.Duration{
|
|
1234 * time.Microsecond,
|
|
1123 * time.Microsecond,
|
|
},
|
|
},
|
|
{
|
|
Hop: 2,
|
|
Hostname: "10.0.0.1",
|
|
IP: netip.MustParseAddr("10.0.0.1"),
|
|
Latencies: []time.Duration{
|
|
5678 * time.Microsecond,
|
|
5432 * time.Microsecond,
|
|
},
|
|
},
|
|
{
|
|
Hop: 3,
|
|
Hostname: "8.8.8.8",
|
|
IP: netip.MustParseAddr("8.8.8.8"),
|
|
Latencies: []time.Duration{
|
|
20123 * time.Microsecond,
|
|
19876 * time.Microsecond,
|
|
},
|
|
},
|
|
},
|
|
Success: true,
|
|
Err: nil,
|
|
},
|
|
wantErr: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := ParseTraceroute(tt.input)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("ParseTraceroute() error = %v, wantErr %v", err, tt.wantErr)
|
|
return
|
|
}
|
|
|
|
if tt.wantErr {
|
|
return
|
|
}
|
|
|
|
// Special handling for error field since it can't be directly compared with cmp.Diff
|
|
gotErr := got.Err
|
|
wantErr := tt.want.Err
|
|
got.Err = nil
|
|
tt.want.Err = nil
|
|
|
|
if diff := cmp.Diff(tt.want, got, IPComparer); diff != "" {
|
|
t.Errorf("ParseTraceroute() mismatch (-want +got):\n%s", diff)
|
|
}
|
|
|
|
// Now check error field separately
|
|
if (gotErr == nil) != (wantErr == nil) {
|
|
t.Errorf("Error field: got %v, want %v", gotErr, wantErr)
|
|
} else if gotErr != nil && wantErr != nil && gotErr.Error() != wantErr.Error() {
|
|
t.Errorf("Error message: got %q, want %q", gotErr.Error(), wantErr.Error())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSafeHostname(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
hostinfo *tailcfg.Hostinfo
|
|
machineKey string
|
|
nodeKey string
|
|
want string
|
|
}{
|
|
{
|
|
name: "valid_hostname",
|
|
hostinfo: &tailcfg.Hostinfo{
|
|
Hostname: "test-node",
|
|
},
|
|
machineKey: "mkey12345678",
|
|
nodeKey: "nkey12345678",
|
|
want: "test-node",
|
|
},
|
|
{
|
|
name: "nil_hostinfo_with_machine_key",
|
|
hostinfo: nil,
|
|
machineKey: "mkey12345678",
|
|
nodeKey: "nkey12345678",
|
|
want: "node-mkey1234",
|
|
},
|
|
{
|
|
name: "nil_hostinfo_with_node_key_only",
|
|
hostinfo: nil,
|
|
machineKey: "",
|
|
nodeKey: "nkey12345678",
|
|
want: "node-nkey1234",
|
|
},
|
|
{
|
|
name: "nil_hostinfo_no_keys",
|
|
hostinfo: nil,
|
|
machineKey: "",
|
|
nodeKey: "",
|
|
want: "unknown-node",
|
|
},
|
|
{
|
|
name: "empty_hostname_with_machine_key",
|
|
hostinfo: &tailcfg.Hostinfo{
|
|
Hostname: "",
|
|
},
|
|
machineKey: "mkey12345678",
|
|
nodeKey: "nkey12345678",
|
|
want: "node-mkey1234",
|
|
},
|
|
{
|
|
name: "empty_hostname_with_node_key_only",
|
|
hostinfo: &tailcfg.Hostinfo{
|
|
Hostname: "",
|
|
},
|
|
machineKey: "",
|
|
nodeKey: "nkey12345678",
|
|
want: "node-nkey1234",
|
|
},
|
|
{
|
|
name: "empty_hostname_no_keys",
|
|
hostinfo: &tailcfg.Hostinfo{
|
|
Hostname: "",
|
|
},
|
|
machineKey: "",
|
|
nodeKey: "",
|
|
want: "unknown-node",
|
|
},
|
|
{
|
|
name: "hostname_exactly_63_chars",
|
|
hostinfo: &tailcfg.Hostinfo{
|
|
Hostname: "123456789012345678901234567890123456789012345678901234567890123",
|
|
},
|
|
machineKey: "mkey12345678",
|
|
nodeKey: "nkey12345678",
|
|
want: "123456789012345678901234567890123456789012345678901234567890123",
|
|
},
|
|
{
|
|
name: "hostname_64_chars_truncated",
|
|
hostinfo: &tailcfg.Hostinfo{
|
|
Hostname: "1234567890123456789012345678901234567890123456789012345678901234",
|
|
},
|
|
machineKey: "mkey12345678",
|
|
nodeKey: "nkey12345678",
|
|
want: "123456789012345678901234567890123456789012345678901234567890123",
|
|
},
|
|
{
|
|
name: "hostname_very_long_truncated",
|
|
hostinfo: &tailcfg.Hostinfo{
|
|
Hostname: "test-node-with-very-long-hostname-that-exceeds-dns-label-limits-of-63-characters-and-should-be-truncated",
|
|
},
|
|
machineKey: "mkey12345678",
|
|
nodeKey: "nkey12345678",
|
|
want: "test-node-with-very-long-hostname-that-exceeds-dns-label-limits",
|
|
},
|
|
{
|
|
name: "hostname_with_special_chars",
|
|
hostinfo: &tailcfg.Hostinfo{
|
|
Hostname: "node-with-special!@#$%",
|
|
},
|
|
machineKey: "mkey12345678",
|
|
nodeKey: "nkey12345678",
|
|
want: "node-with-special!@#$%",
|
|
},
|
|
{
|
|
name: "hostname_with_unicode",
|
|
hostinfo: &tailcfg.Hostinfo{
|
|
Hostname: "node-ñoño-测试",
|
|
},
|
|
machineKey: "mkey12345678",
|
|
nodeKey: "nkey12345678",
|
|
want: "node-ñoño-测试",
|
|
},
|
|
{
|
|
name: "short_machine_key",
|
|
hostinfo: &tailcfg.Hostinfo{
|
|
Hostname: "",
|
|
},
|
|
machineKey: "short",
|
|
nodeKey: "nkey12345678",
|
|
want: "node-short",
|
|
},
|
|
{
|
|
name: "short_node_key",
|
|
hostinfo: &tailcfg.Hostinfo{
|
|
Hostname: "",
|
|
},
|
|
machineKey: "",
|
|
nodeKey: "short",
|
|
want: "node-short",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
got := SafeHostname(tt.hostinfo, tt.machineKey, tt.nodeKey)
|
|
if got != tt.want {
|
|
t.Errorf("SafeHostname() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEnsureValidHostinfo(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
hostinfo *tailcfg.Hostinfo
|
|
machineKey string
|
|
nodeKey string
|
|
wantHostname string
|
|
checkHostinfo func(*testing.T, *tailcfg.Hostinfo)
|
|
}{
|
|
{
|
|
name: "valid_hostinfo_unchanged",
|
|
hostinfo: &tailcfg.Hostinfo{
|
|
Hostname: "test-node",
|
|
OS: "linux",
|
|
},
|
|
machineKey: "mkey12345678",
|
|
nodeKey: "nkey12345678",
|
|
wantHostname: "test-node",
|
|
checkHostinfo: func(t *testing.T, hi *tailcfg.Hostinfo) {
|
|
if hi == nil {
|
|
t.Error("hostinfo should not be nil")
|
|
}
|
|
if hi.Hostname != "test-node" {
|
|
t.Errorf("hostname = %v, want test-node", hi.Hostname)
|
|
}
|
|
if hi.OS != "linux" {
|
|
t.Errorf("OS = %v, want linux", hi.OS)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "nil_hostinfo_creates_default",
|
|
hostinfo: nil,
|
|
machineKey: "mkey12345678",
|
|
nodeKey: "nkey12345678",
|
|
wantHostname: "node-mkey1234",
|
|
checkHostinfo: func(t *testing.T, hi *tailcfg.Hostinfo) {
|
|
if hi == nil {
|
|
t.Error("hostinfo should not be nil")
|
|
}
|
|
if hi.Hostname != "node-mkey1234" {
|
|
t.Errorf("hostname = %v, want node-mkey1234", hi.Hostname)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "empty_hostname_updated",
|
|
hostinfo: &tailcfg.Hostinfo{
|
|
Hostname: "",
|
|
OS: "darwin",
|
|
},
|
|
machineKey: "mkey12345678",
|
|
nodeKey: "nkey12345678",
|
|
wantHostname: "node-mkey1234",
|
|
checkHostinfo: func(t *testing.T, hi *tailcfg.Hostinfo) {
|
|
if hi == nil {
|
|
t.Error("hostinfo should not be nil")
|
|
}
|
|
if hi.Hostname != "node-mkey1234" {
|
|
t.Errorf("hostname = %v, want node-mkey1234", hi.Hostname)
|
|
}
|
|
if hi.OS != "darwin" {
|
|
t.Errorf("OS = %v, want darwin", hi.OS)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "long_hostname_truncated",
|
|
hostinfo: &tailcfg.Hostinfo{
|
|
Hostname: "test-node-with-very-long-hostname-that-exceeds-dns-label-limits-of-63-characters",
|
|
},
|
|
machineKey: "mkey12345678",
|
|
nodeKey: "nkey12345678",
|
|
wantHostname: "test-node-with-very-long-hostname-that-exceeds-dns-label-limits",
|
|
checkHostinfo: func(t *testing.T, hi *tailcfg.Hostinfo) {
|
|
if hi == nil {
|
|
t.Error("hostinfo should not be nil")
|
|
}
|
|
if hi.Hostname != "test-node-with-very-long-hostname-that-exceeds-dns-label-limits" {
|
|
t.Errorf("hostname = %v, want truncated", hi.Hostname)
|
|
}
|
|
if len(hi.Hostname) != 63 {
|
|
t.Errorf("hostname length = %v, want 63", len(hi.Hostname))
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "nil_hostinfo_node_key_only",
|
|
hostinfo: nil,
|
|
machineKey: "",
|
|
nodeKey: "nkey12345678",
|
|
wantHostname: "node-nkey1234",
|
|
checkHostinfo: func(t *testing.T, hi *tailcfg.Hostinfo) {
|
|
if hi == nil {
|
|
t.Error("hostinfo should not be nil")
|
|
}
|
|
if hi.Hostname != "node-nkey1234" {
|
|
t.Errorf("hostname = %v, want node-nkey1234", hi.Hostname)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "nil_hostinfo_no_keys",
|
|
hostinfo: nil,
|
|
machineKey: "",
|
|
nodeKey: "",
|
|
wantHostname: "unknown-node",
|
|
checkHostinfo: func(t *testing.T, hi *tailcfg.Hostinfo) {
|
|
if hi == nil {
|
|
t.Error("hostinfo should not be nil")
|
|
}
|
|
if hi.Hostname != "unknown-node" {
|
|
t.Errorf("hostname = %v, want unknown-node", hi.Hostname)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "empty_hostname_no_keys",
|
|
hostinfo: &tailcfg.Hostinfo{
|
|
Hostname: "",
|
|
},
|
|
machineKey: "",
|
|
nodeKey: "",
|
|
wantHostname: "unknown-node",
|
|
checkHostinfo: func(t *testing.T, hi *tailcfg.Hostinfo) {
|
|
if hi == nil {
|
|
t.Error("hostinfo should not be nil")
|
|
}
|
|
if hi.Hostname != "unknown-node" {
|
|
t.Errorf("hostname = %v, want unknown-node", hi.Hostname)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "preserves_other_fields",
|
|
hostinfo: &tailcfg.Hostinfo{
|
|
Hostname: "test",
|
|
OS: "windows",
|
|
OSVersion: "10.0.19044",
|
|
DeviceModel: "test-device",
|
|
BackendLogID: "log123",
|
|
},
|
|
machineKey: "mkey12345678",
|
|
nodeKey: "nkey12345678",
|
|
wantHostname: "test",
|
|
checkHostinfo: func(t *testing.T, hi *tailcfg.Hostinfo) {
|
|
if hi == nil {
|
|
t.Error("hostinfo should not be nil")
|
|
}
|
|
if hi.Hostname != "test" {
|
|
t.Errorf("hostname = %v, want test", hi.Hostname)
|
|
}
|
|
if hi.OS != "windows" {
|
|
t.Errorf("OS = %v, want windows", hi.OS)
|
|
}
|
|
if hi.OSVersion != "10.0.19044" {
|
|
t.Errorf("OSVersion = %v, want 10.0.19044", hi.OSVersion)
|
|
}
|
|
if hi.DeviceModel != "test-device" {
|
|
t.Errorf("DeviceModel = %v, want test-device", hi.DeviceModel)
|
|
}
|
|
if hi.BackendLogID != "log123" {
|
|
t.Errorf("BackendLogID = %v, want log123", hi.BackendLogID)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "exactly_63_chars_unchanged",
|
|
hostinfo: &tailcfg.Hostinfo{
|
|
Hostname: "123456789012345678901234567890123456789012345678901234567890123",
|
|
},
|
|
machineKey: "mkey12345678",
|
|
nodeKey: "nkey12345678",
|
|
wantHostname: "123456789012345678901234567890123456789012345678901234567890123",
|
|
checkHostinfo: func(t *testing.T, hi *tailcfg.Hostinfo) {
|
|
if hi == nil {
|
|
t.Error("hostinfo should not be nil")
|
|
}
|
|
if len(hi.Hostname) != 63 {
|
|
t.Errorf("hostname length = %v, want 63", len(hi.Hostname))
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
gotHostinfo, gotHostname := EnsureValidHostinfo(tt.hostinfo, tt.machineKey, tt.nodeKey)
|
|
|
|
if gotHostname != tt.wantHostname {
|
|
t.Errorf("EnsureValidHostinfo() hostname = %v, want %v", gotHostname, tt.wantHostname)
|
|
}
|
|
if gotHostinfo == nil {
|
|
t.Error("returned hostinfo should never be nil")
|
|
}
|
|
|
|
if tt.checkHostinfo != nil {
|
|
tt.checkHostinfo(t, gotHostinfo)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSafeHostname_DNSLabelLimit(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testCases := []string{
|
|
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
|
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
|
|
"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
|
|
}
|
|
|
|
for i, hostname := range testCases {
|
|
t.Run(cmp.Diff("", ""), func(t *testing.T) {
|
|
hostinfo := &tailcfg.Hostinfo{Hostname: hostname}
|
|
result := SafeHostname(hostinfo, "mkey", "nkey")
|
|
if len(result) > 63 {
|
|
t.Errorf("test case %d: hostname length = %d, want <= 63", i, len(result))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEnsureValidHostinfo_Idempotent(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
originalHostinfo := &tailcfg.Hostinfo{
|
|
Hostname: "test-node",
|
|
OS: "linux",
|
|
}
|
|
|
|
hostinfo1, hostname1 := EnsureValidHostinfo(originalHostinfo, "mkey", "nkey")
|
|
hostinfo2, hostname2 := EnsureValidHostinfo(hostinfo1, "mkey", "nkey")
|
|
|
|
if hostname1 != hostname2 {
|
|
t.Errorf("hostnames not equal: %v != %v", hostname1, hostname2)
|
|
}
|
|
if hostinfo1.Hostname != hostinfo2.Hostname {
|
|
t.Errorf("hostinfo hostnames not equal: %v != %v", hostinfo1.Hostname, hostinfo2.Hostname)
|
|
}
|
|
if hostinfo1.OS != hostinfo2.OS {
|
|
t.Errorf("hostinfo OS not equal: %v != %v", hostinfo1.OS, hostinfo2.OS)
|
|
}
|
|
}
|