From 24243e21d5a4e939fc87dd1c3cf9692cf8c35ae7 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 18 Feb 2026 08:28:25 +0000 Subject: [PATCH] policy/v2: add policy unmarshal tests for bracketed IPv6 Add end-to-end test cases to TestUnmarshalPolicy that verify bracketed IPv6 addresses are correctly parsed through the full policy pipeline (JSON unmarshal -> splitDestinationAndPort -> parseAlias -> parsePortRange) and survive JSON round-trips. Cover single port, multiple ports, wildcard port, CIDR prefix, port range, bracketed IPv4, and hostname rejection. Updates #2754 --- hscontrol/policy/v2/types_test.go | 186 ++++++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) diff --git a/hscontrol/policy/v2/types_test.go b/hscontrol/policy/v2/types_test.go index acea9c28..6cbc7822 100644 --- a/hscontrol/policy/v2/types_test.go +++ b/hscontrol/policy/v2/types_test.go @@ -1766,6 +1766,192 @@ func TestUnmarshalPolicy(t *testing.T) { }, }, }, + // Issue #2754: IPv6 addresses with brackets in ACL destinations. + { + name: "2754-bracketed-ipv6-single-port", + input: ` +{ + "acls": [{ + "action": "accept", + "src": ["alice@"], + "dst": ["[fd7a:115c:a1e0::87e1]:443"] + }] +} +`, + want: &Policy{ + ACLs: []ACL{ + { + Action: "accept", + Sources: Aliases{ + up("alice@"), + }, + Destinations: []AliasWithPorts{ + { + Alias: pp("fd7a:115c:a1e0::87e1/128"), + Ports: []tailcfg.PortRange{{First: 443, Last: 443}}, + }, + }, + }, + }, + }, + }, + { + name: "2754-bracketed-ipv6-multiple-ports", + input: ` +{ + "acls": [{ + "action": "accept", + "src": ["alice@"], + "dst": ["[fd7a:115c:a1e0::87e1]:80,443"] + }] +} +`, + want: &Policy{ + ACLs: []ACL{ + { + Action: "accept", + Sources: Aliases{ + up("alice@"), + }, + Destinations: []AliasWithPorts{ + { + Alias: pp("fd7a:115c:a1e0::87e1/128"), + Ports: []tailcfg.PortRange{ + {First: 80, Last: 80}, + {First: 443, Last: 443}, + }, + }, + }, + }, + }, + }, + }, + { + name: "2754-bracketed-ipv6-wildcard-port", + input: ` +{ + "acls": [{ + "action": "accept", + "src": ["alice@"], + "dst": ["[fd7a:115c:a1e0::87e1]:*"] + }] +} +`, + want: &Policy{ + ACLs: []ACL{ + { + Action: "accept", + Sources: Aliases{ + up("alice@"), + }, + Destinations: []AliasWithPorts{ + { + Alias: pp("fd7a:115c:a1e0::87e1/128"), + Ports: []tailcfg.PortRange{tailcfg.PortRangeAny}, + }, + }, + }, + }, + }, + }, + { + name: "2754-bracketed-ipv6-cidr-inside-rejected", + input: ` +{ + "acls": [{ + "action": "accept", + "src": ["alice@"], + "dst": ["[fd7a:115c:a1e0::/48]:443"] + }] +} +`, + wantErr: "square brackets are only valid around IPv6 addresses", + }, + { + name: "2754-bracketed-ipv6-port-range", + input: ` +{ + "acls": [{ + "action": "accept", + "src": ["alice@"], + "dst": ["[::1]:80-443"] + }] +} +`, + want: &Policy{ + ACLs: []ACL{ + { + Action: "accept", + Sources: Aliases{ + up("alice@"), + }, + Destinations: []AliasWithPorts{ + { + Alias: pp("::1/128"), + Ports: []tailcfg.PortRange{{First: 80, Last: 443}}, + }, + }, + }, + }, + }, + }, + { + name: "2754-bracketed-ipv6-cidr-outside-brackets", + input: ` +{ + "acls": [{ + "action": "accept", + "src": ["alice@"], + "dst": ["[fd7a:115c:a1e0::2905]/128:80,443"] + }] +} +`, + want: &Policy{ + ACLs: []ACL{ + { + Action: "accept", + Sources: Aliases{ + up("alice@"), + }, + Destinations: []AliasWithPorts{ + { + Alias: pp("fd7a:115c:a1e0::2905/128"), + Ports: []tailcfg.PortRange{ + {First: 80, Last: 80}, + {First: 443, Last: 443}, + }, + }, + }, + }, + }, + }, + }, + { + name: "2754-bracketed-ipv4-rejected", + input: ` +{ + "acls": [{ + "action": "accept", + "src": ["alice@"], + "dst": ["[192.168.1.1]:80"] + }] +} +`, + wantErr: "square brackets are only valid around IPv6 addresses", + }, + { + name: "2754-bracketed-hostname-rejected", + input: ` +{ + "acls": [{ + "action": "accept", + "src": ["alice@"], + "dst": ["[my-hostname]:80"] + }] +} +`, + wantErr: "square brackets are only valid around IPv6 addresses", + }, } cmps := append(util.Comparers,