diff --git a/app/(dashboard)/proxy-hosts/actions.ts b/app/(dashboard)/proxy-hosts/actions.ts
index 326b97bf..6d684357 100644
--- a/app/(dashboard)/proxy-hosts/actions.ts
+++ b/app/(dashboard)/proxy-hosts/actions.ts
@@ -319,6 +319,7 @@ function parseGeoBlockConfig(formData: FormData): {
allow_cidrs: parseStringList("geoblock_allow_cidrs"),
allow_ips: parseStringList("geoblock_allow_ips"),
trusted_proxies: parseStringList("geoblock_trusted_proxies"),
+ fail_closed: formData.get("geoblock_fail_closed") === "on",
response_status: parseOptionalNumber(formData.get("geoblock_response_status")) ?? 403,
response_body: parseOptionalText(formData.get("geoblock_response_body")) ?? "Forbidden",
response_headers: parseResponseHeaders(formData),
diff --git a/app/(dashboard)/settings/actions.ts b/app/(dashboard)/settings/actions.ts
index 38268887..46b7608c 100644
--- a/app/(dashboard)/settings/actions.ts
+++ b/app/(dashboard)/settings/actions.ts
@@ -553,6 +553,7 @@ export async function updateGeoBlockSettingsAction(_prevState: ActionResult | nu
allow_cidrs: parseGeoBlockStringList("geoblock_allow_cidrs", formData),
allow_ips: parseGeoBlockStringList("geoblock_allow_ips", formData),
trusted_proxies: parseGeoBlockStringList("geoblock_trusted_proxies", formData),
+ fail_closed: parseGeoBlockCheckbox(formData.get("geoblock_fail_closed")),
response_status: responseStatus,
response_body: responseBody,
response_headers: parseGeoBlockResponseHeaders(formData),
diff --git a/src/components/proxy-hosts/GeoBlockFields.tsx b/src/components/proxy-hosts/GeoBlockFields.tsx
index 523180c4..1d7f434a 100644
--- a/src/components/proxy-hosts/GeoBlockFields.tsx
+++ b/src/components/proxy-hosts/GeoBlockFields.tsx
@@ -6,10 +6,12 @@ import {
AccordionSummary,
Autocomplete,
Box,
+ Checkbox,
Chip,
CircularProgress,
Collapse,
Divider,
+ FormControlLabel,
Grid,
IconButton,
Stack,
@@ -468,6 +470,19 @@ export function GeoBlockFields({ initialValues, showModeSelector = true }: GeoBl
helperText="Used to parse X-Forwarded-For. Use private_ranges for all RFC-1918 ranges."
/>
+
+
+ }
+ label={Fail closed (block indeterminate IPs)}
+ />
+
+
diff --git a/src/lib/caddy.ts b/src/lib/caddy.ts
index 494b4f58..ee6d8c8b 100644
--- a/src/lib/caddy.ts
+++ b/src/lib/caddy.ts
@@ -730,6 +730,7 @@ function mergeGeoBlockSettings(
allow_ips: [...(global.allow_ips ?? []), ...(host.allow_ips ?? [])],
trusted_proxies: [...(global.trusted_proxies ?? []), ...(host.trusted_proxies ?? [])],
// Host config wins for scalar fields
+ fail_closed: host.fail_closed || global.fail_closed || false,
response_status: host.response_status ?? global.response_status ?? 403,
response_body: host.response_body ?? global.response_body ?? "Forbidden",
response_headers: { ...(global.response_headers ?? {}), ...(host.response_headers ?? {}) },
@@ -784,6 +785,7 @@ function buildBlockerHandler(config: GeoBlockSettings): Record
if (config.allow_ips?.length) handler.allow_ips = config.allow_ips;
if (config.trusted_proxies?.length) handler.trusted_proxies = config.trusted_proxies;
+ if (config.fail_closed) handler.fail_closed = true;
if (config.redirect_url) {
handler.redirect_url = config.redirect_url;
diff --git a/src/lib/settings.ts b/src/lib/settings.ts
index a69b4e3d..4c799400 100644
--- a/src/lib/settings.ts
+++ b/src/lib/settings.ts
@@ -64,6 +64,9 @@ export type GeoBlockSettings = {
// Trusted proxies for X-Forwarded-For parsing
trusted_proxies: string[];
+ // When true, block requests where the real client IP cannot be determined
+ // (e.g. connection from trusted proxy but no usable XFF entry). Default: false (fail-open)
+ fail_closed: boolean;
// Block response customization
response_status: number; // default 403