tls_client_test.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  1. package runtime
  2. import (
  3. "context"
  4. "crypto/sha256"
  5. "crypto/tls"
  6. "encoding/base64"
  7. "encoding/hex"
  8. "net/http"
  9. "net/http/httptest"
  10. "net/url"
  11. "strconv"
  12. "strings"
  13. "sync"
  14. "sync/atomic"
  15. "testing"
  16. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  17. "github.com/mhsanaei/3x-ui/v3/internal/util/crypto"
  18. )
  19. type generationProbeTransport struct {
  20. id string
  21. closed atomic.Int32
  22. }
  23. func (t *generationProbeTransport) RoundTrip(*http.Request) (*http.Response, error) {
  24. return &http.Response{
  25. StatusCode: http.StatusOK,
  26. Body: http.NoBody,
  27. Header: make(http.Header),
  28. Request: &http.Request{},
  29. }, nil
  30. }
  31. func (t *generationProbeTransport) CloseIdleConnections() {
  32. t.closed.Add(1)
  33. }
  34. func TestCredentialRotatingTransportDropsOldPoolBeforeNextRequest(t *testing.T) {
  35. var selected atomic.Pointer[generationProbeTransport]
  36. oldTransport := &generationProbeTransport{id: "old"}
  37. newTransport := &generationProbeTransport{id: "new"}
  38. selected.Store(oldTransport)
  39. rotating, err := newCredentialRotatingTransport(func() (idleClosingRoundTripper, error) {
  40. return selected.Load(), nil
  41. })
  42. if err != nil {
  43. t.Fatalf("newCredentialRotatingTransport: %v", err)
  44. }
  45. rotating.mu.Lock()
  46. initial := rotating.current
  47. rotating.mu.Unlock()
  48. if initial != oldTransport {
  49. t.Fatalf("initial transport = %p, want old %p", initial, oldTransport)
  50. }
  51. selected.Store(newTransport)
  52. InvalidateMasterClientConnections()
  53. req := httptest.NewRequest(http.MethodGet, "https://node.example.test/panel/api/server/status", nil)
  54. resp, err := rotating.RoundTrip(req)
  55. if err != nil {
  56. t.Fatalf("RoundTrip after credential rotation: %v", err)
  57. }
  58. _ = resp.Body.Close()
  59. rotating.mu.Lock()
  60. current := rotating.current
  61. rotating.mu.Unlock()
  62. if current != newTransport {
  63. t.Fatalf("transport after invalidation = %p, want new %p", current, newTransport)
  64. }
  65. if got := oldTransport.closed.Load(); got != 1 {
  66. t.Fatalf("old transport CloseIdleConnections calls = %d, want 1", got)
  67. }
  68. }
  69. func TestReloadMasterClientConnectionsValidatesProviderBeforeInvalidation(t *testing.T) {
  70. before := masterCertEpoch.Load()
  71. SetMasterClientCertProvider(func() (tls.Certificate, error) {
  72. return tls.Certificate{}, context.Canceled
  73. })
  74. if err := ReloadMasterClientConnections(); err == nil {
  75. t.Fatal("reload with an invalid provider unexpectedly succeeded")
  76. }
  77. if got := masterCertEpoch.Load(); got != before {
  78. t.Fatalf("failed reload changed generation from %d to %d", before, got)
  79. }
  80. SetMasterClientCertProvider(func() (tls.Certificate, error) {
  81. return masterCertForTest(t), nil
  82. })
  83. t.Cleanup(func() { SetMasterClientCertProvider(nil) })
  84. if err := ReloadMasterClientConnections(); err != nil {
  85. t.Fatalf("ReloadMasterClientConnections: %v", err)
  86. }
  87. if got := masterCertEpoch.Load(); got != before+1 {
  88. t.Fatalf("successful reload generation = %d, want %d", got, before+1)
  89. }
  90. }
  91. func TestCredentialRotatingTransportRejectsBuildAcrossInvalidation(t *testing.T) {
  92. oldTransport := &generationProbeTransport{id: "old"}
  93. newTransport := &generationProbeTransport{id: "new"}
  94. var selected atomic.Pointer[generationProbeTransport]
  95. selected.Store(oldTransport)
  96. firstBuildCaptured := make(chan struct{})
  97. releaseFirstBuild := make(chan struct{})
  98. var once sync.Once
  99. build := func() (idleClosingRoundTripper, error) {
  100. captured := selected.Load()
  101. once.Do(func() {
  102. close(firstBuildCaptured)
  103. <-releaseFirstBuild
  104. })
  105. return captured, nil
  106. }
  107. type result struct {
  108. transport *credentialRotatingTransport
  109. err error
  110. }
  111. resultCh := make(chan result, 1)
  112. go func() {
  113. transport, err := newCredentialRotatingTransport(build)
  114. resultCh <- result{transport: transport, err: err}
  115. }()
  116. <-firstBuildCaptured
  117. selected.Store(newTransport)
  118. InvalidateMasterClientConnections()
  119. close(releaseFirstBuild)
  120. got := <-resultCh
  121. if got.err != nil {
  122. t.Fatalf("newCredentialRotatingTransport: %v", got.err)
  123. }
  124. got.transport.mu.Lock()
  125. current := got.transport.current
  126. got.transport.mu.Unlock()
  127. if current != newTransport {
  128. t.Fatalf("transport built across invalidation = %p, want new %p", current, newTransport)
  129. }
  130. if calls := oldTransport.closed.Load(); calls != 1 {
  131. t.Fatalf("stale transport CloseIdleConnections calls = %d, want 1", calls)
  132. }
  133. }
  134. func TestHTTPClientForNodeMTLSRebuildsTLSConfigAfterCredentialInvalidation(t *testing.T) {
  135. oldCert := masterCertForTest(t)
  136. newCert := masterCertForTest(t)
  137. selected := oldCert
  138. SetMasterClientCertProvider(func() (tls.Certificate, error) { return selected, nil })
  139. t.Cleanup(func() { SetMasterClientCertProvider(nil) })
  140. client, err := HTTPClientForNode(&model.Node{
  141. Scheme: "https",
  142. Address: "node.example.test",
  143. Port: 443,
  144. TlsVerifyMode: "mtls",
  145. }, "")
  146. if err != nil {
  147. t.Fatalf("HTTPClientForNode: %v", err)
  148. }
  149. rotating, ok := client.Transport.(*credentialRotatingTransport)
  150. if !ok {
  151. t.Fatalf("transport = %T, want *credentialRotatingTransport", client.Transport)
  152. }
  153. leaf := func() []byte {
  154. rotating.mu.Lock()
  155. defer rotating.mu.Unlock()
  156. transport, ok := rotating.current.(*http.Transport)
  157. if !ok {
  158. t.Fatalf("current transport = %T, want *http.Transport", rotating.current)
  159. }
  160. return transport.TLSClientConfig.Certificates[0].Certificate[0]
  161. }
  162. if got := leaf(); string(got) != string(oldCert.Certificate[0]) {
  163. t.Fatal("initial TLS config does not contain the old credential")
  164. }
  165. selected = newCert
  166. InvalidateMasterClientConnections()
  167. ctx, cancel := context.WithCancel(context.Background())
  168. cancel()
  169. req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://node.example.test/", nil)
  170. if err != nil {
  171. t.Fatalf("NewRequestWithContext: %v", err)
  172. }
  173. if _, err := client.Do(req); err == nil {
  174. t.Fatal("canceled request unexpectedly succeeded")
  175. }
  176. if got := leaf(); string(got) != string(newCert.Certificate[0]) {
  177. t.Fatal("TLS config retained the old credential after invalidation")
  178. }
  179. }
  180. func TestHTTPClientForNodeProxyMTLSRebuildKeepsProxyAndNewCredential(t *testing.T) {
  181. oldCert := masterCertForTest(t)
  182. newCert := masterCertForTest(t)
  183. selected := oldCert
  184. SetMasterClientCertProvider(func() (tls.Certificate, error) { return selected, nil })
  185. t.Cleanup(func() { SetMasterClientCertProvider(nil) })
  186. const proxyURL = "http://127.0.0.1:18080"
  187. client, err := HTTPClientForNode(&model.Node{Scheme: "https", TlsVerifyMode: "mtls"}, proxyURL)
  188. if err != nil {
  189. t.Fatalf("HTTPClientForNode: %v", err)
  190. }
  191. rotating, ok := client.Transport.(*credentialRotatingTransport)
  192. if !ok {
  193. t.Fatalf("transport = %T, want rotating transport", client.Transport)
  194. }
  195. current := func() *http.Transport {
  196. rotating.mu.Lock()
  197. defer rotating.mu.Unlock()
  198. transport, ok := rotating.current.(*http.Transport)
  199. if !ok {
  200. t.Fatalf("current transport = %T, want *http.Transport", rotating.current)
  201. }
  202. return transport
  203. }
  204. assertProxy := func(transport *http.Transport) {
  205. t.Helper()
  206. if transport.Proxy == nil {
  207. t.Fatalf("proxy function is nil, want %s", proxyURL)
  208. }
  209. req, _ := http.NewRequest(http.MethodGet, "https://node.example.test/", nil)
  210. got, err := transport.Proxy(req)
  211. if err != nil || got == nil || got.String() != proxyURL {
  212. t.Fatalf("proxy = %v, error = %v, want %s", got, err, proxyURL)
  213. }
  214. }
  215. assertProxy(current())
  216. selected = newCert
  217. InvalidateMasterClientConnections()
  218. ctx, cancel := context.WithCancel(context.Background())
  219. cancel()
  220. req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "https://node.example.test/", nil)
  221. _, _ = client.Do(req)
  222. rebuilt := current()
  223. assertProxy(rebuilt)
  224. if got := rebuilt.TLSClientConfig.Certificates[0].Certificate[0]; string(got) != string(newCert.Certificate[0]) {
  225. t.Fatal("proxy mTLS rebuild retained the old credential")
  226. }
  227. }
  228. // masterCertForTest builds a real CA-signed client certificate for mtls tests.
  229. func masterCertForTest(t *testing.T) tls.Certificate {
  230. t.Helper()
  231. ca, err := crypto.GenerateNodeCA("test ca")
  232. if err != nil {
  233. t.Fatalf("GenerateNodeCA: %v", err)
  234. }
  235. client, err := crypto.IssueClientCert(ca, "master")
  236. if err != nil {
  237. t.Fatalf("IssueClientCert: %v", err)
  238. }
  239. cert, err := tls.X509KeyPair(client.CertPEM, client.KeyPEM)
  240. if err != nil {
  241. t.Fatalf("X509KeyPair: %v", err)
  242. }
  243. return cert
  244. }
  245. // TestTLSConfigForNode_MTLS_PresentsClientCert asserts the mtls branch presents
  246. // the master client cert and verifies the node's server cert against system
  247. // roots (no InsecureSkipVerify, no custom RootCAs).
  248. func TestTLSConfigForNode_MTLS_PresentsClientCert(t *testing.T) {
  249. cert := masterCertForTest(t)
  250. SetMasterClientCertProvider(func() (tls.Certificate, error) { return cert, nil })
  251. t.Cleanup(func() { SetMasterClientCertProvider(nil) })
  252. cfg, err := tlsConfigForNode(&model.Node{TlsVerifyMode: "mtls"})
  253. if err != nil {
  254. t.Fatalf("tlsConfigForNode(mtls): %v", err)
  255. }
  256. if len(cfg.Certificates) != 1 {
  257. t.Fatalf("mtls config must present exactly one client certificate, got %d", len(cfg.Certificates))
  258. }
  259. if cfg.InsecureSkipVerify {
  260. t.Fatal("mtls must NOT skip server verification")
  261. }
  262. if cfg.RootCAs != nil {
  263. t.Fatal("mtls verifies the node server against system roots (RootCAs must be nil)")
  264. }
  265. }
  266. // TestTLSConfigForNode_MTLS_NoProviderFailsClosed asserts mtls fails closed when
  267. // no master client certificate is available, rather than silently dropping auth.
  268. func TestTLSConfigForNode_MTLS_NoProviderFailsClosed(t *testing.T) {
  269. SetMasterClientCertProvider(nil)
  270. if _, err := tlsConfigForNode(&model.Node{TlsVerifyMode: "mtls"}); err == nil {
  271. t.Fatal("mtls without a configured client cert provider must fail closed")
  272. }
  273. }
  274. // nodeForServer builds a node pointing at a loopback test server (loopback is
  275. // SSRF-blocked, so AllowPrivateAddress is set for the guarded dialer).
  276. func nodeForServer(t *testing.T, srv *httptest.Server, mode, pin string) *model.Node {
  277. t.Helper()
  278. u, err := url.Parse(srv.URL)
  279. if err != nil {
  280. t.Fatalf("parse server url: %v", err)
  281. }
  282. port, err := strconv.Atoi(u.Port())
  283. if err != nil {
  284. t.Fatalf("parse server port: %v", err)
  285. }
  286. return &model.Node{
  287. Id: 1,
  288. Name: "n1",
  289. Scheme: "https",
  290. Address: u.Hostname(),
  291. Port: port,
  292. BasePath: "/",
  293. ApiToken: "token",
  294. Enable: true,
  295. AllowPrivateAddress: true,
  296. TlsVerifyMode: mode,
  297. PinnedCertSha256: pin,
  298. }
  299. }
  300. func leafPinBase64(srv *httptest.Server) string {
  301. sum := sha256.Sum256(srv.Certificate().Raw)
  302. return base64.StdEncoding.EncodeToString(sum[:])
  303. }
  304. // A self-signed node must be reachable by Remote ops under skip/pin and
  305. // rejected under verify — the split issue #5264 reported.
  306. func TestRemoteHonorsTLSVerifyMode(t *testing.T) {
  307. srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
  308. w.Header().Set("Content-Type", "application/json")
  309. _, _ = w.Write([]byte(`{"success":true,"obj":[]}`))
  310. }))
  311. defer srv.Close()
  312. goodPin := leafPinBase64(srv)
  313. wrongPin := base64.StdEncoding.EncodeToString(make([]byte, sha256.Size))
  314. cases := []struct {
  315. name string
  316. mode string
  317. pin string
  318. wantErr bool
  319. }{
  320. {"verify rejects self-signed", "verify", "", true},
  321. {"skip accepts self-signed", "skip", "", false},
  322. {"pin accepts matching cert", "pin", goodPin, false},
  323. {"pin rejects mismatched cert", "pin", wrongPin, true},
  324. }
  325. for _, c := range cases {
  326. t.Run(c.name, func(t *testing.T) {
  327. r := NewRemote(nodeForServer(t, srv, c.mode, c.pin), nil)
  328. _, err := r.ListInboundOptions(context.Background())
  329. if c.wantErr && err == nil {
  330. t.Fatalf("mode %q: expected error, got nil", c.mode)
  331. }
  332. if !c.wantErr && err != nil {
  333. t.Fatalf("mode %q: unexpected error: %v", c.mode, err)
  334. }
  335. })
  336. }
  337. }
  338. // The lazily-built client is cached for the Remote's lifetime so repeated
  339. // operations reuse one pooled transport rather than rebuilding TLS each call.
  340. func TestRemoteClientCached(t *testing.T) {
  341. r := NewRemote(&model.Node{Scheme: "https", TlsVerifyMode: "skip"}, nil)
  342. c1, err1 := r.httpClient()
  343. c2, err2 := r.httpClient()
  344. if err1 != nil || err2 != nil {
  345. t.Fatalf("httpClient errors: %v %v", err1, err2)
  346. }
  347. if c1 != c2 {
  348. t.Fatal("expected the same cached client across calls")
  349. }
  350. }
  351. func TestHTTPClientForNodeVerifyShared(t *testing.T) {
  352. // verify mode and plain http both reuse the shared default client.
  353. for _, n := range []*model.Node{
  354. {Scheme: "https", TlsVerifyMode: "verify"},
  355. {Scheme: "https", TlsVerifyMode: ""},
  356. {Scheme: "http", TlsVerifyMode: "skip"},
  357. } {
  358. c, err := HTTPClientForNode(n, "")
  359. if err != nil {
  360. t.Fatalf("HTTPClientForNode(%+v): %v", n, err)
  361. }
  362. if c != defaultNodeHTTPClient {
  363. t.Fatalf("HTTPClientForNode(%+v) = %p, want shared default %p", n, c, defaultNodeHTTPClient)
  364. }
  365. }
  366. }
  367. func TestHTTPClientForNodePinInvalid(t *testing.T) {
  368. // pin mode must fail closed, and with a specific error per cause — not merely
  369. // "some error" (which a bug anywhere in the build path would also satisfy).
  370. cases := []struct {
  371. name string
  372. pin string
  373. wantErr string
  374. }{
  375. {"garbage pin", "not-a-pin", "must be a SHA-256 hash"},
  376. {"empty pin", "", "certificate pin is empty"},
  377. }
  378. for _, c := range cases {
  379. t.Run(c.name, func(t *testing.T) {
  380. _, err := HTTPClientForNode(&model.Node{Scheme: "https", TlsVerifyMode: "pin", PinnedCertSha256: c.pin}, "")
  381. if err == nil {
  382. t.Fatalf("expected error for pin %q", c.pin)
  383. }
  384. if !strings.Contains(err.Error(), c.wantErr) {
  385. t.Fatalf("error = %q, want it to contain %q", err.Error(), c.wantErr)
  386. }
  387. })
  388. }
  389. }
  390. // TestHTTPClientForNode_ProxyPinPreservesPinEnforcement covers the proxy+pin branch
  391. // (tls_client.go:43-52): when a node uses a proxy AND pin mode, the proxy client's
  392. // transport must carry the pinning tls.Config (the `transport.TLSClientConfig = tlsCfg`
  393. // line). Dropping it would silently disable certificate pinning whenever a proxy is set.
  394. func TestHTTPClientForNode_ProxyPinPreservesPinEnforcement(t *testing.T) {
  395. pin := base64.StdEncoding.EncodeToString(make([]byte, sha256.Size))
  396. n := &model.Node{Scheme: "https", TlsVerifyMode: "pin", PinnedCertSha256: pin}
  397. c, err := HTTPClientForNode(n, "socks5://127.0.0.1:1080")
  398. if err != nil {
  399. t.Fatalf("HTTPClientForNode: %v", err)
  400. }
  401. if c == defaultNodeHTTPClient {
  402. t.Fatal("proxy client must not be the shared default client")
  403. }
  404. tr, ok := c.Transport.(*http.Transport)
  405. if !ok {
  406. t.Fatalf("transport is %T, want *http.Transport", c.Transport)
  407. }
  408. if tr.TLSClientConfig == nil || tr.TLSClientConfig.VerifyConnection == nil {
  409. t.Fatal("pin mode over a proxy must install a pinning tls.Config (VerifyConnection); pin enforcement was dropped")
  410. }
  411. }
  412. // TestHTTPClientForNode_ProxyVerifyNoPin covers the proxy+verify branch
  413. // (tls_client.go:40-42): verify mode over a proxy returns the proxy client as-is,
  414. // using system-CA verification and NOT a pin VerifyConnection.
  415. func TestHTTPClientForNode_ProxyVerifyNoPin(t *testing.T) {
  416. n := &model.Node{Scheme: "https", TlsVerifyMode: "verify"}
  417. c, err := HTTPClientForNode(n, "socks5://127.0.0.1:1080")
  418. if err != nil {
  419. t.Fatalf("HTTPClientForNode: %v", err)
  420. }
  421. if c == defaultNodeHTTPClient {
  422. t.Fatal("proxy client must not be the shared default client")
  423. }
  424. if tr, ok := c.Transport.(*http.Transport); ok && tr.TLSClientConfig != nil && tr.TLSClientConfig.VerifyConnection != nil {
  425. t.Fatal("verify mode must not install a pin VerifyConnection")
  426. }
  427. }
  428. // TestTLSConfigForNode_CurrentContract locks the pre-mTLS behavior of
  429. // tlsConfigForNode so the "mtls" branch added later cannot silently regress the
  430. // existing skip/pin modes (characterization — passes on unchanged code).
  431. func TestTLSConfigForNode_CurrentContract(t *testing.T) {
  432. t.Run("skip disables verification with no VerifyConnection", func(t *testing.T) {
  433. cfg, err := tlsConfigForNode(&model.Node{TlsVerifyMode: "skip"})
  434. if err != nil {
  435. t.Fatalf("unexpected error: %v", err)
  436. }
  437. if !cfg.InsecureSkipVerify {
  438. t.Fatal("skip mode must set InsecureSkipVerify")
  439. }
  440. if cfg.VerifyConnection != nil {
  441. t.Fatal("skip mode must not install a VerifyConnection")
  442. }
  443. })
  444. t.Run("pin installs a VerifyConnection", func(t *testing.T) {
  445. pin := base64.StdEncoding.EncodeToString(make([]byte, sha256.Size))
  446. cfg, err := tlsConfigForNode(&model.Node{TlsVerifyMode: "pin", PinnedCertSha256: pin})
  447. if err != nil {
  448. t.Fatalf("unexpected error: %v", err)
  449. }
  450. if cfg.VerifyConnection == nil {
  451. t.Fatal("pin mode must install a VerifyConnection")
  452. }
  453. })
  454. }
  455. func TestDecodeCertPin(t *testing.T) {
  456. raw := sha256.Sum256([]byte("cert"))
  457. hexColon := strings.ToUpper(hex.EncodeToString(raw[:]))
  458. // reinsert colons in openssl -fingerprint style
  459. var withColons strings.Builder
  460. for i := 0; i < len(hexColon); i += 2 {
  461. if i > 0 {
  462. withColons.WriteByte(':')
  463. }
  464. withColons.WriteString(hexColon[i : i+2])
  465. }
  466. cases := []struct {
  467. name string
  468. in string
  469. wantErr bool
  470. }{
  471. {"base64 std", base64.StdEncoding.EncodeToString(raw[:]), false},
  472. {"base64 raw url", base64.RawURLEncoding.EncodeToString(raw[:]), false},
  473. {"hex bare", hex.EncodeToString(raw[:]), false},
  474. {"hex colon openssl", withColons.String(), false},
  475. {"empty", "", true},
  476. {"garbage", "not-a-pin", true},
  477. }
  478. for _, c := range cases {
  479. t.Run(c.name, func(t *testing.T) {
  480. got, err := DecodeCertPin(c.in)
  481. if c.wantErr {
  482. if err == nil {
  483. t.Fatalf("expected error for %q", c.in)
  484. }
  485. return
  486. }
  487. if err != nil {
  488. t.Fatalf("unexpected error for %q: %v", c.in, err)
  489. }
  490. if string(got) != string(raw[:]) {
  491. t.Fatalf("decoded bytes mismatch for %q", c.in)
  492. }
  493. })
  494. }
  495. }