mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-15 17:16:07 +00:00
fix(nodes): honor TLS verify mode skip/pin for remote node operations (#5264)
The node probe honored the per-node TlsVerifyMode (skip/pin) but runtime.Remote used a shared client with no TLSClientConfig, so traffic sync and every other remote op fell back to system-CA verification and failed against self-signed nodes even after the operator set skip/pin. Move the TLS client builder into the runtime layer (HTTPClientForNode / DecodeCertPin) as the single source of truth, have Remote build and cache its per-node client through it, and delegate the service probe to the same builder so the two paths can no longer diverge.
This commit is contained in:
@@ -3,10 +3,8 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -44,75 +42,6 @@ type HeartbeatPatch struct {
|
||||
|
||||
type NodeService struct{}
|
||||
|
||||
var nodeHTTPClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
MaxIdleConns: 64,
|
||||
MaxIdleConnsPerHost: 4,
|
||||
IdleConnTimeout: 60 * time.Second,
|
||||
DialContext: netsafe.SSRFGuardedDialContext,
|
||||
},
|
||||
}
|
||||
|
||||
// nodeHTTPClientFor returns the HTTP client used to reach a node, honoring its
|
||||
// per-node TLS verification mode. "verify" (or any http node) uses the shared
|
||||
// client with default certificate validation. "skip" disables validation.
|
||||
// "pin" disables the default chain check but verifies the leaf certificate's
|
||||
// SHA-256 against the stored pin, keeping MITM protection for self-signed certs.
|
||||
func nodeHTTPClientFor(n *model.Node) (*http.Client, error) {
|
||||
mode := n.TlsVerifyMode
|
||||
if mode == "" {
|
||||
mode = "verify"
|
||||
}
|
||||
if mode == "verify" || n.Scheme == "http" {
|
||||
return nodeHTTPClient, nil
|
||||
}
|
||||
tlsCfg := &tls.Config{InsecureSkipVerify: true}
|
||||
if mode == "pin" {
|
||||
want, err := decodeCertPin(n.PinnedCertSha256)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsCfg.VerifyConnection = func(cs tls.ConnectionState) error {
|
||||
if len(cs.PeerCertificates) == 0 {
|
||||
return common.NewError("node presented no certificate")
|
||||
}
|
||||
sum := sha256.Sum256(cs.PeerCertificates[0].Raw)
|
||||
if subtle.ConstantTimeCompare(sum[:], want) != 1 {
|
||||
return common.NewError("node certificate does not match pinned SHA-256")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return &http.Client{
|
||||
Transport: &http.Transport{
|
||||
MaxIdleConns: 64,
|
||||
MaxIdleConnsPerHost: 4,
|
||||
IdleConnTimeout: 60 * time.Second,
|
||||
DialContext: netsafe.SSRFGuardedDialContext,
|
||||
TLSClientConfig: tlsCfg,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// decodeCertPin accepts a SHA-256 certificate hash as base64 (the format used
|
||||
// by Xray's pinnedPeerCertSha256) or hex with optional colons (the openssl
|
||||
// -fingerprint style) and returns the 32 raw bytes.
|
||||
func decodeCertPin(s string) ([]byte, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return nil, common.NewError("certificate pin is empty")
|
||||
}
|
||||
if b, err := hex.DecodeString(strings.ReplaceAll(s, ":", "")); err == nil && len(b) == sha256.Size {
|
||||
return b, nil
|
||||
}
|
||||
for _, enc := range []*base64.Encoding{base64.StdEncoding, base64.RawStdEncoding, base64.URLEncoding, base64.RawURLEncoding} {
|
||||
if b, err := enc.DecodeString(s); err == nil && len(b) == sha256.Size {
|
||||
return b, nil
|
||||
}
|
||||
}
|
||||
return nil, common.NewError("certificate pin must be a SHA-256 hash (base64 or hex)")
|
||||
}
|
||||
|
||||
// FetchCertFingerprint connects to the node over HTTPS without verifying the
|
||||
// certificate and returns the leaf certificate's SHA-256 as base64, so the UI
|
||||
// can offer a "fetch and pin current certificate" action.
|
||||
@@ -367,7 +296,7 @@ func (s *NodeService) normalize(n *model.Node) error {
|
||||
n.InboundTags = tags
|
||||
}
|
||||
if n.TlsVerifyMode == "pin" {
|
||||
if _, err := decodeCertPin(n.PinnedCertSha256); err != nil {
|
||||
if _, err := runtime.DecodeCertPin(n.PinnedCertSha256); err != nil {
|
||||
return common.NewError(err.Error())
|
||||
}
|
||||
}
|
||||
@@ -692,7 +621,7 @@ func (s *NodeService) Probe(ctx context.Context, n *model.Node) (HeartbeatPatch,
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
client, err := nodeHTTPClientFor(n)
|
||||
client, err := runtime.HTTPClientForNode(n)
|
||||
if err != nil {
|
||||
patch.LastError = err.Error()
|
||||
return patch, err
|
||||
|
||||
Reference in New Issue
Block a user