mirror of
https://github.com/Av0idD4rk/firegex.git
synced 2026-10-23 16:31:00 +00:00
278 lines
9.2 KiB
Python
278 lines
9.2 KiB
Python
import sys
|
|
import os
|
|
import pytest
|
|
|
|
# Ensure backend path is importable
|
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "backend")))
|
|
|
|
from utils import is_interface_name, parse_ip_or_int, is_ip_parse, ip_parse
|
|
from modules.services.models import Address, Service, TRANSPORT, L4, new_id
|
|
from modules.services.nftables import FiregexTables, InstalledRule, resolve_target
|
|
|
|
|
|
def test_is_interface_name():
|
|
# Valid interface names
|
|
assert is_interface_name("eth0") is True
|
|
assert is_interface_name("lo") is True
|
|
assert is_interface_name("wg0") is True
|
|
assert is_interface_name("tun0") is True
|
|
assert is_interface_name("enp3s0") is True
|
|
assert is_interface_name("br-lan") is True
|
|
assert is_interface_name("veth123456") is True
|
|
assert is_interface_name("eth0.100") is True
|
|
|
|
# Invalid interface names
|
|
assert is_interface_name("") is False
|
|
assert is_interface_name(" ") is False
|
|
assert is_interface_name("a" * 16) is False # > 15 chars
|
|
assert is_interface_name("eth 0") is False # space
|
|
assert is_interface_name("eth/0") is False # slash
|
|
assert is_interface_name(123) is False # not a string
|
|
|
|
|
|
def test_parse_ip_or_int():
|
|
# Valid IPs should be normalized to network CIDR
|
|
assert parse_ip_or_int("127.0.0.1") == "127.0.0.1/32"
|
|
assert parse_ip_or_int("192.168.1.0/24") == "192.168.1.0/24"
|
|
assert parse_ip_or_int("::1") == "::1/128"
|
|
|
|
# Valid interfaces should return the trimmed name
|
|
assert parse_ip_or_int("eth0") == "eth0"
|
|
assert parse_ip_or_int(" wg0 ") == "wg0"
|
|
assert parse_ip_or_int("lo") == "lo"
|
|
|
|
# Invalid values should raise ValueError
|
|
with pytest.raises(ValueError):
|
|
parse_ip_or_int("not a valid name with spaces")
|
|
with pytest.raises(ValueError):
|
|
parse_ip_or_int("this_interface_name_is_way_too_long_for_linux")
|
|
|
|
|
|
def test_address_model_interface():
|
|
addr_ip = Address(
|
|
address_id=new_id(),
|
|
service_id=new_id(),
|
|
ip_int="192.168.1.1/32",
|
|
port=80,
|
|
proto=L4.TCP,
|
|
)
|
|
assert addr_ip.is_interface is False
|
|
assert addr_ip.is_ipv6 is False
|
|
|
|
addr_ip6 = Address(
|
|
address_id=new_id(),
|
|
service_id=new_id(),
|
|
ip_int="fd00::1/128",
|
|
port=80,
|
|
proto=L4.TCP,
|
|
)
|
|
assert addr_ip6.is_interface is False
|
|
assert addr_ip6.is_ipv6 is True
|
|
|
|
addr_iface = Address(
|
|
address_id=new_id(),
|
|
service_id=new_id(),
|
|
ip_int="eth0",
|
|
port=80,
|
|
proto=L4.TCP,
|
|
)
|
|
assert addr_iface.is_interface is True
|
|
assert addr_iface.is_ipv6 is False
|
|
|
|
|
|
def test_nftables_match_generation():
|
|
table = FiregexTables.__new__(FiregexTables)
|
|
|
|
# IP match (inbound daddr)
|
|
m_ip_in = table._match("127.0.0.1/32", 80, "ip", "tcp", "daddr", "dport")
|
|
assert len(m_ip_in) == 2
|
|
assert "payload" in m_ip_in[0]["match"]["left"]
|
|
assert m_ip_in[0]["match"]["left"]["payload"]["field"] == "daddr"
|
|
assert m_ip_in[1]["match"]["right"] == 80
|
|
|
|
# Interface match (inbound daddr -> iifname)
|
|
m_iface_in = table._match("eth0", 80, "ip", "tcp", "daddr", "dport")
|
|
assert len(m_iface_in) == 2
|
|
assert "meta" in m_iface_in[0]["match"]["left"]
|
|
assert m_iface_in[0]["match"]["left"]["meta"]["key"] == "iifname"
|
|
assert m_iface_in[0]["match"]["right"] == "eth0"
|
|
assert m_iface_in[1]["match"]["left"]["payload"]["field"] == "dport"
|
|
assert m_iface_in[1]["match"]["right"] == 80
|
|
|
|
# Interface match (outbound saddr -> oifname)
|
|
m_iface_out = table._match("eth0", 80, "ip", "tcp", "saddr", "sport")
|
|
assert len(m_iface_out) == 2
|
|
assert "meta" in m_iface_out[0]["match"]["left"]
|
|
assert m_iface_out[0]["match"]["left"]["meta"]["key"] == "oifname"
|
|
assert m_iface_out[0]["match"]["right"] == "eth0"
|
|
assert m_iface_out[1]["match"]["left"]["payload"]["field"] == "sport"
|
|
assert m_iface_out[1]["match"]["right"] == 80
|
|
|
|
|
|
def test_installed_rule_matches():
|
|
srv = Service(
|
|
service_id="srv-1",
|
|
name="web",
|
|
status="active",
|
|
proto=L4.TCP,
|
|
transport=TRANSPORT.PROXY,
|
|
)
|
|
|
|
addr_ip = Address(address_id="a1", service_id="srv-1", ip_int="10.0.0.1/32", port=80, proto=L4.TCP)
|
|
addr_iface = Address(address_id="a2", service_id="srv-1", ip_int="eth0", port=80, proto=L4.TCP)
|
|
|
|
rule_ip = InstalledRule(
|
|
chain="fgex_nat", handle=10, proto="tcp", port=80, ip_int="10.0.0.1/32"
|
|
)
|
|
rule_iface = InstalledRule(
|
|
chain="fgex_nat", handle=11, proto="tcp", port=80, ip_int="eth0"
|
|
)
|
|
|
|
# IP rule matches IP address, not interface
|
|
assert rule_ip.matches(srv, addr_ip) is True
|
|
assert rule_ip.matches(srv, addr_iface) is False
|
|
|
|
# Interface rule matches interface address, not IP
|
|
assert rule_iface.matches(srv, addr_iface) is True
|
|
assert rule_iface.matches(srv, addr_ip) is False
|
|
|
|
# Mismatched port or proto
|
|
addr_diff_port = Address(address_id="a3", service_id="srv-1", ip_int="eth0", port=8080, proto=L4.TCP)
|
|
assert rule_iface.matches(srv, addr_diff_port) is False
|
|
|
|
addr_diff_proto = Address(address_id="a4", service_id="srv-1", ip_int="eth0", port=80, proto=L4.UDP)
|
|
assert rule_iface.matches(srv, addr_diff_proto) is False
|
|
|
|
|
|
def test_installed_rule_parsing():
|
|
# Verify parsing simulated nftables json rule with meta iifname
|
|
raw_rule = {
|
|
"chain": "fgex_nat",
|
|
"handle": 42,
|
|
"expr": [
|
|
{
|
|
"match": {
|
|
"op": "!=",
|
|
"left": {"meta": {"key": "mark"}},
|
|
"right": 0x133A,
|
|
}
|
|
},
|
|
{
|
|
"match": {
|
|
"op": "==",
|
|
"left": {"meta": {"key": "iifname"}},
|
|
"right": "eth0",
|
|
}
|
|
},
|
|
{
|
|
"match": {
|
|
"op": "==",
|
|
"left": {"payload": {"protocol": "tcp", "field": "dport"}},
|
|
"right": 80,
|
|
}
|
|
},
|
|
{"counter": {"packets": 12, "bytes": 1024}},
|
|
{"redirect": {"port": 38472}},
|
|
]
|
|
}
|
|
|
|
# Simulate get() parsing logic
|
|
table = FiregexTables.__new__(FiregexTables)
|
|
table.list_rules = lambda tables, chains: [raw_rule]
|
|
rules = table.get()
|
|
assert len(rules) == 1
|
|
r = rules[0]
|
|
assert r.chain == "fgex_nat"
|
|
assert r.handle == 42
|
|
assert r.ip_int == "eth0"
|
|
assert r.port == 80
|
|
assert r.proto == "tcp"
|
|
assert r.packets == 12
|
|
assert r.bytes == 1024
|
|
|
|
|
|
def test_proxy_rule_generation_interface_vs_ip():
|
|
table = FiregexTables.__new__(FiregexTables)
|
|
commands = []
|
|
table.cmd = lambda *cmds: commands.extend(cmds)
|
|
|
|
srv = Service(
|
|
service_id="srv-test",
|
|
name="web",
|
|
status="active",
|
|
proto=L4.TCP,
|
|
transport=TRANSPORT.PROXY,
|
|
)
|
|
|
|
# 1. Interface target
|
|
commands.clear()
|
|
table._add_proxy(srv, "eth0", 80, "ip", "tcp", 38000)
|
|
# Should only write to nat_chain and route_chain (NOT nat_output_chain)
|
|
assert len(commands) == 2
|
|
chains = [c["add"]["rule"]["chain"] for c in commands]
|
|
assert "fgex_nat" in chains
|
|
assert "fgex_route" in chains
|
|
assert "fgex_nat_out" not in chains
|
|
|
|
# Check expressions in nat rule
|
|
nat_rule = [c["add"]["rule"] for c in commands if c["add"]["rule"]["chain"] == "fgex_nat"][0]
|
|
assert any(
|
|
e.get("match", {}).get("left", {}).get("meta", {}).get("key") == "iifname"
|
|
and e["match"]["right"] == "eth0"
|
|
for e in nat_rule["expr"]
|
|
)
|
|
|
|
# Check expressions in route rule
|
|
route_rule = [c["add"]["rule"] for c in commands if c["add"]["rule"]["chain"] == "fgex_route"][0]
|
|
assert any(
|
|
e.get("match", {}).get("left", {}).get("meta", {}).get("key") == "oifname"
|
|
and e["match"]["right"] == "eth0"
|
|
for e in route_rule["expr"]
|
|
)
|
|
|
|
# 2. IP target
|
|
commands.clear()
|
|
table._add_proxy(srv, "127.0.0.1/32", 80, "ip", "tcp", 38000)
|
|
# Should write to nat_chain, route_chain, AND nat_output_chain
|
|
assert len(commands) == 3
|
|
chains_ip = [c["add"]["rule"]["chain"] for c in commands]
|
|
assert "fgex_nat" in chains_ip
|
|
assert "fgex_route" in chains_ip
|
|
assert "fgex_nat_out" in chains_ip
|
|
|
|
|
|
def test_queue_rule_generation_interface():
|
|
table = FiregexTables.__new__(FiregexTables)
|
|
commands = []
|
|
table.cmd = lambda *cmds: commands.extend(cmds)
|
|
|
|
srv = Service(
|
|
service_id="srv-test-q",
|
|
name="dns",
|
|
status="active",
|
|
proto=L4.UDP,
|
|
transport=TRANSPORT.NFQUEUE,
|
|
)
|
|
|
|
commands.clear()
|
|
table._add_queue(srv, "wg0", 53, "ip", "udp", [0])
|
|
assert len(commands) == 2
|
|
chains = [c["insert"]["rule"]["chain"] for c in commands]
|
|
assert "fgex_queue_out_0" in chains
|
|
assert "fgex_queue_in_0" in chains
|
|
|
|
in_rule = [c["insert"]["rule"] for c in commands if c["insert"]["rule"]["chain"] == "fgex_queue_in_0"][0]
|
|
assert any(
|
|
e.get("match", {}).get("left", {}).get("meta", {}).get("key") == "iifname"
|
|
and e["match"]["right"] == "wg0"
|
|
for e in in_rule["expr"]
|
|
)
|
|
|
|
out_rule = [c["insert"]["rule"] for c in commands if c["insert"]["rule"]["chain"] == "fgex_queue_out_0"][0]
|
|
assert any(
|
|
e.get("match", {}).get("left", {}).get("meta", {}).get("key") == "oifname"
|
|
and e["match"]["right"] == "wg0"
|
|
for e in out_rule["expr"]
|
|
)
|
|
|