#!/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 -- truncate node name down to 23 chars (max) to avoid vtun issues -- this becomes the vtun "username" node = node:sub(1, 23) local config = aredn.info.get_nvram("config"); local VPNVER = "1.0" -- 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 -- helpers start local hidden = {} function hide(inp) hidden[#hidden + 1] = inp end local conn_err = {} function err(msg) conn_err[#conn_err + 1] = msg end local errors = {} function err2(msg) errors[#errors + 1] = msg 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 -- helpers end local gci_vars = { "enabled", "host", "passwd", "netip", "contact" } function get_connection_info() local c = 0 cursor:foreach("vtun", "server", function(section) for _, var in ipairs(gci_vars) do local key = "conn" .. c .. "_" .. var parms[key] = section[var] if not parms[key] then parms[key] = "" end end c = c + 1 end ) parms.conn_num = c end if parms.button_reboot then local node = aredn.info.get_nvram("node") if node == "" then node = "Node" end local lanip, _, lanmask = aredn.hardware.get_interface_ip4(aredn.hardware.get_iface_name("lan")) local browser = os.getenv("REMOTE_ADDR") local browser6 = browser:match("::ffff:([%d%.]+)") if browser6 then browser = browser6 end local fromlan = false local subnet_change = false if lanip then fromlan = validate_same_subnet(browser, lanip, lanmask) if fromlan then lanmask = ip_to_decimal(lanmask) local cfgip = cursor:get("network", "lan", "ipaddr") local cfgmask = ip_to_decimal(cursor:get("network", "lan", "netmask")) if lanmask ~= cfgmask or nixio.bit.band(ip_to_decimal(lanip), lanmask) ~= nixio.bit.band(ip_to_decimal(cfgip), cfgmask) then subnet_change = true end end end http_header() if fromlan and subnet_change then html.header(node .. " rebooting", true); html.print("
") html.print("

" .. node .. " is rebooting


") html.print("

The LAN subnet has changed. You will need to acquire a new DHCP lease
") html.print("and reset any name service caches you may be using.


") html.print("

When the node reboots you get your new DHCP lease and reconnect with
") html.print("http://localnode.local.mesh:8080/
or
") html.print("http://" .. node .. ".local.mesh:8080/

") else html.header(node .. " rebooting", false) html.print("") html.print("
") html.print("

" .. node .. " is rebooting


") html.print("

Your browser should return to this node in 60 seconds.

") html.print("If something goes astray you can try to connect with

") html.print("http://localnode.local.mesh:8080/
") if node ~= "Node" then html.print("or
http://" .. node .. ".local.mesh:8080/

") end end html.print("
") http_footer() 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("vpnc") 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:commit("vtun") end -- handle connection deletes local do_delete = false for i = 0,9 do local varname = "conn" .. i .. "_del" if parms[varname] then do_delete = true cursor:delete("vtun", "server_" .. i) for x=i+1,9 do cursor:rename("vtun", "server_" .. x, "server_" .. (x - 1)) end end end if do_delete then cursor:commit("vtun") end -- if RESET or FIRST TIME, load servers into parms if parms.button_reset or not parms.reload then cursor:revert("vtun") get_connection_info() -- initialzie the "add" entries to clear them parms.conn_add_enabled = "0" parms.conn_add_host = "" parms.conn_add_passwd = "" parms.conn_add_netip = "" parms.conn_add_contact = "" end -- load connetions from FORM and validate local list = {} for i = 0,parms.conn_num-1 do list[#list + 1] = i end list[#list + 1] = "_add" local conn_num = 0 local vars = { "enabled", "host", "passwd", "netip", "contact" } for _, val in ipairs(list) do for _ = 1, 1 do for _, var in ipairs(vars) do local varname = "conn" .. 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" then if not ((enabled ~= "0" or host ~= "" or passwd ~= "" or netip ~= "" or contact ~= "") and (parms.conn_add or parms.button_save)) then break end elseif parms["conn" .. val .. "_del"] then break end if val == "_add" and parms.button_save then err(val .. " this connection must be added or cleared out before saving changes") break end if netip:match(":") then if not passwd:match("^.+=.+=.+=$") then err("The password is not a wireguard key") end elseif passwd:match("[^%w@]") then err("The password cannot contain non-alphanumeric characters (#" .. conn_num .. ")") end if host == "" then err("A connection server is required") end if passwd == "" then err("A connection password is required") end if netip == "" then err("A connection network IP is required") end if val == "_add" and #conn_err > 0 and conn_err[#conn_err]:match("^" .. val .. " ") then break end parms["conn" .. conn_num .. "_enabled"] = enabled parms["conn" .. conn_num .. "_host"] = host parms["conn" .. conn_num .. "_passwd"] = passwd parms["conn" .. conn_num .. "_netip"] = netip parms["conn" .. conn_num .. "_contact"] = contact conn_num = conn_num + 1 -- clear out the ADD values if val == "_add" then for _, var in ipairs(vars) do parms["conn_add_" .. var] = "" end end end end parms.conn_num = conn_num -- save the connections if cursor:get("aredn", "@supernode[0]", "enable") == "1" then cursor:set("vtun", "@options[0]", "port", "5526") else cursor:delete("vtun", "@options[0]", "port") end local enabled_count = 0 for i = 0,parms.conn_num-1 do local connx_ = "conn" .. i .. "_" local conn_x = "server_" .. i local net = parms[connx_ .. "netip"] local vtun_node_name = (node .. "-" .. 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", conn_x) then cursor:set("vtun", conn_x, "server") end cursor:set("vtun", conn_x, "clientip", clientip) cursor:set("vtun", conn_x, "serverip", serverip) cursor:set("vtun", conn_x, "node", vtun_node_name) cursor:set("vtun", conn_x, "enabled", parms[connx_ .. "enabled"]) cursor:set("vtun", conn_x, "host", parms[connx_ .. "host"]) cursor:set("vtun", conn_x, "passwd", parms[connx_ .. "passwd"]) cursor:set("vtun", conn_x, "netip", parms[connx_ .. "netip"]) cursor:set("vtun", conn_x, "contact", parms[connx_ .. "contact"]) if parms[connx_ .. "enabled"] == "1" then enabled_count = enabled_count + 1 end end -- save the connections the uci vtun file if parms.button_save and #conn_err == 0 then cursor:commit("vtun") 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() local active_wgtun = get_active_wgtun() -- generate page http_header() html.header(node .. " setup", false) html.print([[ ]]) html.print("
") html.alert_banner() html.print("
") -- nav bar html.navbar_admin("vpnc") -- control buttons html.print("") hide("") local notunnels = not nixio.fs.stat("/usr/sbin/vtund") if notunnels then html.print("") config = "notunnels" -- low memory warning elseif isLowMemNode() then html.print("") end -- messages if #conn_err > 0 then html.print("") end if parms.button_save then if #conn_err > 0 then html.print("") elseif #errors > 0 then 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(conn_err) do html.print(msg .. "
") end html.print("
Configuration NOT saved!
Configuration saved, however:
") for _,msg in ipairs(errors) do html.print(msg .. "
") end html.print("
Configuration saved and is now active.
 
") -- html.print("") html.print("") html.print("") html.print("") local list = {} for i = 0,parms.conn_num-1 do list[#list+1] = i end if parms.conn_num < 10 then list[#list+1] = "_add" end local keys = { "enabled", "host", "passwd", "netip", "contact" } local cnum = 0 for _, val in ipairs(list) do for _, var in ipairs(keys) do _G[var] = parms["conn" .. val .. "_" .. var] end if val == "_add" and #list > 1 then html.print("") end html.print("") html.print("") html.print("") html.print("") html.print("") html.print("") html.print("") html.print("") -- contact info for this tunnel html.print("") html.print("") html.print("") -- display any errors while #conn_err > 0 and conn_err[1]:match("^" .. val .. " ") do html.print("") conn_err:remove(i) end html.print("") cnum = cnum + 1 end html.print("
Connect this node to the following servers:

Enabled?ServerPwdNetworkActive Action
") html.print(" ") if val ~= "_add" then if is_tunnel_active(netip, active_tun) or is_wgtunnel_active(passwd, active_wgtun) then html.print("") else html.print("") end end html.print(" ") html.print("
Contact Info/Comment (Optional):
" .. err:gsub("^%S+ ", "") .. "
") -- html.print("

") html.print("

VPN v" .. VPNVER .. "

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