mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-15 18:35:00 +03:00
fix(link): read fm and the mKCP fields from vmess links (#6809)
#6480 taught the vless/trojan importers to rebuild the mKCP header and seed and left the vmess importers ignoring fm. The vmess emitter writes the finalmask as fm, the mKCP header as type and the seed as path, plus mtu/tti. Both importers dropped all of them, so a vmess mKCP inbound with a seed, a header or any finalmask UDP mask (salamander, ...) came back from an outbound subscription, a client external link or the outbound form's link import as an outbound that cannot connect. Feed those fields through the same finalmask and kcp helpers the url-param protocols use, in the Go and TS importers.
This commit is contained in:
@@ -543,7 +543,20 @@ export function parseVmessLink(link: string): Raw | null {
|
|||||||
xhttp.path = json.path ?? '/';
|
xhttp.path = json.path ?? '/';
|
||||||
if (json.mode) xhttp.mode = json.mode;
|
if (json.mode) xhttp.mode = json.mode;
|
||||||
applyXhttpStringFromJson(xhttp, json);
|
applyXhttpStringFromJson(xhttp, json);
|
||||||
|
} else if (network === 'kcp') {
|
||||||
|
applyTransportParams(
|
||||||
|
stream,
|
||||||
|
new URLSearchParams({ mtu: String(json.mtu ?? ''), tti: String(json.tti ?? '') }),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
// The vmess object carries fm as-is and the mKCP header and seed as type and path.
|
||||||
|
const maskParams = new URLSearchParams();
|
||||||
|
if (typeof json.fm === 'string') maskParams.set('fm', json.fm);
|
||||||
|
if (network === 'kcp') {
|
||||||
|
if (typeof json.type === 'string') maskParams.set('headerType', json.type);
|
||||||
|
if (typeof json.path === 'string') maskParams.set('seed', json.path);
|
||||||
|
}
|
||||||
|
applyFinalMaskParam(stream, maskParams);
|
||||||
if (security === 'tls') {
|
if (security === 'tls') {
|
||||||
const tls = stream.tlsSettings as Raw;
|
const tls = stream.tlsSettings as Raw;
|
||||||
tls.serverName = json.sni ?? '';
|
tls.serverName = json.sni ?? '';
|
||||||
|
|||||||
@@ -305,8 +305,25 @@ describe('mKCP share params', () => {
|
|||||||
// The emitter flattens one mkcp-legacy mask per field into headerType/seed; a merged
|
// 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.
|
// mask drops the seed in xray-core (MkcpLegacy.Build), so import rebuilds them separately.
|
||||||
type Mask = { type: string; settings: { header: string; value: string } };
|
type Mask = { type: string; settings: { header: string; value: string } };
|
||||||
|
const vmessKcp = (fields: Record<string, unknown>) =>
|
||||||
|
'vmess://' +
|
||||||
|
Base64.encode(
|
||||||
|
JSON.stringify({
|
||||||
|
v: '2',
|
||||||
|
add: 'h.com',
|
||||||
|
port: 443,
|
||||||
|
id: '11111111-2222-4333-8444-555555555555',
|
||||||
|
net: 'kcp',
|
||||||
|
tls: 'none',
|
||||||
|
...fields,
|
||||||
|
}),
|
||||||
|
);
|
||||||
const parse = (link: string) => {
|
const parse = (link: string) => {
|
||||||
const out = link.startsWith('trojan://') ? parseTrojanLink(link) : parseVlessLink(link);
|
const out = link.startsWith('trojan://')
|
||||||
|
? parseTrojanLink(link)
|
||||||
|
: link.startsWith('vmess://')
|
||||||
|
? parseVmessLink(link)
|
||||||
|
: parseVlessLink(link);
|
||||||
expect(out).not.toBeNull();
|
expect(out).not.toBeNull();
|
||||||
const stream = out!.streamSettings as Record<string, unknown>;
|
const stream = out!.streamSettings as Record<string, unknown>;
|
||||||
const kcp = stream.kcpSettings as { mtu: number; tti: number };
|
const kcp = stream.kcpSettings as { mtu: number; tti: number };
|
||||||
@@ -357,9 +374,42 @@ describe('mKCP share params', () => {
|
|||||||
{ mtu: 1350, tti: 20 },
|
{ mtu: 1350, tti: 20 },
|
||||||
[],
|
[],
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'vmess header and seed from type and path',
|
||||||
|
vmessKcp({ type: 'wechat-video', path: 'secret-seed', mtu: 1400, tti: 50 }),
|
||||||
|
{ mtu: 1400, tti: 50 },
|
||||||
|
[
|
||||||
|
['mkcp-legacy', '', 'secret-seed'],
|
||||||
|
['mkcp-legacy', 'wechat', ''],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'vmess fm mkcp-legacy mask wins over path',
|
||||||
|
vmessKcp({
|
||||||
|
type: 'none',
|
||||||
|
path: 's3cr3tSeed',
|
||||||
|
mtu: 1350,
|
||||||
|
tti: 20,
|
||||||
|
fm: JSON.stringify({
|
||||||
|
udp: [{ settings: { header: '', value: 's3cr3tSeed' }, type: 'mkcp-legacy' }],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
{ mtu: 1350, tti: 20 },
|
||||||
|
[['mkcp-legacy', '', 's3cr3tSeed']],
|
||||||
|
],
|
||||||
])('%s', (_name, link, kcp, masks) => {
|
])('%s', (_name, link, kcp, masks) => {
|
||||||
expect(parse(link)).toEqual({ kcp, masks });
|
expect(parse(link)).toEqual({ kcp, masks });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps a vmess fm udp mask', () => {
|
||||||
|
const fm = JSON.stringify({
|
||||||
|
udp: [{ type: 'salamander', settings: { password: 'obfspw123' } }],
|
||||||
|
});
|
||||||
|
const out = parseVmessLink(vmessKcp({ type: 'none', fm }));
|
||||||
|
expect((out!.streamSettings as Record<string, unknown>).finalmask).toEqual({
|
||||||
|
udp: [{ type: 'salamander', settings: { password: 'obfspw123' } }],
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('parseTrojanLink', () => {
|
describe('parseTrojanLink', () => {
|
||||||
|
|||||||
@@ -205,8 +205,18 @@ func parseVmess(link string) (*ParseResult, error) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case "kcp":
|
||||||
|
applyTransport(stream, url.Values{"mtu": {strconv.Itoa(num(j["mtu"]))}, "tti": {strconv.Itoa(num(j["tti"]))}})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The vmess object carries fm as-is and the mKCP header and seed as type and path.
|
||||||
|
maskParams := url.Values{"fm": {getString(j, "fm", "")}}
|
||||||
|
if network == "kcp" {
|
||||||
|
maskParams.Set("headerType", getString(j, "type", ""))
|
||||||
|
maskParams.Set("seed", getString(j, "path", ""))
|
||||||
|
}
|
||||||
|
applyFinalMask(stream, maskParams)
|
||||||
|
|
||||||
if security == "tls" {
|
if security == "tls" {
|
||||||
tls := stream["tlsSettings"].(map[string]any)
|
tls := stream["tlsSettings"].(map[string]any)
|
||||||
tls["serverName"] = getString(j, "sni", "")
|
tls["serverName"] = getString(j, "sni", "")
|
||||||
|
|||||||
@@ -286,6 +286,11 @@ func mkcpLegacyMasks(t *testing.T, res *ParseResult) []mkcpMask {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func vmessKcpLink(fields string) string {
|
||||||
|
inner := `{"v":"2","add":"h.com","port":443,"id":"11111111-2222-4333-8444-555555555555","net":"kcp","tls":"none",` + fields + `}`
|
||||||
|
return "vmess://" + base64.StdEncoding.EncodeToString([]byte(inner))
|
||||||
|
}
|
||||||
|
|
||||||
func TestParse_KcpShareParams(t *testing.T) {
|
func TestParse_KcpShareParams(t *testing.T) {
|
||||||
// The emitter flattens one mkcp-legacy mask per field into headerType/seed; a merged
|
// 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.
|
// mask drops the seed in xray-core (MkcpLegacy.Build), so import rebuilds them separately.
|
||||||
@@ -323,6 +328,20 @@ func TestParse_KcpShareParams(t *testing.T) {
|
|||||||
wantMTU: 1350,
|
wantMTU: 1350,
|
||||||
wantTTI: 20,
|
wantTTI: 20,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "vmess header and seed from type and path",
|
||||||
|
link: vmessKcpLink(`"type":"wechat-video","path":"secret-seed","mtu":1400,"tti":50`),
|
||||||
|
wantMTU: 1400,
|
||||||
|
wantTTI: 50,
|
||||||
|
wantMasks: []mkcpMask{{"", "secret-seed"}, {"wechat", ""}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "vmess fm mkcp-legacy mask wins over path",
|
||||||
|
link: vmessKcpLink(`"type":"none","path":"s3cr3tSeed","mtu":1350,"tti":20,"fm":"{\"udp\":[{\"settings\":{\"header\":\"\",\"value\":\"s3cr3tSeed\"},\"type\":\"mkcp-legacy\"}]}"`),
|
||||||
|
wantMTU: 1350,
|
||||||
|
wantTTI: 20,
|
||||||
|
wantMasks: []mkcpMask{{"", "s3cr3tSeed"}},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
t.Run(c.name, func(t *testing.T) {
|
t.Run(c.name, func(t *testing.T) {
|
||||||
@@ -340,3 +359,15 @@ func TestParse_KcpShareParams(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParse_VmessFinalMask(t *testing.T) {
|
||||||
|
res, err := ParseLink(vmessKcpLink(`"type":"none","fm":"{\"udp\":[{\"settings\":{\"password\":\"obfspw123\"},\"type\":\"salamander\"}]}"`))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse: %v", err)
|
||||||
|
}
|
||||||
|
udp := finalmaskUDP(t, res)
|
||||||
|
want := []any{map[string]any{"type": "salamander", "settings": map[string]any{"password": "obfspw123"}}}
|
||||||
|
if !reflect.DeepEqual(udp, want) {
|
||||||
|
t.Fatalf("finalmask.udp = %#v, want %#v", udp, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user