#!/usr/bin/env python3 """Speed up captive portal CNA on both live routers. Backup first. No secrets in output.""" from __future__ import annotations import io import re import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent)) import paramiko FONT_RE = re.compile( r'[ \t]*]+(?:fonts\.googleapis\.com|fonts\.gstatic\.com)[^>]*>\s*', re.I, ) FAST_MARK = "wo-cna-fast" FAST_CSS = ( '" ) HUB_RSC = r""" :put "HUB-CNA-FAST START" /system backup save name=wo-before-cna-fast /ip hotspot profile set [find name=hs-otantik] dns-name="" login-by=cookie,http-chap,http-pap :if ([:len [/ipv6 nd find where interface=bridge-hotspot]] = 0) do={ :do { /ipv6 nd add interface=bridge-hotspot ra-lifetime=none advertise-dns=no advertise-mac-address=no comment=wo-hs-no-ra } on-error={ :do { /ipv6 nd add interface=bridge-hotspot ra-lifetime=0s advertise-dns=no comment=wo-hs-no-ra } on-error={} } } else={ :do { /ipv6 nd set [find interface=bridge-hotspot] ra-lifetime=none advertise-dns=no advertise-mac-address=no } on-error={ :do { /ipv6 nd set [find interface=bridge-hotspot] ra-lifetime=0s advertise-dns=no } on-error={} } } :if ([:len [/ipv6 firewall filter find comment="wo-hs-no-ra"]] = 0) do={ :do { /ipv6 firewall filter add chain=output out-interface=bridge-hotspot protocol=icmpv6 icmp-options=134:0-255 action=drop comment=wo-hs-no-ra } on-error={} } :if ([:len [/ipv6 firewall filter find comment="wo-hs-no-v6-fwd"]] = 0) do={ :do { /ipv6 firewall filter add chain=forward in-interface=bridge-hotspot action=drop comment=wo-hs-no-v6-fwd } on-error={} } :do { /ip hotspot walled-garden remove [find dst-host="*.gstatic.com"] } on-error={} :do { /ip hotspot walled-garden remove [find dst-host="www.gstatic.com"] } on-error={} :put "HUB-CNA-FAST DONE" /ip hotspot profile print where name=hs-otantik /ipv6 nd print /ip hotspot walled-garden print where dst-host~"gstatic" """ NKO_RSC = r""" :put "NKO-CNA-FAST START" /system backup save name=wo-before-cna-fast /ip hotspot profile set [find name=hsprof1] dns-name="" login-by=cookie,http-chap,http-pap html-directory=hotspot :do { /ip hotspot profile set [find name=hs-otantik] dns-name="" login-by=cookie,http-chap,http-pap } on-error={} :if ([:len [/ipv6 nd find where interface=hs-bridge]] = 0) do={ :do { /ipv6 nd add interface=hs-bridge ra-lifetime=none advertise-dns=no advertise-mac-address=no comment=wo-hs-no-ra } on-error={ :do { /ipv6 nd add interface=hs-bridge ra-lifetime=0s advertise-dns=no comment=wo-hs-no-ra } on-error={} } } else={ :do { /ipv6 nd set [find interface=hs-bridge] ra-lifetime=none advertise-dns=no advertise-mac-address=no } on-error={ :do { /ipv6 nd set [find interface=hs-bridge] ra-lifetime=0s advertise-dns=no } on-error={} } } :if ([:len [/ipv6 firewall filter find comment="wo-hs-no-ra"]] = 0) do={ :do { /ipv6 firewall filter add chain=output out-interface=hs-bridge protocol=icmpv6 icmp-options=134:0-255 action=drop comment=wo-hs-no-ra } on-error={} } :if ([:len [/ipv6 firewall filter find comment="wo-hs-no-v6-fwd"]] = 0) do={ :do { /ipv6 firewall filter add chain=forward in-interface=hs-bridge action=drop comment=wo-hs-no-v6-fwd } on-error={} } :do { /ip hotspot walled-garden remove [find dst-host="*.gstatic.com"] } on-error={} :do { /ip hotspot walled-garden remove [find dst-host="www.gstatic.com"] } on-error={} :put "NKO-CNA-FAST DONE" /ip hotspot profile print where name=hsprof1 /ipv6 nd print /ip hotspot walled-garden print where dst-host~"gstatic" """ def patch_html(raw: str) -> str: html = FONT_RE.sub("", raw) if FAST_MARK not in html: if re.search(r"", html, re.I): html = re.sub(r"", FAST_CSS + "", html, count=1, flags=re.I) else: html = FAST_CSS + html return html def sftp_patch_html(sftp: paramiko.SFTPClient, paths: list[str]) -> None: for remote in paths: try: with sftp.file(remote, "r") as f: raw = f.read() if isinstance(raw, bytes): text = raw.decode("utf-8", "replace") else: text = str(raw) except Exception as e: print("SKIP", remote, type(e).__name__) continue new = patch_html(text) if new == text: print("UNCHANGED", remote, len(text)) continue bio = io.BytesIO(new.encode("utf-8")) sftp.putfo(bio, remote) print("PATCHED", remote, len(text), "->", len(new)) def apply_hub() -> None: from mk_hub_exec import connect, import_rsc print("==== HUB BACKUP+RSC ====") print(import_rsc("wo-cna-fast.rsc", HUB_RSC, timeout=120)) mk = connect() try: sftp = mk.open_sftp() sftp_patch_html( sftp, [ "hotspot/login.html", "hotspot/alogin.html", "hotspot/status.html", "hotspot/ticket.html", "hotspot/pay.html", "hotspot/code.html", "hotspot/error.html", "hotspot/logout.html", "hotspot/apropos.html", "hotspot/services.html", "hotspot/chat.html", ], ) sftp.close() finally: mk.close() from mk_hub_exec import run print("==== HUB VERIFY ====") print( run( r""" :put [/ip hotspot profile get [find name=hs-otantik] dns-name] :put [/ip hotspot profile get [find name=hs-otantik] login-by] :put [/ip hotspot profile get [find name=hs-otantik] html-directory] :put [/ip hotspot get [find name=hotspot-otantik] ip-of-dns-name] /ipv6 nd print where comment=wo-hs-no-ra or interface=bridge-hotspot :put ("gstatic-wg=" . [:len [/ip hotspot walled-garden find dst-host~"gstatic"]]) """, timeout=40, ) ) def apply_nko() -> None: from mk_rb4011_exec import JUMP, MK print("==== NKO BACKUP+RSC ====") jump = paramiko.SSHClient() jump.set_missing_host_key_policy(paramiko.AutoAddPolicy()) jump.connect(JUMP[0], username=JUMP[1], password=JUMP[2], timeout=25, allow_agent=False, look_for_keys=False) chan = jump.get_transport().open_channel("direct-tcpip", (MK[0], 22), ("127.0.0.1", 0)) mk = paramiko.SSHClient() mk.set_missing_host_key_policy(paramiko.AutoAddPolicy()) mk.connect(MK[0], username=MK[1], password=MK[2], sock=chan, timeout=25, allow_agent=False, look_for_keys=False) try: sftp = mk.open_sftp() with sftp.file("wo-cna-fast.rsc", "w") as f: f.write(NKO_RSC) stdin, stdout, stderr = mk.exec_command("/import file-name=wo-cna-fast.rsc", timeout=120) print(stdout.read().decode("utf-8", "replace")) err = stderr.read().decode("utf-8", "replace") if err.strip(): print("STDERR", err) sftp_patch_html( sftp, [ "hotspot/login.html", "hotspot/alogin.html", "hotspot/status.html", "hotspot/ticket.html", "hotspot/pay.html", "hotspot/code.html", "hotspot/error.html", "hotspot/logout.html", "hotspot/apropos.html", "hotspot/services.html", "hotspot/chat.html", "portal/login.html", "portal/alogin.html", "portal/status.html", "portal/ticket.html", "portal/pay.html", ], ) sftp.close() stdin, stdout, stderr = mk.exec_command( r""" :put [/ip hotspot profile get [find name=hsprof1] dns-name] :put [/ip hotspot profile get [find name=hsprof1] login-by] :put [/ip hotspot profile get [find name=hsprof1] html-directory] :put [/ip hotspot get [find name=hotspot1] ip-of-dns-name] /ipv6 nd print where comment=wo-hs-no-ra or interface=hs-bridge :put ("gstatic-wg=" . [:len [/ip hotspot walled-garden find dst-host~"gstatic"]]) """, timeout=40, ) print("==== NKO VERIFY ====") print(stdout.read().decode("utf-8", "replace")) err = stderr.read().decode("utf-8", "replace") if err.strip(): print("STDERR", err) finally: mk.close() jump.close() if __name__ == "__main__": which = sys.argv[1] if len(sys.argv) > 1 else "both" if which in ("hub", "both"): apply_hub() if which in ("nko", "both"): apply_nko()