ext-respondd.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. #!/usr/bin/env python3
  2. # Code-Base: https://github.com/ffggrz/ffnord-alfred-announce
  3. # + https://github.com/freifunk-mwu/ffnord-alfred-announce
  4. # + https://github.com/FreifunkBremen/respondd
  5. import sys
  6. import socket
  7. import select
  8. import struct
  9. import subprocess
  10. import argparse
  11. import re
  12. import time
  13. # Force encoding to UTF-8
  14. import locale # Ensures that subsequent open()s
  15. locale.getpreferredencoding = lambda _=None: 'UTF-8' # are UTF-8 encoded.
  16. import json
  17. import zlib
  18. import netifaces as netif
  19. def toUTF8(line):
  20. return line.decode("utf-8")
  21. def call(cmdnargs):
  22. output = subprocess.check_output(cmdnargs)
  23. lines = output.splitlines()
  24. lines = [toUTF8(line) for line in lines]
  25. return lines
  26. def merge(a, b):
  27. if isinstance(a, dict) and isinstance(b, dict):
  28. d = dict(a)
  29. d.update({k: merge(a.get(k, None), b[k]) for k in b})
  30. return d
  31. if isinstance(a, list) and isinstance(b, list):
  32. return [merge(x, y) for x, y in itertools.izip_longest(a, b)]
  33. return a if b is None else b
  34. class rateLimit: # rate limit like iptables limit (per minutes)
  35. tLast = None
  36. def __init__(self, _rate_limit, _rate_burst):
  37. self.rate_limit = _rate_limit
  38. self.rate_burst = _rate_burst
  39. self.bucket = _rate_burst
  40. def limit(self):
  41. tNow = time.time()
  42. if self.tLast is None:
  43. self.tLast = tNow
  44. return True
  45. tDiff = tNow - self.tLast
  46. self.tLast = tNow
  47. self.bucket+= (tDiff / (60 / self.rate_limit))
  48. if self.bucket > self.rate_burst:
  49. self.bucket = self.rate_burst
  50. if self.bucket >= 1:
  51. self.bucket-= 1
  52. return True
  53. else:
  54. return False
  55. def getGateway():
  56. #/sys/kernel/debug/batman_adv/bat0/gateways
  57. output = subprocess.check_output(["batctl", "-m", config['batman'], "gwl", "-n"])
  58. output_utf8 = output.decode("utf-8")
  59. lines = output_utf8.splitlines()
  60. j = None
  61. for line in lines:
  62. gw_line = re.match(r"^(\*|=>) +([0-9a-f:]+) \([\d ]+\) ([0-9a-f:]+)", line)
  63. if gw_line:
  64. j = {}
  65. j["gateway"] = gw_line.group(2)
  66. j["gateway_nexthop"] = gw_line.group(3)
  67. return j
  68. def getClients():
  69. #/sys/kernel/debug/batman_adv/bat0/transtable_local
  70. output = subprocess.check_output(["batctl", "-m", config['batman'], "tl", "-n"])
  71. output_utf8 = output.decode("utf-8")
  72. lines = output_utf8.splitlines()
  73. batadv_mac = getDevice_MAC(config['batman'])
  74. j = {"total": 0, "wifi": 0}
  75. for line in lines:
  76. # batman-adv -> translation-table.c -> batadv_tt_local_seq_print_text
  77. # R = BATADV_TT_CLIENT_ROAM
  78. # P = BATADV_TT_CLIENT_NOPURGE
  79. # N = BATADV_TT_CLIENT_NEW
  80. # X = BATADV_TT_CLIENT_PENDING
  81. # W = BATADV_TT_CLIENT_WIFI
  82. # I = BATADV_TT_CLIENT_ISOLA
  83. # . = unset
  84. # * c0:11:73:b2:8f:dd -1 [.P..W.] 1.710 (0xe680a836)
  85. ml = re.match(r"^\s\*\s([0-9a-f:]+)\s+-\d\s\[([RPNXWI\.]+)\]", line, re.I)
  86. if ml:
  87. if not batadv_mac == ml.group(1): # Filter bat0
  88. if not ml.group(1).startswith('33:33:') and not ml.group(1).startswith('01:00:5e:'): # Filter Multicast
  89. j["total"] += 1
  90. if ml.group(2)[4] == 'W':
  91. j["wifi"] += 1
  92. return j
  93. def getDevice_Addresses(dev):
  94. l = []
  95. try:
  96. for ip6 in netif.ifaddresses(dev)[netif.AF_INET6]:
  97. raw6 = ip6['addr'].split('%')
  98. l.append(raw6[0])
  99. for ip in netif.ifaddresses(dev)[netif.AF_INET]:
  100. raw = ip['addr'].split('%')
  101. l.append(raw[0])
  102. except:
  103. pass
  104. return l
  105. def getDevice_MAC(dev):
  106. try:
  107. interface = netif.ifaddresses(dev)
  108. mac = interface[netif.AF_LINK]
  109. return mac[0]['addr']
  110. except:
  111. return None
  112. def getMesh_Interfaces():
  113. j = {}
  114. output = subprocess.check_output(["batctl", "-m", config['batman'], "if"])
  115. output_utf8 = output.decode("utf-8")
  116. lines = output_utf8.splitlines()
  117. for line in lines:
  118. dev_re = re.match(r"^([^:]*)", line)
  119. dev = dev_re.group(1)
  120. j[dev] = getDevice_MAC(dev)
  121. return j
  122. def getBat0_Interfaces():
  123. j = {}
  124. output = subprocess.check_output(["batctl", "-m", config['batman'], "if"])
  125. output_utf8 = output.decode("utf-8")
  126. lines = output_utf8.splitlines()
  127. for line in lines:
  128. dev_line = re.match(r"^([^:]*)", line)
  129. nif = dev_line.group(0)
  130. if_group = ""
  131. if "fastd" in config and nif == config["fastd"]: # keep for compatibility
  132. if_group = "tunnel"
  133. elif nif.find("l2tp") != -1:
  134. if_group = "l2tp"
  135. elif "mesh-vpn" in config and nif in config["mesh-vpn"]:
  136. if_group = "tunnel"
  137. elif "mesh-wlan" in config and nif in config["mesh-wlan"]:
  138. if_group = "wireless"
  139. else:
  140. if_group = "other"
  141. if not if_group in j:
  142. j[if_group] = []
  143. j[if_group].append(getDevice_MAC(nif))
  144. if "l2tp" in j:
  145. if "tunnel" in j:
  146. j["tunnel"] = j["tunnel"] + j["l2tp"]
  147. else:
  148. j["tunnel"] = j["l2tp"]
  149. return j
  150. def getTraffic():
  151. return (lambda fields: dict(
  152. (key, dict(
  153. (type_, int(value_))
  154. for key_, type_, value_ in fields
  155. if key_ == key))
  156. for key in ['rx', 'tx', 'forward', 'mgmt_rx', 'mgmt_tx']
  157. ))(list(
  158. (
  159. key.replace('_bytes', '').replace('_dropped', ''),
  160. 'bytes' if key.endswith('_bytes') else 'dropped' if key.endswith('_dropped') else 'packets',
  161. value
  162. )
  163. for key, value in map(lambda s: list(map(str.strip, s.split(': ', 1))), call(['ethtool', '-S', config['batman']])[1:])
  164. ))
  165. def getMemory():
  166. return dict(
  167. (key.replace('Mem', '').lower(), int(value.split(' ')[0]))
  168. for key, value in map(lambda s: map(str.strip, s.split(': ', 1)), open('/proc/meminfo').readlines())
  169. if key in ('MemTotal', 'MemFree', 'Buffers', 'Cached')
  170. )
  171. def getFastd():
  172. fastd_data = b""
  173. try:
  174. sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
  175. sock.connect(config["fastd_socket"])
  176. except socket.error as err:
  177. print("socket error: ", sys.stderr, err)
  178. return None
  179. while True:
  180. data = sock.recv(1024)
  181. if not data:
  182. break
  183. fastd_data += data
  184. sock.close()
  185. return json.loads(fastd_data.decode("utf-8"))
  186. def getMeshVPNPeers():
  187. j = {}
  188. if "fastd_socket" in config:
  189. fastd = getFastd()
  190. for peer in fastd["peers"].values():
  191. if peer["connection"]:
  192. j[peer["name"]] = {
  193. "established": peer["connection"]["established"]
  194. }
  195. else:
  196. j[peer["name"]] = None
  197. return j
  198. else:
  199. return None
  200. def getNode_ID():
  201. if 'node_id' in aliases["nodeinfo"]:
  202. return aliases["nodeinfo"]["node_id"]
  203. else:
  204. return getDevice_MAC(config["batman"]).replace(':', '')
  205. def getStationDump(dev_list):
  206. j = {}
  207. for dev in dev_list:
  208. try:
  209. # iw dev ibss3 station dump
  210. output = subprocess.check_output(["iw", "dev", dev, "station", "dump"])
  211. output_utf8 = output.decode("utf-8")
  212. lines = output_utf8.splitlines()
  213. mac = ""
  214. for line in lines:
  215. # Station 32:b8:c3:86:3e:e8 (on ibss3)
  216. ml = re.match(r"^Station ([0-9a-f:]+) \(on ([\w\d]+)\)", line, re.I)
  217. if ml:
  218. mac = ml.group(1)
  219. j[mac] = {}
  220. else:
  221. ml = re.match(r"^[\t ]+([^:]+):[\t ]+([^ ]+)", line, re.I)
  222. if ml:
  223. j[mac][ml.group(1)] = ml.group(2)
  224. except:
  225. pass
  226. return j
  227. def getNeighbours():
  228. # https://github.com/freifunk-gluon/packages/blob/master/net/respondd/src/respondd.c
  229. j = {"batadv": {}}
  230. stationDump = None
  231. if 'mesh-wlan' in config:
  232. j["wifi"] = {}
  233. stationDump = getStationDump(config["mesh-wlan"])
  234. mesh_ifs = getMesh_Interfaces()
  235. output = subprocess.check_output(["batctl", "-m", config['batman'], "o", "-n"])
  236. output_utf8 = output.decode("utf-8")
  237. lines = output_utf8.splitlines()
  238. for line in lines:
  239. # * e2:ad:db:b7:66:63 2.712s (175) be:b7:25:4f:8f:96 [mesh-vpn-l2tp-1]
  240. ml = re.match(r"^[ \*\t]*([0-9a-f:]+)[ ]*([\d\.]*)s[ ]*\(([ ]*\d*)\)[ ]*([0-9a-f:]+)[ ]*\[[ ]*(.*)\]", line, re.I)
  241. if ml:
  242. dev = ml.group(5)
  243. mac_origin = ml.group(1)
  244. mac_nhop = ml.group(4)
  245. tq = ml.group(3)
  246. lastseen = ml.group(2)
  247. if mac_origin == mac_nhop:
  248. if 'mesh-wlan' in config and dev in config["mesh-wlan"] and not stationDump is None:
  249. if not mesh_ifs[dev] in j["wifi"]:
  250. j["wifi"][mesh_ifs[dev]] = {}
  251. j["wifi"][mesh_ifs[dev]]["neighbours"] = {}
  252. if mac_origin in stationDump:
  253. j["wifi"][mesh_ifs[dev]]["neighbours"][mac_origin] = {
  254. "signal": stationDump[mac_origin]["signal"],
  255. "noise": 0, # TODO: fehlt noch
  256. "inactive": stationDump[mac_origin]["inactive time"]
  257. }
  258. if dev in mesh_ifs:
  259. if not mesh_ifs[dev] in j["batadv"]:
  260. j["batadv"][mesh_ifs[dev]] = {}
  261. j["batadv"][mesh_ifs[dev]]["neighbours"] = {}
  262. j["batadv"][mesh_ifs[dev]]["neighbours"][mac_origin] = {
  263. "tq": int(tq),
  264. "lastseen": float(lastseen)
  265. }
  266. return j
  267. def getCPUInfo():
  268. j = {}
  269. with open("/proc/cpuinfo", 'r') as fh:
  270. for line in fh:
  271. ml = re.match(r"^(.+?)[\t ]+:[\t ]+(.*)$", line, re.I)
  272. if ml:
  273. j[ml.group(1)] = ml.group(2)
  274. return j
  275. # ======================== Output =========================
  276. # =========================================================
  277. def createNodeinfo():
  278. j = {
  279. "node_id": getNode_ID(),
  280. "hostname": socket.gethostname(),
  281. "network": {
  282. "addresses": getDevice_Addresses(config['bridge']),
  283. "mesh": {
  284. "bat0": {
  285. "interfaces": getBat0_Interfaces()
  286. }
  287. },
  288. "mac": getDevice_MAC(config["batman"])
  289. },
  290. "software": {
  291. "firmware": {
  292. "base": call(['lsb_release', '-is'])[0],
  293. "release": call(['lsb_release', '-ds'])[0]
  294. },
  295. "batman-adv": {
  296. "version": open('/sys/module/batman_adv/version').read().strip(),
  297. # "compat": # /lib/gluon/mesh-batman-adv-core/compat
  298. },
  299. "status-page": {
  300. "api": 0
  301. },
  302. "autoupdater": {
  303. "enabled": False
  304. }
  305. },
  306. "hardware": {
  307. "model": getCPUInfo()["model name"],
  308. "nproc": int(call(['nproc'])[0])
  309. },
  310. # "vpn": True,
  311. "owner": {},
  312. "system": {},
  313. "location": {}
  314. }
  315. if 'mesh-vpn' in config and len(config["mesh-vpn"]) > 0:
  316. try:
  317. j["software"]["fastd"] = {
  318. "version": call(['fastd', '-v'])[0].split(' ')[1],
  319. "enabled": True
  320. }
  321. except:
  322. pass
  323. return merge(j, aliases["nodeinfo"])
  324. def createStatistics():
  325. j = {
  326. "node_id": getNode_ID(),
  327. "clients": getClients(),
  328. "traffic": getTraffic(),
  329. "idletime": float(open('/proc/uptime').read().split(' ')[1]),
  330. "loadavg": float(open('/proc/loadavg').read().split(' ')[0]),
  331. "memory": getMemory(),
  332. "processes": dict(zip(('running', 'total'), map(int, open('/proc/loadavg').read().split(' ')[3].split('/')))),
  333. "uptime": float(open('/proc/uptime').read().split(' ')[0]),
  334. "mesh_vpn" : { # HopGlass-Server: node.flags.uplink = parsePeerGroup(_.get(n, 'statistics.mesh_vpn'))
  335. "groups": {
  336. "backbone": {
  337. "peers": getMeshVPNPeers()
  338. }
  339. }
  340. }
  341. }
  342. gateway = getGateway()
  343. if gateway != None:
  344. j = merge(j, gateway)
  345. return j
  346. def createNeighbours():
  347. #/sys/kernel/debug/batman_adv/bat0/originators
  348. j = {
  349. "node_id": getNode_ID()
  350. }
  351. j = merge(j, getNeighbours())
  352. return j
  353. def sendResponse(request, compress):
  354. json_data = {}
  355. if not RateLimit is None and not RateLimit.limit():
  356. print("rate limit reached!")
  357. return
  358. #https://github.com/freifunk-gluon/packages/blob/master/net/respondd/src/respondd.c
  359. if request == 'statistics':
  360. json_data[request] = createStatistics()
  361. elif request == 'nodeinfo':
  362. json_data[request] = createNodeinfo()
  363. elif request == 'neighbours':
  364. json_data[request] = createNeighbours()
  365. else:
  366. print("unknown command: " + request)
  367. return
  368. json_str = bytes(json.dumps(json_data, separators=(',', ':')), 'UTF-8')
  369. if compress:
  370. encoder = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -15) # The data may be decompressed using zlib and many zlib bindings using -15 as the window size parameter.
  371. gzip_data = encoder.compress(json_str)
  372. gzip_data = gzip_data + encoder.flush()
  373. sock.sendto(gzip_data, sender)
  374. else:
  375. sock.sendto(json_str, sender)
  376. if options["verbose"]:
  377. print(json.dumps(json_data, sort_keys=True, indent=4))
  378. # ===================== Mainfunction ======================
  379. # =========================================================
  380. parser = argparse.ArgumentParser()
  381. parser.add_argument('-d', '--debug', action='store_true', help='Debug Output', required=False)
  382. parser.add_argument('-v', '--verbose', action='store_true', help='Verbose Output', required=False)
  383. args = parser.parse_args()
  384. options = vars(args)
  385. config = {}
  386. try:
  387. with open("config.json", 'r') as cfg_handle:
  388. config = json.load(cfg_handle)
  389. except IOError:
  390. raise
  391. aliases = {}
  392. try:
  393. with open("alias.json", 'r') as cfg_handle:
  394. aliases = json.load(cfg_handle)
  395. except IOError:
  396. raise
  397. if options["debug"]:
  398. print(json.dumps(createNodeinfo(), sort_keys=True, indent=4))
  399. print(json.dumps(createStatistics(), sort_keys=True, indent=4))
  400. print(json.dumps(createNeighbours(), sort_keys=True, indent=4))
  401. #print(json.dumps(getFastd(config["fastd_socket"]), sort_keys=True, indent=4))
  402. #print(json.dumps(getMesh_VPN(), sort_keys=True, indent=4))
  403. sys.exit(1)
  404. if 'addr' in config:
  405. addr = config['addr']
  406. else:
  407. addr = 'ff02::2:1001'
  408. if 'addr' in config:
  409. port = config['port']
  410. else:
  411. port = 1001
  412. if_idx = socket.if_nametoindex(config["bridge"])
  413. group = socket.inet_pton(socket.AF_INET6, addr) + struct.pack("I", if_idx)
  414. sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
  415. sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, group)
  416. sock.bind(('::', port))
  417. if 'rate_limit' in config:
  418. if not 'rate_limit_burst' in config:
  419. config['rate_limit_burst'] = 10
  420. RateLimit = rateLimit(config['rate_limit'], config['rate_limit_burst'])
  421. else:
  422. RateLimit = None
  423. # =========================================================
  424. while True:
  425. if select.select([sock], [], [], 1)[0]:
  426. msg, sender = sock.recvfrom(2048)
  427. if options["verbose"]:
  428. print(msg)
  429. msg_spl = str(msg, 'UTF-8').split(" ")
  430. if msg_spl[0] == 'GET': # multi_request
  431. for req in msg_spl[1:]:
  432. sendResponse(req, True)
  433. else: # single_request
  434. sendResponse(msg_spl[0], False)