clash_service.go 33 KB

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