Firewall refactor

This commit is contained in:
Domingo Dirutigliano
2023-09-28 20:45:58 +02:00
parent 99e4989cfe
commit 71edfc29c4
12 changed files with 212 additions and 166 deletions

View File

@@ -154,7 +154,7 @@ if __name__ == '__main__':
os.chdir(os.path.dirname(os.path.realpath(__file__))) os.chdir(os.path.dirname(os.path.realpath(__file__)))
uvicorn.run( uvicorn.run(
"app:app", "app:app",
host=None, host="0.0.0.0",
port=FIREGEX_PORT, port=FIREGEX_PORT,
reload=DEBUG, reload=DEBUG,
access_log=True, access_log=True,

View File

@@ -2,6 +2,7 @@ import asyncio
from modules.firewall.nftables import FiregexTables from modules.firewall.nftables import FiregexTables
from modules.firewall.models import Rule from modules.firewall.models import Rule
from utils.sqlite import SQLite from utils.sqlite import SQLite
from modules.firewall.models import Action
nft = FiregexTables() 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;')), map(Rule.from_dict, self.db.query('SELECT * FROM rules WHERE active = 1 ORDER BY rule_id;')),
policy=self.policy, policy=self.policy,
allow_loopback=self.allow_loopback, allow_loopback=self.allow_loopback,
allow_established=self.allow_established allow_established=self.allow_established,
allow_icmp=self.allow_icmp
) )
else: else:
nft.reset() nft.reset()
@property @property
def policy(self): def policy(self):
return self.db.get("POLICY", "accept") return self.db.get("POLICY", Action.ACCEPT)
@policy.setter @policy.setter
def policy(self, value): def policy(self, value):
@@ -62,6 +64,14 @@ class FirewallManager:
def allow_loopback(self, value): def allow_loopback(self, value):
self.db.set("allow_loopback", "1" if value else "0") 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 @property
def allow_established(self): def allow_established(self):
return self.db.get("allow_established", "1") == "1" return self.db.get("allow_established", "1") == "1"

View File

@@ -1,23 +1,26 @@
from enum import Enum
class Rule: 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.proto = proto
self.ip_src = ip_src self.src = src
self.ip_dst = ip_dst self.dst = dst
self.port_src_from = port_src_from self.port_src_from = port_src_from
self.port_dst_from = port_dst_from self.port_dst_from = port_dst_from
self.port_src_to = port_src_to self.port_src_to = port_src_to
self.port_dst_to = port_dst_to self.port_dst_to = port_dst_to
self.action = action self.action = action
self.input_mode = mode in ["I"] self.input_mode = mode == "in"
self.output_mode = mode in ["O"] self.output_mode = mode == "out"
self.forward_mode = mode == "forward"
@classmethod @classmethod
def from_dict(cls, var: dict): def from_dict(cls, var: dict):
return cls( return cls(
proto=var["proto"], proto=var["proto"],
ip_src=var["ip_src"], src=var["src"],
ip_dst=var["ip_dst"], dst=var["dst"],
port_dst_from=var["port_dst_from"], port_dst_from=var["port_dst_from"],
port_dst_to=var["port_dst_to"], port_dst_to=var["port_dst_to"],
port_src_from=var["port_src_from"], port_src_from=var["port_src_from"],
@@ -25,3 +28,20 @@ class Rule:
action=var["action"], action=var["action"],
mode=var["mode"] 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"

View File

@@ -1,34 +1,13 @@
from modules.firewall.models import Rule from modules.firewall.models import Rule, Protocol, Mode, Action
from utils import nftables_int_to_json, ip_parse, ip_family, NFTableManager from utils import nftables_int_to_json, ip_family, NFTableManager, is_ip_parse
import copy
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
class FiregexTables(NFTableManager): class FiregexTables(NFTableManager):
rules_chain_in = "firewall_rules_in" rules_chain_in = "firewall_rules_in"
rules_chain_out = "firewall_rules_out" 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 [ return [
{"add":{"chain":{ {"add":{"chain":{
"family":"inet", "family":"inet",
@@ -36,7 +15,16 @@ class FiregexTables(NFTableManager):
"name":self.rules_chain_in, "name":self.rules_chain_in,
"type":"filter", "type":"filter",
"hook":"prerouting", "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 "policy":policy
}}}, }}},
{"add":{"chain":{ {"add":{"chain":{
@@ -45,24 +33,41 @@ class FiregexTables(NFTableManager):
"name":self.rules_chain_out, "name":self.rules_chain_out,
"type":"filter", "type":"filter",
"hook":"postrouting", "hook":"postrouting",
"prio":-150, "prio":0,
"policy":policy_out "policy":Action.ACCEPT
}}}, }}},
] + ([ ] + ([
{ "add":{ "rule": { { "add":{ "rule": {
"family": "inet", "table": self.table_name, "chain": self.rules_chain_out, "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": { { "add":{ "rule": {
"family": "inet", "table": self.table_name, "chain": self.rules_chain_in, "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 []) + ([ ] if allow_loopback else []) + ([
{ "add":{ "rule": { { "add":{ "rule": {
"family": "inet", "table": self.table_name, "chain": self.rules_chain_in, "family": "inet", "table": self.table_name, "chain": self.rules_chain_in,
"expr": [{ "match": {"op": "in", "left": { "ct": { "key": "state" }},"right": ["established"]} }, { "accept": None }] "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): def __init__(self):
super().__init__(self.init_comands(),[ 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}}}, {"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}}}, {"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}}}, {"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) srvs = list(srvs)
self.reset() self.reset()
if policy == "reject": if policy == Action.REJECT:
policy = "drop" policy = Action.DROP
srvs.append(Rule( srvs.append(Rule(
proto="any", proto=Protocol.ANY,
ip_src="any", src="",
ip_dst="any", dst="",
port_src_from=1, port_src_from=1,
port_dst_from=1, port_dst_from=1,
port_src_to=65535, port_src_to=65535,
port_dst_to=65535, port_dst_to=65535,
action="reject", action=Action.REJECT,
mode="I" 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) self.cmd(*rules)
def get_rules(self,*srvs:Rule): def get_rules(self,*srvs:Rule):
rules = [] 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 = [] ip_filters = []
if srv.ip_src.lower() != "any" and srv.ip_dst.lower() != "any":
ip_filters = [ if srv.src != "":
{'match': {'left': {'payload': {'protocol': ip_family(srv.ip_src), 'field': 'saddr'}}, 'op': '==', 'right': nftables_int_to_json(srv.ip_src)}}, if is_ip_parse(srv.src):
{'match': {'left': {'payload': {'protocol': ip_family(srv.ip_dst), 'field': 'daddr'}}, 'op': '==', 'right': nftables_int_to_json(srv.ip_dst)}}, 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 = [] 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 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_from)}})
port_filters.append({'match': {'left': {'payload': {'protocol': str(srv.proto), 'field': 'sport'}}, 'op': '<=', 'right': int(srv.port_src_to)}}) 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_from)}})
port_filters.append({'match': {'left': {'payload': {'protocol': str(srv.proto), 'field': 'dport'}}, 'op': '<=', 'right': int(srv.port_dst_to)}}) port_filters.append({'match': {'left': {'payload': {'protocol': str(srv.proto), 'field': 'dport'}}, 'op': '<=', 'right': int(srv.port_dst_to)}})
if len(port_filters) == 0: 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}] 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": { rules.append({ "add":{ "rule": {
"family": "inet", "family": "inet",
"table": self.table_name, "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 "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 #If srv.output_mode is True, then the rule is in the output chain, so the reject action is not allowed
}}}) }}})

View File

@@ -6,27 +6,29 @@ from utils import ip_parse, ip_family, socketio_emit, PortType
from utils.models import ResetRequest, StatusMessageModel from utils.models import ResetRequest, StatusMessageModel
from modules.firewall.nftables import FiregexTables from modules.firewall.nftables import FiregexTables
from modules.firewall.firewall import FirewallManager from modules.firewall.firewall import FirewallManager
from modules.firewall.models import Protocol, Mode, Action
class RuleModel(BaseModel): class RuleModel(BaseModel):
active: bool active: bool
name: str name: str
proto: str proto: Protocol
ip_src: str src: str
ip_dst: str dst: str
port_src_from: PortType port_src_from: PortType
port_dst_from: PortType port_dst_from: PortType
port_src_to: PortType port_src_to: PortType
port_dst_to: PortType port_dst_to: PortType
action: str action: Action
mode:str mode:Mode
class RuleFormAdd(BaseModel): class RuleFormAdd(BaseModel):
rules: list[RuleModel] rules: list[RuleModel]
policy: str policy: Action
class RuleInfo(BaseModel): class RuleInfo(BaseModel):
rules: list[RuleModel] rules: list[RuleModel]
policy: str policy: Action
enabled: bool enabled: bool
class RenameForm(BaseModel): class RenameForm(BaseModel):
@@ -36,29 +38,30 @@ class FirewallSettings(BaseModel):
keep_rules: bool keep_rules: bool
allow_loopback: bool allow_loopback: bool
allow_established: bool allow_established: bool
allow_icmp: bool
app = APIRouter()
db = SQLite('db/firewall-rules.db', { db = SQLite('db/firewall-rules.db', {
'rules': { 'rules': {
'rule_id': 'INT PRIMARY KEY CHECK (rule_id >= 0)', 'rule_id': 'INT PRIMARY KEY CHECK (rule_id >= 0)',
'mode': 'VARCHAR(1) NOT NULL CHECK (mode IN ("O", "I"))', # O = out, I = in, B = both 'mode': 'VARCHAR(10) NOT NULL CHECK (mode IN ("in", "out", "forward"))',
'name': 'VARCHAR(100) NOT NULL', 'name': 'VARCHAR(100) NOT NULL',
'active' : 'BOOLEAN NOT NULL CHECK (active IN (0, 1))', 'active' : 'BOOLEAN NOT NULL CHECK (active IN (0, 1))',
'proto': 'VARCHAR(3) NOT NULL CHECK (proto IN ("tcp", "udp", "any"))', 'proto': 'VARCHAR(10) NOT NULL CHECK (proto IN ("tcp", "udp", "both", "any"))',
'ip_src': 'VARCHAR(100) NOT NULL', 'src': 'VARCHAR(100) NOT NULL',
'port_src_from': 'INT CHECK(port_src_from > 0 and port_src_from < 65536)', 'port_src_from': 'INT CHECK(port_src_from > 0 and port_src_from < 65536)',
'port_src_to': 'INT CHECK(port_src_to > 0 and port_src_to < 65536 and port_src_from <= port_src_to)', 'port_src_to': 'INT CHECK(port_src_to > 0 and port_src_to < 65536 and port_src_from <= port_src_to)',
'ip_dst': 'VARCHAR(100) NOT NULL', 'dst': 'VARCHAR(100) NOT NULL',
'port_dst_from': 'INT CHECK(port_dst_from > 0 and port_dst_from < 65536)', 'port_dst_from': 'INT CHECK(port_dst_from > 0 and port_dst_from < 65536)',
'port_dst_to': 'INT CHECK(port_dst_to > 0 and port_dst_to < 65536 and port_dst_from <= port_dst_to)', 'port_dst_to': 'INT CHECK(port_dst_to > 0 and port_dst_to < 65536 and port_dst_from <= port_dst_to)',
'action': 'VARCHAR(10) NOT NULL CHECK (action IN ("accept", "drop", "reject"))', 'action': 'VARCHAR(10) NOT NULL CHECK (action IN ("accept", "drop", "reject"))',
}, },
'QUERY':[ 'QUERY':[
"CREATE UNIQUE INDEX IF NOT EXISTS unique_rules ON rules (proto, ip_src, ip_dst, port_src_from, port_src_to, port_dst_from, port_dst_to, mode);" "CREATE UNIQUE INDEX IF NOT EXISTS unique_rules ON rules (proto, src, dst, port_src_from, port_src_to, port_dst_from, port_dst_to, mode);"
] ]
}) })
app = APIRouter()
firewall = FirewallManager(db) firewall = FirewallManager(db)
async def reset(params: ResetRequest): async def reset(params: ResetRequest):
@@ -101,7 +104,8 @@ async def get_settings():
return { return {
"keep_rules": firewall.keep_rules, "keep_rules": firewall.keep_rules,
"allow_loopback": firewall.allow_loopback, "allow_loopback": firewall.allow_loopback,
"allow_established": firewall.allow_established "allow_established": firewall.allow_established,
"allow_icmp": firewall.allow_icmp
} }
@app.post("/settings/set", response_model=StatusMessageModel) @app.post("/settings/set", response_model=StatusMessageModel)
@@ -110,14 +114,15 @@ async def set_settings(form: FirewallSettings):
firewall.keep_rules = form.keep_rules firewall.keep_rules = form.keep_rules
firewall.allow_loopback = form.allow_loopback firewall.allow_loopback = form.allow_loopback
firewall.allow_established = form.allow_established firewall.allow_established = form.allow_established
return {'status': 'ok'} firewall.allow_icmp = form.allow_icmp
return await apply_changes()
@app.get('/rules', response_model=RuleInfo) @app.get('/rules', response_model=RuleInfo)
async def get_rule_list(): async def get_rule_list():
"""Get the list of existent firegex rules""" """Get the list of existent firegex rules"""
return { return {
"policy": firewall.policy, "policy": firewall.policy,
"rules": db.query("SELECT active, name, proto, ip_src, ip_dst, port_src_from, port_dst_from, port_src_to, port_dst_to, action, mode FROM rules ORDER BY rule_id;"), "rules": db.query("SELECT active, name, proto, src, dst, port_src_from, port_dst_from, port_src_to, port_dst_to, action, mode FROM rules ORDER BY rule_id;"),
"enabled": firewall.enabled "enabled": firewall.enabled
} }
@@ -135,31 +140,34 @@ async def disable_firewall():
def parse_and_check_rule(rule:RuleModel): def parse_and_check_rule(rule:RuleModel):
if rule.ip_src.lower().strip() == "any" or rule.ip_dst.lower().split() == "any": is_src_ip = is_dst_ip = True
rule.ip_dst = rule.ip_src = "any"
else: try:
try: rule.src = ip_parse(rule.src)
rule.ip_src = ip_parse(rule.ip_src) except ValueError:
rule.ip_dst = ip_parse(rule.ip_dst) is_src_ip = False
except ValueError:
raise HTTPException(status_code=400, detail="Invalid address") try:
if ip_family(rule.ip_dst) != ip_family(rule.ip_src): rule.dst = ip_parse(rule.dst)
raise HTTPException(status_code=400, detail="Destination and source addresses must be of the same family") except ValueError:
is_dst_ip = False
if not is_src_ip and "/" in rule.src: # Slash is not allowed in ip interfaces names
raise HTTPException(status_code=400, detail="Invalid source address")
if not is_dst_ip and "/" in rule.dst:
raise HTTPException(status_code=400, detail="Invalid destination address")
if is_src_ip and is_dst_ip and ip_family(rule.dst) != ip_family(rule.src):
raise HTTPException(status_code=400, detail="Destination and source addresses must be of the same family")
rule.port_dst_from, rule.port_dst_to = min(rule.port_dst_from, rule.port_dst_to), max(rule.port_dst_from, rule.port_dst_to) rule.port_dst_from, rule.port_dst_to = min(rule.port_dst_from, rule.port_dst_to), max(rule.port_dst_from, rule.port_dst_to)
rule.port_src_from, rule.port_src_to = min(rule.port_src_from, rule.port_src_to), max(rule.port_src_from, rule.port_src_to) rule.port_src_from, rule.port_src_to = min(rule.port_src_from, rule.port_src_to), max(rule.port_src_from, rule.port_src_to)
if rule.proto not in ["tcp", "udp", "any"]:
raise HTTPException(status_code=400, detail="Invalid protocol")
if rule.action not in ["accept", "drop", "reject"]:
raise HTTPException(status_code=400, detail="Invalid action")
return rule return rule
@app.post('/rules/set', response_model=StatusMessageModel) @app.post('/rules/set', response_model=StatusMessageModel)
async def add_new_service(form: RuleFormAdd): async def add_new_service(form: RuleFormAdd):
"""Add a new service""" """Add a new service"""
if form.policy not in ["accept", "drop", "reject"]:
raise HTTPException(status_code=400, detail="Invalid policy")
rules = [parse_and_check_rule(ele) for ele in form.rules] rules = [parse_and_check_rule(ele) for ele in form.rules]
try: try:
db.queries(["DELETE FROM rules"]+ db.queries(["DELETE FROM rules"]+
@@ -167,20 +175,20 @@ async def add_new_service(form: RuleFormAdd):
INSERT INTO rules ( INSERT INTO rules (
rule_id, active, name, rule_id, active, name,
proto, proto,
ip_src, ip_dst, src, dst,
port_src_from, port_dst_from, port_src_from, port_dst_from,
port_src_to, port_dst_to, port_src_to, port_dst_to,
action, mode action, mode
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ? ,?, ?)""", ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ? ,?, ?)""",
rid, ele.active, ele.name, rid, ele.active, ele.name,
ele.proto, ele.proto,
ele.ip_src, ele.ip_dst, ele.src, ele.dst,
ele.port_src_from, ele.port_dst_from, ele.port_src_from, ele.port_dst_from,
ele.port_src_to, ele.port_dst_to, ele.port_src_to, ele.port_dst_to,
ele.action, ele.mode ele.action, ele.mode
) for rid, ele in enumerate(rules)] ) for rid, ele in enumerate(rules)]
) )
firewall.policy = form.policy firewall.policy = form.policy.value
except sqlite3.IntegrityError: except sqlite3.IntegrityError:
raise HTTPException(status_code=400, detail="Error saving the rules: maybe there are duplicated rules") raise HTTPException(status_code=400, detail="Error saving the rules: maybe there are duplicated rules")
return await apply_changes() return await apply_changes()

View File

@@ -70,6 +70,13 @@ def list_files(mypath):
def ip_parse(ip:str): def ip_parse(ip:str):
return str(ip_interface(ip).network) return str(ip_interface(ip).network)
def is_ip_parse(ip:str):
try:
ip_parse(ip)
return True
except Exception:
return False
def addr_parse(ip:str): def addr_parse(ip:str):
return str(ip_address(ip)) return str(ip_address(ip))

View File

@@ -8,11 +8,15 @@ export const ModeSelector = (props:Omit<SegmentedControlProps, "data">) => (
data={[ data={[
{ {
value: RuleMode.IN, value: RuleMode.IN,
label: 'Inbound', label: 'IN',
},
{
value: RuleMode.FORWARD,
label: 'FWD',
}, },
{ {
value: RuleMode.OUT, value: RuleMode.OUT,
label: 'Outbound', label: 'OUT',
} }
]} ]}
size={props.size?props.size:"xs"} size={props.size?props.size:"xs"}

View File

@@ -13,6 +13,10 @@ export const ProtocolSelector = (props:Omit<SegmentedControlProps, "data">) => (
value: Protocol.UDP, value: Protocol.UDP,
label: 'UDP', label: 'UDP',
}, },
{
value: Protocol.BOTH,
label: 'BOTH',
},
{ {
value: Protocol.ANY, value: Protocol.ANY,
label: 'ANY', label: 'ANY',

View File

@@ -5,6 +5,7 @@ import { getapi, postapi } from "../../js/utils"
export enum Protocol { export enum Protocol {
TCP = "tcp", TCP = "tcp",
UDP = "udp", UDP = "udp",
BOTH = "both",
ANY = "any" ANY = "any"
} }
@@ -15,16 +16,17 @@ export enum ActionType {
} }
export enum RuleMode { export enum RuleMode {
OUT = "O", OUT = "out",
IN = "I", IN = "in",
FORWARD = "forward"
} }
export type Rule = { export type Rule = {
active: boolean active: boolean
name:string, name:string,
proto: Protocol, proto: Protocol,
ip_src: string, src: string,
ip_dst: string, dst: string,
port_src_from: number, port_src_from: number,
port_dst_from: number, port_dst_from: number,
port_src_to: number, port_src_to: number,
@@ -48,6 +50,7 @@ export type FirewallSettings = {
keep_rules: boolean, keep_rules: boolean,
allow_loopback: boolean, allow_loopback: boolean,
allow_established: boolean, allow_established: boolean,
allow_icmp: boolean
} }
@@ -74,16 +77,6 @@ export const firewall = {
disable: async() => { disable: async() => {
return await getapi("firewall/disable") as ServerResponse; return await getapi("firewall/disable") as ServerResponse;
}, },
rulenable: async (rule_id:number) => {
return await getapi(`firewall/rule/${rule_id}/enable`) as ServerResponse;
},
ruledisable: async (rule_id:number) => {
return await getapi(`firewall/rule/${rule_id}/disable`) as ServerResponse;
},
rulerename: async (rule_id:number, name: string) => {
const { status } = await postapi(`firewall/rule/${rule_id}/rename`,{ name }) as ServerResponse;
return status === "ok"?undefined:status
},
ruleset: async (data:RuleAddForm) => { ruleset: async (data:RuleAddForm) => {
return await postapi("firewall/rules/set", data) as ServerResponseListed; return await postapi("firewall/rules/set", data) as ServerResponseListed;
} }

View File

@@ -14,19 +14,26 @@ interface ItemProps extends AutocompleteItem {
} }
interface InterfaceInputProps extends Omit<SelectProps, "data">{ interface InterfaceInputProps extends Omit<SelectProps, "data">{
initialCustomInterfaces?:AutocompleteItem[] initialCustomInterfaces?:AutocompleteItem[],
includeInterfaceNames?:boolean
} }
export const InterfaceInput = (props:InterfaceInputProps) => { export const InterfaceInput = ({ initialCustomInterfaces, includeInterfaceNames, ...props }:InterfaceInputProps) => {
const { initialCustomInterfaces, ...propeties } = props
const [customIpInterfaces, setCustomIpInterfaces] = useState<AutocompleteItem[]>(initialCustomInterfaces??[]); const [customIpInterfaces, setCustomIpInterfaces] = useState<AutocompleteItem[]>(initialCustomInterfaces??[]);
const interfacesQuery = ipInterfacesQuery() const interfacesQuery = ipInterfacesQuery()
const interfaces = (!interfacesQuery.isLoading? const getInterfaces = () => {
(interfacesQuery.data!.map(item => ({netint:item.name, value:item.addr, label:item.addr})) as AutocompleteItem[]): if (interfacesQuery.isLoading || !interfacesQuery.data) return []
[]) if(includeInterfaceNames){
const result = interfacesQuery.data.map(item => ({netint:"IP", value:item.addr, label:item.addr})) as AutocompleteItem[]
interfacesQuery.data.map(item => item.name).filter((item, index, arr) => arr.indexOf(item) === index).forEach(item => result.push({netint:"INT", value:item, label:item}))
return result
}
return (interfacesQuery.data.map(item => ({netint:item.name, value:item.addr, label:item.addr})) as AutocompleteItem[])
}
const interfaces = getInterfaces()
return <Select return <Select
placeholder="10.1.1.1" placeholder="10.1.1.1"
@@ -43,6 +50,6 @@ export const InterfaceInput = (props:InterfaceInputProps) => {
return item; return item;
}} }}
style={props.style?{width:"100%", ...props.style}:{width:"100%"}} style={props.style?{width:"100%", ...props.style}:{width:"100%"}}
{...propeties} {...props}
/> />
} }

View File

@@ -5,7 +5,7 @@ import { FirewallSettings, firewall } from '../../components/Firewall/utils';
export function SettingsModal({ opened, onClose }:{ opened:boolean, onClose:()=>void }) { export function SettingsModal({ opened, onClose }:{ opened:boolean, onClose:()=>void }) {
const [settings, setSettings] = useState<FirewallSettings>({keep_rules:false, allow_established:true, allow_loopback:true}) const [settings, setSettings] = useState<FirewallSettings>({keep_rules:false, allow_established:true, allow_loopback:true, allow_icmp:true})
useEffect(()=>{ useEffect(()=>{
firewall.settings().then( res => { firewall.settings().then( res => {
@@ -39,6 +39,7 @@ export function SettingsModal({ opened, onClose }:{ opened:boolean, onClose:()=>
<Space h="md" /> <Space h="md" />
<Switch label="Allow established connection (essential to allow opening connection) (Dangerous to disable)" checked={settings.allow_established} onChange={v => setSettings({...settings, allow_established:v.target.checked})}/> <Switch label="Allow established connection (essential to allow opening connection) (Dangerous to disable)" checked={settings.allow_established} onChange={v => setSettings({...settings, allow_established:v.target.checked})}/>
<Space h="md" /> <Space h="md" />
<Switch label="Allow icmp packets" checked={settings.allow_icmp} onChange={v => setSettings({...settings, allow_icmp:v.target.checked})}/>
<Group position="right" mt="md"> <Group position="right" mt="md">
<Button loading={submitLoading} onClick={submitRequest}>Save Setting</Button> <Button loading={submitLoading} onClick={submitRequest}>Save Setting</Button>

View File

@@ -106,8 +106,8 @@ export const Firewall = () => {
active: true, active: true,
name: "Rule name", name: "Rule name",
proto: Protocol.TCP, proto: Protocol.TCP,
ip_src: "any", src: "",
ip_dst: "any", dst: "",
port_src_from: 1, port_src_from: 1,
port_dst_from: 8080, port_dst_from: 8080,
port_src_to: 65535, port_src_to: 65535,
@@ -140,18 +140,15 @@ export const Firewall = () => {
{(provided, snapshot) => { {(provided, snapshot) => {
const customInt = [ const customInt = [
{ value: "0.0.0.0/0", netint: "ANY IPv4", label: "0.0.0.0/0" }, { value: "0.0.0.0/0", netint: "ANY IPv4", label: "0.0.0.0/0" },
{ value: "::/0", netint: "ANY IPv6", label: "::/0" } { value: "::/0", netint: "ANY IPv6", label: "::/0" },
{ value: "", netint: "ANY", label: "ANY" }
] ]
const src_custom_int = customInt.map(v => v.value).includes(item.ip_src) || item.ip_dst == "any"?[]:[{ value: item.ip_src, netint: "SELECTED", label: item.ip_src }] const src_custom_int = customInt.map(v => v.value).includes(item.src)?[]:[{ value: item.src, netint: "SELECTED", label: item.src }]
const dst_custom_int = customInt.map(v => v.value).includes(item.ip_dst) || item.ip_dst == "any"?[]:[{ value: item.ip_dst, netint: "SELECTED", label: item.ip_dst }] const dst_custom_int = customInt.map(v => v.value).includes(item.dst)?[]:[{ value: item.dst, netint: "SELECTED", label: item.dst }]
const [srcPortEnabled, setSrcPortEnabled] = useState(item.port_src_from != 1 || item.port_src_to != 65535) const [srcPortEnabled, setSrcPortEnabled] = useState(item.port_src_from != 1 || item.port_src_to != 65535)
const [dstPortEnabled, setDstPortEnabled] = useState(item.port_dst_from != 1 || item.port_dst_to != 65535) const [dstPortEnabled, setDstPortEnabled] = useState(item.port_dst_from != 1 || item.port_dst_to != 65535)
const [srcPortValue, setSrcPortValue] = useState(item.port_src_from==item.port_src_to?`${item.port_src_from}`:`${item.port_src_from}-${item.port_src_to}`) const [srcPortValue, setSrcPortValue] = useState(item.port_src_from==item.port_src_to?`${item.port_src_from}`:`${item.port_src_from}-${item.port_src_to}`)
const [dstPortValue, setDstPortValue] = useState(item.port_dst_from==item.port_dst_to?`${item.port_dst_from}`:`${item.port_dst_from}-${item.port_dst_to}`) const [dstPortValue, setDstPortValue] = useState(item.port_dst_from==item.port_dst_to?`${item.port_dst_from}`:`${item.port_dst_from}-${item.port_dst_to}`)
const [ipFilteringEnabled, setIpFilteringEnabled] = useState(!(item.ip_dst == "any" || item.ip_src == "any"))
const [srcIp, setSrcIp] = useState(item.ip_src!="any"?item.ip_src:"")
const [dstIp, setDstIp] = useState(item.ip_dst!="any"?item.ip_dst:"")
const port_range_setter = (rule:Rule, v:string, {src=false, dst=false}:{src?:boolean, dst?:boolean}) => { const port_range_setter = (rule:Rule, v:string, {src=false, dst=false}:{src?:boolean, dst?:boolean}) => {
const elements = v.split("-") const elements = v.split("-")
@@ -173,39 +170,23 @@ export const Firewall = () => {
const ip_setter = (rule:Rule, v:string|null, {src=false, dst=false}:{src?:boolean, dst?:boolean}) => { const ip_setter = (rule:Rule, v:string|null, {src=false, dst=false}:{src?:boolean, dst?:boolean}) => {
const values = v?v:"" const values = v?v:""
if (src){ if (src){
rule.ip_src = values rule.src = values
setSrcIp(values)
} }
if (dst){ if (dst){
rule.ip_dst = values rule.dst = values
setDstIp(values)
} }
updateMe() updateMe()
} }
const set_filtering_ip = (value:boolean) => {
if (!value){
item.ip_src = "any"
item.ip_dst = "any"
}else{
item.ip_src = srcIp
item.ip_dst = dstIp
}
setIpFilteringEnabled(value)
updateMe()
}
const disable_style = { opacity:"0.4", cursor:"not-allowed" } const disable_style = { opacity:"0.4", cursor:"not-allowed" }
const proto_any = item.proto == Protocol.ANY const proto_any = item.proto == Protocol.ANY
const disabletab = { const disabletab = {
ips:!ipFilteringEnabled,
port_box: proto_any, port_box: proto_any,
src_port: !srcPortEnabled || proto_any, src_port: !srcPortEnabled || proto_any,
dst_port: !dstPortEnabled || proto_any dst_port: !dstPortEnabled || proto_any
} }
const additionalStyle = { const additionalStyle = {
ips:disabletab.ips?disable_style:{},
port_box: disabletab.port_box?disable_style:{}, port_box: disabletab.port_box?disable_style:{},
src_port: disabletab.src_port?disable_style:{}, src_port: disabletab.src_port?disable_style:{},
dst_port: disabletab.dst_port?disable_style:{} dst_port: disabletab.dst_port?disable_style:{}
@@ -254,11 +235,9 @@ export const Firewall = () => {
<div style={{width:"100%"}}> <div style={{width:"100%"}}>
<InterfaceInput <InterfaceInput
initialCustomInterfaces={[...src_custom_int, ...customInt]} initialCustomInterfaces={[...src_custom_int, ...customInt]}
value={srcIp} value={item.src}
onChange={v => ip_setter(item, v, {src:true})} onChange={v => ip_setter(item, v, {src:true})}
disabled={disabletab.ips} includeInterfaceNames
error={!disabletab.ips && !srcIp}
style={additionalStyle.ips}
/> />
<Space h="sm" /> <Space h="sm" />
<div className="center-flex" style={{width:"100%"}}> <div className="center-flex" style={{width:"100%"}}>
@@ -289,11 +268,9 @@ export const Firewall = () => {
<div style={{width:"100%"}}> <div style={{width:"100%"}}>
<InterfaceInput <InterfaceInput
initialCustomInterfaces={[...dst_custom_int, ...customInt]} initialCustomInterfaces={[...dst_custom_int, ...customInt]}
defaultValue={dstIp} defaultValue={item.dst}
onChange={v => ip_setter(item, v, {dst:true})} onChange={v => ip_setter(item, v, {dst:true})}
disabled={disabletab.ips} includeInterfaceNames
error={!disabletab.ips && !dstIp}
style={additionalStyle.ips}
/> />
<Space h="sm" /> <Space h="sm" />
<div className="center-flex" style={{width:"100%"}}> <div className="center-flex" style={{width:"100%"}}>
@@ -338,14 +315,6 @@ export const Firewall = () => {
onChange={(value)=>{item.proto = value as Protocol;updateMe()}} onChange={(value)=>{item.proto = value as Protocol;updateMe()}}
style={{width:"100%"}} style={{width:"100%"}}
/> />
<Space h="xs" />
<Button
size="xs" variant="light"
color={ipFilteringEnabled?"lime":"red"}
onClick={()=>set_filtering_ip(!ipFilteringEnabled)}
style={{width:"100%"}}
>{ipFilteringEnabled?"IP Filtering ON":"IP Filtering OFF"}</Button>
</div> </div>
</div> </div>
<Space h="md" /> <Space h="md" />