clash_service.go 44 KB

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