Initial commit: WireGuard config generator (génération de clés côté navigateur)
This commit is contained in:
commit
581a7c9ed3
11 files changed
+3014
No files matched your search
@@ -0,0 +1,2 @@
|
|||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
# Image "unprivileged" officielle : nginx tourne nativement en non-root, écoute sur un port >1024
|
||||||
|
FROM nginxinc/nginx-unprivileged:1.27-alpine
|
||||||
|
|
||||||
|
USER root
|
||||||
|
RUN rm -rf /usr/share/nginx/html/* /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
COPY app/ /usr/share/nginx/html/
|
||||||
|
|
||||||
|
RUN chown -R nginx:nginx /usr/share/nginx/html
|
||||||
|
|
||||||
|
USER nginx
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||||
|
CMD wget -qO- http://127.0.0.1:8080/healthz || exit 1
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# WireGuard Config Generator
|
||||||
|
|
||||||
|
Générateur de configurations WireGuard entièrement statique, sans backend.
|
||||||
|
|
||||||
|
La génération des paires de clés (X25519), des clés pré-partagées et des QR codes se fait **intégralement dans le navigateur** : aucune donnée (clé privée, clé publique, configuration) n'est jamais envoyée à un serveur.
|
||||||
|
|
||||||
|
## Aperçu
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Fonctionnalités
|
||||||
|
|
||||||
|
- Génération de paires de clés WireGuard (X25519) côté client via [TweetNaCl](https://github.com/dchest/tweetnacl-js)
|
||||||
|
- Génération optionnelle d'une clé pré-partagée (PresharedKey) par pair
|
||||||
|
- Attribution automatique des adresses IP à partir d'un réseau CIDR
|
||||||
|
- Support multi-clients : ajout/suppression dynamique de pairs
|
||||||
|
- Génération de la configuration serveur (`wg0.conf`) et d'un fichier `.conf` par client
|
||||||
|
- QR code par client pour import direct dans l'application mobile WireGuard
|
||||||
|
- Aucune dépendance réseau au runtime (CSP `connect-src 'none'`)
|
||||||
|
|
||||||
|
## Déploiement
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
L'application est alors disponible sur `http://<hôte>:8090/`.
|
||||||
|
|
||||||
|
## Stack technique
|
||||||
|
|
||||||
|
- HTML / CSS / JavaScript vanilla, aucun build step
|
||||||
|
- [TweetNaCl.js](https://github.com/dchest/tweetnacl-js) pour la cryptographie X25519
|
||||||
|
- [qrcode-generator](https://github.com/kazuhikoarase/qrcode-generator) pour les QR codes
|
||||||
|
- Servi par `nginx-unprivileged` (image non-root)
|
||||||
|
- Conteneur durci : `read_only`, `cap_drop: ALL`, `no-new-privileges`, en-têtes de sécurité (CSP, X-Frame-Options, etc.)
|
||||||
|
|
||||||
|
## Sécurité
|
||||||
|
|
||||||
|
Toutes les clés privées et publiques sont générées localement dans le navigateur et ne transitent jamais sur le réseau. Le serveur ne fait que servir des fichiers statiques.
|
||||||
+257
@@ -0,0 +1,257 @@
|
|||||||
|
(() => {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// ---------- base64 helpers ----------
|
||||||
|
function toBase64(bytes) {
|
||||||
|
let bin = "";
|
||||||
|
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
||||||
|
return btoa(bin);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- WireGuard key generation (X25519 via TweetNaCl) ----------
|
||||||
|
function genKeyPair() {
|
||||||
|
const kp = nacl.box.keyPair();
|
||||||
|
return {
|
||||||
|
privateKey: toBase64(kp.secretKey),
|
||||||
|
publicKey: toBase64(kp.publicKey),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function genPresharedKey() {
|
||||||
|
return toBase64(nacl.randomBytes(32));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- IP helpers (IPv4 only) ----------
|
||||||
|
function parseCidr(cidr) {
|
||||||
|
const [ip, prefixStr] = cidr.trim().split("/");
|
||||||
|
const prefix = parseInt(prefixStr, 10);
|
||||||
|
const parts = ip.split(".").map(Number);
|
||||||
|
if (parts.length !== 4 || parts.some((p) => isNaN(p) || p < 0 || p > 255) || isNaN(prefix)) {
|
||||||
|
throw new Error("Réseau CIDR invalide: " + cidr);
|
||||||
|
}
|
||||||
|
const base = ((parts[0] << 24) >>> 0) + (parts[1] << 16) + (parts[2] << 8) + parts[3];
|
||||||
|
return { base: base >>> 0, prefix };
|
||||||
|
}
|
||||||
|
|
||||||
|
function intToIp(int) {
|
||||||
|
return [
|
||||||
|
(int >>> 24) & 255,
|
||||||
|
(int >>> 16) & 255,
|
||||||
|
(int >>> 8) & 255,
|
||||||
|
int & 255,
|
||||||
|
].join(".");
|
||||||
|
}
|
||||||
|
|
||||||
|
function nthAddress(cidr, n) {
|
||||||
|
const { base, prefix } = parseCidr(cidr);
|
||||||
|
const hostBits = 32 - prefix;
|
||||||
|
const maxHosts = Math.pow(2, hostBits);
|
||||||
|
if (n >= maxHosts) throw new Error("Le réseau " + cidr + " est trop petit pour ce nombre de pairs.");
|
||||||
|
const network = (base >>> hostBits) << hostBits;
|
||||||
|
return intToIp((network >>> 0) + n);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- DOM ----------
|
||||||
|
const clientsList = document.getElementById("clients-list");
|
||||||
|
const addClientBtn = document.getElementById("add-client");
|
||||||
|
const generateBtn = document.getElementById("generate-btn");
|
||||||
|
const resetBtn = document.getElementById("reset-btn");
|
||||||
|
const resultsEl = document.getElementById("results");
|
||||||
|
const serverSummary = document.getElementById("server-summary");
|
||||||
|
const serverConfText = document.getElementById("server-conf-text");
|
||||||
|
|
||||||
|
let clientCounter = 0;
|
||||||
|
|
||||||
|
function addClientRow(name) {
|
||||||
|
clientCounter++;
|
||||||
|
const row = document.createElement("div");
|
||||||
|
row.className = "client-row";
|
||||||
|
row.innerHTML = `
|
||||||
|
<input type="text" class="client-name" placeholder="Nom du client" value="${name || "client" + clientCounter}" />
|
||||||
|
<button class="btn-danger remove-client" title="Supprimer">✕</button>
|
||||||
|
`;
|
||||||
|
row.querySelector(".remove-client").addEventListener("click", () => {
|
||||||
|
row.remove();
|
||||||
|
});
|
||||||
|
clientsList.appendChild(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
addClientBtn.addEventListener("click", () => addClientRow());
|
||||||
|
addClientRow("client1");
|
||||||
|
|
||||||
|
resetBtn.addEventListener("click", () => {
|
||||||
|
clientsList.innerHTML = "";
|
||||||
|
clientCounter = 0;
|
||||||
|
addClientRow("client1");
|
||||||
|
resultsEl.innerHTML = "";
|
||||||
|
serverSummary.style.display = "none";
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("click", (e) => {
|
||||||
|
if (e.target.matches("[data-copy-target]")) {
|
||||||
|
const targetId = e.target.getAttribute("data-copy-target");
|
||||||
|
const el = document.getElementById(targetId);
|
||||||
|
const text = el.tagName === "TEXTAREA" || el.tagName === "INPUT" ? el.value : el.textContent;
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
const original = e.target.textContent;
|
||||||
|
e.target.textContent = "Copié !";
|
||||||
|
setTimeout(() => (e.target.textContent = original), 1200);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function buildClientConfig({ clientPrivateKey, clientIp, network, dns, serverPubKey, endpoint, port, allowedIps, psk }) {
|
||||||
|
const prefixLen = network.split("/")[1];
|
||||||
|
let conf = `[Interface]\n`;
|
||||||
|
conf += `PrivateKey = ${clientPrivateKey}\n`;
|
||||||
|
conf += `Address = ${clientIp}/${prefixLen}\n`;
|
||||||
|
if (dns) conf += `DNS = ${dns}\n`;
|
||||||
|
conf += `\n[Peer]\n`;
|
||||||
|
conf += `PublicKey = ${serverPubKey}\n`;
|
||||||
|
if (psk) conf += `PresharedKey = ${psk}\n`;
|
||||||
|
conf += `Endpoint = ${endpoint}:${port}\n`;
|
||||||
|
conf += `AllowedIPs = ${allowedIps}\n`;
|
||||||
|
conf += `PersistentKeepalive = 25\n`;
|
||||||
|
return conf;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildServerPeerBlock({ name, clientPubKey, clientIp, psk }) {
|
||||||
|
let block = `# ${name}\n[Peer]\n`;
|
||||||
|
block += `PublicKey = ${clientPubKey}\n`;
|
||||||
|
if (psk) block += `PresharedKey = ${psk}\n`;
|
||||||
|
block += `AllowedIPs = ${clientIp}/32\n`;
|
||||||
|
return block;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderQr(container, text) {
|
||||||
|
try {
|
||||||
|
const qr = qrcode(0, "M");
|
||||||
|
qr.addData(text);
|
||||||
|
qr.make();
|
||||||
|
container.innerHTML = qr.createSvgTag(4, 8);
|
||||||
|
} catch (err) {
|
||||||
|
container.textContent = "QR indisponible (config trop longue)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadFile(filename, content) {
|
||||||
|
const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
generateBtn.addEventListener("click", () => {
|
||||||
|
resultsEl.innerHTML = "";
|
||||||
|
serverSummary.style.display = "none";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const endpoint = document.getElementById("srv-endpoint").value.trim();
|
||||||
|
const port = document.getElementById("srv-port").value.trim() || "51820";
|
||||||
|
const network = document.getElementById("srv-network").value.trim();
|
||||||
|
const serverIp = document.getElementById("srv-server-ip").value.trim();
|
||||||
|
const dns = document.getElementById("srv-dns").value.trim();
|
||||||
|
const allowedIps = document.getElementById("srv-allowed-ips").value.trim() || "0.0.0.0/0, ::/0";
|
||||||
|
const usePsk = document.getElementById("use-psk").checked;
|
||||||
|
let serverPubKeyInput = document.getElementById("srv-pubkey").value.trim();
|
||||||
|
|
||||||
|
if (!endpoint) {
|
||||||
|
alert("Merci de renseigner l'endpoint du serveur.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
parseCidr(network); // validate
|
||||||
|
|
||||||
|
const clientNames = Array.from(document.querySelectorAll(".client-name"))
|
||||||
|
.map((i) => i.value.trim())
|
||||||
|
.filter((v) => v.length > 0);
|
||||||
|
|
||||||
|
if (clientNames.length === 0) {
|
||||||
|
alert("Ajoutez au moins un client.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let serverKeyPair = null;
|
||||||
|
let serverPubKey = serverPubKeyInput;
|
||||||
|
if (!serverPubKey) {
|
||||||
|
serverKeyPair = genKeyPair();
|
||||||
|
serverPubKey = serverKeyPair.publicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverPeerBlocks = [];
|
||||||
|
const resultCards = [];
|
||||||
|
|
||||||
|
clientNames.forEach((name, idx) => {
|
||||||
|
const clientIp = nthAddress(network, idx + 2); // .1 reserved for server
|
||||||
|
const clientKeys = genKeyPair();
|
||||||
|
const psk = usePsk ? genPresharedKey() : null;
|
||||||
|
|
||||||
|
const clientConf = buildClientConfig({
|
||||||
|
clientPrivateKey: clientKeys.privateKey,
|
||||||
|
clientIp,
|
||||||
|
network,
|
||||||
|
dns,
|
||||||
|
serverPubKey,
|
||||||
|
endpoint,
|
||||||
|
port,
|
||||||
|
allowedIps,
|
||||||
|
psk,
|
||||||
|
});
|
||||||
|
|
||||||
|
serverPeerBlocks.push(
|
||||||
|
buildServerPeerBlock({ name, clientPubKey: clientKeys.publicKey, clientIp, psk })
|
||||||
|
);
|
||||||
|
|
||||||
|
resultCards.push({ name, clientIp, clientConf });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Server config summary
|
||||||
|
let serverConf = `[Interface]\n`;
|
||||||
|
serverConf += `Address = ${serverIp}/${network.split("/")[1]}\n`;
|
||||||
|
serverConf += `ListenPort = ${port}\n`;
|
||||||
|
if (serverKeyPair) {
|
||||||
|
serverConf += `PrivateKey = ${serverKeyPair.privateKey}\n`;
|
||||||
|
} else {
|
||||||
|
serverConf += `PrivateKey = <votre clé privée serveur existante>\n`;
|
||||||
|
}
|
||||||
|
serverConf += `\n` + serverPeerBlocks.join("\n");
|
||||||
|
|
||||||
|
serverConfText.textContent = serverConf;
|
||||||
|
serverSummary.style.display = "block";
|
||||||
|
|
||||||
|
resultCards.forEach((card, i) => {
|
||||||
|
const wrapper = document.createElement("div");
|
||||||
|
wrapper.className = "panel result-card";
|
||||||
|
const preId = `client-conf-${i}`;
|
||||||
|
wrapper.innerHTML = `
|
||||||
|
<h3>${card.name} — ${card.clientIp}</h3>
|
||||||
|
<div class="result-body">
|
||||||
|
<pre id="${preId}">${card.clientConf}</pre>
|
||||||
|
<div class="qr-wrap">
|
||||||
|
<div class="qr-canvas"></div>
|
||||||
|
<span class="hint">Scanner avec l'app WireGuard mobile</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<button class="btn-secondary copy-btn" data-copy-target="${preId}">Copier la config</button>
|
||||||
|
<button class="btn-secondary download-btn">Télécharger .conf</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
wrapper.querySelector(".download-btn").addEventListener("click", () => {
|
||||||
|
downloadFile(`${card.name}.conf`, card.clientConf);
|
||||||
|
});
|
||||||
|
renderQr(wrapper.querySelector(".qr-canvas"), card.clientConf);
|
||||||
|
resultsEl.appendChild(wrapper);
|
||||||
|
});
|
||||||
|
|
||||||
|
resultsEl.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||||
|
} catch (err) {
|
||||||
|
alert(err.message || String(err));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>WireGuard Config Generator</title>
|
||||||
|
<link rel="stylesheet" href="style.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1>WireGuard <span>Config Generator</span></h1>
|
||||||
|
<p>Générez des configurations WireGuard prêtes à l'emploi.</p>
|
||||||
|
<span class="badge">🔒 100% local — aucune clé ne quitte votre navigateur</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="panel">
|
||||||
|
<h2>Paramètres du serveur</h2>
|
||||||
|
<div class="grid">
|
||||||
|
<div class="field">
|
||||||
|
<label for="srv-endpoint">Endpoint (hôte ou IP publique)</label>
|
||||||
|
<input type="text" id="srv-endpoint" placeholder="vpn.example.com" />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="srv-port">Port d'écoute</label>
|
||||||
|
<input type="number" id="srv-port" value="51820" min="1" max="65535" />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="srv-network">Réseau VPN (CIDR)</label>
|
||||||
|
<input type="text" id="srv-network" value="10.10.0.0/24" placeholder="10.10.0.0/24" />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="srv-server-ip">IP du serveur dans le VPN</label>
|
||||||
|
<input type="text" id="srv-server-ip" value="10.10.0.1" />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="srv-dns">DNS pour les clients (optionnel)</label>
|
||||||
|
<input type="text" id="srv-dns" placeholder="1.1.1.1" />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="srv-allowed-ips">AllowedIPs côté client</label>
|
||||||
|
<input type="text" id="srv-allowed-ips" value="0.0.0.0/0, ::/0" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label for="srv-pubkey">Clé publique du serveur (laisser vide pour en générer une nouvelle)</label>
|
||||||
|
<input type="text" id="srv-pubkey" placeholder="générée automatiquement si vide" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="checkbox-row field">
|
||||||
|
<input type="checkbox" id="use-psk" checked />
|
||||||
|
<label for="use-psk" style="margin:0;">Ajouter une clé pré-partagée (PresharedKey) par client</label>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<div class="section-title-row">
|
||||||
|
<h2 style="margin:0;">Clients</h2>
|
||||||
|
<button class="btn-secondary" id="add-client">+ Ajouter un client</button>
|
||||||
|
</div>
|
||||||
|
<div class="clients-list" id="clients-list"></div>
|
||||||
|
<p class="hint">L'adresse IP de chaque client est attribuée automatiquement à partir du réseau VPN défini ci-dessus (serveur = .1, premier client = .2, etc.).</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<button class="btn-primary" id="generate-btn">Générer les configurations</button>
|
||||||
|
<button class="btn-secondary" id="reset-btn">Réinitialiser</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="panel" id="server-summary" style="display:none;">
|
||||||
|
<div class="section-title-row">
|
||||||
|
<h2 style="margin:0;">Configuration serveur (wg0.conf)</h2>
|
||||||
|
<button class="btn-secondary copy-btn" data-copy-target="server-conf-text">Copier</button>
|
||||||
|
</div>
|
||||||
|
<div class="server-block" id="server-conf-text"></div>
|
||||||
|
<p class="hint">À placer sur le serveur WireGuard (ex. <code>/etc/wireguard/wg0.conf</code>). Contient la clé privée du serveur : gardez-la secrète.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="results"></section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
Générateur WireGuard — aucune donnée n'est envoyée à un serveur, tout est calculé dans votre navigateur (tweetnacl / X25519).
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="vendor/tweetnacl.min.js"></script>
|
||||||
|
<script src="vendor/qrcode.min.js"></script>
|
||||||
|
<script src="app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+272
@@ -0,0 +1,272 @@
|
|||||||
|
:root {
|
||||||
|
--bg: #0f1420;
|
||||||
|
--bg-elevated: #171d2c;
|
||||||
|
--surface: #1e263a;
|
||||||
|
--surface-hover: #262f47;
|
||||||
|
--border: #2a3349;
|
||||||
|
--border-strong: #3a4560;
|
||||||
|
|
||||||
|
--text: #e6e9f0;
|
||||||
|
--text-muted: #8a93a8;
|
||||||
|
--text-faint: #6b7690;
|
||||||
|
|
||||||
|
--accent: #7c5cff;
|
||||||
|
--accent-strong: #9c85ff;
|
||||||
|
--accent-contrast: #ffffff;
|
||||||
|
--accent-soft: rgba(124, 92, 255, 0.12);
|
||||||
|
|
||||||
|
--ok: #4fd1c5;
|
||||||
|
--ok-soft: rgba(79, 209, 197, 0.12);
|
||||||
|
--warn: #fbbf24;
|
||||||
|
--warn-soft: rgba(251, 191, 36, 0.12);
|
||||||
|
--danger: #ff6b6b;
|
||||||
|
--danger-soft: rgba(255, 107, 107, 0.12);
|
||||||
|
|
||||||
|
--radius-sm: 6px;
|
||||||
|
--radius-md: 10px;
|
||||||
|
--radius-lg: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
padding: 2.5rem 1.5rem 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
header h1 {
|
||||||
|
margin: 0 0 0.4rem;
|
||||||
|
font-size: 1.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
header h1 span {
|
||||||
|
color: var(--accent-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
header p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 0.7rem;
|
||||||
|
padding: 0.25rem 0.7rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--ok-soft);
|
||||||
|
color: var(--ok);
|
||||||
|
border: 1px solid rgba(52, 211, 153, 0.3);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
max-width: 980px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 1rem 1.5rem 4rem;
|
||||||
|
display: grid;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: 1.25rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"], input[type="number"], select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.55rem 0.7rem;
|
||||||
|
background: var(--bg-elevated);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus, select:focus {
|
||||||
|
outline: 2px solid var(--accent-strong);
|
||||||
|
outline-offset: 1px;
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
margin-bottom: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-row input { width: auto; }
|
||||||
|
|
||||||
|
.clients-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.6rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.6rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-row input { flex: 1; }
|
||||||
|
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: 0.55rem 1rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-family: inherit;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--accent);
|
||||||
|
color: var(--accent-contrast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover { background: var(--accent-strong); }
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: var(--surface-hover);
|
||||||
|
color: var(--text);
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover { border-color: var(--accent); color: var(--accent-strong); background: var(--accent-soft); }
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--danger);
|
||||||
|
border: 1px solid rgba(248, 113, 113, 0.4);
|
||||||
|
padding: 0.4rem 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover { background: var(--danger-soft); }
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.7rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-card {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: 1rem;
|
||||||
|
background: var(--bg-elevated);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
border-top: 3px solid var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-card h3 {
|
||||||
|
margin: 0 0 0.6rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--accent-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-body {
|
||||||
|
display: flex;
|
||||||
|
gap: 1.2rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 260px;
|
||||||
|
background: var(--bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: 0.8rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-wrap canvas, .qr-wrap img {
|
||||||
|
background: white;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-block {
|
||||||
|
background: var(--bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: 0.8rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
color: var(--text-faint);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin-top: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-faint);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
padding: 1rem 1.5rem 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
padding: 0.3rem 0.6rem;
|
||||||
|
margin-left: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0.6rem;
|
||||||
|
}
|
||||||
Vendored
+2297
File diff suppressed because it is too large.
Load diff
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
!function(i){"use strict";var m=function(r,n){this.hi=0|r,this.lo=0|n},v=function(r){var n,e=new Float64Array(16);if(r)for(n=0;n<r.length;n++)e[n]=r[n];return e},a=function(){throw new Error("no PRNG")},o=new Uint8Array(16),e=new Uint8Array(32);e[0]=9;var c=v(),w=v([1]),g=v([56129,1]),y=v([30883,4953,19914,30187,55467,16705,2637,112,59544,30585,16505,36039,65139,11119,27886,20995]),l=v([61785,9906,39828,60374,45398,33411,5274,224,53552,61171,33010,6542,64743,22239,55772,9222]),t=v([54554,36645,11616,51542,42930,38181,51040,26924,56412,64982,57905,49316,21502,52590,14035,8553]),f=v([26200,26214,26214,26214,26214,26214,26214,26214,26214,26214,26214,26214,26214,26214,26214,26214]),s=v([41136,18958,6951,50414,58488,44335,6150,12099,55207,15867,153,11085,57099,20417,9344,11139]);function h(r,n){return r<<n|r>>>32-n}function b(r,n){var e=255&r[n+3];return(e=(e=e<<8|255&r[n+2])<<8|255&r[n+1])<<8|255&r[n+0]}function B(r,n){var e=r[n]<<24|r[n+1]<<16|r[n+2]<<8|r[n+3],t=r[n+4]<<24|r[n+5]<<16|r[n+6]<<8|r[n+7];return new m(e,t)}function p(r,n,e){var t;for(t=0;t<4;t++)r[n+t]=255&e,e>>>=8}function S(r,n,e){r[n]=e.hi>>24&255,r[n+1]=e.hi>>16&255,r[n+2]=e.hi>>8&255,r[n+3]=255&e.hi,r[n+4]=e.lo>>24&255,r[n+5]=e.lo>>16&255,r[n+6]=e.lo>>8&255,r[n+7]=255&e.lo}function u(r,n,e,t,o){var i,a=0;for(i=0;i<o;i++)a|=r[n+i]^e[t+i];return(1&a-1>>>8)-1}function A(r,n,e,t){return u(r,n,e,t,16)}function _(r,n,e,t){return u(r,n,e,t,32)}function U(r,n,e,t,o){var i,a,f,u=new Uint32Array(16),c=new Uint32Array(16),w=new Uint32Array(16),y=new Uint32Array(4);for(i=0;i<4;i++)c[5*i]=b(t,4*i),c[1+i]=b(e,4*i),c[6+i]=b(n,4*i),c[11+i]=b(e,16+4*i);for(i=0;i<16;i++)w[i]=c[i];for(i=0;i<20;i++){for(a=0;a<4;a++){for(f=0;f<4;f++)y[f]=c[(5*a+4*f)%16];for(y[1]^=h(y[0]+y[3]|0,7),y[2]^=h(y[1]+y[0]|0,9),y[3]^=h(y[2]+y[1]|0,13),y[0]^=h(y[3]+y[2]|0,18),f=0;f<4;f++)u[4*a+(a+f)%4]=y[f]}for(f=0;f<16;f++)c[f]=u[f]}if(o){for(i=0;i<16;i++)c[i]=c[i]+w[i]|0;for(i=0;i<4;i++)c[5*i]=c[5*i]-b(t,4*i)|0,c[6+i]=c[6+i]-b(n,4*i)|0;for(i=0;i<4;i++)p(r,4*i,c[5*i]),p(r,16+4*i,c[6+i])}else for(i=0;i<16;i++)p(r,4*i,c[i]+w[i]|0)}function E(r,n,e,t){U(r,n,e,t,!1)}function x(r,n,e,t){return U(r,n,e,t,!0),0}var d=new Uint8Array([101,120,112,97,110,100,32,51,50,45,98,121,116,101,32,107]);function K(r,n,e,t,o,i,a){var f,u,c=new Uint8Array(16),w=new Uint8Array(64);if(!o)return 0;for(u=0;u<16;u++)c[u]=0;for(u=0;u<8;u++)c[u]=i[u];for(;64<=o;){for(E(w,c,a,d),u=0;u<64;u++)r[n+u]=(e?e[t+u]:0)^w[u];for(f=1,u=8;u<16;u++)f=f+(255&c[u])|0,c[u]=255&f,f>>>=8;o-=64,n+=64,e&&(t+=64)}if(0<o)for(E(w,c,a,d),u=0;u<o;u++)r[n+u]=(e?e[t+u]:0)^w[u];return 0}function Y(r,n,e,t,o){return K(r,n,null,0,e,t,o)}function L(r,n,e,t,o){var i=new Uint8Array(32);return x(i,t,o,d),Y(r,n,e,t.subarray(16),i)}function T(r,n,e,t,o,i,a){var f=new Uint8Array(32);return x(f,i,a,d),K(r,n,e,t,o,i.subarray(16),f)}function k(r,n){var e,t=0;for(e=0;e<17;e++)t=t+(r[e]+n[e]|0)|0,r[e]=255&t,t>>>=8}var z=new Uint32Array([5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,252]);function R(r,n,e,t,o,i){var a,f,u,c,w=new Uint32Array(17),y=new Uint32Array(17),l=new Uint32Array(17),s=new Uint32Array(17),h=new Uint32Array(17);for(u=0;u<17;u++)y[u]=l[u]=0;for(u=0;u<16;u++)y[u]=i[u];for(y[3]&=15,y[4]&=252,y[7]&=15,y[8]&=252,y[11]&=15,y[12]&=252,y[15]&=15;0<o;){for(u=0;u<17;u++)s[u]=0;for(u=0;u<16&&u<o;++u)s[u]=e[t+u];for(s[u]=1,t+=u,o-=u,k(l,s),f=0;f<17;f++)for(u=w[f]=0;u<17;u++)w[f]=w[f]+l[u]*(u<=f?y[f-u]:320*y[f+17-u]|0)|0;for(f=0;f<17;f++)l[f]=w[f];for(u=c=0;u<16;u++)c=c+l[u]|0,l[u]=255&c,c>>>=8;for(c=c+l[16]|0,l[16]=3&c,c=5*(c>>>2)|0,u=0;u<16;u++)c=c+l[u]|0,l[u]=255&c,c>>>=8;c=c+l[16]|0,l[16]=c}for(u=0;u<17;u++)h[u]=l[u];for(k(l,z),a=0|-(l[16]>>>7),u=0;u<17;u++)l[u]^=a&(h[u]^l[u]);for(u=0;u<16;u++)s[u]=i[u+16];for(s[16]=0,k(l,s),u=0;u<16;u++)r[n+u]=l[u];return 0}function P(r,n,e,t,o,i){var a=new Uint8Array(16);return R(a,0,e,t,o,i),A(r,n,a,0)}function M(r,n,e,t,o){var i;if(e<32)return-1;for(T(r,0,n,0,e,t,o),R(r,16,r,32,e-32,r),i=0;i<16;i++)r[i]=0;return 0}function N(r,n,e,t,o){var i,a=new Uint8Array(32);if(e<32)return-1;if(L(a,0,32,t,o),0!==P(n,16,n,32,e-32,a))return-1;for(T(r,0,n,0,e,t,o),i=0;i<32;i++)r[i]=0;return 0}function O(r,n){var e;for(e=0;e<16;e++)r[e]=0|n[e]}function C(r){var n,e;for(e=0;e<16;e++)r[e]+=65536,n=Math.floor(r[e]/65536),r[(e+1)*(e<15?1:0)]+=n-1+37*(n-1)*(15===e?1:0),r[e]-=65536*n}function F(r,n,e){for(var t,o=~(e-1),i=0;i<16;i++)t=o&(r[i]^n[i]),r[i]^=t,n[i]^=t}function Z(r,n){var e,t,o,i=v(),a=v();for(e=0;e<16;e++)a[e]=n[e];for(C(a),C(a),C(a),t=0;t<2;t++){for(i[0]=a[0]-65517,e=1;e<15;e++)i[e]=a[e]-65535-(i[e-1]>>16&1),i[e-1]&=65535;i[15]=a[15]-32767-(i[14]>>16&1),o=i[15]>>16&1,i[14]&=65535,F(a,i,1-o)}for(e=0;e<16;e++)r[2*e]=255&a[e],r[2*e+1]=a[e]>>8}function G(r,n){var e=new Uint8Array(32),t=new Uint8Array(32);return Z(e,r),Z(t,n),_(e,0,t,0)}function q(r){var n=new Uint8Array(32);return Z(n,r),1&n[0]}function D(r,n){var e;for(e=0;e<16;e++)r[e]=n[2*e]+(n[2*e+1]<<8);r[15]&=32767}function I(r,n,e){var t;for(t=0;t<16;t++)r[t]=n[t]+e[t]|0}functionLine truncated
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
services:
|
||||||
|
wireguard-config-generator:
|
||||||
|
build: .
|
||||||
|
container_name: wireguard-config-generator
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8090:8080"
|
||||||
|
read_only: true
|
||||||
|
tmpfs:
|
||||||
|
- /tmp
|
||||||
|
- /var/cache/nginx
|
||||||
|
- /var/run
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 172 KiB |
+23
@@ -0,0 +1,23 @@
|
|||||||
|
server {
|
||||||
|
listen 8080;
|
||||||
|
server_name _;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# Rien ne doit jamais sortir du navigateur : CSP verrouillée, pas de connect-src externe.
|
||||||
|
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'none'; frame-ancestors 'none'; base-uri 'self'; form-action 'none'" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-Frame-Options "DENY" always;
|
||||||
|
add_header Referrer-Policy "no-referrer" always;
|
||||||
|
add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /healthz {
|
||||||
|
access_log off;
|
||||||
|
return 200 "ok\n";
|
||||||
|
add_header Content-Type text/plain;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user