1
0

clash_service.go 33 KB

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