mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-21 08:15:17 +00:00
Add gateway endpoints into the traefik config
This commit is contained in:
@@ -12,6 +12,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
browserGatewayTarget,
|
||||||
certificates,
|
certificates,
|
||||||
db,
|
db,
|
||||||
domainNamespaces,
|
domainNamespaces,
|
||||||
@@ -277,6 +278,115 @@ export async function getTraefikConfig(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Query browser gateway targets for this exit node
|
||||||
|
const browserGatewayRows = await db
|
||||||
|
.select({
|
||||||
|
// Resource fields
|
||||||
|
resourceId: resources.resourceId,
|
||||||
|
resourceName: resources.name,
|
||||||
|
fullDomain: resources.fullDomain,
|
||||||
|
ssl: resources.ssl,
|
||||||
|
subdomain: resources.subdomain,
|
||||||
|
domainId: resources.domainId,
|
||||||
|
enabled: resources.enabled,
|
||||||
|
wildcard: resources.wildcard,
|
||||||
|
domainCertResolver: domains.certResolver,
|
||||||
|
preferWildcardCert: domains.preferWildcardCert,
|
||||||
|
domainNamespaceId: domainNamespaces.domainNamespaceId,
|
||||||
|
// Browser gateway target fields
|
||||||
|
browserGatewayTargetId: browserGatewayTarget.browserGatewayTargetId,
|
||||||
|
bgType: browserGatewayTarget.type,
|
||||||
|
// Site fields
|
||||||
|
siteId: sites.siteId,
|
||||||
|
siteType: sites.type,
|
||||||
|
siteOnline: sites.online,
|
||||||
|
subnet: sites.subnet,
|
||||||
|
siteExitNodeId: sites.exitNodeId
|
||||||
|
})
|
||||||
|
.from(browserGatewayTarget)
|
||||||
|
.innerJoin(sites, eq(sites.siteId, browserGatewayTarget.siteId))
|
||||||
|
.innerJoin(
|
||||||
|
resources,
|
||||||
|
eq(resources.resourceId, browserGatewayTarget.resourceId)
|
||||||
|
)
|
||||||
|
.leftJoin(domains, eq(domains.domainId, resources.domainId))
|
||||||
|
.leftJoin(
|
||||||
|
domainNamespaces,
|
||||||
|
eq(domainNamespaces.domainId, resources.domainId)
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(resources.enabled, true),
|
||||||
|
or(
|
||||||
|
eq(sites.exitNodeId, exitNodeId),
|
||||||
|
and(
|
||||||
|
isNull(sites.exitNodeId),
|
||||||
|
sql`(${siteTypes.includes("local") ? 1 : 0} = 1)`,
|
||||||
|
eq(sites.type, "local"),
|
||||||
|
sql`(${build != "saas" ? 1 : 0} = 1)`
|
||||||
|
)
|
||||||
|
),
|
||||||
|
inArray(sites.type, siteTypes)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Group browser gateway targets by resource
|
||||||
|
type BrowserGatewayResourceEntry = {
|
||||||
|
resourceId: number;
|
||||||
|
name: string;
|
||||||
|
fullDomain: string | null;
|
||||||
|
ssl: boolean | null;
|
||||||
|
subdomain: string | null;
|
||||||
|
domainId: string | null;
|
||||||
|
enabled: boolean | null;
|
||||||
|
wildcard: boolean | null;
|
||||||
|
domainCertResolver: string | null;
|
||||||
|
preferWildcardCert: boolean | null;
|
||||||
|
targets: {
|
||||||
|
browserGatewayTargetId: number;
|
||||||
|
bgType: string;
|
||||||
|
siteId: number;
|
||||||
|
siteType: string;
|
||||||
|
siteOnline: boolean | null;
|
||||||
|
subnet: string | null;
|
||||||
|
siteExitNodeId: number | null;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
const browserGatewayResourcesMap = new Map<
|
||||||
|
number,
|
||||||
|
BrowserGatewayResourceEntry
|
||||||
|
>();
|
||||||
|
|
||||||
|
for (const row of browserGatewayRows) {
|
||||||
|
if (filterOutNamespaceDomains && row.domainNamespaceId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!browserGatewayResourcesMap.has(row.resourceId)) {
|
||||||
|
browserGatewayResourcesMap.set(row.resourceId, {
|
||||||
|
resourceId: row.resourceId,
|
||||||
|
name: sanitize(row.resourceName) || "",
|
||||||
|
fullDomain: row.fullDomain,
|
||||||
|
ssl: row.ssl,
|
||||||
|
subdomain: row.subdomain,
|
||||||
|
domainId: row.domainId,
|
||||||
|
enabled: row.enabled,
|
||||||
|
wildcard: row.wildcard,
|
||||||
|
domainCertResolver: row.domainCertResolver,
|
||||||
|
preferWildcardCert: row.preferWildcardCert,
|
||||||
|
targets: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
browserGatewayResourcesMap.get(row.resourceId)!.targets.push({
|
||||||
|
browserGatewayTargetId: row.browserGatewayTargetId,
|
||||||
|
bgType: row.bgType,
|
||||||
|
siteId: row.siteId,
|
||||||
|
siteType: row.siteType,
|
||||||
|
siteOnline: row.siteOnline,
|
||||||
|
subnet: row.subnet,
|
||||||
|
siteExitNodeId: row.siteExitNodeId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let siteResourcesWithFullDomain: {
|
let siteResourcesWithFullDomain: {
|
||||||
siteResourceId: number;
|
siteResourceId: number;
|
||||||
fullDomain: string | null;
|
fullDomain: string | null;
|
||||||
@@ -324,6 +434,12 @@ export async function getTraefikConfig(
|
|||||||
domains.add(sr.fullDomain);
|
domains.add(sr.fullDomain);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Include browser gateway resource domains
|
||||||
|
for (const bgResource of browserGatewayResourcesMap.values()) {
|
||||||
|
if (bgResource.enabled && bgResource.ssl && bgResource.fullDomain) {
|
||||||
|
domains.add(bgResource.fullDomain);
|
||||||
|
}
|
||||||
|
}
|
||||||
// get the valid certs for these domains
|
// get the valid certs for these domains
|
||||||
validCerts = await getValidCertificatesForDomains(domains, true); // we are caching here because this is called often
|
validCerts = await getValidCertificatesForDomains(domains, true); // we are caching here because this is called often
|
||||||
// logger.debug(`Valid certs for domains: ${JSON.stringify(validCerts)}`);
|
// logger.debug(`Valid certs for domains: ${JSON.stringify(validCerts)}`);
|
||||||
@@ -925,6 +1041,164 @@ export async function getTraefikConfig(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate Traefik config for browser gateway resources
|
||||||
|
const browserGatewayPort = 39999;
|
||||||
|
for (const [, bgResource] of browserGatewayResourcesMap.entries()) {
|
||||||
|
if (!bgResource.enabled) continue;
|
||||||
|
if (!bgResource.domainId) continue;
|
||||||
|
if (!bgResource.fullDomain) continue;
|
||||||
|
|
||||||
|
if (!config_output.http.routers) config_output.http.routers = {};
|
||||||
|
if (!config_output.http.services) config_output.http.services = {};
|
||||||
|
|
||||||
|
const fullDomain = bgResource.fullDomain;
|
||||||
|
const additionalMiddlewares =
|
||||||
|
config.getRawConfig().traefik.additional_middlewares || [];
|
||||||
|
const routerMiddlewares = [
|
||||||
|
badgerMiddlewareName,
|
||||||
|
...additionalMiddlewares
|
||||||
|
];
|
||||||
|
|
||||||
|
const hostRule = `Host(\`${fullDomain}\`)`;
|
||||||
|
|
||||||
|
// Build TLS config
|
||||||
|
let tls = {};
|
||||||
|
if (!privateConfig.getRawPrivateConfig().flags.use_pangolin_dns) {
|
||||||
|
const domainParts = fullDomain.split(".");
|
||||||
|
let wildCard: string;
|
||||||
|
if (domainParts.length <= 2) {
|
||||||
|
wildCard = `*.${domainParts.join(".")}`;
|
||||||
|
} else {
|
||||||
|
wildCard = `*.${domainParts.slice(1).join(".")}`;
|
||||||
|
}
|
||||||
|
if (!bgResource.subdomain) {
|
||||||
|
wildCard = fullDomain;
|
||||||
|
}
|
||||||
|
|
||||||
|
const globalDefaultResolver =
|
||||||
|
config.getRawConfig().traefik.cert_resolver;
|
||||||
|
const globalDefaultPreferWildcard =
|
||||||
|
config.getRawConfig().traefik.prefer_wildcard_cert;
|
||||||
|
const resolverName = bgResource.domainCertResolver
|
||||||
|
? bgResource.domainCertResolver.trim()
|
||||||
|
: globalDefaultResolver;
|
||||||
|
const preferWildcard =
|
||||||
|
bgResource.preferWildcardCert !== undefined &&
|
||||||
|
bgResource.preferWildcardCert !== null
|
||||||
|
? bgResource.preferWildcardCert
|
||||||
|
: globalDefaultPreferWildcard;
|
||||||
|
|
||||||
|
tls = {
|
||||||
|
certResolver: resolverName,
|
||||||
|
...(preferWildcard ? { domains: [{ main: wildCard }] } : {})
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const matchingCert = validCerts.find(
|
||||||
|
(cert) => cert.queriedDomain === fullDomain
|
||||||
|
);
|
||||||
|
if (!matchingCert) {
|
||||||
|
logger.debug(
|
||||||
|
`No matching certificate found for browser gateway domain: ${fullDomain}`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bgUiServiceName = `bg-r${bgResource.resourceId}-ui-service`;
|
||||||
|
|
||||||
|
if (bgResource.ssl) {
|
||||||
|
const redirectRouterName = `bg-r${bgResource.resourceId}-redirect`;
|
||||||
|
config_output.http.routers![redirectRouterName] = {
|
||||||
|
entryPoints: [config.getRawConfig().traefik.http_entrypoint],
|
||||||
|
middlewares: [redirectHttpsMiddlewareName],
|
||||||
|
service: bgUiServiceName,
|
||||||
|
rule: hostRule,
|
||||||
|
priority: 100
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect online sites for this resource (for any type)
|
||||||
|
const anySiteOnline = bgResource.targets.some((t) => t.siteOnline);
|
||||||
|
|
||||||
|
// Group targets by type and generate per-type websocket routers and services
|
||||||
|
const typeMap = new Map<string, typeof bgResource.targets>();
|
||||||
|
for (const t of bgResource.targets) {
|
||||||
|
if (!typeMap.has(t.bgType)) typeMap.set(t.bgType, []);
|
||||||
|
typeMap.get(t.bgType)!.push(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [bgType, typedTargets] of typeMap.entries()) {
|
||||||
|
const bgKey = `bg-r${bgResource.resourceId}-${bgType}`;
|
||||||
|
const bgRouterName = `${bgKey}-router`;
|
||||||
|
const bgServiceName = `${bgKey}-service`;
|
||||||
|
const bgRule = `${hostRule} && PathPrefix(\`/gateway/${bgType}\`)`;
|
||||||
|
|
||||||
|
const servers = typedTargets
|
||||||
|
.filter((t) => {
|
||||||
|
if (!t.siteOnline && anySiteOnline) return false;
|
||||||
|
if (t.siteType === "newt") return !!t.subnet;
|
||||||
|
return false; // browser gateway only supported on newt sites
|
||||||
|
})
|
||||||
|
.map((t) => ({
|
||||||
|
url: `http://${t.subnet!.split("/")[0]}:${browserGatewayPort}`
|
||||||
|
}))
|
||||||
|
.filter((v, i, a) => a.findIndex((u) => u.url === v.url) === i);
|
||||||
|
|
||||||
|
config_output.http.routers![bgRouterName] = {
|
||||||
|
entryPoints: [
|
||||||
|
bgResource.ssl
|
||||||
|
? config.getRawConfig().traefik.https_entrypoint
|
||||||
|
: config.getRawConfig().traefik.http_entrypoint
|
||||||
|
],
|
||||||
|
middlewares: routerMiddlewares,
|
||||||
|
service: bgServiceName,
|
||||||
|
rule: bgRule,
|
||||||
|
priority: 110, // higher than 105 (UI router) to match /gateway/* first
|
||||||
|
...(bgResource.ssl ? { tls } : {})
|
||||||
|
};
|
||||||
|
|
||||||
|
config_output.http.services![bgServiceName] = {
|
||||||
|
loadBalancer: {
|
||||||
|
servers
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI router: serve the browser gateway pages from the internal pangolin instance
|
||||||
|
// Covers /{type} paths for each configured type plus Next.js assets
|
||||||
|
const internalHost = config.getRawConfig().server.internal_hostname;
|
||||||
|
const internalPort = config.getRawConfig().server.next_port;
|
||||||
|
|
||||||
|
const typePaths = Array.from(typeMap.keys())
|
||||||
|
.map((t) => `PathPrefix(\`/${t}\`)`)
|
||||||
|
.join(" || ");
|
||||||
|
const uiRule = `${hostRule} && (${typePaths} || PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`))`;
|
||||||
|
|
||||||
|
config_output.http.services![bgUiServiceName] = {
|
||||||
|
loadBalancer: {
|
||||||
|
servers: [
|
||||||
|
{
|
||||||
|
url: `http://${internalHost}:${internalPort}`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
config_output.http.routers![`bg-r${bgResource.resourceId}-ui-router`] =
|
||||||
|
{
|
||||||
|
entryPoints: [
|
||||||
|
bgResource.ssl
|
||||||
|
? config.getRawConfig().traefik.https_entrypoint
|
||||||
|
: config.getRawConfig().traefik.http_entrypoint
|
||||||
|
],
|
||||||
|
middlewares: routerMiddlewares,
|
||||||
|
service: bgUiServiceName,
|
||||||
|
rule: uiRule,
|
||||||
|
priority: 105,
|
||||||
|
...(bgResource.ssl ? { tls } : {})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Add Traefik routes for siteResource aliases (HTTP mode + SSL) so that
|
// Add Traefik routes for siteResource aliases (HTTP mode + SSL) so that
|
||||||
// Traefik generates TLS certificates for those domains even when no
|
// Traefik generates TLS certificates for those domains even when no
|
||||||
// matching resource exists yet.
|
// matching resource exists yet.
|
||||||
|
|||||||
@@ -235,6 +235,11 @@ export async function buildTargetConfigurationForNewtClient(
|
|||||||
.from(targetHealthCheck)
|
.from(targetHealthCheck)
|
||||||
.where(eq(targetHealthCheck.siteId, siteId));
|
.where(eq(targetHealthCheck.siteId, siteId));
|
||||||
|
|
||||||
|
const allBrowserGatewayTargets = await db
|
||||||
|
.select()
|
||||||
|
.from(browserGatewayTarget)
|
||||||
|
.where(eq(browserGatewayTarget.siteId, siteId));
|
||||||
|
|
||||||
const { tcpTargets, udpTargets } = allTargets.reduce(
|
const { tcpTargets, udpTargets } = allTargets.reduce(
|
||||||
(acc, target) => {
|
(acc, target) => {
|
||||||
// Filter out invalid targets
|
// Filter out invalid targets
|
||||||
@@ -306,18 +311,17 @@ export async function buildTargetConfigurationForNewtClient(
|
|||||||
(target) => target !== null
|
(target) => target !== null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const browserGatewayTargets = allBrowserGatewayTargets.map((t) => ({
|
||||||
|
id: t.browserGatewayTargetId,
|
||||||
|
type: t.type,
|
||||||
|
destination: t.destination,
|
||||||
|
destinationPort: t.destinationPort
|
||||||
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
validHealthCheckTargets,
|
validHealthCheckTargets,
|
||||||
tcpTargets,
|
tcpTargets,
|
||||||
udpTargets
|
udpTargets,
|
||||||
|
browserGatewayTargets
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function buildBrowserGatewayTargetConfigurationForNewtClient(
|
|
||||||
siteId: number
|
|
||||||
): Promise<BrowserGatewayTarget[]> {
|
|
||||||
return await db
|
|
||||||
.select()
|
|
||||||
.from(browserGatewayTarget)
|
|
||||||
.where(eq(browserGatewayTarget.siteId, siteId));
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -43,8 +43,13 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
|
|||||||
|
|
||||||
const siteId = newt.siteId;
|
const siteId = newt.siteId;
|
||||||
|
|
||||||
const { publicKey, pingResults, newtVersion, backwardsCompatible, chainId } =
|
const {
|
||||||
message.data;
|
publicKey,
|
||||||
|
pingResults,
|
||||||
|
newtVersion,
|
||||||
|
backwardsCompatible,
|
||||||
|
chainId
|
||||||
|
} = message.data;
|
||||||
if (!publicKey) {
|
if (!publicKey) {
|
||||||
logger.warn("Public key not provided");
|
logger.warn("Public key not provided");
|
||||||
return;
|
return;
|
||||||
@@ -191,8 +196,12 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
|
|||||||
.where(eq(newts.newtId, newt.newtId));
|
.where(eq(newts.newtId, newt.newtId));
|
||||||
}
|
}
|
||||||
|
|
||||||
const { tcpTargets, udpTargets, validHealthCheckTargets } =
|
const {
|
||||||
await buildTargetConfigurationForNewtClient(siteId, newtVersion);
|
tcpTargets,
|
||||||
|
udpTargets,
|
||||||
|
validHealthCheckTargets,
|
||||||
|
browserGatewayTargets
|
||||||
|
} = await buildTargetConfigurationForNewtClient(siteId, newtVersion);
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Sending health check targets to newt ${newt.newtId}: ${JSON.stringify(validHealthCheckTargets)}`
|
`Sending health check targets to newt ${newt.newtId}: ${JSON.stringify(validHealthCheckTargets)}`
|
||||||
@@ -212,6 +221,7 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
|
|||||||
tcp: tcpTargets
|
tcp: tcpTargets
|
||||||
},
|
},
|
||||||
healthCheckTargets: validHealthCheckTargets,
|
healthCheckTargets: validHealthCheckTargets,
|
||||||
|
browserGatewayTargets: browserGatewayTargets,
|
||||||
chainId: chainId
|
chainId: chainId
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,18 +3,18 @@ import { eq } from "drizzle-orm";
|
|||||||
import { sendToClient } from "#dynamic/routers/ws";
|
import { sendToClient } from "#dynamic/routers/ws";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import {
|
import {
|
||||||
buildBrowserGatewayTargetConfigurationForNewtClient,
|
|
||||||
buildClientConfigurationForNewtClient,
|
buildClientConfigurationForNewtClient,
|
||||||
buildTargetConfigurationForNewtClient
|
buildTargetConfigurationForNewtClient
|
||||||
} from "./buildConfiguration";
|
} from "./buildConfiguration";
|
||||||
import { canCompress } from "@server/lib/clientVersionChecks";
|
import { canCompress } from "@server/lib/clientVersionChecks";
|
||||||
|
|
||||||
export async function sendNewtSyncMessage(newt: Newt, site: Site) {
|
export async function sendNewtSyncMessage(newt: Newt, site: Site) {
|
||||||
const { tcpTargets, udpTargets, validHealthCheckTargets } =
|
const {
|
||||||
await buildTargetConfigurationForNewtClient(site.siteId);
|
tcpTargets,
|
||||||
|
udpTargets,
|
||||||
const browserGatewayTargets =
|
validHealthCheckTargets,
|
||||||
await buildBrowserGatewayTargetConfigurationForNewtClient(site.siteId);
|
browserGatewayTargets
|
||||||
|
} = await buildTargetConfigurationForNewtClient(site.siteId);
|
||||||
|
|
||||||
let exitNode: ExitNode | undefined;
|
let exitNode: ExitNode | undefined;
|
||||||
if (site.exitNodeId) {
|
if (site.exitNodeId) {
|
||||||
@@ -41,14 +41,7 @@ export async function sendNewtSyncMessage(newt: Newt, site: Site) {
|
|||||||
healthCheckTargets: validHealthCheckTargets,
|
healthCheckTargets: validHealthCheckTargets,
|
||||||
peers: peers,
|
peers: peers,
|
||||||
clientTargets: targets,
|
clientTargets: targets,
|
||||||
browserGatewayTargets: browserGatewayTargets.map((t) => ({
|
browserGatewayTargets: browserGatewayTargets
|
||||||
id: t.browserGatewayTargetId,
|
|
||||||
resourceId: t.resourceId,
|
|
||||||
siteId: t.siteId,
|
|
||||||
type: t.type,
|
|
||||||
destination: t.destination,
|
|
||||||
destinationPort: t.destinationPort
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -58,11 +58,11 @@ const isIronError = (error: unknown): error is IronError => {
|
|||||||
export default function RdpClient() {
|
export default function RdpClient() {
|
||||||
const [form, setForm] = useState<FormState>({
|
const [form, setForm] = useState<FormState>({
|
||||||
username: "Administrator",
|
username: "Administrator",
|
||||||
password: "Wdvwy1W*ITK-(OK.sW?nVK%?mTl30wL0",
|
password: "Password123!",
|
||||||
gatewayAddress: "ws://localhost:7171/jet/rdp",
|
gatewayAddress: "ws://localhost:8082/rdp",
|
||||||
hostname: "172.31.3.58:3389",
|
hostname: "172.31.3.58:3389",
|
||||||
domain: "",
|
domain: "",
|
||||||
authtoken: "abc123",
|
authtoken: "pangolin-browser-gateway-dev",
|
||||||
kdcProxyUrl: "",
|
kdcProxyUrl: "",
|
||||||
pcb: "",
|
pcb: "",
|
||||||
desktopWidth: 1280,
|
desktopWidth: 1280,
|
||||||
|
|||||||
Reference in New Issue
Block a user