outbound.go 33 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219
  1. // Package link provides parsers for VPN share links (vmess://, vless://, etc.)
  2. // and subscription bodies (typically base64-encoded newline lists of such links).
  3. // The output shape matches the wire format used by the panel's Xray template
  4. // outbounds array so that parsed objects can be injected directly.
  5. package link
  6. import (
  7. "encoding/base64"
  8. "encoding/json"
  9. "fmt"
  10. "maps"
  11. "math"
  12. "net/url"
  13. "regexp"
  14. "strconv"
  15. "strings"
  16. "time"
  17. "github.com/mhsanaei/3x-ui/v3/internal/util/maskcompat"
  18. )
  19. // Outbound is the minimal shape we emit for each parsed link.
  20. // Extra fields (mux, etc.) are carried inside settings/streamSettings.
  21. type Outbound map[string]any
  22. // ParseResult holds a parsed outbound together with a stable identity string
  23. // that can be used to correlate the same logical server across refreshes
  24. // (even if the remark changes).
  25. type ParseResult struct {
  26. Outbound Outbound
  27. Identity string
  28. }
  29. // ParseSubscriptionBody accepts the raw body returned by a subscription URL.
  30. // It handles the common case where the body is a base64-encoded blob of
  31. // newline-separated links, and also tolerates an already-decoded text body.
  32. // It returns the list of successfully parsed outbounds (in order) and their
  33. // corresponding identities.
  34. func ParseSubscriptionBody(body []byte) ([]Outbound, []string, error) {
  35. text := strings.TrimSpace(string(body))
  36. if text == "" {
  37. return nil, nil, nil
  38. }
  39. // Try base64 decode first (standard and URL-safe variants).
  40. if decoded, ok := tryBase64(text); ok {
  41. text = strings.TrimSpace(decoded)
  42. }
  43. lines := splitLines(text)
  44. var outbounds []Outbound
  45. var identities []string
  46. seen := map[string]int{}
  47. for _, ln := range lines {
  48. ln = strings.TrimSpace(ln)
  49. if ln == "" || strings.HasPrefix(ln, "#") {
  50. continue
  51. }
  52. res, err := ParseLink(ln)
  53. if err != nil || res == nil {
  54. // Ignore unparseable lines (comments, unsupported protocols, etc.)
  55. continue
  56. }
  57. identity := res.Identity
  58. // A repeated identity would share one stored tag, shifting both tags on every refresh.
  59. if n := seen[res.Identity]; n > 0 {
  60. identity = fmt.Sprintf("%s#%d", res.Identity, n)
  61. }
  62. seen[res.Identity]++
  63. outbounds = append(outbounds, res.Outbound)
  64. identities = append(identities, identity)
  65. }
  66. return outbounds, identities, nil
  67. }
  68. func tryBase64(s string) (string, bool) {
  69. // Remove whitespace that some providers insert.
  70. clean := strings.Map(func(r rune) rune {
  71. if r == ' ' || r == '\n' || r == '\r' || r == '\t' {
  72. return -1
  73. }
  74. return r
  75. }, s)
  76. // Common padding fix
  77. for len(clean)%4 != 0 {
  78. clean += "="
  79. }
  80. // Standard
  81. if b, err := base64.StdEncoding.DecodeString(clean); err == nil {
  82. return string(b), true
  83. }
  84. // URL-safe (no padding)
  85. if b, err := base64.RawURLEncoding.DecodeString(clean); err == nil {
  86. return string(b), true
  87. }
  88. // URL-safe with padding
  89. if b, err := base64.URLEncoding.DecodeString(clean); err == nil {
  90. return string(b), true
  91. }
  92. return "", false
  93. }
  94. func splitLines(s string) []string {
  95. // Accept \n, \r\n, and also some providers use literal \n in the text.
  96. s = strings.ReplaceAll(s, `\n`, "\n")
  97. return strings.FieldsFunc(s, func(r rune) bool { return r == '\n' || r == '\r' })
  98. }
  99. // ParseLink parses a single share link and returns the outbound object plus
  100. // a stable identity for tag correlation. Supported schemes:
  101. // - vmess://
  102. // - vless://
  103. // - trojan://
  104. // - ss:// (modern and legacy)
  105. // - hysteria2:// (also hy2://)
  106. // - wireguard:// (also wg://)
  107. func ParseLink(link string) (*ParseResult, error) {
  108. link = strings.TrimSpace(link)
  109. switch {
  110. case strings.HasPrefix(link, "vmess://"):
  111. return parseVmess(link)
  112. case strings.HasPrefix(link, "vless://"):
  113. return parseVless(link)
  114. case strings.HasPrefix(link, "trojan://"):
  115. return parseTrojan(link)
  116. case strings.HasPrefix(link, "ss://"):
  117. return parseShadowsocks(link)
  118. case strings.HasPrefix(link, "hysteria2://"), strings.HasPrefix(link, "hy2://"):
  119. return parseHysteria2(link)
  120. case strings.HasPrefix(link, "wireguard://"), strings.HasPrefix(link, "wg://"):
  121. return parseWireguard(link)
  122. default:
  123. return nil, fmt.Errorf("unsupported link scheme")
  124. }
  125. }
  126. // --- vmess ---
  127. func parseVmess(link string) (*ParseResult, error) {
  128. b64 := strings.TrimPrefix(link, "vmess://")
  129. // vmess:// base64(json)
  130. raw, err := base64.StdEncoding.DecodeString(padBase64(b64))
  131. if err != nil {
  132. // Some providers use raw URL-safe
  133. raw, err = base64.RawURLEncoding.DecodeString(b64)
  134. }
  135. if err != nil {
  136. return nil, fmt.Errorf("vmess decode: %w", err)
  137. }
  138. var j map[string]any
  139. if err := json.Unmarshal(raw, &j); err != nil {
  140. return nil, fmt.Errorf("vmess json: %w", err)
  141. }
  142. identity := vmessIdentity(j)
  143. network := getString(j, "net", "tcp")
  144. security := "none"
  145. if tls, _ := j["tls"].(string); tls == "tls" {
  146. security = "tls"
  147. }
  148. stream := buildStream(network, security)
  149. // Map known fields (best effort, matching frontend parser coverage)
  150. switch network {
  151. case "ws":
  152. host, _ := j["host"].(string)
  153. setWS(stream, host, getString(j, "path", "/"))
  154. case "grpc":
  155. svc := getString(j, "path", "")
  156. if auth, ok := j["authority"].(string); ok && auth != "" {
  157. stream["grpcSettings"].(map[string]any)["authority"] = auth
  158. }
  159. stream["grpcSettings"].(map[string]any)["serviceName"] = svc
  160. stream["grpcSettings"].(map[string]any)["multiMode"] = getString(j, "type", "") == "multi"
  161. case "httpupgrade":
  162. setHTTPUpgrade(stream, getString(j, "host", ""), getString(j, "path", "/"))
  163. case "xhttp":
  164. xh := stream["xhttpSettings"].(map[string]any)
  165. xh["host"] = getString(j, "host", "")
  166. xh["path"] = getString(j, "path", "/")
  167. if m := getString(j, "mode", ""); m != "" {
  168. xh["mode"] = m
  169. }
  170. // xhttp advanced keys are passed through if present in the json
  171. for _, k := range []string{"xPaddingBytes", "scMaxEachPostBytes", "scMinPostsIntervalMs"} {
  172. if v, ok := j[k]; ok {
  173. xh[k] = v
  174. }
  175. }
  176. case "tcp":
  177. if getString(j, "type", "") == "http" {
  178. stream["tcpSettings"] = map[string]any{
  179. "header": map[string]any{
  180. "type": "http",
  181. "request": map[string]any{
  182. "version": "1.1",
  183. "method": "GET",
  184. "path": splitComma(getString(j, "path", "/")),
  185. "headers": map[string]any{"Host": splitComma(getString(j, "host", ""))},
  186. },
  187. },
  188. }
  189. }
  190. case "kcp":
  191. applyTransport(stream, url.Values{"mtu": {strconv.Itoa(num(j["mtu"]))}, "tti": {strconv.Itoa(num(j["tti"]))}})
  192. }
  193. // The vmess object carries fm as-is and the mKCP header and seed as type and path.
  194. maskParams := url.Values{"fm": {getString(j, "fm", "")}}
  195. if network == "kcp" {
  196. maskParams.Set("headerType", getString(j, "type", ""))
  197. maskParams.Set("seed", getString(j, "path", ""))
  198. }
  199. applyFinalMask(stream, maskParams)
  200. if security == "tls" {
  201. tls := stream["tlsSettings"].(map[string]any)
  202. tls["serverName"] = getString(j, "sni", "")
  203. tls["fingerprint"] = getString(j, "fp", "")
  204. if alpn := getString(j, "alpn", ""); alpn != "" {
  205. tls["alpn"] = splitComma(alpn)
  206. }
  207. // The vmess object names the certificate checks v2rayN does the same way
  208. // the url-param protocols name them in applySecurity.
  209. tls["echConfigList"] = getString(j, "ech", "")
  210. tls["verifyPeerCertByName"] = getString(j, "vcn", "")
  211. tls["pinnedPeerCertSha256"] = getString(j, "pcs", "")
  212. }
  213. port := num(j["port"])
  214. scy := getString(j, "scy", "auto")
  215. if scy == "none" || scy == "zero" {
  216. scy = "auto"
  217. }
  218. ob := Outbound{
  219. "protocol": "vmess",
  220. "tag": getString(j, "ps", ""),
  221. "settings": map[string]any{
  222. "vnext": []any{
  223. map[string]any{
  224. "address": getString(j, "add", ""),
  225. "port": port,
  226. "users": []any{
  227. map[string]any{
  228. "id": getString(j, "id", ""),
  229. "security": scy,
  230. },
  231. },
  232. },
  233. },
  234. },
  235. "streamSettings": stream,
  236. }
  237. return &ParseResult{Outbound: ob, Identity: identity}, nil
  238. }
  239. func vmessIdentity(j map[string]any) string {
  240. // Remove ps (remark) for identity
  241. core := map[string]any{}
  242. for k, v := range j {
  243. if k == "ps" {
  244. continue
  245. }
  246. core[k] = v
  247. }
  248. b, _ := json.Marshal(core)
  249. return "vmess:" + string(b)
  250. }
  251. // --- vless / trojan (URL forms) ---
  252. func parseVless(link string) (*ParseResult, error) {
  253. u, err := url.Parse(link)
  254. if err != nil {
  255. return nil, err
  256. }
  257. if u.Scheme != "vless" {
  258. return nil, fmt.Errorf("not vless")
  259. }
  260. id := u.User.Username()
  261. host := u.Hostname()
  262. port := defaultPort(u.Port(), 443)
  263. params := u.Query()
  264. network := params.Get("type")
  265. if network == "" {
  266. network = "tcp"
  267. }
  268. security := params.Get("security")
  269. if security == "" {
  270. security = "none"
  271. }
  272. stream := buildStream(network, security)
  273. applyTransport(stream, params)
  274. applySecurity(stream, params)
  275. applyFinalMask(stream, params)
  276. identity := "vless:" + u.Scheme + "://" + id + "@" + host + ":" + strconv.Itoa(port) + "?" + canonicalQuery(params)
  277. ob := Outbound{
  278. "protocol": "vless",
  279. "tag": decodeHash(u.Fragment),
  280. "settings": map[string]any{
  281. "address": host,
  282. "port": port,
  283. "id": id,
  284. "flow": params.Get("flow"),
  285. "encryption": firstNonEmpty(params.Get("encryption"), "none"),
  286. },
  287. "streamSettings": stream,
  288. }
  289. return &ParseResult{Outbound: ob, Identity: identity}, nil
  290. }
  291. func parseTrojan(link string) (*ParseResult, error) {
  292. u, err := url.Parse(link)
  293. if err != nil {
  294. return nil, err
  295. }
  296. if u.Scheme != "trojan" {
  297. return nil, fmt.Errorf("not trojan")
  298. }
  299. pw := u.User.Username()
  300. host := u.Hostname()
  301. port := defaultPort(u.Port(), 443)
  302. params := u.Query()
  303. network := params.Get("type")
  304. if network == "" {
  305. network = "tcp"
  306. }
  307. security := params.Get("security")
  308. if security == "" {
  309. security = "tls"
  310. }
  311. stream := buildStream(network, security)
  312. applyTransport(stream, params)
  313. applySecurity(stream, params)
  314. applyFinalMask(stream, params)
  315. identity := "trojan:" + u.Scheme + "://" + pw + "@" + host + ":" + strconv.Itoa(port) + "?" + canonicalQuery(params)
  316. ob := Outbound{
  317. "protocol": "trojan",
  318. "tag": decodeHash(u.Fragment),
  319. "settings": map[string]any{
  320. "servers": []any{
  321. map[string]any{"address": host, "port": port, "password": pw},
  322. },
  323. },
  324. "streamSettings": stream,
  325. }
  326. return &ParseResult{Outbound: ob, Identity: identity}, nil
  327. }
  328. // --- shadowsocks ---
  329. func parseShadowsocks(link string) (*ParseResult, error) {
  330. // Two shapes:
  331. // ss://base64(method:pass)@host:port#remark
  332. // ss://base64(method:pass@host:port)#remark
  333. // Query may carry Xray-native stream params (type/security/sni/alpn/fp)
  334. // emitted by genShadowsocksLink — preserve them like trojan/vless.
  335. remark := ""
  336. if i := strings.Index(link, "#"); i >= 0 {
  337. remark, _ = url.QueryUnescape(link[i+1:])
  338. link = link[:i]
  339. }
  340. rawQuery := ""
  341. if i := strings.Index(link, "?"); i >= 0 {
  342. rawQuery = link[i+1:]
  343. link = link[:i]
  344. }
  345. params, _ := url.ParseQuery(rawQuery)
  346. core := strings.TrimPrefix(link, "ss://")
  347. at := strings.Index(core, "@")
  348. var host, method, pass string
  349. var port int
  350. if at >= 0 {
  351. // modern
  352. userB64 := core[:at]
  353. hp := strings.TrimRight(core[at+1:], "/")
  354. userInfo, err := base64DecodeFlexible(userB64)
  355. if err != nil {
  356. // SIP022 (2022-blake3-*) userinfo is percent-encoded, not base64.
  357. if dec, uerr := url.QueryUnescape(userB64); uerr == nil {
  358. userInfo = dec
  359. } else {
  360. userInfo = userB64 // not b64, rare
  361. }
  362. }
  363. colon := strings.LastIndex(hp, ":")
  364. if colon < 0 {
  365. return nil, fmt.Errorf("bad ss host:port")
  366. }
  367. host = hp[:colon]
  368. port, err = strconv.Atoi(hp[colon+1:])
  369. if err != nil {
  370. return nil, fmt.Errorf("bad ss port %q: %w", hp[colon+1:], err)
  371. }
  372. method, pass = splitMethodPass(userInfo)
  373. } else {
  374. // legacy: whole thing b64
  375. dec, err := base64DecodeFlexible(core)
  376. if err != nil {
  377. return nil, err
  378. }
  379. at = strings.Index(dec, "@")
  380. if at < 0 {
  381. return nil, fmt.Errorf("bad legacy ss")
  382. }
  383. userInfo := dec[:at]
  384. hp := dec[at+1:]
  385. colon := strings.LastIndex(hp, ":")
  386. if colon < 0 {
  387. return nil, fmt.Errorf("bad legacy ss hp")
  388. }
  389. host = hp[:colon]
  390. port, err = strconv.Atoi(hp[colon+1:])
  391. if err != nil {
  392. return nil, fmt.Errorf("bad legacy ss port %q: %w", hp[colon+1:], err)
  393. }
  394. method, pass = splitMethodPass(userInfo)
  395. }
  396. identity := "ss:" + method + ":" + pass + "@" + host + ":" + strconv.Itoa(port)
  397. // The panel and v2rayN express shadowsocks tcp/http obfuscation only as the
  398. // SIP002 plugin, so it has to become the header it stands for.
  399. applyObfsLocalPlugin(params, rawQuery)
  400. network := params.Get("type")
  401. if network == "" {
  402. network = "tcp"
  403. }
  404. security := params.Get("security")
  405. if security == "" {
  406. security = "none"
  407. }
  408. stream := buildStream(network, security)
  409. applyTransport(stream, params)
  410. applySecurity(stream, params)
  411. applyFinalMask(stream, params)
  412. ob := Outbound{
  413. "protocol": "shadowsocks",
  414. "tag": remark,
  415. "settings": map[string]any{
  416. "servers": []any{
  417. map[string]any{"address": host, "port": port, "password": pass, "method": method},
  418. },
  419. },
  420. "streamSettings": stream,
  421. }
  422. return &ParseResult{Outbound: ob, Identity: identity}, nil
  423. }
  424. func splitMethodPass(userInfo string) (string, string) {
  425. before, after, ok := strings.Cut(userInfo, ":")
  426. if !ok {
  427. return "2022-blake3-aes-128-gcm", userInfo // guess
  428. }
  429. return before, after
  430. }
  431. // applyObfsLocalPlugin maps a SIP002 obfs-local=http plugin onto the tcp/http
  432. // response header it stands for; the other plugin values have no Xray header.
  433. func applyObfsLocalPlugin(p url.Values, rawQuery string) {
  434. if p.Get("headerType") != "" || p.Get("type") == "http" {
  435. return
  436. }
  437. plugin := p.Get("plugin")
  438. if plugin == "" {
  439. plugin = rawQueryPlugin(rawQuery)
  440. }
  441. parts := strings.Split(plugin, ";")
  442. if len(parts) == 0 || parts[0] != "obfs-local" {
  443. return
  444. }
  445. obfs, host := "", ""
  446. for _, part := range parts[1:] {
  447. if k, v, ok := strings.Cut(part, "="); ok {
  448. switch k {
  449. case "obfs":
  450. obfs = v
  451. case "obfs-host":
  452. host = v
  453. }
  454. }
  455. }
  456. if obfs != "http" {
  457. return
  458. }
  459. p.Set("type", "tcp")
  460. p.Set("headerType", "http")
  461. if host != "" {
  462. p.Set("host", host)
  463. }
  464. }
  465. // rawQueryPlugin reads the plugin parameter straight out of the query string for
  466. // the pair stdlib discards: a value holding an unencoded semicolon never parses.
  467. func rawQueryPlugin(rawQuery string) string {
  468. for _, segment := range strings.Split(rawQuery, "&") {
  469. if key, value, ok := strings.Cut(segment, "="); ok && key == "plugin" {
  470. if decoded, err := url.QueryUnescape(value); err == nil {
  471. return decoded
  472. }
  473. }
  474. }
  475. return ""
  476. }
  477. // --- hysteria2 ---
  478. func parseHysteria2(link string) (*ParseResult, error) {
  479. u, err := url.Parse(link)
  480. if err != nil {
  481. return nil, err
  482. }
  483. if u.Scheme != "hysteria2" && u.Scheme != "hy2" {
  484. return nil, fmt.Errorf("not hysteria2")
  485. }
  486. auth := u.User.Username()
  487. host := u.Hostname()
  488. port := defaultPort(u.Port(), 443)
  489. params := u.Query()
  490. stream := map[string]any{
  491. "network": "hysteria",
  492. "security": "tls",
  493. "hysteriaSettings": map[string]any{
  494. "version": 2,
  495. "auth": auth,
  496. "udpIdleTimeout": 60,
  497. },
  498. "tlsSettings": map[string]any{
  499. "serverName": params.Get("sni"),
  500. "alpn": splitCommaOrDefault(params.Get("alpn"), []string{"h3"}),
  501. "fingerprint": params.Get("fp"),
  502. "echConfigList": params.Get("ech"),
  503. "verifyPeerCertByName": params.Get("vcn"),
  504. "pinnedPeerCertSha256": params.Get("pinSHA256"),
  505. },
  506. }
  507. applyFinalMask(stream, params)
  508. applyHysteria2Obfs(stream, params)
  509. applyHysteria2Hop(stream, params)
  510. identity := "hysteria2:" + auth + "@" + host + ":" + strconv.Itoa(port) + "?" + canonicalQuery(params)
  511. ob := Outbound{
  512. "protocol": "hysteria",
  513. "tag": decodeHash(u.Fragment),
  514. "settings": map[string]any{"address": host, "port": port, "version": 2},
  515. "streamSettings": stream,
  516. }
  517. return &ParseResult{Outbound: ob, Identity: identity}, nil
  518. }
  519. // --- wireguard ---
  520. func parseWireguard(link string) (*ParseResult, error) {
  521. u, err := url.Parse(link)
  522. if err != nil {
  523. return nil, err
  524. }
  525. if u.Scheme != "wireguard" && u.Scheme != "wg" {
  526. return nil, fmt.Errorf("not wireguard")
  527. }
  528. secret, _ := url.QueryUnescape(u.User.Username())
  529. params := u.Query()
  530. host := u.Hostname()
  531. portStr := u.Port()
  532. endpoint := host
  533. if portStr != "" {
  534. endpoint = host + ":" + portStr
  535. }
  536. addrRaw := firstParam(params, "address", "ip")
  537. allowedRaw := firstParam(params, "allowedips", "allowed_ips")
  538. addrs := splitComma(addrRaw)
  539. if len(addrs) == 0 {
  540. addrs = []string{"0.0.0.0/0", "::/0"}
  541. }
  542. allowed := splitComma(allowedRaw)
  543. if len(allowed) == 0 {
  544. allowed = []string{"0.0.0.0/0", "::/0"}
  545. }
  546. peer := map[string]any{
  547. "publicKey": firstParam(params, "publickey", "publicKey", "public_key", "peerPublicKey"),
  548. "endpoint": endpoint,
  549. "allowedIPs": allowed,
  550. }
  551. if psk := firstParam(params, "presharedkey", "preshared_key", "pre-shared-key", "psk"); psk != "" {
  552. peer["preSharedKey"] = psk
  553. }
  554. if ka := firstParam(params, "keepalive", "persistentkeepalive", "persistent_keepalive"); ka != "" {
  555. if n, err := strconv.Atoi(ka); err == nil {
  556. peer["keepAlive"] = n
  557. }
  558. }
  559. settings := map[string]any{
  560. "secretKey": secret,
  561. "address": addrs,
  562. "peers": []any{peer},
  563. }
  564. if mtu := params.Get("mtu"); mtu != "" {
  565. if n, err := strconv.Atoi(mtu); err == nil {
  566. settings["mtu"] = n
  567. }
  568. }
  569. if res := params.Get("reserved"); res != "" {
  570. parts := splitComma(res)
  571. var iv []int
  572. for _, p := range parts {
  573. if n, err := strconv.Atoi(strings.TrimSpace(p)); err == nil {
  574. iv = append(iv, n)
  575. }
  576. }
  577. if len(iv) > 0 {
  578. settings["reserved"] = iv
  579. }
  580. }
  581. identity := "wireguard:" + secret + "@" + endpoint + "?" + canonicalQuery(params)
  582. ob := Outbound{
  583. "protocol": "wireguard",
  584. "tag": decodeHash(u.Fragment),
  585. "settings": settings,
  586. }
  587. return &ParseResult{Outbound: ob, Identity: identity}, nil
  588. }
  589. // --- helpers ---
  590. func buildStream(network, security string) map[string]any {
  591. stream := map[string]any{"network": network, "security": security}
  592. switch network {
  593. case "tcp":
  594. stream["tcpSettings"] = map[string]any{"header": map[string]any{"type": "none"}}
  595. case "kcp":
  596. stream["kcpSettings"] = map[string]any{
  597. "mtu": 1350, "tti": 20, "uplinkCapacity": 5, "downlinkCapacity": 20,
  598. "cwndMultiplier": 1, "maxSendingWindow": 2097152,
  599. }
  600. case "ws":
  601. stream["wsSettings"] = map[string]any{"path": "/", "host": "", "headers": map[string]any{}, "heartbeatPeriod": 0}
  602. case "grpc":
  603. stream["grpcSettings"] = map[string]any{"serviceName": "", "authority": "", "multiMode": false}
  604. case "httpupgrade":
  605. stream["httpupgradeSettings"] = map[string]any{"path": "/", "host": "", "headers": map[string]any{}}
  606. case "xhttp":
  607. // No scMaxEachPostBytes/scMinPostsIntervalMs seed: xray-core's own
  608. // defaults apply, and the literal values fingerprint traffic (#5141).
  609. stream["xhttpSettings"] = map[string]any{
  610. "path": "/", "host": "", "mode": "auto", "headers": map[string]any{},
  611. "xPaddingBytes": "100-1000",
  612. }
  613. default:
  614. stream["tcpSettings"] = map[string]any{"header": map[string]any{"type": "none"}}
  615. }
  616. switch security {
  617. case "tls":
  618. stream["tlsSettings"] = map[string]any{
  619. "serverName": "", "alpn": []any{}, "fingerprint": "",
  620. "echConfigList": "", "verifyPeerCertByName": "", "pinnedPeerCertSha256": "",
  621. }
  622. case "reality":
  623. stream["realitySettings"] = map[string]any{
  624. "publicKey": "", "fingerprint": "chrome", "serverName": "",
  625. "shortId": "", "spiderX": "", "mldsa65Verify": "",
  626. }
  627. }
  628. return stream
  629. }
  630. func setWS(stream map[string]any, host, path string) {
  631. ws := stream["wsSettings"].(map[string]any)
  632. ws["host"] = host
  633. ws["path"] = path
  634. }
  635. func setHTTPUpgrade(stream map[string]any, host, path string) {
  636. h := stream["httpupgradeSettings"].(map[string]any)
  637. h["host"] = host
  638. h["path"] = path
  639. }
  640. func applyTransport(stream map[string]any, p url.Values) {
  641. net := stream["network"].(string)
  642. host := p.Get("host")
  643. path := firstNonEmpty(p.Get("path"), "/")
  644. switch net {
  645. case "ws":
  646. setWS(stream, host, path)
  647. case "grpc":
  648. gs := stream["grpcSettings"].(map[string]any)
  649. gs["serviceName"] = firstNonEmpty(p.Get("serviceName"), p.Get("path"))
  650. gs["authority"] = p.Get("authority")
  651. gs["multiMode"] = p.Get("mode") == "multi"
  652. case "httpupgrade":
  653. setHTTPUpgrade(stream, host, path)
  654. case "xhttp":
  655. xh := stream["xhttpSettings"].(map[string]any)
  656. xh["host"] = host
  657. xh["path"] = path
  658. if m := p.Get("mode"); m != "" {
  659. xh["mode"] = m
  660. }
  661. if v := p.Get("x_padding_bytes"); v != "" {
  662. xh["xPaddingBytes"] = v
  663. }
  664. if extra := p.Get("extra"); extra != "" {
  665. var parsed map[string]any
  666. if err := json.Unmarshal([]byte(extra), &parsed); err == nil {
  667. maps.Copy(xh, parsed)
  668. }
  669. }
  670. for _, k := range []string{"xPaddingBytes", "scMaxEachPostBytes", "scMinPostsIntervalMs", "uplinkChunkSize"} {
  671. if v := p.Get(k); v != "" {
  672. xh[k] = v
  673. }
  674. }
  675. case "kcp":
  676. // mtu/tti live on kcpSettings; header/seed are finalmask mkcp-legacy (see applyMkcpLegacyFromShare).
  677. kcp := stream["kcpSettings"].(map[string]any)
  678. if n, ok := kcpParamInRange(p.Get("mtu"), kcpMinMTU, kcpMaxMTU); ok {
  679. kcp["mtu"] = n
  680. }
  681. if n, ok := kcpParamInRange(p.Get("tti"), kcpMinTTI, kcpMaxTTI); ok {
  682. kcp["tti"] = n
  683. }
  684. case "tcp":
  685. if p.Get("headerType") == "http" || p.Get("type") == "http" {
  686. stream["tcpSettings"] = map[string]any{
  687. "header": map[string]any{
  688. "type": "http",
  689. "request": map[string]any{
  690. "version": "1.1",
  691. "method": "GET",
  692. "path": splitComma(path),
  693. "headers": map[string]any{"Host": splitComma(host)},
  694. },
  695. },
  696. }
  697. }
  698. }
  699. }
  700. func applySecurity(stream map[string]any, p url.Values) {
  701. sec := stream["security"].(string)
  702. switch sec {
  703. case "tls":
  704. tls := stream["tlsSettings"].(map[string]any)
  705. tls["serverName"] = p.Get("sni")
  706. tls["fingerprint"] = p.Get("fp")
  707. if alpn := p.Get("alpn"); alpn != "" {
  708. tls["alpn"] = splitComma(alpn)
  709. }
  710. tls["echConfigList"] = p.Get("ech")
  711. tls["verifyPeerCertByName"] = p.Get("vcn")
  712. tls["pinnedPeerCertSha256"] = p.Get("pcs")
  713. case "reality":
  714. re := stream["realitySettings"].(map[string]any)
  715. re["serverName"] = p.Get("sni")
  716. re["fingerprint"] = firstNonEmpty(p.Get("fp"), "chrome")
  717. re["publicKey"] = p.Get("pbk")
  718. re["shortId"] = p.Get("sid")
  719. re["spiderX"] = p.Get("spx")
  720. re["mldsa65Verify"] = p.Get("pqv")
  721. }
  722. }
  723. func applyFinalMask(stream map[string]any, p url.Values) {
  724. if fm := p.Get("fm"); fm != "" {
  725. var parsed any
  726. if json.Unmarshal([]byte(fm), &parsed) == nil {
  727. sanitizeFinalMaskQuicParams(parsed)
  728. maskcompat.UpgradeLegacyXdns(parsed)
  729. maskcompat.UpgradeLegacyUDPHop(parsed)
  730. maskcompat.NormalizeUDPHop(parsed)
  731. stream["finalmask"] = parsed
  732. }
  733. }
  734. applyMkcpLegacyFromShare(stream, p)
  735. }
  736. // mKCP bounds mirror xray-core's KCPConfig.Build checks (a value outside them fails
  737. // the whole config load); mtu's ceiling is the int32 that fits its uint32 field everywhere.
  738. const (
  739. kcpMinMTU = 21
  740. kcpMaxMTU = math.MaxInt32
  741. kcpMinTTI = 10
  742. kcpMaxTTI = 1000
  743. )
  744. // kcpParamInRange rejects an out-of-range or malformed link value so buildStream's default stays.
  745. func kcpParamInRange(s string, minVal, maxVal int) (int, bool) {
  746. n, err := strconv.Atoi(s)
  747. return n, err == nil && n >= minVal && n <= maxVal
  748. }
  749. // kcpHeaderTypeToMask maps share-link headerType to mkcp-legacy settings.header
  750. // (inverse of sub.kcpMaskToHeaderType).
  751. var kcpHeaderTypeToMask = map[string]string{
  752. "dns": "dns",
  753. "dtls": "dtls",
  754. "srtp": "srtp",
  755. "utp": "utp",
  756. "wechat-video": "wechat",
  757. "wireguard": "wireguard",
  758. }
  759. // applyMkcpLegacyFromShare restores headerType/seed into finalmask.udp mkcp-legacy,
  760. // matching the shape InboundFormModal / FinalMaskForm emit. fm= mkcp-legacy wins.
  761. func applyMkcpLegacyFromShare(stream map[string]any, p url.Values) {
  762. headerType := strings.TrimSpace(p.Get("headerType"))
  763. seed := p.Get("seed")
  764. if headerType == "" || headerType == "none" {
  765. headerType = ""
  766. }
  767. if headerType == "" && seed == "" {
  768. return
  769. }
  770. if network, _ := stream["network"].(string); network != "" && network != "kcp" {
  771. return
  772. }
  773. maskHeader := ""
  774. if headerType != "" {
  775. mapped, ok := kcpHeaderTypeToMask[headerType]
  776. if !ok {
  777. return
  778. }
  779. maskHeader = mapped
  780. }
  781. finalmask, _ := stream["finalmask"].(map[string]any)
  782. if finalmask == nil {
  783. finalmask = map[string]any{}
  784. }
  785. udp, _ := finalmask["udp"].([]any)
  786. for _, raw := range udp {
  787. m, _ := raw.(map[string]any)
  788. if m != nil {
  789. if t, _ := m["type"].(string); t == "mkcp-legacy" {
  790. return // fm= (or prior) already carries the live mask
  791. }
  792. }
  793. }
  794. // One mask per field, seed first: MkcpLegacy.Build ignores value once header is set,
  795. // and the chain puts the last mask outermost on the wire (header around the cipher).
  796. if seed != "" {
  797. udp = append(udp, mkcpLegacyMask("", seed))
  798. }
  799. if maskHeader != "" {
  800. udp = append(udp, mkcpLegacyMask(maskHeader, ""))
  801. }
  802. finalmask["udp"] = udp
  803. stream["finalmask"] = finalmask
  804. }
  805. func mkcpLegacyMask(header, value string) map[string]any {
  806. return map[string]any{
  807. "type": "mkcp-legacy",
  808. "settings": map[string]any{"header": header, "value": value},
  809. }
  810. }
  811. // gecko packetSize bounds mirror xray-core's salamander buffer cap.
  812. const (
  813. geckoMinPacketSize = 1
  814. geckoMaxPacketSize = 2048
  815. )
  816. // parsePacketSizeRange validates a min/max pair for the Gecko obfs marker.
  817. func parsePacketSizeRange(minStr, maxStr string) (int, int, bool) {
  818. minVal, err1 := strconv.Atoi(minStr)
  819. maxVal, err2 := strconv.Atoi(maxStr)
  820. if err1 != nil || err2 != nil ||
  821. minVal < geckoMinPacketSize || maxVal < minVal || maxVal > geckoMaxPacketSize {
  822. return 0, 0, false
  823. }
  824. return minVal, maxVal, true
  825. }
  826. // applyHysteria2Obfs rebuilds the salamander mask from the standard Hysteria2
  827. // obfs pair. An fm=-carried password wins; gecko adds the packetSize pair.
  828. func applyHysteria2Obfs(stream map[string]any, p url.Values) {
  829. obfs := p.Get("obfs")
  830. isGecko := strings.EqualFold(obfs, "gecko")
  831. if !isGecko && !strings.EqualFold(obfs, "salamander") {
  832. return
  833. }
  834. password := firstParam(p, "obfs-password", "obfs_password", "obfsPassword")
  835. if password == "" {
  836. return
  837. }
  838. packetSize := ""
  839. if isGecko {
  840. // Both halves required with digit+range validation, matching the
  841. // export side; half-specified or non-numeric values are dropped.
  842. minSize := strings.TrimSpace(p.Get("minPacketSize"))
  843. maxSize := strings.TrimSpace(p.Get("maxPacketSize"))
  844. if min, max, ok := parsePacketSizeRange(minSize, maxSize); ok {
  845. packetSize = fmt.Sprintf("%d-%d", min, max)
  846. }
  847. }
  848. finalmask := ensureChildMap(stream, "finalmask")
  849. udp, _ := finalmask["udp"].([]any)
  850. for _, m := range udp {
  851. mask, ok := m.(map[string]any)
  852. if !ok || mask["type"] != "salamander" {
  853. continue
  854. }
  855. settings, ok := mask["settings"].(map[string]any)
  856. if !ok {
  857. settings = map[string]any{}
  858. mask["settings"] = settings
  859. }
  860. if pw, _ := settings["password"].(string); pw == "" {
  861. settings["password"] = password
  862. }
  863. if packetSize != "" {
  864. if ps, _ := settings["packetSize"].(string); ps == "" {
  865. settings["packetSize"] = packetSize
  866. }
  867. }
  868. return
  869. }
  870. settings := map[string]any{"password": password}
  871. if packetSize != "" {
  872. settings["packetSize"] = packetSize
  873. }
  874. finalmask["udp"] = append(udp, map[string]any{
  875. "type": "salamander",
  876. "settings": settings,
  877. })
  878. }
  879. // applyHysteria2Hop rebuilds the UDP port-hopping range from the standard mport
  880. // param. xray-core 26.9.9 replaced finalmask.quicParams.udpHop with a "udphop"
  881. // UDP mask, whose intervalremote mode is what the old key used to do; a mask
  882. // already supplied via fm= wins.
  883. func applyHysteria2Hop(stream map[string]any, p url.Values) {
  884. ports := firstParam(p, "mport")
  885. if ports == "" {
  886. return
  887. }
  888. finalmask := ensureChildMap(stream, "finalmask")
  889. masks, _ := finalmask["udp"].([]any)
  890. for _, rawMask := range masks {
  891. mask, _ := rawMask.(map[string]any)
  892. if maskType, _ := mask["type"].(string); maskType == "udphop" {
  893. return
  894. }
  895. }
  896. finalmask["udp"] = append(masks, map[string]any{
  897. "type": "udphop",
  898. "settings": map[string]any{
  899. "mode": "intervalremote",
  900. "interval": "5-10",
  901. "remotePorts": ports,
  902. },
  903. })
  904. }
  905. func ensureChildMap(parent map[string]any, key string) map[string]any {
  906. m, ok := parent[key].(map[string]any)
  907. if !ok {
  908. m = map[string]any{}
  909. parent[key] = m
  910. }
  911. return m
  912. }
  913. // sanitizeFinalMaskQuicParams coerces the strictly numeric quicParams fields
  914. // of a finalmask blob taken verbatim from a share link's fm= parameter.
  915. // Xray-core rejects the whole config at startup when e.g. keepAlivePeriod
  916. // arrives as a duration string like "10s" or an out-of-range integer, so
  917. // numeric strings are parsed, duration strings are converted to whole
  918. // seconds, the ranged fields are clamped to what xray accepts, and anything
  919. // non-finite, negative, absurdly large, or unparseable is dropped so a bad
  920. // value falls back to xray's default instead of killing the config (#5783).
  921. func sanitizeFinalMaskQuicParams(parsed any) {
  922. fm, ok := parsed.(map[string]any)
  923. if !ok {
  924. return
  925. }
  926. qp, ok := fm["quicParams"].(map[string]any)
  927. if !ok {
  928. return
  929. }
  930. numericKeys := []string{
  931. "initStreamReceiveWindow", "maxStreamReceiveWindow",
  932. "initConnectionReceiveWindow", "maxConnectionReceiveWindow",
  933. "maxIdleTimeout", "keepAlivePeriod", "maxIncomingStreams",
  934. }
  935. for _, key := range numericKeys {
  936. raw, exists := qp[key]
  937. if !exists {
  938. continue
  939. }
  940. n, ok := coerceQuicNumeric(raw)
  941. if ok {
  942. n, ok = clampQuicNumeric(key, n)
  943. }
  944. if !ok {
  945. delete(qp, key)
  946. continue
  947. }
  948. qp[key] = int64(n)
  949. }
  950. }
  951. func coerceQuicNumeric(raw any) (float64, bool) {
  952. switch v := raw.(type) {
  953. case float64:
  954. return math.Trunc(v), true
  955. case string:
  956. if n, err := strconv.ParseFloat(v, 64); err == nil && !math.IsInf(n, 0) && !math.IsNaN(n) {
  957. return math.Trunc(n), true
  958. }
  959. if d, err := time.ParseDuration(v); err == nil {
  960. return math.Trunc(d.Seconds()), true
  961. }
  962. }
  963. return 0, false
  964. }
  965. // clampQuicNumeric enforces xray-core's QuicParamsConfig validation so a
  966. // coerced value cannot still fail the config load: keepAlivePeriod is 0 or
  967. // 2-60, maxIdleTimeout is 0 or 4-120, maxIncomingStreams is 0 or >= 8.
  968. // quicNumericMax keeps values in plain-integer JSON territory and far below
  969. // the uint64 window fields' range.
  970. const quicNumericMax = float64(1e15)
  971. func clampQuicNumeric(key string, n float64) (float64, bool) {
  972. if n < 0 || n > quicNumericMax {
  973. return 0, false
  974. }
  975. if n == 0 {
  976. return 0, true
  977. }
  978. switch key {
  979. case "keepAlivePeriod":
  980. return math.Min(math.Max(n, 2), 60), true
  981. case "maxIdleTimeout":
  982. return math.Min(math.Max(n, 4), 120), true
  983. case "maxIncomingStreams":
  984. return math.Max(n, 8), true
  985. }
  986. return n, true
  987. }
  988. func firstNonEmpty(a, b string) string {
  989. if a != "" {
  990. return a
  991. }
  992. return b
  993. }
  994. func firstParam(p url.Values, keys ...string) string {
  995. for _, k := range keys {
  996. if v := p.Get(k); v != "" {
  997. return v
  998. }
  999. }
  1000. return ""
  1001. }
  1002. // realityPerRequestParams are picked per request by subscription servers (3x-ui randomizes
  1003. // sid/sni, older releases spx too), so they must not split one server into new identities.
  1004. var realityPerRequestParams = map[string]bool{"sid": true, "sni": true, "spx": true}
  1005. func canonicalQuery(p url.Values) string {
  1006. // Sort keys for stable identity
  1007. reality := p.Get("security") == "reality"
  1008. keys := make([]string, 0, len(p))
  1009. for k := range p {
  1010. if reality && realityPerRequestParams[k] {
  1011. continue
  1012. }
  1013. keys = append(keys, k)
  1014. }
  1015. // simple sort
  1016. for i := 0; i < len(keys); i++ {
  1017. for j := i + 1; j < len(keys); j++ {
  1018. if keys[j] < keys[i] {
  1019. keys[i], keys[j] = keys[j], keys[i]
  1020. }
  1021. }
  1022. }
  1023. parts := make([]string, 0, len(keys))
  1024. for _, k := range keys {
  1025. for _, v := range p[k] {
  1026. parts = append(parts, k+"="+v)
  1027. }
  1028. }
  1029. return strings.Join(parts, "&")
  1030. }
  1031. func decodeHash(h string) string {
  1032. if h == "" {
  1033. return ""
  1034. }
  1035. if dec, err := url.QueryUnescape(h); err == nil {
  1036. return dec
  1037. }
  1038. return h
  1039. }
  1040. func defaultPort(p string, def int) int {
  1041. if p == "" {
  1042. return def
  1043. }
  1044. n, err := strconv.Atoi(p)
  1045. if err != nil || n <= 0 {
  1046. return def
  1047. }
  1048. return n
  1049. }
  1050. func num(v any) int {
  1051. switch x := v.(type) {
  1052. case float64:
  1053. return int(x)
  1054. case int:
  1055. return x
  1056. case int64:
  1057. return int(x)
  1058. case string:
  1059. n, _ := strconv.Atoi(x)
  1060. return n
  1061. }
  1062. return 0
  1063. }
  1064. func getString(m map[string]any, key, def string) string {
  1065. if v, ok := m[key]; ok {
  1066. if s, ok := v.(string); ok {
  1067. return s
  1068. }
  1069. }
  1070. return def
  1071. }
  1072. func splitComma(s string) []string {
  1073. if s == "" {
  1074. return nil
  1075. }
  1076. parts := strings.Split(s, ",")
  1077. out := make([]string, 0, len(parts))
  1078. for _, p := range parts {
  1079. p = strings.TrimSpace(p)
  1080. if p != "" {
  1081. out = append(out, p)
  1082. }
  1083. }
  1084. return out
  1085. }
  1086. func splitCommaOrDefault(s string, def []string) []string {
  1087. if s == "" {
  1088. return def
  1089. }
  1090. return splitComma(s)
  1091. }
  1092. func padBase64(s string) string {
  1093. for len(s)%4 != 0 {
  1094. s += "="
  1095. }
  1096. return s
  1097. }
  1098. func base64DecodeFlexible(s string) (string, error) {
  1099. s = padBase64(s)
  1100. if b, err := base64.StdEncoding.DecodeString(s); err == nil {
  1101. return string(b), nil
  1102. }
  1103. if b, err := base64.RawURLEncoding.DecodeString(strings.TrimRight(s, "=")); err == nil {
  1104. return string(b), nil
  1105. }
  1106. return "", fmt.Errorf("base64 decode failed")
  1107. }
  1108. // SlugRemark turns a free-form remark into a tag segment, keeping Unicode
  1109. // letters and digits (so non-ASCII remarks like Cyrillic stay readable) and
  1110. // replacing every other run of characters with a single dash.
  1111. var slugRe = regexp.MustCompile(`[^\p{L}\p{N}]+`)
  1112. func SlugRemark(remark string) string {
  1113. s := strings.ToLower(strings.TrimSpace(remark))
  1114. s = slugRe.ReplaceAllString(s, "-")
  1115. s = strings.Trim(s, "-")
  1116. if s == "" {
  1117. return ""
  1118. }
  1119. // collapse runs of dashes
  1120. for strings.Contains(s, "--") {
  1121. s = strings.ReplaceAll(s, "--", "-")
  1122. }
  1123. return s
  1124. }
  1125. // SuggestTag builds a tag from a prefix and a remark (or index fallback).
  1126. // It is intended for initial assignment; stability is handled by the service layer.
  1127. func SuggestTag(prefix, remark string, idx int) string {
  1128. base := SlugRemark(remark)
  1129. if base == "" {
  1130. base = fmt.Sprintf("%d", idx)
  1131. }
  1132. p := strings.TrimSuffix(prefix, "-")
  1133. if p != "" {
  1134. return p + "-" + base
  1135. }
  1136. return base
  1137. }