Basic clients working

This commit is contained in:
Owen 2025-07-27 10:21:27 -07:00
parent 15adfcca8c
commit 28f8b05dbc
No known key found for this signature in database
GPG key ID: 8271FDFFD9E0CCBD
21 changed files with 387 additions and 87 deletions

View file

@ -94,7 +94,8 @@ export const resources = pgTable("resources", {
enabled: boolean("enabled").notNull().default(true),
stickySession: boolean("stickySession").notNull().default(false),
tlsServerName: varchar("tlsServerName"),
setHostHeader: varchar("setHostHeader")
setHostHeader: varchar("setHostHeader"),
enableProxy: boolean("enableProxy").notNull().default(true),
});
export const targets = pgTable("targets", {

View file

@ -106,7 +106,8 @@ export const resources = sqliteTable("resources", {
.notNull()
.default(false),
tlsServerName: text("tlsServerName"),
setHostHeader: text("setHostHeader")
setHostHeader: text("setHostHeader"),
enableProxy: integer("enableProxy", { mode: "boolean" }).default(true),
});
export const targets = sqliteTable("targets", {

View file

@ -147,7 +147,8 @@ export async function updateClient(
endpoint: site.endpoint,
publicKey: site.publicKey,
serverIP: site.address,
serverPort: site.listenPort
serverPort: site.listenPort,
remoteSubnets: site.remoteSubnets
});
}

View file

@ -2,9 +2,16 @@ import { z } from "zod";
import { MessageHandler } from "../ws";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { db, ExitNode, exitNodes } from "@server/db";
import {
db,
ExitNode,
exitNodes,
resources,
Target,
targets
} from "@server/db";
import { clients, clientSites, Newt, sites } from "@server/db";
import { eq } from "drizzle-orm";
import { eq, and, inArray } from "drizzle-orm";
import { updatePeer } from "../olm/peers";
import axios from "axios";
@ -191,7 +198,8 @@ export const handleGetConfigMessage: MessageHandler = async (context) => {
endpoint: endpoint,
publicKey: site.publicKey,
serverIP: site.address,
serverPort: site.listenPort
serverPort: site.listenPort,
remoteSubnets: site.remoteSubnets
});
} catch (error) {
logger.error(
@ -212,14 +220,96 @@ export const handleGetConfigMessage: MessageHandler = async (context) => {
// Filter out any null values from peers that didn't have an olm
const validPeers = peers.filter((peer) => peer !== null);
// Improved version
const allResources = await db.transaction(async (tx) => {
// First get all resources for the site
const resourcesList = await tx
.select({
resourceId: resources.resourceId,
subdomain: resources.subdomain,
fullDomain: resources.fullDomain,
ssl: resources.ssl,
blockAccess: resources.blockAccess,
sso: resources.sso,
emailWhitelistEnabled: resources.emailWhitelistEnabled,
http: resources.http,
proxyPort: resources.proxyPort,
protocol: resources.protocol
})
.from(resources)
.where(and(eq(resources.siteId, siteId), eq(resources.http, false)));
// Get all enabled targets for these resources in a single query
const resourceIds = resourcesList.map((r) => r.resourceId);
const allTargets =
resourceIds.length > 0
? await tx
.select({
resourceId: targets.resourceId,
targetId: targets.targetId,
ip: targets.ip,
method: targets.method,
port: targets.port,
internalPort: targets.internalPort,
enabled: targets.enabled,
})
.from(targets)
.where(
and(
inArray(targets.resourceId, resourceIds),
eq(targets.enabled, true)
)
)
: [];
// Combine the data in JS instead of using SQL for the JSON
return resourcesList.map((resource) => ({
...resource,
targets: allTargets.filter(
(target) => target.resourceId === resource.resourceId
)
}));
});
const { tcpTargets, udpTargets } = allResources.reduce(
(acc, resource) => {
// Skip resources with no targets
if (!resource.targets?.length) return acc;
// Format valid targets into strings
const formattedTargets = resource.targets
.filter(
(target: Target) =>
resource.proxyPort && target?.ip && target?.port
)
.map(
(target: Target) =>
`${resource.proxyPort}:${target.ip}:${target.port}`
);
// Add to the appropriate protocol array
if (resource.protocol === "tcp") {
acc.tcpTargets.push(...formattedTargets);
} else {
acc.udpTargets.push(...formattedTargets);
}
return acc;
},
{ tcpTargets: [] as string[], udpTargets: [] as string[] }
);
// Build the configuration response
const configResponse = {
ipAddress: site.address,
peers: validPeers
peers: validPeers,
targets: {
udp: udpTargets,
tcp: tcpTargets
}
};
logger.debug("Sending config: ", configResponse);
return {
message: {
type: "newt/wg/receive-config",

View file

@ -4,7 +4,8 @@ import { sendToClient } from "../ws";
export function addTargets(
newtId: string,
targets: Target[],
protocol: string
protocol: string,
port: number | null = null
) {
//create a list of udp and tcp targets
const payloadTargets = targets.map((target) => {
@ -13,19 +14,32 @@ export function addTargets(
}:${target.port}`;
});
const payload = {
sendToClient(newtId, {
type: `newt/${protocol}/add`,
data: {
targets: payloadTargets
}
};
sendToClient(newtId, payload);
});
const payloadTargetsResources = targets.map((target) => {
return `${port ? port + ":" : ""}${
target.ip
}:${target.port}`;
});
sendToClient(newtId, {
type: `newt/wg/${protocol}/add`,
data: {
targets: [payloadTargetsResources[0]] // We can only use one target for WireGuard right now
}
});
}
export function removeTargets(
newtId: string,
targets: Target[],
protocol: string
protocol: string,
port: number | null = null
) {
//create a list of udp and tcp targets
const payloadTargets = targets.map((target) => {
@ -34,11 +48,23 @@ export function removeTargets(
}:${target.port}`;
});
const payload = {
sendToClient(newtId, {
type: `newt/${protocol}/remove`,
data: {
targets: payloadTargets
}
};
sendToClient(newtId, payload);
});
const payloadTargetsResources = targets.map((target) => {
return `${port ? port + ":" : ""}${
target.ip
}:${target.port}`;
});
sendToClient(newtId, {
type: `newt/wg/${protocol}/remove`,
data: {
targets: [payloadTargetsResources[0]] // We can only use one target for WireGuard right now
}
});
}

View file

@ -119,12 +119,12 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
continue;
}
if (site.lastHolePunch && now - site.lastHolePunch > 6 && relay) {
logger.warn(
`Site ${site.siteId} last hole punch is too old, skipping`
);
continue;
}
// if (site.lastHolePunch && now - site.lastHolePunch > 6 && relay) {
// logger.warn(
// `Site ${site.siteId} last hole punch is too old, skipping`
// );
// continue;
// }
// If public key changed, delete old peer from this site
if (client.pubKey && client.pubKey != publicKey) {
@ -175,7 +175,8 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
endpoint: endpoint,
publicKey: site.publicKey,
serverIP: site.address,
serverPort: site.listenPort
serverPort: site.listenPort,
remoteSubnets: site.remoteSubnets
});
}

View file

@ -12,6 +12,7 @@ export async function addPeer(
endpoint: string;
serverIP: string | null;
serverPort: number | null;
remoteSubnets: string | null; // optional, comma-separated list of subnets that this site can access
}
) {
const [olm] = await db
@ -30,7 +31,8 @@ export async function addPeer(
publicKey: peer.publicKey,
endpoint: peer.endpoint,
serverIP: peer.serverIP,
serverPort: peer.serverPort
serverPort: peer.serverPort,
remoteSubnets: peer.remoteSubnets // optional, comma-separated list of subnets that this site can access
}
});
@ -66,6 +68,7 @@ export async function updatePeer(
endpoint: string;
serverIP: string | null;
serverPort: number | null;
remoteSubnets?: string | null; // optional, comma-separated list of subnets that
}
) {
const [olm] = await db
@ -84,7 +87,8 @@ export async function updatePeer(
publicKey: peer.publicKey,
endpoint: peer.endpoint,
serverIP: peer.serverIP,
serverPort: peer.serverPort
serverPort: peer.serverPort,
remoteSubnets: peer.remoteSubnets
}
});

View file

@ -40,7 +40,7 @@ const createHttpResourceSchema = z
siteId: z.number(),
http: z.boolean(),
protocol: z.enum(["tcp", "udp"]),
domainId: z.string()
domainId: z.string(),
})
.strict()
.refine(
@ -59,7 +59,8 @@ const createRawResourceSchema = z
siteId: z.number(),
http: z.boolean(),
protocol: z.enum(["tcp", "udp"]),
proxyPort: z.number().int().min(1).max(65535)
proxyPort: z.number().int().min(1).max(65535),
enableProxy: z.boolean().default(true)
})
.strict()
.refine(
@ -378,7 +379,7 @@ async function createRawResource(
);
}
const { name, http, protocol, proxyPort } = parsedBody.data;
const { name, http, protocol, proxyPort, enableProxy } = parsedBody.data;
// if http is false check to see if there is already a resource with the same port and protocol
const existingResource = await db
@ -411,7 +412,8 @@ async function createRawResource(
name,
http,
protocol,
proxyPort
proxyPort,
enableProxy
})
.returning();

View file

@ -103,7 +103,8 @@ export async function deleteResource(
removeTargets(
newt.newtId,
targetsToBeRemoved,
deletedResource.protocol
deletedResource.protocol,
deletedResource.proxyPort
);
}
}

View file

@ -168,7 +168,8 @@ export async function transferResource(
removeTargets(
newt.newtId,
resourceTargets,
updatedResource.protocol
updatedResource.protocol,
updatedResource.proxyPort
);
}
}
@ -190,7 +191,8 @@ export async function transferResource(
addTargets(
newt.newtId,
resourceTargets,
updatedResource.protocol
updatedResource.protocol,
updatedResource.proxyPort
);
}
}

View file

@ -93,7 +93,8 @@ const updateRawResourceBodySchema = z
name: z.string().min(1).max(255).optional(),
proxyPort: z.number().int().min(1).max(65535).optional(),
stickySession: z.boolean().optional(),
enabled: z.boolean().optional()
enabled: z.boolean().optional(),
enableProxy: z.boolean().optional(),
})
.strict()
.refine((data) => Object.keys(data).length > 0, {

View file

@ -173,7 +173,7 @@ export async function createTarget(
.where(eq(newts.siteId, site.siteId))
.limit(1);
addTargets(newt.newtId, newTarget, resource.protocol);
addTargets(newt.newtId, newTarget, resource.protocol, resource.proxyPort);
}
}
}

View file

@ -105,7 +105,7 @@ export async function deleteTarget(
.where(eq(newts.siteId, site.siteId))
.limit(1);
removeTargets(newt.newtId, [deletedTarget], resource.protocol);
removeTargets(newt.newtId, [deletedTarget], resource.protocol, resource.proxyPort);
}
}

View file

@ -157,7 +157,7 @@ export async function updateTarget(
.where(eq(newts.siteId, site.siteId))
.limit(1);
addTargets(newt.newtId, [updatedTarget], resource.protocol);
addTargets(newt.newtId, [updatedTarget], resource.protocol, resource.proxyPort);
}
}
return response(res, {

View file

@ -66,7 +66,8 @@ export async function traefikConfigProvider(
enabled: resources.enabled,
stickySession: resources.stickySession,
tlsServerName: resources.tlsServerName,
setHostHeader: resources.setHostHeader
setHostHeader: resources.setHostHeader,
enableProxy: resources.enableProxy
})
.from(resources)
.innerJoin(sites, eq(sites.siteId, resources.siteId))
@ -365,6 +366,10 @@ export async function traefikConfigProvider(
}
} else {
// Non-HTTP (TCP/UDP) configuration
if (!resource.enableProxy) {
continue;
}
const protocol = resource.protocol.toLowerCase();
const port = resource.proxyPort;

View file

@ -0,0 +1,29 @@
import { APP_PATH } from "@server/lib/consts";
import Database from "better-sqlite3";
import path from "path";
const version = "1.8.0";
export default async function migration() {
console.log("Running setup script ${version}...");
const location = path.join(APP_PATH, "db", "db.sqlite");
const db = new Database(location);
try {
db.transaction(() => {
db.exec(`
ALTER TABLE 'user' ADD 'termsAcceptedTimestamp' text;
ALTER TABLE 'user' ADD 'termsVersion' text;
ALTER TABLE 'sites' ADD 'remoteSubnets' text;
`);
})();
console.log("Migrated database schema");
} catch (e) {
console.log("Unable to migrate database schema");
throw e;
}
console.log(`${version} migration complete`);
}