clash_service.go 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137
  1. package sub
  2. import (
  3. "errors"
  4. "fmt"
  5. "maps"
  6. "strings"
  7. "github.com/goccy/go-json"
  8. yaml "github.com/goccy/go-yaml"
  9. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  10. wgutil "github.com/mhsanaei/3x-ui/v3/internal/util/wireguard"
  11. )
  12. type SubClashService struct {
  13. enableRouting bool
  14. clashRules string
  15. SubService *SubService
  16. }
  17. func NewSubClashService(enableRouting bool, clashRules string, subService *SubService) *SubClashService {
  18. return &SubClashService{enableRouting: enableRouting, clashRules: clashRules, SubService: subService}
  19. }
  20. func (s *SubClashService) GetClash(subId string, host string) (string, string, error) {
  21. subReq := s.SubService.ForRequest(host)
  22. subReq.subscriptionBody = true
  23. inbounds, err := subReq.getInboundsBySubId(subId)
  24. if err != nil {
  25. return "", "", err
  26. }
  27. externalLinks, err := subReq.getClientExternalLinksBySubId(subId)
  28. if err != nil {
  29. return "", "", err
  30. }
  31. if len(inbounds) == 0 && len(externalLinks) == 0 {
  32. return "", "", nil
  33. }
  34. var proxies []map[string]any
  35. var hasInactiveExternal bool
  36. seenEmails := make(map[string]struct{})
  37. for _, inbound := range inbounds {
  38. clients := subReq.matchingClients(inbound, subId)
  39. if len(clients) == 0 {
  40. continue
  41. }
  42. subReq.projectThroughFallbackMaster(inbound)
  43. if hostEps := subReq.hostEndpoints(inbound, "clash"); len(hostEps) > 0 {
  44. injectExternalProxy(inbound, hostEps)
  45. }
  46. for _, client := range clients {
  47. seenEmails[client.Email] = struct{}{}
  48. proxies = append(proxies, s.getProxies(subReq, inbound, client, host)...)
  49. }
  50. }
  51. for _, ext := range externalLinks {
  52. if !ext.Active {
  53. seenEmails[ext.Email] = struct{}{}
  54. hasInactiveExternal = true
  55. continue
  56. }
  57. for _, el := range expandEntry(ext) {
  58. name := el.Name
  59. if name == "" {
  60. name = ext.Email
  61. }
  62. if proxy := s.clashProxyFromExternal(el.Link, name); proxy != nil {
  63. seenEmails[ext.Email] = struct{}{}
  64. proxies = append(proxies, proxy)
  65. }
  66. }
  67. }
  68. if len(proxies) == 0 && !hasInactiveExternal {
  69. return "", "", nil
  70. }
  71. emails := make([]string, 0, len(seenEmails))
  72. for e := range seenEmails {
  73. emails = append(emails, e)
  74. }
  75. traffic, _ := subReq.AggregateTrafficByEmails(emails)
  76. header := fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000)
  77. if len(proxies) == 0 {
  78. return "", header, nil
  79. }
  80. ensureUniqueProxyNames(proxies)
  81. proxyNames := make([]string, 0, len(proxies)+1)
  82. for _, proxy := range proxies {
  83. if name, ok := proxy["name"].(string); ok && name != "" {
  84. proxyNames = append(proxyNames, name)
  85. }
  86. }
  87. proxyNames = append(proxyNames, "DIRECT")
  88. config := map[string]any{
  89. "proxies": proxies,
  90. "proxy-groups": []map[string]any{{
  91. "name": "PROXY",
  92. "type": "select",
  93. "proxies": proxyNames,
  94. }},
  95. "rules": []string{"MATCH,PROXY"},
  96. }
  97. if s.enableRouting {
  98. resolved, remoteDocument, remote, resolveErr := resolveClashRoutingSource(s.clashRules)
  99. if resolveErr == nil && strings.TrimSpace(resolved) != "" {
  100. if remote {
  101. if err := mergeRemoteClashRules(config, remoteDocument); err != nil {
  102. return "", "", err
  103. }
  104. } else if err := mergeClashRulesYAML(config, resolved); err != nil {
  105. return "", "", err
  106. }
  107. }
  108. }
  109. finalYAML, err := marshalClashYAML(config)
  110. if err != nil {
  111. return "", "", err
  112. }
  113. return string(finalYAML), header, nil
  114. }
  115. // ensureUniqueProxyNames keeps every proxy "name" non-empty and unique:
  116. // mihomo rejects the whole config on a duplicate name (the empty string
  117. // genRemark returns for a remark-less inbound counts), vanishing the Clash
  118. // profile on refresh. See issue #4641.
  119. func ensureUniqueProxyNames(proxies []map[string]any) {
  120. seen := make(map[string]struct{}, len(proxies))
  121. for i, proxy := range proxies {
  122. base, _ := proxy["name"].(string)
  123. if base == "" {
  124. base = fallbackProxyName(proxy, i)
  125. }
  126. name := base
  127. for n := 2; ; n++ {
  128. if _, dup := seen[name]; !dup {
  129. break
  130. }
  131. name = fmt.Sprintf("%s-%d", base, n)
  132. }
  133. seen[name] = struct{}{}
  134. proxy["name"] = name
  135. }
  136. }
  137. func fallbackProxyName(proxy map[string]any, idx int) string {
  138. typ, _ := proxy["type"].(string)
  139. server, _ := proxy["server"].(string)
  140. if typ != "" && server != "" {
  141. return fmt.Sprintf("%s-%s-%v", typ, server, proxy["port"])
  142. }
  143. return fmt.Sprintf("proxy-%d", idx+1)
  144. }
  145. func (s *SubClashService) getProxies(subReq *SubService, inbound *model.Inbound, client model.Client, host string) []map[string]any {
  146. stream := s.streamData(inbound.StreamSettings)
  147. // For node-managed inbounds the Clash proxy "server" must be the
  148. // node's address, not the request host. resolveInboundAddress handles
  149. // the node→subscriber-host fallback chain.
  150. defaultDest := subReq.resolveInboundAddress(inbound)
  151. if defaultDest == "" {
  152. defaultDest = host
  153. }
  154. externalProxies, ok := stream["externalProxy"].([]any)
  155. hasExternalProxy := ok && len(externalProxies) > 0
  156. if !hasExternalProxy {
  157. externalProxies = []any{map[string]any{
  158. "forceTls": "same",
  159. "dest": defaultDest,
  160. "port": float64(inbound.Port),
  161. "remark": "",
  162. }}
  163. }
  164. delete(stream, "externalProxy")
  165. network, _ := stream["network"].(string)
  166. proxies := make([]map[string]any, 0, len(externalProxies))
  167. for _, ep := range externalProxies {
  168. extPrxy, ok := ep.(map[string]any)
  169. if !ok {
  170. continue
  171. }
  172. // Expand the host's {{VAR}} remark template for this client (no-op for
  173. // the synthetic/legacy entry) before it becomes the proxy name.
  174. subReq.renderHostRemark(inbound, client, extPrxy, network)
  175. workingInbound := *inbound
  176. workingInbound.Listen, _ = extPrxy["dest"].(string)
  177. if port, ok := extPrxy["port"].(float64); ok {
  178. workingInbound.Port = int(port)
  179. }
  180. workingStream := cloneStreamForExternalProxy(stream)
  181. forceTls, _ := extPrxy["forceTls"].(string)
  182. switch forceTls {
  183. case "tls":
  184. if workingStream["security"] != "tls" {
  185. workingStream["security"] = "tls"
  186. workingStream["tlsSettings"] = map[string]any{}
  187. }
  188. case "none":
  189. if workingStream["security"] != "none" {
  190. workingStream["security"] = "none"
  191. delete(workingStream, "tlsSettings")
  192. delete(workingStream, "realitySettings")
  193. }
  194. }
  195. security, _ := workingStream["security"].(string)
  196. if hasExternalProxy {
  197. applyExternalProxyTLSToStream(extPrxy, workingStream, security)
  198. }
  199. applyHostStreamOverrides(extPrxy, workingStream)
  200. proxy := s.buildProxy(subReq, &workingInbound, client, workingStream, extPrxy)
  201. if len(proxy) > 0 {
  202. // Host-only mihomo knob: ip-version is a top-level proxy field, set
  203. // last so it cannot be clobbered. Absent for legacy externalProxy.
  204. if v, _ := extPrxy["mihomoIpVersion"].(string); v != "" {
  205. proxy["ip-version"] = v
  206. }
  207. proxies = append(proxies, proxy)
  208. }
  209. }
  210. return proxies
  211. }
  212. func (s *SubClashService) buildProxy(subReq *SubService, inbound *model.Inbound, client model.Client, stream map[string]any, ep map[string]any) map[string]any {
  213. // Hysteria has its own transport + TLS model, applyTransport /
  214. // applySecurity don't fit.
  215. if inbound.Protocol == model.Hysteria {
  216. return s.buildHysteriaProxy(subReq, inbound, client, ep)
  217. }
  218. if inbound.Protocol == model.WireGuard {
  219. return s.buildWireguardProxy(subReq, inbound, client, ep)
  220. }
  221. network, _ := stream["network"].(string)
  222. proxy := map[string]any{
  223. "name": subReq.endpointRemark(inbound, client.Email, ep, network),
  224. "server": inbound.Listen,
  225. "port": inbound.Port,
  226. "udp": true,
  227. }
  228. if !s.applyTransport(proxy, network, stream) {
  229. return nil
  230. }
  231. switch inbound.Protocol {
  232. case model.VMESS:
  233. proxy["type"] = "vmess"
  234. proxy["uuid"] = client.ID
  235. proxy["alterId"] = 0
  236. proxy["cipher"] = normalizeVmessSecurity(client.Security)
  237. case model.VLESS:
  238. proxy["type"] = "vless"
  239. proxy["uuid"] = applyVlessRoute(client.ID, hostVlessRoute(ep))
  240. inboundSettings := subReq.linkSettings(inbound)
  241. streamSecurity, _ := stream["security"].(string)
  242. if client.Flow != "" && !inbound.DisableFlow && vlessFlowAllowed(network, streamSecurity, inboundSettings) {
  243. proxy["flow"] = client.Flow
  244. }
  245. if encryption, ok := inboundSettings["encryption"].(string); ok {
  246. encryption = strings.TrimSpace(encryption)
  247. if encryption != "" && encryption != "none" {
  248. proxy["encryption"] = encryption
  249. }
  250. }
  251. case model.Trojan:
  252. proxy["type"] = "trojan"
  253. proxy["password"] = client.Password
  254. case model.Shadowsocks:
  255. proxy["type"] = "ss"
  256. proxy["password"] = client.Password
  257. inboundSettings := subReq.linkSettings(inbound)
  258. method, _ := inboundSettings["method"].(string)
  259. if method == "" {
  260. return nil
  261. }
  262. proxy["cipher"] = method
  263. if strings.HasPrefix(method, "2022") {
  264. if serverPassword, ok := inboundSettings["password"].(string); ok && serverPassword != "" {
  265. proxy["password"] = fmt.Sprintf("%s:%s", serverPassword, client.Password)
  266. }
  267. }
  268. default:
  269. return nil
  270. }
  271. security, _ := stream["security"].(string)
  272. if !s.applySecurity(proxy, security, stream) {
  273. return nil
  274. }
  275. return proxy
  276. }
  277. // buildHysteriaProxy produces a mihomo-compatible Clash entry for a
  278. // Hysteria (v1) or Hysteria2 inbound. It reads `inbound.StreamSettings`
  279. // directly instead of going through streamData/tlsData, because those
  280. // helpers prune fields (like `allowInsecure` / the salamander obfs
  281. // block) that the hysteria proxy wants preserved.
  282. func (s *SubClashService) buildHysteriaProxy(subReq *SubService, inbound *model.Inbound, client model.Client, ep map[string]any) map[string]any {
  283. inboundSettings := subReq.linkSettings(inbound)
  284. proxyType := "hysteria2"
  285. authKey := "password"
  286. if v, ok := inboundSettings["version"].(float64); ok && int(v) == 1 {
  287. proxyType = "hysteria"
  288. authKey = "auth-str"
  289. }
  290. proxy := map[string]any{
  291. "name": subReq.endpointRemark(inbound, client.Email, ep, "quic"),
  292. "type": proxyType,
  293. "server": inbound.Listen,
  294. "port": inbound.Port,
  295. "udp": true,
  296. authKey: client.Auth,
  297. }
  298. var rawStream map[string]any
  299. _ = json.Unmarshal([]byte(inbound.StreamSettings), &rawStream)
  300. // TLS details — hysteria always uses TLS.
  301. if tlsSettings, ok := rawStream["tlsSettings"].(map[string]any); ok {
  302. if serverName, ok := tlsSettings["serverName"].(string); ok && serverName != "" {
  303. proxy["sni"] = serverName
  304. }
  305. if alpnList, ok := tlsSettings["alpn"].([]any); ok && len(alpnList) > 0 {
  306. out := make([]string, 0, len(alpnList))
  307. for _, a := range alpnList {
  308. if s, ok := a.(string); ok && s != "" {
  309. out = append(out, s)
  310. }
  311. }
  312. if len(out) > 0 {
  313. proxy["alpn"] = out
  314. }
  315. }
  316. if inner, ok := tlsSettings["settings"].(map[string]any); ok {
  317. if insecure, ok := inner["allowInsecure"].(bool); ok && insecure {
  318. proxy["skip-cert-verify"] = true
  319. }
  320. if fp, ok := inner["fingerprint"].(string); ok && fp != "" {
  321. proxy["client-fingerprint"] = fp
  322. }
  323. }
  324. }
  325. if insecure, ok := ep["allowInsecure"].(bool); ok && insecure {
  326. proxy["skip-cert-verify"] = true
  327. }
  328. // Salamander obfs (Hysteria2). Read the same finalmask.udp[salamander]
  329. // block the subscription link generator uses.
  330. if finalmask, ok := rawStream["finalmask"].(map[string]any); ok {
  331. if udpMasks, ok := finalmask["udp"].([]any); ok {
  332. for _, m := range udpMasks {
  333. mask, _ := m.(map[string]any)
  334. if mask == nil || mask["type"] != "salamander" {
  335. continue
  336. }
  337. settings, _ := mask["settings"].(map[string]any)
  338. if pw, ok := settings["password"].(string); ok && pw != "" {
  339. proxy["obfs"] = "salamander"
  340. proxy["obfs-password"] = pw
  341. break
  342. }
  343. }
  344. }
  345. }
  346. // UDP port hopping. mihomo reads the range from a dedicated `ports`
  347. // field (the base `port` stays as the redirect target).
  348. if hopPorts := hysteriaHopPorts(rawStream); hopPorts != "" {
  349. proxy["ports"] = hopPorts
  350. }
  351. return proxy
  352. }
  353. // buildWireguardProxy produces a mihomo-compatible Clash entry for a native
  354. // WireGuard inbound, mirroring genWireguardLink: the peer public key is derived
  355. // from the inbound secretKey, while the private key, tunnel address, and
  356. // pre-shared key come from the client. Returns nil when the client has no key.
  357. func (s *SubClashService) buildWireguardProxy(subReq *SubService, inbound *model.Inbound, client model.Client, ep map[string]any) map[string]any {
  358. if client.PrivateKey == "" {
  359. return nil
  360. }
  361. var inboundSettings map[string]any
  362. _ = json.Unmarshal([]byte(inbound.Settings), &inboundSettings)
  363. secretKey, _ := inboundSettings["secretKey"].(string)
  364. proxy := map[string]any{
  365. "name": subReq.endpointRemark(inbound, client.Email, ep, ""),
  366. "type": "wireguard",
  367. "server": inbound.Listen,
  368. "port": inbound.Port,
  369. "udp": true,
  370. "private-key": client.PrivateKey,
  371. }
  372. if secretKey != "" {
  373. if pub, err := wgutil.PublicKeyFromPrivate(secretKey); err == nil {
  374. proxy["public-key"] = pub
  375. }
  376. }
  377. if client.PreSharedKey != "" {
  378. proxy["pre-shared-key"] = client.PreSharedKey
  379. }
  380. if client.KeepAlive > 0 {
  381. proxy["persistent-keepalive"] = client.KeepAlive
  382. }
  383. for _, addr := range client.AllowedIPs {
  384. ip := stripCIDR(addr)
  385. if ip == "" {
  386. continue
  387. }
  388. if strings.Contains(ip, ":") {
  389. proxy["ipv6"] = ip
  390. } else {
  391. proxy["ip"] = ip
  392. }
  393. }
  394. if mtu, ok := inboundSettings["mtu"].(float64); ok && mtu > 0 {
  395. proxy["mtu"] = int(mtu)
  396. }
  397. if dns, _ := inboundSettings["dns"].(string); dns != "" {
  398. servers := make([]string, 0)
  399. for server := range strings.SplitSeq(dns, ",") {
  400. if server = strings.TrimSpace(server); server != "" {
  401. servers = append(servers, server)
  402. }
  403. }
  404. if len(servers) > 0 {
  405. proxy["dns"] = servers
  406. }
  407. }
  408. return proxy
  409. }
  410. // buildXhttpClashOpts converts xhttpSettings from 3x-ui's camelCase JSON
  411. // storage into the kebab-case map that Mihomo expects under xhttp-opts.
  412. //
  413. // Only client-relevant fields are included (allowlist approach).
  414. // Server-only fields (noSSEHeader, scMaxBufferedPosts, scStreamUpServerSecs,
  415. // serverMaxHeaderBytes) are automatically excluded because they are not in
  416. // the mapping. This is intentional — when Mihomo adds new fields, the mapping
  417. // must be updated explicitly rather than leaking unverified fields to clients.
  418. //
  419. // Returns nil if no non-trivial fields are present.
  420. func buildXhttpClashOpts(xhttp map[string]any) map[string]any {
  421. if xhttp == nil {
  422. return nil
  423. }
  424. opts := map[string]any{}
  425. // Direct fields: path, mode
  426. if v, ok := xhttp["path"].(string); ok && v != "" {
  427. opts["path"] = v
  428. }
  429. if v, ok := xhttp["mode"].(string); ok && v != "" {
  430. opts["mode"] = v
  431. }
  432. // Host: explicit host field wins, then fall back to headers.Host
  433. host := ""
  434. if v, ok := xhttp["host"].(string); ok && v != "" {
  435. host = v
  436. } else if headers, ok := xhttp["headers"].(map[string]any); ok {
  437. host = searchHost(headers)
  438. }
  439. if host != "" {
  440. opts["host"] = host
  441. }
  442. type xhttpStringField struct{ src, dst, skipValue string }
  443. stringFields := []xhttpStringField{
  444. {"xPaddingBytes", "x-padding-bytes", ""},
  445. {"uplinkHTTPMethod", "uplink-http-method", ""},
  446. {"sessionIDPlacement", "session-id-placement", ""},
  447. {"sessionIDKey", "session-id-key", ""},
  448. {"sessionIDTable", "session-id-table", ""},
  449. {"sessionIDLength", "session-id-length", ""},
  450. {"seqPlacement", "seq-placement", ""},
  451. {"seqKey", "seq-key", ""},
  452. {"uplinkDataPlacement", "uplink-data-placement", ""},
  453. {"uplinkDataKey", "uplink-data-key", ""},
  454. {"scMaxEachPostBytes", "sc-max-each-post-bytes", "1000000"},
  455. {"scMinPostsIntervalMs", "sc-min-posts-interval-ms", "30"},
  456. }
  457. for _, f := range stringFields {
  458. if v, ok := xhttp[f.src].(string); ok && v != "" && (f.skipValue == "" || v != f.skipValue) {
  459. opts[f.dst] = v
  460. }
  461. }
  462. // Legacy inbounds (pre xray-core #6258) stored sessionPlacement/sessionKey.
  463. // Fall back to them so not-yet-resaved configs still map. Mirrors the
  464. // frontend migration.
  465. for _, f := range []xhttpStringField{
  466. {"sessionPlacement", "session-id-placement", ""},
  467. {"sessionKey", "session-id-key", ""},
  468. } {
  469. if _, exists := opts[f.dst]; exists {
  470. continue
  471. }
  472. if v, ok := xhttp[f.src].(string); ok && v != "" {
  473. opts[f.dst] = v
  474. }
  475. }
  476. // Bool fields (truthy only)
  477. if v, ok := xhttp["noGRPCHeader"].(bool); ok && v {
  478. opts["no-grpc-header"] = true
  479. }
  480. if v, ok := xhttp["xPaddingObfsMode"].(bool); ok && v {
  481. opts["x-padding-obfs-mode"] = true
  482. // Padding obfs gated fields
  483. for _, field := range []struct{ src, dst string }{
  484. {"xPaddingKey", "x-padding-key"},
  485. {"xPaddingHeader", "x-padding-header"},
  486. {"xPaddingPlacement", "x-padding-placement"},
  487. {"xPaddingMethod", "x-padding-method"},
  488. } {
  489. if v, ok := xhttp[field.src].(string); ok && v != "" {
  490. opts[field.dst] = v
  491. }
  492. }
  493. }
  494. // Non-zero value fields
  495. if v, ok := nonZeroShareValue(xhttp["uplinkChunkSize"]); ok {
  496. opts["uplink-chunk-size"] = v
  497. }
  498. // Nested object: xmux → reuse-settings
  499. if xmux, ok := xhttp["xmux"].(map[string]any); ok && len(xmux) > 0 {
  500. reuse := map[string]any{}
  501. for _, f := range []struct{ src, dst string }{
  502. {"maxConcurrency", "max-concurrency"},
  503. {"maxConnections", "max-connections"},
  504. {"cMaxReuseTimes", "c-max-reuse-times"},
  505. {"hMaxRequestTimes", "h-max-request-times"},
  506. {"hMaxReusableSecs", "h-max-reusable-secs"},
  507. } {
  508. if v, ok := xmux[f.src].(string); ok && v != "" {
  509. reuse[f.dst] = v
  510. }
  511. }
  512. if v, ok := nonZeroShareValue(xmux["hKeepAlivePeriod"]); ok {
  513. reuse["h-keep-alive-period"] = v
  514. }
  515. if len(reuse) > 0 {
  516. opts["reuse-settings"] = reuse
  517. }
  518. }
  519. // Headers (drop Host key)
  520. if rawHeaders, ok := xhttp["headers"].(map[string]any); ok && len(rawHeaders) > 0 {
  521. out := map[string]any{}
  522. for k, v := range rawHeaders {
  523. if strings.EqualFold(k, "host") {
  524. continue
  525. }
  526. out[k] = v
  527. }
  528. if len(out) > 0 {
  529. opts["headers"] = out
  530. }
  531. }
  532. if len(opts) == 0 {
  533. return nil
  534. }
  535. return opts
  536. }
  537. func (s *SubClashService) applyTransport(proxy map[string]any, network string, stream map[string]any) bool {
  538. switch network {
  539. case "", "tcp":
  540. proxy["network"] = "tcp"
  541. tcp, _ := stream["tcpSettings"].(map[string]any)
  542. if tcp != nil {
  543. header, _ := tcp["header"].(map[string]any)
  544. if header != nil {
  545. typeStr, _ := header["type"].(string)
  546. if typeStr != "" && typeStr != "none" {
  547. return false
  548. }
  549. }
  550. }
  551. return true
  552. case "ws":
  553. proxy["network"] = "ws"
  554. ws, _ := stream["wsSettings"].(map[string]any)
  555. wsOpts := map[string]any{}
  556. if ws != nil {
  557. if path, ok := ws["path"].(string); ok && path != "" {
  558. wsOpts["path"] = path
  559. }
  560. host := ""
  561. if v, ok := ws["host"].(string); ok && v != "" {
  562. host = v
  563. } else if headers, ok := ws["headers"].(map[string]any); ok {
  564. host = searchHost(headers)
  565. }
  566. if host != "" {
  567. wsOpts["headers"] = map[string]any{"Host": host}
  568. }
  569. }
  570. if len(wsOpts) > 0 {
  571. proxy["ws-opts"] = wsOpts
  572. }
  573. return true
  574. case "grpc":
  575. proxy["network"] = "grpc"
  576. grpc, _ := stream["grpcSettings"].(map[string]any)
  577. grpcOpts := map[string]any{}
  578. if grpc != nil {
  579. if serviceName, ok := grpc["serviceName"].(string); ok && serviceName != "" {
  580. grpcOpts["grpc-service-name"] = serviceName
  581. }
  582. }
  583. if len(grpcOpts) > 0 {
  584. proxy["grpc-opts"] = grpcOpts
  585. }
  586. return true
  587. case "httpupgrade":
  588. proxy["network"] = "httpupgrade"
  589. hu, _ := stream["httpupgradeSettings"].(map[string]any)
  590. opts := map[string]any{}
  591. if hu != nil {
  592. if path, ok := hu["path"].(string); ok && path != "" {
  593. opts["path"] = path
  594. }
  595. host := ""
  596. if v, ok := hu["host"].(string); ok && v != "" {
  597. host = v
  598. } else if headers, ok := hu["headers"].(map[string]any); ok {
  599. host = searchHost(headers)
  600. }
  601. if host != "" {
  602. opts["headers"] = map[string]any{"Host": host}
  603. }
  604. }
  605. if len(opts) > 0 {
  606. proxy["http-upgrade-opts"] = opts
  607. }
  608. return true
  609. case "xhttp":
  610. proxy["network"] = "xhttp"
  611. xhttp, _ := stream["xhttpSettings"].(map[string]any)
  612. opts := buildXhttpClashOpts(xhttp)
  613. if opts != nil {
  614. proxy["xhttp-opts"] = opts
  615. }
  616. return true
  617. default:
  618. return false
  619. }
  620. }
  621. func (s *SubClashService) applySecurity(proxy map[string]any, security string, stream map[string]any) bool {
  622. switch security {
  623. case "", "none":
  624. proxy["tls"] = false
  625. return true
  626. case "tls":
  627. proxy["tls"] = true
  628. tlsSettings, _ := stream["tlsSettings"].(map[string]any)
  629. if tlsSettings != nil {
  630. if serverName, ok := tlsSettings["serverName"].(string); ok && serverName != "" {
  631. proxy["servername"] = serverName
  632. switch proxy["type"] {
  633. case "trojan":
  634. proxy["sni"] = serverName
  635. }
  636. }
  637. if fingerprint, ok := tlsSettings["fingerprint"].(string); ok && fingerprint != "" {
  638. proxy["client-fingerprint"] = fingerprint
  639. }
  640. if alpn, ok := externalProxyALPNList(tlsSettings["alpn"]); ok {
  641. out := make([]string, 0, len(alpn))
  642. for _, item := range alpn {
  643. if s, ok := item.(string); ok && s != "" {
  644. out = append(out, s)
  645. }
  646. }
  647. if len(out) > 0 {
  648. proxy["alpn"] = out
  649. }
  650. }
  651. if inner, ok := tlsSettings["settings"].(map[string]any); ok {
  652. if insecure, ok := inner["allowInsecure"].(bool); ok && insecure {
  653. proxy["skip-cert-verify"] = true
  654. }
  655. }
  656. if pins, ok := tlsSettings["pin-sha256"].([]any); ok && len(pins) > 0 {
  657. proxy["pin-sha256"] = pins
  658. }
  659. }
  660. return true
  661. case "reality":
  662. proxy["tls"] = true
  663. realitySettings, _ := stream["realitySettings"].(map[string]any)
  664. if realitySettings == nil {
  665. return false
  666. }
  667. if serverName, ok := realitySettings["serverName"].(string); ok && serverName != "" {
  668. proxy["servername"] = serverName
  669. }
  670. realityOpts := map[string]any{}
  671. if publicKey, ok := realitySettings["publicKey"].(string); ok && publicKey != "" {
  672. realityOpts["public-key"] = publicKey
  673. }
  674. if shortID, ok := realitySettings["shortId"].(string); ok && shortID != "" {
  675. realityOpts["short-id"] = shortID
  676. }
  677. if len(realityOpts) > 0 {
  678. proxy["reality-opts"] = realityOpts
  679. }
  680. if fingerprint, ok := realitySettings["fingerprint"].(string); ok && fingerprint != "" {
  681. proxy["client-fingerprint"] = fingerprint
  682. }
  683. return true
  684. default:
  685. return false
  686. }
  687. }
  688. func (s *SubClashService) streamData(stream string) map[string]any {
  689. var streamSettings map[string]any
  690. _ = json.Unmarshal([]byte(stream), &streamSettings)
  691. security, _ := streamSettings["security"].(string)
  692. switch security {
  693. case "tls":
  694. if tlsSettings, ok := streamSettings["tlsSettings"].(map[string]any); ok {
  695. streamSettings["tlsSettings"] = s.tlsData(tlsSettings)
  696. }
  697. case "reality":
  698. if realitySettings, ok := streamSettings["realitySettings"].(map[string]any); ok {
  699. streamSettings["realitySettings"] = s.realityData(realitySettings)
  700. }
  701. }
  702. delete(streamSettings, "sockopt")
  703. return streamSettings
  704. }
  705. func (s *SubClashService) tlsData(tData map[string]any) map[string]any {
  706. tlsData := make(map[string]any, 1)
  707. tlsClientSettings, _ := tData["settings"].(map[string]any)
  708. tlsData["serverName"] = tData["serverName"]
  709. tlsData["alpn"] = tData["alpn"]
  710. if fingerprint, ok := tlsClientSettings["fingerprint"].(string); ok {
  711. tlsData["fingerprint"] = fingerprint
  712. }
  713. if pins, ok := tlsClientSettings["pinnedPeerCertSha256"].([]any); ok && len(pins) > 0 {
  714. tlsData["pin-sha256"] = pins
  715. }
  716. return tlsData
  717. }
  718. func (s *SubClashService) realityData(rData map[string]any) map[string]any {
  719. rDataOut := make(map[string]any, 1)
  720. realityClientSettings, _ := rData["settings"].(map[string]any)
  721. if publicKey, ok := realityClientSettings["publicKey"].(string); ok {
  722. rDataOut["publicKey"] = publicKey
  723. }
  724. if fingerprint, ok := realityClientSettings["fingerprint"].(string); ok {
  725. rDataOut["fingerprint"] = fingerprint
  726. }
  727. if serverNames, ok := rData["serverNames"].([]any); ok && len(serverNames) > 0 {
  728. rDataOut["serverName"] = fmt.Sprint(serverNames[0])
  729. }
  730. if shortIDs, ok := rData["shortIds"].([]any); ok && len(shortIDs) > 0 {
  731. rDataOut["shortId"] = fmt.Sprint(shortIDs[0])
  732. }
  733. return rDataOut
  734. }
  735. func cloneMap(src map[string]any) map[string]any {
  736. if src == nil {
  737. return nil
  738. }
  739. dst := make(map[string]any, len(src))
  740. maps.Copy(dst, src)
  741. return dst
  742. }
  743. func mergeClashRulesYAML(base map[string]any, raw string) error {
  744. raw = strings.TrimSpace(raw)
  745. if raw == "" {
  746. return nil
  747. }
  748. var custom any
  749. if err := yaml.Unmarshal([]byte(raw), &custom); err != nil {
  750. mergeClashRules(base, linesToClashRules(raw))
  751. return nil
  752. }
  753. switch typed := custom.(type) {
  754. case []any:
  755. mergeClashRules(base, typed)
  756. case map[string]any:
  757. for key, value := range typed {
  758. if key == "rules" {
  759. if ruleList, ok := asAnySlice(value); ok {
  760. mergeClashRules(base, ruleList)
  761. }
  762. continue
  763. }
  764. base[key] = value
  765. }
  766. default:
  767. mergeClashRules(base, linesToClashRules(raw))
  768. }
  769. return nil
  770. }
  771. // mergeRemoteClashRules lets remote update only the route graph (see
  772. // remoteClashAllowedKey) and never mutates remote: cached documents are shared.
  773. func mergeRemoteClashRules(base map[string]any, remote map[string]any) error {
  774. if len(remote) == 0 {
  775. return fmt.Errorf("remote Clash routing source must be a YAML map")
  776. }
  777. for key, value := range remote {
  778. if !remoteClashAllowedKey(key) {
  779. continue
  780. }
  781. if err := validateRemoteClashValue(key, value); err != nil {
  782. return err
  783. }
  784. switch key {
  785. case "rules":
  786. rules, _ := asAnySlice(value)
  787. mergeClashRules(base, rules)
  788. case "proxy-groups":
  789. groups, _ := asAnySlice(value)
  790. base["proxy-groups"] = mergeClashProxyGroups(base["proxy-groups"], groups)
  791. default:
  792. base[key] = value
  793. }
  794. }
  795. return validateClashRouteGraph(base)
  796. }
  797. func validateRemoteClashValue(key string, value any) error {
  798. switch key {
  799. case "rules":
  800. rules, ok := asAnySlice(value)
  801. if !ok {
  802. return fmt.Errorf("remote Clash rules must be a list")
  803. }
  804. for _, rule := range rules {
  805. text, ok := rule.(string)
  806. if !ok || strings.TrimSpace(text) == "" {
  807. return fmt.Errorf("remote Clash rules must contain non-empty strings")
  808. }
  809. }
  810. case "proxy-groups":
  811. groups, ok := asAnySlice(value)
  812. if !ok {
  813. return fmt.Errorf("remote Clash proxy-groups must be a list")
  814. }
  815. seen := make(map[string]struct{}, len(groups))
  816. for _, groupValue := range groups {
  817. group, ok := groupValue.(map[string]any)
  818. if !ok {
  819. return fmt.Errorf("remote Clash proxy-groups must contain named group maps with a type")
  820. }
  821. name, nameOK := group["name"].(string)
  822. groupType, typeOK := group["type"].(string)
  823. if !nameOK || !typeOK || strings.TrimSpace(name) == "" || strings.TrimSpace(groupType) == "" {
  824. return fmt.Errorf("remote Clash proxy-groups must contain named group maps with a type")
  825. }
  826. name = strings.TrimSpace(name)
  827. if _, duplicate := seen[name]; duplicate {
  828. return fmt.Errorf("remote Clash proxy-group name %q is duplicated", name)
  829. }
  830. seen[name] = struct{}{}
  831. if useValue, exists := group["use"]; exists {
  832. use, ok := asAnySlice(useValue)
  833. if !ok || len(use) > 0 {
  834. return fmt.Errorf("remote Clash proxy-group %q cannot use proxy-providers", name)
  835. }
  836. }
  837. }
  838. case "rule-providers":
  839. providers, ok := value.(map[string]any)
  840. if !ok {
  841. return fmt.Errorf("remote Clash rule-providers must be a map")
  842. }
  843. for name, provider := range providers {
  844. if strings.TrimSpace(name) == "" {
  845. return fmt.Errorf("remote Clash rule-provider name must not be empty")
  846. }
  847. if _, ok := provider.(map[string]any); !ok {
  848. return fmt.Errorf("remote Clash rule-provider %q must be a map", name)
  849. }
  850. }
  851. }
  852. return nil
  853. }
  854. func remoteClashAllowedKey(key string) bool {
  855. switch key {
  856. case "proxy-groups", "rule-providers", "rules":
  857. return true
  858. default:
  859. return false
  860. }
  861. }
  862. func validateClashRouteGraph(config map[string]any) error {
  863. known := map[string]struct{}{
  864. "DIRECT": {}, "REJECT": {}, "REJECT-DROP": {}, "REJECT-TINYGIF": {}, "PASS": {}, "GLOBAL": {},
  865. }
  866. if proxies, ok := asAnySlice(config["proxies"]); ok {
  867. for _, value := range proxies {
  868. proxy, ok := value.(map[string]any)
  869. if !ok {
  870. continue
  871. }
  872. if name, ok := proxy["name"].(string); ok && strings.TrimSpace(name) != "" {
  873. known[strings.TrimSpace(name)] = struct{}{}
  874. }
  875. }
  876. }
  877. groups, _ := asAnySlice(config["proxy-groups"])
  878. for _, value := range groups {
  879. if name := clashProxyGroupName(value); name != "" {
  880. known[name] = struct{}{}
  881. }
  882. }
  883. for _, value := range groups {
  884. group, ok := value.(map[string]any)
  885. if !ok {
  886. continue
  887. }
  888. name := clashProxyGroupName(group)
  889. refs, exists := group["proxies"]
  890. if !exists {
  891. continue
  892. }
  893. proxies, ok := asAnySlice(refs)
  894. if !ok {
  895. return fmt.Errorf("Clash proxy-group %q proxies must be a list", name)
  896. }
  897. for _, refValue := range proxies {
  898. ref, ok := refValue.(string)
  899. if !ok || strings.TrimSpace(ref) == "" {
  900. return fmt.Errorf("Clash proxy-group %q contains an invalid proxy reference", name)
  901. }
  902. ref = strings.TrimSpace(ref)
  903. if _, exists := known[ref]; !exists {
  904. return fmt.Errorf("Clash proxy-group %q references unknown proxy or group %q", name, ref)
  905. }
  906. }
  907. }
  908. providers, _ := config["rule-providers"].(map[string]any)
  909. for providerName, value := range providers {
  910. provider, ok := value.(map[string]any)
  911. if !ok {
  912. continue
  913. }
  914. via, ok := provider["proxy"].(string)
  915. if !ok || strings.TrimSpace(via) == "" {
  916. continue
  917. }
  918. via = strings.TrimSpace(via)
  919. if _, exists := known[via]; !exists {
  920. return fmt.Errorf("Clash rule-provider %q references unknown proxy or group %q", providerName, via)
  921. }
  922. }
  923. rules, _ := asAnySlice(config["rules"])
  924. for _, value := range rules {
  925. rule, ok := value.(string)
  926. if !ok || strings.TrimSpace(rule) == "" {
  927. return errors.New("Clash rules must contain non-empty strings")
  928. }
  929. parts := strings.Split(rule, ",")
  930. for i := range parts {
  931. parts[i] = strings.TrimSpace(parts[i])
  932. }
  933. if len(parts) < 2 {
  934. return fmt.Errorf("invalid Clash rule %q", rule)
  935. }
  936. if strings.EqualFold(parts[0], "RULE-SET") {
  937. if len(parts) < 3 {
  938. return fmt.Errorf("invalid Clash RULE-SET rule %q", rule)
  939. }
  940. if _, exists := providers[parts[1]]; !exists {
  941. return fmt.Errorf("Clash rule references unknown rule-provider %q", parts[1])
  942. }
  943. }
  944. targetIndex := len(parts) - 1
  945. // Mihomo IP rules may carry trailing no-resolve / src option flags.
  946. for targetIndex >= 1 && (strings.EqualFold(parts[targetIndex], "no-resolve") || strings.EqualFold(parts[targetIndex], "src")) {
  947. targetIndex--
  948. }
  949. if targetIndex < 1 {
  950. return fmt.Errorf("invalid Clash rule target in %q", rule)
  951. }
  952. target := parts[targetIndex]
  953. if _, exists := known[target]; !exists {
  954. return fmt.Errorf("Clash rule references unknown proxy or group %q", target)
  955. }
  956. }
  957. return nil
  958. }
  959. func mergeClashProxyGroups(baseValue any, remoteGroups []any) []any {
  960. baseGroups, _ := asAnySlice(baseValue)
  961. baseByName := make(map[string]any, len(baseGroups))
  962. baseOrder := make([]string, 0, len(baseGroups))
  963. for _, group := range baseGroups {
  964. name := clashProxyGroupName(group)
  965. if name == "" {
  966. continue
  967. }
  968. baseByName[name] = group
  969. baseOrder = append(baseOrder, name)
  970. }
  971. merged := make([]any, 0, len(remoteGroups)+len(baseGroups))
  972. seen := make(map[string]struct{}, len(remoteGroups)+len(baseGroups))
  973. for _, group := range remoteGroups {
  974. name := clashProxyGroupName(group)
  975. if name == "" {
  976. continue
  977. }
  978. if _, duplicate := seen[name]; duplicate {
  979. continue
  980. }
  981. seen[name] = struct{}{}
  982. merged = append(merged, group)
  983. }
  984. for _, name := range baseOrder {
  985. if _, replaced := seen[name]; replaced {
  986. continue
  987. }
  988. merged = append(merged, baseByName[name])
  989. }
  990. return merged
  991. }
  992. func clashProxyGroupName(value any) string {
  993. group, ok := value.(map[string]any)
  994. if !ok {
  995. return ""
  996. }
  997. name, _ := group["name"].(string)
  998. return strings.TrimSpace(name)
  999. }
  1000. func mergeClashRules(base map[string]any, customRules []any) {
  1001. if len(customRules) == 0 {
  1002. return
  1003. }
  1004. baseRules, _ := asAnySlice(base["rules"])
  1005. if hasClashMatchRule(customRules) {
  1006. base["rules"] = customRules
  1007. return
  1008. }
  1009. merged := make([]any, 0, len(customRules)+len(baseRules))
  1010. merged = append(merged, customRules...)
  1011. merged = append(merged, baseRules...)
  1012. base["rules"] = merged
  1013. }
  1014. func asAnySlice(value any) ([]any, bool) {
  1015. switch typed := value.(type) {
  1016. case []any:
  1017. return typed, true
  1018. case []string:
  1019. out := make([]any, 0, len(typed))
  1020. for _, item := range typed {
  1021. out = append(out, item)
  1022. }
  1023. return out, true
  1024. case []map[string]any:
  1025. out := make([]any, 0, len(typed))
  1026. for _, item := range typed {
  1027. out = append(out, item)
  1028. }
  1029. return out, true
  1030. default:
  1031. return nil, false
  1032. }
  1033. }
  1034. func hasClashMatchRule(rules []any) bool {
  1035. for _, rule := range rules {
  1036. ruleText, ok := rule.(string)
  1037. if !ok {
  1038. continue
  1039. }
  1040. parts := strings.SplitN(ruleText, ",", 2)
  1041. if strings.EqualFold(strings.TrimSpace(parts[0]), "MATCH") {
  1042. return true
  1043. }
  1044. }
  1045. return false
  1046. }
  1047. func linesToClashRules(raw string) []any {
  1048. lines := strings.Split(raw, "\n")
  1049. rules := make([]any, 0, len(lines))
  1050. for _, line := range lines {
  1051. line = strings.TrimSpace(line)
  1052. if line == "" || strings.HasPrefix(line, "#") {
  1053. continue
  1054. }
  1055. rules = append(rules, line)
  1056. }
  1057. return rules
  1058. }