1
0

clash_service.go 32 KB

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