Add AmneziaWG 3.1 support with key reissue

Protocol generation is now derived from awg1.conf rather than stored: an
interface speaks 3.1 when HeaderProtectionKey is set and 2.0 otherwise. The
3.0/3.1-only keys (HeaderProtectionKey, ContentPaddingAddition, the
Rekey/Reject/Keepalive timers, MaxHandshakeAttempts, RandomTrailers,
DisableCookies) are read from the conf and emitted only when non-empty, so 2.0
output stays byte-identical — verified by diffing both the client .conf and the
full vpn:// string against the previous implementation.

Changing generation invalidates every issued vpn:// key, so add
POST /api/users/reissue. It recovers the client private key from inside the
stored blob (the only place it exists), keeping ip/pub_key/psk_key intact, so
nothing changes on the wire and users only need to re-import. users gains
key_gen and vpn_key_prev, and users.db is snapshotted before the pass.

install.sh gates 3.1 on module 3.x and kernel >= 5.5 (header protection needs
the chacha library API, absent before 5.5) and falls back to writing a 2.0 conf
instead of aborting, so older kernels keep working as before.
HeaderProtectionKey is generated with awg genpsk and preserved on KEEP_DATA.

The panel marks keys issued on a different generation and offers to reissue
them; /api/v1 now returns gen alongside name/ip/vpn_key.

Also add MIT license headers across the awg-ui sources.
This commit is contained in:
2026-08-17 17:58:00 +03:00
parent faef87a59d
commit 44c637f7af
16 changed files with 413 additions and 43 deletions
+8 -4
View File
@@ -210,6 +210,10 @@ async function ctrl(method: string, urlPath: string, body?: unknown) {
});
}
// Внешний контракт: наружу отдаём только name/ip/gen/vpn_key — psk_key и pub_key
// остаются внутри. gen — поколение AmneziaWG, на параметрах которого выдан ключ.
interface ExtUser { name: string; ip: string; vpn_key: string; key_gen: string }
const ext = express.Router();
ext.use(requireApiKey);
@@ -217,8 +221,8 @@ ext.post("/users", async (req: Request, res: Response) => {
const { name } = (req.body ?? {}) as { name?: string };
const r = await ctrl("POST", "/api/users", { name });
if (r.status >= 400) { res.status(r.status).json(r.data); return; }
const u = r.data as { name: string; ip: string; vpn_key: string };
res.status(201).json({ name: u.name, ip: u.ip, vpn_key: u.vpn_key });
const u = r.data as ExtUser;
res.status(201).json({ name: u.name, ip: u.ip, gen: u.key_gen, vpn_key: u.vpn_key });
});
ext.get("/users", async (_req: Request, res: Response) => {
@@ -229,8 +233,8 @@ ext.get("/users", async (_req: Request, res: Response) => {
ext.get("/users/:name", async (req: Request, res: Response) => {
const r = await ctrl("POST", `/api/users/${encodeURIComponent(req.params.name)}`);
if (r.status >= 400) { res.status(r.status).json(r.data); return; }
const u = r.data as { name: string; ip: string; vpn_key: string };
res.json({ name: u.name, ip: u.ip, vpn_key: u.vpn_key });
const u = r.data as ExtUser;
res.json({ name: u.name, ip: u.ip, gen: u.key_gen, vpn_key: u.vpn_key });
});
ext.delete("/users/:name", async (req: Request, res: Response) => {