115 lines
3.5 KiB
Go
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
|
|
}
|