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..5d1d1d4e --- /dev/null +++ b/package/gluon-hoodselector/luasrc/usr/lib/lua/hoodselector/util.lua @@ -0,0 +1,191 @@ +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 + +-- 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 + +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 + +-- 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 + +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 + +-- 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][2],poly[i][1],poly[i+1][2],poly[i+1][1]) + 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][1] and geo.lat < area[2][1] and geo.lon >= area[1][2] + and geo.lon < area[2][2] ) 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 + +-- 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.restart_services() + local procTBL = { + "fastd", + "tunneldigger", + "network", + } + + 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..fffa9db6 --- /dev/null +++ b/package/gluon-hoodselector/luasrc/usr/sbin/hoodselector @@ -0,0 +1,182 @@ +#!/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 fallback to scan 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 + io.stdout:write('No position found\n') + end +else + io.stdout:write('No VPN connection found\n') +end + +--[[ + +if hoodutil.batmanHasGateway() then +441 io.stdout:write('Batman gateways found\n') +442 local gw_intf = {} +443 table.insert(gw_intf,hoodutil.get_batman_GW_interface()) +444 +445 if next(gw_intf) then +446 -- wifi interface +447 local bssidHood = hoodutil.get_radio_to_bssid(radios,gw_intf[1], jhood) +448 if bssidHood ~= nil then +449 --check if hood inside geo pos +450 local geo = hoodutil.getGeolocation() +451 if geo.lat ~= nil and geo.lon ~= nil then +452 io.stdout:write('Position found.\n') +453 local geoHood = hoodutil.getHoodByGeo(jhood, geo) +454 if geoHood ~= nil then +455 if string.format(hash.md5(table.tostring(bssidHood))) ~= + string.format(hash.md5(table.tostring(geoHood))) then +456 io.stdout:write('Geo hood and bssid hood are not equal, do wifi scan...\n') +457 local sortedWlanList = hoodutil.wlan_list_sorted(radios, mesh_prefix) +458 for i=#sortedWlanList,1,-1 do +459 if(string.lower(geoHood.bssid) ~= string.lower(sortedWlanList[i].bssid)) then +460 table.remove(sortedWlanList, i) +461 end +462 end +463 if next(sortedWlanList) then +464 io.stdout:write('Try to switch back in our real hood!\n') +465 io.stdout:write('After filtering we will test the following wireless networks:\n') +466 for _, network in pairs(sortedWlanList) do +467 print(network["quality"].."\t"..network["frequency"].."\t"..network["bssid"].."\t"..network["ssid"]) +468 end +469 io.stdout:write("Prepare configuration for testing wireless networks...\n") +470 local bssid = hoodutil.test_batman_mesh_networks(sortedWlanList, mesh_prefix) +471 hoodutil.wireless_restart() +472 io.stdout:write("Finished testing wireless networks, restored previous configuration\n") +473 if bssid ~= nil then +474 set_hoodconfig(geoHood, mesh_prefix, radios) +475 hoodutil.vpn_enable() +476 hoodutil.vpn_start() +477 io.stdout:write('Set Geo Hood by Gateway mode\n') +478 write_molwm(geoHood, radios) +479 exit(0) +480 else +481 io.stdout:write('No neighboring freifunk batman advanced mesh found.\n') +482 end +483 else +484 io.stdout:write('No networks left after filtering!\n') +485 end --end next(sortedWlanList) +486 else +487 io.stdout:write('Geo hood are equal to bssid hood no wifi scan necessary.\n') +488 end --end bssidHood ~= geoHood +489 else +490 io.stdout:write('No hood has been defined for current position.\n') +491 end --end geoHood ~= nil +492 else +493 io.stdout:write('No position found.\n') +494 end --end geo.lat ~= nil and geo.lon ~= nil +495 set_hoodconfig(bssidHood, mesh_prefix, radios) +496 io.stdout:write('Hood set by batmanHasGateway mode, GW source is wifi\n') +497 write_molwm(bssidHood,radios) +498 exit(0) +499 end --end bssidHood ~= nil +500 +501 -- mesh lan or wan interface +502 if hoodutil.mesh_lan_wan(gw_intf[1]) then +503 -- if mesh_lan/wan try to get hood by selected bssid of neightbour vpnRouters +504 local neighbourBssid = hoodutil.molw_get_bssid(gw_intf) +505 if neighbourBssid ~= nil then +506 bssidHood = hoodutil.gethoodByBssid(jhood, neighbourBssid) +507 if bssidHood ~= nil then +508 set_hoodconfig(bssidHood, mesh_prefix, radios) +509 io.stdout:write('Hood set by batmanHasGateway mode, GW source is mesh on lan/wan\n') +510 molwmtable["md5hash"] = "\"" .. string.format(hash.md5(table.tostring(bssidHood))) .. "\"" +511 molwmtable["hoodname"] = "\"" .. bssidHood["name"] .. "\"" +512 molwmtable["bssid"] = "\"" .. bssidHood["bssid"] .. "\"" +513 molwm_to_file() +514 exit(0) +515 end +516 end +517 end +518 end --end next(gw_intf) +519 local currendHood = hoodutil.getCurrentHood(jhood) +520 if currendHood ~= nil then +521 write_molwm(currendHood,radios) +522 end +523 end +--]] + +exit(0) -- Debug