var iface = 'wg0'; function parse_placeholder(val) { return true; } function parse_positive_integer(src) { var val = parseInt(src); if (isNaN(val) || val < 1) throw new Error('Cannot parse as a positive integer.'); return val; } function parse_keepalive(src) { return parse_positive_integer(src); } function parse_endpoint(val) { val = val.trim(); if (val.length == 0) throw new Error('Server endpoint cannot be an empty string.'); if (!val.match(/^.+:[0-9]+$/)) throw new Error('Please specify a host and a port in the HOST:PORT format.'); return val; } function parse_key(val) { val = val.trim(); if (val.length == 0) throw new Error('Key as used by WireGuard cannot be an empty string.'); if (!val.match(/^[0-9a-zA-Z+/=]+$/)) throw new Error('Key as used by WireGuard must be Base64-encoded.'); return val; } var ip_address_lib = require('ip-address'); function parse_ip(src, parser) { src = src.trim(); if (src.length == 0) throw new Error('IP address cannot be an empty string.'); try { var result = new parser(src); } catch (err) { throw new Error('Sorry, IP address validation failed with the following error:\n' + err.message); } if (!result.parsedSubnet) throw new Error('Please specify address using the CIDR format (including the netmask).'); var addr = result.correctForm(); var netmask = result.subnetMask; var network_id = result.startAddress().correctForm(); return [addr, netmask, network_id]; } function parse_ipv4(src) { var parser = ip_address_lib.Address4; var result = parse_ip(src, parser); var addr = result[0]; var netmask = result[1]; var network_id = result[2]; var broadcast_addr = new parser(src).endAddress().correctForm(); if (addr == network_id && netmask != 32) throw new Error('Please use a real host IP address, not a network identifier.'); if (addr == broadcast_addr && netmask != 32) throw new Error('Please use a real host IP address, not a broadcast address.'); return [addr, netmask, network_id]; } function parse_ipv6(val) { return parse_ip(val, ip_address_lib.Address6); } var Input = function(name) { this.name = name; } Input.prototype.input_id = function() { return '#param_' + this.name; } Input.prototype.error_id = function() { return this.input_id() + '_error'; } Input.prototype.container_id = function() { return this.input_id() + '_container'; } Input.prototype.value = function() { return $(this.input_id()).val(); } Input.prototype.has_value = function() { return this.value().length > 0; } Input.prototype.set = function(value) { $(this.input_id()).val(value); } Input.prototype.show = function() { $(this.container_id()).show(); } Input.prototype.hide = function() { $(this.container_id()).hide(); } Input.prototype.show_error = function(msg) { this.show(); $(this.error_id()).text(msg); $(this.error_id()).show(); } Input.prototype.hide_error = function() { $(this.error_id()).hide(); } var Value = function(name, parser) { this.name = name; this.input = new Input(name); this.parser = parser; this.optional = false; this.advanced = false; this.value = null; this.error = null; } Value.prototype.parse = function() { try { var value = this.input.value(); if (!this.optional || value.length > 0) value = this.parser(value); this.set_value(value); return true; } catch (err) { this.set_error(err.message); return false; } } Value.prototype.set_value = function(value) { this.value = value; this.error = null; this.input.hide_error(); } Value.prototype.set_error = function(msg) { this.value = null; this.error = msg; this.input.show_error(this.error); } Value.prototype.is_set = function() { return this.input.has_value(); } Value.prototype.set = function(value) { this.input.set(value); } Value.prototype.show = function() { this.input.show(); } Value.prototype.hide = function() { this.input.hide(); } var Endpoint = function(name) { Value.call(this, name, parse_endpoint); } Endpoint.prototype = Object.create(Value.prototype); Endpoint.prototype.constructor = Endpoint; var Key = function(name) { Value.call(this, name, parse_key); } Key.prototype = Object.create(Value.prototype); Key.prototype.constructor = Key; var IPAddr = function(name, parser) { Value.call(this, name, parser); } IPAddr.prototype = Object.create(Value.prototype); IPAddr.prototype.constructor = IPAddr; IPAddr.prototype.set_value = function(value) { Value.prototype.set_value.call(this, value); this.addr = this.value[0]; this.netmask = this.value[1]; this.network_id = this.value[2]; } IPAddr.prototype.full_address = function() { return this.addr + '/' + this.netmask; } IPAddr.prototype.allowed_ips_client = function() { // As of this writing, _every_ tool accepts something like 192.168.0.1/24 // here, _except_ for the Android app, which _requires_ the least // significant bits to be zeroed out. return this.network_id + '/' + this.netmask; } var IPv4 = function(name) { IPAddr.call(this, name, parse_ipv4); } IPv4.prototype = Object.create(IPAddr.prototype); IPv4.prototype.constructor = IPv4; IPv4.prototype.allowed_ips_server = function() { return this.addr + '/' + 32; } var IPv6 = function(name) { IPAddr.call(this, name, parse_ipv6); } IPv6.prototype = Object.create(IPAddr.prototype); IPv6.prototype.constructor = IPv6; IPv6.prototype.allowed_ips_server = function() { return this.addr + '/' + 128; } var Keepalive = function(name) { Value.call(this, name, parse_keepalive); this.optional = true; this.advanced = true; } Keepalive.prototype = Object.create(Value.prototype); Keepalive.prototype.constructor = Keepalive; var Data = function() { this.server_public = new Key('server_public'); this.server_endpoint = new Endpoint('server_endpoint'); this.preshared = new Key('preshared'); this.client_public = new Key('client_public'); this.client_private = new Key('client_private'); this.client_ipv4 = new IPv4('client_ipv4'); this.client_ipv6 = new IPv6('client_ipv6'); this.keepalive = new Keepalive('keepalive'); this.values = [ this.server_public, this.server_endpoint, this.preshared, this.client_public, this.client_private, this.client_ipv4, this.client_ipv6, this.keepalive ]; } Data.prototype.set_from_url = function(url) { var url = new URL(url); var params = new URLSearchParams(url.search); var has = false; this.values.forEach(function(value) { if (params.has(value.name)) { value.set(params.get(value.name)); has = true; } }); return has; } Data.prototype.set_from_this_url = function() { return this.set_from_url(window.location.href); }; Data.prototype.get_perma_url = function() { var url = new URL(window.location.href); url.search = ''; var params = new URLSearchParams(); this.values.forEach(function(value) { params.append(value.name, value.input.value()); }); url.search = params.toString(); return url.toString(); } Data.prototype.parse = function() { var success = true; this.values.forEach(function(value) { if (!value.parse()) success = false; }); if (success) { if (!$('#advanced_params').prop('checked')) this.hide_advanced(); this.hide_error(); } else { this.show_error(); } return success; } Data.prototype.show_error = function() { $('#params_error').text('Please correct the input errors above first.'); $('#params_error').show(); } Data.prototype.hide_error = function() { $('#params_error').hide(); } Data.prototype.show_advanced = function() { this.values.forEach(function(value) { if (!value.advanced) return; value.show(); }); } Data.prototype.hide_advanced = function() { this.values.forEach(function(value) { if (!value.advanced) return; value.hide(); }); } function edit_btn_init(btn) { btn.empty(); btn.append(''); } function edit_btn_save(btn) { btn.empty(); btn.append(''); } function edit_btn_on_click(btn, pre) { var editable = pre.prop('isContentEditable'); pre.prop('contentEditable', !editable); if (editable) { edit_btn_init(btn); btn.blur(); // a.k.a. unfocus } else { edit_btn_save(btn); pre.focus(); } } function dload_btn_init(btn) { btn.empty(); btn.append(''); } function basename(path) { return path.substring(path.lastIndexOf('/') + 1); } function dload_btn_on_click(btn, path, pre) { var blob = new Blob([pre.text()], {type: 'text/plain'}); var url = URL.createObjectURL(blob); var link = $(''); link.prop('href', url); link.prop('download', basename(path)); // Whoever thought of this [0] is fucking crazy: // https://stackoverflow.com/a/36483380/514684 // It literally silently does nothing if this is omitted. link[0].click(); } function make_pre_buttons(path, pre) { var edit_btn = $(''); edit_btn_init(edit_btn); edit_btn.click(function() { edit_btn_on_click(edit_btn, pre); }); var dload_btn = $(''); dload_btn_init(dload_btn); dload_btn.click(function() { dload_btn_on_click(dload_btn, path, pre); }); return $('
') .append(dload_btn) .append(edit_btn); } function format_pre_text(text, name) { var pre = $('').text(text); return $('') .append(pre) .append(make_pre_buttons(name, pre)); } var ConfigFile = function(name, text) { this.name = name; this.text = text; } ConfigFile.prototype.toString = function() { return this.text; } ConfigFile.prototype.format = function() { return format_pre_text(this.toString(), this.name); } var QRCode = function(text) { this.text = text; } QRCode.prototype.format = function() { var canvas = $(''); var qr = new QRious({ element: canvas[0], value: this.text, size: 350 }); return $('').append(canvas); } function wg_quick_client_file(data) { var path = `/etc/wireguard/${iface}.conf`; var contents = `# On the client, put this to # ${path} # and either # * start the wg-quick@${iface} systemd service, # * or run \`wg-quick up ${iface}\`. [Interface] PrivateKey = ${data.client_private.value} Address = ${data.client_ipv4.full_address()}, ${data.client_ipv6.full_address()} [Peer] Endpoint = ${data.server_endpoint.value} PublicKey = ${data.server_public.value} PresharedKey = ${data.preshared.value} AllowedIPs = ${data.client_ipv4.allowed_ips_client()}, ${data.client_ipv6.allowed_ips_client()} `; if (data.keepalive.is_set()) { contents += `PersistentKeepalive = ${data.keepalive.value} `; } return new ConfigFile(path, contents); } function wg_quick_server_file(data) { var path = `/etc/wireguard/${iface}.conf`; return new ConfigFile(path, `# On the server, add this to # ${path} # and restart the wg-quick@${iface} systemd service. # Previous contents goes here... [Peer] PublicKey = ${data.client_public.value} PresharedKey = ${data.preshared.value} AllowedIPs = ${data.client_ipv4.allowed_ips_server()}, ${data.client_ipv6.allowed_ips_server()} `); } function systemd_client_netdev_file(data) { var path = `/etc/systemd/network/${iface}.netdev`; var contents = `# On the client, you need two files. Put this into # ${path} # and after you're done with both files, run # systemctl daemon-reload # systemctl restart systemd-networkd [NetDev] Name = ${iface} Kind = wireguard [WireGuard] PrivateKey = ${data.client_private.value} [WireGuardPeer] Endpoint = ${data.server_endpoint.value} PublicKey = ${data.server_public.value} PresharedKey = ${data.preshared.value} AllowedIPs = ${data.client_ipv4.allowed_ips_client()}, ${data.client_ipv6.allowed_ips_client()} `; if (data.keepalive.is_set()) { contents += `PersistentKeepalive = ${data.keepalive.value} `; } return new ConfigFile(path, contents); } function systemd_client_network_file(data) { var path = `/etc/systemd/network/${iface}.network`; return new ConfigFile(path, `# This is the second file. Put this into # ${path} # and if you're done with the first file already, run # systemctl daemon-reload # systemctl restart systemd-networkd [Match] Name = ${iface} [Network] Address = ${data.client_ipv4.full_address()} Address = ${data.client_ipv6.full_address()} `); } function systemd_server_netdev_file(data) { var path = `/etc/systemd/network/${iface}.netdev`; return new ConfigFile(path, `# On the server, add this to # ${path} # and run # systemctl daemon-reload # systemctl restart systemd-networkd # Previous contents goes here... [WireGuardPeer] PublicKey = ${data.client_public.value} PresharedKey = ${data.preshared.value} AllowedIPs = ${data.client_ipv4.allowed_ips_server()}, ${data.client_ipv6.allowed_ips_server()} `); } function nmcli_client_file(data) { var path = `/etc/NetworkManager/system-connections/${iface}.nmconnection`; var contents = `# On the client, put this to # ${path} # and run # chmod 0600 ${path} # nmcli c reload # nmcli c up ${iface} [connection] id=${iface} type=wireguard interface-name=${iface} [wireguard] private-key=${data.client_private.value} private-key-flags=0 [wireguard-peer.${data.server_public.value}] endpoint=${data.server_endpoint.value} preshared-key=${data.preshared.value} preshared-key-flags=0 allowed-ips=${data.client_ipv4.allowed_ips_client()};${data.client_ipv6.allowed_ips_client()}; `; if (data.keepalive.is_set()) { contents += `persistent-keepalive=${data.keepalive.value} `; } contents += ` [ipv4] address1=${data.client_ipv4.full_address()} method=manual [ipv6] address1=${data.client_ipv6.full_address()} method=manual `; return new ConfigFile(path, contents); } function nmcli_server_file(data) { var path = `/etc/NetworkManager/system-connections/${iface}.nmconnection`; return new ConfigFile(path, `# On the server, add this to # ${path} # and run # nmcli c reload # nmcli c up ${iface} # Previous contents goes here... [wireguard-peer.${data.client_public.value}] preshared-key=${data.preshared.value} preshared-key-flags=0 allowed-ips=${data.client_ipv4.allowed_ips_server()};${data.client_ipv6.allowed_ips_server()}; `); } var shell_info = `# Make sure your .bash_history or whatever is # secure before running this. # Better yet, put into a file and run (possibly, using sudo).`; function manual_client_script(data) { return new ConfigFile('client_setup.sh', `# On the client, run this to set up a connection. ${shell_info} ip link add dev ${iface} type wireguard ip addr add ${data.client_ipv4.full_address()} dev ${iface} ip addr add ${data.client_ipv6.full_address()} dev ${iface} wg set ${iface} \\ private-key <( echo ${data.client_private.value} ) wg set ${iface} \\ peer ${data.server_public.value} \\ preshared-key <( echo ${data.preshared.value} ) \\ endpoint ${data.server_endpoint.value} \\ allowed-ips ${data.client_ipv4.allowed_ips_client()},${data.client_ipv6.allowed_ips_client()} ip link set ${iface} up `); } function manual_server_script(data) { return new ConfigFile('server_setup.sh', `# On the server, run this to tweak an existing connection. ${shell_info} wg set ${iface} \\ peer ${data.client_public.value} \\ preshared-key <( echo ${data.preshared.value} ) \\ allowed-ips ${data.client_ipv4.allowed_ips_server()},${data.client_ipv6.allowed_ips_server()} `); } var Guide = function(name) { this.name = name; } Guide.prototype.format = function(data) { var container = $(''); container.append($('').html(this.name)); return container; } var GuidePermaURL = function() { Guide.call(this, 'Permanent URL'); } GuidePermaURL.prototype = Object.create(Guide.prototype); GuidePermaURL.prototype.constructor = GuidePermaURL; GuidePermaURL.prototype.format = function(data) { var container = Guide.prototype.format.call(this, data); container .append($('').text('Use the following URL to share this configuration. Be careful: it probably contains sensitive data, like your private keys.')) .append($('').append($('', {href: data.get_perma_url()}).text(data.get_perma_url()))); return container; } var GuideServerClient = function(name) { Guide.call(this, name); } GuideServerClient.prototype = Object.create(Guide.prototype); GuideServerClient.prototype.constructor = GuideServerClient; GuideServerClient.prototype.for_server = function(data) { return []; } GuideServerClient.prototype.for_client = function(data) { return []; } GuideServerClient.prototype.join_guides = function(guides) { var container = $(''); guides.forEach(function(guide) { container.append(guide.format()); }); return container; } GuideServerClient.prototype.format = function(data) { var container = Guide.prototype.format.call(this, data); container .append($('') .append($('') .append(this.join_guides(this.for_server(data)))) .append($('') .append(this.join_guides(this.for_client(data))))); return container; } var GuideWgQuick = function() { GuideServerClient.call(this, 'wg-quick'); } GuideWgQuick.prototype = Object.create(GuideServerClient.prototype); GuideWgQuick.prototype.constructor = GuideWgQuick; GuideWgQuick.prototype.for_client = function(data) { var config = wg_quick_client_file(data); var qr = new QRCode(config.text); return [config, qr]; } GuideWgQuick.prototype.for_server = function(data) { return [wg_quick_server_file(data)]; } var GuideSystemd = function() { GuideServerClient.call(this, 'systemd-networkd'); } GuideSystemd.prototype = Object.create(GuideServerClient.prototype); GuideSystemd.prototype.constructor = GuideSystemd; GuideSystemd.prototype.for_client = function(data) { return [systemd_client_netdev_file(data), systemd_client_network_file(data)]; } GuideSystemd.prototype.for_server = function(data) { return [systemd_server_netdev_file(data)]; } var GuideNetworkManager = function() { GuideServerClient.call(this, 'NetworkManager'); } GuideNetworkManager.prototype = Object.create(GuideServerClient.prototype); GuideNetworkManager.prototype.constructor = GuideNetworkManager; GuideNetworkManager.prototype.for_client = function(data) { return [nmcli_client_file(data)]; } GuideNetworkManager.prototype.for_server = function(data) { return [nmcli_server_file(data)]; } var GuideManual = function() { GuideServerClient.call(this, 'ip
& wg
');
}
GuideManual.prototype = Object.create(GuideServerClient.prototype);
GuideManual.prototype.constructor = GuideManual;
GuideManual.prototype.for_client = function(data) {
return [manual_client_script(data)];
}
GuideManual.prototype.for_server = function(data) {
return [manual_server_script(data)];
}
function clear_guides() {
$('#guides').empty();
}
function add_guide(guide, data) {
$('#guides').append(guide.format(data));
}
function guides_from_data(data) {
if (!data.parse()) {
return;
}
var guides = [
new GuidePermaURL(),
new GuideWgQuick(),
new GuideSystemd(),
new GuideNetworkManager(),
new GuideManual()
];
guides.forEach(function(guide) {
add_guide(guide, data);
});
}
function form_on_submit() {
clear_guides();
var data = new Data();
guides_from_data(data);
return false;
}
function advanced_params_on_click(input) {
var data = new Data();
if (input.checked)
data.show_advanced();
else
data.hide_advanced();
}
function main() {
var data = new Data();
if (data.set_from_this_url())
guides_from_data(data);
}
$(function() {
main();
});