feat(inbound): excludeFromSub hides links without disabling (#6463)

* feat(inbound): excludeFromSub hides links without disabling

Add a per-inbound flag that omits subscription output while keeping the
inbound enabled for Xray, auth, and traffic accounting. Fixes #6435.

* fix(inbound): excludeFromSub review follow-ups

gofumpt model.go, sync docs OpenAPI, keep excludeFromSub master-authored
on node mirror, and exercise the legacy add-column migration path in tests.

* fix(sub): keep excluded inbounds' clients in the usage header

The excludeFromSub filter sat in getInboundsBySubId's SQL, so an excluded
inbound's clients never reached seenEmails in the raw, Clash or JSON
renderer. A client that lives only on a hidden inbound (one client per
inbound sharing a subId) dropped out of the Subscription-Userinfo usage,
quota and expiry and out of the info-node state, while the inbound kept
serving it and counting its traffic.

The query returns every enabled inbound again; each renderer skips an
excluded inbound's links but still counts its clients, the same rule the
Clash renderer already applies to external links it cannot express.

---------

Co-authored-by: mrchatam <mrchatam@users.noreply.github.com>
Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com>
This commit is contained in:
mrchatam
2026-09-27 13:56:37 +03:30
committed by GitHub
parent 12d51d7195
commit 6f40a75909
36 changed files with 347 additions and 0 deletions
+18
View File
@@ -330,6 +330,18 @@ func (s *SubService) matchingClients(inbound *model.Inbound, subId string) []mod
return out
}
// countHiddenClients adds an excludeFromSub inbound's clients to the usage set:
// the inbound still serves them, so only its links leave the subscription.
func countHiddenClients(clients []model.Client, seenEmails map[string]struct{}) (anyEnabled bool) {
for _, client := range clients {
seenEmails[client.Email] = struct{}{}
if client.Enable {
anyEnabled = true
}
}
return anyEnabled
}
// overlayInboundTunnelIdentity copies per-inbound tunnel fields from settings.
// An unmatched peer is dropped, malformed settings yield nothing, and empty optional secrets replace shared values (#6641).
func (s *SubService) overlayInboundTunnelIdentity(inbound *model.Inbound, clients []model.Client) ([]model.Client, error) {
@@ -473,6 +485,12 @@ func (s *SubService) getSubs(subId string) ([]string, []string, int64, xray.Clie
if len(clients) == 0 {
continue
}
if inbound.ExcludeFromSub {
if countHiddenClients(clients, seenEmails) {
hasEnabledClient = true
}
continue
}
s.projectThroughFallbackMaster(inbound)
// Host overrides apply AFTER fallback projection so a host's
// address/TLS wins over the projected master stream.