Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
b748b21
Introduced support for load balancing through upstream hosts and cust…
genticflowlabs Mar 17, 2026
f42e35d
Fixed lint errors on RealIpHeader
genticflowlabs Mar 17, 2026
39d9d07
Prevent deleting an upstream host if in use and added e2e tests
genticflowlabs Mar 17, 2026
ffe3306
Schema documentation
genticflowlabs Mar 17, 2026
7517479
Fixes for MySQL
genticflowlabs Mar 17, 2026
24f7c8c
Pgsql fix
genticflowlabs Mar 17, 2026
4b4d0de
Sync with upstream + resolved PR comments
genticflowlabs Jun 1, 2026
35c6ac8
Revert vite config changes
genticflowlabs Jun 1, 2026
6ed15b9
Merge upstream/develop to resolve PR conflict in ProxyHosts/Table.tsx
genticflowlabs Jun 1, 2026
a43971e
Revert authentik.sql.gz to upstream/develop content
genticflowlabs Jun 1, 2026
7aae915
Revert vite.config.ts to upstream/develop
genticflowlabs Jun 1, 2026
8404a0b
Fix getFlagCodeForLocale to match upstream's expected behavior
genticflowlabs Jun 1, 2026
268b192
Run locale-compile synchronously in the vite dev/test plugin
genticflowlabs Jun 1, 2026
c4e15e8
Fix POST /users response example to match the API output
genticflowlabs Jun 1, 2026
e88e7fa
Fix Upstream Host radio not toggling on Custom Location entries
genticflowlabs Jun 1, 2026
324fe17
Fill placeholder forward fields on upstream-mode locations at submit …
genticflowlabs Jun 1, 2026
b16f64d
Always list every custom location in the proxy-host destination cell
genticflowlabs Jun 1, 2026
4178b55
Render proxy-level destination as '/ → ...' when locations exist
genticflowlabs Jun 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions backend/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import app from "./app.js";
import internalCertificate from "./internal/certificate.js";
import internalIpRanges from "./internal/ip_ranges.js";
import internalNginx from "./internal/nginx.js";
import { global as logger } from "./logger.js";
import { migrateUp } from "./migrate.js";
import { getCompiledSchema } from "./schema/index.js";
Expand All @@ -17,7 +18,20 @@ async function appStart() {
.then(() => {
if (!IP_RANGES_FETCH_ENABLED) {
logger.info("IP Ranges fetch is disabled by environment variable");
return;
// nginx.conf no longer hardcodes real_ip_header — it expects ip_ranges.conf
// to set it. When fetching is disabled, that file is never written by
// fetch(), so render it once with empty ranges so real_ip_header is still
// present and respects the configured setting.
return internalIpRanges
.generateConfig([])
.then(() =>
internalNginx.reload().catch((err) => {
logger.warn(`nginx reload after ip_ranges init failed (likely starting): ${err.message}`);
}),
)
.catch((err) => {
logger.error(`Failed to write initial ip_ranges.conf: ${err.message}`);
});
}
logger.info("IP Ranges fetch is enabled");
return internalIpRanges.fetch().catch((err) => {
Expand All @@ -26,7 +40,9 @@ async function appStart() {
})
.then(() => {
internalCertificate.initTimer();
internalIpRanges.initTimer();
if (IP_RANGES_FETCH_ENABLED) {
internalIpRanges.initTimer();
}

const server = app.listen(3000, () => {
logger.info(`Backend PID ${process.pid} listening on port 3000 ...`);
Expand Down
54 changes: 48 additions & 6 deletions backend/internal/ip_ranges.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ProxyAgent } from "proxy-agent";
import errs from "../lib/error.js";
import utils from "../lib/utils.js";
import { ipRanges as logger } from "../logger.js";
import settingModel from "../models/setting.js";
import internalNginx from "./nginx.js";

const __filename = fileURLToPath(import.meta.url);
Expand All @@ -23,6 +24,7 @@ const internalIpRanges = {
interval: null,
interval_processing: false,
iteration_count: 0,
last_ip_ranges: [],

initTimer: () => {
logger.info("IP Ranges Renewal Timer initialized");
Expand Down Expand Up @@ -107,11 +109,22 @@ const internalIpRanges = {
return true;
});

internalIpRanges.last_ip_ranges = clean_ip_ranges;

return internalIpRanges.generateConfig(clean_ip_ranges).then(() => {
if (internalIpRanges.iteration_count) {
// Reload nginx
return internalNginx.reload();
}
// Always reload nginx after writing the config — even on the very first
// iteration. nginx and backend boot in parallel under s6, so by the time
// we finish fetching IP ranges nginx has typically already loaded its
// config without ip_ranges.conf (it's an optional include) and is running
// without `real_ip_header` set. Skipping the reload meant the configured
// real-ip header (e.g. cf-connecting-ip) wasn't honored until the user
// manually re-saved the setting.
//
// If nginx isn't up yet, the reload will fail; we log and continue so the
// boot sequence isn't broken — nginx will read the file when it starts.
return internalNginx.reload().catch((err) => {
logger.warn(`nginx reload after ip_ranges write failed (likely starting): ${err.message}`);
});
});
})
.then(() => {
Expand All @@ -129,7 +142,26 @@ const internalIpRanges = {
* @param {Array} ip_ranges
* @returns {Promise}
*/
generateConfig: (ip_ranges) => {
generateConfig: async (ip_ranges) => {
let realIpHeader = "X-Real-IP";
try {
const setting = await settingModel.query().where("id", "real-ip-header").first();
if (setting?.value) {
const candidate = setting.value === "custom" && setting.meta?.custom
? setting.meta.custom
: setting.value;
// Defense-in-depth: even though the PUT schema validates this, the value lands
// in a raw nginx directive, so reject anything that isn't a plain header name.
if (/^[A-Za-z][A-Za-z0-9-]{0,127}$/.test(candidate)) {
realIpHeader = candidate;
} else {
logger.warn(`Ignoring invalid real-ip-header setting "${candidate}" — falling back to X-Real-IP`);
}
}
} catch (err) {
logger.warn(`Could not read real-ip-header setting: ${err.message} — falling back to X-Real-IP`);
}

const renderEngine = utils.getRenderEngine();
return new Promise((resolve, reject) => {
let template = null;
Expand All @@ -142,7 +174,7 @@ const internalIpRanges = {
}

renderEngine
.parseAndRender(template, { ip_ranges: ip_ranges })
.parseAndRender(template, { ip_ranges: ip_ranges, real_ip_header: realIpHeader })
.then((config_text) => {
fs.writeFileSync(filename, config_text, { encoding: "utf8" });
resolve(true);
Expand All @@ -153,6 +185,16 @@ const internalIpRanges = {
});
});
},

/**
* Regenerate ip_ranges.conf with cached ranges and reload nginx.
* Called when the real-ip-header setting changes.
* @returns {Promise}
*/
regenerate: async () => {
await internalIpRanges.generateConfig(internalIpRanges.last_ip_ranges);
await internalNginx.reload();
},
};

export default internalIpRanges;
2 changes: 2 additions & 0 deletions backend/internal/nginx.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ const internalNginx = {
{ hsts_subdomains: host.hsts_subdomains },
{ access_list: host.access_list },
{ certificate: host.certificate },
{ upstream_host_id: 0 },
{ upstream_host_forward_scheme: "http" },
host.locations[i],
);

Expand Down
6 changes: 3 additions & 3 deletions backend/internal/proxy-host.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ const internalProxyHost = {
// re-fetch with cert
return internalProxyHost.get(access, {
id: row.id,
expand: ["certificate", "owner", "access_list.[clients,items]"],
expand: ["certificate", "owner", "access_list.[clients,items]", "upstream_host.[servers]"],
});
})
.then((row) => {
Expand Down Expand Up @@ -206,7 +206,7 @@ const internalProxyHost = {
return internalProxyHost
.get(access, {
id: thisData.id,
expand: ["owner", "certificate", "access_list.[clients,items]"],
expand: ["owner", "certificate", "access_list.[clients,items]", "upstream_host.[servers]"],
})
.then((row) => {
if (!row.enabled) {
Expand Down Expand Up @@ -323,7 +323,7 @@ const internalProxyHost = {
.then(() => {
return internalProxyHost.get(access, {
id: data.id,
expand: ["certificate", "owner", "access_list"],
expand: ["certificate", "owner", "access_list", "upstream_host.[servers]"],
});
})
.then((row) => {
Expand Down
3 changes: 3 additions & 0 deletions backend/internal/report.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import internalDeadHost from "./dead-host.js";
import internalProxyHost from "./proxy-host.js";
import internalRedirectionHost from "./redirection-host.js";
import internalStream from "./stream.js";
import internalUpstreamHost from "./upstream-host.js";

const internalReport = {
/**
Expand All @@ -19,6 +20,7 @@ const internalReport = {
internalRedirectionHost.getCount(userId, access_data.permission_visibility),
internalStream.getCount(userId, access_data.permission_visibility),
internalDeadHost.getCount(userId, access_data.permission_visibility),
internalUpstreamHost.getCount(userId, access_data.permission_visibility),
];

return Promise.all(promises);
Expand All @@ -29,6 +31,7 @@ const internalReport = {
redirection: counts.shift(),
stream: counts.shift(),
dead: counts.shift(),
upstream: counts.shift(),
};
});
},
Expand Down
5 changes: 5 additions & 0 deletions backend/internal/setting.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import fs from "node:fs";
import errs from "../lib/error.js";
import settingModel from "../models/setting.js";
import internalIpRanges from "./ip_ranges.js";
import internalNginx from "./nginx.js";

const internalSetting = {
Expand Down Expand Up @@ -32,6 +33,10 @@ const internalSetting = {
});
})
.then((row) => {
if (row.id === "real-ip-header") {
return internalIpRanges.regenerate().then(() => row);
}

if (row.id === "default-site") {
// write the html if we need to
if (row.value === "html") {
Expand Down
Loading