1
0

clash_service.go 43 KB

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