mirror of
https://github.com/juanfont/headscale.git
synced 2026-04-10 19:17:25 +02:00
Add support for the Grant policy format as an alternative to ACL format, following Tailscale's policy v2 specification. Grants provide a more structured way to define network access rules with explicit separation of IP-based and capability-based permissions. Key changes: - Add Grant struct with Sources, Destinations, InternetProtocols (ip), and App (capabilities) fields - Add ProtocolPort type for unmarshaling protocol:port strings - Add Grant validation in Policy.validate() to enforce: - Mutual exclusivity of ip and app fields - Required ip or app field presence - Non-empty sources and destinations - Refactor compileFilterRules to support both ACLs and Grants - Convert ACLs to Grants internally via aclToGrants() for unified processing - Extract destinationsToNetPortRange() helper for cleaner code - Rename parseProtocol() to toIANAProtocolNumbers() for clarity - Add ProtocolNumberToName mapping for reverse lookups The Grant format allows policies to be written using either the legacy ACL format or the new Grant format. ACLs are converted to Grants internally, ensuring backward compatibility while enabling the new format's benefits. Updates #2180
220 lines
7.0 KiB
Go
220 lines
7.0 KiB
Go
package v2
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"tailscale.com/tailcfg"
|
|
)
|
|
|
|
// TestParseDestinationAndPort tests the splitDestinationAndPort function using table-driven tests.
|
|
func TestSplitDestinationAndPort(t *testing.T) {
|
|
testCases := []struct {
|
|
input string
|
|
wantDst string
|
|
wantPort string
|
|
wantErrIs error
|
|
wantNoError bool
|
|
}{
|
|
// --- Non-bracketed inputs (existing behavior, unchanged) ---
|
|
|
|
// Hostnames and tags
|
|
{"git-server:*", "git-server", "*", nil, true},
|
|
{"example-host-1:*", "example-host-1", "*", nil, true},
|
|
{"hostname:80-90", "hostname", "80-90", nil, true},
|
|
{"tag:montreal-webserver:80,443", "tag:montreal-webserver", "80,443", nil, true},
|
|
{"tag:api-server:443", "tag:api-server", "443", nil, true},
|
|
|
|
// IPv4 and IPv4 CIDR
|
|
{"192.168.1.0/24:22", "192.168.1.0/24", "22", nil, true},
|
|
{"10.0.0.1:443", "10.0.0.1", "443", nil, true},
|
|
|
|
// Bare IPv6 (no brackets) — last colon splits correctly
|
|
{"fd7a:115c:a1e0::2:22", "fd7a:115c:a1e0::2", "22", nil, true},
|
|
{"fd7a:115c:a1e0::2/128:22", "fd7a:115c:a1e0::2/128", "22", nil, true},
|
|
|
|
// --- Bracketed IPv6: [addr]:port ---
|
|
|
|
// Single port
|
|
{"[fd7a:115c:a1e0::87e1]:22", "fd7a:115c:a1e0::87e1", "22", nil, true},
|
|
{"[::1]:80", "::1", "80", nil, true},
|
|
{"[2001:db8::1]:443", "2001:db8::1", "443", nil, true},
|
|
{"[fe80::1]:22", "fe80::1", "22", nil, true},
|
|
|
|
// Multiple ports
|
|
{"[fd7a:115c:a1e0::87e1]:80,443", "fd7a:115c:a1e0::87e1", "80,443", nil, true},
|
|
{"[::1]:22,80,443", "::1", "22,80,443", nil, true},
|
|
|
|
// Port range
|
|
{"[fd7a:115c:a1e0::2]:80-90", "fd7a:115c:a1e0::2", "80-90", nil, true},
|
|
|
|
// Wildcard port
|
|
{"[fd7a:115c:a1e0::87e1]:*", "fd7a:115c:a1e0::87e1", "*", nil, true},
|
|
|
|
// Unspecified address [::]
|
|
{"[::]:80", "::", "80", nil, true},
|
|
{"[::]:*", "::", "*", nil, true},
|
|
|
|
// Full-length IPv6
|
|
{"[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:443", "2001:0db8:85a3:0000:0000:8a2e:0370:7334", "443", nil, true},
|
|
|
|
// --- Bracketed IPv6 CIDR: [addr]/prefix:port ---
|
|
|
|
{"[fd7a:115c:a1e0::2905]/128:80,443", "fd7a:115c:a1e0::2905/128", "80,443", nil, true},
|
|
{"[fd7a:115c:a1e0::1]/128:22", "fd7a:115c:a1e0::1/128", "22", nil, true},
|
|
{"[2001:db8::1]/32:443", "2001:db8::1/32", "443", nil, true},
|
|
{"[::1]/128:*", "::1/128", "*", nil, true},
|
|
{"[fd7a:115c:a1e0::2]/64:80-90", "fd7a:115c:a1e0::2/64", "80-90", nil, true},
|
|
{"[::]/0:*", "::/0", "*", nil, true},
|
|
|
|
// --- Errors: brackets around non-IPv6 ---
|
|
|
|
// IPv4 in brackets
|
|
{"[192.168.1.1]:80", "", "", ErrBracketsNotIPv6, false},
|
|
{"[10.0.0.1]:443", "", "", ErrBracketsNotIPv6, false},
|
|
{"[192.168.1.1]/32:80", "", "", ErrBracketsNotIPv6, false},
|
|
|
|
// IPv4 CIDR inside brackets
|
|
{"[10.0.0.0/8]:80", "", "", ErrBracketsNotIPv6, false},
|
|
|
|
// Hostnames in brackets
|
|
{"[my-hostname]:80", "", "", ErrBracketsNotIPv6, false},
|
|
{"[git-server]:*", "", "", ErrBracketsNotIPv6, false},
|
|
|
|
// Tags in brackets
|
|
{"[tag:server]:80", "", "", ErrBracketsNotIPv6, false},
|
|
|
|
// --- Errors: CIDR inside brackets (must use [addr]/prefix:port) ---
|
|
|
|
{"[fd7a:115c:a1e0::2/128]:22", "", "", ErrBracketsNotIPv6, false},
|
|
{"[2001:db8::/32]:443", "", "", ErrBracketsNotIPv6, false},
|
|
{"[::1/128]:80", "", "", ErrBracketsNotIPv6, false},
|
|
|
|
// --- Errors: malformed bracket syntax ---
|
|
|
|
// No port after brackets
|
|
{"[::1]", "", "", ErrBracketsNotIPv6, false},
|
|
{"[2001:db8::1]", "", "", ErrBracketsNotIPv6, false},
|
|
|
|
// Empty brackets
|
|
{"[]:80", "", "", ErrBracketsNotIPv6, false},
|
|
|
|
// Missing close bracket
|
|
{"[::1", "", "", ErrBracketsNotIPv6, false},
|
|
{"[2001:db8::1:80", "", "", ErrBracketsNotIPv6, false},
|
|
|
|
// Empty port after colon
|
|
{"[fd7a:115c:a1e0::1]:", "", "", ErrInputEndsWithColon, false},
|
|
{"[::1]:", "", "", ErrInputEndsWithColon, false},
|
|
{"[fd7a::1]/128:", "", "", ErrInputEndsWithColon, false},
|
|
|
|
// Junk after close bracket (not : or /)
|
|
{"[::1]blah", "", "", ErrBracketsNotIPv6, false},
|
|
{"[::1] :80", "", "", ErrBracketsNotIPv6, false},
|
|
|
|
// --- Errors: non-bracketed malformed input (unchanged) ---
|
|
|
|
{"invalidinput", "", "", ErrInputMissingColon, false},
|
|
{":invalid", "", "", ErrInputStartsWithColon, false},
|
|
{"invalid:", "", "", ErrInputEndsWithColon, false},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.input, func(t *testing.T) {
|
|
dst, port, err := splitDestinationAndPort(tc.input)
|
|
|
|
if tc.wantNoError {
|
|
if err != nil {
|
|
t.Fatalf("splitDestinationAndPort(%q) unexpected error: %v", tc.input, err)
|
|
}
|
|
|
|
if dst != tc.wantDst {
|
|
t.Errorf("splitDestinationAndPort(%q) dst = %q, want %q", tc.input, dst, tc.wantDst)
|
|
}
|
|
|
|
if port != tc.wantPort {
|
|
t.Errorf("splitDestinationAndPort(%q) port = %q, want %q", tc.input, port, tc.wantPort)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
if err == nil {
|
|
t.Fatalf("splitDestinationAndPort(%q) = (%q, %q, nil), want error wrapping %v", tc.input, dst, port, tc.wantErrIs)
|
|
}
|
|
|
|
if !errors.Is(err, tc.wantErrIs) {
|
|
t.Errorf("splitDestinationAndPort(%q) error = %v, want error wrapping %v", tc.input, err, tc.wantErrIs)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParsePort(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
expected uint16
|
|
err string
|
|
}{
|
|
{"80", 80, ""},
|
|
{"0", 0, ""},
|
|
{"65535", 65535, ""},
|
|
{"-1", 0, "port number out of range"},
|
|
{"65536", 0, "port number out of range"},
|
|
{"abc", 0, "invalid port number"},
|
|
{"", 0, "invalid port number"},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
result, err := parsePort(test.input)
|
|
if err != nil && err.Error() != test.err {
|
|
t.Errorf("parsePort(%q) error = %v, expected error = %v", test.input, err, test.err)
|
|
}
|
|
|
|
if err == nil && test.err != "" {
|
|
t.Errorf("parsePort(%q) expected error = %v, got nil", test.input, test.err)
|
|
}
|
|
|
|
if result != test.expected {
|
|
t.Errorf("parsePort(%q) = %v, expected %v", test.input, result, test.expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParsePortRange(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
expected []tailcfg.PortRange
|
|
err string
|
|
}{
|
|
{"80", []tailcfg.PortRange{{First: 80, Last: 80}}, ""},
|
|
{"80-90", []tailcfg.PortRange{{First: 80, Last: 90}}, ""},
|
|
{"80,90", []tailcfg.PortRange{{First: 80, Last: 80}, {First: 90, Last: 90}}, ""},
|
|
{"80-91,92,93-95", []tailcfg.PortRange{{First: 80, Last: 91}, {First: 92, Last: 92}, {First: 93, Last: 95}}, ""},
|
|
{"*", []tailcfg.PortRange{tailcfg.PortRangeAny}, ""},
|
|
{"80-", nil, "invalid port range format"},
|
|
{"-90", nil, "invalid port range format"},
|
|
{"80-90,", nil, "invalid port number"},
|
|
{"80,90-", nil, "invalid port range format"},
|
|
{"80-90,abc", nil, "invalid port number"},
|
|
{"80-90,65536", nil, "port number out of range"},
|
|
{"80-90,90-80", nil, "invalid port range: first port is greater than last port"},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
result, err := parsePortRange(test.input)
|
|
if err != nil && err.Error() != test.err {
|
|
t.Errorf("parsePortRange(%q) error = %v, expected error = %v", test.input, err, test.err)
|
|
}
|
|
|
|
if err == nil && test.err != "" {
|
|
t.Errorf("parsePortRange(%q) expected error = %v, got nil", test.input, test.err)
|
|
}
|
|
|
|
if diff := cmp.Diff(result, test.expected); diff != "" {
|
|
t.Errorf("parsePortRange(%q) mismatch (-want +got):\n%s", test.input, diff)
|
|
}
|
|
}
|
|
}
|