mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-25 13:56:10 +00:00
fix(sub): don't project public inbounds through a fallback master
A standalone inbound bound to a public/wildcard listen that still carried a stale inbound_fallbacks row had its share/subscription link rewritten with the master's port + Reality/TLS settings (keeping only its own transport), producing an unusable link that silently fails - the client connects but no traffic flows. The leak hit every backend link surface: subscription URL, JSON sub, Clash sub, and the panel Client Information link. Gate projectThroughFallbackMaster on reachability: only project a child that is not directly reachable on its own listen (loopback or a unix-domain socket). A public or wildcard inbound advertises its own port + security regardless of any fallback row. Legit loopback/socket fallback children still project as before. Closes #4987
This commit is contained in:
@@ -90,6 +90,22 @@ func isLoopbackHost(host string) bool {
|
|||||||
return ip != nil && ip.IsLoopback()
|
return ip != nil && ip.IsLoopback()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// listenIsInternalOnly reports whether a bind address is reachable only from
|
||||||
|
// the same host — a loopback IP or a unix-domain socket. Such an inbound can't
|
||||||
|
// be dialed directly by a remote client, so when it is the child side of a
|
||||||
|
// fallback its share link must be projected through the master. A public or
|
||||||
|
// wildcard listen (""/0.0.0.0/::) is reachable on its own port and advertises
|
||||||
|
// itself.
|
||||||
|
func listenIsInternalOnly(listen string) bool {
|
||||||
|
if listen == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if listen[0] == '@' || listen[0] == '/' {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return isLoopbackHost(listen)
|
||||||
|
}
|
||||||
|
|
||||||
// GetSubs retrieves subscription links for a given subscription ID and host.
|
// GetSubs retrieves subscription links for a given subscription ID and host.
|
||||||
func (s *SubService) GetSubs(subId string, host string) ([]string, []string, int64, xray.ClientTraffic, error) {
|
func (s *SubService) GetSubs(subId string, host string) ([]string, []string, int64, xray.ClientTraffic, error) {
|
||||||
s.PrepareForRequest(host)
|
s.PrepareForRequest(host)
|
||||||
@@ -260,10 +276,20 @@ func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error)
|
|||||||
// Returns true when a projection happened; sub services call this before
|
// Returns true when a projection happened; sub services call this before
|
||||||
// generating links so a child VLESS-WS bound to 127.0.0.1 emits the
|
// generating links so a child VLESS-WS bound to 127.0.0.1 emits the
|
||||||
// master's :443 + TLS state instead of its own loopback endpoint.
|
// master's :443 + TLS state instead of its own loopback endpoint.
|
||||||
|
//
|
||||||
|
// Projection only applies to a child that is not directly reachable on its
|
||||||
|
// own listen (loopback or a unix-domain socket). An inbound on a public or
|
||||||
|
// wildcard listen is reachable on its own port, so it advertises its own
|
||||||
|
// port + security even when a stale fallback rule still names it as a child —
|
||||||
|
// otherwise its share link would leak the master's port and Reality/TLS
|
||||||
|
// settings (#4987).
|
||||||
func (s *SubService) projectThroughFallbackMaster(inbound *model.Inbound) bool {
|
func (s *SubService) projectThroughFallbackMaster(inbound *model.Inbound) bool {
|
||||||
if inbound == nil {
|
if inbound == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if !listenIsInternalOnly(inbound.Listen) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
var master *model.Inbound
|
var master *model.Inbound
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,25 @@ func TestIsRoutableHost(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestListenIsInternalOnly(t *testing.T) {
|
||||||
|
// Reachable only from the same host -> a fallback child here must be
|
||||||
|
// projected through its master.
|
||||||
|
internalOnly := []string{"127.0.0.1", "127.0.0.2", "::1", "[::1]", "@fallback", "/run/x.sock"}
|
||||||
|
for _, v := range internalOnly {
|
||||||
|
if !listenIsInternalOnly(v) {
|
||||||
|
t.Fatalf("listenIsInternalOnly(%q) = false, want true", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Directly reachable on its own port -> never projected, even if a stale
|
||||||
|
// fallback rule names it as a child (#4987).
|
||||||
|
reachable := []string{"", "0.0.0.0", "::", "::0", "1.2.3.4", "10.0.0.5", "192.168.1.10", "vpn.example.com"}
|
||||||
|
for _, v := range reachable {
|
||||||
|
if listenIsInternalOnly(v) {
|
||||||
|
t.Fatalf("listenIsInternalOnly(%q) = true, want false", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestResolveInboundAddress(t *testing.T) {
|
func TestResolveInboundAddress(t *testing.T) {
|
||||||
const reqHost = "sub.example.com"
|
const reqHost = "sub.example.com"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user