Own the network: details, VPN, enterprise Wi-Fi, and a firewall that can also allow
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
@@ -13,6 +13,7 @@ publish on all interfaces.
|
||||
Changes go through firewall-cmd, which is polkit-aware, so they prompt.
|
||||
|
||||
panama-firewall snapshot
|
||||
panama-firewall zone-info ZONE
|
||||
panama-firewall add-service NAME | remove-service NAME
|
||||
panama-firewall add-port PORT/PROTO | remove-port PORT/PROTO
|
||||
panama-firewall set-zone INTERFACE ZONE
|
||||
@@ -305,6 +306,57 @@ def snapshot() -> dict:
|
||||
}
|
||||
|
||||
|
||||
# Zone targets, as a sentence rather than firewalld's vocabulary. "default" is
|
||||
# the one that catches people out: it does not mean "the default zone", it means
|
||||
# "reject anything no rule allowed", which is the answer someone browsing zones
|
||||
# is actually looking for.
|
||||
TARGETS = {
|
||||
"": "anything no rule allows is rejected",
|
||||
"default": "anything no rule allows is rejected",
|
||||
"%%REJECT%%": "anything no rule allows is rejected, with a refusal sent back",
|
||||
"REJECT": "anything no rule allows is rejected, with a refusal sent back",
|
||||
"DROP": "anything no rule allows is dropped without an answer",
|
||||
"ACCEPT": "anything not explicitly blocked is allowed in",
|
||||
}
|
||||
|
||||
|
||||
def zone_info(name: str) -> dict:
|
||||
"""One zone, described -- read-only, for browsing before choosing.
|
||||
|
||||
Separate from `snapshot` because the zone browser asks about zones this
|
||||
machine is not using, and a snapshot only ever describes the active ones.
|
||||
Nothing here changes anything, so it needs no authorization and no confirm.
|
||||
"""
|
||||
require(ZONE, name, "That is not a zone.")
|
||||
known = firewall("--get-zones").split()
|
||||
if name not in known:
|
||||
raise BoundaryError("There is no zone by that name.")
|
||||
|
||||
detail = zone_detail(name)
|
||||
if not detail:
|
||||
raise BoundaryError("That zone could not be read.")
|
||||
|
||||
services = detail.get("services", [])
|
||||
ports = detail.get("ports", [])
|
||||
parts = []
|
||||
parts.append(f"{len(services)} service{'' if len(services) == 1 else 's'}")
|
||||
parts.append(f"{len(ports)} port rule{'' if len(ports) == 1 else 's'}")
|
||||
summary = ", ".join(parts) + "; " + TARGETS.get(
|
||||
detail.get("target", ""), "custom handling for anything no rule allows")
|
||||
|
||||
return {
|
||||
"zone": name,
|
||||
"services": services,
|
||||
"ports": ports,
|
||||
"interfaces": detail.get("interfaces", []),
|
||||
"target": detail.get("target", ""),
|
||||
"richRules": detail.get("richRules", []),
|
||||
"isDefault": name == firewall("--get-default-zone"),
|
||||
"summary": summary,
|
||||
"error": "",
|
||||
}
|
||||
|
||||
|
||||
def require(pattern: re.Pattern, value: str, message: str) -> str:
|
||||
if not pattern.fullmatch(value or ""):
|
||||
raise BoundaryError(message)
|
||||
@@ -333,6 +385,18 @@ def main(arguments: list[str]) -> int:
|
||||
print(json.dumps(snapshot(), separators=(",", ":")))
|
||||
return 0
|
||||
|
||||
# Read-only, so it answers with its own shape rather than a snapshot --
|
||||
# the browser wants one zone described, not the machine's exposure.
|
||||
if len(arguments) == 2 and arguments[0] == "zone-info":
|
||||
try:
|
||||
answer = zone_info(arguments[1])
|
||||
except BoundaryError as error:
|
||||
answer = {"zone": arguments[1], "services": [], "ports": [],
|
||||
"interfaces": [], "target": "", "richRules": [],
|
||||
"isDefault": False, "summary": "", "error": str(error)}
|
||||
print(json.dumps(answer, separators=(",", ":")))
|
||||
return 0
|
||||
|
||||
if len(arguments) == 2 and arguments[0] in ("add-service", "remove-service"):
|
||||
name = require(SERVICE, arguments[1], "That is not a service name.")
|
||||
verb = "--add-service" if arguments[0] == "add-service" else "--remove-service"
|
||||
@@ -351,9 +415,9 @@ def main(arguments: list[str]) -> int:
|
||||
firewall(f"--set-default-zone={zone}", timeout=120)
|
||||
else:
|
||||
raise BoundaryError(
|
||||
"Usage: panama-firewall snapshot | add-service NAME | remove-service NAME | "
|
||||
"add-port PORT/PROTO | remove-port PORT/PROTO | set-zone INTERFACE ZONE | "
|
||||
"set-default-zone ZONE")
|
||||
"Usage: panama-firewall snapshot | zone-info ZONE | add-service NAME | "
|
||||
"remove-service NAME | add-port PORT/PROTO | remove-port PORT/PROTO | "
|
||||
"set-zone INTERFACE ZONE | set-default-zone ZONE")
|
||||
except BoundaryError as error:
|
||||
try:
|
||||
state = snapshot()
|
||||
|
||||
Reference in New Issue
Block a user