host_sub.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381
  1. package sub
  2. import (
  3. "encoding/json"
  4. "maps"
  5. "slices"
  6. "github.com/mhsanaei/3x-ui/v3/internal/database"
  7. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  8. "github.com/mhsanaei/3x-ui/v3/internal/logger"
  9. )
  10. // hostEndpoints loads an inbound's enabled hosts for the given subscription
  11. // format ("raw"|"json"|"clash") and returns them as externalProxy-shaped maps so
  12. // the existing per-format renderers can fan out one link/proxy per host. Returns
  13. // nil when the inbound has no applicable host — the caller then uses the legacy
  14. // inbound/externalProxy path, preserving byte-identical output for zero-host
  15. // inbounds.
  16. func (s *SubService) hostEndpoints(inbound *model.Inbound, format string) []map[string]any {
  17. var hosts []*model.Host
  18. if err := database.GetDB().
  19. Where("inbound_id = ? AND is_disabled = ?", inbound.Id, false).
  20. Order("sort_order asc, id asc").
  21. Find(&hosts).Error; err != nil {
  22. logger.Warning("SubService - hostEndpoints:", err)
  23. return nil
  24. }
  25. if len(hosts) == 0 {
  26. return nil
  27. }
  28. defaultDest := s.resolveInboundAddress(inbound)
  29. eps := make([]map[string]any, 0, len(hosts))
  30. for _, h := range hosts {
  31. if slices.Contains(h.ExcludeFromSubTypes, format) {
  32. continue
  33. }
  34. eps = append(eps, hostToExternalProxyMap(h, defaultDest, inbound.Port))
  35. }
  36. return eps
  37. }
  38. // hostToExternalProxyMap projects a Host onto the externalProxy entry shape the
  39. // raw/json/clash renderers already consume. Address/port fall back to the
  40. // inbound's own when the host leaves them blank (override-only host).
  41. func hostToExternalProxyMap(h *model.Host, defaultDest string, defaultPort int) map[string]any {
  42. dest := h.Address
  43. if dest == "" {
  44. dest = defaultDest
  45. }
  46. port := h.Port
  47. if port == 0 {
  48. port = defaultPort
  49. }
  50. ep := map[string]any{
  51. "forceTls": hostSecurityToForceTls(h.Security),
  52. "dest": dest,
  53. "port": float64(port),
  54. "remark": h.Remark,
  55. // Marks this as a host (not a legacy externalProxy) entry so host-only
  56. // behaviors (e.g. reality SNI/fp override) apply without touching the
  57. // legacy externalProxy path. Not emitted into output.
  58. "isHost": true,
  59. }
  60. sni := h.Sni
  61. if h.OverrideSniFromAddress {
  62. sni = dest
  63. }
  64. if !h.KeepSniBlank && sni != "" {
  65. ep["sni"] = sni
  66. }
  67. if h.Fingerprint != "" {
  68. ep["fingerprint"] = h.Fingerprint
  69. }
  70. if h.CipherSuites != "" {
  71. ep["cipherSuites"] = h.CipherSuites
  72. }
  73. if len(h.Alpn) > 0 {
  74. ep["alpn"] = stringsToAnySlice(h.Alpn)
  75. }
  76. if len(h.PinnedPeerCertSha256) > 0 {
  77. ep["pinnedPeerCertSha256"] = stringsToAnySlice(h.PinnedPeerCertSha256)
  78. }
  79. if h.EchConfigList != "" {
  80. ep["echConfigList"] = h.EchConfigList
  81. }
  82. if h.VerifyPeerCertByName != "" {
  83. ep["verifyPeerCertByName"] = h.VerifyPeerCertByName
  84. }
  85. if h.AllowInsecure {
  86. ep["allowInsecure"] = true
  87. }
  88. if h.HostHeader != "" {
  89. ep["hostHeader"] = h.HostHeader
  90. }
  91. if h.Path != "" {
  92. ep["path"] = h.Path
  93. }
  94. if h.MihomoIpVersion != "" {
  95. ep["mihomoIpVersion"] = h.MihomoIpVersion
  96. }
  97. if h.SockoptParams != "" {
  98. ep["sockoptParams"] = h.SockoptParams
  99. }
  100. if h.MuxParams != "" {
  101. ep["muxParams"] = h.MuxParams
  102. }
  103. if h.FinalMask != "" {
  104. ep["finalMask"] = h.FinalMask
  105. }
  106. if h.VlessRoute != "" {
  107. ep["vlessRoute"] = h.VlessRoute
  108. }
  109. if h.ServerDescription != "" {
  110. ep["serverDescription"] = h.ServerDescription
  111. }
  112. return ep
  113. }
  114. // hostMuxOverride returns a host's muxParams when it is valid JSON, else "".
  115. // Used to override the JSON outbound's mux for that host.
  116. func hostMuxOverride(ep map[string]any) string {
  117. mp, ok := ep["muxParams"].(string)
  118. if ok && mp != "" && json.Valid([]byte(mp)) {
  119. return mp
  120. }
  121. return ""
  122. }
  123. // applyHostStreamOverrides injects a host's free-JSON stream overrides into the
  124. // per-host stream the JSON/Clash renderers build: sockoptParams (re-added since
  125. // the base stream strips sockopt) and finalMask. No-op for legacy externalProxy
  126. // entries (which never carry these keys), so existing output is unchanged.
  127. func applyHostStreamOverrides(ep map[string]any, stream map[string]any) {
  128. if hh, ok := ep["hostHeader"].(string); ok && hh != "" {
  129. for _, key := range []string{"wsSettings", "httpupgradeSettings", "xhttpSettings"} {
  130. if ts, ok := stream[key].(map[string]any); ok && ts != nil {
  131. ts["host"] = hh
  132. }
  133. }
  134. }
  135. if p, ok := ep["path"].(string); ok && p != "" {
  136. for _, key := range []string{"wsSettings", "httpupgradeSettings", "xhttpSettings"} {
  137. if ts, ok := stream[key].(map[string]any); ok && ts != nil {
  138. ts["path"] = p
  139. }
  140. }
  141. }
  142. if sp, ok := ep["sockoptParams"].(string); ok && sp != "" {
  143. var sockopt map[string]any
  144. if json.Unmarshal([]byte(sp), &sockopt) == nil && len(sockopt) > 0 {
  145. stream["sockopt"] = sockopt
  146. }
  147. }
  148. // Host finalmask: merge the host's masks into the stream's finalmask (the
  149. // JSON renderer consumes streamSettings["finalmask"]; clash ignores it).
  150. if fm, ok := ep["finalMask"].(string); ok && fm != "" {
  151. var masks map[string]any
  152. if json.Unmarshal([]byte(fm), &masks) == nil && len(masks) > 0 {
  153. merged := mergeFinalMask(stream["finalmask"], masks)
  154. if len(merged) > 0 {
  155. stream["finalmask"] = merged
  156. }
  157. }
  158. }
  159. // Reality SNI override (host only): JSON realityData reads serverNames and
  160. // clash reads serverName, so set both forms.
  161. if isHostEndpoint(ep) {
  162. if sec, _ := stream["security"].(string); sec == "reality" {
  163. if rs, ok := stream["realitySettings"].(map[string]any); ok && rs != nil {
  164. if sni, ok := externalProxySNI(ep); ok {
  165. rs["serverName"] = sni
  166. rs["serverNames"] = []any{sni}
  167. }
  168. }
  169. }
  170. }
  171. }
  172. // hostSecurityToForceTls maps Host.Security onto the externalProxy forceTls
  173. // vocabulary. "reality"/"same"/"" all keep the inbound's base security ("same")
  174. // — reality parameters can only come from the inbound itself.
  175. func hostSecurityToForceTls(security string) string {
  176. switch security {
  177. case "tls", "none":
  178. return security
  179. default:
  180. return "same"
  181. }
  182. }
  183. func stringsToAnySlice(in []string) []any {
  184. out := make([]any, 0, len(in))
  185. for _, s := range in {
  186. if s != "" {
  187. out = append(out, s)
  188. }
  189. }
  190. return out
  191. }
  192. // injectExternalProxy rewrites the inbound's StreamSettings so its externalProxy
  193. // array is exactly eps. Host endpoints win over any legacy externalProxy.
  194. func injectExternalProxy(inbound *model.Inbound, eps []map[string]any) {
  195. stream := unmarshalStreamSettings(inbound.StreamSettings)
  196. if stream == nil {
  197. stream = map[string]any{}
  198. }
  199. arr := make([]any, len(eps))
  200. for i := range eps {
  201. arr[i] = eps[i]
  202. }
  203. stream["externalProxy"] = arr
  204. if b, err := json.Marshal(stream); err == nil {
  205. inbound.StreamSettings = string(b)
  206. }
  207. }
  208. // linkFromHosts renders a (possibly multi-line) raw link for one client using
  209. // the given host endpoints. It renders ONLY the hosts: an empty eps yields ""
  210. // (no legacy fallback) — the caller decides when to take the legacy path. That
  211. // separation is what makes the zero-hosts fallback mutation-testable.
  212. func (s *SubService) linkFromHosts(inbound *model.Inbound, client model.Client, eps []map[string]any) string {
  213. if len(eps) == 0 {
  214. return ""
  215. }
  216. stream := unmarshalStreamSettings(inbound.StreamSettings)
  217. transport, _ := stream["network"].(string)
  218. // Clone each ep before expanding its remark template: the eps slice is
  219. // shared across all clients of this inbound, so the rendered (per-client)
  220. // remark must not leak into the next client's links.
  221. rendered := make([]map[string]any, len(eps))
  222. for i, ep := range eps {
  223. cp := maps.Clone(ep)
  224. s.renderHostRemark(inbound, client, cp, transport)
  225. rendered[i] = cp
  226. }
  227. clone := *inbound
  228. injectExternalProxy(&clone, rendered)
  229. return s.GetLink(&clone, client.Email)
  230. }
  231. // renderHostRemark expands a host endpoint's {{VAR}} remark template for one
  232. // client in place and marks it final, so the downstream link/proxy/config
  233. // renderers emit it verbatim (via endpointRemark) instead of re-composing it.
  234. // No-op for non-host endpoints (legacy externalProxy / synthetic default), so
  235. // their output stays byte-identical.
  236. func (s *SubService) renderHostRemark(inbound *model.Inbound, client model.Client, ep map[string]any, transport string) {
  237. if !isHostEndpoint(ep) {
  238. return
  239. }
  240. tmpl, _ := ep["remark"].(string)
  241. ep["remark"] = s.genHostRemark(inbound, client, tmpl, transport)
  242. ep["remarkFinal"] = true
  243. }
  244. // endpointRemark returns the remark to stamp on an endpoint's link/proxy/config
  245. // entry. A host endpoint whose template was pre-expanded by renderHostRemark
  246. // carries remarkFinal and is used verbatim; every other entry flows through the
  247. // standard genRemark composition unchanged.
  248. func (s *SubService) endpointRemark(inbound *model.Inbound, email string, ep map[string]any, transport string) string {
  249. if ep != nil {
  250. if final, _ := ep["remarkFinal"].(bool); final {
  251. r, _ := ep["remark"].(string)
  252. return r
  253. }
  254. }
  255. var extra string
  256. if ep != nil {
  257. extra, _ = ep["remark"].(string)
  258. }
  259. return s.genRemark(inbound, email, extra, transport)
  260. }
  261. // applyEndpointHostPath overrides the transport host header / path for a host
  262. // endpoint. It is a no-op for legacy externalProxy entries (which never carry
  263. // hostHeader/path) and only replaces keys the transport already emits, so it
  264. // cannot add spurious params to e.g. a tcp link.
  265. func applyEndpointHostPath(e ShareEndpoint, params map[string]string) {
  266. if e.ep == nil {
  267. return
  268. }
  269. if h, ok := e.ep["hostHeader"].(string); ok && h != "" {
  270. if _, exists := params["host"]; exists {
  271. params["host"] = h
  272. }
  273. }
  274. if p, ok := e.ep["path"].(string); ok && p != "" {
  275. if _, exists := params["path"]; exists {
  276. params["path"] = p
  277. }
  278. }
  279. }
  280. // isHostEndpoint reports whether ep was synthesized from a Host (vs a legacy
  281. // externalProxy entry), so host-only overrides stay off the legacy path.
  282. func isHostEndpoint(ep map[string]any) bool {
  283. v, _ := ep["isHost"].(bool)
  284. return v
  285. }
  286. // applyEndpointRealityParams overrides a reality link's SNI + fingerprint from a
  287. // host (reality's pbk/sid are inherited from the inbound, so they aren't touched).
  288. // Host-only: legacy externalProxy reality links are unchanged.
  289. func applyEndpointRealityParams(e ShareEndpoint, params map[string]string, security string) {
  290. if security != "reality" || e.ep == nil || !isHostEndpoint(e.ep) {
  291. return
  292. }
  293. if sni, ok := externalProxySNI(e.ep); ok {
  294. params["sni"] = sni
  295. }
  296. if fp, ok := e.ep["fingerprint"].(string); ok && fp != "" {
  297. params["fp"] = fp
  298. }
  299. }
  300. // applyEndpointAllowInsecure adds allowInsecure=1 to a TLS/Reality link when the
  301. // host opts into skipping cert verification. No-op for legacy externalProxy
  302. // entries (which never carry the key) and for plaintext (none) endpoints.
  303. func applyEndpointAllowInsecure(e ShareEndpoint, params map[string]string, security string) {
  304. if e.ep == nil || security == "none" {
  305. return
  306. }
  307. if ai, ok := e.ep["allowInsecure"].(bool); ok && ai {
  308. params["allowInsecure"] = "1"
  309. }
  310. }
  311. // applyEndpointFinalMask merges a host's Final Mask into the raw link's fm
  312. // param, mirroring the applyHostStreamOverrides merge on the JSON/Clash path.
  313. func applyEndpointFinalMask(e ShareEndpoint, params map[string]string) {
  314. if merged, ok := endpointFinalMask(e, params["fm"]); ok {
  315. params["fm"] = merged
  316. }
  317. }
  318. // applyEndpointFinalMaskObj is applyEndpointFinalMask for the VMess object form.
  319. func applyEndpointFinalMaskObj(e ShareEndpoint, obj map[string]any) {
  320. baseFm, _ := obj["fm"].(string)
  321. if merged, ok := endpointFinalMask(e, baseFm); ok {
  322. obj["fm"] = merged
  323. }
  324. }
  325. func endpointFinalMask(e ShareEndpoint, baseFm string) (string, bool) {
  326. if e.ep == nil {
  327. return "", false
  328. }
  329. fm, ok := e.ep["finalMask"].(string)
  330. if !ok || fm == "" {
  331. return "", false
  332. }
  333. var masks map[string]any
  334. if json.Unmarshal([]byte(fm), &masks) != nil || len(masks) == 0 {
  335. return "", false
  336. }
  337. var base any
  338. if baseFm != "" {
  339. var baseMap map[string]any
  340. if json.Unmarshal([]byte(baseFm), &baseMap) == nil {
  341. base = baseMap
  342. }
  343. }
  344. return marshalFinalMask(mergeFinalMask(base, masks))
  345. }
  346. // applyEndpointHostPathObj is applyEndpointHostPath for the VMess object form.
  347. func applyEndpointHostPathObj(e ShareEndpoint, obj map[string]any) {
  348. if e.ep == nil {
  349. return
  350. }
  351. if h, ok := e.ep["hostHeader"].(string); ok && h != "" {
  352. if _, exists := obj["host"]; exists {
  353. obj["host"] = h
  354. }
  355. }
  356. if p, ok := e.ep["path"].(string); ok && p != "" {
  357. if _, exists := obj["path"]; exists {
  358. obj["path"] = p
  359. }
  360. }
  361. }