#!/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") 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 is_supernode = cursor:get("aredn", "@supernode[0]", "enable") == "1" -- post_data local parms = {} if os.getenv("REQUEST_METHOD") == "POST" then require('luci.http') 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_alive_time = 300 -- 5 minutes local active_wgtun = {} -- 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(client_pub) local v = active_wgtun[client_pub] 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 is_supernode 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 return { netw[1], netw[2], c, netw[4] } end function get_server_dns() local dns = cursor:get("vtun", "@network[0]", "dns") return dns and dns 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 aredn.html.reboot() end if 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("

") html.print("The configuration has been changed.
This page will not be available until the node is rebooted.
") html.print("
") html.print("") html.print("
") 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 dns = get_server_dns() -- 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 -- 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 local is_new_supernode = false if client_num == 0 and is_supernode then is_new_supernode = true end -- 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 -- Generate a new key if we chance the client name local cname = cursor:get("wireguard", "client_" .. wgclient_num, "name") or "" local ckey = cursor:get("wireguard", "client_" .. wgclient_num, "key") or "" if key == ckey and name ~= cname and name ~= "" then key = "" end if key == "" then local privS = capture("/usr/bin/wg genkey"):match("(%S+)") local pubS = capture("echo " .. privS .. " | /usr/bin/wg pubkey"):match("(%S+)") local privC = capture("/usr/bin/wg genkey"):match("(%S+)") local pubC = capture("echo " .. privC .. " | /usr/bin/wg pubkey"):match("(%S+)") key = privS .. pubS .. privC .. pubC 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 .. "_port"] = port 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 if parms.server_wgnet1 then netw[3] = parms.server_wgnet1 - 1 if netw[3] < 0 then netw[3] = 255 end else netw[3] = parms.server_net1 end if not tonumber(netw[3]) or tonumber(netw[3]) < 0 or tonumber(netw[3]) > 255 then err("The third octet of the network MUST be from 0 to 255") end if parms.server_wgnet2 then netw[4] = parms.server_wgnet2 if not tonumber(netw[4]) or tonumber(netw[4]) % 4 ~= 0 then err("The last octet of the network MUST be a multiple of 2 (ie. 2,4,6,8,10,...)") end else netw[4] = parms.server_net2 if not tonumber(netw[4]) or tonumber(netw[4]) % 4 ~= 0 then err("The last octet of the network MUST be a multiple of 4 (ie. 0,4,8,12,16,...)") end end if not tonumber(netw[4]) or tonumber(netw[4]) < 0 or tonumber(netw[4]) > 255 then err("The last octet of the network MUST be from 0 to 255") end dns = parms.dns if not validate_fqdn(dns) then err("Not a valid DNS name") end if #cli_err == 0 then local net_base = "172.31." if is_supernode then net_base = "172.30." cursor:set("vtun", "@options[0]", "port", "5526") else cursor:delete("vtun", "@options[0]", "port") end local net = net_base .. netw[3] .. "." .. netw[4] cursor:set("vtun", "@network[0]", "start", net) cursor:set("vtun", "@network[0]", "dns", dns) end local netwg = get_wireguard_network_address(netw) -- 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 -- 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 > /dev/null 2>&1") os.execute("/usr/local/bin/restart-services.sh network tunnels olsrd > /dev/null 2>&1") end local active_tun = get_active_tun() 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("") 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 html.print("") 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("") if is_new_supernode then html.print("
") html.print("
Wireguard Server Network:
") html.print(netwg[1] .. "." .. netwg[2] .. "..") else 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]) end html.print("

Tunnel Server DNS Name: ") html.print("
") html.print("") html.print("") if not is_new_supernode then 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 end -- Wireguard html.print("") html.print("") html.print("") html.print("") local keys = { "enabled", "name", "contact", "key" } local cnum = 0 local wg_port = tonumber(cursor:get("vtun", "@options[0]", "port") or 5525) if is_supernode then wg_port = wg_port + 1000 end 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 netwg4 = tonumber(netwg[4]) + 2 * cnum if netwg4 >= 254 then netwg4 = netwg4 - 252 end local fullnet = netwg[1] .. "." .. netwg[2] .. "." .. netwg[3] .. "." .. netwg4 .. ":" .. (wg_port + cnum) 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("") local _, server_pub, client_priv, client_pub = key:match("^(.+=)(.+=)(.+=)(.+=)$") local client_key = val == "_add" and "" or (server_pub .. client_priv .. client_pub) html.print("") html.print("") html.print(" ") if val ~= "_add" and is_wgtunnel_active(client_pub) then html.print("") else html.print("") end html.print("") html.print("") html.print("
Contact Info/Comment (Optional):
" .. cli_err[1]:gsub("^%S+ ", "") .. "
") -- html.print("

") 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()