mirror of
https://github.com/Av0idD4rk/firegex.git
synced 2026-10-23 16:31:00 +00:00
Add transparent TLS proxying and certificate management for NFProxy and NFRegex
- Add Nginx stream proxy module for transparent TLS termination in NFProxy and NFRegex - Add backend routes and SQLite schema for certificate management (upload, view, delete) - Implement DNAT nftables redirection rules for deterministic TLS ports - Add frontend React components (TLSConfigModal, TLSServiceFields) for certificate configuration - Update Dockerfile and docker-entrypoint.sh to support Nginx stream module and sysctl forwarding
This commit is contained in:
parent
bd10f9e8b8
commit
75c54f0155
@ -16,7 +16,7 @@ RUN bun run build
|
|||||||
FROM --platform=$TARGETARCH quay.io/fedora/fedora:43 AS base
|
FROM --platform=$TARGETARCH quay.io/fedora/fedora:43 AS base
|
||||||
RUN dnf -y update && dnf install -y python3.14 libnetfilter_queue \
|
RUN dnf -y update && dnf install -y python3.14 libnetfilter_queue \
|
||||||
libnfnetlink libmnl libcap-ng-utils nftables \
|
libnfnetlink libmnl libcap-ng-utils nftables \
|
||||||
vectorscan libtins python3-nftables libpcap && dnf clean all
|
vectorscan libtins python3-nftables libpcap nginx nginx-mod-stream && dnf clean all
|
||||||
|
|
||||||
RUN mkdir -p /execute/modules
|
RUN mkdir -p /execute/modules
|
||||||
WORKDIR /execute
|
WORKDIR /execute
|
||||||
|
|||||||
@ -11,6 +11,7 @@ from utils.sqlite import SQLite
|
|||||||
from utils import API_VERSION, FIREGEX_PORT, FIREGEX_HOST, FIREGEX_SOCKET, JWT_ALGORITHM, get_interfaces, socketio_emit, DEBUG, SysctlManager, NORELOAD
|
from utils import API_VERSION, FIREGEX_PORT, FIREGEX_HOST, FIREGEX_SOCKET, JWT_ALGORITHM, get_interfaces, socketio_emit, DEBUG, SysctlManager, NORELOAD
|
||||||
from utils.loader import frontend_deploy, load_routers
|
from utils.loader import frontend_deploy, load_routers
|
||||||
from utils.models import ChangePasswordModel, IpInterface, PasswordChangeForm, PasswordForm, ResetRequest, StatusModel, StatusMessageModel
|
from utils.models import ChangePasswordModel, IpInterface, PasswordChangeForm, PasswordForm, ResetRequest, StatusModel, StatusMessageModel
|
||||||
|
from utils.certs import CERTIFICATES_SCHEMA
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
import socketio
|
import socketio
|
||||||
@ -18,7 +19,7 @@ from socketio.exceptions import ConnectionRefusedError
|
|||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
# DB init
|
# DB init
|
||||||
db = SQLite('db/firegex.db')
|
db = SQLite('db/firegex.db', CERTIFICATES_SCHEMA)
|
||||||
sysctl = SysctlManager({
|
sysctl = SysctlManager({
|
||||||
"net.ipv4.conf.all.forwarding": True,
|
"net.ipv4.conf.all.forwarding": True,
|
||||||
"net.ipv6.conf.all.forwarding": True,
|
"net.ipv6.conf.all.forwarding": True,
|
||||||
|
|||||||
@ -451,6 +451,7 @@ class NfQueue {
|
|||||||
// error code taken from the linux kernel:
|
// error code taken from the linux kernel:
|
||||||
// https://elixir.bootlin.com/linux/v5.18.12/source/include/linux/errno.h#L27
|
// https://elixir.bootlin.com/linux/v5.18.12/source/include/linux/errno.h#L27
|
||||||
#define ENOTSUPP 524 /* Operation is not supported */
|
#define ENOTSUPP 524 /* Operation is not supported */
|
||||||
|
#define EOPNOTSUPP 95 /* Operation not supported on transport endpoint */
|
||||||
|
|
||||||
if (error_msg->error != -ENOTSUPP && error_msg->error != -EOPNOTSUPP) {
|
if (error_msg->error != -ENOTSUPP && error_msg->error != -EOPNOTSUPP) {
|
||||||
_clear();
|
_clear();
|
||||||
|
|||||||
@ -9,6 +9,15 @@ if [ -n "$SOCKET_DIR" ]; then
|
|||||||
chmod 755 "$SOCKET_DIR"
|
chmod 755 "$SOCKET_DIR"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Set host sysctls while running as root.
|
||||||
|
if [ -d "/sys_host" ]; then
|
||||||
|
echo "[*] Setting host sysctls..."
|
||||||
|
echo 1 > /sys_host/net.ipv4.conf.all.forwarding 2>/dev/null || true
|
||||||
|
echo 1 > /sys_host/net.ipv6.conf.all.forwarding 2>/dev/null || true
|
||||||
|
echo 1 > /sys_host/net.ipv4.conf.all.route_localnet 2>/dev/null || true
|
||||||
|
echo 1 > /sys_host/net.ipv4.ip_forward 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
echo "[*] Attempting to start with capabilities..."
|
echo "[*] Attempting to start with capabilities..."
|
||||||
|
|
||||||
if capsh --caps="cap_net_admin,cap_setpcap,cap_setuid,cap_setgid,cap_sys_nice+eip" \
|
if capsh --caps="cap_net_admin,cap_setpcap,cap_setuid,cap_setgid,cap_sys_nice+eip" \
|
||||||
@ -21,7 +30,8 @@ then
|
|||||||
--keep=1 \
|
--keep=1 \
|
||||||
--user=nobody \
|
--user=nobody \
|
||||||
--addamb=cap_net_admin,cap_sys_nice \
|
--addamb=cap_net_admin,cap_sys_nice \
|
||||||
-- -c "python3 /execute/app.py DOCKER"
|
--shell=/usr/bin/python3 \
|
||||||
|
-- /execute/app.py DOCKER
|
||||||
else
|
else
|
||||||
echo "[!] capsh failed, running with root user"
|
echo "[!] capsh failed, running with root user"
|
||||||
exec python3 /execute/app.py DOCKER
|
exec python3 /execute/app.py DOCKER
|
||||||
|
|||||||
@ -90,11 +90,11 @@ class FiregexInterceptor:
|
|||||||
proxy_binary_path, stdin=asyncio.subprocess.DEVNULL,
|
proxy_binary_path, stdin=asyncio.subprocess.DEVNULL,
|
||||||
stdout=asyncio.subprocess.PIPE,
|
stdout=asyncio.subprocess.PIPE,
|
||||||
stderr=asyncio.subprocess.STDOUT,
|
stderr=asyncio.subprocess.STDOUT,
|
||||||
env={
|
env=dict(os.environ, **{
|
||||||
"NTHREADS": os.getenv("NTHREADS","1"),
|
"NTHREADS": os.getenv("NTHREADS","1"),
|
||||||
"FIREGEX_NFQUEUE_FAIL_OPEN": "1" if self.srv.fail_open else "0",
|
"FIREGEX_NFQUEUE_FAIL_OPEN": "1" if self.srv.fail_open else "0",
|
||||||
"FIREGEX_NFPROXY_SOCK": self.sock_path
|
"FIREGEX_NFPROXY_SOCK": self.sock_path
|
||||||
},
|
}),
|
||||||
)
|
)
|
||||||
nicenessify(-10, self.process.pid)
|
nicenessify(-10, self.process.pid)
|
||||||
self.outstrem_task = asyncio.create_task(self._stream_handler())
|
self.outstrem_task = asyncio.create_task(self._stream_handler())
|
||||||
|
|||||||
@ -2,8 +2,10 @@ import asyncio
|
|||||||
from modules.nfproxy.firegex import FiregexInterceptor
|
from modules.nfproxy.firegex import FiregexInterceptor
|
||||||
from modules.nfproxy.nftables import FiregexTables, FiregexFilter
|
from modules.nfproxy.nftables import FiregexTables, FiregexFilter
|
||||||
from modules.nfproxy.models import Service, PyFilter
|
from modules.nfproxy.models import Service, PyFilter
|
||||||
|
from modules.nfproxy.nginx import sync_nginx_state
|
||||||
from utils.sqlite import SQLite
|
from utils.sqlite import SQLite
|
||||||
from utils import run_func
|
from utils import run_func
|
||||||
|
from utils.certs import CertsDB, ip_parse as certs_ip_parse
|
||||||
|
|
||||||
class STATUS:
|
class STATUS:
|
||||||
STOP = "stop"
|
STOP = "stop"
|
||||||
@ -72,6 +74,8 @@ class ServiceManager:
|
|||||||
self.interceptor = await FiregexInterceptor.start(self.srv, outstream_func=self.outstream_function, exception_func=self.exception_function)
|
self.interceptor = await FiregexInterceptor.start(self.srv, outstream_func=self.outstream_function, exception_func=self.exception_function)
|
||||||
await self._update_filters_from_db()
|
await self._update_filters_from_db()
|
||||||
self._set_status(STATUS.ACTIVE)
|
self._set_status(STATUS.ACTIVE)
|
||||||
|
if self.srv.tls_enabled:
|
||||||
|
sync_nginx_state()
|
||||||
|
|
||||||
async def stop(self):
|
async def stop(self):
|
||||||
nft.delete(self.srv)
|
nft.delete(self.srv)
|
||||||
@ -79,6 +83,8 @@ class ServiceManager:
|
|||||||
await self.interceptor.stop()
|
await self.interceptor.stop()
|
||||||
self.interceptor = None
|
self.interceptor = None
|
||||||
self._set_status(STATUS.STOP)
|
self._set_status(STATUS.STOP)
|
||||||
|
if self.srv.tls_enabled:
|
||||||
|
sync_nginx_state()
|
||||||
|
|
||||||
async def restart(self):
|
async def restart(self):
|
||||||
await self.stop()
|
await self.stop()
|
||||||
@ -88,6 +94,18 @@ class ServiceManager:
|
|||||||
async with self.lock:
|
async with self.lock:
|
||||||
await self._update_filters_from_db()
|
await self._update_filters_from_db()
|
||||||
|
|
||||||
|
async def update_tls_config(self):
|
||||||
|
async with self.lock:
|
||||||
|
srv_dict = self.db.query("SELECT * FROM services WHERE service_id = ?;", self.srv.id)[0]
|
||||||
|
if srv_dict.get("tls_enabled"):
|
||||||
|
cert, key = CertsDB().get_cert_and_key(srv_dict["ip_int"], srv_dict["port"])
|
||||||
|
srv_dict["tls_cert"] = cert
|
||||||
|
srv_dict["tls_key"] = key
|
||||||
|
self.srv = Service.from_dict(srv_dict)
|
||||||
|
if self.status == STATUS.ACTIVE:
|
||||||
|
await self.restart()
|
||||||
|
sync_nginx_state()
|
||||||
|
|
||||||
class FirewallManager:
|
class FirewallManager:
|
||||||
def __init__(self, db:SQLite, outstream_func=None, exception_func=None):
|
def __init__(self, db:SQLite, outstream_func=None, exception_func=None):
|
||||||
self.db = db
|
self.db = db
|
||||||
@ -99,6 +117,7 @@ class FirewallManager:
|
|||||||
async def close(self):
|
async def close(self):
|
||||||
for key in list(self.service_table.keys()):
|
for key in list(self.service_table.keys()):
|
||||||
await self.remove(key)
|
await self.remove(key)
|
||||||
|
sync_nginx_state()
|
||||||
|
|
||||||
async def remove(self,srv_id):
|
async def remove(self,srv_id):
|
||||||
async with self.lock:
|
async with self.lock:
|
||||||
@ -112,12 +131,21 @@ class FirewallManager:
|
|||||||
|
|
||||||
async def reload(self):
|
async def reload(self):
|
||||||
async with self.lock:
|
async with self.lock:
|
||||||
for srv in self.db.query('SELECT * FROM services;'):
|
services = self.db.query('SELECT * FROM services;')
|
||||||
srv = Service.from_dict(srv)
|
tls_services = [s for s in services if s.get("tls_enabled") and s["service_id"] not in self.service_table]
|
||||||
if srv.id in self.service_table:
|
certs_map = CertsDB().get_multiple_certs_and_keys(tls_services) if tls_services else {}
|
||||||
|
|
||||||
|
for srv in services:
|
||||||
|
if srv["service_id"] in self.service_table:
|
||||||
continue
|
continue
|
||||||
self.service_table[srv.id] = ServiceManager(srv, self.db, outstream_func=self.outstream_function, exception_func=self.exception_function)
|
if srv.get("tls_enabled"):
|
||||||
await self.service_table[srv.id].next(srv.status)
|
cert, key = certs_map.get((certs_ip_parse(srv["ip_int"]), srv["port"]), (None, None))
|
||||||
|
srv["tls_cert"] = cert
|
||||||
|
srv["tls_key"] = key
|
||||||
|
srv_obj = Service.from_dict(srv)
|
||||||
|
self.service_table[srv_obj.id] = ServiceManager(srv_obj, self.db, outstream_func=self.outstream_function, exception_func=self.exception_function)
|
||||||
|
await self.service_table[srv_obj.id].next(srv_obj.status)
|
||||||
|
sync_nginx_state()
|
||||||
|
|
||||||
def get(self,srv_id) -> ServiceManager:
|
def get(self,srv_id) -> ServiceManager:
|
||||||
if srv_id in self.service_table:
|
if srv_id in self.service_table:
|
||||||
|
|||||||
@ -8,6 +8,9 @@ class Service:
|
|||||||
proto: str,
|
proto: str,
|
||||||
ip_int: str,
|
ip_int: str,
|
||||||
fail_open: bool,
|
fail_open: bool,
|
||||||
|
tls_enabled: bool = False,
|
||||||
|
tls_cert: str | None = None,
|
||||||
|
tls_key: str | None = None,
|
||||||
**other,
|
**other,
|
||||||
):
|
):
|
||||||
self.id = service_id
|
self.id = service_id
|
||||||
@ -17,6 +20,9 @@ class Service:
|
|||||||
self.proto = proto
|
self.proto = proto
|
||||||
self.ip_int = ip_int
|
self.ip_int = ip_int
|
||||||
self.fail_open = fail_open
|
self.fail_open = fail_open
|
||||||
|
self.tls_enabled = bool(tls_enabled)
|
||||||
|
self.tls_cert = tls_cert
|
||||||
|
self.tls_key = tls_key
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, var: dict):
|
def from_dict(cls, var: dict):
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
from modules.nfproxy.models import Service
|
from modules.nfproxy.models import Service
|
||||||
from utils import ip_parse, ip_family, NFTableManager, nftables_int_to_json
|
from utils import ip_parse, ip_family, NFTableManager, nftables_int_to_json
|
||||||
|
from modules.nfproxy.nginx import get_tls_ports
|
||||||
|
|
||||||
def convert_protocol_to_l4(proto:str):
|
def convert_protocol_to_l4(proto:str):
|
||||||
if proto == "tcp":
|
if proto == "tcp":
|
||||||
@ -22,9 +23,19 @@ class FiregexFilter:
|
|||||||
return self.port == o.port and self.proto == convert_protocol_to_l4(o.proto) and ip_parse(self.ip_int) == ip_parse(o.ip_int)
|
return self.port == o.port and self.proto == convert_protocol_to_l4(o.proto) and ip_parse(self.ip_int) == ip_parse(o.ip_int)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def matches(self, srv: Service) -> bool:
|
||||||
|
if self.port == srv.port and ip_parse(self.ip_int) == ip_parse(srv.ip_int):
|
||||||
|
return True
|
||||||
|
ssl_port, clear_port = get_tls_ports(srv.ip_int, srv.port)
|
||||||
|
loopback_ip = "::1" if ip_family(srv.ip_int) == "ip6" else "127.0.0.1"
|
||||||
|
if ip_parse(self.ip_int) == ip_parse(loopback_ip) and self.port in (ssl_port, clear_port):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
class FiregexTables(NFTableManager):
|
class FiregexTables(NFTableManager):
|
||||||
input_chain = "nfproxy_input"
|
input_chain = "nfproxy_input"
|
||||||
output_chain = "nfproxy_output"
|
output_chain = "nfproxy_output"
|
||||||
|
nat_chain = "nfproxy_nat"
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__([
|
super().__init__([
|
||||||
@ -45,69 +56,123 @@ class FiregexTables(NFTableManager):
|
|||||||
"hook":"postrouting",
|
"hook":"postrouting",
|
||||||
"prio":108,
|
"prio":108,
|
||||||
"policy":"accept"
|
"policy":"accept"
|
||||||
|
}}},
|
||||||
|
{"add":{"chain":{ #NAT chain for DNAT
|
||||||
|
"family":"inet",
|
||||||
|
"table":self.table_name,
|
||||||
|
"name":self.nat_chain,
|
||||||
|
"type":"nat",
|
||||||
|
"hook":"prerouting",
|
||||||
|
"prio":-100,
|
||||||
|
"policy":"accept"
|
||||||
}}}
|
}}}
|
||||||
],[
|
],[
|
||||||
{"flush":{"chain":{"table":self.table_name,"family":"inet", "name":self.input_chain}}},
|
{"flush":{"chain":{"table":self.table_name,"family":"inet", "name":self.input_chain}}},
|
||||||
{"delete":{"chain":{"table":self.table_name,"family":"inet", "name":self.input_chain}}},
|
{"delete":{"chain":{"table":self.table_name,"family":"inet", "name":self.input_chain}}},
|
||||||
{"flush":{"chain":{"table":self.table_name,"family":"inet", "name":self.output_chain}}},
|
{"flush":{"chain":{"table":self.table_name,"family":"inet", "name":self.output_chain}}},
|
||||||
{"delete":{"chain":{"table":self.table_name,"family":"inet", "name":self.output_chain}}},
|
{"delete":{"chain":{"table":self.table_name,"family":"inet", "name":self.output_chain}}},
|
||||||
|
{"flush":{"chain":{"table":self.table_name,"family":"inet", "name":self.nat_chain}}},
|
||||||
|
{"delete":{"chain":{"table":self.table_name,"family":"inet", "name":self.nat_chain}}},
|
||||||
])
|
])
|
||||||
|
|
||||||
def add(self, srv:Service, queue_range):
|
def add(self, srv:Service, queue_range):
|
||||||
|
|
||||||
for ele in self.get():
|
for ele in self.get():
|
||||||
if ele.__eq__(srv):
|
if ele.matches(srv):
|
||||||
return
|
return
|
||||||
|
|
||||||
init, end = queue_range
|
init, end = queue_range
|
||||||
if init > end:
|
if init > end:
|
||||||
init, end = end, init
|
init, end = end, init
|
||||||
self.cmd(
|
|
||||||
{ "insert":{ "rule": {
|
if srv.tls_enabled:
|
||||||
"family": "inet",
|
ssl_port, clear_port = get_tls_ports(srv.ip_int, srv.port)
|
||||||
"table": self.table_name,
|
loopback_ip = "::1" if ip_family(srv.ip_int) == "ip6" else "127.0.0.1"
|
||||||
"chain": self.output_chain,
|
self.cmd(
|
||||||
"expr": [
|
{ "insert":{ "rule": { # Redirect TLS traffic to local Nginx SSL proxy
|
||||||
{'match': {'left': {'payload': {'protocol': ip_family(srv.ip_int), 'field': 'saddr'}}, 'op': '==', 'right': nftables_int_to_json(srv.ip_int)}},
|
"family": "inet",
|
||||||
{'match': {"left": { "payload": {"protocol": convert_protocol_to_l4(str(srv.proto)), "field": "sport"}}, "op": "==", "right": int(srv.port)}},
|
"table": self.table_name,
|
||||||
{"mangle": {"key": {"meta": {"key": "mark"}},"value": 0x1338}},
|
"chain": self.nat_chain,
|
||||||
{"queue": {"num": str(init) if init == end else {"range":[init, end] }, "flags": ["bypass"]}}
|
"expr": [
|
||||||
]
|
{'match': {'left': {'payload': {'protocol': ip_family(srv.ip_int), 'field': 'daddr'}}, 'op': '==', 'right': nftables_int_to_json(srv.ip_int)}},
|
||||||
}}},
|
{'match': {'left': {'payload': {'protocol': convert_protocol_to_l4(str(srv.proto)), 'field': 'dport'}}, 'op': '==', 'right': int(srv.port)}},
|
||||||
{"insert":{"rule":{
|
{'match': {'left': {'meta': {'key': 'iifname'}}, 'op': '!=', 'right': 'lo'}},
|
||||||
"family": "inet",
|
{'dnat': {'family': ip_family(srv.ip_int), 'addr': loopback_ip, 'port': int(ssl_port)}}
|
||||||
"table": self.table_name,
|
|
||||||
"chain": self.input_chain,
|
|
||||||
"expr": [
|
|
||||||
{'match': {'left': {'payload': {'protocol': ip_family(srv.ip_int), 'field': 'daddr'}}, 'op': '==', 'right': nftables_int_to_json(srv.ip_int)}},
|
|
||||||
{'match': {"left": { "payload": {"protocol": convert_protocol_to_l4(str(srv.proto)), "field": "dport"}}, "op": "==", "right": int(srv.port)}},
|
|
||||||
{"mangle": {"key": {"meta": {"key": "mark"}},"value": 0x1337}},
|
|
||||||
{"queue": {"num": str(init) if init == end else {"range":[init, end] }, "flags": ["bypass"]}}
|
|
||||||
]
|
]
|
||||||
}}}
|
}}},
|
||||||
)
|
{"insert":{"rule":{ # Send cleartext inbound traffic to NFQUEUE
|
||||||
|
"family": "inet",
|
||||||
|
"table": self.table_name,
|
||||||
|
"chain": self.input_chain,
|
||||||
|
"expr": [
|
||||||
|
{'match': {'left': {'payload': {'protocol': ip_family(srv.ip_int), 'field': 'daddr'}}, 'op': '==', 'right': loopback_ip}},
|
||||||
|
{'match': {'left': {'payload': {'protocol': convert_protocol_to_l4(str(srv.proto)), 'field': 'dport'}}, 'op': '==', 'right': int(clear_port)}},
|
||||||
|
{"mangle": {"key": {"meta": {"key": "mark"}},"value": 0x1337}},
|
||||||
|
{"queue": {"num": str(init) if init == end else {"range":[init, end] }, "flags": ["bypass"]}}
|
||||||
|
]
|
||||||
|
}}},
|
||||||
|
{"insert":{"rule":{ # Send cleartext outbound traffic to NFQUEUE
|
||||||
|
"family": "inet",
|
||||||
|
"table": self.table_name,
|
||||||
|
"chain": self.output_chain,
|
||||||
|
"expr": [
|
||||||
|
{'match': {'left': {'payload': {'protocol': ip_family(srv.ip_int), 'field': 'saddr'}}, 'op': '==', 'right': loopback_ip}},
|
||||||
|
{'match': {'left': {'payload': {'protocol': convert_protocol_to_l4(str(srv.proto)), 'field': 'sport'}}, 'op': '==', 'right': int(clear_port)}},
|
||||||
|
{"mangle": {"key": {"meta": {"key": "mark"}},"value": 0x1338}},
|
||||||
|
{"queue": {"num": str(init) if init == end else {"range":[init, end] }, "flags": ["bypass"]}}
|
||||||
|
]
|
||||||
|
}}}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.cmd(
|
||||||
|
{ "insert":{ "rule": { # Send non-TLS outbound traffic to NFQUEUE
|
||||||
|
"family": "inet",
|
||||||
|
"table": self.table_name,
|
||||||
|
"chain": self.output_chain,
|
||||||
|
"expr": [
|
||||||
|
{'match': {'left': {'payload': {'protocol': ip_family(srv.ip_int), 'field': 'saddr'}}, 'op': '==', 'right': nftables_int_to_json(srv.ip_int)}},
|
||||||
|
{'match': {"left": { "payload": {"protocol": convert_protocol_to_l4(str(srv.proto)), "field": "sport"}}, "op": "==", "right": int(srv.port)}},
|
||||||
|
{"mangle": {"key": {"meta": {"key": "mark"}},"value": 0x1338}},
|
||||||
|
{"queue": {"num": str(init) if init == end else {"range":[init, end] }, "flags": ["bypass"]}}
|
||||||
|
]
|
||||||
|
}}},
|
||||||
|
{"insert":{"rule":{ # Send non-TLS inbound traffic to NFQUEUE
|
||||||
|
"family": "inet",
|
||||||
|
"table": self.table_name,
|
||||||
|
"chain": self.input_chain,
|
||||||
|
"expr": [
|
||||||
|
{'match': {'left': {'payload': {'protocol': ip_family(srv.ip_int), 'field': 'daddr'}}, 'op': '==', 'right': nftables_int_to_json(srv.ip_int)}},
|
||||||
|
{'match': {"left": { "payload": {"protocol": convert_protocol_to_l4(str(srv.proto)), "field": "dport"}}, "op": "==", "right": int(srv.port)}},
|
||||||
|
{"mangle": {"key": {"meta": {"key": "mark"}},"value": 0x1337}},
|
||||||
|
{"queue": {"num": str(init) if init == end else {"range":[init, end] }, "flags": ["bypass"]}}
|
||||||
|
]
|
||||||
|
}}}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get(self) -> list[FiregexFilter]:
|
def get(self) -> list[FiregexFilter]:
|
||||||
res = []
|
res = []
|
||||||
for filter in self.list_rules(tables=[self.table_name], chains=[self.input_chain,self.output_chain]):
|
for filter in self.list_rules(tables=[self.table_name], chains=[self.input_chain,self.output_chain,self.nat_chain]):
|
||||||
ip_int = None
|
try:
|
||||||
if isinstance(filter["expr"][0]["match"]["right"],str):
|
ip_int = None
|
||||||
ip_int = str(ip_parse(filter["expr"][0]["match"]["right"]))
|
if isinstance(filter["expr"][0]["match"]["right"],str):
|
||||||
else:
|
ip_int = str(ip_parse(filter["expr"][0]["match"]["right"]))
|
||||||
ip_int = f'{filter["expr"][0]["match"]["right"]["prefix"]["addr"]}/{filter["expr"][0]["match"]["right"]["prefix"]["len"]}'
|
else:
|
||||||
res.append(FiregexFilter(
|
ip_int = f'{filter["expr"][0]["match"]["right"]["prefix"]["addr"]}/{filter["expr"][0]["match"]["right"]["prefix"]["len"]}'
|
||||||
target=filter["chain"],
|
res.append(FiregexFilter(
|
||||||
id=int(filter["handle"]),
|
target=filter["chain"],
|
||||||
proto=filter["expr"][1]["match"]["left"]["payload"]["protocol"],
|
id=int(filter["handle"]),
|
||||||
port=filter["expr"][1]["match"]["right"],
|
proto=filter["expr"][1]["match"]["left"]["payload"]["protocol"],
|
||||||
ip_int=ip_int
|
port=filter["expr"][1]["match"]["right"],
|
||||||
))
|
ip_int=ip_int
|
||||||
|
))
|
||||||
|
except (KeyError, TypeError, IndexError):
|
||||||
|
pass # Rule has unexpected structure (e.g., intermediate NAT rule), skip
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def delete(self, srv:Service):
|
def delete(self, srv:Service):
|
||||||
for filter in self.get():
|
for filter in self.get():
|
||||||
if filter.__eq__(srv):
|
if filter.matches(srv):
|
||||||
self.cmd({ "delete":{ "rule": {
|
self.cmd({ "delete":{ "rule": {
|
||||||
"family": "inet",
|
"family": "inet",
|
||||||
"table": self.table_name,
|
"table": self.table_name,
|
||||||
|
|||||||
103
backend/modules/nfproxy/nginx.py
Normal file
103
backend/modules/nfproxy/nginx.py
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
import glob
|
||||||
|
from utils.sqlite import SQLite
|
||||||
|
from utils.certs import CertsDB, get_tls_ports, ip_parse as certs_ip_parse
|
||||||
|
|
||||||
|
NGINX_CONF_PATH = "/tmp/firegex_nginx.conf"
|
||||||
|
NGINX_PID_PATH = "/tmp/firegex_nginx.pid"
|
||||||
|
NGINX_LOG_PATH = "/tmp/firegex_nginx_error.log"
|
||||||
|
|
||||||
|
def _get_active_tls_services(db_name: str) -> list[dict]:
|
||||||
|
db = SQLite(db_name)
|
||||||
|
db.connect()
|
||||||
|
try:
|
||||||
|
return db.query("SELECT * FROM services WHERE status = 'active' AND tls_enabled = 1;")
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
finally:
|
||||||
|
db.disconnect()
|
||||||
|
|
||||||
|
def update_nginx(services: list[dict]) -> None:
|
||||||
|
if not services:
|
||||||
|
subprocess.run(["nginx", "-c", NGINX_CONF_PATH, "-s", "stop", "-e", NGINX_LOG_PATH], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
|
||||||
|
# Clean up certificates, configuration, and pid
|
||||||
|
for pattern in ("/tmp/firegex_cert_*", "/tmp/firegex_key_*", NGINX_CONF_PATH, NGINX_PID_PATH, NGINX_LOG_PATH):
|
||||||
|
for path in (glob.glob(pattern) if "*" in pattern else [pattern]):
|
||||||
|
try:
|
||||||
|
os.remove(path)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
|
||||||
|
servers_config = []
|
||||||
|
for srv in services:
|
||||||
|
srv_id = srv["service_id"]
|
||||||
|
cert_path = f"/tmp/firegex_cert_{srv_id}.crt"
|
||||||
|
key_path = f"/tmp/firegex_key_{srv_id}.key"
|
||||||
|
|
||||||
|
with open(cert_path, "w") as f:
|
||||||
|
f.write(srv.get("tls_cert") or "")
|
||||||
|
with open(key_path, "w") as f:
|
||||||
|
f.write(srv.get("tls_key") or "")
|
||||||
|
|
||||||
|
ssl_port, clear_port = get_tls_ports(srv["ip_int"], srv["port"])
|
||||||
|
|
||||||
|
ip_addr = srv["ip_int"].split("/")[0]
|
||||||
|
is_ipv6 = ":" in ip_addr
|
||||||
|
loopback_ip = "[::1]" if is_ipv6 else "127.0.0.1"
|
||||||
|
dest_ip = f"[{ip_addr}]" if is_ipv6 else ip_addr
|
||||||
|
|
||||||
|
servers_config.append(
|
||||||
|
f" server {{\n"
|
||||||
|
f" listen {loopback_ip}:{ssl_port} ssl;\n"
|
||||||
|
f" proxy_pass {loopback_ip}:{clear_port};\n"
|
||||||
|
f" ssl_certificate {cert_path};\n"
|
||||||
|
f" ssl_certificate_key {key_path};\n"
|
||||||
|
f" ssl_protocols TLSv1.2 TLSv1.3;\n"
|
||||||
|
f" }}\n"
|
||||||
|
f" server {{\n"
|
||||||
|
f" listen {loopback_ip}:{clear_port};\n"
|
||||||
|
f" proxy_pass {dest_ip}:{srv['port']};\n"
|
||||||
|
f" proxy_ssl on;\n"
|
||||||
|
f" proxy_ssl_verify off;\n"
|
||||||
|
f" proxy_ssl_protocols TLSv1.2 TLSv1.3;\n"
|
||||||
|
f" }}"
|
||||||
|
)
|
||||||
|
|
||||||
|
config_content = ""
|
||||||
|
if os.path.isdir("/usr/share/nginx/modules"):
|
||||||
|
config_content += "include /usr/share/nginx/modules/*.conf;\n"
|
||||||
|
|
||||||
|
config_content += (
|
||||||
|
f"pid {NGINX_PID_PATH};\n"
|
||||||
|
f"error_log {NGINX_LOG_PATH} info;\n\n"
|
||||||
|
f"events {{\n"
|
||||||
|
f" worker_connections 1024;\n"
|
||||||
|
f"}}\n\n"
|
||||||
|
f"stream {{\n"
|
||||||
|
+ "\n".join(servers_config) + "\n"
|
||||||
|
f"}}\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(NGINX_CONF_PATH, "w") as f:
|
||||||
|
f.write(config_content)
|
||||||
|
|
||||||
|
# Try to reload. If Nginx is not running, start it.
|
||||||
|
res = subprocess.run(["nginx", "-c", NGINX_CONF_PATH, "-s", "reload", "-e", NGINX_LOG_PATH], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
if res.returncode != 0:
|
||||||
|
subprocess.run(["nginx", "-c", NGINX_CONF_PATH, "-e", NGINX_LOG_PATH], check=True)
|
||||||
|
|
||||||
|
def sync_nginx_state() -> None:
|
||||||
|
services = _get_active_tls_services('db/nft-pyfilters.db') + _get_active_tls_services('db/nft-regex.db')
|
||||||
|
|
||||||
|
# Batch-resolve certificates using CertsDB
|
||||||
|
certs_map = CertsDB().get_multiple_certs_and_keys(services)
|
||||||
|
for srv in services:
|
||||||
|
cert, key = certs_map.get((certs_ip_parse(srv["ip_int"]), srv["port"]), (None, None))
|
||||||
|
srv["tls_cert"] = cert
|
||||||
|
srv["tls_key"] = key
|
||||||
|
|
||||||
|
update_nginx(services)
|
||||||
|
|
||||||
@ -101,11 +101,11 @@ class FiregexInterceptor:
|
|||||||
self.process = await asyncio.create_subprocess_exec(
|
self.process = await asyncio.create_subprocess_exec(
|
||||||
proxy_binary_path,
|
proxy_binary_path,
|
||||||
stdout=asyncio.subprocess.PIPE, stdin=asyncio.subprocess.PIPE,
|
stdout=asyncio.subprocess.PIPE, stdin=asyncio.subprocess.PIPE,
|
||||||
env={
|
env=dict(os.environ, **{
|
||||||
"MATCH_MODE": "stream" if self.srv.proto == "tcp" else "block",
|
"MATCH_MODE": "stream" if self.srv.proto == "tcp" else "block",
|
||||||
"NTHREADS": os.getenv("NTHREADS","1"),
|
"NTHREADS": os.getenv("NTHREADS","1"),
|
||||||
"FIREGEX_NFQUEUE_FAIL_OPEN": "1" if self.srv.fail_open else "0",
|
"FIREGEX_NFQUEUE_FAIL_OPEN": "1" if self.srv.fail_open else "0",
|
||||||
},
|
}),
|
||||||
)
|
)
|
||||||
nicenessify(-10, self.process.pid)
|
nicenessify(-10, self.process.pid)
|
||||||
line_fut = self.process.stdout.readuntil()
|
line_fut = self.process.stdout.readuntil()
|
||||||
|
|||||||
@ -3,6 +3,8 @@ from modules.nfregex.firegex import FiregexInterceptor, RegexFilter
|
|||||||
from modules.nfregex.nftables import FiregexTables, FiregexFilter
|
from modules.nfregex.nftables import FiregexTables, FiregexFilter
|
||||||
from modules.nfregex.models import Regex, Service
|
from modules.nfregex.models import Regex, Service
|
||||||
from utils.sqlite import SQLite
|
from utils.sqlite import SQLite
|
||||||
|
from modules.nfproxy.nginx import sync_nginx_state
|
||||||
|
from utils.certs import CertsDB, ip_parse as certs_ip_parse
|
||||||
|
|
||||||
class STATUS:
|
class STATUS:
|
||||||
STOP = "stop"
|
STOP = "stop"
|
||||||
@ -63,6 +65,8 @@ class ServiceManager:
|
|||||||
self.interceptor = await FiregexInterceptor.start(self.srv)
|
self.interceptor = await FiregexInterceptor.start(self.srv)
|
||||||
await self._update_filters_from_db()
|
await self._update_filters_from_db()
|
||||||
self._set_status(STATUS.ACTIVE)
|
self._set_status(STATUS.ACTIVE)
|
||||||
|
if self.srv.tls_enabled:
|
||||||
|
sync_nginx_state()
|
||||||
|
|
||||||
async def stop(self):
|
async def stop(self):
|
||||||
nft.delete(self.srv)
|
nft.delete(self.srv)
|
||||||
@ -70,6 +74,8 @@ class ServiceManager:
|
|||||||
await self.interceptor.stop()
|
await self.interceptor.stop()
|
||||||
self.interceptor = None
|
self.interceptor = None
|
||||||
self._set_status(STATUS.STOP)
|
self._set_status(STATUS.STOP)
|
||||||
|
if self.srv.tls_enabled:
|
||||||
|
sync_nginx_state()
|
||||||
|
|
||||||
async def restart(self):
|
async def restart(self):
|
||||||
await self.stop()
|
await self.stop()
|
||||||
@ -79,6 +85,18 @@ class ServiceManager:
|
|||||||
async with self.lock:
|
async with self.lock:
|
||||||
await self._update_filters_from_db()
|
await self._update_filters_from_db()
|
||||||
|
|
||||||
|
async def update_tls_config(self):
|
||||||
|
async with self.lock:
|
||||||
|
srv_dict = self.db.query("SELECT * FROM services WHERE service_id = ?;", self.srv.id)[0]
|
||||||
|
if srv_dict.get("tls_enabled"):
|
||||||
|
cert, key = CertsDB().get_cert_and_key(srv_dict["ip_int"], srv_dict["port"])
|
||||||
|
srv_dict["tls_cert"] = cert
|
||||||
|
srv_dict["tls_key"] = key
|
||||||
|
self.srv = Service.from_dict(srv_dict)
|
||||||
|
if self.status == STATUS.ACTIVE:
|
||||||
|
await self.restart()
|
||||||
|
sync_nginx_state()
|
||||||
|
|
||||||
class FirewallManager:
|
class FirewallManager:
|
||||||
def __init__(self, db:SQLite):
|
def __init__(self, db:SQLite):
|
||||||
self.db = db
|
self.db = db
|
||||||
@ -88,6 +106,7 @@ class FirewallManager:
|
|||||||
async def close(self):
|
async def close(self):
|
||||||
for key in list(self.service_table.keys()):
|
for key in list(self.service_table.keys()):
|
||||||
await self.remove(key)
|
await self.remove(key)
|
||||||
|
sync_nginx_state()
|
||||||
|
|
||||||
async def remove(self,srv_id):
|
async def remove(self,srv_id):
|
||||||
async with self.lock:
|
async with self.lock:
|
||||||
@ -101,12 +120,21 @@ class FirewallManager:
|
|||||||
|
|
||||||
async def reload(self):
|
async def reload(self):
|
||||||
async with self.lock:
|
async with self.lock:
|
||||||
for srv in self.db.query('SELECT * FROM services;'):
|
services = self.db.query('SELECT * FROM services;')
|
||||||
srv = Service.from_dict(srv)
|
tls_services = [s for s in services if s.get("tls_enabled") and s["service_id"] not in self.service_table]
|
||||||
if srv.id in self.service_table:
|
certs_map = CertsDB().get_multiple_certs_and_keys(tls_services) if tls_services else {}
|
||||||
|
|
||||||
|
for srv in services:
|
||||||
|
if srv["service_id"] in self.service_table:
|
||||||
continue
|
continue
|
||||||
self.service_table[srv.id] = ServiceManager(srv, self.db)
|
if srv.get("tls_enabled"):
|
||||||
await self.service_table[srv.id].next(srv.status)
|
cert, key = certs_map.get((certs_ip_parse(srv["ip_int"]), srv["port"]), (None, None))
|
||||||
|
srv["tls_cert"] = cert
|
||||||
|
srv["tls_key"] = key
|
||||||
|
srv_obj = Service.from_dict(srv)
|
||||||
|
self.service_table[srv_obj.id] = ServiceManager(srv_obj, self.db)
|
||||||
|
await self.service_table[srv_obj.id].next(srv_obj.status)
|
||||||
|
sync_nginx_state()
|
||||||
|
|
||||||
def get(self,srv_id) -> ServiceManager:
|
def get(self,srv_id) -> ServiceManager:
|
||||||
if srv_id in self.service_table:
|
if srv_id in self.service_table:
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import base64
|
import base64
|
||||||
|
|
||||||
class Service:
|
class Service:
|
||||||
def __init__(self, service_id: str, status: str, port: int, name: str, proto: str, ip_int: str, fail_open: bool, **other):
|
def __init__(self, service_id: str, status: str, port: int, name: str, proto: str, ip_int: str, fail_open: bool, tls_enabled: bool = False, tls_cert: str | None = None, tls_key: str | None = None, **other):
|
||||||
self.id = service_id
|
self.id = service_id
|
||||||
self.status = status
|
self.status = status
|
||||||
self.port = port
|
self.port = port
|
||||||
@ -9,6 +9,9 @@ class Service:
|
|||||||
self.proto = proto
|
self.proto = proto
|
||||||
self.ip_int = ip_int
|
self.ip_int = ip_int
|
||||||
self.fail_open = fail_open
|
self.fail_open = fail_open
|
||||||
|
self.tls_enabled = bool(tls_enabled)
|
||||||
|
self.tls_cert = tls_cert
|
||||||
|
self.tls_key = tls_key
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, var: dict):
|
def from_dict(cls, var: dict):
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
from modules.nfregex.models import Service
|
from modules.nfregex.models import Service
|
||||||
from utils import ip_parse, ip_family, NFTableManager, nftables_int_to_json
|
from utils import ip_parse, ip_family, NFTableManager, nftables_int_to_json
|
||||||
|
from modules.nfproxy.nginx import get_tls_ports
|
||||||
|
|
||||||
class FiregexFilter:
|
class FiregexFilter:
|
||||||
def __init__(self, proto:str, port:int, ip_int:str, target:str, id:int):
|
def __init__(self, proto:str, port:int, ip_int:str, target:str, id:int):
|
||||||
@ -14,9 +15,19 @@ class FiregexFilter:
|
|||||||
return self.port == o.port and self.proto == o.proto and ip_parse(self.ip_int) == ip_parse(o.ip_int)
|
return self.port == o.port and self.proto == o.proto and ip_parse(self.ip_int) == ip_parse(o.ip_int)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def matches(self, srv: Service) -> bool:
|
||||||
|
if self.port == srv.port and ip_parse(self.ip_int) == ip_parse(srv.ip_int):
|
||||||
|
return True
|
||||||
|
ssl_port, clear_port = get_tls_ports(srv.ip_int, srv.port)
|
||||||
|
loopback_ip = "::1" if ip_family(srv.ip_int) == "ip6" else "127.0.0.1"
|
||||||
|
if ip_parse(self.ip_int) == ip_parse(loopback_ip) and self.port in (ssl_port, clear_port):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
class FiregexTables(NFTableManager):
|
class FiregexTables(NFTableManager):
|
||||||
input_chain = "nfregex_input"
|
input_chain = "nfregex_input"
|
||||||
output_chain = "nfregex_output"
|
output_chain = "nfregex_output"
|
||||||
|
nat_chain = "nfregex_nat"
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__([
|
super().__init__([
|
||||||
@ -37,73 +48,130 @@ class FiregexTables(NFTableManager):
|
|||||||
"hook":"postrouting",
|
"hook":"postrouting",
|
||||||
"prio":107,
|
"prio":107,
|
||||||
"policy":"accept"
|
"policy":"accept"
|
||||||
|
}}},
|
||||||
|
{"add":{"chain":{
|
||||||
|
"family":"inet",
|
||||||
|
"table":self.table_name,
|
||||||
|
"name":self.nat_chain,
|
||||||
|
"type":"nat",
|
||||||
|
"hook":"prerouting",
|
||||||
|
"prio":-100,
|
||||||
|
"policy":"accept"
|
||||||
}}}
|
}}}
|
||||||
],[
|
],[
|
||||||
{"flush":{"chain":{"table":self.table_name,"family":"inet", "name":self.input_chain}}},
|
{"flush":{"chain":{"table":self.table_name,"family":"inet", "name":self.input_chain}}},
|
||||||
{"delete":{"chain":{"table":self.table_name,"family":"inet", "name":self.input_chain}}},
|
{"delete":{"chain":{"table":self.table_name,"family":"inet", "name":self.input_chain}}},
|
||||||
{"flush":{"chain":{"table":self.table_name,"family":"inet", "name":self.output_chain}}},
|
{"flush":{"chain":{"table":self.table_name,"family":"inet", "name":self.output_chain}}},
|
||||||
{"delete":{"chain":{"table":self.table_name,"family":"inet", "name":self.output_chain}}},
|
{"delete":{"chain":{"table":self.table_name,"family":"inet", "name":self.output_chain}}},
|
||||||
|
{"flush":{"chain":{"table":self.table_name,"family":"inet", "name":self.nat_chain}}},
|
||||||
|
{"delete":{"chain":{"table":self.table_name,"family":"inet", "name":self.nat_chain}}},
|
||||||
])
|
])
|
||||||
|
|
||||||
def add(self, srv:Service, queue_range):
|
def add(self, srv:Service, queue_range):
|
||||||
|
|
||||||
for ele in self.get():
|
for ele in self.get():
|
||||||
if ele.__eq__(srv):
|
if ele.matches(srv):
|
||||||
return
|
return
|
||||||
|
|
||||||
init, end = queue_range
|
init, end = queue_range
|
||||||
if init > end:
|
if init > end:
|
||||||
init, end = end, init
|
init, end = end, init
|
||||||
self.cmd(
|
|
||||||
{ "insert":{ "rule": {
|
if srv.tls_enabled:
|
||||||
"family": "inet",
|
ssl_port, clear_port = get_tls_ports(srv.ip_int, srv.port)
|
||||||
"table": self.table_name,
|
loopback_ip = "::1" if ip_family(srv.ip_int) == "ip6" else "127.0.0.1"
|
||||||
"chain": self.output_chain,
|
self.cmd(
|
||||||
"expr": [
|
{ "insert":{ "rule": { # Redirect TLS traffic to local Nginx SSL proxy
|
||||||
{'match': {'left': {'payload': {'protocol': ip_family(srv.ip_int), 'field': 'saddr'}}, 'op': '==', 'right': nftables_int_to_json(srv.ip_int)}},
|
"family": "inet",
|
||||||
{'match': {"left": { "payload": {"protocol": str(srv.proto), "field": "sport"}}, "op": "==", "right": int(srv.port)}},
|
"table": self.table_name,
|
||||||
{"mangle": {"key": {"meta": {"key": "mark"}},"value": 0x1338}},
|
"chain": self.nat_chain,
|
||||||
{"queue": {"num": str(init) if init == end else {"range":[init, end] }, "flags": ["bypass"]}}
|
"expr": [
|
||||||
]
|
{'match': {'left': {'payload': {'protocol': ip_family(srv.ip_int), 'field': 'daddr'}}, 'op': '==', 'right': nftables_int_to_json(srv.ip_int)}},
|
||||||
}}},
|
{'match': {'left': {'payload': {'protocol': str(srv.proto), 'field': 'dport'}}, 'op': '==', 'right': int(srv.port)}},
|
||||||
{"insert":{"rule":{
|
{'match': {'left': {'meta': {'key': 'iifname'}}, 'op': '!=', 'right': 'lo'}},
|
||||||
"family": "inet",
|
{'dnat': {'family': ip_family(srv.ip_int), 'addr': loopback_ip, 'port': int(ssl_port)}}
|
||||||
"table": self.table_name,
|
|
||||||
"chain": self.input_chain,
|
|
||||||
"expr": [
|
|
||||||
{'match': {'left': {'payload': {'protocol': ip_family(srv.ip_int), 'field': 'daddr'}}, 'op': '==', 'right': nftables_int_to_json(srv.ip_int)}},
|
|
||||||
{'match': {"left": { "payload": {"protocol": str(srv.proto), "field": "dport"}}, "op": "==", "right": int(srv.port)}},
|
|
||||||
{"mangle": {"key": {"meta": {"key": "mark"}},"value": 0x1337}},
|
|
||||||
{"queue": {"num": str(init) if init == end else {"range":[init, end] }, "flags": ["bypass"]}}
|
|
||||||
]
|
]
|
||||||
}}}
|
}}},
|
||||||
)
|
{"insert":{"rule":{ # Send cleartext inbound traffic to NFQUEUE
|
||||||
|
"family": "inet",
|
||||||
|
"table": self.table_name,
|
||||||
|
"chain": self.input_chain,
|
||||||
|
"expr": [
|
||||||
|
{'match': {'left': {'payload': {'protocol': ip_family(srv.ip_int), 'field': 'daddr'}}, 'op': '==', 'right': loopback_ip}},
|
||||||
|
{'match': {'left': {'payload': {'protocol': str(srv.proto), 'field': 'dport'}}, 'op': '==', 'right': int(clear_port)}},
|
||||||
|
{"mangle": {"key": {"meta": {"key": "mark"}},"value": 0x1337}},
|
||||||
|
{"queue": {"num": str(init) if init == end else {"range":[init, end] }, "flags": ["bypass"]}}
|
||||||
|
]
|
||||||
|
}}},
|
||||||
|
{"insert":{"rule":{ # Send cleartext outbound traffic to NFQUEUE
|
||||||
|
"family": "inet",
|
||||||
|
"table": self.table_name,
|
||||||
|
"chain": self.output_chain,
|
||||||
|
"expr": [
|
||||||
|
{'match': {'left': {'payload': {'protocol': ip_family(srv.ip_int), 'field': 'saddr'}}, 'op': '==', 'right': loopback_ip}},
|
||||||
|
{'match': {'left': {'payload': {'protocol': str(srv.proto), 'field': 'sport'}}, 'op': '==', 'right': int(clear_port)}},
|
||||||
|
{"mangle": {"key": {"meta": {"key": "mark"}},"value": 0x1338}},
|
||||||
|
{"queue": {"num": str(init) if init == end else {"range":[init, end] }, "flags": ["bypass"]}}
|
||||||
|
]
|
||||||
|
}}}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.cmd(
|
||||||
|
{ "insert":{ "rule": {
|
||||||
|
"family": "inet",
|
||||||
|
"table": self.table_name,
|
||||||
|
"chain": self.output_chain,
|
||||||
|
"expr": [
|
||||||
|
{'match': {'left': {'payload': {'protocol': ip_family(srv.ip_int), 'field': 'saddr'}}, 'op': '==', 'right': nftables_int_to_json(srv.ip_int)}},
|
||||||
|
{'match': {"left": { "payload": {"protocol": str(srv.proto), "field": "sport"}}, "op": "==", "right": int(srv.port)}},
|
||||||
|
{"mangle": {"key": {"meta": {"key": "mark"}},"value": 0x1338}},
|
||||||
|
{"queue": {"num": str(init) if init == end else {"range":[init, end] }, "flags": ["bypass"]}}
|
||||||
|
]
|
||||||
|
}}},
|
||||||
|
{"insert":{"rule":{
|
||||||
|
"family": "inet",
|
||||||
|
"table": self.table_name,
|
||||||
|
"chain": self.input_chain,
|
||||||
|
"expr": [
|
||||||
|
{'match': {'left': {'payload': {'protocol': ip_family(srv.ip_int), 'field': 'daddr'}}, 'op': '==', 'right': nftables_int_to_json(srv.ip_int)}},
|
||||||
|
{'match': {"left": { "payload": {"protocol": str(srv.proto), "field": "dport"}}, "op": "==", "right": int(srv.port)}},
|
||||||
|
{"mangle": {"key": {"meta": {"key": "mark"}},"value": 0x1337}},
|
||||||
|
{"queue": {"num": str(init) if init == end else {"range":[init, end] }, "flags": ["bypass"]}}
|
||||||
|
]
|
||||||
|
}}}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get(self) -> list[FiregexFilter]:
|
def get(self) -> list[FiregexFilter]:
|
||||||
res = []
|
res = []
|
||||||
for filter in self.list_rules(tables=[self.table_name], chains=[self.input_chain,self.output_chain]):
|
for filter in self.list_rules(tables=[self.table_name], chains=[self.input_chain,self.output_chain,self.nat_chain]):
|
||||||
ip_int = None
|
try:
|
||||||
if isinstance(filter["expr"][0]["match"]["right"],str):
|
ip_int = None
|
||||||
ip_int = str(ip_parse(filter["expr"][0]["match"]["right"]))
|
if isinstance(filter["expr"][0]["match"]["right"],str):
|
||||||
else:
|
ip_int = str(ip_parse(filter["expr"][0]["match"]["right"]))
|
||||||
ip_int = f'{filter["expr"][0]["match"]["right"]["prefix"]["addr"]}/{filter["expr"][0]["match"]["right"]["prefix"]["len"]}'
|
else:
|
||||||
res.append(FiregexFilter(
|
ip_int = f'{filter["expr"][0]["match"]["right"]["prefix"]["addr"]}/{filter["expr"][0]["match"]["right"]["prefix"]["len"]}'
|
||||||
target=filter["chain"],
|
res.append(FiregexFilter(
|
||||||
id=int(filter["handle"]),
|
target=filter["chain"],
|
||||||
proto=filter["expr"][1]["match"]["left"]["payload"]["protocol"],
|
id=int(filter["handle"]),
|
||||||
port=filter["expr"][1]["match"]["right"],
|
proto=filter["expr"][1]["match"]["left"]["payload"]["protocol"],
|
||||||
ip_int=ip_int
|
port=filter["expr"][1]["match"]["right"],
|
||||||
))
|
ip_int=ip_int
|
||||||
|
))
|
||||||
|
except (KeyError, TypeError, IndexError):
|
||||||
|
pass # Rule has unexpected structure (e.g., intermediate NAT rule), skip
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def delete(self, srv:Service):
|
def delete(self, srv:Service):
|
||||||
|
delete_cmds = []
|
||||||
for filter in self.get():
|
for filter in self.get():
|
||||||
if filter.__eq__(srv):
|
if filter.matches(srv):
|
||||||
self.cmd({ "delete":{ "rule": {
|
delete_cmds.append({ "delete":{ "rule": {
|
||||||
"family": "inet",
|
"family": "inet",
|
||||||
"table": self.table_name,
|
"table": self.table_name,
|
||||||
"chain": filter.target,
|
"chain": filter.target,
|
||||||
"handle": filter.id
|
"handle": filter.id
|
||||||
}}})
|
}}})
|
||||||
|
if delete_cmds:
|
||||||
|
self.cmd(*delete_cmds)
|
||||||
|
|
||||||
36
backend/routers/certificates.py
Normal file
36
backend/routers/certificates.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
from fastapi import APIRouter
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from utils.models import StatusMessageModel
|
||||||
|
from utils.certs import CertsDB
|
||||||
|
|
||||||
|
app = APIRouter()
|
||||||
|
|
||||||
|
class CertificateModel(BaseModel):
|
||||||
|
ip_int: str
|
||||||
|
port: int
|
||||||
|
cert: str
|
||||||
|
key: str
|
||||||
|
|
||||||
|
class CertificateResponseModel(BaseModel):
|
||||||
|
ip_int: str
|
||||||
|
port: int
|
||||||
|
cert: str | None = None
|
||||||
|
key: str | None = None
|
||||||
|
|
||||||
|
@app.get("", response_model=CertificateResponseModel)
|
||||||
|
async def get_certificate(ip_int: str, port: int):
|
||||||
|
"""Retrieve TLS certificate and key by ip_int and port"""
|
||||||
|
cert, key = CertsDB().get_cert_and_key(ip_int, port)
|
||||||
|
return {"ip_int": ip_int, "port": port, "cert": cert, "key": key}
|
||||||
|
|
||||||
|
@app.post("", response_model=StatusMessageModel)
|
||||||
|
async def upsert_certificate(form: CertificateModel):
|
||||||
|
"""Create or update TLS certificate and key"""
|
||||||
|
CertsDB().upsert_cert_and_key(form.ip_int, form.port, form.cert, form.key)
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
@app.delete("", response_model=StatusMessageModel)
|
||||||
|
async def delete_certificate(ip_int: str, port: int):
|
||||||
|
"""Delete TLS certificate and key by ip_int and port"""
|
||||||
|
CertsDB().delete_cert_and_key(ip_int, port)
|
||||||
|
return {"status": "ok"}
|
||||||
@ -6,7 +6,8 @@ from modules.nfproxy.nftables import FiregexTables
|
|||||||
from modules.nfproxy.firewall import STATUS, FirewallManager
|
from modules.nfproxy.firewall import STATUS, FirewallManager
|
||||||
from utils.sqlite import SQLite
|
from utils.sqlite import SQLite
|
||||||
from utils import ip_parse, refactor_name, socketio_emit, PortType
|
from utils import ip_parse, refactor_name, socketio_emit, PortType
|
||||||
from utils.models import ResetRequest, StatusMessageModel
|
from utils.certs import CertsDB, populate_services_tls_config
|
||||||
|
from utils.models import ResetRequest, StatusMessageModel, TLSConfigForm
|
||||||
import os
|
import os
|
||||||
from firegex.nfproxy.internals import get_filter_names
|
from firegex.nfproxy.internals import get_filter_names
|
||||||
from fastapi.responses import PlainTextResponse
|
from fastapi.responses import PlainTextResponse
|
||||||
@ -27,6 +28,11 @@ class ServiceModel(BaseModel):
|
|||||||
edited_packets: int
|
edited_packets: int
|
||||||
blocked_packets: int
|
blocked_packets: int
|
||||||
fail_open: bool
|
fail_open: bool
|
||||||
|
tls_enabled: bool
|
||||||
|
tls_cert: str|None = None
|
||||||
|
tls_key: str|None = None
|
||||||
|
ssl_port: int|None = None
|
||||||
|
clear_port: int|None = None
|
||||||
|
|
||||||
class RenameForm(BaseModel):
|
class RenameForm(BaseModel):
|
||||||
name:str
|
name:str
|
||||||
@ -49,6 +55,9 @@ class ServiceAddForm(BaseModel):
|
|||||||
proto: str
|
proto: str
|
||||||
ip_int: str
|
ip_int: str
|
||||||
fail_open: bool = True
|
fail_open: bool = True
|
||||||
|
tls_enabled: bool = False
|
||||||
|
tls_cert: str | None = None
|
||||||
|
tls_key: str | None = None
|
||||||
|
|
||||||
class ServiceAddResponse(BaseModel):
|
class ServiceAddResponse(BaseModel):
|
||||||
status:str
|
status:str
|
||||||
@ -69,6 +78,7 @@ db = SQLite('db/nft-pyfilters.db', {
|
|||||||
'l4_proto': 'VARCHAR(3) NOT NULL CHECK (l4_proto IN ("tcp", "udp"))',
|
'l4_proto': 'VARCHAR(3) NOT NULL CHECK (l4_proto IN ("tcp", "udp"))',
|
||||||
'ip_int': 'VARCHAR(100) NOT NULL',
|
'ip_int': 'VARCHAR(100) NOT NULL',
|
||||||
'fail_open': 'BOOLEAN NOT NULL CHECK (fail_open IN (0, 1)) DEFAULT 1',
|
'fail_open': 'BOOLEAN NOT NULL CHECK (fail_open IN (0, 1)) DEFAULT 1',
|
||||||
|
'tls_enabled': 'BOOLEAN NOT NULL CHECK (tls_enabled IN (0, 1)) DEFAULT 0',
|
||||||
},
|
},
|
||||||
'pyfilter': {
|
'pyfilter': {
|
||||||
'name': 'VARCHAR(100) NOT NULL',
|
'name': 'VARCHAR(100) NOT NULL',
|
||||||
@ -138,7 +148,7 @@ firewall = FirewallManager(db, outstream_func=outstream_func, exception_func=exc
|
|||||||
@app.get('/services', response_model=list[ServiceModel])
|
@app.get('/services', response_model=list[ServiceModel])
|
||||||
async def get_service_list():
|
async def get_service_list():
|
||||||
"""Get the list of existent firegex services"""
|
"""Get the list of existent firegex services"""
|
||||||
return db.query("""
|
res = db.query("""
|
||||||
SELECT
|
SELECT
|
||||||
s.service_id service_id,
|
s.service_id service_id,
|
||||||
s.status status,
|
s.status status,
|
||||||
@ -147,12 +157,14 @@ async def get_service_list():
|
|||||||
s.proto proto,
|
s.proto proto,
|
||||||
s.ip_int ip_int,
|
s.ip_int ip_int,
|
||||||
s.fail_open fail_open,
|
s.fail_open fail_open,
|
||||||
|
s.tls_enabled tls_enabled,
|
||||||
COUNT(f.name) n_filters,
|
COUNT(f.name) n_filters,
|
||||||
COALESCE(SUM(f.blocked_packets),0) blocked_packets,
|
COALESCE(SUM(f.blocked_packets),0) blocked_packets,
|
||||||
COALESCE(SUM(f.edited_packets),0) edited_packets
|
COALESCE(SUM(f.edited_packets),0) edited_packets
|
||||||
FROM services s LEFT JOIN pyfilter f ON s.service_id = f.service_id
|
FROM services s LEFT JOIN pyfilter f ON s.service_id = f.service_id
|
||||||
GROUP BY s.service_id;
|
GROUP BY s.service_id;
|
||||||
""")
|
""")
|
||||||
|
return populate_services_tls_config(res)
|
||||||
|
|
||||||
@app.get('/services/{service_id}', response_model=ServiceModel)
|
@app.get('/services/{service_id}', response_model=ServiceModel)
|
||||||
async def get_service_by_id(service_id: str):
|
async def get_service_by_id(service_id: str):
|
||||||
@ -166,6 +178,7 @@ async def get_service_by_id(service_id: str):
|
|||||||
s.proto proto,
|
s.proto proto,
|
||||||
s.ip_int ip_int,
|
s.ip_int ip_int,
|
||||||
s.fail_open fail_open,
|
s.fail_open fail_open,
|
||||||
|
s.tls_enabled tls_enabled,
|
||||||
COUNT(f.name) n_filters,
|
COUNT(f.name) n_filters,
|
||||||
COALESCE(SUM(f.blocked_packets),0) blocked_packets,
|
COALESCE(SUM(f.blocked_packets),0) blocked_packets,
|
||||||
COALESCE(SUM(f.edited_packets),0) edited_packets
|
COALESCE(SUM(f.edited_packets),0) edited_packets
|
||||||
@ -174,7 +187,7 @@ async def get_service_by_id(service_id: str):
|
|||||||
""", service_id)
|
""", service_id)
|
||||||
if len(res) == 0:
|
if len(res) == 0:
|
||||||
raise HTTPException(status_code=400, detail="This service does not exists!")
|
raise HTTPException(status_code=400, detail="This service does not exists!")
|
||||||
return res[0]
|
return populate_services_tls_config(res)[0]
|
||||||
|
|
||||||
@app.post('/services/{service_id}/stop', response_model=StatusMessageModel)
|
@app.post('/services/{service_id}/stop', response_model=StatusMessageModel)
|
||||||
async def service_stop(service_id: str):
|
async def service_stop(service_id: str):
|
||||||
@ -193,6 +206,9 @@ async def service_start(service_id: str):
|
|||||||
@app.delete('/services/{service_id}', response_model=StatusMessageModel)
|
@app.delete('/services/{service_id}', response_model=StatusMessageModel)
|
||||||
async def service_delete(service_id: str):
|
async def service_delete(service_id: str):
|
||||||
"""Request the deletion of a specific service"""
|
"""Request the deletion of a specific service"""
|
||||||
|
srv_res = db.query('SELECT ip_int, port FROM services WHERE service_id = ?;', service_id)
|
||||||
|
if srv_res:
|
||||||
|
CertsDB().delete_cert_and_key(srv_res[0]["ip_int"], srv_res[0]["port"])
|
||||||
db.query('DELETE FROM services WHERE service_id = ?;', service_id)
|
db.query('DELETE FROM services WHERE service_id = ?;', service_id)
|
||||||
db.query('DELETE FROM pyfilter WHERE service_id = ?;', service_id)
|
db.query('DELETE FROM pyfilter WHERE service_id = ?;', service_id)
|
||||||
if os.path.exists(f"db/nfproxy_filters/{service_id}.py"):
|
if os.path.exists(f"db/nfproxy_filters/{service_id}.py"):
|
||||||
@ -218,6 +234,11 @@ async def service_rename(service_id: str, form: RenameForm):
|
|||||||
async def service_settings(service_id: str, form: SettingsForm):
|
async def service_settings(service_id: str, form: SettingsForm):
|
||||||
"""Request to change the settings of a specific service (will cause a restart)"""
|
"""Request to change the settings of a specific service (will cause a restart)"""
|
||||||
|
|
||||||
|
srv_check = db.query("SELECT ip_int, port, tls_enabled FROM services WHERE service_id = ?;", service_id)
|
||||||
|
if len(srv_check) == 0:
|
||||||
|
raise HTTPException(status_code=404, detail="Service not found")
|
||||||
|
old_srv = srv_check[0]
|
||||||
|
|
||||||
if form.port is not None and (form.port < 1 or form.port > 65535):
|
if form.port is not None and (form.port < 1 or form.port > 65535):
|
||||||
raise HTTPException(status_code=400, detail="Invalid port")
|
raise HTTPException(status_code=400, detail="Invalid port")
|
||||||
|
|
||||||
@ -227,6 +248,9 @@ async def service_settings(service_id: str, form: SettingsForm):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
raise HTTPException(status_code=400, detail="Invalid address")
|
raise HTTPException(status_code=400, detail="Invalid address")
|
||||||
|
|
||||||
|
new_ip = form.ip_int if form.ip_int is not None else old_srv["ip_int"]
|
||||||
|
new_port = form.port if form.port is not None else old_srv["port"]
|
||||||
|
|
||||||
keys = []
|
keys = []
|
||||||
values = []
|
values = []
|
||||||
|
|
||||||
@ -237,9 +261,21 @@ async def service_settings(service_id: str, form: SettingsForm):
|
|||||||
if len(keys) == 0:
|
if len(keys) == 0:
|
||||||
raise HTTPException(status_code=400, detail="No settings to change provided")
|
raise HTTPException(status_code=400, detail="No settings to change provided")
|
||||||
|
|
||||||
|
if old_srv["tls_enabled"] and (new_ip != old_srv["ip_int"] or new_port != old_srv["port"]):
|
||||||
|
cert, key = CertsDB().get_cert_and_key(old_srv["ip_int"], old_srv["port"])
|
||||||
|
if cert and key:
|
||||||
|
CertsDB().upsert_cert_and_key(new_ip, new_port, cert, key)
|
||||||
|
CertsDB().delete_cert_and_key(old_srv["ip_int"], old_srv["port"])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
db.query(f'UPDATE services SET {", ".join([f"{key}=?" for key in keys])} WHERE service_id = ?;', *values, service_id)
|
db.query(f'UPDATE services SET {", ".join([f"{key}=?" for key in keys])} WHERE service_id = ?;', *values, service_id)
|
||||||
except sqlite3.IntegrityError:
|
except sqlite3.IntegrityError:
|
||||||
|
# If migration was performed, rollback CertsDB change (upsert old, delete new)
|
||||||
|
if old_srv["tls_enabled"] and (new_ip != old_srv["ip_int"] or new_port != old_srv["port"]):
|
||||||
|
cert, key = CertsDB().get_cert_and_key(new_ip, new_port)
|
||||||
|
if cert and key:
|
||||||
|
CertsDB().upsert_cert_and_key(old_srv["ip_int"], old_srv["port"], cert, key)
|
||||||
|
CertsDB().delete_cert_and_key(new_ip, new_port)
|
||||||
raise HTTPException(status_code=400, detail="A service with these settings already exists")
|
raise HTTPException(status_code=400, detail="A service with these settings already exists")
|
||||||
|
|
||||||
old_status = firewall.get(service_id).status
|
old_status = firewall.get(service_id).status
|
||||||
@ -250,6 +286,29 @@ async def service_settings(service_id: str, form: SettingsForm):
|
|||||||
await refresh_frontend()
|
await refresh_frontend()
|
||||||
return {'status': 'ok'}
|
return {'status': 'ok'}
|
||||||
|
|
||||||
|
@app.put('/services/{service_id}/tls-config', response_model=StatusMessageModel)
|
||||||
|
async def update_service_tls(service_id: str, form: TLSConfigForm):
|
||||||
|
"""Update TLS configuration for a service"""
|
||||||
|
srv_res = db.query("SELECT ip_int, port FROM services WHERE service_id = ?;", service_id)
|
||||||
|
if len(srv_res) == 0:
|
||||||
|
raise HTTPException(status_code=404, detail="Service not found")
|
||||||
|
srv = srv_res[0]
|
||||||
|
|
||||||
|
if form.tls_enabled:
|
||||||
|
if not form.tls_cert or not form.tls_key:
|
||||||
|
raise HTTPException(status_code=400, detail="Cert and Key are required when TLS is enabled")
|
||||||
|
CertsDB().upsert_cert_and_key(srv["ip_int"], srv["port"], form.tls_cert, form.tls_key)
|
||||||
|
|
||||||
|
db.query("""
|
||||||
|
UPDATE services
|
||||||
|
SET tls_enabled = ?
|
||||||
|
WHERE service_id = ?;
|
||||||
|
""", int(form.tls_enabled), service_id)
|
||||||
|
|
||||||
|
await firewall.get(service_id).update_tls_config()
|
||||||
|
await refresh_frontend()
|
||||||
|
return {'status': 'ok'}
|
||||||
|
|
||||||
@app.get('/services/{service_id}/pyfilters', response_model=list[PyFilterModel])
|
@app.get('/services/{service_id}/pyfilters', response_model=list[PyFilterModel])
|
||||||
async def get_service_pyfilter_list(service_id: str):
|
async def get_service_pyfilter_list(service_id: str):
|
||||||
"""Get the list of the pyfilters of a service"""
|
"""Get the list of the pyfilters of a service"""
|
||||||
@ -302,11 +361,16 @@ async def add_new_service(form: ServiceAddForm):
|
|||||||
raise HTTPException(status_code=400, detail="Invalid address")
|
raise HTTPException(status_code=400, detail="Invalid address")
|
||||||
if form.proto not in ["tcp", "http"]:
|
if form.proto not in ["tcp", "http"]:
|
||||||
raise HTTPException(status_code=400, detail="Invalid protocol")
|
raise HTTPException(status_code=400, detail="Invalid protocol")
|
||||||
|
if form.tls_enabled:
|
||||||
|
if not form.tls_cert or not form.tls_key:
|
||||||
|
raise HTTPException(status_code=400, detail="Cert and Key are required when TLS is enabled")
|
||||||
|
CertsDB().upsert_cert_and_key(form.ip_int, form.port, form.tls_cert, form.tls_key)
|
||||||
|
|
||||||
srv_id = None
|
srv_id = None
|
||||||
try:
|
try:
|
||||||
srv_id = gen_service_id()
|
srv_id = gen_service_id()
|
||||||
db.query("INSERT INTO services (service_id ,name, port, status, proto, ip_int, fail_open, l4_proto) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
db.query("INSERT INTO services (service_id, name, port, status, proto, ip_int, fail_open, l4_proto, tls_enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
srv_id, refactor_name(form.name), form.port, STATUS.STOP, form.proto, form.ip_int, form.fail_open, convert_protocol_to_l4(form.proto))
|
srv_id, refactor_name(form.name), form.port, STATUS.STOP, form.proto, form.ip_int, form.fail_open, convert_protocol_to_l4(form.proto), int(form.tls_enabled))
|
||||||
except sqlite3.IntegrityError:
|
except sqlite3.IntegrityError:
|
||||||
raise HTTPException(status_code=400, detail="This type of service already exists")
|
raise HTTPException(status_code=400, detail="This type of service already exists")
|
||||||
await firewall.reload()
|
await firewall.reload()
|
||||||
|
|||||||
@ -8,7 +8,8 @@ from modules.nfregex.nftables import FiregexTables
|
|||||||
from modules.nfregex.firewall import STATUS, FirewallManager
|
from modules.nfregex.firewall import STATUS, FirewallManager
|
||||||
from utils.sqlite import SQLite
|
from utils.sqlite import SQLite
|
||||||
from utils import ip_parse, refactor_name, socketio_emit, PortType
|
from utils import ip_parse, refactor_name, socketio_emit, PortType
|
||||||
from utils.models import ResetRequest, StatusMessageModel
|
from utils.certs import CertsDB, populate_services_tls_config
|
||||||
|
from utils.models import ResetRequest, StatusMessageModel, TLSConfigForm
|
||||||
from modules.nfregex.firegex import test_regex_validity
|
from modules.nfregex.firegex import test_regex_validity
|
||||||
|
|
||||||
class ServiceModel(BaseModel):
|
class ServiceModel(BaseModel):
|
||||||
@ -21,6 +22,11 @@ class ServiceModel(BaseModel):
|
|||||||
n_regex: int
|
n_regex: int
|
||||||
n_packets: int
|
n_packets: int
|
||||||
fail_open: bool
|
fail_open: bool
|
||||||
|
tls_enabled: bool = False
|
||||||
|
tls_cert: str|None = None
|
||||||
|
tls_key: str|None = None
|
||||||
|
ssl_port: int|None = None
|
||||||
|
clear_port: int|None = None
|
||||||
|
|
||||||
class RenameForm(BaseModel):
|
class RenameForm(BaseModel):
|
||||||
name:str
|
name:str
|
||||||
@ -53,6 +59,9 @@ class ServiceAddForm(BaseModel):
|
|||||||
proto: str
|
proto: str
|
||||||
ip_int: str
|
ip_int: str
|
||||||
fail_open: bool = False
|
fail_open: bool = False
|
||||||
|
tls_enabled: bool = False
|
||||||
|
tls_cert: str | None = None
|
||||||
|
tls_key: str | None = None
|
||||||
|
|
||||||
class ServiceAddResponse(BaseModel):
|
class ServiceAddResponse(BaseModel):
|
||||||
status:str
|
status:str
|
||||||
@ -68,7 +77,8 @@ db = SQLite('db/nft-regex.db', {
|
|||||||
'name': 'VARCHAR(100) NOT NULL UNIQUE',
|
'name': 'VARCHAR(100) NOT NULL UNIQUE',
|
||||||
'proto': 'VARCHAR(3) NOT NULL CHECK (proto IN ("tcp", "udp"))',
|
'proto': 'VARCHAR(3) NOT NULL CHECK (proto IN ("tcp", "udp"))',
|
||||||
'ip_int': 'VARCHAR(100) NOT NULL',
|
'ip_int': 'VARCHAR(100) NOT NULL',
|
||||||
'fail_open': 'BOOLEAN NOT NULL CHECK (fail_open IN (0, 1)) DEFAULT 1'
|
'fail_open': 'BOOLEAN NOT NULL CHECK (fail_open IN (0, 1)) DEFAULT 1',
|
||||||
|
'tls_enabled': 'BOOLEAN NOT NULL CHECK (tls_enabled IN (0, 1)) DEFAULT 0',
|
||||||
},
|
},
|
||||||
'regexes': {
|
'regexes': {
|
||||||
'regex': 'TEXT NOT NULL',
|
'regex': 'TEXT NOT NULL',
|
||||||
@ -130,7 +140,7 @@ firewall = FirewallManager(db)
|
|||||||
@app.get('/services', response_model=list[ServiceModel])
|
@app.get('/services', response_model=list[ServiceModel])
|
||||||
async def get_service_list():
|
async def get_service_list():
|
||||||
"""Get the list of existent firegex services"""
|
"""Get the list of existent firegex services"""
|
||||||
return db.query("""
|
res = db.query("""
|
||||||
SELECT
|
SELECT
|
||||||
s.service_id service_id,
|
s.service_id service_id,
|
||||||
s.status status,
|
s.status status,
|
||||||
@ -139,11 +149,13 @@ async def get_service_list():
|
|||||||
s.proto proto,
|
s.proto proto,
|
||||||
s.ip_int ip_int,
|
s.ip_int ip_int,
|
||||||
s.fail_open fail_open,
|
s.fail_open fail_open,
|
||||||
|
s.tls_enabled tls_enabled,
|
||||||
COUNT(r.regex_id) n_regex,
|
COUNT(r.regex_id) n_regex,
|
||||||
COALESCE(SUM(r.blocked_packets),0) n_packets
|
COALESCE(SUM(r.blocked_packets),0) n_packets
|
||||||
FROM services s LEFT JOIN regexes r ON s.service_id = r.service_id
|
FROM services s LEFT JOIN regexes r ON s.service_id = r.service_id
|
||||||
GROUP BY s.service_id;
|
GROUP BY s.service_id;
|
||||||
""")
|
""")
|
||||||
|
return populate_services_tls_config(res)
|
||||||
|
|
||||||
@app.get('/services/{service_id}', response_model=ServiceModel)
|
@app.get('/services/{service_id}', response_model=ServiceModel)
|
||||||
async def get_service_by_id(service_id: str):
|
async def get_service_by_id(service_id: str):
|
||||||
@ -157,6 +169,7 @@ async def get_service_by_id(service_id: str):
|
|||||||
s.proto proto,
|
s.proto proto,
|
||||||
s.ip_int ip_int,
|
s.ip_int ip_int,
|
||||||
s.fail_open fail_open,
|
s.fail_open fail_open,
|
||||||
|
s.tls_enabled tls_enabled,
|
||||||
COUNT(r.regex_id) n_regex,
|
COUNT(r.regex_id) n_regex,
|
||||||
COALESCE(SUM(r.blocked_packets),0) n_packets
|
COALESCE(SUM(r.blocked_packets),0) n_packets
|
||||||
FROM services s LEFT JOIN regexes r ON s.service_id = r.service_id
|
FROM services s LEFT JOIN regexes r ON s.service_id = r.service_id
|
||||||
@ -164,7 +177,7 @@ async def get_service_by_id(service_id: str):
|
|||||||
""", service_id)
|
""", service_id)
|
||||||
if len(res) == 0:
|
if len(res) == 0:
|
||||||
raise HTTPException(status_code=400, detail="This service does not exists!")
|
raise HTTPException(status_code=400, detail="This service does not exists!")
|
||||||
return res[0]
|
return populate_services_tls_config(res)[0]
|
||||||
|
|
||||||
@app.post('/services/{service_id}/stop', response_model=StatusMessageModel)
|
@app.post('/services/{service_id}/stop', response_model=StatusMessageModel)
|
||||||
async def service_stop(service_id: str):
|
async def service_stop(service_id: str):
|
||||||
@ -183,6 +196,9 @@ async def service_start(service_id: str):
|
|||||||
@app.delete('/services/{service_id}', response_model=StatusMessageModel)
|
@app.delete('/services/{service_id}', response_model=StatusMessageModel)
|
||||||
async def service_delete(service_id: str):
|
async def service_delete(service_id: str):
|
||||||
"""Request the deletion of a specific service"""
|
"""Request the deletion of a specific service"""
|
||||||
|
srv_res = db.query('SELECT ip_int, port FROM services WHERE service_id = ?;', service_id)
|
||||||
|
if srv_res:
|
||||||
|
CertsDB().delete_cert_and_key(srv_res[0]["ip_int"], srv_res[0]["port"])
|
||||||
db.query('DELETE FROM services WHERE service_id = ?;', service_id)
|
db.query('DELETE FROM services WHERE service_id = ?;', service_id)
|
||||||
db.query('DELETE FROM regexes WHERE service_id = ?;', service_id)
|
db.query('DELETE FROM regexes WHERE service_id = ?;', service_id)
|
||||||
await firewall.remove(service_id)
|
await firewall.remove(service_id)
|
||||||
@ -206,9 +222,19 @@ async def service_rename(service_id: str, form: RenameForm):
|
|||||||
async def service_settings(service_id: str, form: SettingsForm):
|
async def service_settings(service_id: str, form: SettingsForm):
|
||||||
"""Request to change the settings of a specific service (will cause a restart)"""
|
"""Request to change the settings of a specific service (will cause a restart)"""
|
||||||
|
|
||||||
|
srv_check = db.query("SELECT proto, ip_int, port, tls_enabled FROM services WHERE service_id = ?;", service_id)
|
||||||
|
if len(srv_check) == 0:
|
||||||
|
raise HTTPException(status_code=404, detail="Service not found")
|
||||||
|
old_srv = srv_check[0]
|
||||||
|
|
||||||
if form.proto is not None and form.proto not in ["tcp", "udp"]:
|
if form.proto is not None and form.proto not in ["tcp", "udp"]:
|
||||||
raise HTTPException(status_code=400, detail="Invalid protocol")
|
raise HTTPException(status_code=400, detail="Invalid protocol")
|
||||||
|
|
||||||
|
if old_srv["tls_enabled"]:
|
||||||
|
new_proto = form.proto if form.proto is not None else old_srv["proto"]
|
||||||
|
if new_proto != "tcp":
|
||||||
|
raise HTTPException(status_code=400, detail="TLS is only supported for TCP protocol")
|
||||||
|
|
||||||
if form.port is not None and (form.port < 1 or form.port > 65535):
|
if form.port is not None and (form.port < 1 or form.port > 65535):
|
||||||
raise HTTPException(status_code=400, detail="Invalid port")
|
raise HTTPException(status_code=400, detail="Invalid port")
|
||||||
|
|
||||||
@ -218,6 +244,9 @@ async def service_settings(service_id: str, form: SettingsForm):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
raise HTTPException(status_code=400, detail="Invalid address")
|
raise HTTPException(status_code=400, detail="Invalid address")
|
||||||
|
|
||||||
|
new_ip = form.ip_int if form.ip_int is not None else old_srv["ip_int"]
|
||||||
|
new_port = form.port if form.port is not None else old_srv["port"]
|
||||||
|
|
||||||
keys = []
|
keys = []
|
||||||
values = []
|
values = []
|
||||||
|
|
||||||
@ -228,9 +257,21 @@ async def service_settings(service_id: str, form: SettingsForm):
|
|||||||
if len(keys) == 0:
|
if len(keys) == 0:
|
||||||
raise HTTPException(status_code=400, detail="No settings to change provided")
|
raise HTTPException(status_code=400, detail="No settings to change provided")
|
||||||
|
|
||||||
|
if old_srv["tls_enabled"] and (new_ip != old_srv["ip_int"] or new_port != old_srv["port"]):
|
||||||
|
cert, key = CertsDB().get_cert_and_key(old_srv["ip_int"], old_srv["port"])
|
||||||
|
if cert and key:
|
||||||
|
CertsDB().upsert_cert_and_key(new_ip, new_port, cert, key)
|
||||||
|
CertsDB().delete_cert_and_key(old_srv["ip_int"], old_srv["port"])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
db.query(f'UPDATE services SET {", ".join([f"{key}=?" for key in keys])} WHERE service_id = ?;', *values, service_id)
|
db.query(f'UPDATE services SET {", ".join([f"{key}=?" for key in keys])} WHERE service_id = ?;', *values, service_id)
|
||||||
except sqlite3.IntegrityError:
|
except sqlite3.IntegrityError:
|
||||||
|
# If migration was performed, rollback CertsDB change (upsert old, delete new)
|
||||||
|
if old_srv["tls_enabled"] and (new_ip != old_srv["ip_int"] or new_port != old_srv["port"]):
|
||||||
|
cert, key = CertsDB().get_cert_and_key(new_ip, new_port)
|
||||||
|
if cert and key:
|
||||||
|
CertsDB().upsert_cert_and_key(old_srv["ip_int"], old_srv["port"], cert, key)
|
||||||
|
CertsDB().delete_cert_and_key(new_ip, new_port)
|
||||||
raise HTTPException(status_code=400, detail="A service with these settings already exists")
|
raise HTTPException(status_code=400, detail="A service with these settings already exists")
|
||||||
|
|
||||||
old_status = firewall.get(service_id).status
|
old_status = firewall.get(service_id).status
|
||||||
@ -322,17 +363,49 @@ async def add_new_service(form: ServiceAddForm):
|
|||||||
raise HTTPException(status_code=400, detail="Invalid address")
|
raise HTTPException(status_code=400, detail="Invalid address")
|
||||||
if form.proto not in ["tcp", "udp"]:
|
if form.proto not in ["tcp", "udp"]:
|
||||||
raise HTTPException(status_code=400, detail="Invalid protocol")
|
raise HTTPException(status_code=400, detail="Invalid protocol")
|
||||||
|
if form.tls_enabled:
|
||||||
|
if form.proto != "tcp":
|
||||||
|
raise HTTPException(status_code=400, detail="TLS is only supported for TCP protocol")
|
||||||
|
if not form.tls_cert or not form.tls_key:
|
||||||
|
raise HTTPException(status_code=400, detail="Cert and Key are required when TLS is enabled")
|
||||||
|
CertsDB().upsert_cert_and_key(form.ip_int, form.port, form.tls_cert, form.tls_key)
|
||||||
|
|
||||||
srv_id = None
|
srv_id = None
|
||||||
try:
|
try:
|
||||||
srv_id = gen_service_id()
|
srv_id = gen_service_id()
|
||||||
db.query("INSERT INTO services (service_id ,name, port, status, proto, ip_int, fail_open) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
db.query("INSERT INTO services (service_id ,name, port, status, proto, ip_int, fail_open, tls_enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
srv_id, refactor_name(form.name), form.port, STATUS.STOP, form.proto, form.ip_int, form.fail_open)
|
srv_id, refactor_name(form.name), form.port, STATUS.STOP, form.proto, form.ip_int, form.fail_open, int(form.tls_enabled))
|
||||||
except sqlite3.IntegrityError:
|
except sqlite3.IntegrityError:
|
||||||
raise HTTPException(status_code=400, detail="This type of service already exists")
|
raise HTTPException(status_code=400, detail="This type of service already exists")
|
||||||
await firewall.reload()
|
await firewall.reload()
|
||||||
await refresh_frontend()
|
await refresh_frontend()
|
||||||
return {'status': 'ok', 'service_id': srv_id}
|
return {'status': 'ok', 'service_id': srv_id}
|
||||||
|
|
||||||
|
@app.put('/services/{service_id}/tls-config', response_model=StatusMessageModel)
|
||||||
|
async def update_service_tls(service_id: str, form: TLSConfigForm):
|
||||||
|
"""Update TLS configuration for a service"""
|
||||||
|
srv_res = db.query("SELECT ip_int, port, proto FROM services WHERE service_id = ?;", service_id)
|
||||||
|
if len(srv_res) == 0:
|
||||||
|
raise HTTPException(status_code=404, detail="Service not found")
|
||||||
|
srv = srv_res[0]
|
||||||
|
|
||||||
|
if form.tls_enabled:
|
||||||
|
if srv["proto"] != "tcp":
|
||||||
|
raise HTTPException(status_code=400, detail="TLS is only supported for TCP protocol")
|
||||||
|
if not form.tls_cert or not form.tls_key:
|
||||||
|
raise HTTPException(status_code=400, detail="Cert and Key are required when TLS is enabled")
|
||||||
|
CertsDB().upsert_cert_and_key(srv["ip_int"], srv["port"], form.tls_cert, form.tls_key)
|
||||||
|
|
||||||
|
db.query("""
|
||||||
|
UPDATE services
|
||||||
|
SET tls_enabled = ?
|
||||||
|
WHERE service_id = ?;
|
||||||
|
""", int(form.tls_enabled), service_id)
|
||||||
|
|
||||||
|
await firewall.get(service_id).update_tls_config()
|
||||||
|
await refresh_frontend()
|
||||||
|
return {'status': 'ok'}
|
||||||
|
|
||||||
@app.get('/metrics', response_class = PlainTextResponse)
|
@app.get('/metrics', response_class = PlainTextResponse)
|
||||||
async def metrics():
|
async def metrics():
|
||||||
"""Aggregate metrics"""
|
"""Aggregate metrics"""
|
||||||
|
|||||||
@ -305,6 +305,8 @@ def nicenessify(priority:int, pid:int|None=None):
|
|||||||
ps = psutil.Process(pid)
|
ps = psutil.Process(pid)
|
||||||
if os.name == 'posix':
|
if os.name == 'posix':
|
||||||
ps.nice(priority)
|
ps.nice(priority)
|
||||||
|
except (psutil.AccessDenied, PermissionError) as e:
|
||||||
|
print(f"Permission denied setting priority (pid={pid}, priority={priority}): {e}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error setting priority: {e} {traceback.format_exc()}")
|
print(f"Error setting priority: {e} {traceback.format_exc()}")
|
||||||
pass
|
pass
|
||||||
|
|||||||
106
backend/utils/certs.py
Normal file
106
backend/utils/certs.py
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import hashlib
|
||||||
|
from ipaddress import ip_interface
|
||||||
|
from utils.sqlite import SQLite
|
||||||
|
|
||||||
|
CERTIFICATES_SCHEMA = {
|
||||||
|
'certificates': {
|
||||||
|
'ip_int': 'VARCHAR(100) NOT NULL',
|
||||||
|
'port': 'INT NOT NULL',
|
||||||
|
'cert': 'TEXT NOT NULL',
|
||||||
|
'key': 'TEXT NOT NULL',
|
||||||
|
'PRIMARY KEY': '(ip_int, port)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_certs_db = SQLite('db/firegex.db', schema=CERTIFICATES_SCHEMA)
|
||||||
|
|
||||||
|
def ip_parse(ip: str) -> str:
|
||||||
|
try:
|
||||||
|
return str(ip_interface(ip).network)
|
||||||
|
except Exception:
|
||||||
|
return ip
|
||||||
|
|
||||||
|
def get_tls_ports(ip: str, port: int) -> tuple[int, int]:
|
||||||
|
ip_addr = ip.split("/")[0]
|
||||||
|
key = f"{ip_addr}:{port}"
|
||||||
|
h = int.from_bytes(hashlib.sha256(key.encode('utf-8')).digest()[:4], 'big')
|
||||||
|
ssl_port = 10000 + (h % 10000)
|
||||||
|
clear_port = 20000 + (h % 10000)
|
||||||
|
return ssl_port, clear_port
|
||||||
|
|
||||||
|
class CertsDB:
|
||||||
|
def __init__(self, db_path='db/firegex.db'):
|
||||||
|
# Ignore db_path if it matches the default, otherwise instantiate SQLite
|
||||||
|
# (mostly for compatibility with tests if they pass custom paths)
|
||||||
|
if db_path == 'db/firegex.db':
|
||||||
|
self.db = _certs_db
|
||||||
|
else:
|
||||||
|
self.db = SQLite(db_path)
|
||||||
|
|
||||||
|
if self.db.conn is None:
|
||||||
|
self.db.connect()
|
||||||
|
|
||||||
|
def get_cert_and_key(self, ip_int: str, port: int) -> tuple[str | None, str | None]:
|
||||||
|
ip_int = ip_parse(ip_int)
|
||||||
|
res = self.db.query("SELECT cert, key FROM certificates WHERE ip_int = ? AND port = ?;", ip_int, port)
|
||||||
|
if res:
|
||||||
|
return res[0]["cert"], res[0]["key"]
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
def get_multiple_certs_and_keys(self, services: list[dict]) -> dict[tuple[str, int], tuple[str, str]]:
|
||||||
|
if not services:
|
||||||
|
return {}
|
||||||
|
conditions = []
|
||||||
|
params = []
|
||||||
|
for s in services:
|
||||||
|
conditions.append("(ip_int = ? AND port = ?)")
|
||||||
|
params.extend([ip_parse(s["ip_int"]), s["port"]])
|
||||||
|
query_str = "SELECT ip_int, port, cert, key FROM certificates WHERE " + " OR ".join(conditions) + ";"
|
||||||
|
res = self.db.query(query_str, *params)
|
||||||
|
return {(ip_parse(row["ip_int"]), row["port"]): (row["cert"], row["key"]) for row in res}
|
||||||
|
|
||||||
|
def upsert_cert_and_key(self, ip_int: str, port: int, cert: str, key: str) -> None:
|
||||||
|
ip_int = ip_parse(ip_int)
|
||||||
|
self.db.query("INSERT OR REPLACE INTO certificates (ip_int, port, cert, key) VALUES (?, ?, ?, ?);", ip_int, port, cert, key)
|
||||||
|
|
||||||
|
def delete_cert_and_key(self, ip_int: str, port: int) -> None:
|
||||||
|
ip_int = ip_parse(ip_int)
|
||||||
|
|
||||||
|
# Check if the certificate is still used by any service in regex DB
|
||||||
|
regex_db = SQLite('db/nft-regex.db')
|
||||||
|
regex_db.connect()
|
||||||
|
try:
|
||||||
|
regex_used = regex_db.query("SELECT 1 FROM services WHERE ip_int = ? AND port = ?;", ip_int, port)
|
||||||
|
except Exception:
|
||||||
|
regex_used = []
|
||||||
|
finally:
|
||||||
|
regex_db.disconnect()
|
||||||
|
|
||||||
|
# Check if the certificate is still used by any service in proxy DB
|
||||||
|
proxy_db = SQLite('db/nft-pyfilters.db')
|
||||||
|
proxy_db.connect()
|
||||||
|
try:
|
||||||
|
proxy_used = proxy_db.query("SELECT 1 FROM services WHERE ip_int = ? AND port = ?;", ip_int, port)
|
||||||
|
except Exception:
|
||||||
|
proxy_used = []
|
||||||
|
finally:
|
||||||
|
proxy_db.disconnect()
|
||||||
|
|
||||||
|
if not regex_used and not proxy_used:
|
||||||
|
self.db.query("DELETE FROM certificates WHERE ip_int = ? AND port = ?;", ip_int, port)
|
||||||
|
|
||||||
|
|
||||||
|
def populate_services_tls_config(services: list[dict]) -> list[dict]:
|
||||||
|
if not services:
|
||||||
|
return services
|
||||||
|
certs_map = CertsDB().get_multiple_certs_and_keys(services)
|
||||||
|
for srv in services:
|
||||||
|
if srv.get("tls_enabled"):
|
||||||
|
srv["ssl_port"], srv["clear_port"] = get_tls_ports(srv["ip_int"], srv["port"])
|
||||||
|
cert, key = certs_map.get((ip_parse(srv["ip_int"]), srv["port"]), (None, None))
|
||||||
|
srv["tls_cert"] = cert
|
||||||
|
srv["tls_key"] = key
|
||||||
|
else:
|
||||||
|
srv["ssl_port"], srv["clear_port"] = None, None
|
||||||
|
srv["tls_cert"], srv["tls_key"] = None, None
|
||||||
|
return services
|
||||||
@ -25,3 +25,8 @@ class IpInterface(BaseModel):
|
|||||||
|
|
||||||
class ResetRequest(BaseModel):
|
class ResetRequest(BaseModel):
|
||||||
delete:bool
|
delete:bool
|
||||||
|
|
||||||
|
class TLSConfigForm(BaseModel):
|
||||||
|
tls_enabled: bool
|
||||||
|
tls_cert: str|None = None
|
||||||
|
tls_key: str|None = None
|
||||||
@ -2,6 +2,7 @@ import { Button, Group, Space, TextInput, Notification, Modal, Switch, Segmented
|
|||||||
import { useForm } from '@mantine/form';
|
import { useForm } from '@mantine/form';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { okNotify, regex_ipv4, regex_ipv6 } from '../../js/utils';
|
import { okNotify, regex_ipv4, regex_ipv6 } from '../../js/utils';
|
||||||
|
import TLSServiceFields from '../TLSServiceFields';
|
||||||
import { ImCross } from "react-icons/im"
|
import { ImCross } from "react-icons/im"
|
||||||
import { nfproxy, Service } from './utils';
|
import { nfproxy, Service } from './utils';
|
||||||
import PortAndInterface from '../PortAndInterface';
|
import PortAndInterface from '../PortAndInterface';
|
||||||
@ -18,7 +19,10 @@ function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>
|
|||||||
ip_int:edit?.ip_int??"",
|
ip_int:edit?.ip_int??"",
|
||||||
proto:edit?.proto??"tcp",
|
proto:edit?.proto??"tcp",
|
||||||
fail_open: edit?.fail_open??false,
|
fail_open: edit?.fail_open??false,
|
||||||
autostart: true
|
autostart: true,
|
||||||
|
tls_enabled: false,
|
||||||
|
tls_cert: "",
|
||||||
|
tls_key: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
@ -28,6 +32,8 @@ function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>
|
|||||||
port: (value) => (value>0 && value<65536) ? null : "Invalid port",
|
port: (value) => (value>0 && value<65536) ? null : "Invalid port",
|
||||||
proto: (value) => ["tcp","http"].includes(value) ? null : "Invalid protocol",
|
proto: (value) => ["tcp","http"].includes(value) ? null : "Invalid protocol",
|
||||||
ip_int: (value) => (value.match(regex_ipv6) || value.match(regex_ipv4)) ? null : "Invalid IP address",
|
ip_int: (value) => (value.match(regex_ipv6) || value.match(regex_ipv4)) ? null : "Invalid IP address",
|
||||||
|
tls_cert: (value, values) => !edit && values.tls_enabled && !value ? "Certificate is required when TLS is enabled" : null,
|
||||||
|
tls_key: (value, values) => !edit && values.tls_enabled && !value ? "Private key is required when TLS is enabled" : null,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -38,6 +44,8 @@ function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>
|
|||||||
}
|
}
|
||||||
}, [opened])
|
}, [opened])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const close = () =>{
|
const close = () =>{
|
||||||
onClose()
|
onClose()
|
||||||
form.reset()
|
form.reset()
|
||||||
@ -47,26 +55,36 @@ function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>
|
|||||||
const [submitLoading, setSubmitLoading] = useState(false)
|
const [submitLoading, setSubmitLoading] = useState(false)
|
||||||
const [error, setError] = useState<string|null>(null)
|
const [error, setError] = useState<string|null>(null)
|
||||||
|
|
||||||
const submitRequest = ({ name, port, autostart, proto, ip_int, fail_open }:ServiceAddForm) =>{
|
const submitRequest = (values: ServiceAddForm) => {
|
||||||
setSubmitLoading(true)
|
setSubmitLoading(true)
|
||||||
if (edit){
|
if (edit){
|
||||||
nfproxy.settings(edit.service_id, { port, ip_int, fail_open }).then( res => {
|
nfproxy.settings(edit.service_id, { port: values.port, ip_int: values.ip_int, fail_open: values.fail_open }).then( res => {
|
||||||
if (!res){
|
if (!res){
|
||||||
setSubmitLoading(false)
|
setSubmitLoading(false)
|
||||||
close();
|
close();
|
||||||
okNotify(`Service ${name} settings updated`, `Successfully updated settings for service ${name}`)
|
okNotify(`Service ${values.name} settings updated`, `Successfully updated settings for service ${values.name}`)
|
||||||
}
|
}
|
||||||
}).catch( err => {
|
}).catch( err => {
|
||||||
setSubmitLoading(false)
|
setSubmitLoading(false)
|
||||||
setError("Request Failed! [ "+err+" ]")
|
setError("Request Failed! [ "+err+" ]")
|
||||||
})
|
})
|
||||||
}else{
|
}else{
|
||||||
nfproxy.servicesadd({ name, port, proto, ip_int, fail_open }).then( res => {
|
const payload = {
|
||||||
|
name: values.name,
|
||||||
|
port: values.port,
|
||||||
|
proto: values.proto,
|
||||||
|
ip_int: values.ip_int,
|
||||||
|
fail_open: values.fail_open,
|
||||||
|
tls_enabled: values.tls_enabled,
|
||||||
|
tls_cert: values.tls_enabled ? values.tls_cert : null,
|
||||||
|
tls_key: values.tls_enabled ? values.tls_key : null,
|
||||||
|
}
|
||||||
|
nfproxy.servicesadd(payload).then( res => {
|
||||||
if (res.status === "ok" && res.service_id){
|
if (res.status === "ok" && res.service_id){
|
||||||
setSubmitLoading(false)
|
setSubmitLoading(false)
|
||||||
close();
|
close();
|
||||||
if (autostart) nfproxy.servicestart(res.service_id)
|
if (values.autostart) nfproxy.servicestart(res.service_id)
|
||||||
okNotify(`Service ${name} has been added`, `Successfully added service with port ${port}`)
|
okNotify(`Service ${values.name} has been added`, `Successfully added service with port ${values.port}`)
|
||||||
}else{
|
}else{
|
||||||
setSubmitLoading(false)
|
setSubmitLoading(false)
|
||||||
setError("Invalid request! [ "+res.status+" ]")
|
setError("Invalid request! [ "+res.status+" ]")
|
||||||
@ -129,6 +147,8 @@ function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>
|
|||||||
/>}
|
/>}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{!edit && <TLSServiceFields form={form} />}
|
||||||
|
|
||||||
<Group justify='flex-end' mt="md" mb="sm">
|
<Group justify='flex-end' mt="md" mb="sm">
|
||||||
<Button loading={submitLoading} type="submit" disabled={edit?!form.isDirty():false}>{edit?"Edit Service":"Add Service"}</Button>
|
<Button loading={submitLoading} type="submit" disabled={edit?!form.isDirty():false}>{edit?"Edit Service":"Add Service"}</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
@ -10,12 +10,13 @@ import { BiRename } from 'react-icons/bi'
|
|||||||
import RenameForm from './RenameForm';
|
import RenameForm from './RenameForm';
|
||||||
import { MenuDropDownWithButton } from '../../MainLayout';
|
import { MenuDropDownWithButton } from '../../MainLayout';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { TbPlugConnected } from "react-icons/tb";
|
import { TbPlugConnected, TbShieldLock } from "react-icons/tb";
|
||||||
import { FaFilter } from "react-icons/fa";
|
import { FaFilter } from "react-icons/fa";
|
||||||
import { IoSettingsSharp } from 'react-icons/io5';
|
import { IoSettingsSharp } from 'react-icons/io5';
|
||||||
import AddEditService from '../AddEditService';
|
import AddEditService from '../AddEditService';
|
||||||
import { FaPencilAlt } from "react-icons/fa";
|
import { FaPencilAlt } from "react-icons/fa";
|
||||||
import { ExceptionWarning } from '../ExceptionWarning';
|
import { ExceptionWarning } from '../ExceptionWarning';
|
||||||
|
import TLSConfigModal from '../../TLSConfigModal';
|
||||||
|
|
||||||
export default function ServiceRow({ service, onClick }:{ service:Service, onClick?:()=>void }) {
|
export default function ServiceRow({ service, onClick }:{ service:Service, onClick?:()=>void }) {
|
||||||
|
|
||||||
@ -30,6 +31,7 @@ export default function ServiceRow({ service, onClick }:{ service:Service, onCli
|
|||||||
const [deleteModal, setDeleteModal] = useState(false)
|
const [deleteModal, setDeleteModal] = useState(false)
|
||||||
const [renameModal, setRenameModal] = useState(false)
|
const [renameModal, setRenameModal] = useState(false)
|
||||||
const [editModal, setEditModal] = useState(false)
|
const [editModal, setEditModal] = useState(false)
|
||||||
|
const [tlsModal, setTlsModal] = useState(false)
|
||||||
const isMedium = isMediumScreen()
|
const isMedium = isMediumScreen()
|
||||||
|
|
||||||
const stopService = async () => {
|
const stopService = async () => {
|
||||||
@ -91,6 +93,12 @@ export default function ServiceRow({ service, onClick }:{ service:Service, onCli
|
|||||||
<Badge size="lg" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" style={{ fontSize: "110%" }}>
|
<Badge size="lg" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" style={{ fontSize: "110%" }}>
|
||||||
:{service.port}
|
:{service.port}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
{service.tls_enabled && (
|
||||||
|
<Badge color="green" radius="md" size="lg" variant="light">
|
||||||
|
<TbShieldLock size={14} style={{ marginRight: 4, verticalAlign: 'middle' }} />
|
||||||
|
TLS (lo:{service.clear_port})
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
{isMedium?null:<Space w="xl" />}
|
{isMedium?null:<Space w="xl" />}
|
||||||
</Box>
|
</Box>
|
||||||
@ -115,6 +123,7 @@ export default function ServiceRow({ service, onClick }:{ service:Service, onCli
|
|||||||
<Menu.Item><b>Edit service</b></Menu.Item>
|
<Menu.Item><b>Edit service</b></Menu.Item>
|
||||||
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
|
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
|
||||||
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
||||||
|
<Menu.Item leftSection={<TbShieldLock size={18} />} onClick={()=>setTlsModal(true)}>Configure TLS</Menu.Item>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Menu.Label><b>Danger zone</b></Menu.Label>
|
<Menu.Label><b>Danger zone</b></Menu.Label>
|
||||||
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
||||||
@ -160,5 +169,12 @@ export default function ServiceRow({ service, onClick }:{ service:Service, onCli
|
|||||||
onClose={()=>setEditModal(false)}
|
onClose={()=>setEditModal(false)}
|
||||||
edit={service}
|
edit={service}
|
||||||
/>
|
/>
|
||||||
|
<TLSConfigModal
|
||||||
|
opened={tlsModal}
|
||||||
|
onClose={() => setTlsModal(false)}
|
||||||
|
service={service}
|
||||||
|
updateTlsConfig={nfproxy.updatetlsconfig}
|
||||||
|
queryKey={serviceQueryKey}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,14 +13,29 @@ export type Service = {
|
|||||||
edited_packets:number,
|
edited_packets:number,
|
||||||
blocked_packets:number,
|
blocked_packets:number,
|
||||||
fail_open:boolean,
|
fail_open:boolean,
|
||||||
|
tls_enabled:boolean,
|
||||||
|
tls_cert?:string,
|
||||||
|
tls_key?:string,
|
||||||
|
ssl_port?:number,
|
||||||
|
clear_port?:number,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TLSConfig = {
|
||||||
|
tls_enabled: boolean,
|
||||||
|
tls_cert: string | null,
|
||||||
|
tls_key: string | null,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export type ServiceAddForm = {
|
export type ServiceAddForm = {
|
||||||
name:string,
|
name:string,
|
||||||
port:number,
|
port:number,
|
||||||
proto:string,
|
proto:string,
|
||||||
ip_int:string,
|
ip_int:string,
|
||||||
fail_open: boolean,
|
fail_open: boolean,
|
||||||
|
tls_enabled?: boolean,
|
||||||
|
tls_cert?: string | null,
|
||||||
|
tls_key?: string | null,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ServiceSettings = {
|
export type ServiceSettings = {
|
||||||
@ -88,6 +103,10 @@ export const nfproxy = {
|
|||||||
const { status } = await putapi(`nfproxy/services/${service_id}/settings`,data) as ServerResponse;
|
const { status } = await putapi(`nfproxy/services/${service_id}/settings`,data) as ServerResponse;
|
||||||
return status === "ok"?undefined:status
|
return status === "ok"?undefined:status
|
||||||
},
|
},
|
||||||
|
updatetlsconfig: async (service_id:string, data:TLSConfig) => {
|
||||||
|
const { status } = await putapi(`nfproxy/services/${service_id}/tls-config`,data) as ServerResponse;
|
||||||
|
return status === "ok"?undefined:status
|
||||||
|
},
|
||||||
getpyfilterscode: async (service_id:string) => {
|
getpyfilterscode: async (service_id:string) => {
|
||||||
return await getapi(`nfproxy/services/${service_id}/code`) as string;
|
return await getapi(`nfproxy/services/${service_id}/code`) as string;
|
||||||
},
|
},
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { Button, Group, Space, TextInput, Notification, Modal, Switch, Segmented
|
|||||||
import { useForm } from '@mantine/form';
|
import { useForm } from '@mantine/form';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { okNotify, regex_ipv4, regex_ipv6 } from '../../js/utils';
|
import { okNotify, regex_ipv4, regex_ipv6 } from '../../js/utils';
|
||||||
|
import TLSServiceFields from '../TLSServiceFields';
|
||||||
import { ImCross } from "react-icons/im"
|
import { ImCross } from "react-icons/im"
|
||||||
import { nfregex, Service } from './utils';
|
import { nfregex, Service } from './utils';
|
||||||
import PortAndInterface from '../PortAndInterface';
|
import PortAndInterface from '../PortAndInterface';
|
||||||
@ -18,7 +19,10 @@ function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>
|
|||||||
ip_int:edit?.ip_int??"",
|
ip_int:edit?.ip_int??"",
|
||||||
proto:edit?.proto??"tcp",
|
proto:edit?.proto??"tcp",
|
||||||
fail_open: edit?.fail_open??false,
|
fail_open: edit?.fail_open??false,
|
||||||
autostart: true
|
autostart: true,
|
||||||
|
tls_enabled: false,
|
||||||
|
tls_cert: "",
|
||||||
|
tls_key: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
@ -28,6 +32,8 @@ function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>
|
|||||||
port: (value) => (value>0 && value<65536) ? null : "Invalid port",
|
port: (value) => (value>0 && value<65536) ? null : "Invalid port",
|
||||||
proto: (value) => ["tcp","udp"].includes(value) ? null : "Invalid protocol",
|
proto: (value) => ["tcp","udp"].includes(value) ? null : "Invalid protocol",
|
||||||
ip_int: (value) => (value.match(regex_ipv6) || value.match(regex_ipv4)) ? null : "Invalid IP address",
|
ip_int: (value) => (value.match(regex_ipv6) || value.match(regex_ipv4)) ? null : "Invalid IP address",
|
||||||
|
tls_cert: (value, values) => !edit && values.tls_enabled && !value ? "Certificate is required when TLS is enabled" : null,
|
||||||
|
tls_key: (value, values) => !edit && values.tls_enabled && !value ? "Private key is required when TLS is enabled" : null,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -38,6 +44,8 @@ function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>
|
|||||||
}
|
}
|
||||||
}, [opened])
|
}, [opened])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const close = () =>{
|
const close = () =>{
|
||||||
onClose()
|
onClose()
|
||||||
form.reset()
|
form.reset()
|
||||||
@ -47,26 +55,36 @@ function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>
|
|||||||
const [submitLoading, setSubmitLoading] = useState(false)
|
const [submitLoading, setSubmitLoading] = useState(false)
|
||||||
const [error, setError] = useState<string|null>(null)
|
const [error, setError] = useState<string|null>(null)
|
||||||
|
|
||||||
const submitRequest = ({ name, port, autostart, proto, ip_int, fail_open }:ServiceAddForm) =>{
|
const submitRequest = (values: ServiceAddForm) => {
|
||||||
setSubmitLoading(true)
|
setSubmitLoading(true)
|
||||||
if (edit){
|
if (edit){
|
||||||
nfregex.settings(edit.service_id, { port, proto, ip_int, fail_open }).then( res => {
|
nfregex.settings(edit.service_id, { port: values.port, proto: values.proto, ip_int: values.ip_int, fail_open: values.fail_open }).then( res => {
|
||||||
if (!res){
|
if (!res){
|
||||||
setSubmitLoading(false)
|
setSubmitLoading(false)
|
||||||
close();
|
close();
|
||||||
okNotify(`Service ${name} settings updated`, `Successfully updated settings for service ${name}`)
|
okNotify(`Service ${values.name} settings updated`, `Successfully updated settings for service ${values.name}`)
|
||||||
}
|
}
|
||||||
}).catch( err => {
|
}).catch( err => {
|
||||||
setSubmitLoading(false)
|
setSubmitLoading(false)
|
||||||
setError("Request Failed! [ "+err+" ]")
|
setError("Request Failed! [ "+err+" ]")
|
||||||
})
|
})
|
||||||
}else{
|
}else{
|
||||||
nfregex.servicesadd({ name, port, proto, ip_int, fail_open }).then( res => {
|
const payload = {
|
||||||
|
name: values.name,
|
||||||
|
port: values.port,
|
||||||
|
proto: values.proto,
|
||||||
|
ip_int: values.ip_int,
|
||||||
|
fail_open: values.fail_open,
|
||||||
|
tls_enabled: values.tls_enabled,
|
||||||
|
tls_cert: values.tls_enabled ? values.tls_cert : null,
|
||||||
|
tls_key: values.tls_enabled ? values.tls_key : null,
|
||||||
|
}
|
||||||
|
nfregex.servicesadd(payload).then( res => {
|
||||||
if (res.status === "ok" && res.service_id){
|
if (res.status === "ok" && res.service_id){
|
||||||
setSubmitLoading(false)
|
setSubmitLoading(false)
|
||||||
close();
|
close();
|
||||||
if (autostart) nfregex.servicestart(res.service_id)
|
if (values.autostart) nfregex.servicestart(res.service_id)
|
||||||
okNotify(`Service ${name} has been added`, `Successfully added service with port ${port}`)
|
okNotify(`Service ${values.name} has been added`, `Successfully added service with port ${values.port}`)
|
||||||
}else{
|
}else{
|
||||||
setSubmitLoading(false)
|
setSubmitLoading(false)
|
||||||
setError("Invalid request! [ "+res.status+" ]")
|
setError("Invalid request! [ "+res.status+" ]")
|
||||||
@ -129,6 +147,8 @@ function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{!edit && <TLSServiceFields form={form} disabled={form.values.proto !== 'tcp'} />}
|
||||||
|
|
||||||
<Group justify='flex-end' mt="md" mb="sm">
|
<Group justify='flex-end' mt="md" mb="sm">
|
||||||
<Button loading={submitLoading} type="submit" disabled={edit?!form.isDirty():false}>{edit?"Edit Service":"Add Service"}</Button>
|
<Button loading={submitLoading} type="submit" disabled={edit?!form.isDirty():false}>{edit?"Edit Service":"Add Service"}</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
@ -14,6 +14,8 @@ import { FaFilter } from "react-icons/fa";
|
|||||||
import { VscRegex } from "react-icons/vsc";
|
import { VscRegex } from "react-icons/vsc";
|
||||||
import { IoSettingsSharp } from 'react-icons/io5';
|
import { IoSettingsSharp } from 'react-icons/io5';
|
||||||
import AddEditService from '../AddEditService';
|
import AddEditService from '../AddEditService';
|
||||||
|
import { TbShieldLock } from "react-icons/tb";
|
||||||
|
import TLSConfigModal from '../../TLSConfigModal';
|
||||||
|
|
||||||
export default function ServiceRow({ service, onClick }:{ service:Service, onClick?:()=>void }) {
|
export default function ServiceRow({ service, onClick }:{ service:Service, onClick?:()=>void }) {
|
||||||
|
|
||||||
@ -28,6 +30,7 @@ export default function ServiceRow({ service, onClick }:{ service:Service, onCli
|
|||||||
const [deleteModal, setDeleteModal] = useState(false)
|
const [deleteModal, setDeleteModal] = useState(false)
|
||||||
const [renameModal, setRenameModal] = useState(false)
|
const [renameModal, setRenameModal] = useState(false)
|
||||||
const [editModal, setEditModal] = useState(false)
|
const [editModal, setEditModal] = useState(false)
|
||||||
|
const [tlsModal, setTlsModal] = useState(false)
|
||||||
const isMedium = isMediumScreen()
|
const isMedium = isMediumScreen()
|
||||||
|
|
||||||
const stopService = async () => {
|
const stopService = async () => {
|
||||||
@ -89,6 +92,12 @@ export default function ServiceRow({ service, onClick }:{ service:Service, onCli
|
|||||||
<Badge size="lg" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" style={{ fontSize: "110%" }}>
|
<Badge size="lg" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" style={{ fontSize: "110%" }}>
|
||||||
:{service.port}
|
:{service.port}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
{service.tls_enabled && (
|
||||||
|
<Badge color="green" radius="md" size="lg" variant="light">
|
||||||
|
<TbShieldLock size={14} style={{ marginRight: 4, verticalAlign: 'middle' }} />
|
||||||
|
TLS (lo:{service.clear_port})
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
{isMedium?null:<Space w="xl" />}
|
{isMedium?null:<Space w="xl" />}
|
||||||
</Box>
|
</Box>
|
||||||
@ -109,6 +118,7 @@ export default function ServiceRow({ service, onClick }:{ service:Service, onCli
|
|||||||
<Menu.Item><b>Edit service</b></Menu.Item>
|
<Menu.Item><b>Edit service</b></Menu.Item>
|
||||||
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
|
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
|
||||||
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
||||||
|
<Menu.Item leftSection={<TbShieldLock size={18} />} onClick={()=>setTlsModal(true)}>Configure TLS</Menu.Item>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Menu.Label><b>Danger zone</b></Menu.Label>
|
<Menu.Label><b>Danger zone</b></Menu.Label>
|
||||||
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
||||||
@ -154,5 +164,12 @@ export default function ServiceRow({ service, onClick }:{ service:Service, onCli
|
|||||||
onClose={()=>setEditModal(false)}
|
onClose={()=>setEditModal(false)}
|
||||||
edit={service}
|
edit={service}
|
||||||
/>
|
/>
|
||||||
|
<TLSConfigModal
|
||||||
|
opened={tlsModal}
|
||||||
|
onClose={() => setTlsModal(false)}
|
||||||
|
service={service}
|
||||||
|
updateTlsConfig={nfregex.updatetlsconfig}
|
||||||
|
queryKey={serviceQueryKey}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,11 @@ export type Service = {
|
|||||||
n_packets:number,
|
n_packets:number,
|
||||||
n_regex:number,
|
n_regex:number,
|
||||||
fail_open:boolean,
|
fail_open:boolean,
|
||||||
|
tls_enabled:boolean,
|
||||||
|
tls_cert?: string,
|
||||||
|
tls_key?: string,
|
||||||
|
ssl_port?: number,
|
||||||
|
clear_port?: number,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ServiceAddForm = {
|
export type ServiceAddForm = {
|
||||||
@ -21,6 +26,9 @@ export type ServiceAddForm = {
|
|||||||
proto:string,
|
proto:string,
|
||||||
ip_int:string,
|
ip_int:string,
|
||||||
fail_open: boolean,
|
fail_open: boolean,
|
||||||
|
tls_enabled?: boolean,
|
||||||
|
tls_cert?: string | null,
|
||||||
|
tls_key?: string | null,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ServiceSettings = {
|
export type ServiceSettings = {
|
||||||
@ -28,6 +36,9 @@ export type ServiceSettings = {
|
|||||||
proto?:string,
|
proto?:string,
|
||||||
ip_int?:string,
|
ip_int?:string,
|
||||||
fail_open?: boolean,
|
fail_open?: boolean,
|
||||||
|
tls_enabled?: boolean,
|
||||||
|
tls_cert?: string | null,
|
||||||
|
tls_key?: string | null,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ServiceAddResponse = {
|
export type ServiceAddResponse = {
|
||||||
@ -92,4 +103,8 @@ export const nfregex = {
|
|||||||
const { status } = await putapi(`nfregex/services/${service_id}/settings`,data) as ServerResponse;
|
const { status } = await putapi(`nfregex/services/${service_id}/settings`,data) as ServerResponse;
|
||||||
return status === "ok"?undefined:status
|
return status === "ok"?undefined:status
|
||||||
},
|
},
|
||||||
|
updatetlsconfig: async (service_id: string, data: { tls_enabled: boolean, tls_cert: string | null, tls_key: string | null }) => {
|
||||||
|
const { status } = await putapi(`nfregex/services/${service_id}/tls-config`, data) as ServerResponse;
|
||||||
|
return status === "ok" ? undefined : status;
|
||||||
|
},
|
||||||
}
|
}
|
||||||
137
frontend/src/components/TLSConfigModal.tsx
Normal file
137
frontend/src/components/TLSConfigModal.tsx
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
import { Button, Group, Space, Textarea, Switch, Modal, Notification } from '@mantine/core';
|
||||||
|
import { useForm } from '@mantine/form';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { ImCross } from "react-icons/im";
|
||||||
|
import { okNotify } from '../js/utils';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
export interface Service {
|
||||||
|
service_id: string;
|
||||||
|
name: string;
|
||||||
|
port: number;
|
||||||
|
proto: string;
|
||||||
|
ip_int: string;
|
||||||
|
fail_open: boolean;
|
||||||
|
tls_enabled: boolean;
|
||||||
|
tls_cert?: string;
|
||||||
|
tls_key?: string;
|
||||||
|
ssl_port?: number;
|
||||||
|
clear_port?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TLSConfigModalProps {
|
||||||
|
opened: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
service: Service;
|
||||||
|
updateTlsConfig: (serviceId: string, data: { tls_enabled: boolean; tls_cert: string | null; tls_key: string | null }) => Promise<string | undefined>;
|
||||||
|
queryKey: readonly unknown[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TLSConfigModal({ opened, onClose, service, updateTlsConfig, queryKey }: TLSConfigModalProps) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [submitLoading, setSubmitLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
initialValues: {
|
||||||
|
tls_enabled: service.tls_enabled,
|
||||||
|
tls_cert: service.tls_cert ?? "",
|
||||||
|
tls_key: service.tls_key ?? "",
|
||||||
|
},
|
||||||
|
validate: {
|
||||||
|
tls_cert: (value, values) => values.tls_enabled && !value.trim() ? "Certificate is required when TLS is enabled" : null,
|
||||||
|
tls_key: (value, values) => values.tls_enabled && !value.trim() ? "Private key is required when TLS is enabled" : null,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (opened) {
|
||||||
|
form.setValues({
|
||||||
|
tls_enabled: service.tls_enabled,
|
||||||
|
tls_cert: service.tls_cert ?? "",
|
||||||
|
tls_key: service.tls_key ?? "",
|
||||||
|
});
|
||||||
|
setError(null);
|
||||||
|
}
|
||||||
|
}, [opened, service]);
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
onClose();
|
||||||
|
form.reset();
|
||||||
|
setError(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitRequest = (values: { tls_enabled: boolean; tls_cert: string; tls_key: string }) => {
|
||||||
|
setSubmitLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
tls_enabled: values.tls_enabled,
|
||||||
|
tls_cert: values.tls_enabled ? values.tls_cert : null,
|
||||||
|
tls_key: values.tls_enabled ? values.tls_key : null,
|
||||||
|
};
|
||||||
|
|
||||||
|
updateTlsConfig(service.service_id, payload)
|
||||||
|
.then((res) => {
|
||||||
|
setSubmitLoading(false);
|
||||||
|
if (!res) {
|
||||||
|
close();
|
||||||
|
okNotify(`TLS configuration updated`, `Successfully updated TLS configuration for service ${service.name}`);
|
||||||
|
queryClient.invalidateQueries({ queryKey });
|
||||||
|
} else {
|
||||||
|
setError(`Error: [ ${res} ]`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setSubmitLoading(false);
|
||||||
|
setError(`Request Failed! [ ${err} ]`);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal size="xl" title={`TLS Configuration - ${service.name}`} opened={opened} onClose={close} closeOnClickOutside={false} centered>
|
||||||
|
<form onSubmit={form.onSubmit(submitRequest)}>
|
||||||
|
<Switch
|
||||||
|
label="Enable TLS Decryption / Encryption"
|
||||||
|
description="Decrypt and encrypt incoming and outgoing traffic using Nginx stream proxy"
|
||||||
|
{...form.getInputProps('tls_enabled', { type: 'checkbox' })}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{form.values.tls_enabled && (
|
||||||
|
<>
|
||||||
|
<Space h="md" />
|
||||||
|
<Textarea
|
||||||
|
label="TLS Certificate (PEM format)"
|
||||||
|
placeholder="-----BEGIN CERTIFICATE----- ... -----END CERTIFICATE-----"
|
||||||
|
minRows={6}
|
||||||
|
styles={{ input: { fontFamily: 'monospace' } }}
|
||||||
|
{...form.getInputProps('tls_cert')}
|
||||||
|
/>
|
||||||
|
<Space h="md" />
|
||||||
|
<Textarea
|
||||||
|
label="TLS Private Key (PEM format)"
|
||||||
|
placeholder="-----BEGIN PRIVATE KEY----- ... -----END PRIVATE KEY-----"
|
||||||
|
minRows={6}
|
||||||
|
styles={{ input: { fontFamily: 'monospace' } }}
|
||||||
|
{...form.getInputProps('tls_key')}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Group mt="xl" justify="flex-end" mb="sm">
|
||||||
|
<Button loading={submitLoading} type="submit">Save Configuration</Button>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<>
|
||||||
|
<Space h="md" />
|
||||||
|
<Notification icon={<ImCross size={14} />} color="red" onClose={() => setError(null)}>
|
||||||
|
Error: {error}
|
||||||
|
</Notification>
|
||||||
|
<Space h="md" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
78
frontend/src/components/TLSServiceFields.tsx
Normal file
78
frontend/src/components/TLSServiceFields.tsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { Space, Switch, Textarea } from '@mantine/core';
|
||||||
|
import { UseFormReturnType } from '@mantine/form';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { getapi, regex_ipv4, regex_ipv6 } from '../js/utils';
|
||||||
|
|
||||||
|
interface TLSServiceFieldsProps {
|
||||||
|
form: UseFormReturnType<any>;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TLSServiceFields({ form, disabled }: TLSServiceFieldsProps) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!form.values.tls_enabled || !form.values.ip_int || !form.values.port) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isIpValid = form.values.ip_int.match(regex_ipv4) || form.values.ip_int.match(regex_ipv6);
|
||||||
|
const isPortValid = form.values.port > 0 && form.values.port < 65536;
|
||||||
|
if (!isIpValid || !isPortValid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const debounceTimer = setTimeout(() => {
|
||||||
|
getapi(`certificates?ip_int=${encodeURIComponent(form.values.ip_int)}&port=${form.values.port}`)
|
||||||
|
.then(res => {
|
||||||
|
if (res && res.cert) {
|
||||||
|
form.setFieldValue('tls_cert', res.cert);
|
||||||
|
}
|
||||||
|
if (res && res.key) {
|
||||||
|
form.setFieldValue('tls_key', res.key);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error("Failed to fetch certificate", err);
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
return () => clearTimeout(debounceTimer);
|
||||||
|
}, [form.values.tls_enabled, form.values.ip_int, form.values.port]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (disabled && form.values.tls_enabled) {
|
||||||
|
form.setFieldValue('tls_enabled', false);
|
||||||
|
}
|
||||||
|
}, [disabled, form.values.tls_enabled]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Space h="md" />
|
||||||
|
<Switch
|
||||||
|
label="Enable TLS Decryption / Encryption"
|
||||||
|
description="Decrypt and encrypt incoming and outgoing traffic using Nginx stream proxy"
|
||||||
|
disabled={disabled}
|
||||||
|
{...form.getInputProps('tls_enabled', { type: 'checkbox' })}
|
||||||
|
/>
|
||||||
|
{form.values.tls_enabled && (
|
||||||
|
<>
|
||||||
|
<Space h="md" />
|
||||||
|
<Textarea
|
||||||
|
label="TLS Certificate (PEM format)"
|
||||||
|
placeholder="-----BEGIN CERTIFICATE----- ... -----END CERTIFICATE-----"
|
||||||
|
minRows={4}
|
||||||
|
styles={{ input: { fontFamily: 'monospace' } }}
|
||||||
|
{...form.getInputProps('tls_cert')}
|
||||||
|
/>
|
||||||
|
<Space h="md" />
|
||||||
|
<Textarea
|
||||||
|
label="TLS Private Key (PEM format)"
|
||||||
|
placeholder="-----BEGIN PRIVATE KEY----- ... -----END PRIVATE KEY-----"
|
||||||
|
minRows={4}
|
||||||
|
styles={{ input: { fontFamily: 'monospace' } }}
|
||||||
|
{...form.getInputProps('tls_key')}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -16,7 +16,7 @@ import { FaArrowLeft } from "react-icons/fa";
|
|||||||
import { IoSettingsSharp } from 'react-icons/io5';
|
import { IoSettingsSharp } from 'react-icons/io5';
|
||||||
import AddEditService from '../../components/NFProxy/AddEditService';
|
import AddEditService from '../../components/NFProxy/AddEditService';
|
||||||
import PyFilterView from '../../components/PyFilterView';
|
import PyFilterView from '../../components/PyFilterView';
|
||||||
import { TbPlugConnected } from 'react-icons/tb';
|
import { TbPlugConnected, TbShieldLock } from 'react-icons/tb';
|
||||||
import { CodeHighlight } from '@mantine/code-highlight';
|
import { CodeHighlight } from '@mantine/code-highlight';
|
||||||
import { FaPython } from "react-icons/fa";
|
import { FaPython } from "react-icons/fa";
|
||||||
import { FiFileText } from "react-icons/fi";
|
import { FiFileText } from "react-icons/fi";
|
||||||
@ -24,6 +24,7 @@ import { ModalLog } from '../../components/ModalLog';
|
|||||||
import { useListState } from '@mantine/hooks';
|
import { useListState } from '@mantine/hooks';
|
||||||
import { ExceptionWarning } from '../../components/NFProxy/ExceptionWarning';
|
import { ExceptionWarning } from '../../components/NFProxy/ExceptionWarning';
|
||||||
import { DocsButton } from '../../components/DocsButton';
|
import { DocsButton } from '../../components/DocsButton';
|
||||||
|
import TLSConfigModal from '../../components/TLSConfigModal';
|
||||||
|
|
||||||
export default function ServiceDetailsNFProxy() {
|
export default function ServiceDetailsNFProxy() {
|
||||||
|
|
||||||
@ -34,6 +35,7 @@ export default function ServiceDetailsNFProxy() {
|
|||||||
const [deleteModal, setDeleteModal] = useState(false)
|
const [deleteModal, setDeleteModal] = useState(false)
|
||||||
const [renameModal, setRenameModal] = useState(false)
|
const [renameModal, setRenameModal] = useState(false)
|
||||||
const [editModal, setEditModal] = useState(false)
|
const [editModal, setEditModal] = useState(false)
|
||||||
|
const [tlsModal, setTlsModal] = useState(false)
|
||||||
const [buttonLoading, setButtonLoading] = useState(false)
|
const [buttonLoading, setButtonLoading] = useState(false)
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const filterCode = nfproxyServiceFilterCodeQuery(srv??"")
|
const filterCode = nfproxyServiceFilterCodeQuery(srv??"")
|
||||||
@ -136,11 +138,18 @@ export default function ServiceDetailsNFProxy() {
|
|||||||
<Badge size="xl" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" mr="sm">
|
<Badge size="xl" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" mr="sm">
|
||||||
:{serviceInfo.port}
|
:{serviceInfo.port}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
{serviceInfo.tls_enabled && (
|
||||||
|
<Badge color="green" size="xl" variant="light" radius="md" mr="sm">
|
||||||
|
<TbShieldLock size={16} style={{ marginRight: 6, verticalAlign: 'middle' }} />
|
||||||
|
TLS (lo:{serviceInfo.clear_port})
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
|
||||||
<MenuDropDownWithButton>
|
<MenuDropDownWithButton>
|
||||||
<Menu.Item><b>Edit service</b></Menu.Item>
|
<Menu.Item><b>Edit service</b></Menu.Item>
|
||||||
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
|
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
|
||||||
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
||||||
|
<Menu.Item leftSection={<TbShieldLock size={18} />} onClick={()=>setTlsModal(true)}>Configure TLS</Menu.Item>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Menu.Label><b>Danger zone</b></Menu.Label>
|
<Menu.Label><b>Danger zone</b></Menu.Label>
|
||||||
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
||||||
@ -227,6 +236,13 @@ export default function ServiceDetailsNFProxy() {
|
|||||||
onClose={()=>setEditModal(false)}
|
onClose={()=>setEditModal(false)}
|
||||||
edit={serviceInfo}
|
edit={serviceInfo}
|
||||||
/>
|
/>
|
||||||
|
<TLSConfigModal
|
||||||
|
opened={tlsModal}
|
||||||
|
onClose={() => setTlsModal(false)}
|
||||||
|
service={serviceInfo}
|
||||||
|
updateTlsConfig={nfproxy.updatetlsconfig}
|
||||||
|
queryKey={serviceQueryKey}
|
||||||
|
/>
|
||||||
<ModalLog
|
<ModalLog
|
||||||
opened={openLogModal}
|
opened={openLogModal}
|
||||||
close={()=>setOpenLogModal(false)}
|
close={()=>setOpenLogModal(false)}
|
||||||
|
|||||||
121
tests/nginx_test.py
Normal file
121
tests/nginx_test.py
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
sys.modules['socketio'] = MagicMock()
|
||||||
|
sys.modules['socketio.exceptions'] = MagicMock()
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../backend')))
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), 'utils')))
|
||||||
|
|
||||||
|
from modules.nfproxy.nginx import get_tls_ports, update_nginx, NGINX_CONF_PATH
|
||||||
|
from colors import colors, puts, sep
|
||||||
|
|
||||||
|
# Create mock services
|
||||||
|
services = [
|
||||||
|
{
|
||||||
|
"service_id": "a1b2c3d4",
|
||||||
|
"name": "test_service",
|
||||||
|
"port": 443,
|
||||||
|
"status": "active",
|
||||||
|
"proto": "tcp",
|
||||||
|
"ip_int": "127.0.0.1",
|
||||||
|
"tls_enabled": 1,
|
||||||
|
"tls_cert": "CERT_CONTENT",
|
||||||
|
"tls_key": "KEY_CONTENT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service_id": "e5f6a7b8",
|
||||||
|
"name": "test_service_ipv6",
|
||||||
|
"port": 8443,
|
||||||
|
"status": "active",
|
||||||
|
"proto": "tcp",
|
||||||
|
"ip_int": "::1",
|
||||||
|
"tls_enabled": 1,
|
||||||
|
"tls_cert": "CERT6_CONTENT",
|
||||||
|
"tls_key": "KEY6_CONTENT"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
try:
|
||||||
|
# We monkey-patch subprocess.run to prevent it from calling real nginx if it fails
|
||||||
|
import subprocess
|
||||||
|
orig_run = subprocess.run
|
||||||
|
def mock_run(args, *a, **k):
|
||||||
|
# Just pretend it succeeded
|
||||||
|
class MockCompletedProcess:
|
||||||
|
returncode = 0
|
||||||
|
stdout = b""
|
||||||
|
stderr = b""
|
||||||
|
return MockCompletedProcess()
|
||||||
|
subprocess.run = mock_run
|
||||||
|
|
||||||
|
sep()
|
||||||
|
puts("Running Nginx Config Unit Test...", color=colors.cyan, is_bold=True)
|
||||||
|
update_nginx(services)
|
||||||
|
puts("Nginx update completed successfully! ✔", color=colors.green)
|
||||||
|
|
||||||
|
# Check if config exists and has valid contents
|
||||||
|
if os.path.exists(NGINX_CONF_PATH):
|
||||||
|
with open(NGINX_CONF_PATH, "r") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Check pid and error_log paths
|
||||||
|
assert "pid /tmp/firegex_nginx.pid;" in content
|
||||||
|
assert "error_log /tmp/firegex_nginx_error.log info;" in content
|
||||||
|
|
||||||
|
# Check cert paths
|
||||||
|
assert "ssl_certificate /tmp/firegex_cert_a1b2c3d4.crt;" in content
|
||||||
|
assert "ssl_certificate /tmp/firegex_cert_e5f6a7b8.crt;" in content
|
||||||
|
|
||||||
|
# Verify deterministic ports for IPv4
|
||||||
|
# sha256("127.0.0.1:443")[:4] as int = 3737521872
|
||||||
|
# 3737521872 % 10000 = 3936
|
||||||
|
# ssl_port = 10000 + 3936 = 13936
|
||||||
|
# clear_port = 20000 + 3936 = 23936
|
||||||
|
assert "127.0.0.1:13936" in content
|
||||||
|
assert "127.0.0.1:23936" in content
|
||||||
|
|
||||||
|
# Verify deterministic ports & bracket wrapping for IPv6
|
||||||
|
# sha256("::1:8443")[:4] as int = 2439169896
|
||||||
|
# 2439169896 % 10000 = 9896
|
||||||
|
# ssl_port = 10000 + 9896 = 19896
|
||||||
|
# clear_port = 20000 + 9896 = 29896
|
||||||
|
assert "[::1]:19896" in content
|
||||||
|
assert "[::1]:29896" in content
|
||||||
|
assert "[::1]:8443" in content
|
||||||
|
|
||||||
|
puts("Configuration assertions passed! ✔", color=colors.green)
|
||||||
|
|
||||||
|
# Check temp files were created
|
||||||
|
assert os.path.exists("/tmp/firegex_cert_a1b2c3d4.crt")
|
||||||
|
assert os.path.exists("/tmp/firegex_key_a1b2c3d4.key")
|
||||||
|
assert os.path.exists("/tmp/firegex_cert_e5f6a7b8.crt")
|
||||||
|
assert os.path.exists("/tmp/firegex_key_e5f6a7b8.key")
|
||||||
|
puts("Temporary certificate files created! ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Error: nginx.conf not found! ✗", color=colors.red)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Write dummy files to simulate Nginx creating PID and error log files
|
||||||
|
with open("/tmp/firegex_nginx.pid", "w") as f:
|
||||||
|
f.write("12345")
|
||||||
|
with open("/tmp/firegex_nginx_error.log", "w") as f:
|
||||||
|
f.write("log info")
|
||||||
|
|
||||||
|
# Test cleanup by passing an empty list
|
||||||
|
update_nginx([])
|
||||||
|
|
||||||
|
assert not os.path.exists(NGINX_CONF_PATH)
|
||||||
|
assert not os.path.exists("/tmp/firegex_cert_a1b2c3d4.crt")
|
||||||
|
assert not os.path.exists("/tmp/firegex_key_a1b2c3d4.key")
|
||||||
|
assert not os.path.exists("/tmp/firegex_cert_e5f6a7b8.crt")
|
||||||
|
assert not os.path.exists("/tmp/firegex_key_e5f6a7b8.key")
|
||||||
|
assert not os.path.exists("/tmp/firegex_nginx.pid")
|
||||||
|
assert not os.path.exists("/tmp/firegex_nginx_error.log")
|
||||||
|
puts("Cleanup verified successfully! ✔", color=colors.green)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Restore subprocess.run
|
||||||
|
subprocess.run = orig_run
|
||||||
|
puts("Test passed! ✔", color=colors.green, is_bold=True)
|
||||||
|
sep()
|
||||||
@ -8,6 +8,9 @@ ERROR=0
|
|||||||
|
|
||||||
pip3 install -r requirements.txt
|
pip3 install -r requirements.txt
|
||||||
|
|
||||||
|
echo "Running Nginx config unit test"
|
||||||
|
python3 nginx_test.py || ERROR=1
|
||||||
|
|
||||||
until curl --output /dev/null --silent --fail http://127.0.0.1:4444/api/status; do
|
until curl --output /dev/null --silent --fail http://127.0.0.1:4444/api/status; do
|
||||||
printf '.'
|
printf '.'
|
||||||
sleep 1
|
sleep 1
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user