mirror of
https://github.com/MengMengCode/VoCat.git
synced 2026-08-17 05:13:43 +08:00
feat: verify SOCKS5 with real UDP round trip (#35)
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
package i18n
|
||||
|
||||
// Keep feature-specific diagnostic strings together so additions to the proxy
|
||||
// probe do not cause conflicts in the shared dictionary.
|
||||
func init() {
|
||||
zhToEn["UDP ASSOCIATE 已建立,但实际 UDP 数据没有返回;检查节点 UDP 转发、路由和防火墙。"] = "UDP ASSOCIATE was established, but no UDP payload returned; check the node's UDP forwarding, routing, and firewall."
|
||||
zhToEn["TCP 握手、认证、UDP ASSOCIATE 与真实 UDP DNS 往返均通过。"] = "TCP handshake, authentication, UDP ASSOCIATE, and a real UDP DNS round trip all passed."
|
||||
zhToEn["代理已保存,SOCKS5 认证与真实 UDP 往返均通过。"] = "Proxy saved; SOCKS5 authentication and a real UDP round trip both passed."
|
||||
zhToEn["SOCKS5 认证与真实 UDP 往返探测通过。"] = "SOCKS5 authentication and a real UDP round-trip probe passed."
|
||||
}
|
||||
+246
-1
@@ -3,6 +3,8 @@ package proxy
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -17,18 +19,44 @@ type ProbeResult struct {
|
||||
Reachable bool `json:"reachable"`
|
||||
HandshakeOK bool `json:"handshake_ok"`
|
||||
UDPAssociateOK bool `json:"udp_associate_ok"`
|
||||
UDPExchangeOK bool `json:"udp_exchange_ok"`
|
||||
AuthMethod string `json:"auth_method,omitempty"`
|
||||
RelayAddr string `json:"relay_addr,omitempty"`
|
||||
DNSServer string `json:"dns_server,omitempty"`
|
||||
DNSName string `json:"dns_name,omitempty"`
|
||||
DNSRCode int `json:"dns_rcode,omitempty"`
|
||||
RoundTripMS int64 `json:"round_trip_ms,omitempty"`
|
||||
Diagnosis string `json:"diagnosis,omitempty"`
|
||||
Hint string `json:"hint,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
defaultProbeDNSServer = "1.1.1.1:53"
|
||||
defaultProbeDNSName = "example.com"
|
||||
)
|
||||
|
||||
func ProbeSOCKS5(
|
||||
ctx context.Context,
|
||||
address string,
|
||||
username string,
|
||||
password string,
|
||||
timeout time.Duration,
|
||||
) (ProbeResult, error) {
|
||||
return probeSOCKS5(ctx, address, username, password, timeout, defaultProbeDNSServer, defaultProbeDNSName)
|
||||
}
|
||||
|
||||
// probeSOCKS5 performs both the SOCKS5 control-plane negotiation and a real
|
||||
// UDP DNS round trip through the returned relay. Keeping the target injectable
|
||||
// makes the negative paths deterministic in tests without weakening the
|
||||
// production probe.
|
||||
func probeSOCKS5(
|
||||
ctx context.Context,
|
||||
address string,
|
||||
username string,
|
||||
password string,
|
||||
timeout time.Duration,
|
||||
dnsServer string,
|
||||
dnsName string,
|
||||
) (ProbeResult, error) {
|
||||
address = strings.TrimSpace(address)
|
||||
if _, _, err := net.SplitHostPort(address); err != nil {
|
||||
@@ -122,11 +150,228 @@ func ProbeSOCKS5(
|
||||
port := int(portBytes[0])<<8 | int(portBytes[1])
|
||||
result.UDPAssociateOK = true
|
||||
result.RelayAddr = net.JoinHostPort(host, fmt.Sprintf("%d", port))
|
||||
result.DNSServer = dnsServer
|
||||
result.DNSName = dnsName
|
||||
|
||||
if err := probeUDPExchange(probeContext, connection, &result, host, port, dnsServer, dnsName, timeout); err != nil {
|
||||
if result.Diagnosis == "" {
|
||||
result.Diagnosis = "udp_no_roundtrip"
|
||||
}
|
||||
if result.Hint == "" {
|
||||
result.Hint = i18n.T("UDP ASSOCIATE 已建立,但实际 UDP 数据没有返回;检查节点 UDP 转发、路由和防火墙。")
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
result.Diagnosis = "ready"
|
||||
result.Hint = i18n.T("TCP 握手、认证和 UDP ASSOCIATE 均通过。")
|
||||
result.Hint = i18n.T("TCP 握手、认证、UDP ASSOCIATE 与真实 UDP DNS 往返均通过。")
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func probeUDPExchange(
|
||||
ctx context.Context,
|
||||
control net.Conn,
|
||||
result *ProbeResult,
|
||||
relayHost string,
|
||||
relayPort int,
|
||||
dnsServer string,
|
||||
dnsName string,
|
||||
timeout time.Duration,
|
||||
) error {
|
||||
if result == nil {
|
||||
return errors.New("proxy: probe result is nil")
|
||||
}
|
||||
dnsAddress, err := net.ResolveUDPAddr("udp", strings.TrimSpace(dnsServer))
|
||||
if err != nil {
|
||||
result.Diagnosis = "invalid_dns_target"
|
||||
return fmt.Errorf("proxy: resolve UDP probe target: %w", err)
|
||||
}
|
||||
relayHost = strings.TrimSpace(relayHost)
|
||||
if relayIP := net.ParseIP(relayHost); relayIP != nil && relayIP.IsUnspecified() {
|
||||
remoteHost, _, splitErr := net.SplitHostPort(control.RemoteAddr().String())
|
||||
if splitErr != nil {
|
||||
result.Diagnosis = "invalid_udp_relay"
|
||||
return fmt.Errorf("proxy: resolve wildcard UDP relay: %w", splitErr)
|
||||
}
|
||||
relayHost = remoteHost
|
||||
}
|
||||
relayAddress, err := net.ResolveUDPAddr("udp", net.JoinHostPort(relayHost, fmt.Sprintf("%d", relayPort)))
|
||||
if err != nil {
|
||||
result.Diagnosis = "invalid_udp_relay"
|
||||
return fmt.Errorf("proxy: resolve UDP relay: %w", err)
|
||||
}
|
||||
|
||||
localNetwork := "udp4"
|
||||
if relayAddress.IP != nil && relayAddress.IP.To4() == nil {
|
||||
localNetwork = "udp6"
|
||||
}
|
||||
udpConnection, err := net.ListenUDP(localNetwork, nil)
|
||||
if err != nil {
|
||||
result.Diagnosis = "udp_socket_failed"
|
||||
return fmt.Errorf("proxy: open UDP probe socket: %w", err)
|
||||
}
|
||||
defer udpConnection.Close()
|
||||
|
||||
deadline := time.Now().Add(timeout)
|
||||
if contextDeadline, ok := ctx.Deadline(); ok && contextDeadline.Before(deadline) {
|
||||
deadline = contextDeadline
|
||||
}
|
||||
if err := udpConnection.SetDeadline(deadline); err != nil {
|
||||
return fmt.Errorf("proxy: set UDP probe deadline: %w", err)
|
||||
}
|
||||
|
||||
query, queryID, err := buildDNSQuery(dnsName)
|
||||
if err != nil {
|
||||
result.Diagnosis = "invalid_dns_name"
|
||||
return err
|
||||
}
|
||||
datagram, err := buildSOCKSUDPDatagram(dnsAddress, query)
|
||||
if err != nil {
|
||||
result.Diagnosis = "invalid_dns_target"
|
||||
return err
|
||||
}
|
||||
startedAt := time.Now()
|
||||
if _, err := udpConnection.WriteToUDP(datagram, relayAddress); err != nil {
|
||||
result.Diagnosis = "udp_send_failed"
|
||||
return fmt.Errorf("proxy: send UDP DNS probe: %w", err)
|
||||
}
|
||||
|
||||
responseBuffer := make([]byte, 64*1024)
|
||||
for {
|
||||
if err := ctx.Err(); err != nil {
|
||||
result.Diagnosis = "udp_no_roundtrip"
|
||||
return fmt.Errorf("proxy: UDP DNS probe cancelled: %w", err)
|
||||
}
|
||||
count, sender, err := udpConnection.ReadFromUDP(responseBuffer)
|
||||
if err != nil {
|
||||
result.Diagnosis = "udp_no_roundtrip"
|
||||
return fmt.Errorf("proxy: UDP DNS probe did not return: %w", err)
|
||||
}
|
||||
if !sameUDPAddress(sender, relayAddress) {
|
||||
continue
|
||||
}
|
||||
payload, err := parseSOCKSUDPDatagram(responseBuffer[:count])
|
||||
if err != nil {
|
||||
result.Diagnosis = "udp_invalid_response"
|
||||
return fmt.Errorf("proxy: parse UDP relay response: %w", err)
|
||||
}
|
||||
rcode, err := validateDNSResponse(payload, queryID)
|
||||
if err != nil {
|
||||
result.Diagnosis = "dns_invalid_response"
|
||||
return err
|
||||
}
|
||||
result.UDPExchangeOK = true
|
||||
result.DNSRCode = rcode
|
||||
result.RoundTripMS = time.Since(startedAt).Milliseconds()
|
||||
if result.RoundTripMS < 1 {
|
||||
result.RoundTripMS = 1
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func buildDNSQuery(name string) ([]byte, uint16, error) {
|
||||
name = strings.TrimSuffix(strings.TrimSpace(name), ".")
|
||||
if name == "" || len(name) > 253 {
|
||||
return nil, 0, errors.New("proxy: UDP probe DNS name is invalid")
|
||||
}
|
||||
var idBytes [2]byte
|
||||
if _, err := rand.Read(idBytes[:]); err != nil {
|
||||
return nil, 0, fmt.Errorf("proxy: generate DNS probe ID: %w", err)
|
||||
}
|
||||
queryID := binary.BigEndian.Uint16(idBytes[:])
|
||||
query := make([]byte, 12, 12+len(name)+6)
|
||||
binary.BigEndian.PutUint16(query[0:2], queryID)
|
||||
binary.BigEndian.PutUint16(query[2:4], 0x0100)
|
||||
binary.BigEndian.PutUint16(query[4:6], 1)
|
||||
for _, label := range strings.Split(name, ".") {
|
||||
if label == "" || len(label) > 63 {
|
||||
return nil, 0, errors.New("proxy: UDP probe DNS label is invalid")
|
||||
}
|
||||
query = append(query, byte(len(label)))
|
||||
query = append(query, label...)
|
||||
}
|
||||
query = append(query, 0, 0, 1, 0, 1)
|
||||
return query, queryID, nil
|
||||
}
|
||||
|
||||
func buildSOCKSUDPDatagram(target *net.UDPAddr, payload []byte) ([]byte, error) {
|
||||
if target == nil || target.IP == nil || target.Port < 1 || target.Port > 65535 {
|
||||
return nil, errors.New("proxy: UDP target is invalid")
|
||||
}
|
||||
packet := []byte{0, 0, 0}
|
||||
if ipv4 := target.IP.To4(); ipv4 != nil {
|
||||
packet = append(packet, 1)
|
||||
packet = append(packet, ipv4...)
|
||||
} else if ipv6 := target.IP.To16(); ipv6 != nil {
|
||||
packet = append(packet, 4)
|
||||
packet = append(packet, ipv6...)
|
||||
} else {
|
||||
return nil, errors.New("proxy: UDP target address family is invalid")
|
||||
}
|
||||
packet = append(packet, byte(target.Port>>8), byte(target.Port))
|
||||
packet = append(packet, payload...)
|
||||
return packet, nil
|
||||
}
|
||||
|
||||
func parseSOCKSUDPDatagram(packet []byte) ([]byte, error) {
|
||||
if len(packet) < 4 || packet[0] != 0 || packet[1] != 0 {
|
||||
return nil, errors.New("invalid SOCKS5 UDP header")
|
||||
}
|
||||
if packet[2] != 0 {
|
||||
return nil, errors.New("fragmented SOCKS5 UDP response is unsupported")
|
||||
}
|
||||
offset := 4
|
||||
switch packet[3] {
|
||||
case 1:
|
||||
offset += net.IPv4len
|
||||
case 3:
|
||||
if len(packet) <= offset {
|
||||
return nil, errors.New("truncated SOCKS5 UDP domain")
|
||||
}
|
||||
offset += 1 + int(packet[offset])
|
||||
case 4:
|
||||
offset += net.IPv6len
|
||||
default:
|
||||
return nil, errors.New("unsupported SOCKS5 UDP address type")
|
||||
}
|
||||
if offset+2 > len(packet) {
|
||||
return nil, errors.New("truncated SOCKS5 UDP endpoint")
|
||||
}
|
||||
offset += 2
|
||||
if offset >= len(packet) {
|
||||
return nil, errors.New("empty SOCKS5 UDP payload")
|
||||
}
|
||||
return packet[offset:], nil
|
||||
}
|
||||
|
||||
func validateDNSResponse(payload []byte, queryID uint16) (int, error) {
|
||||
if len(payload) < 12 {
|
||||
return 0, errors.New("proxy: DNS response is truncated")
|
||||
}
|
||||
if binary.BigEndian.Uint16(payload[0:2]) != queryID {
|
||||
return 0, errors.New("proxy: DNS response ID does not match")
|
||||
}
|
||||
flags := binary.BigEndian.Uint16(payload[2:4])
|
||||
if flags&0x8000 == 0 {
|
||||
return 0, errors.New("proxy: DNS response is not a response")
|
||||
}
|
||||
rcode := int(flags & 0x000f)
|
||||
if rcode != 0 {
|
||||
return rcode, fmt.Errorf("proxy: DNS probe returned response code %d", rcode)
|
||||
}
|
||||
return rcode, nil
|
||||
}
|
||||
|
||||
func sameUDPAddress(left, right *net.UDPAddr) bool {
|
||||
if left == nil || right == nil || left.Port != right.Port {
|
||||
return false
|
||||
}
|
||||
if left.IP == nil || right.IP == nil {
|
||||
return true
|
||||
}
|
||||
return left.IP.Equal(right.IP)
|
||||
}
|
||||
|
||||
func readSOCKSAddress(reader io.Reader, addressType byte) (string, error) {
|
||||
switch addressType {
|
||||
case 1:
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestProbeSOCKS5RequiresRealUDPExchange(t *testing.T) {
|
||||
address, stop := startProbeSOCKS5Server(t, false)
|
||||
defer stop()
|
||||
|
||||
result, err := probeSOCKS5(
|
||||
context.Background(),
|
||||
address,
|
||||
"",
|
||||
"",
|
||||
250*time.Millisecond,
|
||||
"192.0.2.53:53",
|
||||
"example.test",
|
||||
)
|
||||
if err == nil {
|
||||
t.Fatal("Probe unexpectedly succeeded when the relay dropped UDP data")
|
||||
}
|
||||
if !result.UDPAssociateOK {
|
||||
t.Fatal("UDP ASSOCIATE should have succeeded")
|
||||
}
|
||||
if result.UDPExchangeOK {
|
||||
t.Fatal("UDP exchange should not be reported as successful")
|
||||
}
|
||||
if result.Diagnosis != "udp_no_roundtrip" {
|
||||
t.Fatalf("Diagnosis = %q, want udp_no_roundtrip", result.Diagnosis)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProbeSOCKS5ReportsRealUDPDNSRoundTrip(t *testing.T) {
|
||||
address, stop := startProbeSOCKS5Server(t, true)
|
||||
defer stop()
|
||||
|
||||
result, err := probeSOCKS5(
|
||||
context.Background(),
|
||||
address,
|
||||
"",
|
||||
"",
|
||||
time.Second,
|
||||
"192.0.2.53:53",
|
||||
"example.test",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Probe returned error: %v", err)
|
||||
}
|
||||
if !result.HandshakeOK || !result.UDPAssociateOK || !result.UDPExchangeOK {
|
||||
t.Fatalf("Probe evidence incomplete: %+v", result)
|
||||
}
|
||||
if result.Diagnosis != "ready" {
|
||||
t.Fatalf("Diagnosis = %q, want ready", result.Diagnosis)
|
||||
}
|
||||
if result.DNSName != "example.test" || result.DNSServer != "192.0.2.53:53" {
|
||||
t.Fatalf("Unexpected DNS evidence: %+v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func startProbeSOCKS5Server(t *testing.T, echoDNS bool) (string, func()) {
|
||||
t.Helper()
|
||||
udpConnection, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)})
|
||||
if err != nil {
|
||||
t.Fatalf("ListenUDP: %v", err)
|
||||
}
|
||||
tcpListener, err := net.Listen("tcp4", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
udpConnection.Close()
|
||||
t.Fatalf("Listen: %v", err)
|
||||
}
|
||||
|
||||
if echoDNS {
|
||||
go func() {
|
||||
buffer := make([]byte, 2048)
|
||||
count, sender, readErr := udpConnection.ReadFromUDP(buffer)
|
||||
if readErr != nil || count < 22 {
|
||||
return
|
||||
}
|
||||
// The test target is IPv4, so the SOCKS5 UDP header is ten bytes.
|
||||
buffer[12] = 0x81
|
||||
buffer[13] = 0x80
|
||||
_, _ = udpConnection.WriteToUDP(buffer[:count], sender)
|
||||
}()
|
||||
}
|
||||
|
||||
go func() {
|
||||
connection, acceptErr := tcpListener.Accept()
|
||||
if acceptErr != nil {
|
||||
return
|
||||
}
|
||||
defer connection.Close()
|
||||
greeting := make([]byte, 3)
|
||||
if _, readErr := io.ReadFull(connection, greeting); readErr != nil {
|
||||
return
|
||||
}
|
||||
if _, writeErr := connection.Write([]byte{5, 0}); writeErr != nil {
|
||||
return
|
||||
}
|
||||
associate := make([]byte, 10)
|
||||
if _, readErr := io.ReadFull(connection, associate); readErr != nil {
|
||||
return
|
||||
}
|
||||
udpPort := udpConnection.LocalAddr().(*net.UDPAddr).Port
|
||||
response := []byte{5, 0, 0, 1, 127, 0, 0, 1, byte(udpPort >> 8), byte(udpPort)}
|
||||
if _, writeErr := connection.Write(response); writeErr != nil {
|
||||
return
|
||||
}
|
||||
_, _ = io.Copy(io.Discard, connection)
|
||||
}()
|
||||
|
||||
return tcpListener.Addr().String(), func() {
|
||||
_ = tcpListener.Close()
|
||||
_ = udpConnection.Close()
|
||||
}
|
||||
}
|
||||
@@ -395,8 +395,8 @@ func (s *Server) saveAndProbeUpstream(
|
||||
)
|
||||
probeResponse := probeMap(probe, probeErr)
|
||||
message := i18n.T("代理已保存;UDP ASSOCIATE 尚未通过。")
|
||||
if probeErr == nil && probe.UDPAssociateOK {
|
||||
message = i18n.T("代理已保存,SOCKS5 认证与 UDP ASSOCIATE 均通过。")
|
||||
if probeErr == nil && probe.UDPExchangeOK {
|
||||
message = i18n.T("代理已保存,SOCKS5 认证与真实 UDP 往返均通过。")
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"data": map[string]any{
|
||||
@@ -425,8 +425,8 @@ func (s *Server) handleUpstreamProbe(w http.ResponseWriter, r *http.Request, id
|
||||
8*time.Second,
|
||||
)
|
||||
message := i18n.T("代理不能承载 VoWiFi 所需的 UDP。")
|
||||
if probeErr == nil && result.UDPAssociateOK {
|
||||
message = i18n.T("SOCKS5 认证与 UDP ASSOCIATE 探测通过。")
|
||||
if probeErr == nil && result.UDPExchangeOK {
|
||||
message = i18n.T("SOCKS5 认证与真实 UDP 往返探测通过。")
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"data": map[string]any{
|
||||
@@ -479,8 +479,8 @@ func (s *Server) handleUpstreamProbeConfig(w http.ResponseWriter, r *http.Reques
|
||||
8*time.Second,
|
||||
)
|
||||
message := i18n.T("代理不能承载 VoWiFi 所需的 UDP。")
|
||||
if probeErr == nil && result.UDPAssociateOK {
|
||||
message = i18n.T("SOCKS5 认证与 UDP ASSOCIATE 探测通过。")
|
||||
if probeErr == nil && result.UDPExchangeOK {
|
||||
message = i18n.T("SOCKS5 认证与真实 UDP 往返探测通过。")
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"data": map[string]any{
|
||||
|
||||
@@ -40,23 +40,35 @@ function ProbeResultPanel({ probe }: { probe: UpstreamProbeResult }) {
|
||||
const { t } = useI18n();
|
||||
const reachable = !!probe.reachable;
|
||||
const handshakeOk = !!probe.handshakeOk;
|
||||
const udpOk = !!probe.udpAssociateOk;
|
||||
const associateOk = !!probe.udpAssociateOk;
|
||||
const udpOk = !!probe.udpExchangeOk;
|
||||
const handshakeState: ProbeState = !reachable ? "pending" : handshakeOk ? "ok" : "fail";
|
||||
const udpState: ProbeState = !handshakeOk ? "pending" : udpOk ? "ok" : "fail";
|
||||
const associateState: ProbeState = !handshakeOk ? "pending" : associateOk ? "ok" : "fail";
|
||||
const udpState: ProbeState = !associateOk ? "pending" : udpOk ? "ok" : "fail";
|
||||
return (
|
||||
<div className="ui-panel-muted space-y-2 rounded-lg p-3">
|
||||
<ProbeRow state={reachable ? "ok" : "fail"} label={t("TCP 连接")} detail={reachable ? t("可连通") : t("无法连接")} />
|
||||
<ProbeRow state={handshakeState} label={t("SOCKS5 握手")} detail={handshakeOk ? authMethodLabel(probe.authMethod) : undefined} />
|
||||
<ProbeRow
|
||||
state={udpState}
|
||||
state={associateState}
|
||||
label={t("UDP Associate(VoWiFi 依赖)")}
|
||||
detail={udpState === "pending" ? undefined : udpOk ? t("支持") : t("不支持")}
|
||||
detail={associateState === "pending" ? undefined : associateOk ? t("已建立") : t("不支持")}
|
||||
/>
|
||||
<ProbeRow
|
||||
state={udpState}
|
||||
label={t("真实 UDP DNS 往返")}
|
||||
detail={udpState === "pending" ? undefined : udpOk ? `${probe.roundTripMs || 0} ms` : t("无返回")}
|
||||
/>
|
||||
{probe.relayAddr ? (
|
||||
<div className="text-[11px] text-gray-400">
|
||||
{t("UDP 中继地址:")}<span className="font-mono">{probe.relayAddr}</span>
|
||||
</div>
|
||||
) : null}
|
||||
{probe.dnsName && probe.dnsServer ? (
|
||||
<div className="text-[11px] text-gray-400">
|
||||
{t("UDP 测试:")}<span className="font-mono">{probe.dnsName} @ {probe.dnsServer}</span>
|
||||
</div>
|
||||
) : null}
|
||||
{probe.hint ? <div className="text-[11px] text-gray-500 dark:text-gray-400">{probe.hint}</div> : null}
|
||||
{probe.error ? <div className="break-all text-[11px] text-red-500">{probe.error}</div> : null}
|
||||
</div>
|
||||
@@ -127,7 +139,7 @@ export function UpstreamDialog({ open, editing, form, testing, probe, onPatch, o
|
||||
</div>
|
||||
{probe ? (
|
||||
<div className="space-y-3">
|
||||
<SectionHeader tone={probe.udpAssociateOk ? "green" : "amber"} title={t("连通性检测结果")} />
|
||||
<SectionHeader tone={probe.udpExchangeOk ? "green" : "amber"} title={t("连通性检测结果")} />
|
||||
<ProbeResultPanel probe={probe} />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -20,8 +20,13 @@ export interface UpstreamProbeResult {
|
||||
reachable?: boolean;
|
||||
handshakeOk?: boolean;
|
||||
udpAssociateOk?: boolean;
|
||||
udpExchangeOk?: boolean;
|
||||
authMethod?: string;
|
||||
relayAddr?: string;
|
||||
dnsServer?: string;
|
||||
dnsName?: string;
|
||||
dnsRcode?: number;
|
||||
roundTripMs?: number;
|
||||
diagnosis?: string;
|
||||
hint?: string;
|
||||
error?: string;
|
||||
|
||||
@@ -684,6 +684,11 @@ export const EN_DICT: Record<string, string> = {
|
||||
"SIM / 设备": "SIM / Device",
|
||||
"SM-DP+ 地址 *": "SM-DP+ Address *",
|
||||
"SOCKS5 认证与 UDP ASSOCIATE 探测通过": "SOCKS5 auth and UDP ASSOCIATE probes passed",
|
||||
"SOCKS5 认证与真实 UDP 往返探测通过": "SOCKS5 authentication and real UDP round-trip probe passed",
|
||||
"真实 UDP DNS 往返": "Real UDP DNS round trip",
|
||||
"已建立": "Established",
|
||||
"无返回": "No response",
|
||||
"UDP 测试:": "UDP test: ",
|
||||
"Telegram / Bark / Email / Pushplus / Webhook": "Telegram / Bark / Email / Pushplus / Webhook",
|
||||
"USB 路径": "USB Path",
|
||||
"USSD 交互终端": "USSD Interactive Terminal",
|
||||
|
||||
@@ -168,8 +168,8 @@ export default function ProxyPage() {
|
||||
},
|
||||
});
|
||||
setUpstreamProbe(data.probe || null);
|
||||
if (data.probe?.udpAssociateOk) {
|
||||
message.success(data.message || t("SOCKS5 鉴权和 UDP Associate 探测通过"));
|
||||
if (data.probe?.udpExchangeOk) {
|
||||
message.success(data.message || t("SOCKS5 认证与真实 UDP 往返探测通过"));
|
||||
} else {
|
||||
message.warning(data.message || t("代理不能承载 VoWiFi 所需的 UDP"));
|
||||
}
|
||||
|
||||
@@ -330,8 +330,13 @@ export interface UpstreamProxyProbe {
|
||||
reachable?: boolean;
|
||||
handshakeOk?: boolean;
|
||||
udpAssociateOk?: boolean;
|
||||
udpExchangeOk?: boolean;
|
||||
authMethod?: string;
|
||||
relayAddr?: string;
|
||||
dnsServer?: string;
|
||||
dnsName?: string;
|
||||
dnsRcode?: number;
|
||||
roundTripMs?: number;
|
||||
diagnosis?: string;
|
||||
hint?: string;
|
||||
error?: string;
|
||||
|
||||
Reference in New Issue
Block a user