Caddy previously ran as a host binary in /opt/caddy with a wgui-caddy systemd/OpenRC unit. Only Caddy is containerized; the app still runs on the host as root because it shells out to wg, ip and iptables. - Add caddy-container-commands.ts as the single source of truth for the run/exec/validate/inspect argument arrays. Every value stays a separate argv element, the image is pinned to caddy:2.11.4-alpine, and the entrypoint is set explicitly because some mirrors publish the image with a null Entrypoint (which made Docker exec "run" as a binary). - Rewrite install-caddy.sh: require_docker with distro-specific hints, pull + recreate the container, assert it is running and echo the daemon error instead of discarding it, normalize the admin socket path in existing Caddyfiles, and clean up a legacy native install only after the container is up so the ingress port is never unbound. Add --purge-legacy / --purge for hosts that want to drop the old install without installing the container. - Move the admin socket to /var/lib/wgui/caddy/admin.sock so it lives inside the mounted Caddy dir and reloads work over docker exec. - Validate candidate configs in a throwaway container, mounting the TLS dir read-only as well: provisioning the tls app loads every certificate referenced by a tls directive, so without that mount a route with a certificate failed validation and its Caddyfile was never committed. - Report docker-based installed/version/service_active, skip validate/reload with a warning when the image is absent, and update the system setup tool check to look for docker. - Issue certificates with a docker exec reload command and chown files to root:root, since the container reads them through a read-only mount. - Update the Setup UI copy and docs (README, AGENTS.md). Also drops the unused EmptyContent import and a stray debug log in the client route form, and stops passing the unsupported cancel_href prop to ClientRouteForm from the route add/edit pages.
154 lines
4.7 KiB
TypeScript
154 lines
4.7 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import type {
|
|
BUN_SQLITE_WGUI_CLIENT_ROUTES,
|
|
BUN_SQLITE_WGUI_TLS_CERTIFICATES,
|
|
} from "@/db/types/db";
|
|
import buildCaddyfile from "./build-caddyfile";
|
|
|
|
const base = {
|
|
https_port: 8443,
|
|
http_port: 8081,
|
|
admin_socket: `/var/lib/wgui/caddy/admin.sock`,
|
|
storage_dir: `/var/lib/wgui/caddy/data`,
|
|
};
|
|
|
|
const certificate = {
|
|
hostname: "media.example.com",
|
|
cert_path: "/var/lib/wgui/tls/media.example.com/fullchain.pem",
|
|
key_path: "/var/lib/wgui/tls/media.example.com/privkey.pem",
|
|
status: "active",
|
|
} as BUN_SQLITE_WGUI_TLS_CERTIFICATES;
|
|
|
|
function route(
|
|
values: Partial<BUN_SQLITE_WGUI_CLIENT_ROUTES>,
|
|
): BUN_SQLITE_WGUI_CLIENT_ROUTES {
|
|
return {
|
|
hostname: "media.example.com",
|
|
path_prefix: "/",
|
|
upstream_url: "http://10.5.0.20:8080",
|
|
enabled: 1,
|
|
...values,
|
|
};
|
|
}
|
|
|
|
describe("buildCaddyfile", () => {
|
|
test("uses a high port and never 80/443", () => {
|
|
const res = buildCaddyfile({ ...base, routes: [], certificates: [] });
|
|
|
|
expect(res.config).toContain("https_port 8443");
|
|
expect(res.config).toContain("http_port 8081");
|
|
expect(res.config).not.toContain("auto_https disable_redirects");
|
|
expect(res.config).not.toContain("https_port 443");
|
|
expect(res.config).not.toContain("http_port 80\n");
|
|
});
|
|
|
|
test("can disable automatic HTTPS redirects", () => {
|
|
const res = buildCaddyfile({
|
|
...base,
|
|
redirect: false,
|
|
routes: [],
|
|
certificates: [],
|
|
});
|
|
|
|
expect(res.config).toContain("auto_https disable_redirects");
|
|
});
|
|
|
|
test("orders longer prefixes before the catch-all", () => {
|
|
const res = buildCaddyfile({
|
|
...base,
|
|
certificates: [certificate],
|
|
routes: [
|
|
route({ name: "root", path_prefix: "/" }),
|
|
route({
|
|
name: "app",
|
|
path_prefix: "/app",
|
|
strip_prefix: 1,
|
|
upstream_url: "http://10.5.0.30:9000",
|
|
}),
|
|
],
|
|
});
|
|
|
|
const app_index = res.config?.indexOf("@route_1 {") || 0;
|
|
const root_index = res.config?.indexOf("handle {") || 0;
|
|
|
|
expect(app_index).toBeGreaterThan(0);
|
|
expect(root_index).toBeGreaterThan(app_index);
|
|
expect(res.config).toContain("path /app /app/*");
|
|
expect(res.config).toContain("uri strip_prefix /app");
|
|
});
|
|
|
|
test("skips routes without an active certificate", () => {
|
|
const res = buildCaddyfile({
|
|
...base,
|
|
certificates: [],
|
|
routes: [route({ name: "media" })],
|
|
});
|
|
|
|
expect(res.config).not.toContain("media.example.com {");
|
|
expect(res.warnings[0]).toContain("certificate");
|
|
});
|
|
|
|
test("scopes a route to its host's WireGuard subnet", () => {
|
|
const res = buildCaddyfile({
|
|
...base,
|
|
certificates: [certificate],
|
|
hosts: [{ id: 0, wg_ip_address: "10.5.0.1" }],
|
|
routes: [route({ host_id: 0, path_prefix: "/app" })],
|
|
});
|
|
|
|
expect(res.config).toContain("remote_ip 10.5.0.0/24");
|
|
});
|
|
|
|
test("appends route allowed_ips to the remote_ip matcher", () => {
|
|
const res = buildCaddyfile({
|
|
...base,
|
|
certificates: [certificate],
|
|
hosts: [{ id: 0, wg_ip_address: "10.5.0.1" }],
|
|
routes: [
|
|
route({
|
|
host_id: 0,
|
|
path_prefix: "/app",
|
|
allowed_ips: "203.0.113.0/24, 198.51.100.0/24",
|
|
}),
|
|
],
|
|
});
|
|
|
|
expect(res.config).toContain(
|
|
"remote_ip 10.5.0.0/24 127.0.0.1/32 ::1/128 203.0.113.0/24 198.51.100.0/24",
|
|
);
|
|
});
|
|
|
|
test("maps insecure upstream TLS to Caddy", () => {
|
|
const res = buildCaddyfile({
|
|
...base,
|
|
certificates: [certificate],
|
|
routes: [
|
|
route({
|
|
upstream_url: "https://10.5.0.20:8443",
|
|
upstream_tls_verify: 0,
|
|
}),
|
|
],
|
|
});
|
|
|
|
expect(res.config).toContain("tls_insecure_skip_verify");
|
|
});
|
|
|
|
test("allows a catch-all route with allowed_ips to have a matcher", () => {
|
|
const res = buildCaddyfile({
|
|
...base,
|
|
certificates: [certificate],
|
|
hosts: [{ id: 0, wg_ip_address: "10.5.0.1" }],
|
|
routes: [
|
|
route({
|
|
host_id: 0,
|
|
path_prefix: "/",
|
|
allowed_ips: "203.0.113.0/24",
|
|
}),
|
|
],
|
|
});
|
|
|
|
expect(res.config).toContain("@route_1 remote_ip");
|
|
expect(res.config).toContain("203.0.113.0/24");
|
|
});
|
|
});
|