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 }