mirror of
https://github.com/fosrl/newt.git
synced 2026-09-30 01:39:08 +02:00
324 lines
8.9 KiB
Go
324 lines
8.9 KiB
Go
package util
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"net"
|
|
"strings"
|
|
|
|
mathrand "math/rand/v2"
|
|
|
|
"github.com/fosrl/newt/logger"
|
|
"golang.zx2c4.com/wireguard/device"
|
|
)
|
|
|
|
// splitDomainHostPort strips a protocol prefix/trailing slash from domain and
|
|
// separates it into host and port (port may be ""). If host is already a
|
|
// literal IP address (v4 or v6, brackets stripped), literalIP is non-nil and
|
|
// resolution can be skipped entirely.
|
|
func splitDomainHostPort(domain string) (host, port string, literalIP net.IP) {
|
|
domain = strings.TrimSpace(domain)
|
|
domain = strings.TrimPrefix(domain, "http://")
|
|
domain = strings.TrimPrefix(domain, "https://")
|
|
domain = strings.TrimSuffix(domain, "/")
|
|
|
|
host, port, err := net.SplitHostPort(domain)
|
|
if err != nil {
|
|
// No port found, use the domain as is
|
|
host = domain
|
|
port = ""
|
|
}
|
|
|
|
// Check if host is already an IP address (IPv4 or IPv6)
|
|
// For IPv6, the host from SplitHostPort will already have brackets stripped
|
|
// but if there was no port, we need to handle bracketed IPv6 addresses
|
|
cleanHost := strings.TrimPrefix(strings.TrimSuffix(host, "]"), "[")
|
|
return host, port, net.ParseIP(cleanHost)
|
|
}
|
|
|
|
// resolveIPs looks up every address (all families) for host, preferring the
|
|
// given upstream DNS servers (each queried directly over UDP) when provided.
|
|
// If every upstream server is unreachable - e.g. the only configured/system
|
|
// DNS server is only reachable over an address family this process's own
|
|
// socket path doesn't currently have a route for (IPv6-only mobile networks
|
|
// commonly hand out IPv6-only resolvers) - this falls back to the platform's
|
|
// own resolver, which routes independently of our socket path and reliably
|
|
// works even then. See https://github.com/fosrl/android/issues/42 and
|
|
// https://github.com/fosrl/pangolin/issues/3471.
|
|
func resolveIPs(host string, publicDNS []string) ([]net.IP, error) {
|
|
if len(publicDNS) == 0 {
|
|
return net.LookupIP(host)
|
|
}
|
|
|
|
var lastErr error
|
|
for _, server := range publicDNS {
|
|
// Ensure the upstream DNS address has a port
|
|
dnsAddr := server
|
|
if _, _, err := net.SplitHostPort(dnsAddr); err != nil {
|
|
// No port specified, default to 53
|
|
dnsAddr = net.JoinHostPort(server, "53")
|
|
}
|
|
|
|
resolver := &net.Resolver{
|
|
PreferGo: true,
|
|
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
|
|
d := net.Dialer{}
|
|
return d.DialContext(ctx, "udp", dnsAddr)
|
|
},
|
|
}
|
|
ips, err := resolver.LookupIP(context.Background(), "ip", host)
|
|
if err == nil {
|
|
return ips, nil
|
|
}
|
|
lastErr = err
|
|
}
|
|
|
|
if ips, err := net.LookupIP(host); err == nil {
|
|
logger.Debug("All upstream DNS servers failed to resolve %s (%v), falling back to platform resolver", host, lastErr)
|
|
return ips, nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("DNS lookup failed using all upstream servers: %v", lastErr)
|
|
}
|
|
|
|
// pickAddr chooses a single address from ips, preferring IPv4 for
|
|
// backward-compatible callers that only ever use one address (e.g. a
|
|
// WireGuard peer endpoint). Returns "" if ips is empty.
|
|
func pickAddr(ips []net.IP) string {
|
|
for _, ip := range ips {
|
|
if ipv4 := ip.To4(); ipv4 != nil {
|
|
return ipv4.String()
|
|
}
|
|
}
|
|
if len(ips) == 0 {
|
|
return ""
|
|
}
|
|
return ips[0].String()
|
|
}
|
|
|
|
func ResolveDomainUpstream(domain string, publicDNS []string) (string, error) {
|
|
host, port, literalIP := splitDomainHostPort(domain)
|
|
if literalIP != nil {
|
|
if port != "" {
|
|
return net.JoinHostPort(literalIP.String(), port), nil
|
|
}
|
|
return literalIP.String(), nil
|
|
}
|
|
|
|
ips, err := resolveIPs(host, publicDNS)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if len(ips) == 0 {
|
|
return "", fmt.Errorf("no IP addresses found for domain %s", host)
|
|
}
|
|
|
|
ipAddr := pickAddr(ips)
|
|
if port != "" {
|
|
ipAddr = net.JoinHostPort(ipAddr, port)
|
|
}
|
|
return ipAddr, nil
|
|
}
|
|
|
|
func ResolveDomain(domain string) (string, error) {
|
|
return ResolveDomainUpstream(domain, nil)
|
|
}
|
|
|
|
// ResolveDomainAllUpstream resolves domain to every candidate address (all
|
|
// families, deduplicated), each formatted as "ip:port" (or bare ip if domain
|
|
// had no port). Unlike ResolveDomainUpstream, which collapses to a single
|
|
// IPv4-preferred address, this lets a caller that can try more than one
|
|
// candidate (e.g. UDP hole punching) reach the destination over whichever
|
|
// address family the local network path actually has a route for, instead of
|
|
// always preferring an IPv4 address that may be completely unreachable (e.g.
|
|
// on an IPv6-only/NAT64 network). See
|
|
// https://github.com/fosrl/olm/issues/108.
|
|
func ResolveDomainAllUpstream(domain string, publicDNS []string) ([]string, error) {
|
|
host, port, literalIP := splitDomainHostPort(domain)
|
|
if literalIP != nil {
|
|
if port != "" {
|
|
return []string{net.JoinHostPort(literalIP.String(), port)}, nil
|
|
}
|
|
return []string{literalIP.String()}, nil
|
|
}
|
|
|
|
ips, err := resolveIPs(host, publicDNS)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(ips) == 0 {
|
|
return nil, fmt.Errorf("no IP addresses found for domain %s", host)
|
|
}
|
|
|
|
seen := make(map[string]bool, len(ips))
|
|
results := make([]string, 0, len(ips))
|
|
for _, ip := range ips {
|
|
s := ip.String()
|
|
if seen[s] {
|
|
continue
|
|
}
|
|
seen[s] = true
|
|
if port != "" {
|
|
s = net.JoinHostPort(s, port)
|
|
}
|
|
results = append(results, s)
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
// ResolveDomainAll is ResolveDomainAllUpstream using only the system/platform
|
|
// resolver (no explicit upstream DNS servers).
|
|
func ResolveDomainAll(domain string) ([]string, error) {
|
|
return ResolveDomainAllUpstream(domain, nil)
|
|
}
|
|
|
|
func ParseLogLevel(level string) logger.LogLevel {
|
|
switch strings.ToUpper(level) {
|
|
case "DEBUG":
|
|
return logger.DEBUG
|
|
case "INFO":
|
|
return logger.INFO
|
|
case "WARN":
|
|
return logger.WARN
|
|
case "ERROR":
|
|
return logger.ERROR
|
|
case "FATAL":
|
|
return logger.FATAL
|
|
default:
|
|
return logger.INFO // default to INFO if invalid level provided
|
|
}
|
|
}
|
|
|
|
// find an available UDP port in the range [minPort, maxPort] and also the next port for the wgtester
|
|
func FindAvailableUDPPort(minPort, maxPort uint16) (uint16, error) {
|
|
if maxPort < minPort {
|
|
return 0, fmt.Errorf("invalid port range: min=%d, max=%d", minPort, maxPort)
|
|
}
|
|
|
|
// We need to check port+1 as well, so adjust the max port to avoid going out of range
|
|
adjustedMaxPort := maxPort - 1
|
|
if adjustedMaxPort < minPort {
|
|
return 0, fmt.Errorf("insufficient port range to find consecutive ports: min=%d, max=%d", minPort, maxPort)
|
|
}
|
|
|
|
// Create a slice of all ports in the range (excluding the last one)
|
|
portRange := make([]uint16, adjustedMaxPort-minPort+1)
|
|
for i := range portRange {
|
|
portRange[i] = minPort + uint16(i)
|
|
}
|
|
|
|
// Fisher-Yates shuffle to randomize the port order
|
|
for i := len(portRange) - 1; i > 0; i-- {
|
|
j := mathrand.IntN(i + 1)
|
|
portRange[i], portRange[j] = portRange[j], portRange[i]
|
|
}
|
|
|
|
// Try each port in the randomized order
|
|
for _, port := range portRange {
|
|
// Check if port is available
|
|
addr1 := &net.UDPAddr{
|
|
IP: net.ParseIP("127.0.0.1"),
|
|
Port: int(port),
|
|
}
|
|
conn1, err1 := net.ListenUDP("udp", addr1)
|
|
if err1 != nil {
|
|
continue // Port is in use or there was an error, try next port
|
|
}
|
|
|
|
conn1.Close()
|
|
return port, nil
|
|
}
|
|
|
|
return 0, fmt.Errorf("no available consecutive UDP ports found in range %d-%d", minPort, maxPort)
|
|
}
|
|
|
|
func FixKey(key string) string {
|
|
// Remove any whitespace
|
|
key = strings.TrimSpace(key)
|
|
|
|
// Decode from base64
|
|
decoded, err := base64.StdEncoding.DecodeString(key)
|
|
if err != nil {
|
|
logger.Fatal("Error decoding base64: %v", err)
|
|
}
|
|
|
|
// Convert to hex
|
|
return hex.EncodeToString(decoded)
|
|
}
|
|
|
|
// this is the opposite of FixKey
|
|
func UnfixKey(hexKey string) string {
|
|
// Decode from hex
|
|
decoded, err := hex.DecodeString(hexKey)
|
|
if err != nil {
|
|
logger.Fatal("Error decoding hex: %v", err)
|
|
}
|
|
|
|
// Convert to base64
|
|
return base64.StdEncoding.EncodeToString(decoded)
|
|
}
|
|
|
|
func MapToWireGuardLogLevel(level logger.LogLevel) int {
|
|
switch level {
|
|
case logger.DEBUG:
|
|
return device.LogLevelVerbose
|
|
// case logger.INFO:
|
|
// return device.LogLevel
|
|
case logger.WARN:
|
|
return device.LogLevelError
|
|
case logger.ERROR, logger.FATAL:
|
|
return device.LogLevelSilent
|
|
default:
|
|
return device.LogLevelSilent
|
|
}
|
|
}
|
|
|
|
// GetProtocol returns protocol number from IPv4 packet (fast path)
|
|
func GetProtocol(packet []byte) (uint8, bool) {
|
|
if len(packet) < 20 {
|
|
return 0, false
|
|
}
|
|
version := packet[0] >> 4
|
|
if version == 4 {
|
|
return packet[9], true
|
|
} else if version == 6 {
|
|
if len(packet) < 40 {
|
|
return 0, false
|
|
}
|
|
return packet[6], true
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
// GetDestPort returns destination port from TCP/UDP packet (fast path)
|
|
func GetDestPort(packet []byte) (uint16, bool) {
|
|
if len(packet) < 20 {
|
|
return 0, false
|
|
}
|
|
|
|
version := packet[0] >> 4
|
|
var headerLen int
|
|
|
|
if version == 4 {
|
|
ihl := packet[0] & 0x0F
|
|
headerLen = int(ihl) * 4
|
|
if len(packet) < headerLen+4 {
|
|
return 0, false
|
|
}
|
|
} else if version == 6 {
|
|
headerLen = 40
|
|
if len(packet) < headerLen+4 {
|
|
return 0, false
|
|
}
|
|
} else {
|
|
return 0, false
|
|
}
|
|
|
|
// Destination port is at bytes 2-3 of TCP/UDP header
|
|
port := binary.BigEndian.Uint16(packet[headerLen+2 : headerLen+4])
|
|
return port, true
|
|
}
|