From 9169886426c7710ad931a464696c4db82b2bfe37 Mon Sep 17 00:00:00 2001 From: Mygod Date: Wed, 22 Jul 2026 00:27:18 -0400 Subject: [PATCH 1/3] Support dual-stack UDP wildcard listeners --- README.MD | 2 +- README_ES.MD | 2 +- README_FA.MD | 2 +- README_IT.MD | 2 +- README_RU.MD | 2 +- README_ZH.MD | 2 +- internal/config/server.go | 3 +- internal/config/server_test.go | 21 +++ internal/udpserver/server_runtime.go | 7 +- .../udpserver/server_runtime_linux_test.go | 156 ++++++++++++++++++ server_config.toml.simple | 4 +- 11 files changed, 194 insertions(+), 9 deletions(-) create mode 100644 internal/udpserver/server_runtime_linux_test.go diff --git a/README.MD b/README.MD index 9b65229f..3505cc51 100644 --- a/README.MD +++ b/README.MD @@ -714,7 +714,7 @@ These settings are critical on the server: | Parameter | Sample Value | Allowed Values / Real Behavior | Full Explanation | | :--- | :--- | :--- | :--- | -| `UDP_HOST` | `"0.0.0.0"` | if empty, this value is used | Address where the DNS server binds.
`0.0.0.0` means listen on all interfaces. | +| `UDP_HOST` | `"0.0.0.0"` | if empty, this value is used | Address where the DNS server binds.
The generic UDP wildcard (`0.0.0.0`) accepts both IPv4 and IPv6 on systems with IPv4-mapped IPv6 support; otherwise it remains IPv4-only. Use `::` to explicitly request the IPv6 wildcard, which also accepts IPv4-mapped traffic where supported. A concrete address restricts the listener to that family. | | `UDP_PORT` | `53` | `1..65535` | UDP port used by the server.
In most deployments this should remain `53` so resolvers can query it directly. | | `UDP_READERS` | `4` | auto-default if `<=0` | Number of goroutines reading directly from the UDP socket.
A larger number may help on very busy servers, but beyond a point it only increases context switching. | | `DNS_REQUEST_WORKERS` | `8` | auto-default if `<=0` | Number of workers that take requests from the front-door queue and pass them into the session/decode layer. | diff --git a/README_ES.MD b/README_ES.MD index 5de4f2a8..499909aa 100644 --- a/README_ES.MD +++ b/README_ES.MD @@ -716,7 +716,7 @@ Estos ajustes son críticos en el servidor: | Parámetro | Valor de ejemplo | Valores permitidos / Comportamiento real | Explicación completa | | :--- | :--- | :--- | :--- | -| `UDP_HOST` | `"0.0.0.0"` | si está vacío, se usa este valor | Dirección donde se vincula el servidor DNS.
`0.0.0.0` significa escuchar en todas las interfaces. | +| `UDP_HOST` | `"0.0.0.0"` | si está vacío, se usa este valor | Dirección donde se vincula el servidor DNS.
El comodín UDP genérico (`0.0.0.0` o `::`) acepta IPv4 e IPv6 en sistemas compatibles con IPv6 que mapea IPv4; de lo contrario, Go usa la familia de direcciones disponible. Una dirección concreta limita el listener a esa familia. | | `UDP_PORT` | `53` | `1..65535` | Puerto UDP usado por el servidor.
En la mayoría de los despliegues debe permanecer en `53` para que los resolutores puedan consultarlo directamente. | | `UDP_READERS` | `4` | valor automático por defecto si es `<=0` | Número de goroutines que leen directamente del socket UDP.
Un número mayor puede ayudar en servidores muy concurridos, pero a partir de cierto punto solo aumenta el cambio de contexto. | | `DNS_REQUEST_WORKERS` | `8` | valor automático por defecto si es `<=0` | Número de workers que toman solicitudes de la cola de entrada y las pasan a la capa de sesión/decodificación. | diff --git a/README_FA.MD b/README_FA.MD index a73ae7c8..51261ee8 100644 --- a/README_FA.MD +++ b/README_FA.MD @@ -685,7 +685,7 @@ sudo journalctl -u masterdnsvpn-client -f | پارامتر | مقدار نمونه | مقادیر مجاز / رفتار واقعی | توضیح کامل | | :--- | :--- | :--- | :--- | -| `UDP_HOST` | `"0.0.0.0"` | اگر خالی باشد همین مقدار استفاده می‌شود | آدرسی که سرور DNS روی آن bind می‌شود.
`0.0.0.0` یعنی روی همه interfaceها گوش بدهد. | +| `UDP_HOST` | `"0.0.0.0"` | اگر خالی باشد همین مقدار استفاده می‌شود | آدرسی که سرور DNS روی آن bind می‌شود.
wildcard عمومی UDP (`0.0.0.0` یا `::`) در سیستم‌هایی که IPv6 نگاشت‌شده از IPv4 را پشتیبانی می‌کنند، هم IPv4 و هم IPv6 را می‌پذیرد؛ در غیر این صورت Go از خانوادهٔ آدرس موجود استفاده می‌کند. یک آدرس مشخص، listener را به همان خانواده محدود می‌کند. | | `UDP_PORT` | `53` | `1..65535` | پورت UDP سرور است.
به‌طور معمول باید همان `53` باشد تا resolverها بتوانند مستقیماً به آن query بفرستند. | | `UDP_READERS` | `4` | اگر `<=0` باشد auto-default | تعداد goroutineهای خواندن مستقیم از socket UDP.
عدد بالاتر در سرورهای پر ترافیک مفید است، ولی از یک حد به بعد فقط context switching را زیاد می‌کند. | | `DNS_REQUEST_WORKERS` | `8` | اگر `<=0` باشد auto-default | تعداد workerهایی که requestهای ورودی را از front-door queue برمی‌دارند و به لایه session/decode می‌دهند. | diff --git a/README_IT.MD b/README_IT.MD index 21941e24..a0d0ca9d 100644 --- a/README_IT.MD +++ b/README_IT.MD @@ -716,7 +716,7 @@ Queste impostazioni sono critiche sul server: | Parametro | Valore di Esempio | Valori Consentiti / Comportamento Reale | Spiegazione Completa | | :--- | :--- | :--- | :--- | -| `UDP_HOST` | `"0.0.0.0"` | se vuoto, viene usato questo valore | Indirizzo su cui il server DNS si lega.
`0.0.0.0` significa ascoltare su tutte le interfacce. | +| `UDP_HOST` | `"0.0.0.0"` | se vuoto, viene usato questo valore | Indirizzo su cui il server DNS si lega.
Il wildcard UDP generico (`0.0.0.0` o `::`) accetta sia IPv4 sia IPv6 sui sistemi che supportano IPv6 con mapping IPv4; altrimenti Go usa la famiglia di indirizzi disponibile. Un indirizzo concreto limita il listener a quella famiglia. | | `UDP_PORT` | `53` | `1..65535` | Porta UDP usata dal server.
Nella maggior parte delle distribuzioni questa dovrebbe rimanere `53` così che i resolver possano interrogarla direttamente. | | `UDP_READERS` | `4` | predefinito automatico se `<=0` | Numero di goroutine che leggono direttamente dal socket UDP.
Un numero maggiore può essere d'aiuto su server molto trafficati, ma oltre un certo punto aumenta solo il context switching. | | `DNS_REQUEST_WORKERS` | `8` | predefinito automatico se `<=0` | Numero di worker che prelevano le richieste dalla coda di ingresso e le passano al livello di sessione/decodifica. | diff --git a/README_RU.MD b/README_RU.MD index d7d06191..ea85b086 100644 --- a/README_RU.MD +++ b/README_RU.MD @@ -719,7 +719,7 @@ Copy-Item client_resolvers.simple client_resolvers.txt | Парамент | Пример значения | Значения | Полное описание | | :--- | :--- | :--- | :--- | -| `UDP_HOST` | `"0.0.0.0"` | если поле пустое, используется значение по умолчанию. | Адрес, на котором привязан DNS-сервер.
`0.0.0.0` означает прослушивание на всех интерфейсах. | +| `UDP_HOST` | `"0.0.0.0"` | если поле пустое, используется значение по умолчанию. | Адрес, на котором привязан DNS-сервер.
Универсальный UDP-адрес (`0.0.0.0` или `::`) принимает IPv4 и IPv6 в системах с поддержкой IPv4-mapped IPv6; в остальных случаях Go использует доступное семейство адресов. Конкретный адрес ограничивает listener этим семейством. | | `UDP_PORT` | `53` | `1..65535` | UDP-порт, используемый сервером.
В большинстве случаев должно быть `53`, чтобы резолверы могли напрямую отправлять запросы на этот порт. | | `UDP_READERS` | `4` | По умолчанию если `<=0` | Количество горутин, осуществляющих чтение непосредственно из UDP-сокета.
Большее количество может помочь на очень загруженных серверах, но после определенного предела это только увеличивает количество переключений контекста. | | `DNS_REQUEST_WORKERS` | `8` | По умолчанию если `<=0` | Количество обработчиков, которые принимают запросы из входной очереди и передают их на уровень сеанса/декодирования. | diff --git a/README_ZH.MD b/README_ZH.MD index 97b20612..fc19662a 100644 --- a/README_ZH.MD +++ b/README_ZH.MD @@ -716,7 +716,7 @@ Copy-Item client_resolvers.simple client_resolvers.txt | 参数 | 示例值 | 允许的值 / 实际行为 | 完整说明 | | :--- | :--- | :--- | :--- | -| `UDP_HOST` | `"0.0.0.0"` | 若为空则使用此值 | DNS 服务器绑定的地址。
`0.0.0.0` 表示监听所有网络接口。 | +| `UDP_HOST` | `"0.0.0.0"` | 若为空则使用此值 | DNS 服务器绑定的地址。
通用 UDP 通配地址(`0.0.0.0` 或 `::`)在支持 IPv4 映射 IPv6 的系统上同时接受 IPv4 和 IPv6;否则 Go 使用可用的地址族。指定具体地址会将监听器限制为该地址族。 | | `UDP_PORT` | `53` | `1..65535` | 服务器使用的 UDP 端口。
在大多数部署中应保持为 `53`,以便解析器能直接查询它。 | | `UDP_READERS` | `4` | 若 `<=0` 则使用自动默认值 | 直接从 UDP 套接字读取的 goroutine 数量。
较大的数值在非常繁忙的服务器上可能有帮助,但超过某个点后只会增加上下文切换。 | | `DNS_REQUEST_WORKERS` | `8` | 若 `<=0` 则使用自动默认值 | 从前门队列中取出请求并将其传入会话/解码层的工作线程数量。 | diff --git a/internal/config/server.go b/internal/config/server.go index 927e9b2a..ff46054c 100644 --- a/internal/config/server.go +++ b/internal/config/server.go @@ -10,6 +10,7 @@ package config import ( "flag" "fmt" + "net" "os" "path/filepath" "reflect" @@ -472,7 +473,7 @@ func finalizeServerConfig(cfg ServerConfig) (ServerConfig, error) { } func (c ServerConfig) Address() string { - return fmt.Sprintf("%s:%d", c.UDPHost, c.UDPPort) + return net.JoinHostPort(c.UDPHost, strconv.Itoa(c.UDPPort)) } func (c ServerConfig) DropLogInterval() time.Duration { diff --git a/internal/config/server_test.go b/internal/config/server_test.go index f489041e..77f8988e 100644 --- a/internal/config/server_test.go +++ b/internal/config/server_test.go @@ -15,6 +15,27 @@ import ( "testing" ) +func TestServerConfigAddress(t *testing.T) { + tests := []struct { + name string + host string + want string + }{ + {name: "IPv4", host: "0.0.0.0", want: "0.0.0.0:53"}, + {name: "IPv6", host: "::", want: "[::]:53"}, + {name: "concrete IPv6", host: "2001:db8::1", want: "[2001:db8::1]:53"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := ServerConfig{UDPHost: tt.host, UDPPort: 53} + if got := cfg.Address(); got != tt.want { + t.Fatalf("Address()=%q want=%q", got, tt.want) + } + }) + } +} + func TestLoadServerConfigWithOverridesAppliesFlagPrecedence(t *testing.T) { dir := t.TempDir() configPath := filepath.Join(dir, "server_config.toml") diff --git a/internal/udpserver/server_runtime.go b/internal/udpserver/server_runtime.go index 6cd98b20..5ff02945 100644 --- a/internal/udpserver/server_runtime.go +++ b/internal/udpserver/server_runtime.go @@ -28,8 +28,13 @@ func (s *Server) configureSocketBuffers(conn *net.UDPConn) { } func (s *Server) openUDPListeners() ([]*net.UDPConn, error) { + ip := net.ParseIP(s.cfg.UDPHost) + if ip != nil && ip.IsUnspecified() && ip.To4() != nil { + // Let Go select a dual-stack wildcard socket where the platform supports it. + ip = nil + } addr := &net.UDPAddr{ - IP: net.ParseIP(s.cfg.UDPHost), + IP: ip, Port: s.cfg.UDPPort, } desired := s.cfg.EffectiveUDPReaders() diff --git a/internal/udpserver/server_runtime_linux_test.go b/internal/udpserver/server_runtime_linux_test.go new file mode 100644 index 00000000..df9b82b8 --- /dev/null +++ b/internal/udpserver/server_runtime_linux_test.go @@ -0,0 +1,156 @@ +//go:build linux + +package udpserver + +import ( + "net" + "testing" + "time" + + "masterdnsvpn-go/internal/config" + "masterdnsvpn-go/internal/logger" +) + +const dualStackTestTimeout = 2 * time.Second + +func TestWildcardUDPListenerIsDualStack(t *testing.T) { + probe, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv6unspecified}) + if err != nil { + t.Skipf("IPv6 wildcard unavailable: %v", err) + } + defer probe.Close() + probeAddr := probe.LocalAddr().(*net.UDPAddr) + probeClient, err := net.DialUDP("udp4", nil, &net.UDPAddr{ + IP: net.IPv4(127, 0, 0, 1), + Port: probeAddr.Port, + }) + if err != nil { + t.Skipf("IPv4 loopback unavailable: %v", err) + } + defer probeClient.Close() + probeDeadline := time.Now().Add(dualStackTestTimeout) + if err := probe.SetReadDeadline(probeDeadline); err != nil { + t.Skipf("set mapped-IPv6 probe read deadline: %v", err) + } + if err := probeClient.SetWriteDeadline(probeDeadline); err != nil { + t.Skipf("set mapped-IPv6 probe write deadline: %v", err) + } + if _, err := probeClient.Write([]byte("probe")); err != nil { + t.Skipf("IPv4-mapped IPv6 unavailable: %v", err) + } + probeBuffer := make([]byte, 5) + n, _, err := probe.ReadFromUDP(probeBuffer) + if err != nil { + t.Skipf("IPv4-mapped IPv6 unavailable: %v", err) + } + if string(probeBuffer[:n]) != "probe" { + t.Fatalf("mapped-IPv6 probe received %q", probeBuffer[:n]) + } + + server := New(config.ServerConfig{ + UDPHost: "0.0.0.0", + UDPPort: 0, + UDPReaders: 1, + SocketBufferSize: 64 * 1024, + }, logger.New("Dual Stack UDP Test", "ERROR"), nil) + conns, err := server.openUDPListeners() + if err != nil { + t.Fatalf("openUDPListeners failed: %v", err) + } + if len(conns) == 0 { + t.Fatal("openUDPListeners returned no listeners") + } + listener := conns[0] + t.Cleanup(func() { _ = listener.Close() }) + for i, conn := range conns { + localAddr := conn.LocalAddr().(*net.UDPAddr) + if localAddr.IP.To4() != nil { + t.Fatalf("wildcard listener %d is IPv4-only: %s", i, localAddr) + } + if i != 0 { + _ = conn.Close() + } + } + localAddr := listener.LocalAddr().(*net.UDPAddr) + + tests := []struct { + name string + network string + ip net.IP + }{ + {name: "IPv4", network: "udp4", ip: net.IPv4(127, 0, 0, 1)}, + {name: "IPv6", network: "udp6", ip: net.IPv6loopback}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + target := &net.UDPAddr{IP: tt.ip, Port: localAddr.Port} + client, err := net.DialUDP(tt.network, nil, target) + if err != nil { + t.Fatalf("dial %s: %v", target, err) + } + defer client.Close() + + deadline := time.Now().Add(dualStackTestTimeout) + if err := listener.SetDeadline(deadline); err != nil { + t.Fatalf("set listener deadline: %v", err) + } + if err := client.SetDeadline(deadline); err != nil { + t.Fatalf("set client deadline: %v", err) + } + + payload := []byte(tt.name) + if _, err := client.Write(payload); err != nil { + t.Fatalf("write to %s: %v", target, err) + } + buffer := make([]byte, 64) + n, peer, err := listener.ReadFromUDP(buffer) + if err != nil { + t.Fatalf("read from %s: %v", target, err) + } + if string(buffer[:n]) != tt.name { + t.Fatalf("listener received %q want=%q", buffer[:n], payload) + } + if _, err := listener.WriteToUDP(buffer[:n], peer); err != nil { + t.Fatalf("reply to %s: %v", peer, err) + } + n, err = client.Read(buffer) + if err != nil { + t.Fatalf("read reply from %s: %v", target, err) + } + if string(buffer[:n]) != tt.name { + t.Fatalf("client received %q want=%q", buffer[:n], payload) + } + }) + } +} + +func TestExplicitIPv6WildcardRemainsIPv6(t *testing.T) { + probe, err := net.ListenUDP("udp6", &net.UDPAddr{IP: net.IPv6loopback}) + if err != nil { + t.Skipf("IPv6 loopback unavailable: %v", err) + } + _ = probe.Close() + + server := New(config.ServerConfig{ + UDPHost: "::", + UDPPort: 0, + UDPReaders: 1, + SocketBufferSize: 64 * 1024, + }, logger.New("IPv6 Wildcard UDP Test", "ERROR"), nil) + conns, err := server.openUDPListeners() + if err != nil { + t.Fatalf("openUDPListeners failed: %v", err) + } + if len(conns) == 0 { + t.Fatal("openUDPListeners returned no listeners") + } + for _, conn := range conns { + t.Cleanup(func() { _ = conn.Close() }) + } + for i, conn := range conns { + if localAddr := conn.LocalAddr().(*net.UDPAddr); localAddr.IP.To4() != nil { + t.Fatalf("explicit IPv6 wildcard listener %d became IPv4-only: %s", i, localAddr) + } + } +} diff --git a/server_config.toml.simple b/server_config.toml.simple index b37d66b0..e8247901 100644 --- a/server_config.toml.simple +++ b/server_config.toml.simple @@ -40,7 +40,9 @@ SUPPORTED_DOWNLOAD_COMPRESSION_TYPES = [0, 1, 2, 3] # - Defines front-door queueing and worker limits before session handling. # ------------------------------------------------------------------------------ -# UDP bind endpoint. +# UDP bind endpoint. On systems with IPv4-mapped IPv6 support, the default +# "0.0.0.0" accepts both IPv4 and IPv6 through the same bind. Use "::" to +# explicitly request an IPv6 wildcard on systems without mapped IPv6 support. UDP_HOST = "0.0.0.0" UDP_PORT = 53 From d8fe635ac2c2ffc0f49410bca3b5beb1f4b226ff Mon Sep 17 00:00:00 2001 From: Mygod Date: Thu, 23 Jul 2026 01:03:27 -0400 Subject: [PATCH 2/3] Use IPv6 wildcard as the default UDP bind --- README.MD | 2 +- README_ES.MD | 2 +- README_FA.MD | 2 +- README_IT.MD | 2 +- README_RU.MD | 2 +- README_ZH.MD | 2 +- internal/config/server.go | 7 +++- internal/config/server_test.go | 38 +++++++++++++++++++ internal/udpserver/server_runtime.go | 7 +--- .../udpserver/server_runtime_linux_test.go | 4 +- server_config.toml.simple | 9 +++-- 11 files changed, 57 insertions(+), 20 deletions(-) diff --git a/README.MD b/README.MD index 3505cc51..4160e600 100644 --- a/README.MD +++ b/README.MD @@ -714,7 +714,7 @@ These settings are critical on the server: | Parameter | Sample Value | Allowed Values / Real Behavior | Full Explanation | | :--- | :--- | :--- | :--- | -| `UDP_HOST` | `"0.0.0.0"` | if empty, this value is used | Address where the DNS server binds.
The generic UDP wildcard (`0.0.0.0`) accepts both IPv4 and IPv6 on systems with IPv4-mapped IPv6 support; otherwise it remains IPv4-only. Use `::` to explicitly request the IPv6 wildcard, which also accepts IPv4-mapped traffic where supported. A concrete address restricts the listener to that family. | +| `UDP_HOST` | `"::"` | unbracketed IP literal; if empty, this value is used | Address where the DNS server binds.
The default IPv6 wildcard (`::`) accepts IPv4-mapped traffic where supported, so one socket receives both IPv6 and IPv4; otherwise it remains IPv6-only. Use `0.0.0.0` for the IPv4 wildcard. A concrete address restricts the listener to that family. | | `UDP_PORT` | `53` | `1..65535` | UDP port used by the server.
In most deployments this should remain `53` so resolvers can query it directly. | | `UDP_READERS` | `4` | auto-default if `<=0` | Number of goroutines reading directly from the UDP socket.
A larger number may help on very busy servers, but beyond a point it only increases context switching. | | `DNS_REQUEST_WORKERS` | `8` | auto-default if `<=0` | Number of workers that take requests from the front-door queue and pass them into the session/decode layer. | diff --git a/README_ES.MD b/README_ES.MD index 499909aa..872c2858 100644 --- a/README_ES.MD +++ b/README_ES.MD @@ -716,7 +716,7 @@ Estos ajustes son críticos en el servidor: | Parámetro | Valor de ejemplo | Valores permitidos / Comportamiento real | Explicación completa | | :--- | :--- | :--- | :--- | -| `UDP_HOST` | `"0.0.0.0"` | si está vacío, se usa este valor | Dirección donde se vincula el servidor DNS.
El comodín UDP genérico (`0.0.0.0` o `::`) acepta IPv4 e IPv6 en sistemas compatibles con IPv6 que mapea IPv4; de lo contrario, Go usa la familia de direcciones disponible. Una dirección concreta limita el listener a esa familia. | +| `UDP_HOST` | `"::"` | literal IP sin corchetes; si está vacío, se usa este valor | Dirección donde se vincula el servidor DNS.
El comodín IPv6 predeterminado (`::`) acepta tráfico IPv4 mapeado donde sea compatible, por lo que un solo socket recibe IPv6 e IPv4; de lo contrario, permanece solo en IPv6. Usa `0.0.0.0` para el comodín IPv4. Una dirección concreta limita el listener a esa familia. | | `UDP_PORT` | `53` | `1..65535` | Puerto UDP usado por el servidor.
En la mayoría de los despliegues debe permanecer en `53` para que los resolutores puedan consultarlo directamente. | | `UDP_READERS` | `4` | valor automático por defecto si es `<=0` | Número de goroutines que leen directamente del socket UDP.
Un número mayor puede ayudar en servidores muy concurridos, pero a partir de cierto punto solo aumenta el cambio de contexto. | | `DNS_REQUEST_WORKERS` | `8` | valor automático por defecto si es `<=0` | Número de workers que toman solicitudes de la cola de entrada y las pasan a la capa de sesión/decodificación. | diff --git a/README_FA.MD b/README_FA.MD index 51261ee8..c558f8b0 100644 --- a/README_FA.MD +++ b/README_FA.MD @@ -685,7 +685,7 @@ sudo journalctl -u masterdnsvpn-client -f | پارامتر | مقدار نمونه | مقادیر مجاز / رفتار واقعی | توضیح کامل | | :--- | :--- | :--- | :--- | -| `UDP_HOST` | `"0.0.0.0"` | اگر خالی باشد همین مقدار استفاده می‌شود | آدرسی که سرور DNS روی آن bind می‌شود.
wildcard عمومی UDP (`0.0.0.0` یا `::`) در سیستم‌هایی که IPv6 نگاشت‌شده از IPv4 را پشتیبانی می‌کنند، هم IPv4 و هم IPv6 را می‌پذیرد؛ در غیر این صورت Go از خانوادهٔ آدرس موجود استفاده می‌کند. یک آدرس مشخص، listener را به همان خانواده محدود می‌کند. | +| `UDP_HOST` | `"::"` | IP بدون براکت؛ اگر خالی باشد همین مقدار استفاده می‌شود | آدرسی که سرور DNS روی آن bind می‌شود.
wildcard پیش‌فرض IPv6 (`::`) در سیستم‌های سازگار، ترافیک IPv4 نگاشت‌شده را نیز می‌پذیرد تا یک socket هم IPv6 و هم IPv4 را دریافت کند؛ در غیر این صورت فقط IPv6 باقی می‌ماند. برای wildcard مربوط به IPv4 از `0.0.0.0` استفاده کنید. یک آدرس مشخص، listener را به همان خانواده محدود می‌کند. | | `UDP_PORT` | `53` | `1..65535` | پورت UDP سرور است.
به‌طور معمول باید همان `53` باشد تا resolverها بتوانند مستقیماً به آن query بفرستند. | | `UDP_READERS` | `4` | اگر `<=0` باشد auto-default | تعداد goroutineهای خواندن مستقیم از socket UDP.
عدد بالاتر در سرورهای پر ترافیک مفید است، ولی از یک حد به بعد فقط context switching را زیاد می‌کند. | | `DNS_REQUEST_WORKERS` | `8` | اگر `<=0` باشد auto-default | تعداد workerهایی که requestهای ورودی را از front-door queue برمی‌دارند و به لایه session/decode می‌دهند. | diff --git a/README_IT.MD b/README_IT.MD index a0d0ca9d..b018d039 100644 --- a/README_IT.MD +++ b/README_IT.MD @@ -716,7 +716,7 @@ Queste impostazioni sono critiche sul server: | Parametro | Valore di Esempio | Valori Consentiti / Comportamento Reale | Spiegazione Completa | | :--- | :--- | :--- | :--- | -| `UDP_HOST` | `"0.0.0.0"` | se vuoto, viene usato questo valore | Indirizzo su cui il server DNS si lega.
Il wildcard UDP generico (`0.0.0.0` o `::`) accetta sia IPv4 sia IPv6 sui sistemi che supportano IPv6 con mapping IPv4; altrimenti Go usa la famiglia di indirizzi disponibile. Un indirizzo concreto limita il listener a quella famiglia. | +| `UDP_HOST` | `"::"` | indirizzo IP letterale senza parentesi; se vuoto, viene usato questo valore | Indirizzo su cui il server DNS si lega.
Il wildcard IPv6 predefinito (`::`) accetta il traffico IPv4 mappato dove supportato, quindi un solo socket riceve sia IPv6 sia IPv4; altrimenti rimane solo IPv6. Usa `0.0.0.0` per il wildcard IPv4. Un indirizzo concreto limita il listener a quella famiglia. | | `UDP_PORT` | `53` | `1..65535` | Porta UDP usata dal server.
Nella maggior parte delle distribuzioni questa dovrebbe rimanere `53` così che i resolver possano interrogarla direttamente. | | `UDP_READERS` | `4` | predefinito automatico se `<=0` | Numero di goroutine che leggono direttamente dal socket UDP.
Un numero maggiore può essere d'aiuto su server molto trafficati, ma oltre un certo punto aumenta solo il context switching. | | `DNS_REQUEST_WORKERS` | `8` | predefinito automatico se `<=0` | Numero di worker che prelevano le richieste dalla coda di ingresso e le passano al livello di sessione/decodifica. | diff --git a/README_RU.MD b/README_RU.MD index ea85b086..5ba59c23 100644 --- a/README_RU.MD +++ b/README_RU.MD @@ -719,7 +719,7 @@ Copy-Item client_resolvers.simple client_resolvers.txt | Парамент | Пример значения | Значения | Полное описание | | :--- | :--- | :--- | :--- | -| `UDP_HOST` | `"0.0.0.0"` | если поле пустое, используется значение по умолчанию. | Адрес, на котором привязан DNS-сервер.
Универсальный UDP-адрес (`0.0.0.0` или `::`) принимает IPv4 и IPv6 в системах с поддержкой IPv4-mapped IPv6; в остальных случаях Go использует доступное семейство адресов. Конкретный адрес ограничивает listener этим семейством. | +| `UDP_HOST` | `"::"` | IP-литерал без скобок; если поле пустое, используется значение по умолчанию. | Адрес, на котором привязан DNS-сервер.
IPv6-адрес по умолчанию (`::`) принимает IPv4-mapped трафик там, где это поддерживается, поэтому один сокет обслуживает IPv6 и IPv4; в противном случае он остается только IPv6. Используйте `0.0.0.0` для IPv4 wildcard. Конкретный адрес ограничивает listener этим семейством. | | `UDP_PORT` | `53` | `1..65535` | UDP-порт, используемый сервером.
В большинстве случаев должно быть `53`, чтобы резолверы могли напрямую отправлять запросы на этот порт. | | `UDP_READERS` | `4` | По умолчанию если `<=0` | Количество горутин, осуществляющих чтение непосредственно из UDP-сокета.
Большее количество может помочь на очень загруженных серверах, но после определенного предела это только увеличивает количество переключений контекста. | | `DNS_REQUEST_WORKERS` | `8` | По умолчанию если `<=0` | Количество обработчиков, которые принимают запросы из входной очереди и передают их на уровень сеанса/декодирования. | diff --git a/README_ZH.MD b/README_ZH.MD index fc19662a..778e01e2 100644 --- a/README_ZH.MD +++ b/README_ZH.MD @@ -716,7 +716,7 @@ Copy-Item client_resolvers.simple client_resolvers.txt | 参数 | 示例值 | 允许的值 / 实际行为 | 完整说明 | | :--- | :--- | :--- | :--- | -| `UDP_HOST` | `"0.0.0.0"` | 若为空则使用此值 | DNS 服务器绑定的地址。
通用 UDP 通配地址(`0.0.0.0` 或 `::`)在支持 IPv4 映射 IPv6 的系统上同时接受 IPv4 和 IPv6;否则 Go 使用可用的地址族。指定具体地址会将监听器限制为该地址族。 | +| `UDP_HOST` | `"::"` | 不带方括号的 IP 字面量;若为空则使用此值 | DNS 服务器绑定的地址。
默认 IPv6 通配地址(`::`)在系统支持时也接受 IPv4 映射流量,因此一个套接字可同时接收 IPv6 和 IPv4;否则它仅使用 IPv6。使用 `0.0.0.0` 可请求 IPv4 通配地址。指定具体地址会将监听器限制为该地址族。 | | `UDP_PORT` | `53` | `1..65535` | 服务器使用的 UDP 端口。
在大多数部署中应保持为 `53`,以便解析器能直接查询它。 | | `UDP_READERS` | `4` | 若 `<=0` 则使用自动默认值 | 直接从 UDP 套接字读取的 goroutine 数量。
较大的数值在非常繁忙的服务器上可能有帮助,但超过某个点后只会增加上下文切换。 | | `DNS_REQUEST_WORKERS` | `8` | 若 `<=0` 则使用自动默认值 | 从前门队列中取出请求并将其传入会话/解码层的工作线程数量。 | diff --git a/internal/config/server.go b/internal/config/server.go index ff46054c..7e3e5a9f 100644 --- a/internal/config/server.go +++ b/internal/config/server.go @@ -115,7 +115,7 @@ type ServerConfigFlagBinder struct { func defaultServerConfig() ServerConfig { return ServerConfig{ ProtocolType: "SOCKS5", - UDPHost: "0.0.0.0", + UDPHost: "::", UDPPort: 53, UDPReaders: 4, SocketBufferSize: 8 * 1024 * 1024, @@ -283,7 +283,10 @@ func finalizeServerConfig(cfg ServerConfig) (ServerConfig, error) { } if cfg.UDPHost == "" { - cfg.UDPHost = "0.0.0.0" + cfg.UDPHost = defaultServerConfig().UDPHost + } + if net.ParseIP(cfg.UDPHost) == nil { + return cfg, fmt.Errorf("invalid UDP_HOST %q: must be an unbracketed IP literal", cfg.UDPHost) } if cfg.UDPPort <= 0 || cfg.UDPPort > 65535 { diff --git a/internal/config/server_test.go b/internal/config/server_test.go index 77f8988e..94e7f2b2 100644 --- a/internal/config/server_test.go +++ b/internal/config/server_test.go @@ -36,6 +36,41 @@ func TestServerConfigAddress(t *testing.T) { } } +func TestServerConfigUDPHostValidation(t *testing.T) { + tests := []struct { + name string + host string + want string + wantErr bool + }{ + {name: "empty uses IPv6 wildcard", want: "::"}, + {name: "IPv4 literal", host: "0.0.0.0", want: "0.0.0.0"}, + {name: "IPv6 literal", host: "2001:db8::1", want: "2001:db8::1"}, + {name: "non-IP value", host: "not-an-ip", wantErr: true}, + {name: "bracketed IPv6 literal", host: "[::]", wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := defaultServerConfig() + cfg.UDPHost = tt.host + got, err := finalizeServerConfig(cfg) + if tt.wantErr { + if err == nil { + t.Fatalf("finalizeServerConfig(%q) unexpectedly succeeded", tt.host) + } + return + } + if err != nil { + t.Fatalf("finalizeServerConfig(%q) returned error: %v", tt.host, err) + } + if got.UDPHost != tt.want { + t.Fatalf("UDPHost=%q want=%q", got.UDPHost, tt.want) + } + }) + } +} + func TestLoadServerConfigWithOverridesAppliesFlagPrecedence(t *testing.T) { dir := t.TempDir() configPath := filepath.Join(dir, "server_config.toml") @@ -291,6 +326,9 @@ func TestLoadServerConfigFromJSONBase64AppliesDefaults(t *testing.T) { if cfg.UDPPort != 5301 { t.Fatalf("unexpected JSON base64 UDP port: got=%d want=%d", cfg.UDPPort, 5301) } + if cfg.UDPHost != "::" { + t.Fatalf("unexpected default UDP host: got=%q want=%q", cfg.UDPHost, "::") + } if cfg.MaxPacketsPerBatch != defaultServerConfig().MaxPacketsPerBatch { t.Fatalf("expected default max packets per batch to apply: got=%d want=%d", cfg.MaxPacketsPerBatch, defaultServerConfig().MaxPacketsPerBatch) } diff --git a/internal/udpserver/server_runtime.go b/internal/udpserver/server_runtime.go index 5ff02945..6cd98b20 100644 --- a/internal/udpserver/server_runtime.go +++ b/internal/udpserver/server_runtime.go @@ -28,13 +28,8 @@ func (s *Server) configureSocketBuffers(conn *net.UDPConn) { } func (s *Server) openUDPListeners() ([]*net.UDPConn, error) { - ip := net.ParseIP(s.cfg.UDPHost) - if ip != nil && ip.IsUnspecified() && ip.To4() != nil { - // Let Go select a dual-stack wildcard socket where the platform supports it. - ip = nil - } addr := &net.UDPAddr{ - IP: ip, + IP: net.ParseIP(s.cfg.UDPHost), Port: s.cfg.UDPPort, } desired := s.cfg.EffectiveUDPReaders() diff --git a/internal/udpserver/server_runtime_linux_test.go b/internal/udpserver/server_runtime_linux_test.go index df9b82b8..b78210c0 100644 --- a/internal/udpserver/server_runtime_linux_test.go +++ b/internal/udpserver/server_runtime_linux_test.go @@ -13,7 +13,7 @@ import ( const dualStackTestTimeout = 2 * time.Second -func TestWildcardUDPListenerIsDualStack(t *testing.T) { +func TestIPv6WildcardUDPListenerIsDualStack(t *testing.T) { probe, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv6unspecified}) if err != nil { t.Skipf("IPv6 wildcard unavailable: %v", err) @@ -48,7 +48,7 @@ func TestWildcardUDPListenerIsDualStack(t *testing.T) { } server := New(config.ServerConfig{ - UDPHost: "0.0.0.0", + UDPHost: "::", UDPPort: 0, UDPReaders: 1, SocketBufferSize: 64 * 1024, diff --git a/server_config.toml.simple b/server_config.toml.simple index e8247901..e13e3e67 100644 --- a/server_config.toml.simple +++ b/server_config.toml.simple @@ -40,10 +40,11 @@ SUPPORTED_DOWNLOAD_COMPRESSION_TYPES = [0, 1, 2, 3] # - Defines front-door queueing and worker limits before session handling. # ------------------------------------------------------------------------------ -# UDP bind endpoint. On systems with IPv4-mapped IPv6 support, the default -# "0.0.0.0" accepts both IPv4 and IPv6 through the same bind. Use "::" to -# explicitly request an IPv6 wildcard on systems without mapped IPv6 support. -UDP_HOST = "0.0.0.0" +# UDP bind endpoint. Use an unbracketed IP literal. The default "::" accepts +# IPv4-mapped traffic through the same IPv6 socket where supported. Without +# IPv4-mapped IPv6 support it remains IPv6-only; use "0.0.0.0" for the IPv4 +# wildcard. +UDP_HOST = "::" UDP_PORT = 53 # UDP readers, DNS workers, and front-door request queue are smart-sized From 56219a883b0d84bcae53e98d2c55236fb69ebdf4 Mon Sep 17 00:00:00 2001 From: Mygod Date: Thu, 23 Jul 2026 16:47:12 -0400 Subject: [PATCH 3/3] Use a family-neutral default UDP bind --- README.MD | 2 +- README_ES.MD | 2 +- README_FA.MD | 2 +- README_IT.MD | 2 +- README_RU.MD | 2 +- README_ZH.MD | 2 +- internal/config/server.go | 7 ++----- internal/config/server_test.go | 7 ++++--- internal/udpserver/server_runtime.go | 10 +++++++++- .../udpserver/server_runtime_linux_test.go | 6 +++--- internal/udpserver/server_runtime_test.go | 18 ++++++++++++++++++ server_config.toml.simple | 9 ++++----- 12 files changed, 46 insertions(+), 23 deletions(-) create mode 100644 internal/udpserver/server_runtime_test.go diff --git a/README.MD b/README.MD index 4160e600..f83a44ce 100644 --- a/README.MD +++ b/README.MD @@ -714,7 +714,7 @@ These settings are critical on the server: | Parameter | Sample Value | Allowed Values / Real Behavior | Full Explanation | | :--- | :--- | :--- | :--- | -| `UDP_HOST` | `"::"` | unbracketed IP literal; if empty, this value is used | Address where the DNS server binds.
The default IPv6 wildcard (`::`) accepts IPv4-mapped traffic where supported, so one socket receives both IPv6 and IPv4; otherwise it remains IPv6-only. Use `0.0.0.0` for the IPv4 wildcard. A concrete address restricts the listener to that family. | +| `UDP_HOST` | `""` | empty for the family-neutral wildcard, or an unbracketed IP literal | Address where the DNS server binds.
The empty default lets Go choose a generic UDP wildcard. It accepts IPv4 and IPv6 through one socket where IPv4-mapped IPv6 is supported; otherwise Go selects an available family. Explicit `0.0.0.0` and `::` wildcards retain their IPv4 or IPv6 family where mapped dual-stack sockets are unavailable. A concrete address restricts the listener to that family. | | `UDP_PORT` | `53` | `1..65535` | UDP port used by the server.
In most deployments this should remain `53` so resolvers can query it directly. | | `UDP_READERS` | `4` | auto-default if `<=0` | Number of goroutines reading directly from the UDP socket.
A larger number may help on very busy servers, but beyond a point it only increases context switching. | | `DNS_REQUEST_WORKERS` | `8` | auto-default if `<=0` | Number of workers that take requests from the front-door queue and pass them into the session/decode layer. | diff --git a/README_ES.MD b/README_ES.MD index 872c2858..c5d8d1f9 100644 --- a/README_ES.MD +++ b/README_ES.MD @@ -716,7 +716,7 @@ Estos ajustes son críticos en el servidor: | Parámetro | Valor de ejemplo | Valores permitidos / Comportamiento real | Explicación completa | | :--- | :--- | :--- | :--- | -| `UDP_HOST` | `"::"` | literal IP sin corchetes; si está vacío, se usa este valor | Dirección donde se vincula el servidor DNS.
El comodín IPv6 predeterminado (`::`) acepta tráfico IPv4 mapeado donde sea compatible, por lo que un solo socket recibe IPv6 e IPv4; de lo contrario, permanece solo en IPv6. Usa `0.0.0.0` para el comodín IPv4. Una dirección concreta limita el listener a esa familia. | +| `UDP_HOST` | `""` | vacío para el comodín independiente de la familia, o un literal IP sin corchetes | Dirección donde se vincula el servidor DNS.
El valor vacío predeterminado permite que Go elija un comodín UDP genérico. Acepta IPv4 e IPv6 mediante un solo socket donde se admite IPv6 con mapeo de IPv4; de lo contrario, Go selecciona una familia disponible. Los comodines explícitos `0.0.0.0` y `::` conservan su familia IPv4 o IPv6 cuando no hay sockets de doble pila mapeados. Una dirección concreta limita el listener a esa familia. | | `UDP_PORT` | `53` | `1..65535` | Puerto UDP usado por el servidor.
En la mayoría de los despliegues debe permanecer en `53` para que los resolutores puedan consultarlo directamente. | | `UDP_READERS` | `4` | valor automático por defecto si es `<=0` | Número de goroutines que leen directamente del socket UDP.
Un número mayor puede ayudar en servidores muy concurridos, pero a partir de cierto punto solo aumenta el cambio de contexto. | | `DNS_REQUEST_WORKERS` | `8` | valor automático por defecto si es `<=0` | Número de workers que toman solicitudes de la cola de entrada y las pasan a la capa de sesión/decodificación. | diff --git a/README_FA.MD b/README_FA.MD index c558f8b0..b08c4b27 100644 --- a/README_FA.MD +++ b/README_FA.MD @@ -685,7 +685,7 @@ sudo journalctl -u masterdnsvpn-client -f | پارامتر | مقدار نمونه | مقادیر مجاز / رفتار واقعی | توضیح کامل | | :--- | :--- | :--- | :--- | -| `UDP_HOST` | `"::"` | IP بدون براکت؛ اگر خالی باشد همین مقدار استفاده می‌شود | آدرسی که سرور DNS روی آن bind می‌شود.
wildcard پیش‌فرض IPv6 (`::`) در سیستم‌های سازگار، ترافیک IPv4 نگاشت‌شده را نیز می‌پذیرد تا یک socket هم IPv6 و هم IPv4 را دریافت کند؛ در غیر این صورت فقط IPv6 باقی می‌ماند. برای wildcard مربوط به IPv4 از `0.0.0.0` استفاده کنید. یک آدرس مشخص، listener را به همان خانواده محدود می‌کند. | +| `UDP_HOST` | `""` | خالی برای wildcard مستقل از خانواده، یا IP بدون براکت | آدرسی که سرور DNS روی آن bind می‌شود.
مقدار خالی پیش‌فرض اجازه می‌دهد Go یک wildcard عمومی UDP انتخاب کند. در سیستم‌های دارای پشتیبانی IPv6 نگاشت‌شده از IPv4، یک socket هر دو IPv4 و IPv6 را می‌پذیرد؛ در غیر این صورت Go یک خانوادهٔ موجود را انتخاب می‌کند. wildcardهای صریح `0.0.0.0` و `::` در نبود socket دوپشتهٔ نگاشت‌شده، خانوادهٔ IPv4 یا IPv6 خود را حفظ می‌کنند. یک آدرس مشخص، listener را به همان خانواده محدود می‌کند. | | `UDP_PORT` | `53` | `1..65535` | پورت UDP سرور است.
به‌طور معمول باید همان `53` باشد تا resolverها بتوانند مستقیماً به آن query بفرستند. | | `UDP_READERS` | `4` | اگر `<=0` باشد auto-default | تعداد goroutineهای خواندن مستقیم از socket UDP.
عدد بالاتر در سرورهای پر ترافیک مفید است، ولی از یک حد به بعد فقط context switching را زیاد می‌کند. | | `DNS_REQUEST_WORKERS` | `8` | اگر `<=0` باشد auto-default | تعداد workerهایی که requestهای ورودی را از front-door queue برمی‌دارند و به لایه session/decode می‌دهند. | diff --git a/README_IT.MD b/README_IT.MD index b018d039..5c19f927 100644 --- a/README_IT.MD +++ b/README_IT.MD @@ -716,7 +716,7 @@ Queste impostazioni sono critiche sul server: | Parametro | Valore di Esempio | Valori Consentiti / Comportamento Reale | Spiegazione Completa | | :--- | :--- | :--- | :--- | -| `UDP_HOST` | `"::"` | indirizzo IP letterale senza parentesi; se vuoto, viene usato questo valore | Indirizzo su cui il server DNS si lega.
Il wildcard IPv6 predefinito (`::`) accetta il traffico IPv4 mappato dove supportato, quindi un solo socket riceve sia IPv6 sia IPv4; altrimenti rimane solo IPv6. Usa `0.0.0.0` per il wildcard IPv4. Un indirizzo concreto limita il listener a quella famiglia. | +| `UDP_HOST` | `""` | vuoto per il wildcard indipendente dalla famiglia, oppure un indirizzo IP letterale senza parentesi | Indirizzo su cui il server DNS si lega.
Il valore vuoto predefinito lascia che Go scelga un wildcard UDP generico. Accetta IPv4 e IPv6 tramite un solo socket dove è supportato IPv6 con mapping IPv4; altrimenti Go seleziona una famiglia disponibile. I wildcard espliciti `0.0.0.0` e `::` mantengono la propria famiglia IPv4 o IPv6 quando i socket dual-stack mappati non sono disponibili. Un indirizzo concreto limita il listener a quella famiglia. | | `UDP_PORT` | `53` | `1..65535` | Porta UDP usata dal server.
Nella maggior parte delle distribuzioni questa dovrebbe rimanere `53` così che i resolver possano interrogarla direttamente. | | `UDP_READERS` | `4` | predefinito automatico se `<=0` | Numero di goroutine che leggono direttamente dal socket UDP.
Un numero maggiore può essere d'aiuto su server molto trafficati, ma oltre un certo punto aumenta solo il context switching. | | `DNS_REQUEST_WORKERS` | `8` | predefinito automatico se `<=0` | Numero di worker che prelevano le richieste dalla coda di ingresso e le passano al livello di sessione/decodifica. | diff --git a/README_RU.MD b/README_RU.MD index 5ba59c23..9607fca7 100644 --- a/README_RU.MD +++ b/README_RU.MD @@ -719,7 +719,7 @@ Copy-Item client_resolvers.simple client_resolvers.txt | Парамент | Пример значения | Значения | Полное описание | | :--- | :--- | :--- | :--- | -| `UDP_HOST` | `"::"` | IP-литерал без скобок; если поле пустое, используется значение по умолчанию. | Адрес, на котором привязан DNS-сервер.
IPv6-адрес по умолчанию (`::`) принимает IPv4-mapped трафик там, где это поддерживается, поэтому один сокет обслуживает IPv6 и IPv4; в противном случае он остается только IPv6. Используйте `0.0.0.0` для IPv4 wildcard. Конкретный адрес ограничивает listener этим семейством. | +| `UDP_HOST` | `""` | пустое значение для wildcard без привязки к семейству или IP-литерал без скобок | Адрес, на котором привязан DNS-сервер.
Пустое значение по умолчанию позволяет Go выбрать универсальный UDP wildcard. При поддержке IPv4-mapped IPv6 один сокет принимает IPv4 и IPv6; иначе Go выбирает доступное семейство. Явные wildcard `0.0.0.0` и `::` сохраняют свое семейство IPv4 или IPv6, когда mapped dual-stack сокеты недоступны. Конкретный адрес ограничивает listener этим семейством. | | `UDP_PORT` | `53` | `1..65535` | UDP-порт, используемый сервером.
В большинстве случаев должно быть `53`, чтобы резолверы могли напрямую отправлять запросы на этот порт. | | `UDP_READERS` | `4` | По умолчанию если `<=0` | Количество горутин, осуществляющих чтение непосредственно из UDP-сокета.
Большее количество может помочь на очень загруженных серверах, но после определенного предела это только увеличивает количество переключений контекста. | | `DNS_REQUEST_WORKERS` | `8` | По умолчанию если `<=0` | Количество обработчиков, которые принимают запросы из входной очереди и передают их на уровень сеанса/декодирования. | diff --git a/README_ZH.MD b/README_ZH.MD index 778e01e2..64570125 100644 --- a/README_ZH.MD +++ b/README_ZH.MD @@ -716,7 +716,7 @@ Copy-Item client_resolvers.simple client_resolvers.txt | 参数 | 示例值 | 允许的值 / 实际行为 | 完整说明 | | :--- | :--- | :--- | :--- | -| `UDP_HOST` | `"::"` | 不带方括号的 IP 字面量;若为空则使用此值 | DNS 服务器绑定的地址。
默认 IPv6 通配地址(`::`)在系统支持时也接受 IPv4 映射流量,因此一个套接字可同时接收 IPv6 和 IPv4;否则它仅使用 IPv6。使用 `0.0.0.0` 可请求 IPv4 通配地址。指定具体地址会将监听器限制为该地址族。 | +| `UDP_HOST` | `""` | 留空表示与地址族无关的通配地址,或填写不带方括号的 IP 字面量 | DNS 服务器绑定的地址。
默认空值让 Go 选择通用 UDP 通配地址。在支持 IPv4 映射 IPv6 的系统上,一个套接字可同时接收 IPv4 和 IPv6;否则 Go 会选择可用的地址族。当映射双栈套接字不可用时,显式通配地址 `0.0.0.0` 和 `::` 会保留各自的 IPv4 或 IPv6 地址族。指定具体地址会将监听器限制为该地址族。 | | `UDP_PORT` | `53` | `1..65535` | 服务器使用的 UDP 端口。
在大多数部署中应保持为 `53`,以便解析器能直接查询它。 | | `UDP_READERS` | `4` | 若 `<=0` 则使用自动默认值 | 直接从 UDP 套接字读取的 goroutine 数量。
较大的数值在非常繁忙的服务器上可能有帮助,但超过某个点后只会增加上下文切换。 | | `DNS_REQUEST_WORKERS` | `8` | 若 `<=0` 则使用自动默认值 | 从前门队列中取出请求并将其传入会话/解码层的工作线程数量。 | diff --git a/internal/config/server.go b/internal/config/server.go index 7e3e5a9f..0dfc1fb6 100644 --- a/internal/config/server.go +++ b/internal/config/server.go @@ -115,7 +115,7 @@ type ServerConfigFlagBinder struct { func defaultServerConfig() ServerConfig { return ServerConfig{ ProtocolType: "SOCKS5", - UDPHost: "::", + UDPHost: "", UDPPort: 53, UDPReaders: 4, SocketBufferSize: 8 * 1024 * 1024, @@ -282,10 +282,7 @@ func finalizeServerConfig(cfg ServerConfig) (ServerConfig, error) { return cfg, fmt.Errorf("invalid PROTOCOL_TYPE: %q", cfg.ProtocolType) } - if cfg.UDPHost == "" { - cfg.UDPHost = defaultServerConfig().UDPHost - } - if net.ParseIP(cfg.UDPHost) == nil { + if cfg.UDPHost != "" && net.ParseIP(cfg.UDPHost) == nil { return cfg, fmt.Errorf("invalid UDP_HOST %q: must be an unbracketed IP literal", cfg.UDPHost) } diff --git a/internal/config/server_test.go b/internal/config/server_test.go index 94e7f2b2..86ba6dfc 100644 --- a/internal/config/server_test.go +++ b/internal/config/server_test.go @@ -21,6 +21,7 @@ func TestServerConfigAddress(t *testing.T) { host string want string }{ + {name: "family-neutral wildcard", host: "", want: ":53"}, {name: "IPv4", host: "0.0.0.0", want: "0.0.0.0:53"}, {name: "IPv6", host: "::", want: "[::]:53"}, {name: "concrete IPv6", host: "2001:db8::1", want: "[2001:db8::1]:53"}, @@ -43,7 +44,7 @@ func TestServerConfigUDPHostValidation(t *testing.T) { want string wantErr bool }{ - {name: "empty uses IPv6 wildcard", want: "::"}, + {name: "empty uses family-neutral wildcard", want: ""}, {name: "IPv4 literal", host: "0.0.0.0", want: "0.0.0.0"}, {name: "IPv6 literal", host: "2001:db8::1", want: "2001:db8::1"}, {name: "non-IP value", host: "not-an-ip", wantErr: true}, @@ -326,8 +327,8 @@ func TestLoadServerConfigFromJSONBase64AppliesDefaults(t *testing.T) { if cfg.UDPPort != 5301 { t.Fatalf("unexpected JSON base64 UDP port: got=%d want=%d", cfg.UDPPort, 5301) } - if cfg.UDPHost != "::" { - t.Fatalf("unexpected default UDP host: got=%q want=%q", cfg.UDPHost, "::") + if cfg.UDPHost != "" { + t.Fatalf("unexpected default UDP host: got=%q want empty", cfg.UDPHost) } if cfg.MaxPacketsPerBatch != defaultServerConfig().MaxPacketsPerBatch { t.Fatalf("expected default max packets per batch to apply: got=%d want=%d", cfg.MaxPacketsPerBatch, defaultServerConfig().MaxPacketsPerBatch) diff --git a/internal/udpserver/server_runtime.go b/internal/udpserver/server_runtime.go index 6cd98b20..0cf4364a 100644 --- a/internal/udpserver/server_runtime.go +++ b/internal/udpserver/server_runtime.go @@ -10,6 +10,7 @@ package udpserver import ( "context" "errors" + "fmt" "net" "sync" "time" @@ -28,8 +29,15 @@ func (s *Server) configureSocketBuffers(conn *net.UDPConn) { } func (s *Server) openUDPListeners() ([]*net.UDPConn, error) { + var ip net.IP + if s.cfg.UDPHost != "" { + ip = net.ParseIP(s.cfg.UDPHost) + if ip == nil { + return nil, fmt.Errorf("invalid UDP_HOST %q: must be an unbracketed IP literal", s.cfg.UDPHost) + } + } addr := &net.UDPAddr{ - IP: net.ParseIP(s.cfg.UDPHost), + IP: ip, Port: s.cfg.UDPPort, } desired := s.cfg.EffectiveUDPReaders() diff --git a/internal/udpserver/server_runtime_linux_test.go b/internal/udpserver/server_runtime_linux_test.go index b78210c0..08084d72 100644 --- a/internal/udpserver/server_runtime_linux_test.go +++ b/internal/udpserver/server_runtime_linux_test.go @@ -13,7 +13,7 @@ import ( const dualStackTestTimeout = 2 * time.Second -func TestIPv6WildcardUDPListenerIsDualStack(t *testing.T) { +func TestDefaultUDPListenerIsDualStack(t *testing.T) { probe, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv6unspecified}) if err != nil { t.Skipf("IPv6 wildcard unavailable: %v", err) @@ -48,7 +48,7 @@ func TestIPv6WildcardUDPListenerIsDualStack(t *testing.T) { } server := New(config.ServerConfig{ - UDPHost: "::", + UDPHost: "", UDPPort: 0, UDPReaders: 1, SocketBufferSize: 64 * 1024, @@ -125,7 +125,7 @@ func TestIPv6WildcardUDPListenerIsDualStack(t *testing.T) { } } -func TestExplicitIPv6WildcardRemainsIPv6(t *testing.T) { +func TestExplicitIPv6WildcardUsesIPv6Socket(t *testing.T) { probe, err := net.ListenUDP("udp6", &net.UDPAddr{IP: net.IPv6loopback}) if err != nil { t.Skipf("IPv6 loopback unavailable: %v", err) diff --git a/internal/udpserver/server_runtime_test.go b/internal/udpserver/server_runtime_test.go new file mode 100644 index 00000000..aa92ee04 --- /dev/null +++ b/internal/udpserver/server_runtime_test.go @@ -0,0 +1,18 @@ +package udpserver + +import ( + "testing" + + "masterdnsvpn-go/internal/config" +) + +func TestOpenUDPListenersRejectsInvalidUDPHost(t *testing.T) { + for _, host := range []string{"not-an-ip", "[::]"} { + t.Run(host, func(t *testing.T) { + server := &Server{cfg: config.ServerConfig{UDPHost: host}} + if _, err := server.openUDPListeners(); err == nil { + t.Fatalf("openUDPListeners(%q) unexpectedly succeeded", host) + } + }) + } +} diff --git a/server_config.toml.simple b/server_config.toml.simple index e13e3e67..9234f58b 100644 --- a/server_config.toml.simple +++ b/server_config.toml.simple @@ -40,11 +40,10 @@ SUPPORTED_DOWNLOAD_COMPRESSION_TYPES = [0, 1, 2, 3] # - Defines front-door queueing and worker limits before session handling. # ------------------------------------------------------------------------------ -# UDP bind endpoint. Use an unbracketed IP literal. The default "::" accepts -# IPv4-mapped traffic through the same IPv6 socket where supported. Without -# IPv4-mapped IPv6 support it remains IPv6-only; use "0.0.0.0" for the IPv4 -# wildcard. -UDP_HOST = "::" +# UDP bind endpoint. Leave empty for Go's family-neutral wildcard, which uses +# one dual-stack socket where IPv4-mapped IPv6 is supported and otherwise +# selects an available family. Non-empty values must be unbracketed IP literals. +UDP_HOST = "" UDP_PORT = 53 # UDP readers, DNS workers, and front-door request queue are smart-sized