Release 2.6.1: resolve AWG endpoint hostnames to IP before connect.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package awg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
@@ -9,6 +10,7 @@ import (
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Conf is a parsed AmneziaWG / WireGuard client configuration (AWG 2.0 fields included).
|
||||
@@ -444,7 +446,117 @@ func HostPort(raw string) (host, port string, err error) {
|
||||
return h, p, nil
|
||||
}
|
||||
|
||||
// lookupIP is overridable in tests.
|
||||
var lookupIP = defaultLookupIP
|
||||
|
||||
func defaultLookupIP(host string) ([]net.IP, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
|
||||
defer cancel()
|
||||
|
||||
try := func(r *net.Resolver) ([]net.IP, error) {
|
||||
addrs, err := r.LookupIPAddr(ctx, host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]net.IP, 0, len(addrs))
|
||||
for _, a := range addrs {
|
||||
if a.IP != nil {
|
||||
out = append(out, a.IP)
|
||||
}
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil, fmt.Errorf("нет IP-адресов")
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// PreferGo avoids flaky Windows WinDNS "No such host" for some A/AAAA shapes.
|
||||
if ips, err := try(&net.Resolver{PreferGo: true, Dial: resolverDial}); err == nil {
|
||||
return ips, nil
|
||||
}
|
||||
if ips, err := try(net.DefaultResolver); err == nil {
|
||||
return ips, nil
|
||||
}
|
||||
// Last resort: classic LookupIP (system).
|
||||
return net.LookupIP(host)
|
||||
}
|
||||
|
||||
func resolverDial(ctx context.Context, network, _ string) (net.Conn, error) {
|
||||
d := net.Dialer{Timeout: 3 * time.Second}
|
||||
var last error
|
||||
for _, dns := range []string{"1.1.1.1:53", "8.8.8.8:53", "9.9.9.9:53"} {
|
||||
c, err := d.DialContext(ctx, network, dns)
|
||||
if err == nil {
|
||||
return c, nil
|
||||
}
|
||||
last = err
|
||||
}
|
||||
return nil, last
|
||||
}
|
||||
|
||||
// ResolveEndpoint turns "hostname:port" into "ip:port" for amneziawg-go IpcSet
|
||||
// (which fails on bare hostnames with "No such host is known"). Prefers IPv4.
|
||||
func ResolveEndpoint(endpoint string) (string, error) {
|
||||
return resolveEndpointWith(endpoint, lookupIP)
|
||||
}
|
||||
|
||||
// ResolveEndpoint rewrites c.Endpoint to a resolved IP:port (IPv4 preferred).
|
||||
func (c *Conf) ResolveEndpoint() error {
|
||||
resolved, err := ResolveEndpoint(c.Endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Endpoint = resolved
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveEndpointWith(endpoint string, lookup func(string) ([]net.IP, error)) (string, error) {
|
||||
endpoint = strings.TrimSpace(endpoint)
|
||||
if endpoint == "" {
|
||||
return "", fmt.Errorf("awg: нет Endpoint")
|
||||
}
|
||||
host, port, err := net.SplitHostPort(endpoint)
|
||||
if err != nil {
|
||||
host = trimBrackets(endpoint)
|
||||
port = "51820"
|
||||
} else {
|
||||
host = trimBrackets(host)
|
||||
}
|
||||
if host == "" {
|
||||
return "", fmt.Errorf("awg: нет Endpoint")
|
||||
}
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
return net.JoinHostPort(ip.String(), port), nil
|
||||
}
|
||||
ips, err := lookup(host)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("awg: не удалось разрешить DNS для Endpoint %q: %v", host, err)
|
||||
}
|
||||
var v4, v6 net.IP
|
||||
for _, ip := range ips {
|
||||
if ip == nil {
|
||||
continue
|
||||
}
|
||||
if ip4 := ip.To4(); ip4 != nil {
|
||||
v4 = ip4
|
||||
break
|
||||
}
|
||||
if v6 == nil {
|
||||
v6 = ip
|
||||
}
|
||||
}
|
||||
chosen := v4
|
||||
if chosen == nil {
|
||||
chosen = v6
|
||||
}
|
||||
if chosen == nil {
|
||||
return "", fmt.Errorf("awg: не удалось разрешить DNS для Endpoint %q: нет IP-адресов", host)
|
||||
}
|
||||
return net.JoinHostPort(chosen.String(), port), nil
|
||||
}
|
||||
|
||||
// ToIPC builds the amneziawg-go UAPI configuration string.
|
||||
// Endpoint hostnames are resolved to IP first (IPv4 preferred).
|
||||
func (c Conf) ToIPC() (string, error) {
|
||||
priv, err := decodeKey(c.PrivateKey)
|
||||
if err != nil {
|
||||
@@ -454,6 +566,10 @@ func (c Conf) ToIPC() (string, error) {
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("public key: %w", err)
|
||||
}
|
||||
endpoint, err := ResolveEndpoint(c.Endpoint)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var b strings.Builder
|
||||
b.WriteString("private_key=")
|
||||
b.WriteString(hex.EncodeToString(priv))
|
||||
@@ -477,7 +593,7 @@ func (c Conf) ToIPC() (string, error) {
|
||||
b.WriteString("\npublic_key=")
|
||||
b.WriteString(hex.EncodeToString(pub))
|
||||
b.WriteString("\nendpoint=")
|
||||
b.WriteString(c.Endpoint)
|
||||
b.WriteString(endpoint)
|
||||
for _, ip := range c.AllowedIPs {
|
||||
b.WriteString("\nallowed_ip=")
|
||||
b.WriteString(strings.TrimSpace(ip))
|
||||
|
||||
@@ -2,6 +2,7 @@ package awg
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -139,6 +140,81 @@ func TestDecodeKeyURLSafe(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveEndpointPrefersIPv4(t *testing.T) {
|
||||
got, err := resolveEndpointWith("peer.example:41421", func(host string) ([]net.IP, error) {
|
||||
if host != "peer.example" {
|
||||
t.Fatalf("host %q", host)
|
||||
}
|
||||
return []net.IP{
|
||||
net.ParseIP("2001:db8::1"),
|
||||
net.ParseIP("203.0.113.44"),
|
||||
}, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != "203.0.113.44:41421" {
|
||||
t.Fatalf("got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveEndpointAlreadyIP(t *testing.T) {
|
||||
got, err := resolveEndpointWith("203.0.113.10:51820", func(string) ([]net.IP, error) {
|
||||
t.Fatal("lookup should not run for IP")
|
||||
return nil, nil
|
||||
})
|
||||
if err != nil || got != "203.0.113.10:51820" {
|
||||
t.Fatalf("%q %v", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveEndpointDNSError(t *testing.T) {
|
||||
_, err := resolveEndpointWith("missing.invalid:41421", func(string) ([]net.IP, error) {
|
||||
return nil, &net.DNSError{Err: "no such host", Name: "missing.invalid", IsNotFound: true}
|
||||
})
|
||||
if err == nil || !stringsContains(err.Error(), "не удалось разрешить DNS") {
|
||||
t.Fatalf("expected Russian DNS error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToIPCResolvesEndpoint(t *testing.T) {
|
||||
old := lookupIP
|
||||
lookupIP = func(host string) ([]net.IP, error) {
|
||||
return []net.IP{net.ParseIP("198.51.100.9")}, nil
|
||||
}
|
||||
defer func() { lookupIP = old }()
|
||||
|
||||
c := Conf{
|
||||
PrivateKey: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
|
||||
PublicKey: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE=",
|
||||
Address: []string{"10.8.0.2/32"},
|
||||
Endpoint: "peer.example:41421",
|
||||
}
|
||||
ipc, err := c.ToIPC()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !stringsContains(ipc, "endpoint=198.51.100.9:41421") {
|
||||
t.Fatalf("ipc missing resolved endpoint: %s", ipc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfResolveEndpointMutates(t *testing.T) {
|
||||
old := lookupIP
|
||||
lookupIP = func(string) ([]net.IP, error) {
|
||||
return []net.IP{net.ParseIP("198.51.100.1")}, nil
|
||||
}
|
||||
defer func() { lookupIP = old }()
|
||||
|
||||
c := Conf{Endpoint: "host.example:41421"}
|
||||
if err := c.ResolveEndpoint(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c.Endpoint != "198.51.100.1:41421" {
|
||||
t.Fatalf("endpoint %q", c.Endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
func containsAll(s string, parts ...string) bool {
|
||||
for _, p := range parts {
|
||||
if !stringsContains(s, p) {
|
||||
|
||||
@@ -46,6 +46,10 @@ func (e *Engine) Start(ctx context.Context, profile config.Profile, _ string) er
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// amneziawg-go IpcSet does not resolve hostnames reliably (Windows WSAHOST_NOT_FOUND).
|
||||
if err := cfg.ResolveEndpoint(); err != nil {
|
||||
return err
|
||||
}
|
||||
ipc, err := cfg.ToIPC()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
Reference in New Issue
Block a user