mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 11:15:03 +03:00
fix(node): keep master requests on the node they were sent to
The master's node clients followed redirects to any host, presenting the same panel-wide credentials (the mTLS client certificate) and replaying the request there, so one node could have the master push to another. Node clients and the certificate-pin probe now stop at a redirect off the original host:port and report the 3xx as the node's answer. The probe change is wiring; TestNodeClientsDoNotFollowARedirectToAnotherHost covers the guard.
This commit is contained in:
@@ -152,7 +152,16 @@ func newNodeTransport(tlsCfg *tls.Config) *http.Transport {
|
||||
|
||||
// defaultNodeHTTPClient reaches nodes trusting the system CA store ("verify"
|
||||
// mode or plain http); shared so connections pool across nodes.
|
||||
var defaultNodeHTTPClient = &http.Client{Transport: newNodeTransport(nil)}
|
||||
var defaultNodeHTTPClient = &http.Client{Transport: newNodeTransport(nil), CheckRedirect: NoCrossHostRedirect}
|
||||
|
||||
// NoCrossHostRedirect keeps a request on the host:port it was sent to: the master presents
|
||||
// the same credentials to every node, so a node's Location must not carry them elsewhere.
|
||||
func NoCrossHostRedirect(req *http.Request, via []*http.Request) error {
|
||||
if len(via) >= 10 || !strings.EqualFold(req.URL.Host, via[0].URL.Host) {
|
||||
return http.ErrUseLastResponse
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// nodeClients caches one client per node: heartbeat and traffic sync reach it
|
||||
// every few seconds, and a rebuilt client would open its own empty pool.
|
||||
@@ -214,6 +223,7 @@ func HTTPClientForNode(n *model.Node, proxyURL string) (*http.Client, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client.CheckRedirect = NoCrossHostRedirect
|
||||
|
||||
nodeClientsMu.Lock()
|
||||
if entry, ok := nodeClientsCache[key]; ok {
|
||||
|
||||
@@ -662,3 +662,34 @@ func TestDecodeCertPin(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// The master presents the same credentials to every node; a node's redirect must not
|
||||
// carry a request (a push, the mTLS certificate) to another host.
|
||||
func TestNodeClientsDoNotFollowARedirectToAnotherHost(t *testing.T) {
|
||||
var otherHit atomic.Int32
|
||||
other := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { otherHit.Add(1) }))
|
||||
defer other.Close()
|
||||
hostile := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, other.URL+"/panel/api/inbounds/update/1", http.StatusTemporaryRedirect)
|
||||
}))
|
||||
defer hostile.Close()
|
||||
u, _ := url.Parse(hostile.URL)
|
||||
port, _ := strconv.Atoi(u.Port())
|
||||
for _, node := range []*model.Node{
|
||||
{Id: 41, Address: u.Hostname(), Port: port, Scheme: "https", TlsVerifyMode: "skip", AllowPrivateAddress: true},
|
||||
{Id: 42, Address: u.Hostname(), Port: port, Scheme: "http", AllowPrivateAddress: true},
|
||||
} {
|
||||
client, err := HTTPClientForNode(node, "")
|
||||
if err != nil {
|
||||
t.Fatalf("HTTPClientForNode: %v", err)
|
||||
}
|
||||
req, _ := http.NewRequestWithContext(netsafe.ContextWithAllowPrivate(context.Background(), true),
|
||||
http.MethodPost, hostile.URL+"/panel/api/inbounds/list", strings.NewReader("{}"))
|
||||
if resp, err := client.Do(req); err == nil {
|
||||
_ = resp.Body.Close()
|
||||
}
|
||||
}
|
||||
if otherHit.Load() != 0 {
|
||||
t.Fatalf("a node's redirect reached another host %d time(s)", otherHit.Load())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +90,8 @@ func (s *NodeService) FetchCertFingerprint(ctx context.Context, n *model.Node) (
|
||||
DialContext: netsafe.SSRFGuardedDialContext,
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // lgtm[go/disabled-certificate-check]
|
||||
},
|
||||
// The pin must be the certificate of the host asked, not of wherever it redirects.
|
||||
CheckRedirect: runtime.NoCrossHostRedirect,
|
||||
}
|
||||
defer client.CloseIdleConnections()
|
||||
resp, err := client.Do(req)
|
||||
|
||||
Reference in New Issue
Block a user