164 lines
5.9 KiB
Python
164 lines
5.9 KiB
Python
import json
|
|
import os
|
|
import subprocess
|
|
import time
|
|
from pathlib import Path
|
|
|
|
SERVERS_FILE = Path(os.environ.get("SERVERS_FILE", "/cache/servers.json"))
|
|
|
|
_fw_cache = {}
|
|
|
|
DEFAULT_SERVERS = [
|
|
{"name": "ubuntu-web (.65)", "host": "192.168.0.65", "user": "rob", "key": "/root/.ssh/id_ed25519", "sudo": True},
|
|
{"name": "ubuntu VPS (.2)", "host": "66.179.94.204", "user": "rob", "key": "/root/.ssh/id_ed25519", "sudo": True},
|
|
{"name": "jetson-nano (.23)", "host": "192.168.0.23", "user": "rob", "key": "/root/.ssh/id_ed25519", "sudo": True},
|
|
]
|
|
|
|
|
|
def load_servers():
|
|
if SERVERS_FILE.exists():
|
|
try:
|
|
servers = json.loads(SERVERS_FILE.read_text())
|
|
if servers:
|
|
return servers
|
|
except Exception:
|
|
pass
|
|
return DEFAULT_SERVERS
|
|
|
|
|
|
def ssh_exec(server, cmd, timeout=30):
|
|
# Local agent mode: run directly on this host (no SSH)
|
|
if server.get("host") == "local":
|
|
try:
|
|
r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout)
|
|
return r.returncode, r.stdout, r.stderr
|
|
except Exception as e:
|
|
return 1, "", str(e)
|
|
host = server.get("host")
|
|
user = server.get("user", "rob")
|
|
key = server.get("key", "/root/.ssh/id_ed25519")
|
|
ssh_cmd = [
|
|
"ssh", "-i", key,
|
|
"-o", "StrictHostKeyChecking=no",
|
|
"-o", "UserKnownHostsFile=/dev/null",
|
|
"-o", "ConnectTimeout=10",
|
|
"-o", "BatchMode=yes",
|
|
f"{user}@{host}", cmd,
|
|
]
|
|
try:
|
|
r = subprocess.run(ssh_cmd, capture_output=True, text=True, timeout=timeout)
|
|
return r.returncode, r.stdout, r.stderr
|
|
except Exception as e:
|
|
return 1, "", str(e)
|
|
|
|
|
|
def _sudo(server):
|
|
return "sudo " if server.get("sudo", True) else ""
|
|
|
|
|
|
def firewall_status(server):
|
|
key = f"{server.get('name')}:{server.get('host')}"
|
|
now = time.time()
|
|
item = _fw_cache.get(key)
|
|
if item and now - item[0] < 120:
|
|
return item[1]
|
|
s = _sudo(server)
|
|
status = {
|
|
"name": server.get("name", server.get("host")),
|
|
"host": server.get("host"),
|
|
"reachable": False,
|
|
"country_block": False,
|
|
"scanner_block": False,
|
|
"scanner_ips": 0,
|
|
"country_cidrs": 0,
|
|
"error": None,
|
|
}
|
|
|
|
# single combined SSH call for all firewall checks
|
|
cmd = (
|
|
"echo ok; "
|
|
f"{s}ipset list blocked_countries 2>/dev/null | grep -c /; "
|
|
f"{s}iptables -C INPUT -m set --match-set blocked_countries src -j DROP 2>/dev/null && echo cb_active || echo cb_inactive; "
|
|
f"{s}iptables -C INPUT -j SCANNER-BLOCK 2>/dev/null && echo sb_active || echo sb_inactive; "
|
|
f"{s}iptables -S SCANNER-BLOCK 2>/dev/null | grep -c DROP"
|
|
)
|
|
rc, out, err = ssh_exec(server, cmd)
|
|
if rc != 0 or "ok" not in out:
|
|
status["error"] = (err or out or "unreachable").strip()
|
|
return status
|
|
status["reachable"] = True
|
|
lines = [l.strip() for l in out.splitlines() if l.strip()]
|
|
try:
|
|
status["country_cidrs"] = int(lines[1]) if len(lines) > 1 else 0
|
|
except (ValueError, IndexError):
|
|
status["country_cidrs"] = 0
|
|
if len(lines) > 2:
|
|
status["country_block"] = lines[2] == "cb_active"
|
|
if len(lines) > 3:
|
|
status["scanner_block"] = lines[3] == "sb_active"
|
|
if len(lines) > 4:
|
|
try:
|
|
status["scanner_ips"] = int(lines[4])
|
|
except ValueError:
|
|
pass
|
|
|
|
_fw_cache[key] = (now, status)
|
|
return status
|
|
|
|
|
|
def block_ip(server, ip):
|
|
s = _sudo(server)
|
|
# validate IP to avoid shell injection
|
|
if not ip or any(c not in "0123456789." for c in ip):
|
|
return {"error": "invalid IP"}
|
|
rc, out, err = ssh_exec(server, f"{s}iptables -C SCANNER-BLOCK -s {ip} -j DROP 2>/dev/null || {s}iptables -A SCANNER-BLOCK -s {ip} -j DROP")
|
|
if rc != 0:
|
|
return {"error": (err or out).strip()}
|
|
return {"status": "blocked", "ip": ip}
|
|
|
|
|
|
def unblock_ip(server, ip):
|
|
s = _sudo(server)
|
|
if not ip or any(c not in "0123456789." for c in ip):
|
|
return {"error": "invalid IP"}
|
|
rc, out, err = ssh_exec(server, f"{s}iptables -D SCANNER-BLOCK -s {ip} -j DROP 2>/dev/null")
|
|
if rc != 0:
|
|
return {"error": (err or out).strip()}
|
|
return {"status": "unblocked", "ip": ip}
|
|
|
|
|
|
def toggle_country(server, enabled):
|
|
s = _sudo(server)
|
|
rule_in = f"{s}iptables -C INPUT -m set --match-set blocked_countries src -j DROP 2>/dev/null"
|
|
rule_du = f"{s}iptables -C DOCKER-USER -m set --match-set blocked_countries src -j DROP 2>/dev/null"
|
|
add_in = f"{s}iptables -I INPUT 1 -m set --match-set blocked_countries src -j DROP"
|
|
add_du = f"{s}iptables -I DOCKER-USER 1 -m set --match-set blocked_countries src -j DROP"
|
|
del_in = f"{s}iptables -D INPUT -m set --match-set blocked_countries src -j DROP 2>/dev/null"
|
|
del_du = f"{s}iptables -D DOCKER-USER -m set --match-set blocked_countries src -j DROP 2>/dev/null"
|
|
if enabled:
|
|
cmd = f"({rule_in}) || {add_in}; ({rule_du}) || {add_du}"
|
|
else:
|
|
cmd = f"{del_in}; {del_du}"
|
|
rc, out, err = ssh_exec(server, cmd)
|
|
if rc != 0:
|
|
return {"error": (err or out).strip()}
|
|
return {"status": "enabled" if enabled else "disabled", "type": "country"}
|
|
|
|
|
|
def toggle_scanner(server, enabled):
|
|
s = _sudo(server)
|
|
rule_in = f"{s}iptables -C INPUT -j SCANNER-BLOCK 2>/dev/null"
|
|
rule_du = f"{s}iptables -C DOCKER-USER -j SCANNER-BLOCK 2>/dev/null"
|
|
add_in = f"{s}iptables -I INPUT 1 -j SCANNER-BLOCK"
|
|
add_du = f"{s}iptables -I DOCKER-USER 1 -j SCANNER-BLOCK"
|
|
del_in = f"{s}iptables -D INPUT -j SCANNER-BLOCK 2>/dev/null"
|
|
del_du = f"{s}iptables -D DOCKER-USER -j SCANNER-BLOCK 2>/dev/null"
|
|
if enabled:
|
|
cmd = f"({rule_in}) || {add_in}; ({rule_du}) || {add_du}"
|
|
else:
|
|
cmd = f"{del_in}; {del_du}"
|
|
rc, out, err = ssh_exec(server, cmd)
|
|
if rc != 0:
|
|
return {"error": (err or out).strip()}
|
|
return {"status": "enabled" if enabled else "disabled", "type": "scanner"}
|