Add custom SNI for proxy

This commit is contained in:
Maksim Alekseev 2026-05-22 09:39:12 +03:00
parent 326d027f6c
commit 337ecc44c3
4 changed files with 39 additions and 13 deletions

View file

@ -1703,7 +1703,8 @@ export class Inbound extends XrayCommonClass {
static applyExternalProxyTLSParams(externalProxy, params, security) { static applyExternalProxyTLSParams(externalProxy, params, security) {
if (!externalProxy || security !== 'tls') return; if (!externalProxy || security !== 'tls') return;
if (externalProxy.dest?.length > 0) params.set("sni", externalProxy.dest); const sni = externalProxy.sni?.length > 0 ? externalProxy.sni : externalProxy.dest;
if (sni?.length > 0) params.set("sni", sni);
if (externalProxy.fingerprint?.length > 0) params.set("fp", externalProxy.fingerprint); if (externalProxy.fingerprint?.length > 0) params.set("fp", externalProxy.fingerprint);
const alpn = Inbound.externalProxyAlpn(externalProxy.alpn); const alpn = Inbound.externalProxyAlpn(externalProxy.alpn);
if (alpn.length > 0) params.set("alpn", alpn); if (alpn.length > 0) params.set("alpn", alpn);
@ -1711,7 +1712,8 @@ export class Inbound extends XrayCommonClass {
static applyExternalProxyTLSObj(externalProxy, obj, security) { static applyExternalProxyTLSObj(externalProxy, obj, security) {
if (!externalProxy || !obj || security !== 'tls') return; if (!externalProxy || !obj || security !== 'tls') return;
if (externalProxy.dest?.length > 0) obj.sni = externalProxy.dest; const sni = externalProxy.sni?.length > 0 ? externalProxy.sni : externalProxy.dest;
if (sni?.length > 0) obj.sni = sni;
if (externalProxy.fingerprint?.length > 0) obj.fp = externalProxy.fingerprint; if (externalProxy.fingerprint?.length > 0) obj.fp = externalProxy.fingerprint;
const alpn = Inbound.externalProxyAlpn(externalProxy.alpn); const alpn = Inbound.externalProxyAlpn(externalProxy.alpn);
if (alpn.length > 0) obj.alpn = alpn; if (alpn.length > 0) obj.alpn = alpn;

View file

@ -101,6 +101,7 @@ const externalProxy = computed({
dest: window.location.hostname, dest: window.location.hostname,
port: inbound.value.port, port: inbound.value.port,
remark: '', remark: '',
sni: '',
fingerprint: '', fingerprint: '',
alpn: [], alpn: [],
}]; }];
@ -1685,7 +1686,7 @@ watch(() => inbound.value?.protocol, () => stampAdvancedTextFor('stream'));
<a-form-item label="External Proxy"> <a-form-item label="External Proxy">
<a-switch v-model:checked="externalProxy" /> <a-switch v-model:checked="externalProxy" />
<a-button v-if="externalProxy" size="small" type="primary" :style="{ marginLeft: '10px' }" <a-button v-if="externalProxy" size="small" type="primary" :style="{ marginLeft: '10px' }"
@click="inbound.stream.externalProxy.push({ forceTls: 'same', dest: '', port: 443, remark: '', fingerprint: '', alpn: [] })"> @click="inbound.stream.externalProxy.push({ forceTls: 'same', dest: '', port: 443, remark: '', sni: '', fingerprint: '', alpn: [] })">
<template #icon> <template #icon>
<PlusOutlined /> <PlusOutlined />
</template> </template>
@ -1712,11 +1713,12 @@ watch(() => inbound.value?.protocol, () => stampAdvancedTextFor('stream'));
</a-input> </a-input>
</a-input-group> </a-input-group>
<a-input-group v-if="row.forceTls === 'tls'" compact :style="{ marginTop: '6px' }"> <a-input-group v-if="row.forceTls === 'tls'" compact :style="{ marginTop: '6px' }">
<a-select v-model:value="row.fingerprint" :style="{ width: '35%' }" placeholder="Fingerprint"> <a-input v-model:value="row.sni" :style="{ width: '30%' }" placeholder="SNI (defaults to host)" />
<a-select v-model:value="row.fingerprint" :style="{ width: '30%' }" placeholder="Fingerprint">
<a-select-option value="">Default</a-select-option> <a-select-option value="">Default</a-select-option>
<a-select-option v-for="fp in FINGERPRINTS" :key="fp" :value="fp">{{ fp }}</a-select-option> <a-select-option v-for="fp in FINGERPRINTS" :key="fp" :value="fp">{{ fp }}</a-select-option>
</a-select> </a-select>
<a-select v-model:value="row.alpn" mode="multiple" :style="{ width: '65%' }" placeholder="ALPN"> <a-select v-model:value="row.alpn" mode="multiple" :style="{ width: '40%' }" placeholder="ALPN">
<a-select-option v-for="alpn in ALPNS" :key="alpn" :value="alpn">{{ alpn }}</a-select-option> <a-select-option v-for="alpn in ALPNS" :key="alpn" :value="alpn">{{ alpn }}</a-select-option>
</a-select> </a-select>
</a-input-group> </a-input-group>

View file

@ -867,8 +867,8 @@ func applyExternalProxyTLSObj(ep map[string]any, obj map[string]any, security st
if security != "tls" { if security != "tls" {
return return
} }
if dest, ok := ep["dest"].(string); ok && dest != "" { if sni, ok := externalProxySNI(ep); ok {
obj["sni"] = dest obj["sni"] = sni
} }
if fp, ok := ep["fingerprint"].(string); ok && fp != "" { if fp, ok := ep["fingerprint"].(string); ok && fp != "" {
obj["fp"] = fp obj["fp"] = fp
@ -882,8 +882,8 @@ func applyExternalProxyTLSParams(ep map[string]any, params map[string]string, se
if security != "tls" { if security != "tls" {
return return
} }
if dest, ok := ep["dest"].(string); ok && dest != "" { if sni, ok := externalProxySNI(ep); ok {
params["sni"] = dest params["sni"] = sni
} }
if fp, ok := ep["fingerprint"].(string); ok && fp != "" { if fp, ok := ep["fingerprint"].(string); ok && fp != "" {
params["fp"] = fp params["fp"] = fp
@ -902,8 +902,8 @@ func applyExternalProxyTLSToStream(ep map[string]any, stream map[string]any, sec
tlsSettings = map[string]any{} tlsSettings = map[string]any{}
stream["tlsSettings"] = tlsSettings stream["tlsSettings"] = tlsSettings
} }
if dest, ok := ep["dest"].(string); ok && dest != "" { if sni, ok := externalProxySNI(ep); ok {
tlsSettings["serverName"] = dest tlsSettings["serverName"] = sni
} }
if fp, ok := ep["fingerprint"].(string); ok && fp != "" { if fp, ok := ep["fingerprint"].(string); ok && fp != "" {
tlsSettings["fingerprint"] = fp tlsSettings["fingerprint"] = fp
@ -919,6 +919,16 @@ func applyExternalProxyTLSToStream(ep map[string]any, stream map[string]any, sec
} }
} }
func externalProxySNI(ep map[string]any) (string, bool) {
if sni, ok := ep["sni"].(string); ok && sni != "" {
return sni, true
}
if dest, ok := ep["dest"].(string); ok && dest != "" {
return dest, true
}
return "", false
}
func externalProxyALPN(value any) (string, bool) { func externalProxyALPN(value any) (string, bool) {
switch v := value.(type) { switch v := value.(type) {
case string: case string:

View file

@ -449,14 +449,15 @@ func TestApplyExternalProxyTLSParams_UsesProxyDomainAndOverrides(t *testing.T) {
} }
ep := map[string]any{ ep := map[string]any{
"dest": "proxy.example.com", "dest": "proxy.example.com",
"sni": "tls.example.com",
"fingerprint": "chrome", "fingerprint": "chrome",
"alpn": []any{"h3", "h2"}, "alpn": []any{"h3", "h2"},
} }
applyExternalProxyTLSParams(ep, params, "tls") applyExternalProxyTLSParams(ep, params, "tls")
if params["sni"] != "proxy.example.com" { if params["sni"] != "tls.example.com" {
t.Fatalf("sni = %q, want proxy.example.com", params["sni"]) t.Fatalf("sni = %q, want tls.example.com", params["sni"])
} }
if params["fp"] != "chrome" { if params["fp"] != "chrome" {
t.Fatalf("fp = %q, want chrome", params["fp"]) t.Fatalf("fp = %q, want chrome", params["fp"])
@ -466,6 +467,17 @@ func TestApplyExternalProxyTLSParams_UsesProxyDomainAndOverrides(t *testing.T) {
} }
} }
func TestApplyExternalProxyTLSParams_FallsBackToDestSNI(t *testing.T) {
params := map[string]string{"security": "tls"}
ep := map[string]any{"dest": "proxy.example.com"}
applyExternalProxyTLSParams(ep, params, "tls")
if params["sni"] != "proxy.example.com" {
t.Fatalf("sni = %q, want proxy.example.com", params["sni"])
}
}
func TestApplyExternalProxyTLSParams_DoesNotApplyForNone(t *testing.T) { func TestApplyExternalProxyTLSParams_DoesNotApplyForNone(t *testing.T) {
params := map[string]string{ params := map[string]string{
"security": "none", "security": "none",