mirror of https://github.com/tailscale/tailscale/
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
582 lines
13 KiB
Go
582 lines
13 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
//go:build !ts_omit_serve
|
|
|
|
package conffile
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"tailscale.com/tailcfg"
|
|
"tailscale.com/types/opt"
|
|
)
|
|
|
|
// TestTarget_UnmarshalJSON tests Target JSON unmarshaling
|
|
func TestTarget_UnmarshalJSON(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
json string
|
|
wantProtocol ServiceProtocol
|
|
wantDest string
|
|
wantPorts string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "tun_mode",
|
|
json: `"TUN"`,
|
|
wantProtocol: ProtoTUN,
|
|
wantDest: "",
|
|
wantPorts: "*",
|
|
},
|
|
{
|
|
name: "http_with_host_port",
|
|
json: `"http://localhost:8080"`,
|
|
wantProtocol: ProtoHTTP,
|
|
wantDest: "localhost",
|
|
wantPorts: "8080",
|
|
},
|
|
{
|
|
name: "https_with_host_port",
|
|
json: `"https://example.com:443"`,
|
|
wantProtocol: ProtoHTTPS,
|
|
wantDest: "example.com",
|
|
wantPorts: "443",
|
|
},
|
|
{
|
|
name: "https_insecure",
|
|
json: `"https+insecure://localhost:9000"`,
|
|
wantProtocol: ProtoHTTPSInsecure,
|
|
wantDest: "localhost",
|
|
wantPorts: "9000",
|
|
},
|
|
{
|
|
name: "tcp_with_host_port",
|
|
json: `"tcp://127.0.0.1:3000"`,
|
|
wantProtocol: ProtoTCP,
|
|
wantDest: "127.0.0.1",
|
|
wantPorts: "3000",
|
|
},
|
|
{
|
|
name: "tls_terminated_tcp",
|
|
json: `"tls-terminated-tcp://backend:5000"`,
|
|
wantProtocol: ProtoTLSTerminatedTCP,
|
|
wantDest: "backend",
|
|
wantPorts: "5000",
|
|
},
|
|
{
|
|
name: "file_protocol",
|
|
json: `"file:///var/www/html"`,
|
|
wantProtocol: ProtoFile,
|
|
wantDest: "/var/www/html",
|
|
wantPorts: "",
|
|
},
|
|
{
|
|
name: "file_with_relative_path",
|
|
json: `"file://./public"`,
|
|
wantProtocol: ProtoFile,
|
|
wantDest: "public",
|
|
wantPorts: "",
|
|
},
|
|
{
|
|
name: "invalid_no_protocol",
|
|
json: `"localhost:8080"`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "unsupported_protocol",
|
|
json: `"ftp://server:21"`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "invalid_json",
|
|
json: `not-a-json-string`,
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var target Target
|
|
err := target.UnmarshalJSON([]byte(tt.json))
|
|
|
|
if tt.wantErr {
|
|
if err == nil {
|
|
t.Error("expected error, got nil")
|
|
}
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if target.Protocol != tt.wantProtocol {
|
|
t.Errorf("Protocol = %q, want %q", target.Protocol, tt.wantProtocol)
|
|
}
|
|
if target.Destination != tt.wantDest {
|
|
t.Errorf("Destination = %q, want %q", target.Destination, tt.wantDest)
|
|
}
|
|
|
|
if tt.wantPorts != "" {
|
|
gotPorts := target.DestinationPorts.String()
|
|
if tt.wantPorts == "*" {
|
|
// PortRangeAny case
|
|
if target.DestinationPorts != tailcfg.PortRangeAny {
|
|
t.Errorf("DestinationPorts = %v, want PortRangeAny", target.DestinationPorts)
|
|
}
|
|
} else if gotPorts != tt.wantPorts {
|
|
t.Errorf("DestinationPorts = %q, want %q", gotPorts, tt.wantPorts)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestTarget_MarshalText tests Target text marshaling
|
|
func TestTarget_MarshalText(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
target Target
|
|
want string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "tun_mode",
|
|
target: Target{
|
|
Protocol: ProtoTUN,
|
|
Destination: "",
|
|
DestinationPorts: tailcfg.PortRangeAny,
|
|
},
|
|
want: "TUN",
|
|
},
|
|
{
|
|
name: "http_target",
|
|
target: Target{
|
|
Protocol: ProtoHTTP,
|
|
Destination: "localhost",
|
|
DestinationPorts: tailcfg.PortRange{
|
|
First: 8080,
|
|
Last: 8080,
|
|
},
|
|
},
|
|
want: "http://localhost:8080",
|
|
},
|
|
{
|
|
name: "https_target",
|
|
target: Target{
|
|
Protocol: ProtoHTTPS,
|
|
Destination: "example.com",
|
|
DestinationPorts: tailcfg.PortRange{
|
|
First: 443,
|
|
Last: 443,
|
|
},
|
|
},
|
|
want: "https://example.com:443",
|
|
},
|
|
{
|
|
name: "tcp_target",
|
|
target: Target{
|
|
Protocol: ProtoTCP,
|
|
Destination: "10.0.0.1",
|
|
DestinationPorts: tailcfg.PortRange{
|
|
First: 3000,
|
|
Last: 3000,
|
|
},
|
|
},
|
|
want: "tcp://10.0.0.1:3000",
|
|
},
|
|
{
|
|
name: "file_target",
|
|
target: Target{
|
|
Protocol: ProtoFile,
|
|
Destination: "/var/www",
|
|
},
|
|
want: "file:///var/www",
|
|
},
|
|
{
|
|
name: "unsupported_protocol",
|
|
target: Target{
|
|
Protocol: "unknown",
|
|
Destination: "test",
|
|
},
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := tt.target.MarshalText()
|
|
|
|
if tt.wantErr {
|
|
if err == nil {
|
|
t.Error("expected error, got nil")
|
|
}
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if string(got) != tt.want {
|
|
t.Errorf("MarshalText() = %q, want %q", string(got), tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestTarget_RoundTrip tests unmarshal then marshal
|
|
func TestTarget_RoundTrip(t *testing.T) {
|
|
tests := []string{
|
|
`"TUN"`,
|
|
`"http://localhost:8080"`,
|
|
`"https://example.com:443"`,
|
|
`"tcp://10.0.0.1:3000"`,
|
|
`"file:///var/www/html"`,
|
|
`"https+insecure://test:9999"`,
|
|
`"tls-terminated-tcp://backend:5000"`,
|
|
}
|
|
|
|
for _, original := range tests {
|
|
t.Run(original, func(t *testing.T) {
|
|
var target Target
|
|
if err := target.UnmarshalJSON([]byte(original)); err != nil {
|
|
t.Fatalf("UnmarshalJSON failed: %v", err)
|
|
}
|
|
|
|
marshaled, err := target.MarshalText()
|
|
if err != nil {
|
|
t.Fatalf("MarshalText failed: %v", err)
|
|
}
|
|
|
|
// Unmarshal again
|
|
var target2 Target
|
|
if err := target2.UnmarshalJSON(marshaled); err != nil {
|
|
t.Fatalf("second UnmarshalJSON failed: %v", err)
|
|
}
|
|
|
|
// Compare
|
|
if target.Protocol != target2.Protocol {
|
|
t.Errorf("Protocol mismatch: %q != %q", target.Protocol, target2.Protocol)
|
|
}
|
|
if target.Destination != target2.Destination {
|
|
t.Errorf("Destination mismatch: %q != %q", target.Destination, target2.Destination)
|
|
}
|
|
if target.DestinationPorts != target2.DestinationPorts {
|
|
t.Errorf("DestinationPorts mismatch: %v != %v", target.DestinationPorts, target2.DestinationPorts)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestServiceProtocol_Constants tests protocol constants
|
|
func TestServiceProtocol_Constants(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
protocol ServiceProtocol
|
|
value string
|
|
}{
|
|
{"http", ProtoHTTP, "http"},
|
|
{"https", ProtoHTTPS, "https"},
|
|
{"https_insecure", ProtoHTTPSInsecure, "https+insecure"},
|
|
{"tcp", ProtoTCP, "tcp"},
|
|
{"tls_terminated_tcp", ProtoTLSTerminatedTCP, "tls-terminated-tcp"},
|
|
{"file", ProtoFile, "file"},
|
|
{"tun", ProtoTUN, "TUN"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if string(tt.protocol) != tt.value {
|
|
t.Errorf("protocol = %q, want %q", tt.protocol, tt.value)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestTarget_PortRanges tests various port range formats
|
|
func TestTarget_PortRanges(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
json string
|
|
wantFirst uint16
|
|
wantLast uint16
|
|
}{
|
|
{
|
|
name: "single_port",
|
|
json: `"tcp://localhost:8080"`,
|
|
wantFirst: 8080,
|
|
wantLast: 8080,
|
|
},
|
|
{
|
|
name: "port_range",
|
|
json: `"tcp://localhost:8000-8100"`,
|
|
wantFirst: 8000,
|
|
wantLast: 8100,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var target Target
|
|
if err := target.UnmarshalJSON([]byte(tt.json)); err != nil {
|
|
t.Fatalf("UnmarshalJSON failed: %v", err)
|
|
}
|
|
|
|
if target.DestinationPorts.First != tt.wantFirst {
|
|
t.Errorf("DestinationPorts.First = %d, want %d", target.DestinationPorts.First, tt.wantFirst)
|
|
}
|
|
if target.DestinationPorts.Last != tt.wantLast {
|
|
t.Errorf("DestinationPorts.Last = %d, want %d", target.DestinationPorts.Last, tt.wantLast)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestFindOverlappingRange tests port range overlap detection
|
|
func TestFindOverlappingRange(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
haystack []tailcfg.PortRange
|
|
needle tailcfg.PortRange
|
|
wantFound bool
|
|
}{
|
|
{
|
|
name: "no_overlap",
|
|
haystack: []tailcfg.PortRange{
|
|
{First: 80, Last: 80},
|
|
{First: 443, Last: 443},
|
|
},
|
|
needle: tailcfg.PortRange{First: 8080, Last: 8080},
|
|
wantFound: false,
|
|
},
|
|
{
|
|
name: "exact_match",
|
|
haystack: []tailcfg.PortRange{
|
|
{First: 80, Last: 80},
|
|
{First: 443, Last: 443},
|
|
},
|
|
needle: tailcfg.PortRange{First: 80, Last: 80},
|
|
wantFound: true,
|
|
},
|
|
{
|
|
name: "needle_contains_haystack",
|
|
haystack: []tailcfg.PortRange{
|
|
{First: 8080, Last: 8090},
|
|
},
|
|
needle: tailcfg.PortRange{First: 8000, Last: 9000},
|
|
wantFound: true,
|
|
},
|
|
{
|
|
name: "haystack_contains_needle",
|
|
haystack: []tailcfg.PortRange{
|
|
{First: 8000, Last: 9000},
|
|
},
|
|
needle: tailcfg.PortRange{First: 8080, Last: 8090},
|
|
wantFound: true,
|
|
},
|
|
{
|
|
name: "partial_overlap_start",
|
|
haystack: []tailcfg.PortRange{
|
|
{First: 8050, Last: 8100},
|
|
},
|
|
needle: tailcfg.PortRange{First: 8000, Last: 8060},
|
|
wantFound: true,
|
|
},
|
|
{
|
|
name: "partial_overlap_end",
|
|
haystack: []tailcfg.PortRange{
|
|
{First: 8000, Last: 8050},
|
|
},
|
|
needle: tailcfg.PortRange{First: 8040, Last: 8100},
|
|
wantFound: true,
|
|
},
|
|
{
|
|
name: "empty_haystack",
|
|
haystack: []tailcfg.PortRange{},
|
|
needle: tailcfg.PortRange{First: 80, Last: 80},
|
|
wantFound: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := findOverlappingRange(tt.haystack, tt.needle)
|
|
found := result != nil
|
|
|
|
if found != tt.wantFound {
|
|
t.Errorf("findOverlappingRange() found = %v, want %v", found, tt.wantFound)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestServicesConfigFile_Structure tests the config file structure
|
|
func TestServicesConfigFile_Structure(t *testing.T) {
|
|
scf := ServicesConfigFile{
|
|
Version: "0.0.1",
|
|
Services: map[tailcfg.ServiceName]*ServiceDetailsFile{
|
|
"test-service": {
|
|
Version: "",
|
|
Endpoints: map[*tailcfg.ProtoPortRange]*Target{
|
|
{Proto: 6, Ports: tailcfg.PortRange{First: 443, Last: 443}}: {
|
|
Protocol: ProtoHTTPS,
|
|
Destination: "localhost",
|
|
DestinationPorts: tailcfg.PortRange{
|
|
First: 8443,
|
|
Last: 8443,
|
|
},
|
|
},
|
|
},
|
|
Advertised: opt.NewBool(true),
|
|
},
|
|
},
|
|
}
|
|
|
|
if scf.Version != "0.0.1" {
|
|
t.Errorf("Version = %q, want 0.0.1", scf.Version)
|
|
}
|
|
|
|
if len(scf.Services) != 1 {
|
|
t.Errorf("Services length = %d, want 1", len(scf.Services))
|
|
}
|
|
|
|
svc, ok := scf.Services["test-service"]
|
|
if !ok {
|
|
t.Fatal("test-service not found")
|
|
}
|
|
|
|
if svc.Advertised != opt.NewBool(true) {
|
|
t.Error("Advertised should be true")
|
|
}
|
|
}
|
|
|
|
// TestServiceDetailsFile_Advertised tests the Advertised field
|
|
func TestServiceDetailsFile_Advertised(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
advertised opt.Bool
|
|
wantSet bool
|
|
wantValue bool
|
|
}{
|
|
{
|
|
name: "advertised_true",
|
|
advertised: opt.NewBool(true),
|
|
wantSet: true,
|
|
wantValue: true,
|
|
},
|
|
{
|
|
name: "advertised_false",
|
|
advertised: opt.NewBool(false),
|
|
wantSet: true,
|
|
wantValue: false,
|
|
},
|
|
{
|
|
name: "advertised_unset",
|
|
advertised: "",
|
|
wantSet: false,
|
|
wantValue: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
sdf := ServiceDetailsFile{
|
|
Advertised: tt.advertised,
|
|
}
|
|
|
|
if tt.wantSet {
|
|
val, ok := sdf.Advertised.Get()
|
|
if !ok {
|
|
t.Error("Advertised should be set")
|
|
}
|
|
if val != tt.wantValue {
|
|
t.Errorf("Advertised value = %v, want %v", val, tt.wantValue)
|
|
}
|
|
} else {
|
|
if _, ok := sdf.Advertised.Get(); ok {
|
|
t.Error("Advertised should not be set")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestTarget_FilePathCleaning tests that file paths are cleaned
|
|
func TestTarget_FilePathCleaning(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
json string
|
|
wantPath string
|
|
}{
|
|
{
|
|
name: "absolute_path",
|
|
json: `"file:///var/www/html"`,
|
|
wantPath: "/var/www/html",
|
|
},
|
|
{
|
|
name: "relative_path_with_dot",
|
|
json: `"file://./public"`,
|
|
wantPath: "public",
|
|
},
|
|
{
|
|
name: "path_with_double_slash",
|
|
json: `"file://var//www//html"`,
|
|
wantPath: "var/www/html",
|
|
},
|
|
{
|
|
name: "path_with_dot_dot",
|
|
json: `"file://var/www/../static"`,
|
|
wantPath: "var/static",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var target Target
|
|
if err := target.UnmarshalJSON([]byte(tt.json)); err != nil {
|
|
t.Fatalf("UnmarshalJSON failed: %v", err)
|
|
}
|
|
|
|
if target.Destination != tt.wantPath {
|
|
t.Errorf("Destination = %q, want %q", target.Destination, tt.wantPath)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestTarget_IPv6Addresses tests IPv6 address handling
|
|
func TestTarget_IPv6Addresses(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
json string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "ipv6_with_port",
|
|
json: `"tcp://[::1]:8080"`,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "ipv6_full_address",
|
|
json: `"https://[2001:db8::1]:443"`,
|
|
wantErr: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var target Target
|
|
err := target.UnmarshalJSON([]byte(tt.json))
|
|
|
|
if tt.wantErr && err == nil {
|
|
t.Error("expected error, got nil")
|
|
}
|
|
if !tt.wantErr && err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|