tls_client_test.go 21 KB

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