#!/usr/bin/env python3 """Isolate hotspot onto WiFi only. Never puts captive portal on ether2/ether3. Leaves 192.168.88.0/24 (office LAN / this PC) untouched. """ from __future__ import annotations import sys sys.path.insert(0, r"c:\laragon\www\light-otantik-nux\scripts") from mk_client import connect, ok, replies_to_rows, talk, trap_msg HOST = "192.168.88.1" USER = "admin" PASSWORD = "" HS_BR = "bridge-hotspot" HS_IP = "10.10.88.1/24" HS_NET = "10.10.88.0/24" HS_GW = "10.10.88.1" HS_POOL = "hs-pool" HS_RANGES = "10.10.88.10-10.10.88.250" DHCP_NAME = "dhcp-hs" HS_PROFILE = "hs-otantik" HS_SERVER = "hotspot-otantik" LAN_NET = "192.168.88.0/24" def run(sock, words): r = talk(sock, words) if not ok(r): raise RuntimeError("%s -> %s" % (words[0], trap_msg(r))) return r def rows(sock, path, *q): return replies_to_rows(talk(sock, [path, *q])) def exists(sock, path, key, value): return bool(rows(sock, path, "?%s=%s" % (key, value))) def add_if_missing(sock, add_path, print_path, key, value, args): if exists(sock, print_path, key, value): print("SKIP", add_path, value) return r = talk(sock, [add_path] + ["=%s=%s" % (k, v) for k, v in args.items()]) if ok(r): print("OK ", add_path, value) else: raise RuntimeError("FAIL %s %s %s" % (add_path, value, trap_msg(r))) def main() -> None: sock = connect(HOST, USER, PASSWORD, 8728, 20) print("LOGIN OK") r = talk(sock, ["/system/backup/save", "=name=wo-before-hs-isolate"]) print("BACKUP", "OK" if ok(r) else trap_msg(r)) add_if_missing( sock, "/interface/bridge/add", "/interface/bridge/print", "name", HS_BR, {"name": HS_BR, "comment": "wifi-otantik-hs"}, ) # Move wlan1 off the office LAN bridge onto the hotspot bridge. wlan_ports = rows(sock, "/interface/bridge/port/print", "?interface=wlan1") for p in wlan_ports: br = p.get("bridge", "") pid = p.get(".id", "") if br == HS_BR: print("SKIP wlan1 already on", HS_BR) continue if not pid: continue print("MOVE wlan1 from", br, "->", HS_BR) run(sock, ["/interface/bridge/port/remove", "=.id=" + pid]) if not exists(sock, "/interface/bridge/port/print", "interface", "wlan1"): run( sock, [ "/interface/bridge/port/add", "=bridge=" + HS_BR, "=interface=wlan1", "=comment=wifi-otantik-hs", ], ) print("OK wlan1 port on", HS_BR) # Keep office LAN in LAN list; also allow hotspot clients out to WAN. if not exists(sock, "/interface/list/member/print", "interface", HS_BR): run( sock, [ "/interface/list/member/add", "=list=LAN", "=interface=" + HS_BR, "=comment=wifi-otantik-hs", ], ) print("OK list LAN +=", HS_BR) else: print("SKIP list member", HS_BR) add_if_missing( sock, "/ip/address/add", "/ip/address/print", "comment", "wifi-otantik-hs", {"address": HS_IP, "interface": HS_BR, "comment": "wifi-otantik-hs"}, ) add_if_missing( sock, "/ip/pool/add", "/ip/pool/print", "name", HS_POOL, {"name": HS_POOL, "ranges": HS_RANGES}, ) add_if_missing( sock, "/ip/dhcp-server/network/add", "/ip/dhcp-server/network/print", "address", HS_NET, { "address": HS_NET, "gateway": HS_GW, "dns-server": HS_GW, "comment": "wifi-otantik-hs", }, ) add_if_missing( sock, "/ip/dhcp-server/add", "/ip/dhcp-server/print", "name", DHCP_NAME, { "name": DHCP_NAME, "interface": HS_BR, "address-pool": HS_POOL, "lease-time": "1h", "disabled": "no", }, ) add_if_missing( sock, "/ip/firewall/nat/add", "/ip/firewall/nat/print", "comment", "wifi-otantik-hs-masq", { "chain": "srcnat", "src-address": HS_NET, "action": "masquerade", "comment": "wifi-otantik-hs-masq", }, ) add_if_missing( sock, "/ip/dns/static/add", "/ip/dns/static/print", "comment", "wifi-otantik-hs", {"name": "hotspot.lan", "address": HS_GW, "comment": "wifi-otantik-hs"}, ) add_if_missing( sock, "/ip/hotspot/profile/add", "/ip/hotspot/profile/print", "name", HS_PROFILE, { "name": HS_PROFILE, "hotspot-address": HS_GW, "dns-name": "hotspot.lan", "html-directory": "hotspot", "login-by": "cookie,http-chap,http-pap", }, ) add_if_missing( sock, "/ip/hotspot/add", "/ip/hotspot/print", "name", HS_SERVER, { "name": HS_SERVER, "interface": HS_BR, "address-pool": HS_POOL, "profile": HS_PROFILE, "disabled": "no", }, ) # If a future script wrongly binds hotspot to the office bridge, LAN still bypasses. if not exists(sock, "/ip/hotspot/ip-binding/print", "comment", "wifi-otantik-lan-bypass"): run( sock, [ "/ip/hotspot/ip-binding/add", "=address=" + LAN_NET, "=type=bypassed", "=comment=wifi-otantik-lan-bypass", ], ) print("OK LAN bypass", LAN_NET) else: print("SKIP LAN bypass") for host in [ "portal.otantik.cm", "wifi.otantik.cm", "business.otantik.cm", "*.otantik.cm", "mesomb.hachther.com", "*.mesomb.hachther.com", "fonts.googleapis.com", "fonts.gstatic.com", ]: if not exists(sock, "/ip/hotspot/walled-garden/print", "dst-host", host): r = talk( sock, [ "/ip/hotspot/walled-garden/add", "=action=allow", "=dst-host=" + host, "=comment=wifi-otantik", ], ) print("WG ", host, "OK" if ok(r) else trap_msg(r)) else: print("SKIP wg", host) if not exists(sock, "/ip/hotspot/walled-garden/ip/print", "dst-address", "83.147.38.107"): r = talk( sock, [ "/ip/hotspot/walled-garden/ip/add", "=action=accept", "=dst-address=83.147.38.107", "=comment=wifi-otantik", ], ) print("WGIP 83.147.38.107", "OK" if ok(r) else trap_msg(r)) print("\n==== VERIFY (must keep ether2/3 on office bridge) ====") for p in rows(sock, "/interface/bridge/port/print"): print(" port", p.get("interface"), "->", p.get("bridge")) for h in rows(sock, "/ip/hotspot/print"): print(" hotspot", h.get("name"), "iface=", h.get("interface"), "disabled=", h.get("disabled")) for a in rows(sock, "/ip/address/print"): print(" addr", a.get("address"), "on", a.get("interface")) sock.close() print("DONE") if __name__ == "__main__": main()