mirror of
https://github.com/juanfont/headscale.git
synced 2026-08-15 20:00:43 +09:00
4cca63155d
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.
Pattern applied across the tree:
In-package [Foo], [Foo.Bar]
Cross-package [pkg.Foo], [pkg.Foo.Bar]
Stdlib [netip.Prefix], [errors.Is], [context.Context]
Tailscale [tailcfg.MapResponse], [tailcfg.Node.CapMap],
[tailcfg.NodeAttrSuggestExitNode]
Skip rules:
- File:line refs left as plain text
- HuJSON wire keys inside backtick raw strings untouched
- ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
symbols, left as plain text
- JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
link tags, decorative dividers — all left as-is
151 lines
4.3 KiB
Go
151 lines
4.3 KiB
Go
package v2
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/netip"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"tailscale.com/tailcfg"
|
|
)
|
|
|
|
// Port parsing errors.
|
|
var (
|
|
ErrInputMissingColon = errors.New("input must contain a colon character separating destination and port")
|
|
ErrInputStartsWithColon = errors.New("input cannot start with a colon character")
|
|
ErrInputEndsWithColon = errors.New("input cannot end with a colon character")
|
|
ErrInvalidPortRangeFormat = errors.New("invalid port range format")
|
|
ErrPortRangeInverted = errors.New("invalid port range: first port is greater than last port")
|
|
ErrPortMustBePositive = errors.New("first port must be >0, or use '*' for wildcard")
|
|
ErrInvalidPortNumber = errors.New("invalid first integer")
|
|
ErrPortNumberOutOfRange = errors.New("port number out of range")
|
|
ErrBracketsNotIPv6 = errors.New("square brackets are only valid around IPv6 addresses")
|
|
)
|
|
|
|
// splitDestinationAndPort takes an input string and returns the destination and port as a tuple, or an error if the input is invalid.
|
|
// It supports two bracketed IPv6 forms:
|
|
// - "[addr]:port" (RFC 3986, e.g. "[::1]:80")
|
|
// - "[addr]/prefix:port" (e.g. "[fd7a::1]/128:80,443")
|
|
//
|
|
// Brackets are only accepted around IPv6 addresses, not IPv4, hostnames, or other alias types.
|
|
// Bracket stripping reduces both forms to bare "addr:port" or "addr/prefix:port",
|
|
// which the normal [strings.LastIndex] of ":" split handles correctly because
|
|
// port strings never contain colons.
|
|
func splitDestinationAndPort(input string) (string, string, error) {
|
|
// Handle RFC 3986 bracketed IPv6 (e.g. "[::1]:80" or "[fd7a::1]/128:80,443").
|
|
// Strip brackets after validation and fall through to normal parsing.
|
|
if strings.HasPrefix(input, "[") {
|
|
closeBracket := strings.Index(input, "]")
|
|
if closeBracket == -1 {
|
|
return "", "", ErrBracketsNotIPv6
|
|
}
|
|
|
|
host := input[1:closeBracket]
|
|
|
|
addr, err := netip.ParseAddr(host)
|
|
if err != nil || !addr.Is6() {
|
|
return "", "", fmt.Errorf("%w: %q", ErrBracketsNotIPv6, host)
|
|
}
|
|
|
|
rest := input[closeBracket+1:]
|
|
if len(rest) == 0 || (rest[0] != ':' && rest[0] != '/') {
|
|
return "", "", fmt.Errorf("%w: %q", ErrBracketsNotIPv6, input)
|
|
}
|
|
|
|
// Strip brackets: "[addr]:port" → "addr:port",
|
|
// "[addr]/prefix:port" → "addr/prefix:port".
|
|
input = host + rest
|
|
}
|
|
|
|
// Find the last occurrence of the colon character
|
|
lastColonIndex := strings.LastIndex(input, ":")
|
|
|
|
// Check if the colon character is present and not at the beginning or end of the string
|
|
if lastColonIndex == -1 {
|
|
return "", "", ErrInputMissingColon
|
|
}
|
|
|
|
if lastColonIndex == 0 {
|
|
return "", "", ErrInputStartsWithColon
|
|
}
|
|
|
|
if lastColonIndex == len(input)-1 {
|
|
return "", "", ErrInputEndsWithColon
|
|
}
|
|
|
|
// Split the string into destination and port based on the last colon
|
|
destination := input[:lastColonIndex]
|
|
port := input[lastColonIndex+1:]
|
|
|
|
return destination, port, nil
|
|
}
|
|
|
|
// parsePortRange parses a port definition string and returns a slice of [tailcfg.PortRange] structs.
|
|
func parsePortRange(portDef string) ([]tailcfg.PortRange, error) {
|
|
if portDef == "*" {
|
|
return []tailcfg.PortRange{tailcfg.PortRangeAny}, nil
|
|
}
|
|
|
|
var portRanges []tailcfg.PortRange
|
|
|
|
parts := strings.SplitSeq(portDef, ",")
|
|
|
|
for part := range parts {
|
|
if strings.Contains(part, "-") {
|
|
rangeParts := strings.Split(part, "-")
|
|
|
|
rangeParts = slices.DeleteFunc(rangeParts, func(e string) bool {
|
|
return e == ""
|
|
})
|
|
if len(rangeParts) != 2 {
|
|
return nil, ErrInvalidPortRangeFormat
|
|
}
|
|
|
|
first, err := parsePort(rangeParts[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
last, err := parsePort(rangeParts[1])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if first > last {
|
|
return nil, ErrPortRangeInverted
|
|
}
|
|
|
|
portRanges = append(portRanges, tailcfg.PortRange{First: first, Last: last})
|
|
} else {
|
|
port, err := parsePort(part)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if port < 1 {
|
|
return nil, ErrPortMustBePositive
|
|
}
|
|
|
|
portRanges = append(portRanges, tailcfg.PortRange{First: port, Last: port})
|
|
}
|
|
}
|
|
|
|
return portRanges, nil
|
|
}
|
|
|
|
// parsePort parses a single port number from a string.
|
|
func parsePort(portStr string) (uint16, error) {
|
|
port, err := strconv.Atoi(portStr)
|
|
if err != nil {
|
|
return 0, ErrInvalidPortNumber
|
|
}
|
|
|
|
if port < 0 || port > 65535 {
|
|
return 0, ErrPortNumberOutOfRange
|
|
}
|
|
|
|
return uint16(port), nil
|
|
}
|