diff --git a/desktop/boxweb-browser.service b/desktop/boxweb-browser.service new file mode 100644 index 0000000..e8acc53 --- /dev/null +++ b/desktop/boxweb-browser.service @@ -0,0 +1,12 @@ +[Unit] +Description = Running Web Browser for boxweb +Requires=labwc.service +Requires=boxweb.service + +[Service] +Type=simple +ExecStart = /home/l_samenv/boxweb/start_browser.py + +[Install] +WantedBy = default.target + diff --git a/desktop/labwc.service b/desktop/labwc.service new file mode 100644 index 0000000..448cdd9 --- /dev/null +++ b/desktop/labwc.service @@ -0,0 +1,11 @@ +[Unit] +Description=LabWC Wayland compositor + +[Service] +User=l_samenv +Type=simple +ExecStart=/usr/bin/labwc +Restart=on-failure + +[Install] +WantedBy=default.target diff --git a/install.py b/install.py index 72cb669..b776fc4 100755 --- a/install.py +++ b/install.py @@ -47,6 +47,9 @@ except ImportError: if serial is None: exit() +main_ports = {22} # ssh + +router_ports = set() TOOLS = BoxInfo.TOOLS BOX_TYPES = {'ionopi', 'ionopimax', 'control-box', 'bare-apu', 'dual-eth-rpi'} @@ -198,6 +201,18 @@ def do_cmd(*command, sudo=True): show.dirty = True +def do_copy(src, dst): + """copy or delete (src=None)""" + as_root = not dst.is_relative_to('/home/l_samenv') + if src is None: + if dst.exists(): + do_cmd('rm', dst, sudo=as_root) + return + if dst.exists() and getmtime(src) <= getmtime(dst): + return + do_cmd('cp', src, dst, sudo=as_root) + + def frappy(cfg=None, port=None, requirements='', **kwds): if not cfg: return False, None @@ -209,27 +224,18 @@ def frappy(cfg=None, port=None, requirements='', **kwds): return False, FRAPPY_SERVICE -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 +def router(**opts): + active, enabled = check_service('nftables') + if opts: + for key in opts: + try: + router_ports.add(int(key)) + except ValueError: + pass + router_ports.update((1110, 1111, 1112)) if not opts: return True, None try: - os.remove(TO_SYSTEM[0] / 'etc/nftables.conf') with open(TOOLS / 'requirements.txt') as f: pip_requirements['root']['tools'] = f.read() except FileNotFoundError: @@ -281,8 +287,21 @@ def pip(): def boxweb(port=8080, page=None): - port = int(port) - servicecfg = None if page is None else BOXWEB_SERVICE.format(port=port, page=page) + if page is None: + servicecfg = None + else: + port = int(port) + main_ports.add(port) + if port == 8080: + main_ports.add(80) + servicecfg = BOXWEB_SERVICE.format(port=port, page=page) + for file, dest in (('labwc.service', '/etc/systemd/system'), + ('boxweb-browser.service', '/home/l_samenv/.config/systemd/user')): + src = TOOLS / 'desktop' / file + dst = Path(dest) / file + if not page: + src = None # -> remove + do_copy(src, dst) return port <= 1024, servicecfg @@ -697,6 +716,7 @@ def handle_config(): if servicecfg and to_start.get('service') is None: to_start[service] = 'restart', as_root + change_firewall(router_ports, main_ports, doit) if 'dialout' not in unix_cmd('id l_samenv'): do_cmd('usermod -a -G dialout l_samenv') pip() diff --git a/nftables.conf b/nftables.conf deleted file mode 100644 index fb32d41..0000000 --- a/nftables.conf +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/sbin/nft -f - -flush ruleset - -table inet filter { - chain input { - type filter hook input priority 0; - - # accept any localhost traffic - iif lo accept - - # accept traffic originated from us - ct state established,related accept - - # activate the following line to accept common local services - tcp dport { 22 } ct state new accept - - # ICMPv6 packets which must not be dropped, see https://tools.ietf.org/html/rfc4890#section-4.4.1 - meta nfproto ipv6 icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, echo-reply, echo-request, nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert, 148, 149 } accept - ip6 saddr fe80::/10 icmpv6 type { 130, 131, 132, 143, 151, 152, 153 } accept - - # count and drop any other traffic - counter drop - } -} diff --git a/router.py b/router.py index 1aa8bec..0fd8d10 100644 --- a/router.py +++ b/router.py @@ -421,10 +421,6 @@ class Service: @classmethod 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(1111, None, InfoHandler, 5, handler_args=(log.INFO,)) Service(1112, None, InfoHandler, 5, handler_args=(log.WARN,)) @@ -439,7 +435,7 @@ class Service: else: remoteport = port Service(port, (host, remoteport), TcpHandler) - change_firewall(firewall_on, cls.firewall_ports) + change_firewall(cls.firewall_ports, None) while True: try: # log.debug('select %r', list(cls.readers)) @@ -476,3 +472,5 @@ if __name__ == '__main__': routercfg = BoxInfo().read_config('ROUTER') if routercfg: Service.run(routercfg) + else: + print('router is not configured') diff --git a/utils.py b/utils.py index 85d28d3..0540e0f 100644 --- a/utils.py +++ b/utils.py @@ -2,6 +2,7 @@ import os import socket import threading import re +import tempfile from glob import glob from pathlib import Path from configparser import ConfigParser @@ -10,6 +11,48 @@ from subprocess import Popen, PIPE FIREWALL_CONF = '/etc/nftables.conf' +FIREWALL_MAIN = Path('~/.config/firewall').expanduser() + +NFTABLES_CONF = """#!/usr/sbin/nft -f + +flush ruleset + +table inet filter { + chain input { + type filter hook input priority 0; + + # accept any localhost traffic + iif lo accept + + # accept traffic originated from us + ct state established,related accept + + # activate the following line to accept common local services + tcp dport { %s } ct state new accept + + # ICMPv6 packets which must not be dropped, see https://tools.ietf.org/html/rfc4890#section-4.4.1 + meta nfproto ipv6 icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, echo-reply, echo-request, nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert, 148, 149 } accept + ip6 saddr fe80::/10 icmpv6 type { 130, 131, 132, 143, 151, 152, 153 } accept + + # count and drop any other traffic + counter drop + } +} +%s +""" + +ROUTE80 = """ +table ip nat { + chain prerouting { + type nat hook prerouting priority 0; policy accept; + tcp dport 80 redirect to 8080 + } + + chain postrouting { + type nat hook postrouting priority 0; policy accept; + } +} +""" if os.geteuid(): @@ -255,45 +298,49 @@ def check_service(service, set_on=None, execute=None, as_root=True): 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: - check_service('nftables', False, execute) - return active or enabled - - pattern = re.compile('(tcp dport ({.*}) ct state new accept)', re.MULTILINE | re.DOTALL) - - for filename in FIREWALL_CONF, BoxInfo.TOOLS / 'nftables.conf': - with open(filename) as f: - content = f.read() +def change_firewall(router_ports, main_ports, execute=None): + """configure the firewall + router_ports: the ports needed for the router + main_ports: other ports needed to be open. when None, read it from FIREWALL_MAIN + execute: None: do it quietly, True: do it, False: display only + """ + if main_ports is None: try: - ((prevline, prevports),) = pattern.findall(content) - break - except (TypeError, ValueError): - pass + with open(FIREWALL_MAIN) as fil: + main_ports = set(int(p) for p in f.read().split()) + except FileNotFoundError: + main_ports = {22} else: - print(f'{FIREWALL_CONF} does not contain expected pattern for open ports - firewall off?') - return False + main_ports.add(22) # always add ssh + if execute: + with open(FIREWALL_MAIN, 'w') as fil: + fil.write(' '.join(str(p) for p in sorted(main_ports))) + fil.write('\n') - # 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: + ports = main_ports | router_ports + active, enabled = check_service('nftables') + + with open(FIREWALL_CONF) as f: + oldcontent = f.read() + + portstr = ', '.join(str(p) for p in sorted(ports)) + content = NFTABLES_CONF % (portstr, ROUTE80 if {80, 8080} <= ports else '') + + if content == oldcontent: 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) + if execute is not None: + print(f'set firewall ports to {portstr}') + if execute != 0: + with tempfile.NamedTemporaryFile('w') as fil: + fil.write(content) + fil.flush() + unix_cmd('cp', fil.name, FIREWALL_CONF, execute=execute) + unix_cmd('chmod 0644', FIREWALL_CONF, execute=execute) + unix_cmd('systemctl restart nftables', execute=execute) unix_cmd('systemctl enable nftables', execute=execute) - elif ports - prevportset: - print('need sudo rights to modify firewall') return True