This commit is contained in:
CaffeineFueled 2025-06-20 17:00:06 +02:00
commit fd1d199ae8
6 changed files with 1421 additions and 0 deletions

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 WireGuard Configuration Generator
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

46
README.md Normal file
View file

@ -0,0 +1,46 @@
# WireGuard Configuration Generator
A web-based tool for generating WireGuard VPN configurations with cryptographically secure key generation. Supports both **Hub-and-Spoke** and **Mesh Network** topologies.
## TODO
- QR Code generator for config
- Download all config at once
- Make `PresharedKey` and other options optional
- Container
- frontend rework (I hate frontend)
## 🔐 Production-Ready Cryptography
This project uses **real cryptographic implementations** suitable for production WireGuard deployments, not demonstration code.
### Cryptographic Implementation
#### **Libraries Used**
- **[TweetNaCl.js v1.0.3](https://tweetnacl.js.org/)** - Audited, lightweight cryptographic library
- **Web Crypto API** - Browser-native cryptographic operations when available
- **HMAC-SHA256 Fallback** - Custom implementation for browsers without Web Crypto API
#### Cryptographic Flow
1. **Seed Generation/Input**
- Generate cryptographically secure 32-byte seed
- Or accept user-provided hex seed for reproducibility
2. **Key Derivation**
- Use HKDF to derive keys from seed with unique salts
- Private keys: `HKDF(seed, "WireGuard v1 private key", key_index)`
- Preshared keys: `HKDF(seed, "WireGuard v1 preshared key", key_index)`
3. **Public Key Generation**
- Apply Curve25519 scalar multiplication: `public = private * G`
- Where G is the Curve25519 base point
4. **Key Validation**
- Verify key lengths (32 bytes each)
- Check private key clamping
- Confirm public key derivation
# License
WIP

108
index.html Normal file
View file

@ -0,0 +1,108 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WireGuard Hub-and-Spoke Configuration Generator</title>
<link rel="stylesheet" href="static/styles.css">
<script src="https://cdn.jsdelivr.net/npm/tweetnacl@1.0.3/nacl.min.js"></script>
</head>
<body>
<div class="container">
<div class="navigation">
<a href="mesh-generator/" class="nav-link">Mesh Network Generator →</a>
</div>
<h1>WireGuard Hub-and-Spoke Configuration Generator</h1>
<div class="privacy-disclaimer">
<strong>Privacy Notice:</strong> All configuration generation and cryptographic operations are performed entirely in your browser. No data is transmitted to any server - your keys, configurations, and settings remain completely private and local to your device.
</div>
<form id="configForm">
<div class="form-row">
<div class="form-group">
<label for="serverName">Server Name:</label>
<input type="text" id="serverName" value="wg-server" required>
</div>
<div class="form-group">
<label for="serverPort">Server Port:</label>
<input type="number" id="serverPort" value="51820" min="1" max="65535" required>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="serverNetwork">Server Network (CIDR):</label>
<input type="text" id="serverNetwork" value="10.0.0.0/24" pattern="^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}\/([1-2]?[0-9]|3[0-2])$" required>
</div>
<div class="form-group">
<label for="serverEndpoint">Server Public Endpoint:</label>
<input type="text" id="serverEndpoint" placeholder="your-server.com or IP" required>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="clientCount">Number of Clients:</label>
<input type="number" id="clientCount" value="3" min="1" max="50" required>
</div>
<div class="form-group">
<label for="dns">DNS Servers (optional):</label>
<input type="text" id="dns" placeholder="e.g. 8.8.8.8, 1.1.1.1">
</div>
</div>
<div class="seed-section">
<label>Cryptographic Seed (for reproducible key generation):</label>
<div class="seed-display" id="seedDisplay">Click "Generate New Seed" or "Generate Configurations" to create a seed</div>
<div class="seed-buttons">
<button type="button" class="seed-btn" onclick="generateNewSeed()">Generate New Seed</button>
<button type="button" class="seed-btn" onclick="copySeed()">Copy Seed</button>
<button type="button" class="seed-btn" onclick="pasteSeed()">Paste Seed</button>
</div>
<div class="form-group" style="margin-top: 10px;">
<label for="customSeed">Custom Seed (paste hex string to reuse):</label>
<input type="text" id="customSeed" placeholder="Enter 64-character hex string or leave empty for random">
</div>
</div>
<button type="button" onclick="generateConfigs()">Generate Configurations</button>
</form>
<div id="configOutput" class="config-output" style="display: none;">
<div class="config-section">
<h3>Server Configuration</h3>
<div class="config-content" id="serverConfig"></div>
<button class="download-btn" onclick="downloadConfig('server', document.getElementById('serverConfig').textContent)">
Download Server Config
</button>
</div>
<div class="config-section">
<h3>Client Configurations</h3>
<div class="client-configs-row" id="clientConfigs"></div>
</div>
</div>
</div>
<footer class="footer">
<div class="footer-content">
<div class="footer-column">
<h4>License</h4>
<p>MIT License</p>
</div>
<div class="footer-column">
<h4>Open Source</h4>
<p><a href="https://git.ittavern.com/CaffeineFueled/wireguard-config-generator">Repository</a></p>
</div>
<div class="footer-column">
<h4>Disclaimer</h4>
<p>WireGuard® is a registered trademark of Jason A. Donenfeld. This generator is not affiliated with the official WireGuard project.</p>
</div>
</div>
</footer>
<script src="static/script.js"></script>
</body>
</html>

97
mesh-generator/index.html Normal file
View file

@ -0,0 +1,97 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WireGuard Mesh Network Configuration Generator</title>
<link rel="stylesheet" href="../static/styles.css">
<script src="https://cdn.jsdelivr.net/npm/tweetnacl@1.0.3/nacl.min.js"></script>
</head>
<body>
<div class="container">
<div class="navigation">
<a href="../" class="nav-link">← Hub-and-Spoke Generator</a>
</div>
<h1>WireGuard Mesh Network Configuration Generator</h1>
<div class="privacy-disclaimer">
<strong>Privacy Notice:</strong> All configuration generation and cryptographic operations are performed entirely in your browser. No data is transmitted to any server - your keys, configurations, and settings remain completely private and local to your device.
</div>
<p class="description">Generate a full mesh WireGuard configuration where every peer can communicate directly with every other peer.</p>
<form id="meshConfigForm">
<div class="form-section">
<h3>Network Settings</h3>
<div class="form-row">
<div class="form-group">
<label for="networkCIDR">Network CIDR:</label>
<input type="text" id="networkCIDR" value="10.0.0.0/24" pattern="^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}\/([1-2]?[0-9]|3[0-2])$" required>
</div>
<div class="form-group">
<label for="dns">DNS Servers (optional):</label>
<input type="text" id="dns" placeholder="e.g. 8.8.8.8, 1.1.1.1">
</div>
</div>
</div>
<div class="form-section">
<h3>Peer Configuration</h3>
<div class="form-group">
<label for="peerCount">Number of Peers:</label>
<input type="number" id="peerCount" value="3" min="2" max="20" required>
<button type="button" id="updatePeersBtn" onclick="updatePeerFields()">Update Peer Fields</button>
</div>
</div>
<div id="peerFields" class="form-section">
<h3>Peer Details</h3>
<div id="peerInputs"></div>
</div>
<div class="seed-section">
<label>Cryptographic Seed (for reproducible key generation):</label>
<div class="seed-display" id="seedDisplay">Click "Generate New Seed" or "Generate Configurations" to create a seed</div>
<div class="seed-buttons">
<button type="button" class="seed-btn" onclick="generateNewSeed()">Generate New Seed</button>
<button type="button" class="seed-btn" onclick="copySeed()">Copy Seed</button>
<button type="button" class="seed-btn" onclick="pasteSeed()">Paste Seed</button>
</div>
<div class="form-group" style="margin-top: 10px;">
<label for="customSeed">Custom Seed (paste hex string to reuse):</label>
<input type="text" id="customSeed" placeholder="Enter 64-character hex string or leave empty for random">
</div>
</div>
<button type="button" onclick="generateMeshConfigs()">Generate Mesh Configurations</button>
</form>
<div id="configOutput" class="config-output" style="display: none;">
<div class="config-section">
<h3>Mesh Network Configurations</h3>
<div class="client-configs-row" id="meshConfigs"></div>
</div>
</div>
</div>
<footer class="footer">
<div class="footer-content">
<div class="footer-column">
<h4>License</h4>
<p>MIT License</p>
</div>
<div class="footer-column">
<h4>Open Source</h4>
<p><a href="https://git.ittavern.com/CaffeineFueled/wireguard-config-generator">Repository</a></p>
</div>
<div class="footer-column">
<h4>Disclaimer</h4>
<p>WireGuard® is a registered trademark of Jason A. Donenfeld. This generator is not affiliated with the official WireGuard project.</p>
</div>
</div>
</footer>
<script src="../static/script.js"></script>
</body>
</html>

837
static/script.js Normal file
View file

@ -0,0 +1,837 @@
// Production-ready cryptographic functions for WireGuard key generation
// Uses TweetNaCl.js for proper Curve25519 operations and Web Crypto API for HKDF
class WireGuardCrypto {
constructor() {
this.crypto = window.crypto || window.msCrypto;
// Check if TweetNaCl is available
if (typeof nacl === 'undefined') {
throw new Error('TweetNaCl library is required but not loaded');
}
// Check if Web Crypto API is available
this.hasWebCrypto = !!(this.crypto && this.crypto.subtle);
if (!this.hasWebCrypto) {
console.warn('Web Crypto API not available, falling back to less secure methods');
}
// Key counter for deterministic key generation
this.keyCounter = 0;
}
// Generate a cryptographically secure random seed
generateSeed() {
const array = new Uint8Array(32);
this.crypto.getRandomValues(array);
return array;
}
// HKDF implementation using Web Crypto API when available
async hkdf(seed, salt, info, length = 32) {
if (this.hasWebCrypto) {
try {
// Import the seed as key material
const keyMaterial = await this.crypto.subtle.importKey(
'raw',
seed,
'HKDF',
false,
['deriveBits']
);
// Derive key using HKDF
const derivedBits = await this.crypto.subtle.deriveBits(
{
name: 'HKDF',
hash: 'SHA-256',
salt: salt,
info: info
},
keyMaterial,
length * 8 // length in bits
);
return new Uint8Array(derivedBits);
} catch (error) {
console.warn('Web Crypto HKDF failed, falling back to HMAC-based implementation:', error);
return this.hkdfFallback(seed, salt, info, length);
}
} else {
return this.hkdfFallback(seed, salt, info, length);
}
}
// Fallback HKDF implementation using TweetNaCl's hash function
hkdfFallback(seed, salt, info, length = 32) {
// HKDF-Extract
const prk = this.hmacSha256(salt.length > 0 ? salt : new Uint8Array(32), seed);
// HKDF-Expand
const n = Math.ceil(length / 32);
const okm = new Uint8Array(length);
let t = new Uint8Array(0);
for (let i = 1; i <= n; i++) {
const concat = new Uint8Array(t.length + info.length + 1);
concat.set(t);
concat.set(info, t.length);
concat[concat.length - 1] = i;
t = this.hmacSha256(prk, concat);
const copyLength = Math.min(32, length - (i - 1) * 32);
okm.set(t.subarray(0, copyLength), (i - 1) * 32);
}
return okm;
}
// HMAC-SHA256 implementation using TweetNaCl
hmacSha256(key, data) {
const blockSize = 64;
const hashSize = 32;
// If key is longer than block size, hash it
if (key.length > blockSize) {
key = nacl.hash(key).subarray(0, hashSize);
}
// Pad key to block size
const paddedKey = new Uint8Array(blockSize);
paddedKey.set(key);
// Create inner and outer padding
const ipad = new Uint8Array(blockSize);
const opad = new Uint8Array(blockSize);
for (let i = 0; i < blockSize; i++) {
ipad[i] = paddedKey[i] ^ 0x36;
opad[i] = paddedKey[i] ^ 0x5c;
}
// Inner hash
const innerData = new Uint8Array(blockSize + data.length);
innerData.set(ipad);
innerData.set(data, blockSize);
const innerHash = nacl.hash(innerData);
// Outer hash
const outerData = new Uint8Array(blockSize + hashSize);
outerData.set(opad);
outerData.set(innerHash);
return nacl.hash(outerData);
}
// Generate a WireGuard private key using proper key derivation
async generatePrivateKey(seed) {
const salt = new TextEncoder().encode('WireGuard v1 private key');
const info = new Uint8Array(4);
// Convert key counter to bytes (little endian)
const keyIndex = this.keyCounter++;
info[0] = keyIndex & 0xff;
info[1] = (keyIndex >> 8) & 0xff;
info[2] = (keyIndex >> 16) & 0xff;
info[3] = (keyIndex >> 24) & 0xff;
const keyMaterial = await this.hkdf(seed, salt, info, 32);
// Apply Curve25519 key clamping as per RFC 7748
keyMaterial[0] &= 248; // Clear bottom 3 bits
keyMaterial[31] &= 127; // Clear top bit
keyMaterial[31] |= 64; // Set second-highest bit
return this.arrayToBase64(keyMaterial);
}
// Generate public key from private key using real Curve25519 scalar multiplication
generatePublicKey(privateKeyBase64) {
try {
const privateKeyBytes = this.base64ToArray(privateKeyBase64);
// Use TweetNaCl's scalar multiplication with base point
const publicKeyBytes = nacl.scalarMult.base(privateKeyBytes);
return this.arrayToBase64(publicKeyBytes);
} catch (error) {
throw new Error('Failed to generate public key: ' + error.message);
}
}
// Generate preshared key using proper key derivation
async generatePresharedKey(seed) {
const salt = new TextEncoder().encode('WireGuard v1 preshared key');
const info = new Uint8Array(4);
const keyIndex = this.keyCounter++;
info[0] = keyIndex & 0xff;
info[1] = (keyIndex >> 8) & 0xff;
info[2] = (keyIndex >> 16) & 0xff;
info[3] = (keyIndex >> 24) & 0xff;
const keyMaterial = await this.hkdf(seed, salt, info, 32);
return this.arrayToBase64(keyMaterial);
}
// Reset key counter for deterministic generation
resetKeyCounter() {
this.keyCounter = 0;
}
// Utility functions
arrayToBase64(array) {
return btoa(String.fromCharCode.apply(null, array));
}
base64ToArray(base64) {
const binary = atob(base64);
const array = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
array[i] = binary.charCodeAt(i);
}
return array;
}
arrayToHex(array) {
return Array.from(array).map(b => b.toString(16).padStart(2, '0')).join('');
}
hexToArray(hex) {
if (hex.length !== 64) {
throw new Error('Hex string must be exactly 64 characters (32 bytes)');
}
const array = new Uint8Array(32);
for (let i = 0; i < 32; i++) {
array[i] = parseInt(hex.substr(i * 2, 2), 16);
}
return array;
}
// Parse seed from input (hex string or generate new)
parseSeed(customSeedHex) {
if (customSeedHex && customSeedHex.trim()) {
return this.hexToArray(customSeedHex.trim());
} else {
return this.generateSeed();
}
}
// Validate that generated keys are valid
validateKeyPair(privateKeyBase64, publicKeyBase64) {
try {
const privateKey = this.base64ToArray(privateKeyBase64);
const publicKey = this.base64ToArray(publicKeyBase64);
// Check lengths
if (privateKey.length !== 32 || publicKey.length !== 32) {
console.warn('Invalid key length');
return false;
}
// Check key clamping on private key
if ((privateKey[0] & 7) !== 0 || (privateKey[31] & 128) !== 0 || (privateKey[31] & 64) === 0) {
console.warn('Invalid key clamping');
return false;
}
// Verify public key matches private key
const derivedPublic = nacl.scalarMult.base(privateKey);
for (let i = 0; i < 32; i++) {
if (derivedPublic[i] !== publicKey[i]) {
console.warn('Public key does not match private key');
return false;
}
}
return true;
} catch (error) {
console.error('Key validation error:', error);
return false;
}
}
// Get info about the crypto implementation
getCryptoInfo() {
return {
library: 'TweetNaCl.js v1.0.3',
hasWebCrypto: this.hasWebCrypto,
curve: 'Curve25519 (X25519)',
hkdf: this.hasWebCrypto ? 'Web Crypto API' : 'HMAC-SHA256 fallback',
secure: true
};
}
}
let currentConfigs = null;
let currentSeed = null;
let currentMeshConfigs = null;
// Initialize crypto with error handling
let crypto;
try {
crypto = new WireGuardCrypto();
console.log('Crypto initialized:', crypto.getCryptoInfo());
} catch (error) {
console.error('Failed to initialize crypto:', error);
alert('Cryptographic library failed to initialize. Please refresh the page.');
}
async function generateConfigs() {
try {
const serverName = document.getElementById('serverName').value;
const serverPort = parseInt(document.getElementById('serverPort').value);
const serverNetwork = document.getElementById('serverNetwork').value;
const serverEndpoint = document.getElementById('serverEndpoint').value;
const clientCount = parseInt(document.getElementById('clientCount').value);
const dns = document.getElementById('dns').value;
// Validate inputs
if (!validateInputs(serverNetwork, serverEndpoint, clientCount)) {
return;
}
// Show loading state
const generateBtn = document.querySelector('button[onclick="generateConfigs()"]');
const originalText = generateBtn.textContent;
generateBtn.textContent = 'Generating...';
generateBtn.disabled = true;
// Generate or use existing seed
const customSeedHex = document.getElementById('customSeed').value;
let seed;
if (customSeedHex && customSeedHex.trim()) {
// Use custom seed from input
seed = crypto.parseSeed(customSeedHex);
} else if (currentSeed) {
// Use existing seed if available
seed = currentSeed;
} else {
// Generate new seed only if none exists
seed = crypto.generateSeed();
}
currentSeed = seed;
// Update seed display and input field
const seedHex = crypto.arrayToHex(seed);
document.getElementById('seedDisplay').textContent = seedHex;
if (!customSeedHex || !customSeedHex.trim()) {
document.getElementById('customSeed').value = seedHex;
}
// Reset key counter for deterministic generation
crypto.resetKeyCounter();
// Generate server keys
const serverPrivateKey = await crypto.generatePrivateKey(seed);
const serverPublicKey = crypto.generatePublicKey(serverPrivateKey);
// Parse network
const [networkBase, cidr] = serverNetwork.split('/');
const networkParts = networkBase.split('.').map(n => parseInt(n));
// Generate client configurations
const clients = [];
for (let i = 1; i <= clientCount; i++) {
const clientPrivateKey = await crypto.generatePrivateKey(seed);
const clientPublicKey = crypto.generatePublicKey(clientPrivateKey);
const presharedKey = await crypto.generatePresharedKey(seed);
// Calculate client IP (server gets .1, clients get .2, .3, etc.)
const clientIP = `${networkParts[0]}.${networkParts[1]}.${networkParts[2]}.${networkParts[3] + i}`;
clients.push({
name: `client-${i}`,
privateKey: clientPrivateKey,
publicKey: clientPublicKey,
presharedKey: presharedKey,
ip: clientIP
});
}
// Generate configurations
const serverConfig = generateServerConfig(serverName, serverPrivateKey, serverPort,
`${networkParts[0]}.${networkParts[1]}.${networkParts[2]}.${networkParts[3] + 1}`,
cidr, clients);
const clientConfigs = clients.map(client =>
generateClientConfig(client, serverPublicKey, serverEndpoint, serverPort,
`${networkParts[0]}.${networkParts[1]}.${networkParts[2]}.${networkParts[3] + 1}`,
cidr, dns)
);
// Store configurations
currentConfigs = {
server: serverConfig,
clients: clientConfigs.map((config, index) => ({
name: clients[index].name,
config: config
}))
};
// Display configurations
displayConfigurations();
// Restore button
generateBtn.textContent = originalText;
generateBtn.disabled = false;
} catch (error) {
// Restore button on error
const generateBtn = document.querySelector('button[onclick="generateConfigs()"]');
if (generateBtn) {
generateBtn.textContent = 'Generate Configurations';
generateBtn.disabled = false;
}
alert('Error generating configurations: ' + error.message);
}
}
function validateInputs(serverNetwork, serverEndpoint, clientCount) {
// Validate network CIDR
const cidrRegex = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}\/([1-2]?[0-9]|3[0-2])$/;
if (!cidrRegex.test(serverNetwork)) {
alert('Invalid network CIDR format');
return false;
}
// Validate endpoint
if (!serverEndpoint.trim()) {
alert('Server endpoint is required');
return false;
}
// Validate client count
if (clientCount < 1 || clientCount > 50) {
alert('Client count must be between 1 and 50');
return false;
}
return true;
}
function generateServerConfig(name, privateKey, port, serverIP, cidr, clients) {
let config = `# ${name} Configuration
[Interface]
PrivateKey = ${privateKey}
Address = ${serverIP}/${cidr}
ListenPort = ${port}
SaveConfig = true
# PostUp and PostDown rules for NAT
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
`;
clients.forEach(client => {
config += `# ${client.name}
[Peer]
PublicKey = ${client.publicKey}
PresharedKey = ${client.presharedKey}
AllowedIPs = ${client.ip}/32
`;
});
return config;
}
function generateClientConfig(client, serverPublicKey, serverEndpoint, serverPort, serverIP, cidr, dns) {
let config = `# ${client.name} Configuration
[Interface]
PrivateKey = ${client.privateKey}
Address = ${client.ip}/${cidr}`;
// Only add DNS if it's not empty
if (dns && dns.trim()) {
config += `
DNS = ${dns.trim()}`;
}
config += `
[Peer]
PublicKey = ${serverPublicKey}
PresharedKey = ${client.presharedKey}
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = ${serverEndpoint}:${serverPort}
PersistentKeepalive = 25
`;
return config;
}
function displayConfigurations() {
// Show server config
document.getElementById('serverConfig').textContent = currentConfigs.server;
// Generate client configs in row layout
const clientConfigs = document.getElementById('clientConfigs');
clientConfigs.innerHTML = '';
currentConfigs.clients.forEach((client, index) => {
// Create client config item
const configItem = document.createElement('div');
configItem.className = 'client-config-item';
configItem.innerHTML = `
<div class="client-config-title">${client.name}</div>
<div class="config-content">${client.config}</div>
<button class="download-btn" onclick="downloadConfig('${client.name}', \`${client.config.replace(/`/g, '\\`')}\`)">
Download ${client.name} Config
</button>
`;
clientConfigs.appendChild(configItem);
});
document.getElementById('configOutput').style.display = 'block';
}
function downloadConfig(name, content) {
const blob = new Blob([content], { type: 'text/plain' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${name}.conf`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}
// Seed management functions
function generateNewSeed() {
const seed = crypto.generateSeed();
currentSeed = seed;
const hexSeed = crypto.arrayToHex(seed);
document.getElementById('seedDisplay').textContent = hexSeed;
document.getElementById('customSeed').value = hexSeed;
}
function copySeed() {
if (currentSeed) {
const hexSeed = crypto.arrayToHex(currentSeed);
navigator.clipboard.writeText(hexSeed).then(() => {
alert('Seed copied to clipboard!');
}).catch(() => {
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = hexSeed;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
alert('Seed copied to clipboard!');
});
} else {
alert('No seed to copy. Generate configurations first.');
}
}
function pasteSeed() {
navigator.clipboard.readText().then(text => {
if (text && text.length === 64 && /^[0-9a-fA-F]+$/.test(text)) {
document.getElementById('customSeed').value = text;
document.getElementById('seedDisplay').textContent = text;
// Update current seed
try {
currentSeed = crypto.hexToArray(text);
alert('Seed pasted successfully!');
} catch (error) {
alert('Invalid seed format.');
}
} else {
alert('Invalid seed in clipboard. Must be 64-character hex string.');
}
}).catch(() => {
alert('Cannot read from clipboard. Please paste manually into the Custom Seed field.');
});
}
// Initialize form validation
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('configForm');
if (form) {
form.addEventListener('submit', function(e) {
e.preventDefault();
generateConfigs();
});
}
const meshForm = document.getElementById('meshConfigForm');
if (meshForm) {
meshForm.addEventListener('submit', function(e) {
e.preventDefault();
generateMeshConfigs();
});
// Initialize with default peer fields for mesh
updatePeerFields();
}
// Validate custom seed input
const customSeedInput = document.getElementById('customSeed');
if (customSeedInput) {
customSeedInput.addEventListener('input', function(e) {
const value = e.target.value.trim();
if (value && (value.length !== 64 || !/^[0-9a-fA-F]*$/.test(value))) {
e.target.style.borderColor = '#dc3545';
} else {
e.target.style.borderColor = '#ddd';
// Update current seed and display when valid seed is entered
if (value && value.length === 64 && /^[0-9a-fA-F]+$/.test(value)) {
try {
currentSeed = crypto.hexToArray(value);
document.getElementById('seedDisplay').textContent = value;
} catch (error) {
console.warn('Invalid hex seed:', error);
}
}
}
});
// Handle clearing the custom seed input
customSeedInput.addEventListener('keydown', function(e) {
if (e.key === 'Delete' || e.key === 'Backspace') {
// If user is clearing the field, don't update currentSeed immediately
// Let them finish editing first
}
});
}
});
// === MESH NETWORK FUNCTIONS ===
function updatePeerFields() {
const peerCount = parseInt(document.getElementById('peerCount').value);
const peerInputs = document.getElementById('peerInputs');
if (peerCount < 2 || peerCount > 20) {
alert('Number of peers must be between 2 and 20');
return;
}
peerInputs.innerHTML = '';
for (let i = 1; i <= peerCount; i++) {
const peerGroup = document.createElement('div');
peerGroup.className = 'peer-input-group';
peerGroup.innerHTML = `
<h4>Peer ${i}</h4>
<div class="peer-row">
<div class="form-group">
<label for="peerName${i}">Name:</label>
<input type="text" id="peerName${i}" value="peer-${i}" required>
</div>
<div class="form-group">
<label for="peerEndpoint${i}">Endpoint (optional):</label>
<input type="text" id="peerEndpoint${i}" placeholder="domain.com or IP address">
</div>
<div class="form-group">
<label for="peerPort${i}">Port:</label>
<input type="number" id="peerPort${i}" value="51820" min="1" max="65535" required>
</div>
</div>
`;
peerInputs.appendChild(peerGroup);
}
}
async function generateMeshConfigs() {
try {
const networkCIDR = document.getElementById('networkCIDR').value;
const dns = document.getElementById('dns').value;
const peerCount = parseInt(document.getElementById('peerCount').value);
// Validate inputs
if (!validateMeshInputs(networkCIDR, peerCount)) {
return;
}
// Show loading state
const generateBtn = document.querySelector('button[onclick="generateMeshConfigs()"]');
const originalText = generateBtn.textContent;
generateBtn.textContent = 'Generating...';
generateBtn.disabled = true;
// Generate or use existing seed
const customSeedHex = document.getElementById('customSeed').value;
let seed;
if (customSeedHex && customSeedHex.trim()) {
// Use custom seed from input
seed = crypto.parseSeed(customSeedHex);
} else if (currentSeed) {
// Use existing seed if available
seed = currentSeed;
} else {
// Generate new seed only if none exists
seed = crypto.generateSeed();
}
currentSeed = seed;
// Update seed display and input field
const seedHex = crypto.arrayToHex(seed);
document.getElementById('seedDisplay').textContent = seedHex;
if (!customSeedHex || !customSeedHex.trim()) {
document.getElementById('customSeed').value = seedHex;
}
// Reset key counter for deterministic generation
crypto.resetKeyCounter();
// Parse network
const [networkBase, cidr] = networkCIDR.split('/');
const networkParts = networkBase.split('.').map(n => parseInt(n));
// Collect peer information
const peers = [];
for (let i = 1; i <= peerCount; i++) {
const name = document.getElementById(`peerName${i}`).value.trim();
const endpoint = document.getElementById(`peerEndpoint${i}`).value.trim();
const port = parseInt(document.getElementById(`peerPort${i}`).value);
if (!name) {
alert(`Peer ${i} name is required`);
// Restore button
generateBtn.textContent = originalText;
generateBtn.disabled = false;
return;
}
const privateKey = await crypto.generatePrivateKey(seed);
const publicKey = crypto.generatePublicKey(privateKey);
const ip = `${networkParts[0]}.${networkParts[1]}.${networkParts[2]}.${networkParts[3] + i}`;
peers.push({
name: name,
privateKey: privateKey,
publicKey: publicKey,
endpoint: endpoint,
port: port,
ip: ip
});
}
// Generate preshared keys for each pair of peers
const presharedKeys = {};
for (let i = 0; i < peers.length; i++) {
for (let j = i + 1; j < peers.length; j++) {
const key = `${i}-${j}`;
presharedKeys[key] = await crypto.generatePresharedKey(seed);
}
}
// Generate configurations for each peer
const meshConfigs = peers.map((peer, index) => ({
name: peer.name,
config: generateMeshPeerConfig(peer, peers, index, cidr, dns, presharedKeys)
}));
// Store configurations
currentMeshConfigs = meshConfigs;
// Display configurations
displayMeshConfigurations();
// Restore button
generateBtn.textContent = originalText;
generateBtn.disabled = false;
} catch (error) {
// Restore button on error
const generateBtn = document.querySelector('button[onclick="generateMeshConfigs()"]');
if (generateBtn) {
generateBtn.textContent = 'Generate Mesh Configurations';
generateBtn.disabled = false;
}
alert('Error generating mesh configurations: ' + error.message);
}
}
function validateMeshInputs(networkCIDR, peerCount) {
// Validate network CIDR
const cidrRegex = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}\/([1-2]?[0-9]|3[0-2])$/;
if (!cidrRegex.test(networkCIDR)) {
alert('Invalid network CIDR format');
return false;
}
// Validate peer count
if (peerCount < 2 || peerCount > 20) {
alert('Number of peers must be between 2 and 20');
return false;
}
return true;
}
function generateMeshPeerConfig(currentPeer, allPeers, currentIndex, cidr, dns, presharedKeys) {
let config = `# ${currentPeer.name} Configuration (Mesh Network)
[Interface]
PrivateKey = ${currentPeer.privateKey}
Address = ${currentPeer.ip}/${cidr}`;
// Only add DNS if it's not empty
if (dns && dns.trim()) {
config += `
DNS = ${dns.trim()}`;
}
config += `
ListenPort = ${currentPeer.port}
`;
// Add all other peers as peers
allPeers.forEach((peer, peerIndex) => {
if (peerIndex !== currentIndex) {
// Get preshared key for this pair
const minIndex = Math.min(currentIndex, peerIndex);
const maxIndex = Math.max(currentIndex, peerIndex);
const presharedKey = presharedKeys[`${minIndex}-${maxIndex}`];
config += `# ${peer.name}
[Peer]
PublicKey = ${peer.publicKey}
PresharedKey = ${presharedKey}
AllowedIPs = ${peer.ip}/32`;
// Add endpoint if available
if (peer.endpoint) {
config += `
Endpoint = ${peer.endpoint}:${peer.port}`;
}
config += `
PersistentKeepalive = 25
`;
}
});
return config;
}
function displayMeshConfigurations() {
const meshConfigs = document.getElementById('meshConfigs');
meshConfigs.innerHTML = '';
currentMeshConfigs.forEach((peerConfig, index) => {
const configItem = document.createElement('div');
configItem.className = 'client-config-item';
configItem.innerHTML = `
<div class="client-config-title">${peerConfig.name}</div>
<div class="config-content">${peerConfig.config}</div>
<button class="download-btn" onclick="downloadConfig('${peerConfig.name}', \`${peerConfig.config.replace(/`/g, '\\`')}\`)">
Download ${peerConfig.name} Config
</button>
`;
meshConfigs.appendChild(configItem);
});
document.getElementById('configOutput').style.display = 'block';
}

312
static/styles.css Normal file
View file

@ -0,0 +1,312 @@
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background: white;
border-radius: 8px;
padding: 30px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.navigation {
margin-bottom: 20px;
}
.nav-link {
color: #007bff;
text-decoration: none;
font-weight: 500;
padding: 8px 12px;
border: 1px solid #007bff;
border-radius: 4px;
transition: all 0.2s;
}
.nav-link:hover {
background-color: #007bff;
color: white;
}
h1 {
color: #333;
text-align: center;
margin-bottom: 30px;
}
.privacy-disclaimer {
background-color: #e8f5e8;
border: 1px solid #28a745;
border-radius: 6px;
padding: 12px 16px;
margin-bottom: 25px;
color: #155724;
font-size: 14px;
line-height: 1.4;
}
.privacy-disclaimer strong {
color: #0d4b14;
}
.description {
text-align: center;
color: #666;
margin-bottom: 30px;
font-style: italic;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: 600;
color: #555;
}
input, select {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
}
button {
background-color: #007bff;
color: white;
padding: 12px 30px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
margin: 10px 5px;
}
button:hover {
background-color: #0056b3;
}
.config-output {
margin-top: 30px;
}
.config-section {
margin-bottom: 30px;
padding: 20px;
background-color: #f8f9fa;
border-radius: 6px;
border-left: 4px solid #007bff;
}
.config-content {
background-color: #2d3748;
color: #e2e8f0;
padding: 15px;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 13px;
line-height: 1.4;
white-space: pre-wrap;
overflow-x: auto;
}
.download-btn {
background-color: #28a745;
font-size: 14px;
padding: 8px 16px;
}
.download-btn:hover {
background-color: #218838;
}
.client-configs-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 20px;
margin-top: 20px;
}
.client-config-item {
background-color: #f8f9fa;
border-radius: 6px;
padding: 15px;
border-left: 4px solid #28a745;
}
.client-config-title {
font-weight: 600;
color: #333;
margin-bottom: 10px;
font-size: 16px;
}
.error {
color: #dc3545;
font-size: 14px;
margin-top: 5px;
}
.form-row {
display: flex;
gap: 20px;
}
.form-row .form-group {
flex: 1;
}
.seed-section {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 6px;
padding: 15px;
margin-bottom: 20px;
}
.seed-display {
font-family: 'Courier New', monospace;
background-color: #e9ecef;
padding: 8px;
border-radius: 4px;
font-size: 12px;
word-break: break-all;
border: 1px solid #ced4da;
}
.seed-buttons {
margin-top: 10px;
}
.seed-btn {
background-color: #6c757d;
font-size: 12px;
padding: 6px 12px;
margin-right: 5px;
}
.seed-btn:hover {
background-color: #545b62;
}
/* === MESH-SPECIFIC STYLES === */
.form-section {
margin-bottom: 30px;
padding: 20px;
background-color: #f8f9fa;
border-radius: 6px;
border-left: 4px solid #007bff;
}
.form-section h3 {
margin-top: 0;
color: #333;
margin-bottom: 15px;
}
#updatePeersBtn {
background-color: #6c757d;
font-size: 14px;
padding: 8px 16px;
margin: 5px 0 0 10px;
}
#updatePeersBtn:hover {
background-color: #545b62;
}
.peer-input-group {
background-color: #ffffff;
border: 1px solid #dee2e6;
border-radius: 6px;
padding: 15px;
margin-bottom: 15px;
}
.peer-input-group h4 {
margin-top: 0;
margin-bottom: 15px;
color: #495057;
font-size: 16px;
}
.peer-row {
display: grid;
grid-template-columns: 1fr 1fr 100px;
gap: 15px;
align-items: end;
}
@media (max-width: 768px) {
.peer-row {
grid-template-columns: 1fr;
gap: 10px;
}
.form-row {
flex-direction: column;
gap: 0;
}
.client-configs-row {
grid-template-columns: 1fr;
}
}
.footer {
margin-top: 50px;
background-color: #343a40;
color: #fff;
padding: 30px 0;
}
.footer-content {
max-width: 800px;
margin: 0 auto;
padding: 0 20px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 30px;
}
.footer-column h4 {
margin-bottom: 15px;
color: #fff;
font-size: 16px;
}
.footer-column p {
margin: 0;
color: #adb5bd;
font-size: 14px;
line-height: 1.5;
}
.footer-column a {
color: #007bff;
text-decoration: none;
}
.footer-column a:hover {
color: #0056b3;
text-decoration: underline;
}
@media (max-width: 768px) {
.footer-content {
grid-template-columns: 1fr;
gap: 20px;
text-align: center;
}
}