add(todo): additional setting to implement

This commit is contained in:
Domingo Dirutigliano
2023-09-26 01:17:09 +02:00
parent a9446d6dc6
commit 0c972baa9c
13 changed files with 164 additions and 21 deletions

View File

@@ -6,7 +6,7 @@ from jose import jwt
from passlib.context import CryptContext from passlib.context import CryptContext
from fastapi_socketio import SocketManager from fastapi_socketio import SocketManager
from utils.sqlite import SQLite from utils.sqlite import SQLite
from utils import API_VERSION, FIREGEX_PORT, JWT_ALGORITHM, get_interfaces, refresh_frontend, DEBUG, SysctlManager from utils import API_VERSION, FIREGEX_PORT, JWT_ALGORITHM, get_interfaces, socketio_emit, DEBUG, SysctlManager
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 fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
@@ -41,6 +41,9 @@ def create_access_token(data: dict):
encoded_jwt = jwt.encode(to_encode, JWT_SECRET(), algorithm=JWT_ALGORITHM) encoded_jwt = jwt.encode(to_encode, JWT_SECRET(), algorithm=JWT_ALGORITHM)
return encoded_jwt return encoded_jwt
async def refresh_frontend(additional:list[str]=[]):
await socketio_emit([]+additional)
async def check_login(token: str = Depends(oauth2_scheme)): async def check_login(token: str = Depends(oauth2_scheme)):
if not token: if not token:
return False return False

View File

@@ -21,7 +21,38 @@ class FirewallManager:
async def reload(self): async def reload(self):
async with self.lock: async with self.lock:
if self.db.get("ENABLED", "0") == "1": if self.db.get("ENABLED", "0") == "1":
nft.set(map(Rule.from_dict, self.db.query('SELECT * FROM rules WHERE active = 1 ORDER BY rule_id;')), policy=self.db.get('POLICY', 'accept')) additional_rules = []
if self.allow_loopback:
pass #TODO complete rule
if self.allow_established:
pass #TODO complete rule
rules = list(map(Rule.from_dict, self.db.query('SELECT * FROM rules WHERE active = 1 ORDER BY rule_id;')), policy=self.db.get('POLICY', 'accept'))
nft.set(additional_rules + rules)
else: else:
nft.reset() nft.reset()
@property
def keep_rules(self):
return self.db.get("keep_rules", "0") == "1"
@keep_rules.setter
def keep_rules(self, value):
self.db.set("keep_rules", "1" if value else "0")
@property
def allow_loopback(self):
return self.db.get("allow_loopback", "1") == "1"
@allow_loopback.setter
def allow_loopback(self, value):
self.db.set("allow_loopback", "1" if value else "0")
@property
def allow_established(self):
return self.db.get("allow_established", "1") == "1"
@allow_established.setter
def allow_established(self, value):
self.db.set("allow_established", "1" if value else "0")

View File

@@ -3,7 +3,7 @@ from modules.regexproxy.proxy import Filter, Proxy
import random, socket, asyncio import random, socket, asyncio
from base64 import b64decode from base64 import b64decode
from utils.sqlite import SQLite from utils.sqlite import SQLite
from utils import refresh_frontend from utils import socketio_emit
class STATUS: class STATUS:
WAIT = "wait" WAIT = "wait"
@@ -130,10 +130,10 @@ class ServiceManager:
while True: while True:
if check_port_is_open(self.proxy.public_port): if check_port_is_open(self.proxy.public_port):
self._set_status(to) self._set_status(to)
await refresh_frontend() await socketio_emit(["regexproxy"])
await self.proxy.start(in_pause=(to==STATUS.PAUSE)) await self.proxy.start(in_pause=(to==STATUS.PAUSE))
self._set_status(STATUS.STOP) self._set_status(STATUS.STOP)
await refresh_frontend() await socketio_emit(["regexproxy"])
return return
else: else:
await asyncio.sleep(.5) await asyncio.sleep(.5)

View File

@@ -2,7 +2,7 @@ import sqlite3
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, HTTPException
from pydantic import BaseModel from pydantic import BaseModel
from utils.sqlite import SQLite from utils.sqlite import SQLite
from utils import ip_parse, ip_family, refactor_name, refresh_frontend, PortType from utils import ip_parse, ip_family, socketio_emit, PortType
from utils.models import ResetRequest, StatusMessageModel from utils.models import ResetRequest, StatusMessageModel
from modules.firewall.nftables import FiregexTables from modules.firewall.nftables import FiregexTables
from modules.firewall.firewall import FirewallManager from modules.firewall.firewall import FirewallManager
@@ -35,6 +35,11 @@ class RuleAddResponse(BaseModel):
class RenameForm(BaseModel): class RenameForm(BaseModel):
name:str name:str
class FirewallSettings(BaseModel):
keep_rules: bool
allow_loopback: bool
allow_established: bool
app = APIRouter() app = APIRouter()
db = SQLite('db/firewall-rules.db', { db = SQLite('db/firewall-rules.db', {
@@ -77,16 +82,39 @@ async def startup():
await firewall.init() await firewall.init()
async def shutdown(): async def shutdown():
keep_rules = firewall.keep_rules
db.backup() db.backup()
if not keep_rules:
await firewall.close() await firewall.close()
db.disconnect() db.disconnect()
db.restore() db.restore()
async def refresh_frontend(additional:list[str]=[]):
await socketio_emit(["firewall"]+additional)
async def apply_changes(): async def apply_changes():
await firewall.reload() await firewall.reload()
await refresh_frontend() await refresh_frontend()
return {'status': 'ok'} return {'status': 'ok'}
@app.get("/settings", response_model=FirewallSettings)
async def get_settings():
"""Get the firewall settings"""
return {
"keep_rules": firewall.keep_rules,
"allow_loopback": firewall.allow_loopback,
"allow_established": firewall.allow_established
}
@app.post("/settings/set", response_model=StatusMessageModel)
async def set_settings(form: FirewallSettings):
"""Set the firewall settings"""
firewall.keep_rules = form.keep_rules
firewall.allow_loopback = form.allow_loopback
firewall.allow_established = form.allow_established
return {'status': 'ok'}
@app.get('/rules', response_model=RuleInfo) @app.get('/rules', response_model=RuleInfo)
async def get_rule_list(): async def get_rule_list():
"""Get the list of existent firegex rules""" """Get the list of existent firegex rules"""

View File

@@ -7,7 +7,7 @@ from pydantic import BaseModel
from modules.nfregex.nftables import FiregexTables 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, refresh_frontend, PortType from utils import ip_parse, refactor_name, socketio_emit, PortType
from utils.models import ResetRequest, StatusMessageModel from utils.models import ResetRequest, StatusMessageModel
class ServiceModel(BaseModel): class ServiceModel(BaseModel):
@@ -79,6 +79,9 @@ db = SQLite('db/nft-regex.db', {
] ]
}) })
async def refresh_frontend(additional:list[str]=[]):
await socketio_emit(["nfregex"]+additional)
async def reset(params: ResetRequest): async def reset(params: ResetRequest):
if not params.delete: if not params.delete:
db.backup() db.backup()

View File

@@ -4,7 +4,7 @@ from fastapi import APIRouter, HTTPException
from pydantic import BaseModel from pydantic import BaseModel
from modules.porthijack.models import Service from modules.porthijack.models import Service
from utils.sqlite import SQLite from utils.sqlite import SQLite
from utils import addr_parse, ip_family, refactor_name, refresh_frontend, PortType from utils import addr_parse, ip_family, refactor_name, socketio_emit, PortType
from utils.models import ResetRequest, StatusMessageModel from utils.models import ResetRequest, StatusMessageModel
from modules.porthijack.nftables import FiregexTables from modules.porthijack.nftables import FiregexTables
from modules.porthijack.firewall import FirewallManager from modules.porthijack.firewall import FirewallManager
@@ -75,6 +75,9 @@ async def shutdown():
db.disconnect() db.disconnect()
db.restore() db.restore()
async def refresh_frontend(additional:list[str]=[]):
await socketio_emit(["porthijack"]+additional)
def gen_service_id(): def gen_service_id():
while True: while True:
res = secrets.token_hex(8) res = secrets.token_hex(8)

View File

@@ -5,7 +5,7 @@ from pydantic import BaseModel
from modules.regexproxy.utils import STATUS, ProxyManager, gen_internal_port, gen_service_id from modules.regexproxy.utils import STATUS, ProxyManager, gen_internal_port, gen_service_id
from utils.sqlite import SQLite from utils.sqlite import SQLite
from utils.models import ResetRequest, StatusMessageModel from utils.models import ResetRequest, StatusMessageModel
from utils import refactor_name, refresh_frontend, PortType from utils import refactor_name, socketio_emit, PortType
app = APIRouter() app = APIRouter()
db = SQLite("db/regextcpproxy.db",{ db = SQLite("db/regextcpproxy.db",{
@@ -56,7 +56,8 @@ async def shutdown():
db.disconnect() db.disconnect()
db.restore() db.restore()
async def refresh_frontend(additional:list[str]=[]):
await socketio_emit(["regexproxy"]+additional)
class GeneralStatModel(BaseModel): class GeneralStatModel(BaseModel):
closed:int closed:int

View File

@@ -4,6 +4,7 @@ import os, socket, psutil, sys, nftables
from fastapi_socketio import SocketManager from fastapi_socketio import SocketManager
from fastapi import Path from fastapi import Path
from typing import Annotated from typing import Annotated
import json
LOCALHOST_IP = socket.gethostbyname(os.getenv("LOCALHOST_IP","127.0.0.1")) LOCALHOST_IP = socket.gethostbyname(os.getenv("LOCALHOST_IP","127.0.0.1"))
@@ -25,8 +26,8 @@ async def run_func(func, *args, **kwargs):
else: else:
return func(*args, **kwargs) return func(*args, **kwargs)
async def refresh_frontend(): async def socketio_emit(elements:list[str]):
await socketio.emit("update","Refresh") await socketio.emit("update",elements)
def refactor_name(name:str): def refactor_name(name:str):
name = name.strip() name = name.strip()

View File

@@ -5,7 +5,7 @@ import { ImCross } from 'react-icons/im';
import { Outlet, Route, Routes } from 'react-router-dom'; import { Outlet, Route, Routes } from 'react-router-dom';
import MainLayout from './components/MainLayout'; import MainLayout from './components/MainLayout';
import { PasswordSend, ServerStatusResponse } from './js/models'; import { PasswordSend, ServerStatusResponse } from './js/models';
import { DEV_IP_BACKEND, errorNotify, fireUpdateRequest, getstatus, HomeRedirector, IS_DEV, login, setpassword } from './js/utils'; import { DEV_IP_BACKEND, errorNotify, getstatus, HomeRedirector, IS_DEV, login, setpassword } from './js/utils';
import NFRegex from './pages/NFRegex'; import NFRegex from './pages/NFRegex';
import io from 'socket.io-client'; import io from 'socket.io-client';
import RegexProxy from './pages/RegexProxy'; import RegexProxy from './pages/RegexProxy';
@@ -13,6 +13,7 @@ import ServiceDetailsNFRegex from './pages/NFRegex/ServiceDetails';
import ServiceDetailsProxyRegex from './pages/RegexProxy/ServiceDetails'; import ServiceDetailsProxyRegex from './pages/RegexProxy/ServiceDetails';
import PortHijack from './pages/PortHijack'; import PortHijack from './pages/PortHijack';
import { Firewall } from './pages/Firewall'; import { Firewall } from './pages/Firewall';
import { useQueryClient } from '@tanstack/react-query';
const socket = IS_DEV?io("ws://"+DEV_IP_BACKEND, {transports: ["websocket", "polling"], path:"/sock" }):io({transports: ["websocket", "polling"], path:"/sock"}); const socket = IS_DEV?io("ws://"+DEV_IP_BACKEND, {transports: ["websocket", "polling"], path:"/sock" }):io({transports: ["websocket", "polling"], path:"/sock"});
@@ -24,6 +25,7 @@ function App() {
const [reqError, setReqError] = useState<undefined|string>() const [reqError, setReqError] = useState<undefined|string>()
const [error, setError] = useState<string|null>() const [error, setError] = useState<string|null>()
const [loadinBtn, setLoadingBtn] = useState(false); const [loadinBtn, setLoadingBtn] = useState(false);
const queryClient = useQueryClient()
const getStatus = () =>{ const getStatus = () =>{
getstatus().then( res =>{ getstatus().then( res =>{
@@ -39,8 +41,8 @@ function App() {
useEffect(()=>{ useEffect(()=>{
getStatus() getStatus()
socket.on("update", () => { socket.on("update", (data) => {
fireUpdateRequest() queryClient.invalidateQueries({ queryKey: data })
}) })
socket.on("connect_error", (err) => { socket.on("connect_error", (err) => {
errorNotify("Socket.Io connection failed! ",`Error message: [${err.message}]`) errorNotify("Socket.Io connection failed! ",`Error message: [${err.message}]`)

View File

@@ -44,6 +44,12 @@ export type RuleAddForm = {
policy: ActionType policy: ActionType
} }
export type FirewallSettings = {
keep_rules: boolean,
allow_loopback: boolean,
allow_established: boolean,
}
export type ServerResponseListed = { export type ServerResponseListed = {
status:(ServerResponse & {rule_id:number})[]|string, status:(ServerResponse & {rule_id:number})[]|string,
@@ -56,6 +62,12 @@ export const firewall = {
rules: async() => { rules: async() => {
return await getapi("firewall/rules") as RuleInfo; return await getapi("firewall/rules") as RuleInfo;
}, },
settings: async() => {
return await getapi("firewall/settings") as FirewallSettings;
},
setsettings: async(data:FirewallSettings) => {
return await postapi("firewall/settings/set", data) as ServerResponse;
},
enable: async() => { enable: async() => {
return await getapi("firewall/enable") as ServerResponse; return await getapi("firewall/enable") as ServerResponse;
}, },

View File

@@ -103,11 +103,6 @@ export function HomeRedirector(){
return <Navigate to={path} replace/> return <Navigate to={path} replace/>
} }
export function fireUpdateRequest(){ //TODO: change me: specify what to update
queryClient.invalidateQueries({ queryKey: [] })
}
export async function resetfiregex(delete_data:boolean = false){ export async function resetfiregex(delete_data:boolean = false){
const { status } = await postapi("reset",{delete:delete_data}) as ServerResponse; const { status } = await postapi("reset",{delete:delete_data}) as ServerResponse;
return (status === "ok"?undefined:status) return (status === "ok"?undefined:status)

View File

@@ -0,0 +1,48 @@
import { Button, Group, Space, Modal, Switch } from '@mantine/core';
import { useEffect, useState } from 'react';
import { errorNotify, okNotify } from '../../js/utils';
import { FirewallSettings, firewall } from '../../components/Firewall/utils';
export function SettingsModal({ opened, onClose }:{ opened:boolean, onClose:()=>void }) {
const [settings, setSettings] = useState<FirewallSettings>({keep_rules:false, allow_established:true, allow_loopback:true})
useEffect(()=>{
firewall.settings().then( res => {
setSettings(res)
}).catch( err => {
errorNotify("Setting fetch failed!", err.toString())
onClose()
})
},[])
const [submitLoading, setSubmitLoading] = useState(false)
const submitRequest = () =>{
setSubmitLoading(true)
firewall.setsettings(settings).then( () => {
okNotify("Settings updated!", "Settings updated successfully")
setSubmitLoading(false)
onClose()
}).catch( err => {
errorNotify("Settings update failed!", err.toString())
setSubmitLoading(false)
})
}
return <Modal size="xl" title="Change firewall settings" opened={opened} onClose={onClose} closeOnClickOutside={false} centered>
<Switch label="Keep rules on firegex shutdown" checked={settings.keep_rules} onChange={v => setSettings({...settings, keep_rules:v.target.checked})}/>
<Space h="md" />
<Switch label="Allow loopback to communicate with itself" checked={settings.allow_loopback} onChange={v => setSettings({...settings, allow_loopback:v.target.checked})}/>
<Space h="md" />
<Switch label="Allow established connection (essential to allow opening connection) (Dangerous to disable)" checked={settings.allow_established} onChange={v => setSettings({...settings, allow_established:v.target.checked})}/>
<Space h="md" />
<Group position="right" mt="md">
<Button loading={submitLoading} onClick={submitRequest}>Save Setting</Button>
</Group>
</Modal>
}

View File

@@ -18,6 +18,8 @@ import { ModeSelector } from "../../components/Firewall/ModeSelector";
import { OnOffButton } from "../../components/OnOffButton"; import { OnOffButton } from "../../components/OnOffButton";
import { LuArrowBigRightDash } from "react-icons/lu" import { LuArrowBigRightDash } from "react-icons/lu"
import { ImCheckmark, ImCross } from "react-icons/im"; import { ImCheckmark, ImCross } from "react-icons/im";
import { IoSettingsSharp } from "react-icons/io5";
import { SettingsModal } from "./SettingsModal";
export const Firewall = () => { export const Firewall = () => {
@@ -25,6 +27,7 @@ export const Firewall = () => {
const [tooltipAddOpened, setTooltipAddOpened] = useState(false); const [tooltipAddOpened, setTooltipAddOpened] = useState(false);
const [tooltipRefreshOpened, setTooltipRefreshOpened] = useState(false); const [tooltipRefreshOpened, setTooltipRefreshOpened] = useState(false);
const [tooltipApplyOpened, setTooltipApplyOpened] = useState(false); const [tooltipApplyOpened, setTooltipApplyOpened] = useState(false);
const [tooltipSettingsOpened, setTooltipSettingsOpened] = useState(false);
const [currentPolicy, setCurrentPolicy] = useState<ActionType>(ActionType.ACCEPT) const [currentPolicy, setCurrentPolicy] = useState<ActionType>(ActionType.ACCEPT)
const [tooltipAddRulOpened, setTooltipAddRulOpened] = useState(false) const [tooltipAddRulOpened, setTooltipAddRulOpened] = useState(false)
const queryClient = useQueryClient() const queryClient = useQueryClient()
@@ -32,6 +35,7 @@ export const Firewall = () => {
const [state, handlers] = useListState<Rule & {rule_id:string}>([]); const [state, handlers] = useListState<Rule & {rule_id:string}>([]);
const [enableFwModal, setEnableFwModal] = useState(false) const [enableFwModal, setEnableFwModal] = useState(false)
const [applyChangeModal, setApplyChangeModal] = useState(false) const [applyChangeModal, setApplyChangeModal] = useState(false)
const [settingsModal, setSettingsModal] = useState(false)
const theme = useMantineTheme(); const theme = useMantineTheme();
const [updateMevalueinternal, internalUpdateme] = useState(false) const [updateMevalueinternal, internalUpdateme] = useState(false)
@@ -383,6 +387,12 @@ export const Firewall = () => {
onMouseEnter={() => setTooltipRefreshOpened(true)} onMouseLeave={() => setTooltipRefreshOpened(false)}><TbReload size={18} /></ActionIcon> onMouseEnter={() => setTooltipRefreshOpened(true)} onMouseLeave={() => setTooltipRefreshOpened(false)}><TbReload size={18} /></ActionIcon>
</Tooltip> </Tooltip>
<Space w="xs" /> <Space w="xs" />
<Tooltip label="Settings" position='bottom' color="cyan" opened={tooltipSettingsOpened}>
<ActionIcon color="cyan" onClick={()=>setSettingsModal(true)} size="lg" radius="md" variant="filled"
onFocus={() => setTooltipSettingsOpened(false)} onBlur={() => setTooltipSettingsOpened(false)}
onMouseEnter={() => setTooltipSettingsOpened(true)} onMouseLeave={() => setTooltipSettingsOpened(false)}><IoSettingsSharp size={18} /></ActionIcon>
</Tooltip>
<Space w="xs" />
<Tooltip label="Apply" position='bottom' color="grape" opened={tooltipApplyOpened}> <Tooltip label="Apply" position='bottom' color="grape" opened={tooltipApplyOpened}>
<ActionIcon color="grape" onClick={applyChanges} size="lg" radius="md" variant="filled" <ActionIcon color="grape" onClick={applyChanges} size="lg" radius="md" variant="filled"
onFocus={() => setTooltipApplyOpened(false)} onBlur={() => setTooltipApplyOpened(false)} onFocus={() => setTooltipApplyOpened(false)} onBlur={() => setTooltipApplyOpened(false)}
@@ -436,5 +446,11 @@ export const Firewall = () => {
opened={applyChangeModal} opened={applyChangeModal}
/> />
<SettingsModal
opened={settingsModal}
onClose={()=>setSettingsModal(false)}
/>
</> </>
} }