mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-25 01:55:25 +08:00
fix(outbound): dial IPv6 literals correctly in TCP probes (#6850)
The TCP probe built its dial address as host:port with string formatting, so an outbound whose server is an unbracketed IPv6 literal got an invalid target and was reported unreachable although it carried traffic. The address is now built with net.JoinHostPort, and an already-bracketed IPv6 literal is unwrapped first so it is not bracketed twice.
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -271,7 +272,12 @@ func extractOutboundEndpoints(ob map[string]any) []string {
|
||||
host, _ := addr.(string)
|
||||
p := numAsInt(port)
|
||||
if host != "" && p > 0 {
|
||||
out = append(out, fmt.Sprintf("%s:%d", host, p))
|
||||
if len(host) > 2 && host[0] == '[' && host[len(host)-1] == ']' {
|
||||
if ip, err := netip.ParseAddr(host[1 : len(host)-1]); err == nil && ip.Is6() {
|
||||
host = host[1 : len(host)-1]
|
||||
}
|
||||
}
|
||||
out = append(out, net.JoinHostPort(host, strconv.Itoa(p)))
|
||||
}
|
||||
}
|
||||
switch protocol {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -64,3 +67,143 @@ func TestExtractOutboundEndpointsMasque(t *testing.T) {
|
||||
t.Fatalf("extractOutboundEndpoints() = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func endpointOutbound(tag, protocol, host string, port int, flat bool) map[string]any {
|
||||
server := map[string]any{"address": host, "port": port}
|
||||
var settings map[string]any
|
||||
switch {
|
||||
case flat:
|
||||
settings = server
|
||||
case protocol == "vless" || protocol == "vmess":
|
||||
settings = map[string]any{"vnext": []any{server}}
|
||||
default:
|
||||
settings = map[string]any{"servers": []any{server}}
|
||||
}
|
||||
return map[string]any{"tag": tag, "protocol": protocol, "settings": settings}
|
||||
}
|
||||
|
||||
func TestOutboundEndpointsIPv6DialAddresses(t *testing.T) {
|
||||
tests := []struct {
|
||||
protocol string
|
||||
host string
|
||||
wantHost string
|
||||
flat bool
|
||||
}{
|
||||
{"vless", "2001:3040:1:100::301:2020", "2001:3040:1:100::301:2020", false},
|
||||
{"vless", "[2001:3040:1:100::301:2020]", "2001:3040:1:100::301:2020", false},
|
||||
{"vmess", "::1", "::1", false},
|
||||
{"trojan", "[::1]", "::1", false},
|
||||
{"shadowsocks", "192.0.2.1", "192.0.2.1", false},
|
||||
{"socks", "proxy.example.com", "proxy.example.com", false},
|
||||
{"http", "::1", "::1", false},
|
||||
{"hysteria", "::1", "::1", true},
|
||||
{"masque", "[::1]", "::1", true},
|
||||
{"vless", "fe80::1%eth0", "fe80::1%eth0", true},
|
||||
{"vless", "[fe80::1%eth0]", "fe80::1%eth0", true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.protocol+"/"+tt.host, func(t *testing.T) {
|
||||
ob := endpointOutbound("probe", tt.protocol, tt.host, 8443, tt.flat)
|
||||
endpoints := extractOutboundEndpoints(ob)
|
||||
if len(endpoints) != 1 {
|
||||
t.Fatalf("endpoints = %v, want one dial address", endpoints)
|
||||
}
|
||||
gotHost, gotPort, err := net.SplitHostPort(endpoints[0])
|
||||
if err != nil {
|
||||
t.Fatalf("endpoint %q cannot be dialed: %v", endpoints[0], err)
|
||||
}
|
||||
if gotHost != tt.wantHost || gotPort != "8443" {
|
||||
t.Fatalf("dial target = (%q, %q), want (%q, 8443)", gotHost, gotPort, tt.wantHost)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOutboundTCPIPv6ReachesListener(t *testing.T) {
|
||||
listener, err := net.Listen("tcp6", "[::1]:0")
|
||||
if err != nil {
|
||||
t.Skipf("IPv6 loopback unavailable: %v", err)
|
||||
}
|
||||
defer listener.Close()
|
||||
go func() {
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
conn.Close()
|
||||
}
|
||||
}()
|
||||
port := listener.Addr().(*net.TCPAddr).Port
|
||||
for _, tc := range []struct {
|
||||
protocol string
|
||||
flat bool
|
||||
}{
|
||||
{protocol: "vless"},
|
||||
{protocol: "vless", flat: true},
|
||||
{protocol: "vmess"},
|
||||
{protocol: "socks"},
|
||||
} {
|
||||
t.Run(fmt.Sprintf("%s/flat=%t", tc.protocol, tc.flat), func(t *testing.T) {
|
||||
items := []any{
|
||||
endpointOutbound("raw", tc.protocol, "::1", port, tc.flat),
|
||||
endpointOutbound("bracketed", tc.protocol, "[::1]", port, tc.flat),
|
||||
}
|
||||
svc := &OutboundService{}
|
||||
for _, ob := range items {
|
||||
t.Run(ob.(map[string]any)["tag"].(string), func(t *testing.T) {
|
||||
r, err := svc.TestOutbound(mustJSON(t, ob), "", "", "tcp")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertTCPListenerResult(t, r, listener.Addr().String())
|
||||
})
|
||||
}
|
||||
t.Run("batch", func(t *testing.T) {
|
||||
results, err := svc.TestOutbounds(mustJSON(t, items), "", "", "tcp")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(results) != 2 || results[0].Tag != "raw" || results[1].Tag != "bracketed" {
|
||||
t.Fatalf("batch results lost input ordering: %+v", results)
|
||||
}
|
||||
for _, r := range results {
|
||||
assertTCPListenerResult(t, r, listener.Addr().String())
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func assertTCPListenerResult(t *testing.T, r *TestOutboundResult, address string) {
|
||||
t.Helper()
|
||||
if !r.Success || r.Error != "" || r.Mode != "tcp" || len(r.Endpoints) != 1 {
|
||||
t.Fatalf("TCP probe failed to reach %s: %+v", address, r)
|
||||
}
|
||||
endpoint := r.Endpoints[0]
|
||||
if !endpoint.Success || endpoint.Error != "" || endpoint.Address != address {
|
||||
t.Fatalf("endpoint = %+v, want successful dial to %s", endpoint, address)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOutboundTCPReportsRefusedIPv6Connection(t *testing.T) {
|
||||
listener, err := net.Listen("tcp6", "[::1]:0")
|
||||
if err != nil {
|
||||
t.Skipf("IPv6 loopback unavailable: %v", err)
|
||||
}
|
||||
address := listener.Addr().String()
|
||||
port := listener.Addr().(*net.TCPAddr).Port
|
||||
listener.Close()
|
||||
for _, host := range []string{"::1", "[::1]"} {
|
||||
t.Run(host, func(t *testing.T) {
|
||||
ob := endpointOutbound("closed", "vless", host, port, false)
|
||||
r, err := (&OutboundService{}).TestOutbound(mustJSON(t, ob), "", "", "tcp")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r.Success || len(r.Endpoints) != 1 || r.Endpoints[0].Success || r.Endpoints[0].Address != address || !strings.Contains(r.Error, "connectex:") && !strings.Contains(r.Error, "connection refused") {
|
||||
t.Fatalf("closed IPv6 listener did not produce a connection error: %+v", r)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user