clash_service.go 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542
  1. package sub
  2. import (
  3. "errors"
  4. "fmt"
  5. "maps"
  6. "net/netip"
  7. "slices"
  8. "strings"
  9. "github.com/goccy/go-json"
  10. yaml "github.com/goccy/go-yaml"
  11. "github.com/mhsanaei/3x-ui/v3/internal/amneziawg"
  12. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  13. "github.com/mhsanaei/3x-ui/v3/internal/tuic"
  14. wgutil "github.com/mhsanaei/3x-ui/v3/internal/util/wireguard"
  15. )
  16. type SubClashService struct {
  17. enableRouting bool
  18. clashRules string
  19. SubService *SubService
  20. }
  21. var errNoLegacyClashProxies = errors.New("no Clash for Windows-compatible proxies found; use the Mihomo subscription for modern proxy types")
  22. func NewSubClashService(enableRouting bool, clashRules string, subService *SubService) *SubClashService {
  23. return &SubClashService{enableRouting: enableRouting, clashRules: clashRules, SubService: subService}
  24. }
  25. func (s *SubClashService) GetClash(subId string, host string) (string, string, error) {
  26. return s.getClash(subId, host, false)
  27. }
  28. func (s *SubClashService) GetClashLegacy(subId string, host string) (string, string, error) {
  29. return s.getClash(subId, host, true)
  30. }
  31. func (s *SubClashService) getClash(subId string, host string, legacy bool) (string, string, error) {
  32. subReq := s.SubService.ForRequest(host)
  33. subReq.subscriptionBody = true
  34. inbounds, err := subReq.getInboundsBySubId(subId)
  35. if err != nil {
  36. return "", "", err
  37. }
  38. externalLinks, err := subReq.getClientExternalLinksBySubId(subId)
  39. if err != nil {
  40. return "", "", err
  41. }
  42. if len(inbounds) == 0 && len(externalLinks) == 0 {
  43. return "", "", nil
  44. }
  45. var proxies []map[string]any
  46. var hasInactiveExternal bool
  47. var hasEnabledClient bool
  48. seenEmails := make(map[string]struct{})
  49. for _, inbound := range inbounds {
  50. clients := subReq.matchingClients(inbound, subId)
  51. if len(clients) == 0 {
  52. continue
  53. }
  54. subReq.projectThroughFallbackMaster(inbound)
  55. if hostEps := subReq.hostEndpoints(inbound, "clash"); len(hostEps) > 0 {
  56. injectExternalProxy(inbound, hostEps)
  57. }
  58. for _, client := range clients {
  59. if client.Enable {
  60. hasEnabledClient = true
  61. }
  62. seenEmails[client.Email] = struct{}{}
  63. proxies = append(proxies, s.getProxies(subReq, inbound, client, host)...)
  64. }
  65. }
  66. for _, ext := range externalLinks {
  67. if ext.Enable {
  68. hasEnabledClient = true
  69. }
  70. // Count the client even when no proxy comes out of this link, so the
  71. // quota header does not shrink because a node is unrepresentable in Clash.
  72. seenEmails[ext.Email] = struct{}{}
  73. if !ext.Active {
  74. hasInactiveExternal = true
  75. continue
  76. }
  77. for _, el := range expandEntry(ext) {
  78. name := el.Name
  79. if name == "" {
  80. name = ext.Email
  81. }
  82. if proxy := s.clashProxyFromExternal(el.Link, name); proxy != nil {
  83. proxies = append(proxies, proxy)
  84. }
  85. }
  86. }
  87. if len(proxies) == 0 && !hasInactiveExternal {
  88. return "", "", nil
  89. }
  90. if legacy {
  91. proxies = legacyClashProxies(proxies)
  92. if len(proxies) == 0 {
  93. return "", "", errNoLegacyClashProxies
  94. }
  95. }
  96. emails := make([]string, 0, len(seenEmails))
  97. for e := range seenEmails {
  98. emails = append(emails, e)
  99. }
  100. slices.Sort(emails)
  101. traffic, _ := subReq.AggregateTrafficByEmails(emails)
  102. traffic.Enable = hasEnabledClient
  103. header := subReq.subscriptionUserinfo(traffic)
  104. if mode, remark := subReq.resolveInfoNodeRemark(subId, emails, traffic, len(proxies) > 0); mode != infoNodeNone {
  105. dummyProxy := map[string]any{
  106. "name": remark,
  107. "type": "socks5",
  108. "server": "127.0.0.1",
  109. "port": 1080,
  110. }
  111. if mode == infoNodeExpired || mode == infoNodeDepleted {
  112. proxies = []map[string]any{dummyProxy}
  113. } else {
  114. proxies = append([]map[string]any{dummyProxy}, proxies...)
  115. }
  116. }
  117. if len(proxies) == 0 {
  118. return "", header, nil
  119. }
  120. ensureUniqueProxyNames(proxies)
  121. proxyNames := make([]string, 0, len(proxies)+1)
  122. for _, proxy := range proxies {
  123. if isDummyProxy(proxy) && len(proxies) > 1 {
  124. continue
  125. }
  126. if name, ok := proxy["name"].(string); ok && name != "" {
  127. proxyNames = append(proxyNames, name)
  128. }
  129. }
  130. proxyNames = append(proxyNames, "DIRECT")
  131. config := map[string]any{
  132. "proxies": proxies,
  133. "proxy-groups": []map[string]any{{
  134. "name": "PROXY",
  135. "type": "select",
  136. "proxies": proxyNames,
  137. }},
  138. "rules": []string{"MATCH,PROXY"},
  139. }
  140. // Custom Clash routing can inject Mihomo-only groups, rules, providers or a
  141. // top-level proxies key — exactly what the legacy filter just removed.
  142. if s.enableRouting && !legacy {
  143. resolved, remoteDocument, remote, resolveErr := resolveClashRoutingSource(s.clashRules)
  144. if resolveErr == nil && strings.TrimSpace(resolved) != "" {
  145. if remote {
  146. if err := mergeRemoteClashRules(config, remoteDocument); err != nil {
  147. return "", "", err
  148. }
  149. } else if err := mergeClashRulesYAML(config, resolved); err != nil {
  150. return "", "", err
  151. }
  152. }
  153. }
  154. finalYAML, err := marshalClashYAML(config)
  155. if err != nil {
  156. return "", "", err
  157. }
  158. return string(finalYAML), header, nil
  159. }
  160. func legacyClashProxies(proxies []map[string]any) []map[string]any {
  161. compatible := make([]map[string]any, 0, len(proxies))
  162. for _, proxy := range proxies {
  163. if filtered := legacyClashProxy(proxy); filtered != nil {
  164. compatible = append(compatible, filtered)
  165. }
  166. }
  167. return compatible
  168. }
  169. func legacyClashProxy(proxy map[string]any) map[string]any {
  170. proxyType, _ := proxy["type"].(string)
  171. network, _ := proxy["network"].(string)
  172. if _, reality := proxy["reality-opts"]; reality {
  173. return nil
  174. }
  175. var fields []string
  176. var cipher string
  177. switch proxyType {
  178. case "vmess":
  179. if !legacyClashNetwork(network) || !legacyVmessCipher(proxy["cipher"]) {
  180. return nil
  181. }
  182. fields = []string{
  183. "name", "type", "server", "port", "uuid", "alterId", "cipher", "udp",
  184. "network", "tls", "skip-cert-verify", "servername", "grpc-opts", "ws-opts",
  185. }
  186. case "trojan":
  187. tls, _ := proxy["tls"].(bool)
  188. if !tls || !legacyClashNetwork(network) {
  189. return nil
  190. }
  191. fields = []string{
  192. "name", "type", "server", "port", "password", "alpn", "sni", "skip-cert-verify",
  193. "udp", "network", "grpc-opts", "ws-opts",
  194. }
  195. case "ss":
  196. tls, _ := proxy["tls"].(bool)
  197. cipher = legacyShadowsocksCipher(proxy["cipher"])
  198. if (network != "" && network != "tcp") || tls || cipher == "" {
  199. return nil
  200. }
  201. fields = []string{"name", "type", "server", "port", "password", "cipher", "udp", "plugin", "plugin-opts"}
  202. default:
  203. return nil
  204. }
  205. filtered := make(map[string]any, len(fields))
  206. for _, field := range fields {
  207. if value, exists := proxy[field]; exists {
  208. filtered[field] = value
  209. }
  210. }
  211. if proxyType == "ss" {
  212. filtered["cipher"] = cipher
  213. }
  214. return filtered
  215. }
  216. func legacyClashNetwork(network string) bool {
  217. switch network {
  218. case "", "tcp", "ws", "grpc":
  219. return true
  220. default:
  221. return false
  222. }
  223. }
  224. func legacyVmessCipher(value any) bool {
  225. cipher, _ := value.(string)
  226. switch strings.ToLower(strings.TrimSpace(cipher)) {
  227. case "auto", "aes-128-gcm", "chacha20-poly1305", "none":
  228. return true
  229. default:
  230. return false
  231. }
  232. }
  233. func legacyShadowsocksCipher(value any) string {
  234. cipher, _ := value.(string)
  235. cipher = strings.ToLower(strings.TrimSpace(cipher))
  236. switch cipher {
  237. case "chacha20-poly1305":
  238. return "chacha20-ietf-poly1305"
  239. case "aes-128-gcm", "aes-192-gcm", "aes-256-gcm",
  240. "aes-128-cfb", "aes-192-cfb", "aes-256-cfb",
  241. "aes-128-ctr", "aes-192-ctr", "aes-256-ctr",
  242. "rc4-md5", "chacha20-ietf", "xchacha20",
  243. "chacha20-ietf-poly1305", "xchacha20-ietf-poly1305":
  244. return cipher
  245. default:
  246. return ""
  247. }
  248. }
  249. // ensureUniqueProxyNames keeps every proxy "name" non-empty and unique:
  250. // mihomo rejects the whole config on a duplicate name (the empty string
  251. // genRemark returns for a remark-less inbound counts), vanishing the Clash
  252. // profile on refresh. See issue #4641.
  253. func ensureUniqueProxyNames(proxies []map[string]any) {
  254. seen := make(map[string]struct{}, len(proxies))
  255. for i, proxy := range proxies {
  256. base, _ := proxy["name"].(string)
  257. if base == "" {
  258. base = fallbackProxyName(proxy, i)
  259. }
  260. name := base
  261. for n := 2; ; n++ {
  262. if _, dup := seen[name]; !dup {
  263. break
  264. }
  265. name = fmt.Sprintf("%s-%d", base, n)
  266. }
  267. seen[name] = struct{}{}
  268. proxy["name"] = name
  269. }
  270. }
  271. func isDummyProxy(proxy map[string]any) bool {
  272. typ, _ := proxy["type"].(string)
  273. server, _ := proxy["server"].(string)
  274. var port int
  275. switch p := proxy["port"].(type) {
  276. case int:
  277. port = p
  278. case float64:
  279. port = int(p)
  280. }
  281. return typ == "socks5" && server == "127.0.0.1" && port == 1080
  282. }
  283. func fallbackProxyName(proxy map[string]any, idx int) string {
  284. typ, _ := proxy["type"].(string)
  285. server, _ := proxy["server"].(string)
  286. if typ != "" && server != "" {
  287. return fmt.Sprintf("%s-%s-%v", typ, server, proxy["port"])
  288. }
  289. return fmt.Sprintf("proxy-%d", idx+1)
  290. }
  291. func (s *SubClashService) getProxies(subReq *SubService, inbound *model.Inbound, client model.Client, host string) []map[string]any {
  292. stream := s.streamData(inbound.StreamSettings)
  293. // For node-managed inbounds the Clash proxy "server" must be the
  294. // node's address, not the request host. resolveInboundAddress handles
  295. // the node→subscriber-host fallback chain.
  296. defaultDest := subReq.resolveInboundAddress(inbound)
  297. if defaultDest == "" {
  298. defaultDest = host
  299. }
  300. externalProxies, ok := stream["externalProxy"].([]any)
  301. hasExternalProxy := ok && len(externalProxies) > 0
  302. if !hasExternalProxy {
  303. externalProxies = []any{map[string]any{
  304. "forceTls": "same",
  305. "dest": defaultDest,
  306. "port": float64(inbound.Port),
  307. "remark": "",
  308. }}
  309. }
  310. delete(stream, "externalProxy")
  311. network, _ := stream["network"].(string)
  312. proxies := make([]map[string]any, 0, len(externalProxies))
  313. for _, ep := range externalProxies {
  314. extPrxy, ok := ep.(map[string]any)
  315. if !ok {
  316. continue
  317. }
  318. // Expand the host's {{VAR}} remark template for this client (no-op for
  319. // the synthetic/legacy entry) before it becomes the proxy name.
  320. subReq.renderHostRemark(inbound, client, extPrxy, network)
  321. workingInbound := *inbound
  322. // A Clash "server" is a bare host, not a URI authority, and the custom
  323. // share address stores IPv6 literals bracketed.
  324. dest, _ := extPrxy["dest"].(string)
  325. workingInbound.Listen = strings.Trim(dest, "[]")
  326. if port, ok := extPrxy["port"].(float64); ok {
  327. workingInbound.Port = int(port)
  328. }
  329. workingStream := cloneStreamForExternalProxy(stream)
  330. forceTls, _ := extPrxy["forceTls"].(string)
  331. switch forceTls {
  332. case "tls":
  333. if workingStream["security"] != "tls" {
  334. workingStream["security"] = "tls"
  335. workingStream["tlsSettings"] = map[string]any{}
  336. }
  337. case "none":
  338. if workingStream["security"] != "none" {
  339. workingStream["security"] = "none"
  340. delete(workingStream, "tlsSettings")
  341. delete(workingStream, "realitySettings")
  342. }
  343. }
  344. security, _ := workingStream["security"].(string)
  345. if hasExternalProxy {
  346. applyExternalProxyTLSToStream(extPrxy, workingStream, security)
  347. }
  348. applyHostStreamOverrides(extPrxy, workingStream)
  349. proxy := s.buildProxy(subReq, &workingInbound, client, workingStream, extPrxy)
  350. if len(proxy) > 0 {
  351. // Host-only mihomo knob: ip-version is a top-level proxy field, set
  352. // last so it cannot be clobbered. Absent for legacy externalProxy.
  353. if v, _ := extPrxy["mihomoIpVersion"].(string); v != "" {
  354. proxy["ip-version"] = v
  355. }
  356. proxies = append(proxies, proxy)
  357. }
  358. }
  359. return proxies
  360. }
  361. func (s *SubClashService) buildProxy(subReq *SubService, inbound *model.Inbound, client model.Client, stream map[string]any, ep map[string]any) map[string]any {
  362. // Hysteria has its own transport + TLS model, applyTransport /
  363. // applySecurity don't fit.
  364. if inbound.Protocol == model.Hysteria {
  365. return s.buildHysteriaProxy(subReq, inbound, client, ep)
  366. }
  367. if inbound.Protocol == model.WireGuard {
  368. return s.buildWireguardProxy(subReq, inbound, client, ep)
  369. }
  370. if inbound.Protocol == model.TUIC {
  371. return s.buildTuicProxy(subReq, inbound, client, ep)
  372. }
  373. if inbound.Protocol == model.AmneziaWG {
  374. return s.buildAmneziaWGProxy(subReq, inbound, client, ep)
  375. }
  376. network, _ := stream["network"].(string)
  377. proxy := map[string]any{
  378. "name": subReq.endpointRemark(inbound, client.Email, ep, network),
  379. "server": inbound.Listen,
  380. "port": inbound.Port,
  381. "udp": true,
  382. }
  383. if !s.applyTransport(proxy, network, stream) {
  384. return nil
  385. }
  386. switch inbound.Protocol {
  387. case model.VMESS:
  388. proxy["type"] = "vmess"
  389. proxy["uuid"] = client.ID
  390. proxy["alterId"] = 0
  391. proxy["cipher"] = normalizeVmessSecurity(client.Security)
  392. case model.VLESS:
  393. proxy["type"] = "vless"
  394. proxy["uuid"] = applyVlessRoute(client.ID, hostVlessRoute(ep))
  395. inboundSettings := subReq.linkSettings(inbound)
  396. streamSecurity, _ := stream["security"].(string)
  397. if client.Flow != "" && !inbound.DisableFlow && vlessFlowAllowed(network, streamSecurity, inboundSettings) {
  398. proxy["flow"] = client.Flow
  399. }
  400. if encryption, ok := inboundSettings["encryption"].(string); ok {
  401. encryption = strings.TrimSpace(encryption)
  402. if encryption != "" && encryption != "none" {
  403. proxy["encryption"] = encryption
  404. }
  405. }
  406. case model.Trojan:
  407. proxy["type"] = "trojan"
  408. proxy["password"] = client.Password
  409. case model.Shadowsocks:
  410. proxy["type"] = "ss"
  411. proxy["password"] = client.Password
  412. inboundSettings := subReq.linkSettings(inbound)
  413. method, _ := inboundSettings["method"].(string)
  414. if method == "" {
  415. return nil
  416. }
  417. proxy["cipher"] = method
  418. if strings.HasPrefix(method, "2022") {
  419. if serverPassword, ok := inboundSettings["password"].(string); ok && serverPassword != "" {
  420. proxy["password"] = fmt.Sprintf("%s:%s", serverPassword, client.Password)
  421. }
  422. }
  423. default:
  424. return nil
  425. }
  426. security, _ := stream["security"].(string)
  427. if !s.applySecurity(proxy, security, stream) {
  428. return nil
  429. }
  430. return proxy
  431. }
  432. // buildHysteriaProxy produces a mihomo-compatible Clash entry for a
  433. // Hysteria (v1) or Hysteria2 inbound. It reads `inbound.StreamSettings`
  434. // directly instead of going through streamData/tlsData, because those
  435. // helpers prune fields (like `allowInsecure` / the salamander obfs
  436. // block) that the hysteria proxy wants preserved.
  437. func (s *SubClashService) buildHysteriaProxy(subReq *SubService, inbound *model.Inbound, client model.Client, ep map[string]any) map[string]any {
  438. inboundSettings := subReq.linkSettings(inbound)
  439. proxyType := "hysteria2"
  440. authKey := "password"
  441. if v, ok := inboundSettings["version"].(float64); ok && int(v) == 1 {
  442. proxyType = "hysteria"
  443. authKey = "auth-str"
  444. }
  445. proxy := map[string]any{
  446. "name": subReq.endpointRemark(inbound, client.Email, ep, "quic"),
  447. "type": proxyType,
  448. "server": inbound.Listen,
  449. "port": inbound.Port,
  450. "udp": true,
  451. authKey: client.Auth,
  452. }
  453. var rawStream map[string]any
  454. _ = json.Unmarshal([]byte(inbound.StreamSettings), &rawStream)
  455. // TLS details — hysteria always uses TLS.
  456. if tlsSettings, ok := rawStream["tlsSettings"].(map[string]any); ok {
  457. if serverName, ok := tlsSettings["serverName"].(string); ok && serverName != "" {
  458. proxy["sni"] = serverName
  459. }
  460. if alpnList, ok := tlsSettings["alpn"].([]any); ok && len(alpnList) > 0 {
  461. out := make([]string, 0, len(alpnList))
  462. for _, a := range alpnList {
  463. if s, ok := a.(string); ok && s != "" {
  464. out = append(out, s)
  465. }
  466. }
  467. if len(out) > 0 {
  468. proxy["alpn"] = out
  469. }
  470. }
  471. if inner, ok := tlsSettings["settings"].(map[string]any); ok {
  472. if insecure, ok := inner["allowInsecure"].(bool); ok && insecure {
  473. proxy["skip-cert-verify"] = true
  474. }
  475. if fp, ok := inner["fingerprint"].(string); ok && fp != "" {
  476. proxy["client-fingerprint"] = fp
  477. }
  478. }
  479. }
  480. if insecure, ok := ep["allowInsecure"].(bool); ok && insecure {
  481. proxy["skip-cert-verify"] = true
  482. }
  483. // Salamander obfs (Hysteria2). Read the same finalmask.udp[salamander]
  484. // block the subscription link generator uses.
  485. if finalmask, ok := rawStream["finalmask"].(map[string]any); ok {
  486. if udpMasks, ok := finalmask["udp"].([]any); ok {
  487. for _, m := range udpMasks {
  488. mask, _ := m.(map[string]any)
  489. if mask == nil || mask["type"] != "salamander" {
  490. continue
  491. }
  492. settings, _ := mask["settings"].(map[string]any)
  493. if pw, ok := settings["password"].(string); ok && pw != "" {
  494. proxy["obfs"] = "salamander"
  495. proxy["obfs-password"] = pw
  496. break
  497. }
  498. }
  499. }
  500. }
  501. // UDP port hopping. mihomo reads the range from a dedicated `ports`
  502. // field (the base `port` stays as the redirect target).
  503. if hopPorts := hysteriaHopPorts(rawStream); hopPorts != "" {
  504. proxy["ports"] = hopPorts
  505. }
  506. return proxy
  507. }
  508. // buildWireguardProxy produces a mihomo-compatible Clash entry for a native
  509. // WireGuard inbound, mirroring genWireguardLink: the peer public key is derived
  510. // from the inbound secretKey, while the private key, tunnel address, and
  511. // pre-shared key come from the client. Returns nil when the client has no key.
  512. func (s *SubClashService) buildWireguardProxy(subReq *SubService, inbound *model.Inbound, client model.Client, ep map[string]any) map[string]any {
  513. if client.PrivateKey == "" {
  514. return nil
  515. }
  516. var inboundSettings map[string]any
  517. _ = json.Unmarshal([]byte(inbound.Settings), &inboundSettings)
  518. secretKey, _ := inboundSettings["secretKey"].(string)
  519. proxy := map[string]any{
  520. "name": subReq.endpointRemark(inbound, client.Email, ep, ""),
  521. "type": "wireguard",
  522. "server": inbound.Listen,
  523. "port": inbound.Port,
  524. "udp": true,
  525. "private-key": client.PrivateKey,
  526. }
  527. if secretKey != "" {
  528. if pub, err := wgutil.PublicKeyFromPrivate(secretKey); err == nil {
  529. proxy["public-key"] = pub
  530. }
  531. }
  532. if client.PreSharedKey != "" {
  533. proxy["pre-shared-key"] = client.PreSharedKey
  534. }
  535. if ka := client.KeepAliveSeconds(); ka > 0 {
  536. proxy["persistent-keepalive"] = ka
  537. }
  538. for _, addr := range client.AllowedIPs {
  539. ip := stripCIDR(addr)
  540. if ip == "" {
  541. continue
  542. }
  543. if strings.Contains(ip, ":") {
  544. proxy["ipv6"] = ip
  545. } else {
  546. proxy["ip"] = ip
  547. }
  548. }
  549. if mtu, ok := inboundSettings["mtu"].(float64); ok && mtu > 0 {
  550. proxy["mtu"] = int(mtu)
  551. }
  552. if dns, _ := inboundSettings["dns"].(string); dns != "" {
  553. servers := make([]string, 0)
  554. for server := range strings.SplitSeq(dns, ",") {
  555. if server = strings.TrimSpace(server); server != "" {
  556. servers = append(servers, server)
  557. }
  558. }
  559. if len(servers) > 0 {
  560. proxy["dns"] = servers
  561. }
  562. }
  563. return proxy
  564. }
  565. func (s *SubClashService) buildTuicProxy(subReq *SubService, inbound *model.Inbound, client model.Client, ep map[string]any) map[string]any {
  566. inst, ok := tuic.InstanceFromInbound(inbound)
  567. if !ok {
  568. return nil
  569. }
  570. uuid := client.ID
  571. password := client.Password
  572. for _, c := range inst.Clients {
  573. if c.Email == client.Email {
  574. if uuid == "" {
  575. uuid = c.UUID
  576. }
  577. if password == "" {
  578. password = c.Password
  579. }
  580. break
  581. }
  582. }
  583. if uuid == "" || password == "" {
  584. return nil
  585. }
  586. server := inbound.Listen
  587. if server == "" || server == "0.0.0.0" || server == "::" {
  588. server = subReq.resolveInboundAddress(inbound)
  589. }
  590. proxy := map[string]any{
  591. "name": subReq.endpointRemark(inbound, client.Email, ep, "tuic"),
  592. "type": "tuic",
  593. "server": server,
  594. "port": inbound.Port,
  595. "uuid": uuid,
  596. "password": password,
  597. "congestion-controller": inst.CongestionControl,
  598. "udp-relay-mode": inst.UDPRelayMode,
  599. "reduce-rtt": inst.ZeroRTTHandshake,
  600. }
  601. if len(inst.ALPN) > 0 {
  602. proxy["alpn"] = inst.ALPN
  603. }
  604. if inst.SNI != "" {
  605. proxy["sni"] = inst.SNI
  606. }
  607. if sni, ok := externalProxySNI(ep); ok {
  608. proxy["sni"] = sni
  609. }
  610. if alpn, ok := externalProxyALPN(ep["alpn"]); ok {
  611. proxy["alpn"] = strings.Split(alpn, ",")
  612. }
  613. if ai, ok := ep["allowInsecure"].(bool); ok && ai {
  614. proxy["skip-cert-verify"] = true
  615. }
  616. return proxy
  617. }
  618. // amneziaWGClientAddresses prefers this inbound's own settings entry over the
  619. // shared clients.wg_allowed_ips column, which for an identity attached to both
  620. // a wireguard and an amneziawg inbound holds the other one's address.
  621. func amneziaWGClientAddresses(settingsClients []model.Client, client model.Client) []string {
  622. for i := range settingsClients {
  623. if !strings.EqualFold(settingsClients[i].Email, client.Email) {
  624. continue
  625. }
  626. if len(settingsClients[i].AllowedIPs) > 0 {
  627. return settingsClients[i].AllowedIPs
  628. }
  629. break
  630. }
  631. return client.AllowedIPs
  632. }
  633. // allBareIPs reports whether every entry is a plain IP address — no port,
  634. // scheme, and no zone, which mihomo brackets into a udp:// URL it then rejects.
  635. func allBareIPs(servers []string) bool {
  636. for _, s := range servers {
  637. addr, err := netip.ParseAddr(s)
  638. if err != nil || addr.Zone() != "" {
  639. return false
  640. }
  641. }
  642. return true
  643. }
  644. // buildAmneziaWGProxy emits a mihomo Clash entry for an AmneziaWG inbound:
  645. // type stays "wireguard", the obfuscation rides in amnezia-wg-option.
  646. func (s *SubClashService) buildAmneziaWGProxy(subReq *SubService, inbound *model.Inbound, client model.Client, ep map[string]any) map[string]any {
  647. if client.PrivateKey == "" {
  648. return nil
  649. }
  650. var parsed amneziawg.InboundSettings
  651. if err := json.Unmarshal([]byte(inbound.Settings), &parsed); err != nil || parsed.Server == nil {
  652. return nil
  653. }
  654. server := parsed.Server
  655. proxy := map[string]any{
  656. "name": subReq.endpointRemark(inbound, client.Email, ep, ""),
  657. "type": "wireguard",
  658. "server": inbound.Listen,
  659. "port": inbound.Port,
  660. "udp": true,
  661. "private-key": client.PrivateKey,
  662. }
  663. if server.PublicKey != "" {
  664. proxy["public-key"] = server.PublicKey
  665. }
  666. if client.PreSharedKey != "" {
  667. proxy["pre-shared-key"] = client.PreSharedKey
  668. }
  669. if ka := client.KeepAliveSeconds(); ka > 0 {
  670. proxy["persistent-keepalive"] = ka
  671. }
  672. for _, addr := range amneziaWGClientAddresses(parsed.Clients, client) {
  673. ip := stripCIDR(addr)
  674. if ip == "" {
  675. continue
  676. }
  677. if strings.Contains(ip, ":") {
  678. proxy["ipv6"] = ip
  679. } else {
  680. proxy["ip"] = ip
  681. }
  682. }
  683. // Always emitted: mihomo's own 1408 default sits above the interface
  684. // amneziawgnet actually runs once s4 passes 12, so the tunnel fragments.
  685. proxy["mtu"] = amneziawg.EffectiveMTU(server.MTU, server.S4)
  686. var dns []string
  687. if server.PrimaryDNS != "" {
  688. dns = append(dns, server.PrimaryDNS)
  689. }
  690. if server.SecondaryDNS != "" {
  691. dns = append(dns, server.SecondaryDNS)
  692. }
  693. if len(dns) > 0 {
  694. proxy["dns"] = dns
  695. // mihomo ignores dns without this flag, but aborts the whole config on
  696. // a value its dns.ParseNameServer rejects, so only bare IPs opt in.
  697. if allBareIPs(dns) {
  698. proxy["remote-dns-resolve"] = true
  699. }
  700. }
  701. awg := map[string]any{}
  702. if server.Jc != 0 {
  703. awg["jc"] = server.Jc
  704. }
  705. if server.Jmin != 0 {
  706. awg["jmin"] = server.Jmin
  707. }
  708. if server.Jmax != 0 {
  709. awg["jmax"] = server.Jmax
  710. }
  711. if server.S1 != 0 {
  712. awg["s1"] = server.S1
  713. }
  714. if server.S2 != 0 {
  715. awg["s2"] = server.S2
  716. }
  717. if server.S3 != 0 {
  718. awg["s3"] = server.S3
  719. }
  720. if server.S4 != 0 {
  721. awg["s4"] = server.S4
  722. }
  723. if server.H1 != "" {
  724. awg["h1"] = server.H1
  725. }
  726. if server.H2 != "" {
  727. awg["h2"] = server.H2
  728. }
  729. if server.H3 != "" {
  730. awg["h3"] = server.H3
  731. }
  732. if server.H4 != "" {
  733. awg["h4"] = server.H4
  734. }
  735. for i, v := range []string{server.I1, server.I2, server.I3, server.I4, server.I5} {
  736. if v != "" {
  737. awg[fmt.Sprintf("i%d", i+1)] = v
  738. }
  739. }
  740. needsV3 := false
  741. if server.HeaderProtectionKey != "" {
  742. awg["header-protection-key"] = server.HeaderProtectionKey
  743. needsV3 = true
  744. }
  745. if server.ContentPaddingAddition != "" {
  746. awg["content-padding-addition"] = server.ContentPaddingAddition
  747. needsV3 = true
  748. }
  749. if server.RekeyAfterTime != "" {
  750. awg["rekey-after-time"] = server.RekeyAfterTime
  751. needsV3 = true
  752. }
  753. if server.RekeyTimeout != "" {
  754. awg["rekey-timeout"] = server.RekeyTimeout
  755. needsV3 = true
  756. }
  757. if server.RejectAfterTime != "" {
  758. awg["reject-after-time"] = server.RejectAfterTime
  759. needsV3 = true
  760. }
  761. if server.KeepaliveTimeout != "" {
  762. awg["keepalive-timeout"] = server.KeepaliveTimeout
  763. needsV3 = true
  764. }
  765. if server.MaxHandshakeAttempts != "" {
  766. awg["max-handshake-attempts"] = server.MaxHandshakeAttempts
  767. needsV3 = true
  768. }
  769. if server.RandomTrailers {
  770. awg["random-trailers"] = true
  771. needsV3 = true
  772. }
  773. if server.DisableCookies {
  774. awg["disable-cookies"] = true
  775. needsV3 = true
  776. }
  777. if needsV3 {
  778. awg["version"] = 3
  779. }
  780. if len(awg) > 0 {
  781. proxy["amnezia-wg-option"] = awg
  782. }
  783. return proxy
  784. }
  785. // buildXhttpClashOpts converts xhttpSettings from 3x-ui's camelCase JSON
  786. // storage into the kebab-case map that Mihomo expects under xhttp-opts.
  787. //
  788. // Only client-relevant fields are included (allowlist approach).
  789. // Server-only fields (noSSEHeader, scMaxBufferedPosts, scStreamUpServerSecs,
  790. // serverMaxHeaderBytes) are automatically excluded because they are not in
  791. // the mapping. This is intentional — when Mihomo adds new fields, the mapping
  792. // must be updated explicitly rather than leaking unverified fields to clients.
  793. //
  794. // Returns nil if no non-trivial fields are present.
  795. func buildXhttpClashOpts(xhttp map[string]any) map[string]any {
  796. if xhttp == nil {
  797. return nil
  798. }
  799. opts := map[string]any{}
  800. // Direct fields: path, mode
  801. if v, ok := xhttp["path"].(string); ok && v != "" {
  802. opts["path"] = v
  803. }
  804. if v, ok := xhttp["mode"].(string); ok && v != "" {
  805. opts["mode"] = v
  806. }
  807. // Host: explicit host field wins, then fall back to headers.Host
  808. host := ""
  809. if v, ok := xhttp["host"].(string); ok && v != "" {
  810. host = v
  811. } else if headers, ok := xhttp["headers"].(map[string]any); ok {
  812. host = searchHost(headers)
  813. }
  814. if host != "" {
  815. opts["host"] = host
  816. }
  817. type xhttpStringField struct{ src, dst, skipValue string }
  818. stringFields := []xhttpStringField{
  819. {"xPaddingBytes", "x-padding-bytes", ""},
  820. {"uplinkHTTPMethod", "uplink-http-method", ""},
  821. {"sessionIDPlacement", "session-id-placement", ""},
  822. {"sessionIDKey", "session-id-key", ""},
  823. {"sessionIDTable", "session-id-table", ""},
  824. {"sessionIDLength", "session-id-length", ""},
  825. {"seqPlacement", "seq-placement", ""},
  826. {"seqKey", "seq-key", ""},
  827. {"uplinkDataPlacement", "uplink-data-placement", ""},
  828. {"uplinkDataKey", "uplink-data-key", ""},
  829. {"scMaxEachPostBytes", "sc-max-each-post-bytes", "1000000"},
  830. {"scMinPostsIntervalMs", "sc-min-posts-interval-ms", "30"},
  831. }
  832. for _, f := range stringFields {
  833. if v, ok := xhttp[f.src].(string); ok && v != "" && (f.skipValue == "" || v != f.skipValue) {
  834. opts[f.dst] = v
  835. }
  836. }
  837. // Legacy inbounds (pre xray-core #6258) stored sessionPlacement/sessionKey.
  838. // Fall back to them so not-yet-resaved configs still map. Mirrors the
  839. // frontend migration.
  840. for _, f := range []xhttpStringField{
  841. {"sessionPlacement", "session-id-placement", ""},
  842. {"sessionKey", "session-id-key", ""},
  843. } {
  844. if _, exists := opts[f.dst]; exists {
  845. continue
  846. }
  847. if v, ok := xhttp[f.src].(string); ok && v != "" {
  848. opts[f.dst] = v
  849. }
  850. }
  851. // Bool fields (truthy only)
  852. if v, ok := xhttp["noGRPCHeader"].(bool); ok && v {
  853. opts["no-grpc-header"] = true
  854. }
  855. if v, ok := xhttp["xPaddingObfsMode"].(bool); ok && v {
  856. opts["x-padding-obfs-mode"] = true
  857. // Padding obfs gated fields
  858. for _, field := range []struct{ src, dst string }{
  859. {"xPaddingKey", "x-padding-key"},
  860. {"xPaddingHeader", "x-padding-header"},
  861. {"xPaddingPlacement", "x-padding-placement"},
  862. {"xPaddingMethod", "x-padding-method"},
  863. } {
  864. if v, ok := xhttp[field.src].(string); ok && v != "" {
  865. opts[field.dst] = v
  866. }
  867. }
  868. }
  869. // Non-zero value fields
  870. if v, ok := nonZeroShareValue(xhttp["uplinkChunkSize"]); ok {
  871. opts["uplink-chunk-size"] = v
  872. }
  873. // Nested object: xmux → reuse-settings
  874. if xmux, ok := xhttp["xmux"].(map[string]any); ok && len(xmux) > 0 {
  875. reuse := map[string]any{}
  876. for _, f := range []struct{ src, dst string }{
  877. {"maxConcurrency", "max-concurrency"},
  878. {"maxConnections", "max-connections"},
  879. {"cMaxReuseTimes", "c-max-reuse-times"},
  880. {"hMaxRequestTimes", "h-max-request-times"},
  881. {"hMaxReusableSecs", "h-max-reusable-secs"},
  882. } {
  883. if v, ok := xmux[f.src].(string); ok && v != "" {
  884. reuse[f.dst] = v
  885. }
  886. }
  887. if v, ok := nonZeroShareValue(xmux["hKeepAlivePeriod"]); ok {
  888. reuse["h-keep-alive-period"] = v
  889. }
  890. if len(reuse) > 0 {
  891. opts["reuse-settings"] = reuse
  892. }
  893. }
  894. // Headers (drop Host key)
  895. if rawHeaders, ok := xhttp["headers"].(map[string]any); ok && len(rawHeaders) > 0 {
  896. out := map[string]any{}
  897. for k, v := range rawHeaders {
  898. if strings.EqualFold(k, "host") {
  899. continue
  900. }
  901. out[k] = v
  902. }
  903. if len(out) > 0 {
  904. opts["headers"] = out
  905. }
  906. }
  907. if len(opts) == 0 {
  908. return nil
  909. }
  910. return opts
  911. }
  912. func (s *SubClashService) applyTransport(proxy map[string]any, network string, stream map[string]any) bool {
  913. switch network {
  914. case "", "tcp":
  915. proxy["network"] = "tcp"
  916. tcp, _ := stream["tcpSettings"].(map[string]any)
  917. if tcp != nil {
  918. header, _ := tcp["header"].(map[string]any)
  919. if header != nil {
  920. typeStr, _ := header["type"].(string)
  921. if typeStr != "" && typeStr != "none" {
  922. return false
  923. }
  924. }
  925. }
  926. return true
  927. case "ws":
  928. proxy["network"] = "ws"
  929. ws, _ := stream["wsSettings"].(map[string]any)
  930. wsOpts := map[string]any{}
  931. if ws != nil {
  932. if path, ok := ws["path"].(string); ok && path != "" {
  933. wsOpts["path"] = path
  934. }
  935. host := ""
  936. if v, ok := ws["host"].(string); ok && v != "" {
  937. host = v
  938. } else if headers, ok := ws["headers"].(map[string]any); ok {
  939. host = searchHost(headers)
  940. }
  941. if host != "" {
  942. wsOpts["headers"] = map[string]any{"Host": host}
  943. }
  944. }
  945. if len(wsOpts) > 0 {
  946. proxy["ws-opts"] = wsOpts
  947. }
  948. return true
  949. case "grpc":
  950. proxy["network"] = "grpc"
  951. grpc, _ := stream["grpcSettings"].(map[string]any)
  952. grpcOpts := map[string]any{}
  953. if grpc != nil {
  954. if serviceName, ok := grpc["serviceName"].(string); ok && serviceName != "" {
  955. grpcOpts["grpc-service-name"] = serviceName
  956. }
  957. }
  958. if len(grpcOpts) > 0 {
  959. proxy["grpc-opts"] = grpcOpts
  960. }
  961. return true
  962. case "httpupgrade":
  963. proxy["network"] = "httpupgrade"
  964. hu, _ := stream["httpupgradeSettings"].(map[string]any)
  965. opts := map[string]any{}
  966. if hu != nil {
  967. if path, ok := hu["path"].(string); ok && path != "" {
  968. opts["path"] = path
  969. }
  970. host := ""
  971. if v, ok := hu["host"].(string); ok && v != "" {
  972. host = v
  973. } else if headers, ok := hu["headers"].(map[string]any); ok {
  974. host = searchHost(headers)
  975. }
  976. if host != "" {
  977. opts["headers"] = map[string]any{"Host": host}
  978. }
  979. }
  980. if len(opts) > 0 {
  981. proxy["http-upgrade-opts"] = opts
  982. }
  983. return true
  984. case "xhttp":
  985. proxy["network"] = "xhttp"
  986. xhttp, _ := stream["xhttpSettings"].(map[string]any)
  987. opts := buildXhttpClashOpts(xhttp)
  988. if opts != nil {
  989. proxy["xhttp-opts"] = opts
  990. }
  991. return true
  992. default:
  993. return false
  994. }
  995. }
  996. func (s *SubClashService) applySecurity(proxy map[string]any, security string, stream map[string]any) bool {
  997. switch security {
  998. case "", "none":
  999. proxy["tls"] = false
  1000. return true
  1001. case "tls":
  1002. proxy["tls"] = true
  1003. tlsSettings, _ := stream["tlsSettings"].(map[string]any)
  1004. if tlsSettings != nil {
  1005. if serverName, ok := tlsSettings["serverName"].(string); ok && serverName != "" {
  1006. proxy["servername"] = serverName
  1007. switch proxy["type"] {
  1008. case "trojan":
  1009. proxy["sni"] = serverName
  1010. }
  1011. }
  1012. if fingerprint, ok := tlsSettings["fingerprint"].(string); ok && fingerprint != "" {
  1013. proxy["client-fingerprint"] = fingerprint
  1014. }
  1015. if alpn, ok := externalProxyALPNList(tlsSettings["alpn"]); ok {
  1016. out := make([]string, 0, len(alpn))
  1017. for _, item := range alpn {
  1018. if s, ok := item.(string); ok && s != "" {
  1019. out = append(out, s)
  1020. }
  1021. }
  1022. if len(out) > 0 {
  1023. proxy["alpn"] = out
  1024. }
  1025. }
  1026. if inner, ok := tlsSettings["settings"].(map[string]any); ok {
  1027. if insecure, ok := inner["allowInsecure"].(bool); ok && insecure {
  1028. proxy["skip-cert-verify"] = true
  1029. }
  1030. }
  1031. if pins, ok := tlsSettings["pin-sha256"].([]any); ok && len(pins) > 0 {
  1032. proxy["pin-sha256"] = pins
  1033. }
  1034. }
  1035. return true
  1036. case "reality":
  1037. proxy["tls"] = true
  1038. realitySettings, _ := stream["realitySettings"].(map[string]any)
  1039. if realitySettings == nil {
  1040. return false
  1041. }
  1042. if serverName, ok := realitySettings["serverName"].(string); ok && serverName != "" {
  1043. proxy["servername"] = serverName
  1044. }
  1045. realityOpts := map[string]any{}
  1046. if publicKey, ok := realitySettings["publicKey"].(string); ok && publicKey != "" {
  1047. realityOpts["public-key"] = publicKey
  1048. }
  1049. if shortID, ok := realitySettings["shortId"].(string); ok && shortID != "" {
  1050. realityOpts["short-id"] = shortID
  1051. }
  1052. if len(realityOpts) > 0 {
  1053. // Xray 26.9.8+ rejects REALITY handshakes without an ML-KEM key share.
  1054. realityOpts["support-x25519mlkem768"] = true
  1055. proxy["reality-opts"] = realityOpts
  1056. }
  1057. proxy["client-fingerprint"] = "chrome"
  1058. if fingerprint, ok := realitySettings["fingerprint"].(string); ok && fingerprint != "" {
  1059. proxy["client-fingerprint"] = fingerprint
  1060. }
  1061. return true
  1062. default:
  1063. return false
  1064. }
  1065. }
  1066. func (s *SubClashService) streamData(stream string) map[string]any {
  1067. var streamSettings map[string]any
  1068. _ = json.Unmarshal([]byte(stream), &streamSettings)
  1069. security, _ := streamSettings["security"].(string)
  1070. switch security {
  1071. case "tls":
  1072. if tlsSettings, ok := streamSettings["tlsSettings"].(map[string]any); ok {
  1073. streamSettings["tlsSettings"] = s.tlsData(tlsSettings)
  1074. }
  1075. case "reality":
  1076. if realitySettings, ok := streamSettings["realitySettings"].(map[string]any); ok {
  1077. streamSettings["realitySettings"] = s.realityData(realitySettings)
  1078. }
  1079. }
  1080. delete(streamSettings, "sockopt")
  1081. return streamSettings
  1082. }
  1083. func (s *SubClashService) tlsData(tData map[string]any) map[string]any {
  1084. tlsData := make(map[string]any, 1)
  1085. tlsClientSettings, _ := tData["settings"].(map[string]any)
  1086. tlsData["serverName"] = tData["serverName"]
  1087. tlsData["alpn"] = tData["alpn"]
  1088. if fingerprint, ok := tlsClientSettings["fingerprint"].(string); ok {
  1089. tlsData["fingerprint"] = fingerprint
  1090. }
  1091. if pins, ok := tlsClientSettings["pinnedPeerCertSha256"].([]any); ok && len(pins) > 0 {
  1092. tlsData["pin-sha256"] = pins
  1093. }
  1094. return tlsData
  1095. }
  1096. func (s *SubClashService) realityData(rData map[string]any) map[string]any {
  1097. rDataOut := make(map[string]any, 1)
  1098. realityClientSettings, _ := rData["settings"].(map[string]any)
  1099. if publicKey, ok := realityClientSettings["publicKey"].(string); ok {
  1100. rDataOut["publicKey"] = publicKey
  1101. }
  1102. if fingerprint, ok := realityClientSettings["fingerprint"].(string); ok {
  1103. rDataOut["fingerprint"] = fingerprint
  1104. }
  1105. if serverNames, ok := rData["serverNames"].([]any); ok && len(serverNames) > 0 {
  1106. rDataOut["serverName"] = fmt.Sprint(serverNames[0])
  1107. }
  1108. if shortIDs, ok := rData["shortIds"].([]any); ok && len(shortIDs) > 0 {
  1109. rDataOut["shortId"] = fmt.Sprint(shortIDs[0])
  1110. }
  1111. return rDataOut
  1112. }
  1113. func cloneMap(src map[string]any) map[string]any {
  1114. if src == nil {
  1115. return nil
  1116. }
  1117. dst := make(map[string]any, len(src))
  1118. maps.Copy(dst, src)
  1119. return dst
  1120. }
  1121. func mergeClashRulesYAML(base map[string]any, raw string) error {
  1122. raw = strings.TrimSpace(raw)
  1123. if raw == "" {
  1124. return nil
  1125. }
  1126. var custom any
  1127. if err := yaml.Unmarshal([]byte(raw), &custom); err != nil {
  1128. mergeClashRules(base, linesToClashRules(raw))
  1129. return nil
  1130. }
  1131. switch typed := custom.(type) {
  1132. case []any:
  1133. mergeClashRules(base, typed)
  1134. case map[string]any:
  1135. for key, value := range typed {
  1136. if key == "rules" {
  1137. if ruleList, ok := asAnySlice(value); ok {
  1138. mergeClashRules(base, ruleList)
  1139. }
  1140. continue
  1141. }
  1142. base[key] = value
  1143. }
  1144. default:
  1145. mergeClashRules(base, linesToClashRules(raw))
  1146. }
  1147. return nil
  1148. }
  1149. // mergeRemoteClashRules lets remote update only the route graph (see
  1150. // remoteClashAllowedKey) and never mutates remote: cached documents are shared.
  1151. func mergeRemoteClashRules(base map[string]any, remote map[string]any) error {
  1152. if len(remote) == 0 {
  1153. return fmt.Errorf("remote Clash routing source must be a YAML map")
  1154. }
  1155. for key, value := range remote {
  1156. if !remoteClashAllowedKey(key) {
  1157. continue
  1158. }
  1159. if err := validateRemoteClashValue(key, value); err != nil {
  1160. return err
  1161. }
  1162. switch key {
  1163. case "rules":
  1164. rules, _ := asAnySlice(value)
  1165. mergeClashRules(base, rules)
  1166. case "proxy-groups":
  1167. groups, _ := asAnySlice(value)
  1168. base["proxy-groups"] = mergeClashProxyGroups(base["proxy-groups"], groups)
  1169. default:
  1170. base[key] = value
  1171. }
  1172. }
  1173. return validateClashRouteGraph(base)
  1174. }
  1175. func validateRemoteClashValue(key string, value any) error {
  1176. switch key {
  1177. case "rules":
  1178. rules, ok := asAnySlice(value)
  1179. if !ok {
  1180. return fmt.Errorf("remote Clash rules must be a list")
  1181. }
  1182. for _, rule := range rules {
  1183. text, ok := rule.(string)
  1184. if !ok || strings.TrimSpace(text) == "" {
  1185. return fmt.Errorf("remote Clash rules must contain non-empty strings")
  1186. }
  1187. }
  1188. case "proxy-groups":
  1189. groups, ok := asAnySlice(value)
  1190. if !ok {
  1191. return fmt.Errorf("remote Clash proxy-groups must be a list")
  1192. }
  1193. seen := make(map[string]struct{}, len(groups))
  1194. for _, groupValue := range groups {
  1195. group, ok := groupValue.(map[string]any)
  1196. if !ok {
  1197. return fmt.Errorf("remote Clash proxy-groups must contain named group maps with a type")
  1198. }
  1199. name, nameOK := group["name"].(string)
  1200. groupType, typeOK := group["type"].(string)
  1201. if !nameOK || !typeOK || strings.TrimSpace(name) == "" || strings.TrimSpace(groupType) == "" {
  1202. return fmt.Errorf("remote Clash proxy-groups must contain named group maps with a type")
  1203. }
  1204. name = strings.TrimSpace(name)
  1205. if _, duplicate := seen[name]; duplicate {
  1206. return fmt.Errorf("remote Clash proxy-group name %q is duplicated", name)
  1207. }
  1208. seen[name] = struct{}{}
  1209. if useValue, exists := group["use"]; exists {
  1210. use, ok := asAnySlice(useValue)
  1211. if !ok || len(use) > 0 {
  1212. return fmt.Errorf("remote Clash proxy-group %q cannot use proxy-providers", name)
  1213. }
  1214. }
  1215. }
  1216. case "rule-providers":
  1217. providers, ok := value.(map[string]any)
  1218. if !ok {
  1219. return fmt.Errorf("remote Clash rule-providers must be a map")
  1220. }
  1221. for name, provider := range providers {
  1222. if strings.TrimSpace(name) == "" {
  1223. return fmt.Errorf("remote Clash rule-provider name must not be empty")
  1224. }
  1225. if _, ok := provider.(map[string]any); !ok {
  1226. return fmt.Errorf("remote Clash rule-provider %q must be a map", name)
  1227. }
  1228. }
  1229. }
  1230. return nil
  1231. }
  1232. func remoteClashAllowedKey(key string) bool {
  1233. switch key {
  1234. case "proxy-groups", "rule-providers", "rules":
  1235. return true
  1236. default:
  1237. return false
  1238. }
  1239. }
  1240. func validateClashRouteGraph(config map[string]any) error {
  1241. known := map[string]struct{}{
  1242. "DIRECT": {}, "REJECT": {}, "REJECT-DROP": {}, "REJECT-TINYGIF": {}, "PASS": {}, "GLOBAL": {},
  1243. }
  1244. if proxies, ok := asAnySlice(config["proxies"]); ok {
  1245. for _, value := range proxies {
  1246. proxy, ok := value.(map[string]any)
  1247. if !ok {
  1248. continue
  1249. }
  1250. if name, ok := proxy["name"].(string); ok && strings.TrimSpace(name) != "" {
  1251. known[strings.TrimSpace(name)] = struct{}{}
  1252. }
  1253. }
  1254. }
  1255. groups, _ := asAnySlice(config["proxy-groups"])
  1256. for _, value := range groups {
  1257. if name := clashProxyGroupName(value); name != "" {
  1258. known[name] = struct{}{}
  1259. }
  1260. }
  1261. for _, value := range groups {
  1262. group, ok := value.(map[string]any)
  1263. if !ok {
  1264. continue
  1265. }
  1266. name := clashProxyGroupName(group)
  1267. refs, exists := group["proxies"]
  1268. if !exists {
  1269. continue
  1270. }
  1271. proxies, ok := asAnySlice(refs)
  1272. if !ok {
  1273. return fmt.Errorf("Clash proxy-group %q proxies must be a list", name)
  1274. }
  1275. for _, refValue := range proxies {
  1276. ref, ok := refValue.(string)
  1277. if !ok || strings.TrimSpace(ref) == "" {
  1278. return fmt.Errorf("Clash proxy-group %q contains an invalid proxy reference", name)
  1279. }
  1280. ref = strings.TrimSpace(ref)
  1281. if _, exists := known[ref]; !exists {
  1282. return fmt.Errorf("Clash proxy-group %q references unknown proxy or group %q", name, ref)
  1283. }
  1284. }
  1285. }
  1286. providers, _ := config["rule-providers"].(map[string]any)
  1287. for providerName, value := range providers {
  1288. provider, ok := value.(map[string]any)
  1289. if !ok {
  1290. continue
  1291. }
  1292. via, ok := provider["proxy"].(string)
  1293. if !ok || strings.TrimSpace(via) == "" {
  1294. continue
  1295. }
  1296. via = strings.TrimSpace(via)
  1297. if _, exists := known[via]; !exists {
  1298. return fmt.Errorf("Clash rule-provider %q references unknown proxy or group %q", providerName, via)
  1299. }
  1300. }
  1301. rules, _ := asAnySlice(config["rules"])
  1302. for _, value := range rules {
  1303. rule, ok := value.(string)
  1304. if !ok || strings.TrimSpace(rule) == "" {
  1305. return errors.New("Clash rules must contain non-empty strings")
  1306. }
  1307. parts := strings.Split(rule, ",")
  1308. for i := range parts {
  1309. parts[i] = strings.TrimSpace(parts[i])
  1310. }
  1311. if len(parts) < 2 {
  1312. return fmt.Errorf("invalid Clash rule %q", rule)
  1313. }
  1314. if strings.EqualFold(parts[0], "RULE-SET") {
  1315. if len(parts) < 3 {
  1316. return fmt.Errorf("invalid Clash RULE-SET rule %q", rule)
  1317. }
  1318. if _, exists := providers[parts[1]]; !exists {
  1319. return fmt.Errorf("Clash rule references unknown rule-provider %q", parts[1])
  1320. }
  1321. }
  1322. targetIndex := len(parts) - 1
  1323. // Mihomo IP rules may carry trailing no-resolve / src option flags.
  1324. for targetIndex >= 1 && (strings.EqualFold(parts[targetIndex], "no-resolve") || strings.EqualFold(parts[targetIndex], "src")) {
  1325. targetIndex--
  1326. }
  1327. if targetIndex < 1 {
  1328. return fmt.Errorf("invalid Clash rule target in %q", rule)
  1329. }
  1330. target := parts[targetIndex]
  1331. if _, exists := known[target]; !exists {
  1332. return fmt.Errorf("Clash rule references unknown proxy or group %q", target)
  1333. }
  1334. }
  1335. return nil
  1336. }
  1337. func mergeClashProxyGroups(baseValue any, remoteGroups []any) []any {
  1338. baseGroups, _ := asAnySlice(baseValue)
  1339. baseByName := make(map[string]any, len(baseGroups))
  1340. baseOrder := make([]string, 0, len(baseGroups))
  1341. for _, group := range baseGroups {
  1342. name := clashProxyGroupName(group)
  1343. if name == "" {
  1344. continue
  1345. }
  1346. baseByName[name] = group
  1347. baseOrder = append(baseOrder, name)
  1348. }
  1349. merged := make([]any, 0, len(remoteGroups)+len(baseGroups))
  1350. seen := make(map[string]struct{}, len(remoteGroups)+len(baseGroups))
  1351. for _, group := range remoteGroups {
  1352. name := clashProxyGroupName(group)
  1353. if name == "" {
  1354. continue
  1355. }
  1356. if _, duplicate := seen[name]; duplicate {
  1357. continue
  1358. }
  1359. seen[name] = struct{}{}
  1360. merged = append(merged, group)
  1361. }
  1362. for _, name := range baseOrder {
  1363. if _, replaced := seen[name]; replaced {
  1364. continue
  1365. }
  1366. merged = append(merged, baseByName[name])
  1367. }
  1368. return merged
  1369. }
  1370. func clashProxyGroupName(value any) string {
  1371. group, ok := value.(map[string]any)
  1372. if !ok {
  1373. return ""
  1374. }
  1375. name, _ := group["name"].(string)
  1376. return strings.TrimSpace(name)
  1377. }
  1378. func mergeClashRules(base map[string]any, customRules []any) {
  1379. if len(customRules) == 0 {
  1380. return
  1381. }
  1382. baseRules, _ := asAnySlice(base["rules"])
  1383. if hasClashMatchRule(customRules) {
  1384. base["rules"] = customRules
  1385. return
  1386. }
  1387. merged := make([]any, 0, len(customRules)+len(baseRules))
  1388. merged = append(merged, customRules...)
  1389. merged = append(merged, baseRules...)
  1390. base["rules"] = merged
  1391. }
  1392. func asAnySlice(value any) ([]any, bool) {
  1393. switch typed := value.(type) {
  1394. case []any:
  1395. return typed, true
  1396. case []string:
  1397. out := make([]any, 0, len(typed))
  1398. for _, item := range typed {
  1399. out = append(out, item)
  1400. }
  1401. return out, true
  1402. case []map[string]any:
  1403. out := make([]any, 0, len(typed))
  1404. for _, item := range typed {
  1405. out = append(out, item)
  1406. }
  1407. return out, true
  1408. default:
  1409. return nil, false
  1410. }
  1411. }
  1412. func hasClashMatchRule(rules []any) bool {
  1413. for _, rule := range rules {
  1414. ruleText, ok := rule.(string)
  1415. if !ok {
  1416. continue
  1417. }
  1418. parts := strings.SplitN(ruleText, ",", 2)
  1419. if strings.EqualFold(strings.TrimSpace(parts[0]), "MATCH") {
  1420. return true
  1421. }
  1422. }
  1423. return false
  1424. }
  1425. func linesToClashRules(raw string) []any {
  1426. lines := strings.Split(raw, "\n")
  1427. rules := make([]any, 0, len(lines))
  1428. for _, line := range lines {
  1429. line = strings.TrimSpace(line)
  1430. if line == "" || strings.HasPrefix(line, "#") {
  1431. continue
  1432. }
  1433. rules = append(rules, line)
  1434. }
  1435. return rules
  1436. }