fix(link): restore mKCP seed and headerType on share-link import (#6480)

* fix(link): restore mKCP seed and headerType on share-link import

applyTransport / applyTransportParams ignored kcp query params that
applyKcpShareParams emits, so re-imported outbounds lost seed and
header and could not talk to the inbound. Mirror those fields (plus
mtu/tti) into kcpSettings in both Go and TS importers.

Fixes #6476

* fix(link): restore mKCP header/seed via finalmask mkcp-legacy

* fix(link): split mKCP header and seed into separate masks on import

Both importers folded a share link's headerType and seed into one
mkcp-legacy mask {header, value}. xray-core's MkcpLegacy.Build ignores
value once header is set (and reads it as the fake DNS domain for
header=dns), so an imported outbound carried the header mask but no
AES-128-GCM seed while the emitting inbound has both, and could not
connect — the failure #6476 reports, now for every link carrying both
params. Emit one mask per field, seed first: the finalmask array's
first item is the innermost layer, which puts the header around the
cipher as legacy mKCP did.

Also bound mtu/tti to KCPConfig.Build's accepted ranges (mtu >= 21,
tti 10..1000, decimal digits only on both importers) so a pasted link
cannot fail the whole Xray config load, and look header types up as
own properties so a prototype key such as "constructor" is not mapped.

---------

Co-authored-by: mrchatam <287639636+mrchatam@users.noreply.github.com>
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
This commit is contained in:
mrchatam
2026-09-13 23:58:13 +03:30
committed by GitHub
parent 939c470698
commit 5ad9df69b9
4 changed files with 299 additions and 8 deletions
+90
View File
@@ -704,6 +704,15 @@ func applyTransport(stream map[string]any, p url.Values) {
xh[k] = v
}
}
case "kcp":
// mtu/tti live on kcpSettings; header/seed are finalmask mkcp-legacy (see applyMkcpLegacyFromShare).
kcp := stream["kcpSettings"].(map[string]any)
if n, ok := kcpParamInRange(p.Get("mtu"), kcpMinMTU, kcpMaxMTU); ok {
kcp["mtu"] = n
}
if n, ok := kcpParamInRange(p.Get("tti"), kcpMinTTI, kcpMaxTTI); ok {
kcp["tti"] = n
}
case "tcp":
if p.Get("headerType") == "http" || p.Get("type") == "http" {
stream["tcpSettings"] = map[string]any{
@@ -753,6 +762,87 @@ func applyFinalMask(stream map[string]any, p url.Values) {
stream["finalmask"] = parsed
}
}
applyMkcpLegacyFromShare(stream, p)
}
// mKCP bounds mirror xray-core's KCPConfig.Build checks (a value outside them fails
// the whole config load); mtu's ceiling is the int32 that fits its uint32 field everywhere.
const (
kcpMinMTU = 21
kcpMaxMTU = math.MaxInt32
kcpMinTTI = 10
kcpMaxTTI = 1000
)
// kcpParamInRange rejects an out-of-range or malformed link value so buildStream's default stays.
func kcpParamInRange(s string, minVal, maxVal int) (int, bool) {
n, err := strconv.Atoi(s)
return n, err == nil && n >= minVal && n <= maxVal
}
// kcpHeaderTypeToMask maps share-link headerType to mkcp-legacy settings.header
// (inverse of sub.kcpMaskToHeaderType).
var kcpHeaderTypeToMask = map[string]string{
"dns": "dns",
"dtls": "dtls",
"srtp": "srtp",
"utp": "utp",
"wechat-video": "wechat",
"wireguard": "wireguard",
}
// applyMkcpLegacyFromShare restores headerType/seed into finalmask.udp mkcp-legacy,
// matching the shape InboundFormModal / FinalMaskForm emit. fm= mkcp-legacy wins.
func applyMkcpLegacyFromShare(stream map[string]any, p url.Values) {
headerType := strings.TrimSpace(p.Get("headerType"))
seed := p.Get("seed")
if headerType == "" || headerType == "none" {
headerType = ""
}
if headerType == "" && seed == "" {
return
}
if network, _ := stream["network"].(string); network != "" && network != "kcp" {
return
}
maskHeader := ""
if headerType != "" {
mapped, ok := kcpHeaderTypeToMask[headerType]
if !ok {
return
}
maskHeader = mapped
}
finalmask, _ := stream["finalmask"].(map[string]any)
if finalmask == nil {
finalmask = map[string]any{}
}
udp, _ := finalmask["udp"].([]any)
for _, raw := range udp {
m, _ := raw.(map[string]any)
if m != nil {
if t, _ := m["type"].(string); t == "mkcp-legacy" {
return // fm= (or prior) already carries the live mask
}
}
}
// One mask per field, seed first: MkcpLegacy.Build ignores value once header is set,
// and the chain puts the last mask outermost on the wire (header around the cipher).
if seed != "" {
udp = append(udp, mkcpLegacyMask("", seed))
}
if maskHeader != "" {
udp = append(udp, mkcpLegacyMask(maskHeader, ""))
}
finalmask["udp"] = udp
stream["finalmask"] = finalmask
}
func mkcpLegacyMask(header, value string) map[string]any {
return map[string]any{
"type": "mkcp-legacy",
"settings": map[string]any{"header": header, "value": value},
}
}
// gecko packetSize bounds mirror xray-core's salamander buffer cap.
@@ -4,6 +4,7 @@ import (
"encoding/base64"
"net/url"
"reflect"
"slices"
"testing"
)
@@ -244,3 +245,76 @@ func TestParseTrojanAndSS_CoreFields(t *testing.T) {
t.Errorf("ss server = %#v", ssrv)
}
}
type mkcpMask struct{ header, value string }
func mkcpLegacyMasks(t *testing.T, res *ParseResult) []mkcpMask {
t.Helper()
var out []mkcpMask
for _, raw := range finalmaskUDP(t, res) {
mask, _ := raw.(map[string]any)
if mask["type"] != "mkcp-legacy" {
t.Fatalf("unexpected udp mask %#v", mask)
}
settings, _ := mask["settings"].(map[string]any)
header, _ := settings["header"].(string)
value, _ := settings["value"].(string)
out = append(out, mkcpMask{header, value})
}
return out
}
func TestParse_KcpShareParams(t *testing.T) {
// The emitter flattens one mkcp-legacy mask per field into headerType/seed; a merged
// mask drops the seed in xray-core (MkcpLegacy.Build), so import rebuilds them separately.
cases := []struct {
name string
link string
wantMTU int
wantTTI int
wantMasks []mkcpMask
}{
{
name: "vless header and seed become two masks, seed first",
link: "vless://uuid@h.com:443?type=kcp&headerType=wechat-video&seed=secret-seed&mtu=1400&tti=50&security=none#kcp1",
wantMTU: 1400,
wantTTI: 50,
wantMasks: []mkcpMask{{"", "secret-seed"}, {"wechat", ""}},
},
{
name: "trojan header only adds no seed mask",
link: "trojan://pw@h.com:443?type=kcp&headerType=srtp&security=none#kcp-tj",
wantMTU: 1350,
wantTTI: 20,
wantMasks: []mkcpMask{{"srtp", ""}},
},
{
name: "seed only adds no header mask",
link: "vless://uuid@h.com:443?type=kcp&headerType=none&seed=abc123&security=none",
wantMTU: 1350,
wantTTI: 20,
wantMasks: []mkcpMask{{"", "abc123"}},
},
{
name: "mtu/tti outside KCPConfig.Build bounds keep the defaults",
link: "vless://uuid@h.com:443?type=kcp&mtu=10&tti=5000&security=none",
wantMTU: 1350,
wantTTI: 20,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
res, err := ParseLink(c.link)
if err != nil {
t.Fatalf("parse: %v", err)
}
kcp := streamSub(t, res, "kcpSettings")
if kcp["mtu"] != c.wantMTU || kcp["tti"] != c.wantTTI {
t.Fatalf("kcpSettings mtu/tti = %v/%v, want %d/%d", kcp["mtu"], kcp["tti"], c.wantMTU, c.wantTTI)
}
if got := mkcpLegacyMasks(t, res); !slices.Equal(got, c.wantMasks) {
t.Fatalf("mkcp-legacy masks = %v, want %v", got, c.wantMasks)
}
})
}
}