probe_protocol_case_test.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. package outbound
  2. import (
  3. "encoding/json"
  4. "net"
  5. "net/http"
  6. "net/http/httptest"
  7. "net/url"
  8. "testing"
  9. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  10. )
  11. // The core lowercases a protocol id and a transport name before it resolves
  12. // either, so every reader here has to accept the spelling the core accepts.
  13. func TestTestOutboundsTCPModeForcesCoreSpelledUDPToHTTPProbe(t *testing.T) {
  14. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  15. w.WriteHeader(http.StatusNoContent)
  16. }))
  17. defer srv.Close()
  18. withStubProcess(t, func(cfg *xray.Config, configPath string) batchProcess {
  19. return &stubProcess{cfg: cfg, serveSocks: true}
  20. })
  21. withEgressTraceProbe(t, func(*url.URL) *TestEgressResult {
  22. return &TestEgressResult{IPv4: "198.51.100.2", Country: "ZZ", Warp: "off"}
  23. })
  24. batch := mustJSON(t, []any{map[string]any{"tag": "wg", "protocol": "WireGuard"}})
  25. results, err := (&OutboundService{}).TestOutbounds(batch, srv.URL, "", "tcp")
  26. if err != nil {
  27. t.Fatalf("TestOutbounds: %v", err)
  28. }
  29. r := results[0]
  30. if !r.Success || r.Mode != "http" {
  31. t.Errorf(`"WireGuard" outbound in tcp mode = %+v, want success with mode %q`, r, "http")
  32. }
  33. if r.Egress == nil || r.Egress.IPv4 != "198.51.100.2" {
  34. t.Errorf(`"WireGuard" outbound egress = %+v`, r.Egress)
  35. }
  36. }
  37. func TestOutboundTransportIsUDPMatchesTheCore(t *testing.T) {
  38. tests := []struct {
  39. name string
  40. ob map[string]any
  41. want bool
  42. }{
  43. {"canonical wireguard", map[string]any{"protocol": "wireguard"}, true},
  44. {"capitalised wireguard", map[string]any{"protocol": "WireGuard"}, true},
  45. {"upper hysteria", map[string]any{"protocol": "HYSTERIA"}, true},
  46. {"amneziawg", map[string]any{"protocol": "amneziawg"}, true},
  47. // The core's MASQUE client dials HTTP/3 over QUIC unless the ALPN offers h2 without h3.
  48. {"masque over http/3", map[string]any{"protocol": "masque", "streamSettings": map[string]any{"network": "masque"}}, true},
  49. {"masque pinned to http/2", map[string]any{"protocol": "masque", "streamSettings": map[string]any{"network": "masque", "tlsSettings": map[string]any{"alpn": []any{"h2"}}}}, false},
  50. {"kcp transport", map[string]any{"streamSettings": map[string]any{"network": "kcp"}}, true},
  51. {"kcp transport capitalised", map[string]any{"streamSettings": map[string]any{"network": "KCP"}}, true},
  52. {"mkcp alias", map[string]any{"streamSettings": map[string]any{"network": "mkcp"}}, true},
  53. {"mkcp alias capitalised", map[string]any{"streamSettings": map[string]any{"network": "MKCP"}}, true},
  54. {"tcp transport", map[string]any{"streamSettings": map[string]any{"network": "tcp"}}, false},
  55. {"plain vless", map[string]any{"protocol": "vless"}, false},
  56. {"matched but tcp", map[string]any{"protocol": "vless", "streamSettings": map[string]any{"network": "ws"}}, false},
  57. }
  58. for _, tt := range tests {
  59. t.Run(tt.name, func(t *testing.T) {
  60. if got := outboundTransportIsUDP(tt.ob); got != tt.want {
  61. t.Errorf("outboundTransportIsUDP(%v) = %v, want %v", tt.ob, got, tt.want)
  62. }
  63. })
  64. }
  65. }
  66. func TestBuildBatchTestConfigReadsTheProtocolIDLikeTheCore(t *testing.T) {
  67. items := []*httpBatchItem{
  68. {tag: "wg", outbound: map[string]any{"tag": "wg", "protocol": "WireGuard"}},
  69. {tag: "awg", outbound: map[string]any{"tag": "awg", "protocol": "AmneziaWG"}},
  70. }
  71. cfg := buildBatchTestConfig(items, nil, []int{61011, 61012})
  72. raw, err := json.Marshal(cfg)
  73. if err != nil {
  74. t.Fatalf("marshal config: %v", err)
  75. }
  76. var m map[string]any
  77. if err := json.Unmarshal(raw, &m); err != nil {
  78. t.Fatalf("unmarshal config: %v", err)
  79. }
  80. outbounds, _ := m["outbounds"].([]any)
  81. byTag := make(map[string]map[string]any, len(outbounds))
  82. for _, entry := range outbounds {
  83. ob, _ := entry.(map[string]any)
  84. tag, _ := ob["tag"].(string)
  85. byTag[tag] = ob
  86. }
  87. wg := byTag["wg"]
  88. if wg == nil {
  89. t.Fatalf("wg outbound missing from the temp config: %v", outbounds)
  90. }
  91. if settings, _ := wg["settings"].(map[string]any); settings == nil || settings["noKernelTun"] != true {
  92. t.Errorf(`"WireGuard" settings = %v, want noKernelTun: the probe instance must not create a kernel device`, wg["settings"])
  93. }
  94. awg := byTag["awg"]
  95. if awg == nil {
  96. t.Fatalf("awg outbound missing from the temp config: %v", outbounds)
  97. }
  98. if protocol, _ := awg["protocol"].(string); protocol != "socks" {
  99. t.Errorf(`"AmneziaWG" protocol = %q, want %q: a raw amneziawg entry fails the whole temp config`, protocol, "socks")
  100. }
  101. }
  102. func TestTestOutboundsRejectsUntestableIDsInAnyCase(t *testing.T) {
  103. tests := []struct {
  104. name string
  105. protocol string
  106. }{
  107. {"canonical freedom", "freedom"},
  108. {"capitalised freedom", "Freedom"},
  109. {"upper freedom", "FREEDOM"},
  110. {"canonical blackhole", "blackhole"},
  111. {"capitalised blackhole", "Blackhole"},
  112. }
  113. const wantErr = "Outbound has no testable endpoint"
  114. for _, tt := range tests {
  115. t.Run(tt.name, func(t *testing.T) {
  116. batch := mustJSON(t, []any{map[string]any{"tag": "t1", "protocol": tt.protocol}})
  117. results, err := (&OutboundService{}).TestOutbounds(batch, "", "", "tcp")
  118. if err != nil {
  119. t.Fatalf("TestOutbounds: %v", err)
  120. }
  121. r := results[0]
  122. if r.Success {
  123. t.Errorf("%q outbound = %+v, want a rejection", tt.protocol, r)
  124. }
  125. if r.Error != wantErr {
  126. t.Errorf("%q error = %q, want %q", tt.protocol, r.Error, wantErr)
  127. }
  128. })
  129. }
  130. }
  131. func TestTestOutboundsTCPLaneReadsProtocolIDCaseInsensitively(t *testing.T) {
  132. l, err := net.Listen("tcp", "127.0.0.1:0")
  133. if err != nil {
  134. t.Fatalf("listen: %v", err)
  135. }
  136. defer l.Close()
  137. go func() {
  138. for {
  139. conn, err := l.Accept()
  140. if err != nil {
  141. return
  142. }
  143. conn.Close()
  144. }
  145. }()
  146. port := l.Addr().(*net.TCPAddr).Port
  147. batch := mustJSON(t, []any{map[string]any{
  148. "tag": "t1",
  149. "protocol": "SOCKS",
  150. "settings": map[string]any{"servers": []any{map[string]any{"address": "127.0.0.1", "port": port}}},
  151. }})
  152. results, err := (&OutboundService{}).TestOutbounds(batch, "", "", "tcp")
  153. if err != nil {
  154. t.Fatalf("TestOutbounds: %v", err)
  155. }
  156. r := results[0]
  157. if !r.Success || r.Mode != "tcp" || len(r.Endpoints) != 1 {
  158. t.Errorf(`"SOCKS" outbound in tcp mode = %+v, want a successful tcp probe with one endpoint`, r)
  159. }
  160. }