Files
dnshelper/resolver/parse.go
T

115 lines
3.5 KiB
Go

package resolver
import (
"fmt"
"net"
"strconv"
"strings"
"time"
)
// ServerMode represents the DNS resolution strategy selected by the -server flag.
type ServerMode struct {
Mode string // "default", "local", "gateway", "explicit"
ExplicitAddr string // IP or IP:port when Mode is "explicit"
}
// QueryConfig holds per-invocation DNS query settings.
type QueryConfig struct {
Timeout time.Duration // Per-query timeout (default 3s)
Verbose bool // Emit diagnostic trace to stderr
}
// BootstrapResolvers is the ordered list of public DNS servers used as fallback
// (and as the complete pool in "default" mode when no local resolvers are found).
var BootstrapResolvers = []string{
"1.1.1.1",
"8.8.8.8",
"1.0.0.1",
"8.8.4.4",
"9.9.9.9",
"208.67.222.222",
}
// PrivateTLDs are locally significant top-level domain suffixes that should
// only be resolved by the local DNS server (FR-007).
var PrivateTLDs = map[string]bool{
"local": true,
"internal": true,
"lan": true,
"home": true,
"corp": true,
"private": true,
}
// ParseServerFlag parses the value of the -server CLI flag into a ServerMode.
//
// Recognised values (case-insensitive keywords):
// - "" → Mode "default"
// - "local" → Mode "local"
// - "gateway" → Mode "gateway"
// - IP → Mode "explicit", ExplicitAddr set to the bare IP
// - IP:port → Mode "explicit", ExplicitAddr set to "IP:port" (port 1-65535)
//
// Any other value returns an error.
func ParseServerFlag(value string) (ServerMode, error) {
switch strings.ToLower(value) {
case "":
return ServerMode{Mode: "default"}, nil
case "local":
return ServerMode{Mode: "local"}, nil
case "gateway":
return ServerMode{Mode: "gateway"}, nil
}
// Try to parse as IP:port first.
host, portStr, err := net.SplitHostPort(value)
if err == nil {
// SplitHostPort succeeded — validate the host and port.
if net.ParseIP(host) == nil {
return ServerMode{}, fmt.Errorf("invalid server address %q: host is not a valid IP", value)
}
port, convErr := strconv.Atoi(portStr)
if convErr != nil {
return ServerMode{}, fmt.Errorf("invalid port in server address %q: %w", value, convErr)
}
if port < 1 || port > 65535 {
return ServerMode{}, fmt.Errorf("invalid port in server address %q: port must be between 1 and 65535", value)
}
return ServerMode{Mode: "explicit", ExplicitAddr: value}, nil
}
// No port — check if it is a bare IP address.
if ip := net.ParseIP(value); ip != nil {
return ServerMode{Mode: "explicit", ExplicitAddr: value}, nil
}
return ServerMode{}, fmt.Errorf("invalid server value %q: must be empty, \"local\", \"gateway\", a bare IP, or IP:port", value)
}
// ExtractLabelLevels returns all queryable domain levels from hostname, from
// most-specific to least-specific, excluding single-label names.
//
// Examples:
// - "host.sub.example.com" → ["host.sub.example.com", "sub.example.com", "example.com"]
// - "www.example.com" → ["www.example.com", "example.com"]
// - "example.com" → ["example.com"]
// - "localhost" → nil
// - "example.com." → ["example.com"] (trailing dot stripped)
func ExtractLabelLevels(hostname string) []string {
// Strip trailing dot (FQDN notation).
hostname = strings.TrimSuffix(hostname, ".")
parts := strings.Split(hostname, ".")
if len(parts) < 2 {
return nil
}
// Generate all suffixes with at least 2 labels.
var levels []string
for i := 0; i <= len(parts)-2; i++ {
levels = append(levels, strings.Join(parts[i:], "."))
}
return levels
}