mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 12:44:22 +00:00
fix(external-proxy): relabel "Host" as "Address", add per-entry ECH (#4935)
The external proxy "Host" field was bound to dest (the connection address that becomes the link host) but labeled "Host", misleading users into thinking it set a transport host header. Relabel it to "Address" to match what it actually controls. Add per-entry ECH (echConfigList) to the external proxy schema, form (shown under Force TLS = TLS), the TS link generator, and the Go sub services: ech is emitted on share links and vmess objects, and written into the stream so the JSON subscription picks it up via the existing tlsData reader.
This commit is contained in:
parent
b40f869f2a
commit
a8d5d0dfab
18 changed files with 67 additions and 16 deletions
|
|
@ -137,6 +137,7 @@ function applyExternalProxyTLSObj(
|
|||
if (alpn.length > 0) obj.alpn = alpn;
|
||||
const pins = externalProxyPins(externalProxy.pinnedPeerCertSha256);
|
||||
if (pins.length > 0) obj.pcs = pins;
|
||||
if (externalProxy.echConfigList && externalProxy.echConfigList.length > 0) obj.ech = externalProxy.echConfigList;
|
||||
}
|
||||
|
||||
export interface GenVmessLinkInput {
|
||||
|
|
@ -280,6 +281,7 @@ function applyExternalProxyTLSParams(
|
|||
if (alpn.length > 0) params.set('alpn', alpn);
|
||||
const pins = externalProxyPins(externalProxy.pinnedPeerCertSha256);
|
||||
if (pins.length > 0) params.set('pcs', pins);
|
||||
if (externalProxy.echConfigList && externalProxy.echConfigList.length > 0) params.set('ech', externalProxy.echConfigList);
|
||||
}
|
||||
|
||||
export interface GenVlessLinkInput {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ const newEntry = () => ({
|
|||
fingerprint: '',
|
||||
alpn: [],
|
||||
pinnedPeerCertSha256: [],
|
||||
echConfigList: '',
|
||||
});
|
||||
|
||||
function Field({ label, children }: { label: ReactNode; children: ReactNode }) {
|
||||
|
|
@ -92,9 +93,9 @@ export default function ExternalProxyForm({
|
|||
/>
|
||||
</Form.Item>
|
||||
</Field>
|
||||
<Field label={t('host')}>
|
||||
<Field label={t('pages.inbounds.address')}>
|
||||
<Form.Item name={[field.name, 'dest']} noStyle>
|
||||
<Input placeholder={t('host')} />
|
||||
<Input placeholder={t('pages.inbounds.address')} />
|
||||
</Form.Item>
|
||||
</Field>
|
||||
<Field label={t('pages.inbounds.port')}>
|
||||
|
|
@ -125,7 +126,7 @@ export default function ExternalProxyForm({
|
|||
<div className="ext-proxy-grid ext-proxy-grid--tls">
|
||||
<Field label="SNI">
|
||||
<Form.Item name={[field.name, 'sni']} noStyle>
|
||||
<Input placeholder={t('pages.inbounds.form.sniPlaceholder')} />
|
||||
<Input placeholder={t('pages.inbounds.form.serverNameIndication')} />
|
||||
</Form.Item>
|
||||
</Field>
|
||||
<Field label={t('pages.inbounds.form.fingerprint')}>
|
||||
|
|
@ -157,6 +158,11 @@ export default function ExternalProxyForm({
|
|||
</Form.Item>
|
||||
</Field>
|
||||
</div>
|
||||
<Field label={t('pages.inbounds.form.echConfig')}>
|
||||
<Form.Item name={[field.name, 'echConfigList']} noStyle>
|
||||
<Input placeholder={t('pages.inbounds.form.echConfig')} />
|
||||
</Form.Item>
|
||||
</Field>
|
||||
<Field label={t('pages.inbounds.form.pinnedPeerCertSha256')}>
|
||||
<Space.Compact block>
|
||||
<Form.Item name={[field.name, 'pinnedPeerCertSha256']} noStyle>
|
||||
|
|
|
|||
|
|
@ -23,5 +23,6 @@ export const ExternalProxyEntrySchema = z.object({
|
|||
),
|
||||
alpn: z.array(AlpnSchema).optional(),
|
||||
pinnedPeerCertSha256: z.array(z.string()).optional(),
|
||||
echConfigList: z.string().optional(),
|
||||
});
|
||||
export type ExternalProxyEntry = z.infer<typeof ExternalProxyEntrySchema>;
|
||||
|
|
|
|||
|
|
@ -1053,6 +1053,9 @@ func applyExternalProxyTLSObj(ep map[string]any, obj map[string]any, security st
|
|||
if pins, ok := externalProxyPins(ep["pinnedPeerCertSha256"]); ok {
|
||||
obj["pcs"] = joinAnyStrings(pins)
|
||||
}
|
||||
if ech, ok := ep["echConfigList"].(string); ok && ech != "" {
|
||||
obj["ech"] = ech
|
||||
}
|
||||
}
|
||||
|
||||
func applyExternalProxyTLSParams(ep map[string]any, params map[string]string, security string) {
|
||||
|
|
@ -1071,6 +1074,9 @@ func applyExternalProxyTLSParams(ep map[string]any, params map[string]string, se
|
|||
if pins, ok := externalProxyPins(ep["pinnedPeerCertSha256"]); ok {
|
||||
params["pcs"] = joinAnyStrings(pins)
|
||||
}
|
||||
if ech, ok := ep["echConfigList"].(string); ok && ech != "" {
|
||||
params["ech"] = ech
|
||||
}
|
||||
}
|
||||
|
||||
// applyExternalProxyHysteriaParams overrides the cert pin for a single
|
||||
|
|
@ -1143,6 +1149,14 @@ func applyExternalProxyTLSToStream(ep map[string]any, stream map[string]any, sec
|
|||
}
|
||||
settings["pinnedPeerCertSha256"] = pins
|
||||
}
|
||||
if ech, ok := ep["echConfigList"].(string); ok && ech != "" {
|
||||
settings, _ := tlsSettings["settings"].(map[string]any)
|
||||
if settings == nil {
|
||||
settings = map[string]any{}
|
||||
tlsSettings["settings"] = settings
|
||||
}
|
||||
settings["echConfigList"] = ech
|
||||
}
|
||||
}
|
||||
|
||||
func externalProxySNI(ep map[string]any) (string, bool) {
|
||||
|
|
|
|||
|
|
@ -575,6 +575,47 @@ func TestApplyExternalProxyTLSParams_ExplicitSNIOverridesUpstream(t *testing.T)
|
|||
}
|
||||
}
|
||||
|
||||
func TestApplyExternalProxy_ECHPropagates(t *testing.T) {
|
||||
const ech = "ech-config-base64"
|
||||
|
||||
t.Run("url params", func(t *testing.T) {
|
||||
params := map[string]string{"security": "tls"}
|
||||
ep := map[string]any{"dest": "proxy.example.com", "echConfigList": ech}
|
||||
applyExternalProxyTLSParams(ep, params, "tls")
|
||||
if params["ech"] != ech {
|
||||
t.Fatalf("ech param = %q, want %q", params["ech"], ech)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("vmess obj", func(t *testing.T) {
|
||||
obj := map[string]any{}
|
||||
ep := map[string]any{"dest": "proxy.example.com", "echConfigList": ech}
|
||||
applyExternalProxyTLSObj(ep, obj, "tls")
|
||||
if obj["ech"] != ech {
|
||||
t.Fatalf("ech obj = %v, want %q", obj["ech"], ech)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("json stream settings", func(t *testing.T) {
|
||||
stream := map[string]any{"security": "tls", "tlsSettings": map[string]any{}}
|
||||
ep := map[string]any{"dest": "proxy.example.com", "echConfigList": ech}
|
||||
applyExternalProxyTLSToStream(ep, stream, "tls")
|
||||
settings, _ := stream["tlsSettings"].(map[string]any)["settings"].(map[string]any)
|
||||
if settings["echConfigList"] != ech {
|
||||
t.Fatalf("echConfigList = %v, want %q", settings["echConfigList"], ech)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("non-tls security drops ech", func(t *testing.T) {
|
||||
params := map[string]string{}
|
||||
ep := map[string]any{"echConfigList": ech}
|
||||
applyExternalProxyTLSParams(ep, params, "none")
|
||||
if _, ok := params["ech"]; ok {
|
||||
t.Fatalf("ech must not be set when security != tls")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplyExternalProxyTLSToStream_DoesNotLeakAcrossProxies(t *testing.T) {
|
||||
stream := map[string]any{
|
||||
"security": "tls",
|
||||
|
|
|
|||
|
|
@ -551,7 +551,6 @@
|
|||
"maxSendingWindow": "أقصى نافذة إرسال",
|
||||
"externalProxy": "وكيل خارجي",
|
||||
"forceTls": "فرض TLS",
|
||||
"sniPlaceholder": "SNI (افتراضياً host)",
|
||||
"fingerprint": "بصمة",
|
||||
"defaultOption": "افتراضي",
|
||||
"routeMark": "Route Mark",
|
||||
|
|
|
|||
|
|
@ -552,7 +552,6 @@
|
|||
"maxSendingWindow": "Max Sending Window",
|
||||
"externalProxy": "External Proxy",
|
||||
"forceTls": "Force TLS",
|
||||
"sniPlaceholder": "SNI (defaults to host)",
|
||||
"fingerprint": "Fingerprint",
|
||||
"defaultOption": "Default",
|
||||
"routeMark": "Route Mark",
|
||||
|
|
|
|||
|
|
@ -551,7 +551,6 @@
|
|||
"maxSendingWindow": "Máx. ventana de envío",
|
||||
"externalProxy": "Proxy externo",
|
||||
"forceTls": "Forzar TLS",
|
||||
"sniPlaceholder": "SNI (por defecto = host)",
|
||||
"fingerprint": "Fingerprint",
|
||||
"defaultOption": "Por defecto",
|
||||
"routeMark": "Route Mark",
|
||||
|
|
|
|||
|
|
@ -551,7 +551,6 @@
|
|||
"maxSendingWindow": "حداکثر پنجره ارسال",
|
||||
"externalProxy": "پراکسی خارجی",
|
||||
"forceTls": "اجبار TLS",
|
||||
"sniPlaceholder": "SNI (پیشفرض همان host)",
|
||||
"fingerprint": "اثرانگشت",
|
||||
"defaultOption": "پیشفرض",
|
||||
"routeMark": "علامت مسیر",
|
||||
|
|
|
|||
|
|
@ -551,7 +551,6 @@
|
|||
"maxSendingWindow": "Maks. jendela pengiriman",
|
||||
"externalProxy": "Proxy eksternal",
|
||||
"forceTls": "Paksa TLS",
|
||||
"sniPlaceholder": "SNI (default = host)",
|
||||
"fingerprint": "Fingerprint",
|
||||
"defaultOption": "Default",
|
||||
"routeMark": "Route Mark",
|
||||
|
|
|
|||
|
|
@ -551,7 +551,6 @@
|
|||
"maxSendingWindow": "最大送信ウィンドウ",
|
||||
"externalProxy": "外部プロキシ",
|
||||
"forceTls": "TLS を強制",
|
||||
"sniPlaceholder": "SNI (デフォルトは host)",
|
||||
"fingerprint": "Fingerprint",
|
||||
"defaultOption": "デフォルト",
|
||||
"routeMark": "Route Mark",
|
||||
|
|
|
|||
|
|
@ -551,7 +551,6 @@
|
|||
"maxSendingWindow": "Máx. janela de envio",
|
||||
"externalProxy": "Proxy externo",
|
||||
"forceTls": "Forçar TLS",
|
||||
"sniPlaceholder": "SNI (padrão = host)",
|
||||
"fingerprint": "Fingerprint",
|
||||
"defaultOption": "Padrão",
|
||||
"routeMark": "Route Mark",
|
||||
|
|
|
|||
|
|
@ -551,7 +551,6 @@
|
|||
"maxSendingWindow": "Макс. окно отправки",
|
||||
"externalProxy": "External Proxy",
|
||||
"forceTls": "Принудительный TLS",
|
||||
"sniPlaceholder": "SNI (по умолчанию = host)",
|
||||
"fingerprint": "Fingerprint",
|
||||
"defaultOption": "По умолчанию",
|
||||
"routeMark": "Route Mark",
|
||||
|
|
|
|||
|
|
@ -551,7 +551,6 @@
|
|||
"maxSendingWindow": "Maks. gönderme penceresi",
|
||||
"externalProxy": "Harici proxy",
|
||||
"forceTls": "TLS zorla",
|
||||
"sniPlaceholder": "SNI (varsayılan host)",
|
||||
"fingerprint": "Fingerprint",
|
||||
"defaultOption": "Varsayılan",
|
||||
"routeMark": "Route Mark",
|
||||
|
|
|
|||
|
|
@ -551,7 +551,6 @@
|
|||
"maxSendingWindow": "Макс. вікно відправки",
|
||||
"externalProxy": "External Proxy",
|
||||
"forceTls": "Примусовий TLS",
|
||||
"sniPlaceholder": "SNI (за замовчуванням = host)",
|
||||
"fingerprint": "Fingerprint",
|
||||
"defaultOption": "За замовчуванням",
|
||||
"routeMark": "Route Mark",
|
||||
|
|
|
|||
|
|
@ -551,7 +551,6 @@
|
|||
"maxSendingWindow": "Cửa sổ gửi tối đa",
|
||||
"externalProxy": "Proxy ngoài",
|
||||
"forceTls": "Bắt buộc TLS",
|
||||
"sniPlaceholder": "SNI (mặc định = host)",
|
||||
"fingerprint": "Fingerprint",
|
||||
"defaultOption": "Mặc định",
|
||||
"routeMark": "Route Mark",
|
||||
|
|
|
|||
|
|
@ -551,7 +551,6 @@
|
|||
"maxSendingWindow": "最大发送窗口",
|
||||
"externalProxy": "外部代理",
|
||||
"forceTls": "强制 TLS",
|
||||
"sniPlaceholder": "SNI (默认为 host)",
|
||||
"fingerprint": "指纹",
|
||||
"defaultOption": "默认",
|
||||
"routeMark": "Route Mark",
|
||||
|
|
|
|||
|
|
@ -551,7 +551,6 @@
|
|||
"maxSendingWindow": "最大發送視窗",
|
||||
"externalProxy": "外部代理",
|
||||
"forceTls": "強制 TLS",
|
||||
"sniPlaceholder": "SNI (預設為 host)",
|
||||
"fingerprint": "指紋",
|
||||
"defaultOption": "預設",
|
||||
"routeMark": "Route Mark",
|
||||
|
|
|
|||
Loading…
Reference in a new issue