mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-27 16:25:31 +03:00
fix(xray): allow a private DoH server on the port Xray dials (#6918)
The DNS allow rule opened port 53 for a private server given as https:// or h2c://, but Xray dials remote DoH on the URL's port, 443 by default, so the rule never matched and the private-IP block dropped the queries. Remote DoH now takes its port from the URL and ignores an object entry's port field, which Xray ignores as well; the +local forms skip routing and keep their previous handling.
This commit is contained in:
@@ -130,16 +130,8 @@ type dnsServerEndpoint struct {
|
|||||||
port int
|
port int
|
||||||
}
|
}
|
||||||
|
|
||||||
// privateDnsServerEndpoint extracts a literal, private/internal (ip, port)
|
// privateDnsServerEndpoint returns the private (ip, port) of a dns.servers string or object entry; an
|
||||||
// endpoint from a dns.servers entry, or ok=false if the entry is a domain
|
// object's "port" wins over the address's own except for remote DoH, which Xray dials from its URL.
|
||||||
// name, a special Xray keyword (localhost, fakedns, ...), or resolves to a
|
|
||||||
// public IP.
|
|
||||||
//
|
|
||||||
// A dns.servers entry is either a bare string or an object with an
|
|
||||||
// "address" field (see frontend/src/schemas/dns.ts DnsServerEntrySchema);
|
|
||||||
// the object form may also carry an explicit "port" (default 53 there,
|
|
||||||
// per DnsServerObjectInnerSchema), which takes precedence over any port
|
|
||||||
// embedded in the address itself.
|
|
||||||
func privateDnsServerEndpoint(entry any) (dnsServerEndpoint, bool) {
|
func privateDnsServerEndpoint(entry any) (dnsServerEndpoint, bool) {
|
||||||
var address string
|
var address string
|
||||||
explicitPort := 0
|
explicitPort := 0
|
||||||
@@ -159,7 +151,7 @@ func privateDnsServerEndpoint(entry any) (dnsServerEndpoint, bool) {
|
|||||||
if host == "" {
|
if host == "" {
|
||||||
return dnsServerEndpoint{}, false
|
return dnsServerEndpoint{}, false
|
||||||
}
|
}
|
||||||
if explicitPort > 0 {
|
if explicitPort > 0 && !isRemoteDoh(address) {
|
||||||
port = explicitPort
|
port = explicitPort
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,18 +167,26 @@ func privateDnsServerEndpoint(entry any) (dnsServerEndpoint, bool) {
|
|||||||
return dnsServerEndpoint{ip: ip.String(), port: port}, true
|
return dnsServerEndpoint{ip: ip.String(), port: port}, true
|
||||||
}
|
}
|
||||||
|
|
||||||
// splitAddressHostPort extracts the bare host and port (defaulting to 53)
|
// isRemoteDoh reports a routed DoH address (https://, h2c://), which Xray dials from the URL alone.
|
||||||
// from an Xray-core DNS server address string. Those may carry a URI
|
func isRemoteDoh(address string) bool {
|
||||||
// scheme (tcp://, tcp+local://, https://, https+local://, quic://,
|
scheme, _, ok := strings.Cut(strings.TrimSpace(address), "://")
|
||||||
// quic+local://) and, for DoH, a path and/or a bracketed IPv6 host — all
|
return ok && (strings.EqualFold(scheme, "https") || strings.EqualFold(scheme, "h2c"))
|
||||||
// of that is stripped down to host[:port] before parsing.
|
}
|
||||||
|
|
||||||
|
// splitAddressHostPort strips an Xray DNS address's scheme, DoH path and IPv6 brackets down to host
|
||||||
|
// and port, defaulting to 443 for remote DoH (https://, h2c://) and 53 otherwise.
|
||||||
func splitAddressHostPort(address string) (host string, port int) {
|
func splitAddressHostPort(address string) (host string, port int) {
|
||||||
address = strings.TrimSpace(address)
|
address = strings.TrimSpace(address)
|
||||||
if address == "" {
|
if address == "" {
|
||||||
return "", 0
|
return "", 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
port = 53
|
||||||
if idx := strings.Index(address, "://"); idx != -1 {
|
if idx := strings.Index(address, "://"); idx != -1 {
|
||||||
|
// Remote DoH goes out on the HTTPS port, h2c included; the +local forms skip routing.
|
||||||
|
if isRemoteDoh(address) {
|
||||||
|
port = 443
|
||||||
|
}
|
||||||
address = address[idx+3:]
|
address = address[idx+3:]
|
||||||
}
|
}
|
||||||
// Drop a DoH path, e.g. "1.1.1.1/dns-query".
|
// Drop a DoH path, e.g. "1.1.1.1/dns-query".
|
||||||
@@ -194,7 +194,6 @@ func splitAddressHostPort(address string) (host string, port int) {
|
|||||||
address = address[:idx]
|
address = address[:idx]
|
||||||
}
|
}
|
||||||
|
|
||||||
port = 53
|
|
||||||
host = address
|
host = address
|
||||||
if strings.HasPrefix(host, "[") {
|
if strings.HasPrefix(host, "[") {
|
||||||
// Bracketed IPv6, with or without a port: "[::1]" / "[::1]:53".
|
// Bracketed IPv6, with or without a port: "[::1]" / "[::1]:53".
|
||||||
|
|||||||
@@ -183,6 +183,34 @@ func TestEnsureDnsServerRouting_StripsSchemePortAndPath(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEnsureDnsServerRouting_RemoteDohUsesHttpsPort(t *testing.T) {
|
||||||
|
in := `{
|
||||||
|
"dns": {"servers": [
|
||||||
|
"https://10.255.255.1/dns-query",
|
||||||
|
"h2c://10.255.255.2/dns-query",
|
||||||
|
"https://10.255.255.3:8443/dns-query"
|
||||||
|
]},
|
||||||
|
"routing": {"rules": [{"type":"field","outboundTag":"blocked","ip":["geoip:private"]}]}
|
||||||
|
}`
|
||||||
|
out, err := EnsureDnsServerRouting(in)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected err: %v", err)
|
||||||
|
}
|
||||||
|
rules := rulesFromRaw(t, out)
|
||||||
|
if len(rules) != 3 {
|
||||||
|
t.Fatalf("got %d rules, want 2 allow rules + block: %v", len(rules), rules)
|
||||||
|
}
|
||||||
|
if p, _ := rules[0]["port"].(string); p != "443" {
|
||||||
|
t.Fatalf("rules[0] port = %v, want \"443\"", rules[0]["port"])
|
||||||
|
}
|
||||||
|
if ips := readRuleIPs(rules[0]["ip"]); len(ips) != 2 || ips[0] != "10.255.255.1" || ips[1] != "10.255.255.2" {
|
||||||
|
t.Fatalf("rules[0] ip = %v, want [10.255.255.1 10.255.255.2]", ips)
|
||||||
|
}
|
||||||
|
if p, _ := rules[1]["port"].(string); p != "8443" {
|
||||||
|
t.Fatalf("rules[1] port = %v, want \"8443\"", rules[1]["port"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestEnsureDnsServerRouting_SkipsSpecialAndDomainAddresses(t *testing.T) {
|
func TestEnsureDnsServerRouting_SkipsSpecialAndDomainAddresses(t *testing.T) {
|
||||||
in := `{
|
in := `{
|
||||||
"dns": {"servers": ["localhost", "fakedns", "dns.google", "8.8.8.8"]},
|
"dns": {"servers": ["localhost", "fakedns", "dns.google", "8.8.8.8"]},
|
||||||
@@ -465,3 +493,20 @@ func TestEnsureDnsServerRouting_InvalidJsonReturnsAsIs(t *testing.T) {
|
|||||||
t.Fatalf("expected raw passthrough on error, got %q", out)
|
t.Fatalf("expected raw passthrough on error, got %q", out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Xray dials a DoH server from its URL alone; the object's "port" (53 in entries saved by older
|
||||||
|
// forms) never reaches it, so the allow rule must still open the URL's port.
|
||||||
|
func TestEnsureDnsServerRouting_RemoteDohIgnoresObjectPort(t *testing.T) {
|
||||||
|
in := `{
|
||||||
|
"dns": {"servers": [{"address": "https://10.255.255.4/dns-query", "port": 53, "domains": ["lan"]}]},
|
||||||
|
"routing": {"rules": [{"type":"field","outboundTag":"blocked","ip":["geoip:private"]}]}
|
||||||
|
}`
|
||||||
|
out, err := EnsureDnsServerRouting(in)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected err: %v", err)
|
||||||
|
}
|
||||||
|
rules := rulesFromRaw(t, out)
|
||||||
|
if p, _ := rules[0]["port"].(string); p != "443" {
|
||||||
|
t.Fatalf("rules[0] port = %v, want \"443\"", rules[0]["port"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user