init
This commit is contained in:
commit
fd1d199ae8
6 changed files with 1421 additions and 0 deletions
21
LICENSE
Normal file
21
LICENSE
Normal 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
46
README.md
Normal 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
108
index.html
Normal 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
97
mesh-generator/index.html
Normal 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
837
static/script.js
Normal 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
312
static/styles.css
Normal 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;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue