mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-04 03:44:22 +00:00
05ad7f417c
* feat: add per-node outbound routing for panel-to-node connections * feat(ui): add outbound tag selector to node form with i18n * fix(xray): avoid potential overflow warning in node egress rule allocation * chore: run "npm run gen" * fix --------- Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
110 lines
3.0 KiB
Go
110 lines
3.0 KiB
Go
package runtime
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"crypto/subtle"
|
|
"crypto/tls"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/util/common"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/util/netproxy"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/util/netsafe"
|
|
)
|
|
|
|
// defaultNodeHTTPClient reaches nodes trusting the system CA store ("verify"
|
|
// mode or plain http); shared so connections pool across nodes.
|
|
var defaultNodeHTTPClient = &http.Client{
|
|
Transport: &http.Transport{
|
|
MaxIdleConns: 64,
|
|
MaxIdleConnsPerHost: 4,
|
|
IdleConnTimeout: 60 * time.Second,
|
|
DialContext: netsafe.SSRFGuardedDialContext,
|
|
},
|
|
}
|
|
|
|
func HTTPClientForNode(n *model.Node, proxyURL string) (*http.Client, error) {
|
|
mode := n.TlsVerifyMode
|
|
if mode == "" {
|
|
mode = "verify"
|
|
}
|
|
if proxyURL != "" {
|
|
client, err := netproxy.NewHTTPClient(proxyURL, remoteHTTPTimeout)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if mode == "verify" || n.Scheme == "http" {
|
|
return client, nil
|
|
}
|
|
transport, ok := client.Transport.(*http.Transport)
|
|
if !ok {
|
|
return client, nil
|
|
}
|
|
tlsCfg, err := tlsConfigForNode(n)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
transport.TLSClientConfig = tlsCfg
|
|
return client, nil
|
|
}
|
|
if mode == "verify" || n.Scheme == "http" {
|
|
return defaultNodeHTTPClient, nil
|
|
}
|
|
tlsCfg, err := tlsConfigForNode(n)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &http.Client{
|
|
Transport: &http.Transport{
|
|
MaxIdleConns: 64,
|
|
MaxIdleConnsPerHost: 4,
|
|
IdleConnTimeout: 60 * time.Second,
|
|
DialContext: netsafe.SSRFGuardedDialContext,
|
|
TLSClientConfig: tlsCfg,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func tlsConfigForNode(n *model.Node) (*tls.Config, error) {
|
|
tlsCfg := &tls.Config{InsecureSkipVerify: true} // lgtm[go/disabled-certificate-check]
|
|
if n.TlsVerifyMode == "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 tlsCfg, nil
|
|
}
|
|
|
|
// DecodeCertPin decodes a SHA-256 cert pin given as base64 (Xray's
|
|
// pinnedPeerCertSha256 form) or hex with optional colons into 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)")
|
|
}
|