diff options
Diffstat (limited to 'protocols')
4 files changed, 286 insertions, 28 deletions
diff --git a/protocols/luci-proto-wireguard/Makefile b/protocols/luci-proto-wireguard/Makefile index f88b90bbfa..75c5e1868f 100644 --- a/protocols/luci-proto-wireguard/Makefile +++ b/protocols/luci-proto-wireguard/Makefile @@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk LUCI_TITLE:=Support for WireGuard VPN -LUCI_DEPENDS:=+wireguard-tools +LUCI_DEPENDS:=+wireguard-tools +libuci-lua LUCI_PKGARCH:=all include ../../luci.mk diff --git a/protocols/luci-proto-wireguard/htdocs/luci-static/resources/protocol/wireguard.js b/protocols/luci-proto-wireguard/htdocs/luci-static/resources/protocol/wireguard.js index e7e69a3d5b..5795eaebec 100644 --- a/protocols/luci-proto-wireguard/htdocs/luci-static/resources/protocol/wireguard.js +++ b/protocols/luci-proto-wireguard/htdocs/luci-static/resources/protocol/wireguard.js @@ -11,6 +11,13 @@ var generateKey = rpc.declare({ expect: { keys: {} } }); +var generateQrCode = rpc.declare({ + object: 'luci.wireguard', + method: 'generateQrCode', + params: ['privkey', 'psk', 'allowed_ips'], + expect: { qr_code: '' } +}); + function validateBase64(section_id, value) { if (value.length == 0) return true; @@ -24,6 +31,24 @@ function validateBase64(section_id, value) { return true; } +function findSection(sections, name) { + for (var i = 0; i < sections.length; i++) { + var section = sections[i]; + if (section['.name'] == name) return section; + } + + return null; +} + +function generateDescription(name, texts) { + return E('li', { 'style': 'color: inherit;' }, [ + E('span', name), + E('ul', texts.map(function (text) { + return E('li', { 'style': 'color: inherit;' }, text); + })) + ]); +} + return network.registerProtocol('wireguard', { getI18n: function() { return _('WireGuard VPN'); @@ -131,6 +156,77 @@ return network.registerProtocol('wireguard', { o.datatype = 'string'; o.optional = true; + o = ss.option(form.Value, 'description', _('QR-Code')); + o.render = L.bind(function (view, section_id) { + var sections = uci.sections('network'); + var client = findSection(sections, section_id); + var serverName = this.getIfname(); + var server = findSection(sections, serverName); + + var interfaceTexts = [ + 'PrivateKey: ' + _('A random, on the fly generated "PrivateKey", the key will not be saved on the router') + ]; + + var peerTexts = [ + 'PublicKey: ' + _('The "PublicKey" of that wg interface'), + 'AllowedIPs: ' + _('The list of this client\'s "AllowedIPs" or "0.0.0.0/0, ::/0" if not configured'), + 'PresharedKey: ' + _('If available, the client\'s "PresharedKey"') + ]; + + var description = [ + E('span', [ + _('If there are any unsaved changes for this client, please save the configuration before generating a QR-Code'), + E('br'), + _('The QR-Code works per wg interface, it will be refreshed with every button click and transfers the following information:') + ]), + E('ul', [ + generateDescription('[Interface]', interfaceTexts), + generateDescription('[Peer]', peerTexts) + ]) + ]; + + return E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('QR-Code')), + E('div', { + 'style': 'display: flex; flex-direction: column; align-items: baseline;', + 'id': 'qr-' + section_id + }, [ + E('button', { + 'class': 'btn cbi-button cbi-button-apply', + 'click': ui.createHandlerFn(this, function (server, client, section_id) { + var qrDiv = document.getElementById('qr-' + section_id); + var qrEl = qrDiv.querySelector('value'); + var qrBtn = qrDiv.querySelector('button'); + var qrencodeErr = '<b>%q</b>'.format( + _('For QR-Code support please install the qrencode package!')); + + if (qrEl.innerHTML != '' && qrEl.innerHTML != qrencodeErr) { + qrEl.innerHTML = ''; + qrBtn.innerHTML = _('Generate New QR-Code') + } else { + qrEl.innerHTML = _('Loading QR-Code...'); + + generateQrCode(server.private_key, client.preshared_key, + client.allowed_ips).then(function (qrCode) { + if (qrCode == '') { + qrEl.innerHTML = qrencodeErr; + } else { + qrEl.innerHTML = qrCode; + qrBtn.innerHTML = _('Hide QR-Code'); + } + }); + } + }, server, client, section_id) + }, _('Generate new QR-Code')), + E('value', { + 'class': 'cbi-section', + 'style': 'margin: 0;' + }), + E('div', { 'class': 'cbi-value-description' }, description) + ]) + ]); + }, this); + o = ss.option(form.Value, 'public_key', _('Public Key'), _('Required. Base64-encoded public key of peer.')); o.validate = validateBase64; o.rmempty = false; diff --git a/protocols/luci-proto-wireguard/root/usr/libexec/rpcd/luci.wireguard b/protocols/luci-proto-wireguard/root/usr/libexec/rpcd/luci.wireguard index a6c951f3de..ce21570cce 100755 --- a/protocols/luci-proto-wireguard/root/usr/libexec/rpcd/luci.wireguard +++ b/protocols/luci-proto-wireguard/root/usr/libexec/rpcd/luci.wireguard @@ -1,26 +1,185 @@ -#!/bin/sh - -. /usr/share/libubox/jshn.sh - -case "$1" in - list) - json_init - json_add_object "generateKeyPair" - json_close_object - json_dump - ;; - call) - case "$2" in - generateKeyPair) - prv=$(wg genkey) - pub=$(echo $prv | wg pubkey) - json_init - json_add_object "keys" - json_add_string "priv" "$prv" - json_add_string "pub" "$pub" - json_close_object - json_dump - ;; - esac - ;; -esac +#!/usr/bin/env lua + +local json = require "luci.jsonc" +local sys = require "luci.sys" +local io = require "io" +local uci = require "uci" +local fs = require "nixio.fs" + +local methods = { + generateKeyPair = { + call = function() + local prv = sys.exec("wg genkey 2>/dev/null"):sub(1, -2) + local pub = sys.exec("echo '" .. prv .. "' | wg pubkey 2>/dev/null"):sub(1, -2) + + return {keys = {priv = prv, pub = pub}} + end + }, + generateQrCode = { + args = {privkey = "privkey", psk = "psk", allowed_ips = {"allowed_ips"}}, + call = function(args) + local qr_code + + if fs.access("/usr/bin/qrencode") then + local psk = args.psk + local listen_port = args.listen_port + local allowed_ips = args.allowed_ips + + local pubkey = sys.exec("echo '" .. args.privkey .. "' | wg pubkey 2>/dev/null"):sub(1, -2) + local client_privkey = sys.exec("wg genkey 2>/dev/null"):sub(1, -2) + + local iface_qr = { + "[Interface]", + "PrivateKey = " .. client_privkey, + } + + local peer_qr = { + "[Peer]", + "PublicKey = " .. pubkey, + } + + if not allowed_ips or next(allowed_ips) == nil then + allowed_ips = {"0.0.0.0/0", "::/0"} + end + table.insert(peer_qr, "AllowedIPs = " .. table.concat(allowed_ips, ", ")) + + if psk then + table.insert(peer_qr, "PresharedKey = " .. psk) + end + + qr_enc = table.concat(iface_qr, "\n") .. "\n\n" .. table.concat(peer_qr, "\n") + qr_code = sys.exec("/usr/bin/qrencode --inline --8bit --type=SVG --output=- '" .. qr_enc .. "' 2>/dev/null") + end + + return {qr_code = qr_code} + end + }, + getWgInstances = { + call = function() + local data = {} + local last_device = "" + local qr_pubkey = {} + + local wg_dump = io.popen("wg show all dump 2>/dev/null") + if wg_dump then + local line + for line in wg_dump:lines() do + local line = string.split(line, "\t") + if not (last_device == line[1]) then + last_device = line[1] + data[line[1]] = { + name = line[1], + public_key = line[3], + listen_port = line[4], + fwmark = line[5], + peers = {} + } + if not line[3] or line[3] == "" or line[3] == "(none)" then + qr_pubkey[line[1]] = "" + else + qr_pubkey[line[1]] = "PublicKey = " .. line[3] + end + else + local peer_name + local cur = uci.cursor() + + cur:foreach( + "network", + "wireguard_" .. line[1], + function(s) + if s.public_key == line[2] then + peer_name = s.description + end + end + ) + + local peer = { + name = peer_name, + public_key = line[2], + endpoint = line[4], + allowed_ips = {}, + latest_handshake = line[6], + transfer_rx = line[7], + transfer_tx = line[8], + persistent_keepalive = line[9] + } + + if not (line[4] == "(none)") then + local ipkey, ipvalue + for ipkey, ipvalue in pairs(string.split(line[5], ",")) do + if #ipvalue > 0 then + table.insert(peer["allowed_ips"], ipvalue) + end + end + end + + table.insert(data[line[1]].peers, peer) + end + end + end + + return data + end + } +} + +local function parseInput() + local parse = json.new() + local done, err + + while true do + local chunk = io.read(4096) + if not chunk then + break + elseif not done and not err then + done, err = parse:parse(chunk) + end + end + + if not done then + print(json.stringify({error = err or "Incomplete input"})) + os.exit(1) + end + + return parse:get() +end + +local function validateArgs(func, uargs) + local method = methods[func] + if not method then + print(json.stringify({error = "Method not found"})) + os.exit(1) + end + + if type(uargs) ~= "table" then + print(json.stringify({error = "Invalid arguments"})) + os.exit(1) + end + + uargs.ubus_rpc_session = nil + + local k, v + local margs = method.args or {} + for k, v in pairs(uargs) do + if margs[k] == nil or (v ~= nil and type(v) ~= type(margs[k])) then + print(json.stringify({error = "Invalid arguments"})) + os.exit(1) + end + end + + return method +end + +if arg[1] == "list" then + local _, method, rv = nil, nil, {} + for _, method in pairs(methods) do + rv[_] = method.args or {} + end + print((json.stringify(rv):gsub(":%[%]", ":{}"))) +elseif arg[1] == "call" then + local args = parseInput() + local method = validateArgs(arg[2], args) + local result, code = method.call(args) + print((json.stringify(result):gsub("^%[%]$", "{}"))) + os.exit(code or 0) +end diff --git a/protocols/luci-proto-wireguard/root/usr/share/rpcd/acl.d/luci-wireguard.json b/protocols/luci-proto-wireguard/root/usr/share/rpcd/acl.d/luci-wireguard.json index 4bbcb81578..04877d4f49 100644 --- a/protocols/luci-proto-wireguard/root/usr/share/rpcd/acl.d/luci-wireguard.json +++ b/protocols/luci-proto-wireguard/root/usr/share/rpcd/acl.d/luci-wireguard.json @@ -3,7 +3,10 @@ "description": "Grant access to LuCI Wireguard procedures", "write": { "ubus": { - "luci.wireguard": [ "generateKeyPair" ] + "luci.wireguard": [ + "generateKeyPair", + "generateQrCode" + ] } } } |