diff --git a/frontend/public/openapi.json b/frontend/public/openapi.json index 8f014b715..f82e88ba0 100644 --- a/frontend/public/openapi.json +++ b/frontend/public/openapi.json @@ -2185,7 +2185,7 @@ "tags": [ "Inbounds" ], - "summary": "Lightweight picker projection of the authenticated user’s inbounds. Returns id, remark, tag, protocol, port, a server-computed tlsFlowCapable flag (true for VLESS / port-fallback on TCP with tls or reality), and ssMethod (the Shadowsocks cipher, empty for non-Shadowsocks inbounds — used by the client UI to generate a valid Shadowsocks 2022 PSK). Use this for dropdowns and attach pickers — it skips settings, streamSettings, and clientStats so the payload stays small even on panels with thousands of clients.", + "summary": "Lightweight picker projection of the authenticated user’s inbounds. Returns id, remark, tag, protocol, port, a server-computed tlsFlowCapable flag (true for VLESS on TCP with tls or reality, or on XHTTP with VLESS encryption / vlessenc enabled), and ssMethod (the Shadowsocks cipher, empty for non-Shadowsocks inbounds — used by the client UI to generate a valid Shadowsocks 2022 PSK). Use this for dropdowns and attach pickers — it skips settings, streamSettings, and clientStats so the payload stays small even on panels with thousands of clients.", "operationId": "get_panel_api_inbounds_options", "responses": { "200": { diff --git a/frontend/src/lib/xray/protocol-capabilities.ts b/frontend/src/lib/xray/protocol-capabilities.ts index fced61282..9568ea111 100644 --- a/frontend/src/lib/xray/protocol-capabilities.ts +++ b/frontend/src/lib/xray/protocol-capabilities.ts @@ -16,16 +16,16 @@ const SS_BLAKE3_CHACHA20 = '2022-blake3-chacha20-poly1305'; export interface CapabilityProtocolSlice { protocol: string; + settings?: { encryption?: string; decryption?: string }; streamSettings?: { network?: string; security?: string }; } export interface CapabilityVlessSlice extends CapabilityProtocolSlice { - settings?: { clients?: { flow?: string }[] }; + settings?: { encryption?: string; decryption?: string; clients?: { flow?: string }[] }; } -export interface CapabilityShadowsocksSlice { - protocol: string; - settings?: { method?: string }; +export interface CapabilityShadowsocksSlice extends CapabilityProtocolSlice { + settings?: { encryption?: string; method?: string }; } export function canEnableTls(values: CapabilityProtocolSlice): boolean { @@ -39,11 +39,28 @@ export function canEnableReality(values: CapabilityProtocolSlice): boolean { return REALITY_NETWORKS.includes(values.streamSettings?.network ?? ''); } +// VLESS encryption (vlessenc / ML-KEM) is on when encryption or decryption holds +// a generated value (e.g. "mlkem768x25519plus.native.0rtt.") rather than +// the "none"/"" sentinel. The value is never the literal "vlessenc" (that is the +// `xray vlessenc` subcommand). decryption is the server-side value; encryption is +// stored for link generation — either being set means it is on. +function hasVlessEncryption(settings: CapabilityProtocolSlice['settings']): boolean { + const isSet = (v?: string) => v != null && v !== '' && v !== 'none'; + return isSet(settings?.encryption) || isSet(settings?.decryption); +} + export function canEnableTlsFlow(values: CapabilityProtocolSlice): boolean { + if (values.protocol !== 'vless') return false; + const network = values.streamSettings?.network; const security = values.streamSettings?.security; - if (security !== 'tls' && security !== 'reality') return false; - if (values.streamSettings?.network !== 'tcp') return false; - return values.protocol === 'vless'; + + // Classic XTLS Vision: raw TCP carried over TLS or REALITY. + if (network === 'tcp' && (security === 'tls' || security === 'reality')) return true; + + // vlessenc carries Vision over XHTTP without transport TLS. + if (network === 'xhttp' && hasVlessEncryption(values.settings)) return true; + + return false; } export function canEnableStream(values: { protocol: string }): boolean { diff --git a/frontend/src/pages/api-docs/endpoints.ts b/frontend/src/pages/api-docs/endpoints.ts index 1a6112767..f553eeb9f 100644 --- a/frontend/src/pages/api-docs/endpoints.ts +++ b/frontend/src/pages/api-docs/endpoints.ts @@ -122,7 +122,7 @@ export const sections: readonly Section[] = [ { method: 'GET', path: '/panel/api/inbounds/options', - summary: 'Lightweight picker projection of the authenticated user’s inbounds. Returns id, remark, tag, protocol, port, a server-computed tlsFlowCapable flag (true for VLESS / port-fallback on TCP with tls or reality), and ssMethod (the Shadowsocks cipher, empty for non-Shadowsocks inbounds — used by the client UI to generate a valid Shadowsocks 2022 PSK). Use this for dropdowns and attach pickers — it skips settings, streamSettings, and clientStats so the payload stays small even on panels with thousands of clients.', + summary: 'Lightweight picker projection of the authenticated user’s inbounds. Returns id, remark, tag, protocol, port, a server-computed tlsFlowCapable flag (true for VLESS on TCP with tls or reality, or on XHTTP with VLESS encryption / vlessenc enabled), and ssMethod (the Shadowsocks cipher, empty for non-Shadowsocks inbounds — used by the client UI to generate a valid Shadowsocks 2022 PSK). Use this for dropdowns and attach pickers — it skips settings, streamSettings, and clientStats so the payload stays small even on panels with thousands of clients.', responseSchema: 'InboundOption', responseSchemaArray: true, }, diff --git a/frontend/src/pages/inbounds/info/helpers.ts b/frontend/src/pages/inbounds/info/helpers.ts index dd6c858bd..f65485db1 100644 --- a/frontend/src/pages/inbounds/info/helpers.ts +++ b/frontend/src/pages/inbounds/info/helpers.ts @@ -121,6 +121,10 @@ export function buildInboundInfo(dbInbound: DBInboundLike): InboundInfo { }), isVlessTlsFlow: canEnableTlsFlow({ protocol: dbInbound.protocol, + settings: { + encryption: settings.encryption as string | undefined, + decryption: settings.decryption as string | undefined, + }, streamSettings: { network, security }, }), host: readNetworkHost(stream, network), diff --git a/frontend/src/test/inbound-from-db.test.ts b/frontend/src/test/inbound-from-db.test.ts index 7369212db..4ce94da52 100644 --- a/frontend/src/test/inbound-from-db.test.ts +++ b/frontend/src/test/inbound-from-db.test.ts @@ -180,6 +180,35 @@ describe('protocol-capability helpers with raw coerced shapes', () => { streamSettings: { network: 'tcp', security: 'tls' }, })).toBe(false); }); + + it('canEnableTlsFlow allows vless + xhttp when vlessenc encryption is set', () => { + const enc = 'mlkem768x25519plus.native.0rtt.G3cdPSd1-NnlpTbWNSM5vHsT5VNzWfFzYSKwbUMnV1Y'; + const dec = 'mlkem768x25519plus.native.600s.mMFxPe7lz5xoq2qBk22cQYefu5fpc_2dGR8lMOKem0E'; + // XHTTP + a real (generated) encryption value → Vision flow allowed. + expect(canEnableTlsFlow({ + protocol: 'vless', + settings: { encryption: enc }, + streamSettings: { network: 'xhttp', security: 'none' }, + })).toBe(true); + // decryption alone (server-side value) is enough on XHTTP. + expect(canEnableTlsFlow({ + protocol: 'vless', + settings: { decryption: dec, encryption: 'none' }, + streamSettings: { network: 'xhttp', security: 'none' }, + })).toBe(true); + // No encryption → stays gated off. + expect(canEnableTlsFlow({ + protocol: 'vless', + settings: { encryption: 'none' }, + streamSettings: { network: 'xhttp', security: 'none' }, + })).toBe(false); + // vlessenc is XHTTP-only: TCP without tls/reality is not Vision-capable. + expect(canEnableTlsFlow({ + protocol: 'vless', + settings: { decryption: dec, encryption: enc }, + streamSettings: { network: 'tcp', security: 'none' }, + })).toBe(false); + }); }); describe('getInboundClients with schema-shaped inbound', () => { diff --git a/internal/sub/service.go b/internal/sub/service.go index 37f81b54c..a83a505f5 100644 --- a/internal/sub/service.go +++ b/internal/sub/service.go @@ -445,6 +445,20 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string { return buildVmessLink(obj) } +// vlessEncryptionEnabled reports whether the VLESS inbound settings enable +// VLESS-level encryption (vlessenc / ML-KEM). When on, the encryption/decryption +// fields hold a generated dotted string (e.g. "mlkem768x25519plus.native.0rtt."); +// "none" or empty means off. The value is never the literal "vlessenc" — that is +// the `xray vlessenc` CLI subcommand name, not a stored value. +func vlessEncryptionEnabled(settings map[string]any) bool { + for _, key := range []string{"encryption", "decryption"} { + if v, ok := settings[key].(string); ok && v != "" && v != "none" { + return true + } + } + return false +} + func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string { if inbound.Protocol != model.VLESS { return "" @@ -484,6 +498,11 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string { } default: params["security"] = "none" + // VLESS encryption (vlessenc / ML-KEM) carries XTLS Vision over XHTTP + // without transport TLS. + if streamNetwork == "xhttp" && len(clients[clientIndex].Flow) > 0 && vlessEncryptionEnabled(settings) { + params["flow"] = clients[clientIndex].Flow + } } externalProxies, _ := stream["externalProxy"].([]any) diff --git a/internal/web/service/client_crud.go b/internal/web/service/client_crud.go index 22afca21a..6b27719a0 100644 --- a/internal/web/service/client_crud.go +++ b/internal/web/service/client_crud.go @@ -146,7 +146,7 @@ func (s *ClientService) fillProtocolDefaults(c *model.Client, ib *model.Inbound) } func clientWithInboundFlow(c model.Client, ib *model.Inbound) model.Client { - if !inboundCanEnableTlsFlow(string(ib.Protocol), ib.StreamSettings) { + if !inboundCanEnableTlsFlow(string(ib.Protocol), ib.StreamSettings, ib.Settings) { c.Flow = "" } return c diff --git a/internal/web/service/client_flow_isolation_test.go b/internal/web/service/client_flow_isolation_test.go index 137b00527..c418debf9 100644 --- a/internal/web/service/client_flow_isolation_test.go +++ b/internal/web/service/client_flow_isolation_test.go @@ -10,23 +10,31 @@ import ( func TestClientWithInboundFlow_GatesByInboundCapability(t *testing.T) { const vision = "xtls-rprx-vision" + const enc = `{"encryption":"mlkem768x25519plus.native.0rtt.G3cdPSd1-NnlpTbWNSM5vHsT5VNzWfFzYSKwbUMnV1Y"}` cases := []struct { name string protocol model.Protocol streamSettings string + settings string wantFlow string }{ - {"vless tcp reality keeps flow", model.VLESS, `{"network":"tcp","security":"reality"}`, vision}, - {"vless tcp tls keeps flow", model.VLESS, `{"network":"tcp","security":"tls"}`, vision}, - {"vless ws tls clears flow", model.VLESS, `{"network":"ws","security":"tls"}`, ""}, - {"vless grpc tls clears flow", model.VLESS, `{"network":"grpc","security":"tls"}`, ""}, - {"vless tcp none clears flow", model.VLESS, `{"network":"tcp","security":"none"}`, ""}, - {"vmess tcp tls clears flow", model.VMESS, `{"network":"tcp","security":"tls"}`, ""}, - {"empty stream clears flow", model.VLESS, "", ""}, + {"vless tcp reality keeps flow", model.VLESS, `{"network":"tcp","security":"reality"}`, "", vision}, + {"vless tcp tls keeps flow", model.VLESS, `{"network":"tcp","security":"tls"}`, "", vision}, + {"vless ws tls clears flow", model.VLESS, `{"network":"ws","security":"tls"}`, "", ""}, + {"vless grpc tls clears flow", model.VLESS, `{"network":"grpc","security":"tls"}`, "", ""}, + {"vless tcp none clears flow", model.VLESS, `{"network":"tcp","security":"none"}`, "", ""}, + {"vmess tcp tls clears flow", model.VMESS, `{"network":"tcp","security":"tls"}`, "", ""}, + {"empty stream clears flow", model.VLESS, "", "", ""}, + // vlessenc (ML-KEM) keeps Vision flow without transport TLS only on XHTTP. + // TCP without tls/reality clears it even with vlessenc set. + {"vless tcp vlessenc clears flow", model.VLESS, `{"network":"tcp","security":"none"}`, enc, ""}, + {"vless xhttp vlessenc keeps flow", model.VLESS, `{"network":"xhttp","security":"none"}`, enc, vision}, + {"vless xhttp no encryption clears flow", model.VLESS, `{"network":"xhttp","security":"none"}`, `{"encryption":"none"}`, ""}, + {"vless xhttp empty settings clears flow", model.VLESS, `{"network":"xhttp","security":"none"}`, "", ""}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - ib := &model.Inbound{Protocol: tc.protocol, StreamSettings: tc.streamSettings} + ib := &model.Inbound{Protocol: tc.protocol, StreamSettings: tc.streamSettings, Settings: tc.settings} got := clientWithInboundFlow(model.Client{Email: "x@example.com", Flow: vision}, ib) if got.Flow != tc.wantFlow { t.Errorf("Flow = %q, want %q", got.Flow, tc.wantFlow) diff --git a/internal/web/service/inbound.go b/internal/web/service/inbound.go index 0f705c6c5..b4d050354 100644 --- a/internal/web/service/inbound.go +++ b/internal/web/service/inbound.go @@ -196,7 +196,7 @@ func (s *InboundService) GetInboundOptions(userId int) ([]InboundOption, error) Tag: r.Tag, Protocol: r.Protocol, Port: r.Port, - TlsFlowCapable: inboundCanEnableTlsFlow(r.Protocol, r.StreamSettings), + TlsFlowCapable: inboundCanEnableTlsFlow(r.Protocol, r.StreamSettings, r.Settings), SsMethod: inboundShadowsocksMethod(r.Protocol, r.Settings), }) } diff --git a/internal/web/service/inbound_clients.go b/internal/web/service/inbound_clients.go index 20378588d..3f6b14e2a 100644 --- a/internal/web/service/inbound_clients.go +++ b/internal/web/service/inbound_clients.go @@ -210,7 +210,7 @@ func (s *InboundService) buildTargetClientFromSource(source model.Client, target case model.VLESS: target.ID = s.generateRandomCredential(targetProtocol) if (flow == "xtls-rprx-vision" || flow == "xtls-rprx-vision-udp443") && - inboundCanEnableTlsFlow(string(targetProtocol), targetInbound.StreamSettings) { + inboundCanEnableTlsFlow(string(targetProtocol), targetInbound.StreamSettings, targetInbound.Settings) { target.Flow = flow } case model.Trojan, model.Shadowsocks: diff --git a/internal/web/service/inbound_protocol.go b/internal/web/service/inbound_protocol.go index d295c32be..4d11e11ab 100644 --- a/internal/web/service/inbound_protocol.go +++ b/internal/web/service/inbound_protocol.go @@ -22,9 +22,14 @@ func inboundShadowsocksMethod(protocol, settings string) string { return s.Method } -// inboundCanEnableTlsFlow mirrors Inbound.canEnableTlsFlow() from the frontend: -// XTLS Vision is only valid for VLESS on TCP with tls or reality. -func inboundCanEnableTlsFlow(protocol, streamSettings string) bool { +// inboundCanEnableTlsFlow mirrors canEnableTlsFlow() from the frontend +// (frontend/src/lib/xray/protocol-capabilities.ts). XTLS Vision is valid for +// VLESS on TCP with tls or reality (classic), and on XHTTP when VLESS encryption +// (vlessenc / ML-KEM) is enabled — there the post-quantum, VLESS-level +// encryption stands in for the transport TLS that Vision relies on. settings is +// the inbound's raw settings JSON, which carries the encryption value +// (streamSettings does not). +func inboundCanEnableTlsFlow(protocol, streamSettings, settings string) bool { if protocol != string(model.VLESS) { return false } @@ -38,15 +43,51 @@ func inboundCanEnableTlsFlow(protocol, streamSettings string) bool { if err := json.Unmarshal([]byte(streamSettings), &stream); err != nil { return false } - if stream.Network != "tcp" { + switch stream.Network { + case "tcp": + return stream.Security == "tls" || stream.Security == "reality" + case "xhttp": + return vlessEncryptionEnabled(settings) + default: return false } - return stream.Security == "tls" || stream.Security == "reality" +} + +// vlessEncryptionEnabled reports whether a VLESS inbound has VLESS-level +// encryption (vlessenc / ML-KEM) configured. When enabled these fields hold a +// generated dotted string (e.g. "mlkem768x25519plus.native.0rtt."); "none" +// or empty means off. The value is never the literal "vlessenc" — that is the +// name of the `xray vlessenc` CLI subcommand, not a stored value. +// +// Both fields are checked: decryption is the authoritative server-side value +// xray-core reads, while encryption is stored by the panel for link generation. +// The ML-KEM/X25519 buttons set both, but accepting either keeps the gate +// working for inbounds configured via the API or raw JSON. +func vlessEncryptionEnabled(settings string) bool { + if settings == "" { + return false + } + var s struct { + Encryption string `json:"encryption"` + Decryption string `json:"decryption"` + } + if err := json.Unmarshal([]byte(settings), &s); err != nil { + return false + } + return vlessEncValueSet(s.Encryption) || vlessEncValueSet(s.Decryption) +} + +// vlessEncValueSet reports whether a VLESS encryption/decryption field holds a +// real (generated) value rather than the "none"/empty sentinel. +func vlessEncValueSet(v string) bool { + return v != "" && v != "none" } // inboundCanHostFallbacks gates the settings.fallbacks injection. // Xray only honors fallbacks on VLESS and Trojan inbounds carried over -// TCP transport with TLS or Reality security. +// TCP transport with TLS or Reality security. This is intentionally stricter +// than inboundCanEnableTlsFlow (which also accepts XHTTP+vlessenc): fallbacks +// are a raw-TCP-only feature. func inboundCanHostFallbacks(ib *model.Inbound) bool { if ib == nil { return false @@ -54,13 +95,13 @@ func inboundCanHostFallbacks(ib *model.Inbound) bool { if ib.Protocol != model.VLESS && ib.Protocol != model.Trojan { return false } - return inboundCanEnableTlsFlow(string(ib.Protocol), ib.StreamSettings) || - (ib.Protocol == model.Trojan && trojanStreamSupportsFallbacks(ib.StreamSettings)) + return streamSupportsFallbacks(ib.StreamSettings) } -// trojanStreamSupportsFallbacks mirrors the Trojan side of the same gate -// (Trojan reuses XTLS-Vision capable streams: tcp + tls or reality). -func trojanStreamSupportsFallbacks(streamSettings string) bool { +// streamSupportsFallbacks reports whether the stream is raw TCP carried over +// TLS or REALITY — the only transport Xray honors inbound fallbacks on (and the +// classic requirement for XTLS Vision before vlessenc). +func streamSupportsFallbacks(streamSettings string) bool { if streamSettings == "" { return false } diff --git a/internal/web/service/inbound_protocol_test.go b/internal/web/service/inbound_protocol_test.go new file mode 100644 index 000000000..db696db20 --- /dev/null +++ b/internal/web/service/inbound_protocol_test.go @@ -0,0 +1,90 @@ +package service + +import ( + "testing" + + "github.com/mhsanaei/3x-ui/v3/internal/database/model" +) + +// A representative vlessenc/ML-KEM encryption value as produced by `xray +// vlessenc` — a dotted string, never the literal "vlessenc". +const vlessEncValue = "mlkem768x25519plus.native.0rtt.G3cdPSd1-NnlpTbWNSM5vHsT5VNzWfFzYSKwbUMnV1Y" + +func TestInboundCanEnableTlsFlow(t *testing.T) { + cases := []struct { + name string + protocol string + streamSettings string + settings string + want bool + }{ + {"vless tcp tls", string(model.VLESS), `{"network":"tcp","security":"tls"}`, "", true}, + {"vless tcp reality", string(model.VLESS), `{"network":"tcp","security":"reality"}`, "", true}, + {"vless tcp none no enc", string(model.VLESS), `{"network":"tcp","security":"none"}`, "", false}, + {"vless ws tls", string(model.VLESS), `{"network":"ws","security":"tls"}`, "", false}, + {"vless grpc reality", string(model.VLESS), `{"network":"grpc","security":"reality"}`, "", false}, + {"vmess tcp tls", string(model.VMESS), `{"network":"tcp","security":"tls"}`, "", false}, + {"empty stream", string(model.VLESS), "", "", false}, + + // vlessenc is gated to XHTTP only. TCP without tls/reality is NOT + // Vision-capable even with vlessenc set — the combination only works on + // XHTTP in practice. + {"vless tcp vlessenc not capable", string(model.VLESS), `{"network":"tcp","security":"none"}`, `{"decryption":"mlkem768x25519plus.native.600s.mMFxPe7lz5xoq2qBk22cQYefu5fpc_2dGR8lMOKem0E","encryption":"mlkem768x25519plus.native.0rtt.hT4AY_tPWY9NVuKR3BIXxXq6zx9DqN2X86QPYW09XEM"}`, false}, + // ws is a framed transport — vlessenc never enables Vision there. + {"vless ws vlessenc still off", string(model.VLESS), `{"network":"ws","security":"none"}`, `{"encryption":"` + vlessEncValue + `"}`, false}, + + // XHTTP + VLESS encryption (the #5157 case). + {"vless xhttp vlessenc", string(model.VLESS), `{"network":"xhttp","security":"none"}`, `{"encryption":"` + vlessEncValue + `"}`, true}, + {"vless xhttp encryption none", string(model.VLESS), `{"network":"xhttp","security":"none"}`, `{"encryption":"none"}`, false}, + {"vless xhttp no settings", string(model.VLESS), `{"network":"xhttp","security":"none"}`, "", false}, + // Regression for PR #5185: the gate is "any non-none encryption", NOT an + // equality check against the literal "vlessenc" (which the buggy PR used + // and which never matches a real, generated encryption value). An x25519 + // auth value must enable it just like the ML-KEM value above. + {"vless xhttp x25519 enc", string(model.VLESS), `{"network":"xhttp","security":"none"}`, `{"encryption":"native.0rtt.121s-180s.xRMUYYjQctqYO1pSyffM-w"}`, true}, + // Server-side configs (API/JSON) may carry only decryption; that alone + // must also enable the flow gate. + {"vless xhttp decryption only", string(model.VLESS), `{"network":"xhttp","security":"none"}`, `{"decryption":"` + vlessEncValue + `","encryption":"none"}`, true}, + // XHTTP without encryption stays off even with tls (Vision over XHTTP is + // gated on vlessenc, not transport security). + {"vless xhttp tls no encryption", string(model.VLESS), `{"network":"xhttp","security":"tls"}`, `{"encryption":"none"}`, false}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := inboundCanEnableTlsFlow(tc.protocol, tc.streamSettings, tc.settings) + if got != tc.want { + t.Errorf("inboundCanEnableTlsFlow(%q, %q, %q) = %v, want %v", + tc.protocol, tc.streamSettings, tc.settings, got, tc.want) + } + }) + } +} + +// Fallbacks must remain raw-TCP-only and must NOT follow the broadened flow gate +// onto XHTTP+vlessenc. +func TestInboundCanHostFallbacks_StaysTcpOnly(t *testing.T) { + cases := []struct { + name string + protocol model.Protocol + streamSettings string + settings string + want bool + }{ + {"vless tcp tls", model.VLESS, `{"network":"tcp","security":"tls"}`, "", true}, + {"trojan tcp reality", model.Trojan, `{"network":"tcp","security":"reality"}`, "", true}, + {"vless xhttp vlessenc not fallback-capable", model.VLESS, `{"network":"xhttp","security":"none"}`, `{"encryption":"` + vlessEncValue + `"}`, false}, + {"vmess tcp tls not fallback-capable", model.VMESS, `{"network":"tcp","security":"tls"}`, "", false}, + {"nil-ish empty stream", model.VLESS, "", "", false}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + ib := &model.Inbound{Protocol: tc.protocol, StreamSettings: tc.streamSettings, Settings: tc.settings} + if got := inboundCanHostFallbacks(ib); got != tc.want { + t.Errorf("inboundCanHostFallbacks = %v, want %v", got, tc.want) + } + }) + } + if inboundCanHostFallbacks(nil) { + t.Errorf("inboundCanHostFallbacks(nil) = true, want false") + } +}