host_sub_test.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  1. package sub
  2. import (
  3. "fmt"
  4. "net/url"
  5. "path/filepath"
  6. "strings"
  7. "testing"
  8. "github.com/mhsanaei/3x-ui/v3/internal/database"
  9. "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest"
  10. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  11. )
  12. func seedSubDB(t *testing.T) {
  13. t.Helper()
  14. dbDir := t.TempDir()
  15. t.Setenv("XUI_DB_FOLDER", dbDir)
  16. dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db"))
  17. }
  18. // seedSubInbound creates a VLESS inbound with one client wired into the
  19. // normalized clients/client_inbounds tables so getInboundsBySubId resolves it.
  20. func seedSubInbound(t *testing.T, subId, tag string, port, subSortIndex int, stream string) *model.Inbound {
  21. t.Helper()
  22. db := database.GetDB()
  23. uuid := "11111111-2222-4333-8444-" + fmt.Sprintf("%012d", port)
  24. email := tag + "@e"
  25. settings := fmt.Sprintf(`{"clients":[{"id":%q,"email":%q,"subId":%q,"enable":true}],"decryption":"none"}`, uuid, email, subId)
  26. ib := &model.Inbound{
  27. UserId: 1, Tag: tag, Enable: true, Listen: "203.0.113.5", Port: port,
  28. Protocol: model.VLESS, Remark: tag, Settings: settings, StreamSettings: stream,
  29. SubSortIndex: subSortIndex,
  30. }
  31. if err := db.Create(ib).Error; err != nil {
  32. t.Fatalf("seed inbound %s: %v", tag, err)
  33. }
  34. client := &model.ClientRecord{Email: email, SubID: subId, UUID: uuid, Enable: true}
  35. if err := db.Create(client).Error; err != nil {
  36. t.Fatalf("seed client %s: %v", email, err)
  37. }
  38. if err := db.Create(&model.ClientInbound{ClientId: client.Id, InboundId: ib.Id}).Error; err != nil {
  39. t.Fatalf("seed client_inbound %s: %v", email, err)
  40. }
  41. return ib
  42. }
  43. func seedHost(t *testing.T, h *model.Host) *model.Host {
  44. t.Helper()
  45. if err := database.GetDB().Create(h).Error; err != nil {
  46. t.Fatalf("seed host: %v", err)
  47. }
  48. return h
  49. }
  50. const wsTLSStream = `{"network":"ws","security":"tls","wsSettings":{"path":"/base","host":"base.host"},"tlsSettings":{"serverName":"base.sni"}}`
  51. // #1 — an inbound with no hosts renders identically to the legacy path: a single
  52. // link from the inbound's own address. Mutation-checks the zero-hosts fallback.
  53. func TestSub_ZeroHosts_IdenticalOutput(t *testing.T) {
  54. seedSubDB(t)
  55. seedSubInbound(t, "s1", "z", 4431, 1, `{"network":"tcp","security":"tls","tlsSettings":{"serverName":"base.sni"}}`)
  56. links, _, _, _, err := NewSubService("").GetSubs("s1", "req.example.com")
  57. if err != nil {
  58. t.Fatalf("GetSubs: %v", err)
  59. }
  60. if len(links) != 1 {
  61. t.Fatalf("links = %d, want 1", len(links))
  62. }
  63. if !strings.Contains(links[0], "203.0.113.5:4431") {
  64. t.Fatalf("zero-hosts link should use the inbound address: %s", links[0])
  65. }
  66. if strings.Contains(links[0], "\n") {
  67. t.Fatalf("zero-hosts must be a single link: %s", links[0])
  68. }
  69. }
  70. // #2 — N enabled hosts render N links, ordered by sort_order, each carrying its
  71. // own address/port/sni and host-header/path override.
  72. func TestSub_NHosts_EmitsNLinksOrdered(t *testing.T) {
  73. seedSubDB(t)
  74. ib := seedSubInbound(t, "s1", "n", 4432, 1, wsTLSStream)
  75. seedHost(t, &model.Host{InboundId: ib.Id, SortOrder: 2, Remark: "B", Address: "b.cdn.com", Port: 8443, Security: "tls", Sni: "b.sni", HostHeader: "b.host", Path: "/b"})
  76. seedHost(t, &model.Host{InboundId: ib.Id, SortOrder: 1, Remark: "A", Address: "a.cdn.com", Port: 2096, Security: "tls", Sni: "a.sni", HostHeader: "a.host", Path: "/a"})
  77. links, _, _, _, err := NewSubService("").GetSubs("s1", "req.example.com")
  78. if err != nil {
  79. t.Fatalf("GetSubs: %v", err)
  80. }
  81. parts := strings.Split(strings.Join(links, "\n"), "\n")
  82. if len(parts) != 2 {
  83. t.Fatalf("want 2 host links, got %d: %v", len(parts), parts)
  84. }
  85. if !strings.Contains(parts[0], "a.cdn.com:2096") || !strings.Contains(parts[0], "sni=a.sni") ||
  86. !strings.Contains(parts[0], "host=a.host") || !strings.Contains(parts[0], "path=%2Fa") {
  87. t.Fatalf("host A link (sort_order 1) wrong: %s", parts[0])
  88. }
  89. if !strings.Contains(parts[1], "b.cdn.com:8443") || !strings.Contains(parts[1], "sni=b.sni") ||
  90. !strings.Contains(parts[1], "host=b.host") || !strings.Contains(parts[1], "path=%2Fb") {
  91. t.Fatalf("host B link (sort_order 2) wrong: %s", parts[1])
  92. }
  93. }
  94. // #3 — a disabled host is omitted; the inbound falls back to its legacy link.
  95. func TestSub_DisabledHostSkipped(t *testing.T) {
  96. seedSubDB(t)
  97. ib := seedSubInbound(t, "s1", "d", 4433, 1, wsTLSStream)
  98. seedHost(t, &model.Host{InboundId: ib.Id, SortOrder: 1, Remark: "OFF", Address: "off.cdn.com", Port: 8443, IsDisabled: true})
  99. links, _, _, _, err := NewSubService("").GetSubs("s1", "req.example.com")
  100. if err != nil {
  101. t.Fatalf("GetSubs: %v", err)
  102. }
  103. joined := strings.Join(links, "\n")
  104. if strings.Contains(joined, "off.cdn.com") {
  105. t.Fatalf("disabled host must not render: %s", joined)
  106. }
  107. if !strings.Contains(joined, "203.0.113.5:4433") {
  108. t.Fatalf("with only a disabled host, the inbound's own link should render: %s", joined)
  109. }
  110. }
  111. // #4 — when both hosts and a legacy externalProxy are set, hosts win and the
  112. // externalProxy entry is ignored.
  113. func TestSub_HostAndExternalProxy_Precedence(t *testing.T) {
  114. seedSubDB(t)
  115. stream := `{"network":"ws","security":"tls","wsSettings":{"path":"/base","host":"base.host"},"tlsSettings":{"serverName":"base.sni"},"externalProxy":[{"forceTls":"tls","dest":"legacy.cdn.com","port":7443,"remark":"L"}]}`
  116. ib := seedSubInbound(t, "s1", "p", 4434, 1, stream)
  117. seedHost(t, &model.Host{InboundId: ib.Id, SortOrder: 1, Remark: "H", Address: "host.cdn.com", Port: 8443, Security: "tls", Sni: "host.sni"})
  118. links, _, _, _, err := NewSubService("").GetSubs("s1", "req.example.com")
  119. if err != nil {
  120. t.Fatalf("GetSubs: %v", err)
  121. }
  122. joined := strings.Join(links, "\n")
  123. if !strings.Contains(joined, "host.cdn.com:8443") {
  124. t.Fatalf("host should win: %s", joined)
  125. }
  126. if strings.Contains(joined, "legacy.cdn.com") {
  127. t.Fatalf("externalProxy must be ignored when hosts exist: %s", joined)
  128. }
  129. }
  130. // #5 — hosts that share a remark but differ in address/port are NOT deduped:
  131. // distinct hosts produce distinct links. Mutation-checks the (absent) dedup.
  132. func TestSub_NHosts_NoDedup(t *testing.T) {
  133. seedSubDB(t)
  134. ib := seedSubInbound(t, "s1", "dd", 4435, 1, wsTLSStream)
  135. seedHost(t, &model.Host{InboundId: ib.Id, SortOrder: 1, Remark: "SAME", Address: "one.cdn.com", Port: 8443, Security: "tls"})
  136. seedHost(t, &model.Host{InboundId: ib.Id, SortOrder: 2, Remark: "SAME", Address: "two.cdn.com", Port: 8443, Security: "tls"})
  137. links, _, _, _, err := NewSubService("").GetSubs("s1", "req.example.com")
  138. if err != nil {
  139. t.Fatalf("GetSubs: %v", err)
  140. }
  141. joined := strings.Join(links, "\n")
  142. parts := strings.Split(joined, "\n")
  143. if len(parts) != 2 {
  144. t.Fatalf("two distinct hosts must yield two links, got %d: %v", len(parts), parts)
  145. }
  146. if !strings.Contains(joined, "one.cdn.com") || !strings.Contains(joined, "two.cdn.com") {
  147. t.Fatalf("both distinct host addresses must appear: %s", joined)
  148. }
  149. }
  150. // #6 — host sort_order composes with inbound SubSortIndex: inbounds order by
  151. // SubSortIndex, hosts within an inbound by sort_order.
  152. func TestSub_HostSortComposesWithSubSortIndex(t *testing.T) {
  153. seedSubDB(t)
  154. // inbound "second" has a higher SubSortIndex so it must come after "first".
  155. ibFirst := seedSubInbound(t, "s1", "first", 4436, 1, wsTLSStream)
  156. ibSecond := seedSubInbound(t, "s1", "second", 4437, 2, wsTLSStream)
  157. seedHost(t, &model.Host{InboundId: ibSecond.Id, SortOrder: 1, Remark: "S", Address: "second-host.com", Port: 8443, Security: "tls"})
  158. seedHost(t, &model.Host{InboundId: ibFirst.Id, SortOrder: 1, Remark: "F", Address: "first-host.com", Port: 8443, Security: "tls"})
  159. links, _, _, _, err := NewSubService("").GetSubs("s1", "req.example.com")
  160. if err != nil {
  161. t.Fatalf("GetSubs: %v", err)
  162. }
  163. joined := strings.Join(links, "\n")
  164. firstAt := strings.Index(joined, "first-host.com")
  165. secondAt := strings.Index(joined, "second-host.com")
  166. if firstAt < 0 || secondAt < 0 {
  167. t.Fatalf("both inbound hosts should render: %s", joined)
  168. }
  169. if firstAt > secondAt {
  170. t.Fatalf("inbound order must follow SubSortIndex (first before second): %s", joined)
  171. }
  172. }
  173. // #7 — host overrides apply AFTER projectThroughFallbackMaster: the host's
  174. // address/sni win over the projected master stream.
  175. func TestSub_HostOverFallback(t *testing.T) {
  176. seedSubDB(t)
  177. db := database.GetDB()
  178. master := &model.Inbound{
  179. UserId: 1, Tag: "master", Enable: true, Listen: "203.0.113.9", Port: 9443,
  180. Protocol: model.VLESS, Remark: "master",
  181. Settings: `{"clients":[],"decryption":"none"}`,
  182. StreamSettings: `{"network":"tcp","security":"tls","tlsSettings":{"serverName":"master.sni"}}`,
  183. }
  184. if err := db.Create(master).Error; err != nil {
  185. t.Fatalf("seed master: %v", err)
  186. }
  187. // child listens internal-only so projection triggers.
  188. child := seedSubInbound(t, "s1", "child", 4438, 1, `{"network":"tcp","security":"none"}`)
  189. child.Listen = "127.0.0.1"
  190. if err := db.Model(&model.Inbound{}).Where("id = ?", child.Id).Update("listen", "127.0.0.1").Error; err != nil {
  191. t.Fatalf("set child listen: %v", err)
  192. }
  193. if err := db.Create(&model.InboundFallback{MasterId: master.Id, ChildId: child.Id}).Error; err != nil {
  194. t.Fatalf("seed fallback: %v", err)
  195. }
  196. seedHost(t, &model.Host{InboundId: child.Id, SortOrder: 1, Remark: "H", Address: "host.cdn.com", Port: 8443, Security: "tls", Sni: "host.sni"})
  197. links, _, _, _, err := NewSubService("").GetSubs("s1", "req.example.com")
  198. if err != nil {
  199. t.Fatalf("GetSubs: %v", err)
  200. }
  201. joined := strings.Join(links, "\n")
  202. if !strings.Contains(joined, "host.cdn.com:8443") || !strings.Contains(joined, "sni=host.sni") {
  203. t.Fatalf("host override must win over fallback master: %s", joined)
  204. }
  205. if strings.Contains(joined, "203.0.113.9") || strings.Contains(joined, "sni=master.sni") {
  206. t.Fatalf("master endpoint/sni must be overridden by the host: %s", joined)
  207. }
  208. }
  209. // #8 — a client only gets hosts for inbounds it is actually on (the
  210. // clients ⋈ client_inbounds ⋈ inbounds join), never arbitrary inbounds.
  211. func TestSub_HostsResolveViaClientInbounds(t *testing.T) {
  212. seedSubDB(t)
  213. seedSubInbound(t, "s1", "mine", 4439, 1, wsTLSStream) // client on s1
  214. other := seedSubInbound(t, "s2", "other", 4440, 1, wsTLSStream) // client on s2 only
  215. seedHost(t, &model.Host{InboundId: other.Id, SortOrder: 1, Remark: "X", Address: "other-host.com", Port: 8443, Security: "tls"})
  216. links, _, _, _, err := NewSubService("").GetSubs("s1", "req.example.com")
  217. if err != nil {
  218. t.Fatalf("GetSubs: %v", err)
  219. }
  220. joined := strings.Join(links, "\n")
  221. if strings.Contains(joined, "other-host.com") {
  222. t.Fatalf("host on an inbound the client is not on must not appear: %s", joined)
  223. }
  224. }
  225. // allowInsecure renders as allowInsecure=1 in the raw link and
  226. // skip-cert-verify: true in the Clash proxy.
  227. func TestSub_HostAllowInsecure(t *testing.T) {
  228. seedSubDB(t)
  229. ib := seedSubInbound(t, "s1", "ai", 4450, 1, wsTLSStream)
  230. seedHost(t, &model.Host{InboundId: ib.Id, SortOrder: 0, Remark: "AI", Address: "ai.cdn.com", Port: 8443, Security: "tls", AllowInsecure: true})
  231. links, _, _, _, err := NewSubService("").GetSubs("s1", "req.example.com")
  232. if err != nil {
  233. t.Fatalf("GetSubs: %v", err)
  234. }
  235. if !strings.Contains(strings.Join(links, "\n"), "allowInsecure=1") {
  236. t.Fatalf("raw link should carry allowInsecure=1: %s", strings.Join(links, "\n"))
  237. }
  238. clash := NewSubClashService(false, "", NewSubService(""))
  239. yaml, _, err := clash.GetClash("s1", "req.example.com")
  240. if err != nil {
  241. t.Fatalf("GetClash: %v", err)
  242. }
  243. if !strings.Contains(yaml, "skip-cert-verify: true") {
  244. t.Fatalf("clash proxy should carry skip-cert-verify: true:\n%s", yaml)
  245. }
  246. }
  247. // A host's Host header and path reach the Clash and JSON renderers even when
  248. // the inbound's own ws settings leave them empty (#5944).
  249. func TestSub_HostHeaderReachesClashAndJson(t *testing.T) {
  250. seedSubDB(t)
  251. ib := seedSubInbound(t, "s1", "hh", 4457, 1,
  252. `{"network":"ws","security":"tls","wsSettings":{"path":"/"},"tlsSettings":{"serverName":"base.sni"}}`)
  253. seedHost(t, &model.Host{
  254. InboundId: ib.Id, SortOrder: 0, Remark: "HH", Address: "hh.cdn.com", Port: 8443, Security: "tls",
  255. HostHeader: "cdn.example.com", Path: "/ws-path",
  256. })
  257. clash := NewSubClashService(false, "", NewSubService(""))
  258. yaml, _, err := clash.GetClash("s1", "req.example.com")
  259. if err != nil {
  260. t.Fatalf("GetClash: %v", err)
  261. }
  262. if !strings.Contains(yaml, "Host: cdn.example.com") {
  263. t.Fatalf("clash ws-opts should carry the host record's Host header:\n%s", yaml)
  264. }
  265. if !strings.Contains(yaml, "path: /ws-path") {
  266. t.Fatalf("clash ws-opts should carry the host record's path:\n%s", yaml)
  267. }
  268. js := NewSubJsonService("", "", "", "", NewSubService(""))
  269. out, _, err := js.GetJson("s1", "req.example.com", false)
  270. if err != nil {
  271. t.Fatalf("GetJson: %v", err)
  272. }
  273. if !strings.Contains(out, `"host": "cdn.example.com"`) && !strings.Contains(out, `"host":"cdn.example.com"`) {
  274. t.Fatalf("json wsSettings should carry the host record's Host header:\n%s", out)
  275. }
  276. }
  277. // A host's Final Mask reaches the raw share link as the fm param, merged with
  278. // any inbound-level mask (#5831).
  279. func TestSub_HostFinalMask_RawLink(t *testing.T) {
  280. seedSubDB(t)
  281. ib := seedSubInbound(t, "s1", "fmh", 4455, 1,
  282. `{"network":"tcp","security":"tls","tlsSettings":{"serverName":"base.sni"},"finalmask":{"tcp":[{"type":"sudoku"}]}}`)
  283. seedHost(t, &model.Host{
  284. InboundId: ib.Id, SortOrder: 0, Remark: "FM", Address: "fm.cdn.com", Port: 8443, Security: "tls",
  285. FinalMask: `{"tcp":[{"type":"fragment"}]}`,
  286. })
  287. links, _, _, _, err := NewSubService("").GetSubs("s1", "req.example.com")
  288. if err != nil {
  289. t.Fatalf("GetSubs: %v", err)
  290. }
  291. joined := strings.Join(links, "\n")
  292. wantFm := "fm=" + url.QueryEscape(`{"tcp":[{"type":"sudoku"},{"type":"fragment"}]}`)
  293. if !strings.Contains(joined, wantFm) {
  294. t.Fatalf("raw link should merge the host Final Mask into fm.\n got: %s\nwant substring: %s", joined, wantFm)
  295. }
  296. }
  297. // A host's sockoptParams is injected into the JSON output stream (sockopt is
  298. // stripped from the base stream, re-added per host).
  299. func TestSub_HostSockoptJSON(t *testing.T) {
  300. seedSubDB(t)
  301. ib := seedSubInbound(t, "s1", "so", 4460, 1,
  302. `{"network":"xhttp","security":"tls","xhttpSettings":{"path":"/x","mode":"auto"},"tlsSettings":{"serverName":"base.sni"}}`)
  303. seedHost(t, &model.Host{
  304. InboundId: ib.Id, SortOrder: 0, Remark: "SO", Address: "so.cdn.com", Port: 8443, Security: "tls",
  305. SockoptParams: `{"tcpFastOpen":true}`,
  306. })
  307. js := NewSubJsonService("", "", "", "", NewSubService(""))
  308. out, _, err := js.GetJson("s1", "req.example.com", false)
  309. if err != nil {
  310. t.Fatalf("GetJson: %v", err)
  311. }
  312. if !strings.Contains(out, "sockopt") || !strings.Contains(out, "tcpFastOpen") {
  313. t.Fatalf("json should include the host sockopt:\n%s", out)
  314. }
  315. }
  316. // A host's muxParams override the JSON outbound's mux.
  317. func TestSub_HostMuxJSON(t *testing.T) {
  318. seedSubDB(t)
  319. ib := seedSubInbound(t, "s1", "mx", 4470, 1, wsTLSStream)
  320. seedHost(t, &model.Host{
  321. InboundId: ib.Id, SortOrder: 0, Remark: "MX", Address: "mx.cdn.com", Port: 8443, Security: "tls",
  322. MuxParams: `{"enabled":true,"concurrency":8}`,
  323. })
  324. js := NewSubJsonService("", "", "", "", NewSubService(""))
  325. out, _, err := js.GetJson("s1", "req.example.com", false)
  326. if err != nil {
  327. t.Fatalf("GetJson: %v", err)
  328. }
  329. if !strings.Contains(out, "concurrency") {
  330. t.Fatalf("json should include the host mux override:\n%s", out)
  331. }
  332. }
  333. // A reality host overrides SNI + fingerprint while inheriting pbk/sid from the
  334. // inbound (reality keys can't be host-supplied).
  335. func TestSub_HostRealitySniOverride(t *testing.T) {
  336. seedSubDB(t)
  337. realityStream := `{"network":"tcp","security":"reality","tcpSettings":{"header":{"type":"none"}},"realitySettings":{"serverNames":["base.reality.com"],"shortIds":["abcd"],"settings":{"publicKey":"PBK","fingerprint":"chrome"}}}`
  338. ib := seedSubInbound(t, "s1", "rl", 4490, 1, realityStream)
  339. seedHost(t, &model.Host{
  340. InboundId: ib.Id, SortOrder: 0, Remark: "RL", Address: "rl.cdn.com", Port: 8443,
  341. Security: "reality", Sni: "host.reality.com", Fingerprint: "firefox",
  342. })
  343. links, _, _, _, err := NewSubService("").GetSubs("s1", "req.example.com")
  344. if err != nil {
  345. t.Fatalf("GetSubs: %v", err)
  346. }
  347. joined := strings.Join(links, "\n")
  348. if !strings.Contains(joined, "rl.cdn.com:8443") || !strings.Contains(joined, "security=reality") {
  349. t.Fatalf("reality host base wrong: %s", joined)
  350. }
  351. if !strings.Contains(joined, "sni=host.reality.com") || !strings.Contains(joined, "fp=firefox") {
  352. t.Fatalf("reality host sni/fp override not applied: %s", joined)
  353. }
  354. if strings.Contains(joined, "sni=base.reality.com") {
  355. t.Fatalf("base reality sni must be overridden: %s", joined)
  356. }
  357. if !strings.Contains(joined, "pbk=PBK") || !strings.Contains(joined, "sid=abcd") {
  358. t.Fatalf("reality pbk/sid must be inherited from the inbound: %s", joined)
  359. }
  360. }
  361. // #9 — ExcludeFromSubTypes is honored per format: a host excluded from clash is
  362. // absent from GetClash but present in the raw GetSubs output.
  363. func TestSub_ExcludeFromSubTypes(t *testing.T) {
  364. seedSubDB(t)
  365. ib := seedSubInbound(t, "s1", "x", 4441, 1, wsTLSStream)
  366. seedHost(t, &model.Host{InboundId: ib.Id, SortOrder: 1, Remark: "H", Address: "clashless.cdn.com", Port: 8443, Security: "tls", ExcludeFromSubTypes: []string{"clash"}})
  367. sub := NewSubService("")
  368. links, _, _, _, err := sub.GetSubs("s1", "req.example.com")
  369. if err != nil {
  370. t.Fatalf("GetSubs: %v", err)
  371. }
  372. if !strings.Contains(strings.Join(links, "\n"), "clashless.cdn.com") {
  373. t.Fatalf("host not excluded from raw should appear in GetSubs")
  374. }
  375. clash := NewSubClashService(false, "", NewSubService(""))
  376. yaml, _, err := clash.GetClash("s1", "req.example.com")
  377. if err != nil {
  378. t.Fatalf("GetClash: %v", err)
  379. }
  380. if strings.Contains(yaml, "clashless.cdn.com") {
  381. t.Fatalf("host excluded from clash must not appear in GetClash:\n%s", yaml)
  382. }
  383. }
  384. // A host that forces plain TLS over a Reality inbound must not leave the
  385. // Reality identity behind: pbk/sid/spx and the Reality dest sni describe a
  386. // handshake the endpoint no longer performs.
  387. func TestSub_HostTlsOverRealityDropsRealityParams(t *testing.T) {
  388. seedSubDB(t)
  389. reality := `{"network":"tcp","security":"reality","realitySettings":{"serverNames":["master-dest.example.com"],"publicKey":"MASTERPBK","shortIds":["ab12"],"fingerprint":"chrome"}}`
  390. ib := seedSubInbound(t, "s1", "reality-in", 4461, 1, reality)
  391. seedHost(t, &model.Host{InboundId: ib.Id, SortOrder: 1, Remark: "H", Address: "edge.example.com", Port: 443, Security: "tls"})
  392. links, _, _, _, err := NewSubService("").GetSubs("s1", "req.example.com")
  393. if err != nil {
  394. t.Fatalf("GetSubs: %v", err)
  395. }
  396. joined := strings.Join(links, "\n")
  397. if !strings.Contains(joined, "security=tls") {
  398. t.Fatalf("host forces tls, link must say so: %s", joined)
  399. }
  400. for _, leaked := range []string{"pbk=", "sid=", "spx=", "sni=master-dest.example.com"} {
  401. if strings.Contains(joined, leaked) {
  402. t.Fatalf("reality parameter %q survived a tls host override: %s", leaked, joined)
  403. }
  404. }
  405. }
  406. // A host's cipher suites override the inbound's own in the JSON subscription,
  407. // while a host that leaves the field blank inherits them.
  408. func TestSub_HostCipherSuitesJSON(t *testing.T) {
  409. seedSubDB(t)
  410. ib := seedSubInbound(t, "s1", "cs", 4462, 1,
  411. `{"network":"tcp","security":"tls","tlsSettings":{"serverName":"base.sni","cipherSuites":"TLS_CHACHA20_POLY1305_SHA256"}}`)
  412. seedHost(t, &model.Host{
  413. InboundId: ib.Id, SortOrder: 0, Remark: "CS", Address: "cs.cdn.com", Port: 8443, Security: "tls",
  414. CipherSuites: "TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256",
  415. })
  416. seedHost(t, &model.Host{
  417. InboundId: ib.Id, SortOrder: 1, Remark: "INHERIT", Address: "inh.cdn.com", Port: 8443, Security: "tls",
  418. })
  419. out, _, err := NewSubJsonService("", "", "", "", NewSubService("")).GetJson("s1", "req.example.com", false)
  420. if err != nil {
  421. t.Fatalf("GetJson: %v", err)
  422. }
  423. if !strings.Contains(out, `"cipherSuites": "TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256"`) &&
  424. !strings.Contains(out, `"cipherSuites":"TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256"`) {
  425. t.Fatalf("json tlsSettings should carry the host's cipher suites:\n%s", out)
  426. }
  427. if !strings.Contains(out, `"cipherSuites": "TLS_CHACHA20_POLY1305_SHA256"`) &&
  428. !strings.Contains(out, `"cipherSuites":"TLS_CHACHA20_POLY1305_SHA256"`) {
  429. t.Fatalf("a host with no cipher suites should inherit the inbound's:\n%s", out)
  430. }
  431. }