Firewall refactor
This commit is contained in:
@@ -2,6 +2,7 @@ import asyncio
|
||||
from modules.firewall.nftables import FiregexTables
|
||||
from modules.firewall.models import Rule
|
||||
from utils.sqlite import SQLite
|
||||
from modules.firewall.models import Action
|
||||
|
||||
nft = FiregexTables()
|
||||
|
||||
@@ -25,14 +26,15 @@ class FirewallManager:
|
||||
map(Rule.from_dict, self.db.query('SELECT * FROM rules WHERE active = 1 ORDER BY rule_id;')),
|
||||
policy=self.policy,
|
||||
allow_loopback=self.allow_loopback,
|
||||
allow_established=self.allow_established
|
||||
allow_established=self.allow_established,
|
||||
allow_icmp=self.allow_icmp
|
||||
)
|
||||
else:
|
||||
nft.reset()
|
||||
|
||||
@property
|
||||
def policy(self):
|
||||
return self.db.get("POLICY", "accept")
|
||||
return self.db.get("POLICY", Action.ACCEPT)
|
||||
|
||||
@policy.setter
|
||||
def policy(self, value):
|
||||
@@ -61,6 +63,14 @@ class FirewallManager:
|
||||
@allow_loopback.setter
|
||||
def allow_loopback(self, value):
|
||||
self.db.set("allow_loopback", "1" if value else "0")
|
||||
|
||||
@property
|
||||
def allow_icmp(self):
|
||||
return self.db.get("allow_icmp", "1") == "1"
|
||||
|
||||
@allow_icmp.setter
|
||||
def allow_icmp(self, value):
|
||||
self.db.set("allow_icmp", "1" if value else "0")
|
||||
|
||||
@property
|
||||
def allow_established(self):
|
||||
|
||||
@@ -1,27 +1,47 @@
|
||||
from enum import Enum
|
||||
|
||||
class Rule:
|
||||
def __init__(self, proto: str, ip_src:str, ip_dst:str, port_src_from:str, port_dst_from:str, port_src_to:str, port_dst_to:str, action:str, mode:str):
|
||||
def __init__(self, proto: str, src:str, dst:str, port_src_from:str, port_dst_from:str, port_src_to:str, port_dst_to:str, action:str, mode:str):
|
||||
self.proto = proto
|
||||
self.ip_src = ip_src
|
||||
self.ip_dst = ip_dst
|
||||
self.src = src
|
||||
self.dst = dst
|
||||
self.port_src_from = port_src_from
|
||||
self.port_dst_from = port_dst_from
|
||||
self.port_src_to = port_src_to
|
||||
self.port_dst_to = port_dst_to
|
||||
self.action = action
|
||||
self.input_mode = mode in ["I"]
|
||||
self.output_mode = mode in ["O"]
|
||||
self.input_mode = mode == "in"
|
||||
self.output_mode = mode == "out"
|
||||
self.forward_mode = mode == "forward"
|
||||
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, var: dict):
|
||||
return cls(
|
||||
proto=var["proto"],
|
||||
ip_src=var["ip_src"],
|
||||
ip_dst=var["ip_dst"],
|
||||
src=var["src"],
|
||||
dst=var["dst"],
|
||||
port_dst_from=var["port_dst_from"],
|
||||
port_dst_to=var["port_dst_to"],
|
||||
port_src_from=var["port_src_from"],
|
||||
port_src_to=var["port_src_to"],
|
||||
action=var["action"],
|
||||
mode=var["mode"]
|
||||
)
|
||||
)
|
||||
|
||||
class Protocol(str, Enum):
|
||||
TCP = "tcp",
|
||||
UDP = "udp",
|
||||
BOTH = "both",
|
||||
ANY = "any"
|
||||
|
||||
|
||||
class Mode(str, Enum):
|
||||
IN = "in",
|
||||
OUT = "out",
|
||||
FORWARD = "forward"
|
||||
|
||||
class Action(str, Enum):
|
||||
ACCEPT = "accept",
|
||||
DROP = "drop",
|
||||
REJECT = "reject"
|
||||
@@ -1,34 +1,13 @@
|
||||
from modules.firewall.models import Rule
|
||||
from utils import nftables_int_to_json, ip_parse, ip_family, NFTableManager
|
||||
|
||||
|
||||
class FiregexHijackRule():
|
||||
def __init__(self, proto:str, ip_src:str, ip_dst:str, port_src_from:int, port_dst_from:int, port_src_to:int, port_dst_to:int, action:str, target:str, id:int):
|
||||
self.id = id
|
||||
self.target = target
|
||||
self.proto = proto
|
||||
self.ip_src = ip_src
|
||||
self.ip_dst = ip_dst
|
||||
self.port_src_from = min(port_src_from, port_src_to)
|
||||
self.port_dst_from = min(port_dst_from, port_dst_to)
|
||||
self.port_src_to = max(port_src_from, port_src_to)
|
||||
self.port_dst_to = max(port_dst_from, port_dst_to)
|
||||
self.action = action
|
||||
|
||||
def __eq__(self, o: object) -> bool:
|
||||
if isinstance(o, FiregexHijackRule) or isinstance(o, Rule):
|
||||
return self.action == o.action and self.proto == o.proto and\
|
||||
ip_parse(self.ip_src) == ip_parse(o.ip_src) and ip_parse(self.ip_dst) == ip_parse(o.ip_dst) and\
|
||||
int(self.port_src_from) == int(o.port_src_from) and int(self.port_dst_from) == int(o.port_dst_from) and\
|
||||
int(self.port_src_to) == int(o.port_src_to) and int(self.port_dst_to) == int(o.port_dst_to)
|
||||
return False
|
||||
|
||||
from modules.firewall.models import Rule, Protocol, Mode, Action
|
||||
from utils import nftables_int_to_json, ip_family, NFTableManager, is_ip_parse
|
||||
import copy
|
||||
|
||||
class FiregexTables(NFTableManager):
|
||||
rules_chain_in = "firewall_rules_in"
|
||||
rules_chain_out = "firewall_rules_out"
|
||||
rules_chain_fwd = "firewall_rules_fwd"
|
||||
|
||||
def init_comands(self, policy:str="accept", policy_out:str="accept", allow_loopback=False, allow_established=False):
|
||||
def init_comands(self, policy:str=Action.ACCEPT, allow_loopback=False, allow_established=False, allow_icmp=False):
|
||||
return [
|
||||
{"add":{"chain":{
|
||||
"family":"inet",
|
||||
@@ -36,7 +15,16 @@ class FiregexTables(NFTableManager):
|
||||
"name":self.rules_chain_in,
|
||||
"type":"filter",
|
||||
"hook":"prerouting",
|
||||
"prio":-150,
|
||||
"prio":0,
|
||||
"policy":policy
|
||||
}}},
|
||||
{"add":{"chain":{
|
||||
"family":"inet",
|
||||
"table":self.table_name,
|
||||
"name":self.rules_chain_fwd,
|
||||
"type":"filter",
|
||||
"hook":"forward",
|
||||
"prio":0,
|
||||
"policy":policy
|
||||
}}},
|
||||
{"add":{"chain":{
|
||||
@@ -45,24 +33,41 @@ class FiregexTables(NFTableManager):
|
||||
"name":self.rules_chain_out,
|
||||
"type":"filter",
|
||||
"hook":"postrouting",
|
||||
"prio":-150,
|
||||
"policy":policy_out
|
||||
"prio":0,
|
||||
"policy":Action.ACCEPT
|
||||
}}},
|
||||
] + ([
|
||||
{ "add":{ "rule": {
|
||||
"family": "inet", "table": self.table_name, "chain": self.rules_chain_out,
|
||||
"expr": [{ "match": { "op": "==", "left": { "meta": { "key": "iif"}}, "right": "lo"}},{"accept": None}]
|
||||
"expr": [{ "match": { "op": "==", "left": { "meta": { "key": "iif" }}, "right": "lo"}},{"accept": None}]
|
||||
}}},
|
||||
{ "add":{ "rule": {
|
||||
"family": "inet", "table": self.table_name, "chain": self.rules_chain_in,
|
||||
"expr": [{ "match": { "op": "==", "left": { "meta": { "key": "iif"}}, "right": "lo"}},{"accept": None}]
|
||||
"expr": [{ "match": { "op": "==", "left": { "meta": { "key": "iif" }}, "right": "lo"}},{"accept": None}]
|
||||
}}}
|
||||
] if allow_loopback else []) + ([
|
||||
{ "add":{ "rule": {
|
||||
"family": "inet", "table": self.table_name, "chain": self.rules_chain_in,
|
||||
"expr": [{ "match": {"op": "in", "left": { "ct": { "key": "state" }},"right": ["established"]} }, { "accept": None }]
|
||||
}}}
|
||||
] if allow_established else [])
|
||||
] if allow_established else []) + ([
|
||||
{ "add":{ "rule": {
|
||||
"family": "inet", "table": self.table_name, "chain": self.rules_chain_in,
|
||||
"expr": [{ "match": { "op": "==", "left": { "meta": { "key": "l4proto" } }, "right": "icmp"} }, { "accept": None }]
|
||||
}}},
|
||||
{ "add":{ "rule": {
|
||||
"family": "inet", "table": self.table_name, "chain": self.rules_chain_fwd,
|
||||
"expr": [{ "match": { "op": "==", "left": { "meta": { "key": "l4proto" } }, "right": "icmp"} }, { "accept": None }]
|
||||
}}},
|
||||
{ "add":{ "rule": {
|
||||
"family": "inet", "table": self.table_name, "chain": self.rules_chain_in,
|
||||
"expr": [{ "match": { "op": "==", "left": { "meta": { "key": "l4proto" } }, "right": "ipv6-icmp"} }, { "accept": None }]
|
||||
}}},
|
||||
{ "add":{ "rule": {
|
||||
"family": "inet", "table": self.table_name, "chain": self.rules_chain_fwd,
|
||||
"expr": [{ "match": { "op": "==", "left": { "meta": { "key": "l4proto" } }, "right": "ipv6-icmp"} }, { "accept": None }]
|
||||
}}}
|
||||
] if allow_icmp else [])
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(self.init_comands(),[
|
||||
@@ -70,39 +75,57 @@ class FiregexTables(NFTableManager):
|
||||
{"delete":{"chain":{"table":self.table_name,"family":"inet", "name":self.rules_chain_in}}},
|
||||
{"flush":{"chain":{"table":self.table_name,"family":"inet", "name":self.rules_chain_out}}},
|
||||
{"delete":{"chain":{"table":self.table_name,"family":"inet", "name":self.rules_chain_out}}},
|
||||
{"flush":{"chain":{"table":self.table_name,"family":"inet", "name":self.rules_chain_fwd}}},
|
||||
{"delete":{"chain":{"table":self.table_name,"family":"inet", "name":self.rules_chain_fwd}}},
|
||||
])
|
||||
|
||||
def set(self, srvs:list[Rule], policy:str="accept", allow_loopback=False, allow_established=False):
|
||||
def set(self, srvs:list[Rule], policy:str="accept", allow_loopback=False, allow_established=False, allow_icmp=False):
|
||||
srvs = list(srvs)
|
||||
self.reset()
|
||||
if policy == "reject":
|
||||
policy = "drop"
|
||||
if policy == Action.REJECT:
|
||||
policy = Action.DROP
|
||||
srvs.append(Rule(
|
||||
proto="any",
|
||||
ip_src="any",
|
||||
ip_dst="any",
|
||||
proto=Protocol.ANY,
|
||||
src="",
|
||||
dst="",
|
||||
port_src_from=1,
|
||||
port_dst_from=1,
|
||||
port_src_to=65535,
|
||||
port_dst_to=65535,
|
||||
action="reject",
|
||||
mode="I"
|
||||
action=Action.REJECT,
|
||||
mode=Mode.IN
|
||||
))
|
||||
rules = self.init_comands(policy, allow_loopback=allow_loopback, allow_established=allow_established) + self.get_rules(*srvs)
|
||||
rules = self.init_comands(policy, allow_loopback=allow_loopback, allow_established=allow_established, allow_icmp=allow_icmp) + self.get_rules(*srvs)
|
||||
self.cmd(*rules)
|
||||
|
||||
def get_rules(self,*srvs:Rule):
|
||||
rules = []
|
||||
for srv in srvs:
|
||||
final_srvs:list[Rule] = []
|
||||
for ele in srvs:
|
||||
if ele.proto == Protocol.BOTH:
|
||||
udp_rule = copy.deepcopy(ele)
|
||||
udp_rule.proto = Protocol.UDP.value
|
||||
ele.proto = Protocol.TCP.value
|
||||
final_srvs.append(udp_rule)
|
||||
final_srvs.append(ele)
|
||||
|
||||
for srv in final_srvs:
|
||||
ip_filters = []
|
||||
if srv.ip_src.lower() != "any" and srv.ip_dst.lower() != "any":
|
||||
ip_filters = [
|
||||
{'match': {'left': {'payload': {'protocol': ip_family(srv.ip_src), 'field': 'saddr'}}, 'op': '==', 'right': nftables_int_to_json(srv.ip_src)}},
|
||||
{'match': {'left': {'payload': {'protocol': ip_family(srv.ip_dst), 'field': 'daddr'}}, 'op': '==', 'right': nftables_int_to_json(srv.ip_dst)}},
|
||||
]
|
||||
|
||||
if srv.src != "":
|
||||
if is_ip_parse(srv.src):
|
||||
ip_filters.append({'match': {'left': {'payload': {'protocol': ip_family(srv.src), 'field': 'saddr'}}, 'op': '==', 'right': nftables_int_to_json(srv.src)}})
|
||||
else:
|
||||
ip_filters.append({"match": { "op": "==", "left": { "meta": { "key": "iifname" } }, "right": srv.src} })
|
||||
|
||||
if srv.dst != "":
|
||||
if is_ip_parse(srv.dst):
|
||||
ip_filters.append({'match': {'left': {'payload': {'protocol': ip_family(srv.dst), 'field': 'daddr'}}, 'op': '==', 'right': nftables_int_to_json(srv.dst)}})
|
||||
else:
|
||||
ip_filters.append({"match": { "op": "==", "left": { "meta": { "key": "oifname" } }, "right": srv.dst} })
|
||||
|
||||
port_filters = []
|
||||
if srv.proto != "any":
|
||||
if not srv.proto in [Protocol.ANY, Protocol.BOTH]:
|
||||
if srv.port_src_from != 1 or srv.port_src_to != 65535: #Any Port
|
||||
port_filters.append({'match': {'left': {'payload': {'protocol': str(srv.proto), 'field': 'sport'}}, 'op': '>=', 'right': int(srv.port_src_from)}})
|
||||
port_filters.append({'match': {'left': {'payload': {'protocol': str(srv.proto), 'field': 'sport'}}, 'op': '<=', 'right': int(srv.port_src_to)}})
|
||||
@@ -110,13 +133,13 @@ class FiregexTables(NFTableManager):
|
||||
port_filters.append({'match': {'left': {'payload': {'protocol': str(srv.proto), 'field': 'dport'}}, 'op': '>=', 'right': int(srv.port_dst_from)}})
|
||||
port_filters.append({'match': {'left': {'payload': {'protocol': str(srv.proto), 'field': 'dport'}}, 'op': '<=', 'right': int(srv.port_dst_to)}})
|
||||
if len(port_filters) == 0:
|
||||
port_filters.append({'match': {'left': {'payload': {'protocol': str(srv.proto), 'field': 'sport'}}, 'op': '!=', 'right': 0}}) #filter the protocol if no port is specified
|
||||
port_filters.append({'match': {'left': {'meta': {'key': 'l4proto'}}, 'op': '==', 'right': srv.proto}}) #filter the protocol if no port is specified
|
||||
|
||||
end_rules = [{'accept': None} if srv.action == "accept" else {'reject': {}} if (srv.action == "reject" and not srv.output_mode) else {'drop': None}]
|
||||
rules.append({ "add":{ "rule": {
|
||||
"family": "inet",
|
||||
"table": self.table_name,
|
||||
"chain": self.rules_chain_out if srv.output_mode else self.rules_chain_in,
|
||||
"chain": self.rules_chain_out if srv.output_mode else self.rules_chain_in if srv.input_mode else self.rules_chain_fwd,
|
||||
"expr": ip_filters + port_filters + end_rules
|
||||
#If srv.output_mode is True, then the rule is in the output chain, so the reject action is not allowed
|
||||
}}})
|
||||
|
||||
Reference in New Issue
Block a user