diff --git a/package/gluon-hoodselector/Makefile b/package/gluon-hoodselector/Makefile new file mode 100644 index 00000000..9094ae33 --- /dev/null +++ b/package/gluon-hoodselector/Makefile @@ -0,0 +1,30 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=gluon-hoodselector + +GLUON_VERSION = 3 +PKG_VERSION:=2 + +PKG_BUILD_DIR := $(BUILD_DIR)/$(PKG_NAME) + +include ../gluon.mk + +define Package/gluon-hoodselector + SECTION:=gluon + CATEGORY:=Gluon + TITLE:=Automatically sorte routers into network hoods. + DEPENDS:=+gluon-site +gluon-mesh-batman-adv-15 @GLUON_MULTIDOMAIN + CONFLICTS:=+gluon-config-mode-domain-select +endef + +define Package/gluon-hoodselector/description + This is the hoodselector. The hoodselector is one of the main components for + splitting a layer 2 mesh network into seperated network segments (hoods). + The job of the hoodselector is to automatically detect in which hood + the router is located based on geo settings or by scanning its environment. + Based on these informations the hoodselector should select a hood from a + list of known hoods (hoodlist) and adjust vpn, wireless and mesh on lan + configuration based on the settings given for the selected hood. +endef + +$(eval $(call BuildPackageGluon,gluon-hoodselector)) diff --git a/package/gluon-hoodselector/check_site.lua b/package/gluon-hoodselector/check_site.lua new file mode 100644 index 00000000..4a832e43 --- /dev/null +++ b/package/gluon-hoodselector/check_site.lua @@ -0,0 +1,37 @@ +need_string_match(in_domain({'domain_seed'}), '^' .. ('%x'):rep(64) .. '$') + +function need_nil(path) + need(path, function(val) + if val == nil then + return true + end + return false + end, true, "default hood should not contain shapes") + return nil +end + +--Need to check if not default hood and does the default not contain shapes +if this_domain() ~= need_string(in_site({'default_domain'})) then + local no_shapes = true + for _,shape in ipairs(need_table(in_domain({'hoodselector', 'shapes'}))) do + no_shapes = false + if #shape >= 2 then + for _, pos in ipairs(shape) do + if pos.lat == nil or not ( pos.lat < 90.0 and pos.lat > -90.0 ) then + need(in_domain({'hoodselector', 'shapes'}), function(err) return false end, true, "lat muss match a rage +/-90.0") + end + if pos.lon == nil or not ( pos.lon < 180.0 and pos.lon > -180.0 ) then + need(in_domain({'hoodselector', 'shapes'}), function(err) return false end, true, "lon muss match a rage +/-180.0") + end + end + end + if #shape < 2 then + need(in_domain({'hoodselector', 'shapes'}), function(err) return false end, true, "needs to have at lease 2 coordiantes for rectangular shapes.") + end + end + if no_shapes then + need(in_domain({'hoodselector', 'shapes'}), function(err) return false end, true, "no shapes are defined in hoods") + end +else -- ente by default hood + need_nil(in_domain({'hoodselector', 'shapes'})) +end diff --git a/package/gluon-hoodselector/files/usr/lib/micron.d/hoodselector b/package/gluon-hoodselector/files/usr/lib/micron.d/hoodselector new file mode 100644 index 00000000..6e793151 --- /dev/null +++ b/package/gluon-hoodselector/files/usr/lib/micron.d/hoodselector @@ -0,0 +1 @@ +*/2 * * * * /usr/sbin/hoodselector diff --git a/package/gluon-hoodselector/luasrc/usr/lib/lua/hoodselector/util.lua b/package/gluon-hoodselector/luasrc/usr/lib/lua/hoodselector/util.lua new file mode 100644 index 00000000..6e45e13d --- /dev/null +++ b/package/gluon-hoodselector/luasrc/usr/lib/lua/hoodselector/util.lua @@ -0,0 +1,192 @@ +local fs = require 'nixio.fs' +local json = require 'jsonc' +local uci = require('simple-uci').cursor() +local site = require 'gluon.site' + +local M = {} + +function M.split(s, delimiter) + local result = {}; + for match in (s..delimiter):gmatch("(.-)"..delimiter) do + table.insert(result, match); + end + return result; +end + +local PID = M.split(io.open("/proc/self/stat", 'r'):read('*a'), " ")[1] + +function M.log(msg) + if msg then + io.stdout:write(msg.."\n") + os.execute("logger hoodselector["..PID.."]: "..msg) + end +end + +function M.get_domains() + local list = {} + for domain_path in fs.glob('/lib/gluon/domains/*.json') do + table.insert(list, { + domain_code = domain_path:match('([^/]+)%.json$'), + domain = assert(json.load(domain_path)), + }) + end + return list +end + +-- Return the default hood in the hood list. +-- This method can return the following data: +-- * default hood +-- * nil if no default hood has been defined +function M.getDefaultHood(jhood) + for _, h in pairs(jhood) do + if h.domain_code == site.default_domain() then + return h + end + end + return nil +end + +function M.fastd_installed() + if io.open("/usr/bin/fastd", 'r') ~= nil then + return true + end + return false +end + +function M.tunneldigger_installed() + if io.open("/usr/bin/tunneldigger", 'r') ~= nil then + return true + end + return false +end + +-- bool if direct VPN. The detection is realaise by searching the fastd network interface inside the originator table +function M.directVPN(vpnIfaceList) + for _,vpnIface in ipairs(vpnIfaceList) do + local file = io.open("/sys/kernel/debug/batman_adv/bat0/originators", 'r') + if file ~= nil then + for outgoingIF in file:lines() do + -- escape special chars "[]-" + if outgoingIF:match(string.gsub("%[ " .. vpnIface .. "%]","%-", "%%-")) then + return true + end + end + end + end + return false +end + +-- Get Geoposition. Return nil for no position +function M.getGeolocation() + return {["lat"] = tonumber(uci:get('gluon-node-info', uci:get_first('gluon-node-info', 'location'), 'latitude')), + ["lon"] = tonumber(uci:get('gluon-node-info', uci:get_first('gluon-node-info', 'location'), 'longitude')) } +end + +-- Source with pseudocode: https://de.wikipedia.org/wiki/Punkt-in-Polygon-Test_nach_Jordan +-- see also https://en.wikipedia.org/wiki/Point_in_polygon +-- parameters: points A = (x_A,y_A), B = (x_B,y_B), C = (x_C,y_C) +-- return value: −1 if the ray from A to the right bisects the edge [BC] (the lower vortex of [BC] +-- is not seen as part of [BC]); +-- 0 if A is on [BC]; +-- +1 else +function M.crossProdTest(x_A,y_A,x_B,y_B,x_C,y_C) + if y_A == y_B and y_B == y_C then + if (x_B <= x_A and x_A <= x_C) or (x_C <= x_A and x_A <= x_B) then + return 0 + end + return 1 + end + if not ((y_A == y_B) and (x_A == x_B)) then + if y_B > y_C then + -- swap B and C + local h = x_B + x_B = x_C + x_C = h + h = y_B + y_B = y_C + y_C = h + end + if (y_A <= y_B) or (y_A > y_C) then + return 1 + end + local Delta = (x_B-x_A) * (y_C-y_A) - (y_B-y_A) * (x_C-x_A) + if Delta > 0 then + return 1 + elseif Delta < 0 then + return -1 + end + end + return 0 +end + +-- Source with pseudocode: https://de.wikipedia.org/wiki/Punkt-in-Polygon-Test_nach_Jordan +-- see also: https://en.wikipedia.org/wiki/Point_in_polygon +-- let P be a 2D Polygon and Q a 2D Point +-- return value: +1 if Q within P; +-- −1 if Q outside of P; +-- 0 if Q on an edge of P +function M.pointInPolygon(poly, point) + local t = -1 + for i=1,#poly-1 do + t = t * M.crossProdTest(point.lon,point.lat,poly[i].lon,poly[i].lat,poly[i+1].lon,poly[i+1].lat) + if t == 0 then break end + end + return t +end + +-- Return hood from the hood file based on geo position or nil if no real hood could be determined +-- First check if an area has > 2 points and is hence a polygon. Else assume it is a rectangular +-- box defined by two points (south-west and north-east) +function M.getHoodByGeo(jhood,geo) + for _, hood in pairs(jhood) do + if hood.domain_code ~= site.default_domain() then + for _, area in pairs(hood.domain.hoodselector.shapes) do + if #area > 2 then + if (M.pointInPolygon(area,geo) == 1) then + return hood + end + else + if ( geo.lat >= area[1].lat and geo.lat < area[2].lat and geo.lon >= area[1].lon + and geo.lon < area[2].lon ) then + return hood + end + end + end + end + end + return nil +end + +function M.set_hoodconfig(geoHood) + if uci:get('gluon', 'core', 'domain') ~= geoHood.domain_code then + uci:set('gluon', 'core', 'domain', geoHood.domain_code) + uci:save('gluon') + uci:commit('gluon') -- necessary? + os.execute('gluon-reconfigure') + io.stdout:write("Set hood \""..geoHood.domain.domain_names[geoHood.domain_code].."\"\n") + return true + end + return false +end + +function M.restart_services() + local procTBL = { + "fastd", + "tunneldigger", + "network", + "gluon-respondd", + } + + for proc in ipairs(procTBL) do + if io.open("/etc/init.d/"..proc, 'r') ~= nil then + print(proc.." restarting ...") + os.execute("/etc/init.d/"..proc.." restart") + end + end + if io.open("/etc/config/wireless", 'r') then + print("wifi restarting ...") + os.execute("wifi") + end +end + +return M diff --git a/package/gluon-hoodselector/luasrc/usr/sbin/hoodselector b/package/gluon-hoodselector/luasrc/usr/sbin/hoodselector new file mode 100755 index 00000000..9ee722fd --- /dev/null +++ b/package/gluon-hoodselector/luasrc/usr/sbin/hoodselector @@ -0,0 +1,95 @@ +#!/usr/bin/lua + +-- PID file to ensure the hoodselector isn't running parallel +local pidPath="/var/run/hoodselector.pid" + +local uci = require('simple-uci').cursor() +local hoodutil = require("hoodselector.util") + +if io.open(pidPath, "r") ~=nil then + hoodutil.log("The hoodselector is still running.") + os.exit(1) +else + if io.open(pidPath, "w") ==nil then + hoodutil.log("Can`t create pid file on "..pidPath) + os.exit(1) + end +end + +-- Program terminating function including removing of PID file +local function exit(exc) + if io.open(pidPath, "r") ~=nil then + os.remove(pidPath) + end + os.exit(tonumber(exc)) +end + +-- initialization done +-- +local function get_mesh_vpn_interface() + local ret = {} + if hoodutil.fastd_installed() then + local vpnifac = uci:get('fastd', 'mesh_vpn_backbone', 'net') + if vpnifac ~= nil then + vpnifac = vpnifac:gsub("%_",'-') + table.insert(ret,vpnifac) + else + hoodutil.log("fastd uci config broken! abort...") + exit(1) + end + end + if hoodutil.tunneldigger_installed() then + local vpnifac = uci:get('tunneldigger', 'mesh_vpn', 'interface') + if vpnifac ~= nil then + table.insert(ret,vpnifac) + else + hoodutil.log("tunneldigger uci config broken! abort...") + exit(1) + end + end + return ret +end + +-- INITIALIZE AND PREPARE DATA -- +-- read hoodfile... +local jhood = hoodutil.get_domains() + +-- get defaul hood +local defaultHood = hoodutil.getDefaultHood(jhood) + +-- VPN MODE +-- If we have a VPN connection then we will try to get the routers location and +-- select the hood coresponding to our location. +-- If no hood for the location has been defined, we will select +-- the default hood. +-- If we can not get our routers location, we will continure to next mode. +if hoodutil.directVPN(get_mesh_vpn_interface()) then + io.stdout:write('VPN connection found.\n') + local geo = hoodutil.getGeolocation() + if geo.lat ~= nil and geo.lon ~= nil then + io.stdout:write('Position found.\n') + local geoHood = hoodutil.getHoodByGeo(jhood, geo) + if geoHood ~= nil then + if hoodutil.set_hoodconfig(geoHood) then + hoodutil.restart_services() -- TMP solution + io.stdout:write('Hood set by VPN mode.\n') + end + exit(0) + end + io.stdout:write('No hood has been defined for current position.\n') + if hoodutil.set_hoodconfig(defaultHood) then + hoodutil.restart_services() -- TMP solution + io.stdout:write('Hood set by VPN mode.\n') + end + exit(0) + else + -- The hoodselector should continure with next states because thier can be other + -- VPN routers in the local mesh network which provides a possition and therfore + -- have set a geo base hood. + io.stdout:write('No position found\n') + end +else + io.stdout:write('No VPN connection found\n') +end + +exit(0)