Compare commits

...

7 Commits

Author SHA1 Message Date
Owen ea1badf4e0 Add middleware for rewriting host headers 2026-06-14 12:04:02 -07:00
Owen 4435a669a6 Fill in missing ui urls from the passed params 2026-06-14 11:35:27 -07:00
Owen 90eceb457a Clean up url passing 2026-06-14 11:10:05 -07:00
Owen f39cbc9bf4 Add same signature to oss 2026-06-14 11:03:14 -07:00
Owen 50da863bb7 Add maintence page support for remote nodes 2026-06-13 21:45:52 -07:00
Owen c6ddd5c402 Open up holepunch requirements 2026-06-13 14:14:34 -07:00
Owen 0fb5ace9c7 Support the browser gateways on the remote nodes 2026-06-13 14:08:03 -07:00
9 changed files with 88 additions and 38 deletions
+8 -1
View File
@@ -511,6 +511,12 @@ export class TraefikConfigManager {
let traefikConfig; let traefikConfig;
try { try {
const currentExitNode = await getCurrentExitNodeId(); const currentExitNode = await getCurrentExitNodeId();
const maintenancePort = config.getRawConfig().server.next_port;
const maintenanceHost =
config.getRawConfig().server.internal_hostname;
const pangolinUIUrl = `http://${maintenanceHost}:${maintenancePort}`;
// logger.debug(`Fetching traefik config for exit node: ${currentExitNode}`); // logger.debug(`Fetching traefik config for exit node: ${currentExitNode}`);
traefikConfig = await getTraefikConfig( traefikConfig = await getTraefikConfig(
// this is called by the local exit node to get its own config // this is called by the local exit node to get its own config
@@ -521,7 +527,8 @@ export class TraefikConfigManager {
build == "saas" build == "saas"
? false ? false
: config.getRawConfig().traefik.allow_raw_resources, // dont allow raw resources on saas otherwise use config : config.getRawConfig().traefik.allow_raw_resources, // dont allow raw resources on saas otherwise use config
build != "oss" // generate browser gateway targets on cloud and enterprise pangolinUIUrl, // generate maintenance pages on cloud and hybrid
pangolinUIUrl // generate browser gateway targets on cloud and hybrid
); );
const domains = new Set<string>(); const domains = new Set<string>();
+2 -2
View File
@@ -44,8 +44,8 @@ export async function getTraefikConfig(
filterOutNamespaceDomains = false, // UNUSED BUT USED IN PRIVATE filterOutNamespaceDomains = false, // UNUSED BUT USED IN PRIVATE
generateLoginPageRouters = false, // UNUSED BUT USED IN PRIVATE generateLoginPageRouters = false, // UNUSED BUT USED IN PRIVATE
allowRawResources = true, allowRawResources = true,
allowMaintenancePage = true, // UNUSED BUT USED IN PRIVATE maintenancePageUiUrl: string | null = null, // UNUSED BUT USED IN PRIVATE
allowBrowserGatewayResources = true browserGatewayUiUrl: string | null = null // UNUSED BUT USED IN PRIVATE
): Promise<any> { ): Promise<any> {
// Get resources with their targets and sites in a single optimized query // Get resources with their targets and sites in a single optimized query
// Start from sites on this exit node, then join to targets and resources // Start from sites on this exit node, then join to targets and resources
+64 -28
View File
@@ -84,8 +84,8 @@ export async function getTraefikConfig(
filterOutNamespaceDomains = false, filterOutNamespaceDomains = false,
generateLoginPageRouters = false, generateLoginPageRouters = false,
allowRawResources = true, allowRawResources = true,
allowMaintenancePage = true, maintenancePageUiUrl: string | null = null,
allowBrowserGatewayResources = true browserGatewayUiUrl: string | null = null
): Promise<any> { ): Promise<any> {
// Get resources with their targets and sites in a single optimized query // Get resources with their targets and sites in a single optimized query
// Start from sites on this exit node, then join to targets and resources // Start from sites on this exit node, then join to targets and resources
@@ -317,7 +317,7 @@ export async function getTraefikConfig(
BrowserGatewayResourceEntry BrowserGatewayResourceEntry
>(); >();
if (allowBrowserGatewayResources) { if (browserGatewayUiUrl) {
for (const row of resourcesWithTargetsAndSites) { for (const row of resourcesWithTargetsAndSites) {
if (!["ssh", "vnc", "rdp"].includes(row.mode)) { if (!["ssh", "vnc", "rdp"].includes(row.mode)) {
continue; continue;
@@ -630,10 +630,11 @@ export async function getTraefikConfig(
} }
} }
if (showMaintenancePage && allowMaintenancePage) { if (showMaintenancePage && maintenancePageUiUrl) {
const maintenanceServiceName = `${key}-maintenance-service`; const maintenanceServiceName = `${key}-maintenance-service`;
const maintenanceRouterName = `${key}-maintenance-router`; const maintenanceRouterName = `${key}-maintenance-router`;
const rewriteMiddlewareName = `${key}-maintenance-rewrite`; const rewriteMiddlewareName = `${key}-maintenance-rewrite`;
const maintenanceHeadersMiddlewareName = `${key}-maintenance-headers`;
const entrypointHttp = const entrypointHttp =
config.getRawConfig().traefik.http_entrypoint; config.getRawConfig().traefik.http_entrypoint;
@@ -646,15 +647,11 @@ export async function getTraefikConfig(
? `*.${domainParts.slice(1).join(".")}` ? `*.${domainParts.slice(1).join(".")}`
: fullDomain; : fullDomain;
const maintenancePort = config.getRawConfig().server.next_port;
const maintenanceHost =
config.getRawConfig().server.internal_hostname;
config_output.http.services[maintenanceServiceName] = { config_output.http.services[maintenanceServiceName] = {
loadBalancer: { loadBalancer: {
servers: [ servers: [
{ {
url: `http://${maintenanceHost}:${maintenancePort}` url: maintenancePageUiUrl
} }
], ],
passHostHeader: true passHostHeader: true
@@ -673,12 +670,26 @@ export async function getTraefikConfig(
} }
}; };
config_output.http.middlewares[
maintenanceHeadersMiddlewareName
] = {
headers: {
customRequestHeaders: {
Host: "app.pangolin.net", // if we are sending to the cloud the host needs to be this but we will pull the p-host to find the resource
"p-host": fullDomain
}
}
};
config_output.http.routers[maintenanceRouterName] = { config_output.http.routers[maintenanceRouterName] = {
entryPoints: [ entryPoints: [
resource.ssl ? entrypointHttps : entrypointHttp resource.ssl ? entrypointHttps : entrypointHttp
], ],
service: maintenanceServiceName, service: maintenanceServiceName,
middlewares: [rewriteMiddlewareName], middlewares: [
rewriteMiddlewareName,
maintenanceHeadersMiddlewareName
],
rule: rule, rule: rule,
priority: 2000, priority: 2000,
...(resource.ssl ? { tls } : {}) ...(resource.ssl ? { tls } : {})
@@ -691,6 +702,7 @@ export async function getTraefikConfig(
resource.ssl ? entrypointHttps : entrypointHttp resource.ssl ? entrypointHttps : entrypointHttp
], ],
service: maintenanceServiceName, service: maintenanceServiceName,
middlewares: [maintenanceHeadersMiddlewareName],
rule: `${rule} && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`) || Path(\`/favicon.ico\`)) `, rule: `${rule} && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`) || Path(\`/favicon.ico\`)) `,
priority: 2001, priority: 2001,
...(resource.ssl ? { tls } : {}) ...(resource.ssl ? { tls } : {})
@@ -1027,7 +1039,7 @@ export async function getTraefikConfig(
} }
} }
if (allowBrowserGatewayResources) { if (browserGatewayUiUrl) {
// Generate Traefik config for browser gateway resources // Generate Traefik config for browser gateway resources
const browserGatewayPort = 39999; const browserGatewayPort = 39999;
for (const [, bgResource] of browserGatewayResourcesMap.entries()) { for (const [, bgResource] of browserGatewayResourcesMap.entries()) {
@@ -1119,20 +1131,17 @@ export async function getTraefikConfig(
} }
} }
if (showBgMaintenancePage && allowMaintenancePage) { if (showBgMaintenancePage && maintenancePageUiUrl) {
const bgMaintenanceServiceName = `bg-r${bgResource.resourceId}-maintenance-service`; const bgMaintenanceServiceName = `bg-r${bgResource.resourceId}-maintenance-service`;
const bgMaintenanceRouterName = `bg-r${bgResource.resourceId}-maintenance-router`; const bgMaintenanceRouterName = `bg-r${bgResource.resourceId}-maintenance-router`;
const bgRewriteMiddlewareName = `bg-r${bgResource.resourceId}-maintenance-rewrite`; const bgRewriteMiddlewareName = `bg-r${bgResource.resourceId}-maintenance-rewrite`;
const bgMaintenanceHeadersMiddlewareName = `bg-r${bgResource.resourceId}-maintenance-headers`;
const entrypointHttp = const entrypointHttp =
config.getRawConfig().traefik.http_entrypoint; config.getRawConfig().traefik.http_entrypoint;
const entrypointHttps = const entrypointHttps =
config.getRawConfig().traefik.https_entrypoint; config.getRawConfig().traefik.https_entrypoint;
const maintenancePort = config.getRawConfig().server.next_port;
const maintenanceHost =
config.getRawConfig().server.internal_hostname;
if (!config_output.http.services) if (!config_output.http.services)
config_output.http.services = {}; config_output.http.services = {};
if (!config_output.http.middlewares) if (!config_output.http.middlewares)
@@ -1144,7 +1153,7 @@ export async function getTraefikConfig(
loadBalancer: { loadBalancer: {
servers: [ servers: [
{ {
url: `http://${maintenanceHost}:${maintenancePort}` url: maintenancePageUiUrl
} }
], ],
passHostHeader: true passHostHeader: true
@@ -1158,12 +1167,26 @@ export async function getTraefikConfig(
} }
}; };
config_output.http.middlewares![
bgMaintenanceHeadersMiddlewareName
] = {
headers: {
customRequestHeaders: {
Host: "app.pangolin.net", // if we are sending to the cloud the host needs to be this but we will pull the p-host to find the resource
"p-host": fullDomain
}
}
};
config_output.http.routers![bgMaintenanceRouterName] = { config_output.http.routers![bgMaintenanceRouterName] = {
entryPoints: [ entryPoints: [
bgResource.ssl ? entrypointHttps : entrypointHttp bgResource.ssl ? entrypointHttps : entrypointHttp
], ],
service: bgMaintenanceServiceName, service: bgMaintenanceServiceName,
middlewares: [bgRewriteMiddlewareName], middlewares: [
bgRewriteMiddlewareName,
bgMaintenanceHeadersMiddlewareName
],
rule: hostRule, rule: hostRule,
priority: 2000, priority: 2000,
...(bgResource.ssl ? { tls } : {}) ...(bgResource.ssl ? { tls } : {})
@@ -1176,6 +1199,7 @@ export async function getTraefikConfig(
bgResource.ssl ? entrypointHttps : entrypointHttp bgResource.ssl ? entrypointHttps : entrypointHttp
], ],
service: bgMaintenanceServiceName, service: bgMaintenanceServiceName,
middlewares: [bgMaintenanceHeadersMiddlewareName],
rule: `${hostRule} && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`) || Path(\`/favicon.ico\`))`, rule: `${hostRule} && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`) || Path(\`/favicon.ico\`))`,
priority: 2001, priority: 2001,
...(bgResource.ssl ? { tls } : {}) ...(bgResource.ssl ? { tls } : {})
@@ -1234,9 +1258,8 @@ export async function getTraefikConfig(
// The primary type is used for the path rewrite (e.g. /rdp), mirroring // The primary type is used for the path rewrite (e.g. /rdp), mirroring
// how the maintenance page rewrites everything to /maintenance-screen. // how the maintenance page rewrites everything to /maintenance-screen.
const primaryType = typeMap.keys().next().value as string; const primaryType = typeMap.keys().next().value as string;
const internalHost = config.getRawConfig().server.internal_hostname;
const internalPort = config.getRawConfig().server.next_port;
const uiRewriteMiddlewareName = `bg-r${bgResource.resourceId}-ui-rewrite`; const uiRewriteMiddlewareName = `bg-r${bgResource.resourceId}-ui-rewrite`;
const uiHeadersMiddlewareName = `bg-r${bgResource.resourceId}-ui-headers`;
const entrypoint = bgResource.ssl const entrypoint = bgResource.ssl
? config.getRawConfig().traefik.https_entrypoint ? config.getRawConfig().traefik.https_entrypoint
: config.getRawConfig().traefik.http_entrypoint; : config.getRawConfig().traefik.http_entrypoint;
@@ -1252,11 +1275,20 @@ export async function getTraefikConfig(
} }
}; };
config_output.http.middlewares![uiHeadersMiddlewareName] = {
headers: {
customRequestHeaders: {
Host: "app.pangolin.net", // if we are sending to the cloud the host needs to be this but we will pull the p-host to find the resource
"p-host": fullDomain
}
}
};
config_output.http.services![bgUiServiceName] = { config_output.http.services![bgUiServiceName] = {
loadBalancer: { loadBalancer: {
servers: [ servers: [
{ {
url: `http://${internalHost}:${internalPort}` url: browserGatewayUiUrl
} }
] ]
} }
@@ -1267,7 +1299,11 @@ export async function getTraefikConfig(
`bg-r${bgResource.resourceId}-assets-router` `bg-r${bgResource.resourceId}-assets-router`
] = { ] = {
entryPoints: [entrypoint], entryPoints: [entrypoint],
middlewares: routerMiddlewares, middlewares: [
...routerMiddlewares,
uiRewriteMiddlewareName,
uiHeadersMiddlewareName
],
service: bgUiServiceName, service: bgUiServiceName,
rule: `${hostRule} && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`) || Path(\`/favicon.ico\`))`, rule: `${hostRule} && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`) || Path(\`/favicon.ico\`))`,
priority: 101, priority: 101,
@@ -1279,7 +1315,11 @@ export async function getTraefikConfig(
`bg-r${bgResource.resourceId}-ui-router` `bg-r${bgResource.resourceId}-ui-router`
] = { ] = {
entryPoints: [entrypoint], entryPoints: [entrypoint],
middlewares: [...routerMiddlewares, uiRewriteMiddlewareName], middlewares: [
...routerMiddlewares,
uiRewriteMiddlewareName,
uiHeadersMiddlewareName
],
service: bgUiServiceName, service: bgUiServiceName,
rule: hostRule, rule: hostRule,
priority: 100, priority: 100,
@@ -1312,10 +1352,6 @@ export async function getTraefikConfig(
const siteResourceRouterName = `${srKey}-router`; const siteResourceRouterName = `${srKey}-router`;
const siteResourceRewriteMiddlewareName = `${srKey}-rewrite`; const siteResourceRewriteMiddlewareName = `${srKey}-rewrite`;
const maintenancePort = config.getRawConfig().server.next_port;
const maintenanceHost =
config.getRawConfig().server.internal_hostname;
if (!config_output.http.routers) { if (!config_output.http.routers) {
config_output.http.routers = {}; config_output.http.routers = {};
} }
@@ -1331,7 +1367,7 @@ export async function getTraefikConfig(
loadBalancer: { loadBalancer: {
servers: [ servers: [
{ {
url: `http://${maintenanceHost}:${maintenancePort}` url: maintenancePageUiUrl
} }
], ],
passHostHeader: true passHostHeader: true
+4 -2
View File
@@ -277,6 +277,8 @@ hybridRouter.get(
); );
} }
const pangolinUIUrl = config.getRawConfig().app.dashboard_url; // points to the dashboard to serve from there
try { try {
const traefikConfig = await getTraefikConfig( const traefikConfig = await getTraefikConfig(
remoteExitNode.exitNodeId, remoteExitNode.exitNodeId,
@@ -284,8 +286,8 @@ hybridRouter.get(
true, // But don't allow domain namespace resources true, // But don't allow domain namespace resources
false, // Dont include login pages, false, // Dont include login pages,
true, // allow raw resources true, // allow raw resources
false, // dont generate maintenance page pangolinUIUrl, // dont generate maintenance page
false // dont generate browser gateway targets pangolinUIUrl // generate browser gateway targets
); );
return response(res, { return response(res, {
@@ -54,7 +54,7 @@ export const handleNewtGetConfigMessage: MessageHandler = async (context) => {
// TODO: somehow we should make sure a recent hole punch has happened if this occurs (hole punch could be from the last restart if done quickly) // TODO: somehow we should make sure a recent hole punch has happened if this occurs (hole punch could be from the last restart if done quickly)
} }
if (existingSite.lastHolePunch && now - existingSite.lastHolePunch > 5) { if (existingSite.lastHolePunch && now - existingSite.lastHolePunch > 12) {
logger.warn( logger.warn(
`Site last hole punch is too old; skipping this register. The site is failing to hole punch and identify its network address with the server. Can the site reach the server on UDP port ${config.getRawConfig().gerbil.clients_start_port}?` `Site last hole punch is too old; skipping this register. The site is failing to hole punch and identify its network address with the server. Can the site reach the server on UDP port ${config.getRawConfig().gerbil.clients_start_port}?`
); );
@@ -348,7 +348,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
// this prevents us from accepting a register from an olm that has not hole punched yet. // this prevents us from accepting a register from an olm that has not hole punched yet.
// the olm will pump the register so we can keep checking // the olm will pump the register so we can keep checking
// TODO: I still think there is a better way to do this rather than locking it out here but ??? // TODO: I still think there is a better way to do this rather than locking it out here but ???
if (now - (client.lastHolePunch || 0) > 5 && sitesCount > 0) { if (now - (client.lastHolePunch || 0) > 12 && sitesCount > 0) {
logger.warn( logger.warn(
`[handleOlmRegisterMessage] Client last hole punch is too old and we have sites to send; skipping this register. The client is failing to hole punch and identify its network address with the server. Can the client reach the server on UDP port ${config.getRawConfig().gerbil.clients_start_port}?`, `[handleOlmRegisterMessage] Client last hole punch is too old and we have sites to send; skipping this register. The client is failing to hole punch and identify its network address with the server. Can the client reach the server on UDP port ${config.getRawConfig().gerbil.clients_start_port}?`,
{ orgId: client.orgId, clientId: client.clientId } { orgId: client.orgId, clientId: client.clientId }
@@ -17,13 +17,18 @@ export async function traefikConfigProvider(
// Get the current exit node name from config // Get the current exit node name from config
const currentExitNodeId = await getCurrentExitNodeId(); const currentExitNodeId = await getCurrentExitNodeId();
const maintenancePort = config.getRawConfig().server.next_port;
const maintenanceHost = config.getRawConfig().server.internal_hostname;
const pangolinUIUrl = `http://${maintenanceHost}:${maintenancePort}`;
const traefikConfig = await getTraefikConfig( const traefikConfig = await getTraefikConfig(
currentExitNodeId, currentExitNodeId,
config.getRawConfig().traefik.site_types, config.getRawConfig().traefik.site_types,
build == "oss", // filter out the namespace domains in open source build == "oss", // filter out the namespace domains in open source
build != "oss", // generate the login pages on the cloud and and enterprise, build != "oss", // generate the login pages on the cloud and and enterprise,
config.getRawConfig().traefik.allow_raw_resources, config.getRawConfig().traefik.allow_raw_resources,
build != "oss" // generate browser gateway resources on cloud and enterprise pangolinUIUrl,
pangolinUIUrl
); );
if (traefikConfig?.http?.middlewares) { if (traefikConfig?.http?.middlewares) {
+1 -1
View File
@@ -28,7 +28,7 @@ export default async function MaintenanceScreen() {
try { try {
const headersList = await headers(); const headersList = await headers();
const host = headersList.get("host") || ""; const host = headersList.get("p-host") || headersList.get("host") || "";
const hostname = host.split(":")[0]; const hostname = host.split(":")[0];
const res = await priv.get<AxiosResponse<GetMaintenanceInfoResponse>>( const res = await priv.get<AxiosResponse<GetMaintenanceInfoResponse>>(
+1 -1
View File
@@ -6,7 +6,7 @@ import { cache } from "react";
export const getBrowserTargetForRequest = cache(async () => { export const getBrowserTargetForRequest = cache(async () => {
const headersList = await headers(); const headersList = await headers();
const host = headersList.get("host") || ""; const host = headersList.get("p-host") || headersList.get("host") || "";
const hostname = host.split(":")[0]; const hostname = host.split(":")[0];
try { try {