#!/usr/bin/lua --[[ Part of AREDN -- Used for creating Amateur Radio Emergency Data Networks Copyright (C) 2021 Tim Wilkinson Original Perl Copyright (c) 2015 Darryl Quinn See Contributors file for additional contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation version 3 of the License. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Additional Terms: Additional use restrictions exist on the AREDN(TM) trademark and logo. See AREDNLicense.txt for more info. Attributions to the AREDN Project must be retained in the source code. If importing this code into a new or existing project attribution to the AREDN project must be added to the source code. You must not misrepresent the origin of the material contained within. Modified versions must be modified to attribute to the original source and be marked in reasonable ways as differentiate it from the original version --]] require("nixio") require("aredn.http") require("aredn.utils") require("aredn.html") require("aredn.hardware") aredn.info = require("aredn.info") require("uci") local html = aredn.html local cursor = uci.cursor("/etc/config.mesh"); local node = aredn.info.get_nvram("node") if node == "" then node = "NOCALL" end local config = aredn.info.get_nvram("config"); local VPNVER = "1.1" -- post_data local parms = {} if os.getenv("REQUEST_METHOD") == "POST" then require('luci.ohttp') local request = luci.http.Request(nixio.getenv(), function() local v = io.read(1024) if not v then io.close() end return v end ) parms = request:formvalue() end -- wireguard local wireguard_mask_size = tonumber(cursor:get("wireguard", "@wireguard_server[0]", "masksize") or 26) local wireguard_max = nixio.bit.lshift(1, 32 - wireguard_mask_size) local wireguard_alive_time = 300 -- 5 minutes -- helpers start local cli_err = {} function err(msg) cli_err[#cli_err + 1] = msg end local errors = {} function err2(msg) errors[#errors + 1] = msg end local hidden = {} function hide(inp) hidden[#hidden + 1] = inp end function get_active_tun() local tuns = {} local f = io.popen("ps -w | grep vtun | grep ' tun '") if f then for line in f:lines() do local m = line:match(".*:.*-(172%-.*)%stun%stun.*") if m then tuns[#tuns + 1] = m:gsub("-", ".") end end f:close() end return tuns end function get_active_wgtun() local tuns = {} local f = io.popen("/usr/bin/wg show all latest-handshakes") if f then for line in f:lines() do local k,v = line:match("^%S+%s+(%S+)%s+(%S+)%s*$") if k then tuns[k] = tonumber(v) -- time in seconds end end f:close() end return tuns end function is_tunnel_active(ip, tunnels) for _, aip in ipairs(tunnels) do if ip == aip then return true end end return false end function is_wgtunnel_active(key, wgtunnels) local key = key:match("^.*=(.*=)$") local v = wgtunnels[key] if v and v + wireguard_alive_time > os.time() then return true end return false end function get_server_network_address() local server_net = cursor:get("vtun", "@network[0]", "start") if not server_net then local mac = aredn.hardware.get_interface_mac("eth0") local a, b = mac:match("^..:..:..:..:(..):(..)$") local net_base = "172.31." if cursor:get("aredn", "@supernode[0]", "enable") == "1" then net_base = "172.30." end server_net = net_base .. tonumber(b, 16) .. "." .. ((tonumber(a, 16) * 4) % 256) end local a, b, c, d = server_net:match("^(%d+).(%d+).(%d+).(%d+)$") return { a, b, c, d } end function get_wireguard_network_address(netw) local c = netw[3] + 1 if c > 255 then c = 0 end local d = nixio.bit.band(netw[4], nixio.bit.lshift(255, 32 - wireguard_mask_size)) return { netw[1], netw[2], c, d, wireguard_mask_size } end function get_server_dns() local dns = cursor:get("vtun", "@network[0]", "dns") return dns and dns or "" end function get_wireguard_public() local wg = cursor:get("wireguard", "@wireguard_server[0]", "public") return wg or "" end -- helper end -- load client info from uci local gci_vars = { "enabled", "name", "passwd", "netip", "contact" } function get_client_info() local c = 0 cursor:foreach("vtun", "client", function(section) for _, var in ipairs(gci_vars) do local key = "client" .. c .. "_" .. var parms[key] = section[var] if not parms[key] then parms[key] = "" end end c = c + 1 end ) parms.client_num = c end -- wireguard local gci_vars = { "enabled", "name", "key", "clientip", "contact" } function get_wgclient_info() local c = 0 cursor:foreach("wireguard", "client", function(section) for _, var in ipairs(gci_vars) do local key = "wgclient" .. c .. "_" .. var parms[key] = section[var] if not parms[key] then parms[key] = "" end end c = c + 1 end ) parms.wgclient_num = c end if parms.button_reboot then os.execute("reboot >/dev/null 2>&1") os.exit() end if config == "" or nixio.fs.stat("/tmp/reboot-required") then http_header(); html.header(node .. " setup", true); html.print("
") html.alert_banner() html.navbar_admin("vpn") html.print("") html.print("
") html.print("

") if config == "" then html.print("This page is not available until the configuration has been set.") else html.print("The configuration has been changed.
This page will not be available until the node is rebooted.
") html.print("
") html.print("") html.print("
") end html.print("
") http_footer() os.exit(); end if parms.button_reset then cursor:revert("vtun") cursor:delete("vtun", "@options[0]", "port") cursor:delete("vtun", "@network[0]", "start") cursor:delete("vtun", "@network[0]", "dns") end -- get vtun network address local netw = get_server_network_address() local netwg = get_wireguard_network_address(netw) local dns = get_server_dns() local wireguard_public = get_wireguard_public() -- if RESET or FIRST TIME load client/servers from file into parms if parms.button_reset or not parms.reload then cursor:revert("vtun") get_client_info() get_wgclient_info() parms.server_net1 = netw[3] parms.server_net2 = netw[4] parms.dns = dns parms.wireguard_public = wireguard_public -- initialize the "add" entries to clear them parms.client_add_enabled = "0" parms.client_add_name = "" parms.client_add_passwd = "" parms.wgclient_add_enabled = "0" parms.wgclient_add_name = "" parms.wgclient_add_key = "" end local list = {} for i = 0,parms.client_num-1 do list[#list + 1] = i end list[#list + 1] = "_add" local client_num = 0 local vars = { "enabled", "name", "passwd", "netip", "contact" } local vars2 = { "net", "enabled", "name", "passwd", "netip", "contact" } for _, val in ipairs(list) do for _ = 1,1 do for _, var in ipairs(vars) do local varname = "client" .. val .. "_" .. var if var == "enabled" and not parms[varname] then parms[varname] = "0" elseif not parms[varname] then parms[varname] = "" elseif var == "contact" then parms[varname] = parms[varname]:gsub("^%s+", ""):gsub("%s+$", ""):sub(1,210):gsub('"',"""):gsub("'","'"):gsub("<","<"):gsub(">",">") else parms[varname] = parms[varname]:gsub("^%s+", ""):gsub("%s+$", "") end if val ~= "_add" and parms[varname] == "" and var == "enabled" then parms[varname] = "0" end _G[var] = parms[varname] end if val == "_add" and not ((enabled ~= "0" or name ~= "" or passwd ~= "" or contact ~= "") and (parms.client_add or parms.button_save)) then break end if val == "_add" and parms.button_save then err(val .. " this client must be added or cleared out before saving changes") break end if passwd == "" then err("A client password is required") end if passwd:match("[^%w@]") then err("The password cannot contain non-alphanumeric characters (#" .. client_num .. ")") end if not passwd:match("%a") then err("The password must contain at least one alphabetic character (#" .. client_num .. ")") end if name == "" then err("A client name is required") end if val == "_add" and #cli_err > 0 and cli_err[#cli_err]:match("^" .. val .. " ") then break end parms["client" .. client_num .. "_enabled"] = enabled parms["client" .. client_num .. "_name"] = name:upper() parms["client" .. client_num .. "_passwd"] = passwd parms["client" .. client_num .. "_netip"] = netip parms["client" .. client_num .. "_contact"] = contact -- commit the data from this client client_num = client_num + 1 -- clear out the ADD values if val == "_add" then for _, var in ipairs(vars2) do parms["client_add_" .. var] = "" end end end end parms.client_num = client_num -- wireguard local vars = { "enabled", "name", "key", "clientip", "contact" } local wgclient_num = 0 for val = 0, parms.wgclient_num do if val == tonumber(parms.wgclient_num) then val = "_add" end for _ = 1,1 do for _, var in ipairs(vars) do local varname = "wgclient" .. val .. "_" .. var if var == "enabled" and not parms[varname] then parms[varname] = "0" elseif not parms[varname] then parms[varname] = "" elseif var == "contact" then parms[varname] = parms[varname]:gsub("^%s+", ""):gsub("%s+$", ""):sub(1,210):gsub('"',"""):gsub("'","'"):gsub("<","<"):gsub(">",">") else parms[varname] = parms[varname]:gsub("^%s+", ""):gsub("%s+$", "") end if val ~= "_add" and parms[varname] == "" and var == "enabled" then parms[varname] = "0" end _G[var] = parms[varname] end if val == "_add" and not ((enabled ~= "0" or name ~= "" or contact ~= "") and (parms.wgclient_add or parms.button_save)) then break end if val == "_add" and parms.button_save then err(val .. " this wireguard client must be added or cleared out before saving changes") break end if name == "" then err("A client name is required") end if val == "_add" and #cli_err > 0 and cli_err[#cli_err]:match("^" .. val .. " ") then break end if key == "" then local priv = capture("/usr/bin/wg genkey"):match("(%S+)") local pub = capture("echo " .. priv .. " | /usr/bin/wg pubkey"):match("(%S+)") key = priv .. pub end parms["wgclient" .. wgclient_num .. "_enabled"] = enabled parms["wgclient" .. wgclient_num .. "_name"] = name:upper() parms["wgclient" .. wgclient_num .. "_key"] = key parms["wgclient" .. wgclient_num .. "_clientip"] = clientip parms["wgclient" .. wgclient_num .. "_contact"] = contact -- commit the data from this client wgclient_num = wgclient_num + 1 -- clear out the ADD values if val == "_add" then for _, var in ipairs(vars) do parms["wgclient_add_" .. var] = "" end end end end parms.wgclient_num = wgclient_num -- SAVE the server network numbers and dns into the UCI netw[3] = parms.server_net1 netw[4] = parms.server_net2 dns = parms.dns if not tonumber(parms.server_net1) or tonumber(parms.server_net1) < 0 or tonumber(parms.server_net1) > 255 then err("The third octet of the network MUST be from 0 to 255") end if not tonumber(parms.server_net2) or tonumber(parms.server_net2) < 0 or tonumber(parms.server_net2) > 255 then err("The last octet of the network MUST be from 0 to 255") end if not tonumber(parms.server_net2) or tonumber(parms.server_net2) %4 ~= 0 then err("The last octet of the network MUST be a multiple of 4 (ie. 0,4,8,12,16,...)") end if not validate_fqdn(dns) then err("Not a valid DNS name") end if #cli_err == 0 then local net_base = "172.31." if cursor:get("aredn", "@supernode[0]", "enable") == "1" then net_base = "172.30." cursor:set("vtun", "@options[0]", "port", "5526") else cursor:delete("vtun", "@options[0]", "port") end local net = net_base .. parms.server_net1 .. "." .. parms.server_net2 cursor:set("vtun", "@network[0]", "start", net) cursor:set("vtun", "@network[0]", "dns", dns) end -- SAVE the clients local enabled_count = 0 for i = 0,client_num-1 do local clientx = "client" .. i local client_x = "client_" .. i local net = parms[clientx .. "_netip"] local vtun_node_name = (parms[clientx .. "_name"]:sub(1,23) .. "-" .. net:gsub("%.", "-")):upper() local base = ip_to_decimal(net) local clientip = decimal_to_ip(base + 1) local serverip = decimal_to_ip(base + 2) if not cursor:get("vtun", client_x) then cursor:set("vtun", client_x, 'client') end cursor:set("vtun", client_x, "netip", net) cursor:set("vtun", client_x, "enabled", parms[clientx .. "_enabled"]) cursor:set("vtun", client_x, "name", parms[clientx .. "_name"]) cursor:set("vtun", client_x, "contact", parms[clientx .. "_contact"]) cursor:set("vtun", client_x, "passwd", parms[clientx .. "_passwd"]) cursor:set("vtun", client_x, "clientip", clientip) cursor:set("vtun", client_x, "serverip", serverip) cursor:set("vtun", client_x, "node", vtun_node_name) if parms[clientx .. "_enabled"] == "1" then enabled_count = enabled_count + 1 end end -- wireguard for i = 0,wgclient_num-1 do local clientx = "wgclient" .. i local client_x = "client_" .. i if not cursor:get("wireguard", client_x) then cursor:set("wireguard", client_x, 'client') end cursor:set("wireguard", client_x, "enabled", parms[clientx .. "_enabled"]) cursor:set("wireguard", client_x, "name", parms[clientx .. "_name"]) cursor:set("wireguard", client_x, "contact", parms[clientx .. "_contact"]) cursor:set("wireguard", client_x, "key", parms[clientx .. "_key"]) cursor:set("wireguard", client_x, "clientip", parms[clientx .. "_clientip"]) end local maxclients = tonumber(cursor:get("aredn", "@tunnel[0]", "maxclients")) if not maxclients then maxclients = 10 end if enabled_count > maxclients then err("Number of clients enabled (" .. enabled_count .. ") exceeds maxclients value (" .. maxclients .. ")") end -- save configuration (commit) if parms.button_save and #cli_err == 0 then cursor:commit("vtun") cursor:commit("wireguard") os.execute("/usr/local/bin/node-setup -a mesh > /dev/null 2>&1") os.execute("/etc/init.d/olsrd restart > /dev/null 2>&1") os.execute("/etc/init.d/vtundsrv restart > /dev/null 2>&1") os.execute("/etc/init.d/network restart > /dev/null 2>&1") end local active_tun = get_active_tun() local active_wgtun = get_active_wgtun() -- generate the page http_header() html.header(node .. " setup", true) html.print("
") html.alert_banner() html.print("
") -- navigation bar html.navbar_admin("vpn") html.print("") -- control buttons html.print("") hide("") -- unsupported tunnels local notunnels = not nixio.fs.stat("/usr/sbin/vtund") if notunnels then html.print("") config = "notunnels" cli_err = {} -- low memory warning elseif isLowMemNode() then html.print("") end -- messages if #cli_err > 0 then html.print("") end if parms.button_save then if #cli_err > 0 then html.print("") for _,msg in ipairs(errors) do html.print(msg .. "
") end html.print("") else html.print("") end html.print("") end -- everything else if config == "mesh" then html.print("") end html.print("
") html.print("Help") html.print("   ") html.print(" ") html.print(" ") html.print(" ") html.print("
 
  Tunnels are no longer supported on this hardware  
  Recommend not to use tunneling due to low memory on this node  
ERROR:
") for _,msg in ipairs(cli_err) do html.print(msg .. "
") end html.print("
Configuration NOT saved!
Configuration saved and is now active.
 
") -- print vpn clients html.print("") html.print("
") html.print("") html.print("
Tunnel Server Network:
") html.print(netw[1] .. "." .. netw[2] .. "..") html.print("
Wireguard Server Network:
") html.print(netwg[1] .. "." .. netwg[2] .. "." .. netwg[3] .. "." .. netwg[4] .. "/" .. netwg[5]) html.print("

Tunnel Server DNS Name: ") html.print("
") html.print("") html.print("") html.print("") html.print("") html.print("") html.print("") -- loop local list = {} for i = 0,client_num-1 do list[#list+1] = i end if client_num < 100 then list[#list+1] = "_add" end local keys = { "enabled", "name", "passwd", "contact" } local cnum = 0 for _, val in ipairs(list) do for _, var in ipairs(keys) do _G[var] = parms["client" .. val .. "_" .. var] end html.print("") html.print("") html.print("") html.print("") html.print("") if val == "_add" then html.print("") else html.print("") end html.print("") -- display any errors while #cli_err > 0 and cli_err[1]:match("^" .. val .. " ") do html.print("") table.remove(cli_err) end html.print("") cnum = cnum + 1 end -- Wireguard html.print("") html.print("") html.print("") html.print("") local keys = { "enabled", "name", "contact", "key" } local cnum = 1 for val = 0, wgclient_num do if val == wgclient_num then val = "_add" end for _, var in ipairs(keys) do _G[var] = parms["wgclient" .. val .. "_" .. var] end html.print("") html.print("") html.print("") html.print("") local fullnet = netwg[1] .. "." .. netwg[2] .. "." .. netwg[3] .. "." .. (netwg[4] + 1 + cnum) .. "/" .. netwg[5] html.print("") html.print("") if val == "_add" then html.print("") else html.print("") end html.print("") -- display any errors while #cli_err > 0 and cli_err[1]:match("^" .. val .. " ") do html.print("") table.remove(cli_err) end html.print("") cnum = cnum + 1 end html.print("
 
Allow the following clients to connect to this server:

Enabled?ClientPwdNetActive Action
") html.print("") html.print("") -- handle rollover of netw local net if netw[4] + cnum * 4 > 252 then netw[3] = netw[3] + 1 netw[4] = 0 net = 0 cnum = 0 else net = cnum end local lastnet = netw[4] + net * 4 local fullnet = netw[1] .. "." .. netw[2] .. "." .. netw[3] .. "." .. lastnet html.print("") html.print(" ") if val ~= "_add" and is_tunnel_active(fullnet, active_tun) then html.print("") else html.print("") end html.print("") html.print("") html.print("
Contact Info/Comment (Optional):
" .. cli_err[1]:gsub("^%S+ ", "") .. "
Allow the following clients to connect to this Wireguard server:

Enabled?ClientKeyClientActive Action
") html.print("") html.print("") html.print("") html.print(" ") if val ~= "_add" and is_wgtunnel_active(key, active_wgtun) then html.print("") else html.print("") end html.print("") html.print("") html.print("
Contact Info/Comment (Optional):
" .. cli_err[1]:gsub("^%S+ ", "") .. "
") -- html.print("

Tunnel v" .. VPNVER .. "

") hide("") hide("") -- add hidden forms fields for _, h in ipairs(hidden) do html.print(h) end -- close the form html.print("
") html.footer() html.print("") http_footer()