boxtools on raspberry pi

- rename to_system to apu_system / cm3_system /cm4_system
- fixes in router
- firewall may be switched off
- firewall is a parameter of router, also used when no routing is configured.
This commit is contained in:
l_samenv
2024-05-13 17:55:54 +02:00
parent 793606db2b
commit 1318666db0
7 changed files with 148 additions and 90 deletions

View File

@ -7,7 +7,6 @@ import threading
import re import re
import queue import queue
from utils import MainIf from utils import MainIf
from subprocess import Popen, PIPE
from configparser import ConfigParser from configparser import ConfigParser
# display tty device # display tty device

View File

@ -18,9 +18,8 @@ import types
import socket import socket
from subprocess import Popen, PIPE from subprocess import Popen, PIPE
from ipaddress import IPv4Interface from ipaddress import IPv4Interface
from configparser import ConfigParser
from os.path import join, getmtime, exists, basename from os.path import join, getmtime, exists, basename
from utils import BoxInfo from utils import BoxInfo, check_service, unix_cmd, change_firewall
if os.geteuid() != 0: if os.geteuid() != 0:
exit("You need to have root privileges to run this script.\nPlease try again, this time using 'sudo'. Exiting.") exit("You need to have root privileges to run this script.\nPlease try again, this time using 'sudo'. Exiting.")
@ -66,7 +65,7 @@ max-lease-time 7200;
authoritative; authoritative;
""" """
ROUTER_TEMPLATE = f"""[Unit] ROUTER_SERVICE = f"""[Unit]
Description = Routing to locally connected hardware Description = Routing to locally connected hardware
After = network.target After = network.target
@ -77,7 +76,7 @@ ExecStart = /usr/bin/python3 {TOOLS}/router.py
WantedBy = multi-user.target WantedBy = multi-user.target
""" """
FRAPPY_TEMPLATE = """[Unit] FRAPPY_SERVICE = """[Unit]
Description = Running frappy server Description = Running frappy server
After = network.target After = network.target
@ -89,7 +88,7 @@ ExecStart = /usr/bin/python3 /home/l_samenv/frappy/bin/frappy-server %s
WantedBy = multi-user.target WantedBy = multi-user.target
""" """
DISPLAY_TEMPLATE = f"""[Unit] DISPLAY_SERVICE = f"""[Unit]
Description = status display Description = status display
After = network.target After = network.target
@ -111,10 +110,15 @@ pip_requirements = {
box = BoxInfo() box = BoxInfo()
dhcp_server_cfg = [] dhcp_server_cfg = []
TO_SYSTEM = f'{TOOLS}/to_system' TO_SYSTEM = f'{TOOLS}/{box.typ}_system'
os.chdir(TO_SYSTEM) os.chdir(TO_SYSTEM)
def do_cmd(command):
unix_cmd(command, doit)
show.dirty = True
def frappy(cfg=None, port=None, requirements='', **kwds): def frappy(cfg=None, port=None, requirements='', **kwds):
if not cfg: if not cfg:
return None return None
@ -124,10 +128,26 @@ def frappy(cfg=None, port=None, requirements='', **kwds):
cfg = '-p %s %s' % (port, cfg) cfg = '-p %s %s' % (port, cfg)
with open('/home/l_samenv/frappy/requirements.txt') as f: with open('/home/l_samenv/frappy/requirements.txt') as f:
req['frappy main'] = f.read() req['frappy main'] = f.read()
return FRAPPY_TEMPLATE % cfg return FRAPPY_SERVICE % cfg
def router(**opts): def router(firewall=False, **opts):
if firewall:
active, enabled = check_service('nftables')
ports = {22}
if opts:
for port in firewall.split(','):
ports.add(int(port))
for key in opts:
try:
ports.add(int(key))
except ValueError:
pass
ports.update((1110, 1111, 1112))
if change_firewall(True, ports, doit):
show.dirty = True
elif change_firewall(False, set(), doit):
show.dirty = True
if not opts: if not opts:
return None return None
try: try:
@ -136,7 +156,7 @@ def router(**opts):
pip_requirements['root']['tools'] = f.read() pip_requirements['root']['tools'] = f.read()
except FileNotFoundError: except FileNotFoundError:
pass pass
return ROUTER_TEMPLATE return ROUTER_SERVICE
def display_update(cfg): def display_update(cfg):
@ -154,7 +174,7 @@ def display_update(cfg):
def display(**opts): def display(**opts):
if not opts: if not opts:
return None return None
return DISPLAY_TEMPLATE return DISPLAY_SERVICE
def pip(): def pip():
@ -176,23 +196,13 @@ def pip():
os.remove(tmpname) os.remove(tmpname)
else: else:
print(pipcmd) print(pipcmd)
unix_cmd(pipcmd, stdout=None) unix_cmd(pipcmd, doit, stdout=None)
show.dirty = True show.dirty = True
SERVICES = dict(router=router, frappy=frappy, display=display) SERVICES = dict(router=router, frappy=frappy, display=display)
def unix_cmd(command, always=False, stdout=PIPE):
if doit or always:
if not always:
print('$ %s' % command)
result = Popen(command.split(), stdout=stdout).communicate()[0]
return (result or b'').decode()
else:
print('> %s' % command)
def write_when_new(filename, content, ignore_reduction=False): def write_when_new(filename, content, ignore_reduction=False):
if content is None: if content is None:
lines = [] lines = []
@ -490,21 +500,20 @@ def handle_config():
replace_in_file('/etc/default/isc-dhcp-server', replace_in_file('/etc/default/isc-dhcp-server',
r'INTERFACESv4="(.*)"', ' '.join([n for n in box.macaddr if n != box.main_if])) r'INTERFACESv4="(.*)"', ' '.join([n for n in box.macaddr if n != box.main_if]))
elif doit: elif doit:
unix_cmd('systemctl stop isc-dhcp-server') check_service('isc-dhcp-server', False)
unix_cmd('systemctl disable isc-dhcp-server')
content = [] content = []
dirty = False dirty = False
for section, section_dict in config.items(): for section, section_dict in config.items():
if COMMENT not in section_dict: # if COMMENT not in section_dict:
dirty = True # dirty = True
content.append('[%s]' % section) # content.append('[%s]' % section)
content.append(COMMENT) # content.append(COMMENT)
if section == 'DISPLAY': if section == 'DISPLAY':
if display_update(section_dict): if display_update(section_dict):
to_start['display'] = 'restart' to_start['display'] = 'restart'
for key, value in section_dict.items(): # for key, value in section_dict.items():
content.append('%s=%s' % (key, value)) # content.append('%s=%s' % (key, value))
content.append('') # content.append('')
if dirty: if dirty:
print(cfgfile) print(cfgfile)
print('\n'.join(content)) print('\n'.join(content))
@ -520,33 +529,17 @@ def handle_config():
section = service.upper() section = service.upper()
section_dict = config.get(section, {}) section_dict = config.get(section, {})
servicecfg = service_func(**section_dict) servicecfg = service_func(**section_dict)
result = unix_cmd('systemctl show -p WantedBy -p ActiveState %s' % service, True) active, enabled = check_service(service)
active = False
enabled = False
for line in result.split('\n'):
if line.startswith('WantedBy=m'):
enabled = True
elif line.strip() == 'ActiveState=active':
active = True
if servicecfg is None: if servicecfg is None:
if active: if active or enabled:
unix_cmd('systemctl stop %s' % service) check_service(service, False, doit)
show.dirty = True show.dirty = True
if enabled:
unix_cmd('systemctl disable %s' % service)
show.dirty = True
if service == 'router' and active or enabled:
if doit:
shutil.copy(join(TOOLS, 'nftables.conf'), '/etc/nftables.conf')
else:
print('cp nftables.conf /etc/nftables.conf')
unix_cmd('systemctl restart nftables')
else: else:
if not enabled: if not enabled:
to_start[service] = 'enable' to_start[service] = 'enable'
elif not active: elif not active:
to_start[service] = 'restart' to_start[service] = 'restart'
if write_when_new('/etc/systemd/system/%s.service' % service, servicecfg): if write_when_new(f'/etc/systemd/system/{service}.service', servicecfg):
show.dirty = True show.dirty = True
reload_systemd = True reload_systemd = True
if servicecfg and to_start.get('service') is None: if servicecfg and to_start.get('service') is None:
@ -554,25 +547,25 @@ def handle_config():
pip() pip()
if reload_systemd: if reload_systemd:
unix_cmd('systemctl daemon-reload') do_cmd('systemctl daemon-reload')
for service, action in to_start.items(): for service, action in to_start.items():
show.dirty = True show.dirty = True
if action == 'if_restart': if action == 'if_restart':
unix_cmd('ifdown %s' % service) do_cmd(f'ifdown {service}')
for service, action in to_start.items(): for service, action in to_start.items():
if action == 'if_restart': if action == 'if_restart':
unix_cmd('ifup %s' % service) do_cmd(f'ifup {service}')
else: else:
if action == 'restart': if action == 'restart':
unix_cmd('systemctl restart %s' % service) do_cmd(f'systemctl restart {service}')
unix_cmd('systemctl enable %s' % service) do_cmd(f'systemctl enable {service}')
if box.change_if_names: if box.change_if_names:
print('interface name system has to be changed from enp*s0 to eth*') print('interface name system has to be changed from enp*s0 to eth*')
if replace_in_file('/etc/default/grub', if replace_in_file('/etc/default/grub',
r'GRUB_CMDLINE_LINUX_DEFAULT="quiet(.*)"', r'GRUB_CMDLINE_LINUX_DEFAULT="quiet(.*)"',
' net.ifnames=0"'): ' net.ifnames=0"'):
unix_cmd('update-grub') do_cmd('update-grub')
result = [f'config file:\n {cfgfile}'] result = [f'config file:\n {cfgfile}']
for section, section_dict in config.items(): for section, section_dict in config.items():

View File

@ -9,9 +9,7 @@ from glob import glob
from select import select from select import select
from serial import serial_for_url from serial import serial_for_url
from subprocess import Popen, PIPE, check_output, call, DEVNULL from subprocess import Popen, PIPE, check_output, call, DEVNULL
from utils import BoxInfo from utils import BoxInfo, change_firewall
FIREWALL_CONF = '/etc/nftables.conf'
class Log: class Log:
DEBUG = 0 DEBUG = 0
@ -169,7 +167,7 @@ class Router(IoHandler):
return return
except Exception as e: except Exception as e:
msg = f'error in request: {e!r}' msg = f'error in request: {e!r}'
service.failures[service.port] = msg self.service.failures[service.port] = msg
log.error(msg) log.error(msg)
self.close() self.close()
@ -401,36 +399,12 @@ class Service:
self.failures.pop(self.port, None) self.failures.pop(self.port, None)
self.handlers[handler.fno] = handler self.handlers[handler.fno] = handler
@classmethod
def change_firewall(cls):
pattern = re.compile('(tcp dport ({.*}) ct state new accept)', re.MULTILINE | re.DOTALL)
with open(FIREWALL_CONF) as f:
content = f.read()
try:
((prevline, prevports),) = pattern.findall(content)
except (TypeError, ValueError):
print(f'{FIREWALL_CONF} does not contain expected pattern for open ports - firewall off?')
return
# parse previous port set
prevportset = {int(p) for p in prevports[1:-1].split(',')}
# keep port numbers below 1024
ports = {p for p in prevportset if p < 1024} | set(cls.firewall_ports)
line = prevline.replace(prevports, '{ %s }' % ', '.join((str(p) for p in sorted(ports))))
if prevportset != ports:
if os.geteuid() == 0:
with open('f{FIREWALL_CONF}.tmp', 'w') as f:
f.write(content.replace(prevline, line))
os.rename('f{FIREWALL_CONF}.tmp', FIREWALL_CONF)
unix_cmd('systemctl restart nftables')
unix_cmd('systemctl enable nftables')
else:
print('need sudo rights to modify firewall')
@classmethod @classmethod
def run(cls, routes): def run(cls, routes):
firewall = routes.pop('firewall', None)
firewall_on = firewall is not None
if firewall_on:
cls.firewall_ports = set(int(r) for r in firewall.split(',') if r.strip())
Service(1110, None, InfoHandler, 5, handler_args=(log.DEBUG,)) Service(1110, None, InfoHandler, 5, handler_args=(log.DEBUG,))
Service(1111, None, InfoHandler, 5, handler_args=(log.INFO,)) Service(1111, None, InfoHandler, 5, handler_args=(log.INFO,))
Service(1112, None, InfoHandler, 5, handler_args=(log.WARN,)) Service(1112, None, InfoHandler, 5, handler_args=(log.WARN,))
@ -445,7 +419,7 @@ class Service:
else: else:
remoteport = port remoteport = port
Service(port, (host, remoteport), TcpHandler) Service(port, (host, remoteport), TcpHandler)
cls.change_firewall() change_firewall(firewall_on, cls.firewall_ports)
while True: while True:
try: try:
# log.debug('select %r', list(cls.readers)) # log.debug('select %r', list(cls.readers))

View File

@ -1,9 +1,14 @@
import os import os
import socket import socket
import threading import threading
import re
from glob import glob from glob import glob
from configparser import ConfigParser from configparser import ConfigParser
from netifaces import interfaces, ifaddresses, gateways, AF_INET, AF_LINK from netifaces import interfaces, ifaddresses, gateways, AF_INET, AF_LINK
from subprocess import Popen, PIPE
FIREWALL_CONF = '/etc/nftables.conf'
if os.geteuid(): if os.geteuid():
@ -130,3 +135,90 @@ class MainIf:
else: else:
self.prev_ip = None self.prev_ip = None
return self.carrier, self.ip, self.hostnameresult[0], self.gateway return self.carrier, self.ip, self.hostnameresult[0], self.gateway
def unix_cmd(command, execute=None, stdout=PIPE):
if execute != False: # None or True
if execute:
print('$ %s' % command)
result = Popen(command.split(), stdout=stdout).communicate()[0]
return (result or b'').decode()
else:
print('> %s' % command)
def check_service(service, set_on=None, execute=None):
"""check or set state of systemd service
set_on is None or not given: query only
bool(set_on) is True: start and enable if not yet done
set_on == False/0: stop and disable if not yet done
sim: print out command instead of executing
"""
result = unix_cmd(f'systemctl show -p WantedBy -p ActiveState {service}')
enabled = False
active = False
for line in result.split('\n'):
if line.startswith('WantedBy=') and line.strip() != 'WantedBy=':
enabled = True
elif line.strip() == 'ActiveState=active':
active = True
if set_on:
if not active:
unix_cmd(f'systemctl start {service}', execute)
if not enabled:
unix_cmd(f'systemctl enable {service}', execute)
elif set_on is not None:
if active:
unix_cmd(f'systemctl stop {service}', execute)
if enabled:
unix_cmd(f'systemctl disable {service}', execute)
return active, enabled
def change_firewall(set_on, ports, execute=None):
ports.add(22) # always add ssh
active, enabled = check_service('nftables')
if not set_on:
if os.geteuid() == 0:
check_service('nftables', False, execute)
else:
print('need sudo rights to modify firewall')
return active or enabled
pattern = re.compile('(tcp dport ({.*}) ct state new accept)', re.MULTILINE | re.DOTALL)
for filename in FIREWALL_CONF, os.path.join(BoxInfo.TOOLS, 'nftables.conf'):
with open(filename) as f:
content = f.read()
try:
((prevline, prevports),) = pattern.findall(content)
break
except (TypeError, ValueError):
pass
else:
print(f'{FIREWALL_CONF} does not contain expected pattern for open ports - firewall off?')
return False
# parse previous port set
prevportset = {int(p) for p in prevports[1:-1].split(',')}
line = prevline.replace(prevports, '{ %s }' % ', '.join((str(p) for p in sorted(ports))))
if prevportset == ports:
if active and enabled:
return False
check_service('nftables', True, execute)
return True
if os.geteuid() == 0:
if execute is not None:
print(f'change firewall ports to {ports}')
if execute != 0:
with open('f{FIREWALL_CONF}.tmp', 'w') as f:
f.write(content.replace(prevline, line))
os.rename('f{FIREWALL_CONF}.tmp', FIREWALL_CONF)
unix_cmd('systemctl restart nftables', execute)
unix_cmd('systemctl enable nftables', execute)
elif ports - prevportset:
print('need sudo rights to modify firewall')
return True