import { ActionIcon, Badge, Box, Card, Divider, FloatingIndicator, Group, LoadingOverlay, Space, Stack, Switch, Table, Tabs, TextInput, ThemeIcon, Title, Tooltip, useMantineTheme } from "@mantine/core" import { useEffect, useState } from "react"; import { BsPlusLg, BsTrashFill } from "react-icons/bs" import { rem } from '@mantine/core'; import { ActionType, Protocol, Rule, RuleMode, Table as NFTables, firewall, firewallRulesQuery } from "../../components/Firewall/utils"; import { errorNotify, getErrorMessage, isMediumScreen, makeid, okNotify } from "../../js/utils"; import { useListState, useMediaQuery } from '@mantine/hooks'; import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd'; import { TbGripVertical, TbReload } from "react-icons/tb"; import { useQueryClient } from "@tanstack/react-query"; import { TiTick } from "react-icons/ti"; import YesNoModal from "../../components/YesNoModal"; import { PortRangeInput } from "../../components/PortInput"; import { InterfaceInput } from "../../components/InterfaceInput"; import { ActionTypeSelector } from "../../components/Firewall/ActionTypeSelector"; import { ProtocolSelector } from "../../components/Firewall/ProtocolSelector"; import { ModeSelector } from "../../components/Firewall/ModeSelector"; import { OnOffButton } from "../../components/OnOffButton"; import { LuArrowBigRightDash } from "react-icons/lu" import { ImCheckmark, ImCross } from "react-icons/im"; import { IoSettingsSharp } from "react-icons/io5"; import { SettingsModal } from "./SettingsModal"; import { FaDirections } from "react-icons/fa"; import { PiWallLight } from "react-icons/pi"; import { DocsButton } from "../../components/DocsButton"; export const Firewall = () => { const [currentPolicy, setCurrentPolicy] = useState(ActionType.ACCEPT) const queryClient = useQueryClient() const rules = firewallRulesQuery() const [state, handlers] = useListState([]); const [enableFwModal, setEnableFwModal] = useState(false) const [applyChangeModal, setApplyChangeModal] = useState(false) const [settingsModal, setSettingsModal] = useState(false) const theme = useMantineTheme(); const isMedium = useMediaQuery(`(min-width: 950px)`) ?? true const [updateMevalueinternal, internalUpdateme] = useState(false) const updateMe = () => { internalUpdateme(!updateMevalueinternal) } useEffect(() => { if (rules.isError) errorNotify("Firewall Update failed!", getErrorMessage(rules.error)) }, [rules.isError]) useEffect(() => { if (!rules.isLoading && rules.isFetched && !rules.isFetching) { setCurrentPolicy(rules.data?.policy ?? ActionType.ACCEPT) handlers.setState(JSON.parse(JSON.stringify((rules.data?.rules ?? []).map(v => ({ rule_id: makeid(30), ...v }))))) } }, [rules.isFetched, rules.isLoading, rules.isFetching]) const fwEnabled = rules.data?.enabled ?? false const valuesChanged = JSON.stringify(rules.data?.rules) != JSON.stringify(state.map(v => { const { rule_id, ...rest } = v return rest })) || rules.data?.policy != currentPolicy const [selectedTab, setSelectedTab] = useState(NFTables.FILTER) const enableFirewall = () => { if (valuesChanged) { applyChangesRaw().then(() => enableFirewallRaw()) } else { enableFirewallRaw() } } const enableFirewallRaw = () => { return firewall.enable() .then(() => okNotify("Firewall enabled", "The firewall has been enabled")) .catch((e) => errorNotify("Firewall enable failed!", getErrorMessage(e))) } const disableFirewallRaw = () => { return firewall.disable() .then(() => okNotify("Firewall disabled", "The firewall has been disabled")) .catch((e) => errorNotify("Firewall disable failed!", getErrorMessage(e))) } const switchState = () => { if (fwEnabled) disableFirewallRaw() else if ([ActionType.REJECT, ActionType.DROP].includes(currentPolicy) || valuesChanged) { setEnableFwModal(true) } else { enableFirewall() } } const applyChanges = () => { if (fwEnabled && rules.data?.policy == ActionType.ACCEPT && [ActionType.REJECT, ActionType.DROP].includes(currentPolicy)) { setApplyChangeModal(true) } else { applyChangesRaw() } } const emptyRuleAdd = () => { handlers.insert(0, { rule_id: makeid(30), active: true, name: "Rule name", proto: Protocol.TCP, src: "", dst: "", port_src_from: 1, port_dst_from: 8080, port_src_to: 65535, port_dst_to: 8080, action: ActionType.ACCEPT, mode: RuleMode.IN, table: selectedTab }) } const parseRules = () => { return state } const applyChangesRaw = () => { const parsedRules = parseRules() if (parsedRules === undefined) { errorNotify("Firewall rules apply failed!", "The firewall rules are not valid") return Promise.reject() } else { return firewall.ruleset({ rules: state, policy: currentPolicy }) .then(() => okNotify("Firewall rules applied", "The firewall rules has been applied")) .catch((e) => errorNotify("Firewall rules apply failed!", getErrorMessage(e))) } } // Draggable indexes must be contiguous inside a Droppable, so the visible rules are // re-indexed for the tab while keeping their position in `state` for the actual edits. const visibleRules = state .map((item, index) => ({ item, index })) .filter(({ item }) => item.table == selectedTab) const items = visibleRules.map(({ item, index }, visibleIndex) => ( {(provided, snapshot) => { const customInt = [ { value: "0.0.0.0/0", netint: "ANY IPv4", label: "0.0.0.0/0" }, { value: "::/0", netint: "ANY IPv6", label: "::/0" }, { value: "", netint: "ANY", label: "ANY" } ] const src_custom_int = customInt.map(v => v.value).includes(item.src) ? [] : [{ value: item.src, netint: "SELECTED", label: item.src }] const dst_custom_int = customInt.map(v => v.value).includes(item.dst) ? [] : [{ value: item.dst, netint: "SELECTED", label: item.dst }] const [srcPortEnabled, setSrcPortEnabled] = useState(item.port_src_from != 1 || item.port_src_to != 65535) const [dstPortEnabled, setDstPortEnabled] = useState(item.port_dst_from != 1 || item.port_dst_to != 65535) const [srcPortValue, setSrcPortValue] = useState(item.port_src_from == item.port_src_to ? `${item.port_src_from}` : `${item.port_src_from}-${item.port_src_to}`) const [dstPortValue, setDstPortValue] = useState(item.port_dst_from == item.port_dst_to ? `${item.port_dst_from}` : `${item.port_dst_from}-${item.port_dst_to}`) const port_range_setter = (rule: Rule, v: string, { src = false, dst = false }: { src?: boolean, dst?: boolean }) => { const elements = v.split("-") const values = [elements[0] ? parseInt(elements[0]) : 0, elements[1] ? parseInt(elements[1]) : 0] values[1] = values[1] ? values[1] : values[0] if (src) { rule.port_src_from = values[0] rule.port_src_to = values[1] setSrcPortValue(v) } if (dst) { rule.port_dst_from = values[0] rule.port_dst_to = values[1] setDstPortValue(v) } updateMe() } const ip_setter = (rule: Rule, v: string | null, { src = false, dst = false }: { src?: boolean, dst?: boolean }) => { const values = v ? v : "" if (src) { rule.src = values } if (dst) { rule.dst = values } updateMe() } const proto_any = item.proto == Protocol.ANY const disabletab = { port_box: proto_any, src_port: !srcPortEnabled || proto_any, dst_port: !dstPortEnabled || proto_any } return { item.active = !item.active updateMe() }} color="teal" size="lg" thumbIcon={ item.active ? ( ) : ( ) } /> handlers.remove(index)} size="lg" radius="md" variant="filled"> { item.name = v.target.value; updateMe() }} style={{ flex: 1 }} /> ip_setter(item, v, { src: true })} includeInterfaceNames /> { const value = !srcPortEnabled setSrcPortEnabled(value) if (!value) { item.port_src_from = 1 item.port_src_to = 65535 updateMe() } else { port_range_setter(item, srcPortValue, { src: true }) } }} size="lg" disabled={disabletab.port_box} variant="light" /> port_range_setter(item, v.target.value, { src: true })} value={srcPortValue} disabled={disabletab.src_port} error={!disabletab.src_port && !srcPortValue} style={{ flex: 1 }} /> ip_setter(item, v, { dst: true })} includeInterfaceNames /> { const value = !dstPortEnabled setDstPortEnabled(value) if (!value) { item.port_dst_from = 1 item.port_dst_to = 65535 updateMe() } else { port_range_setter(item, dstPortValue, { dst: true }) } }} size="lg" disabled={disabletab.port_box} variant="light" /> port_range_setter(item, v.target.value, { dst: true })} value={dstPortValue} disabled={disabletab.dst_port} error={!disabletab.dst_port && !dstPortValue} style={{ flex: 1 }} /> { item.mode = value as RuleMode; updateMe() }} style={{ flexGrow: 1, minWidth: rem(160) }} table={item.table} /> { item.proto = value as Protocol; updateMe() }} style={{ flexGrow: 1, minWidth: rem(160) }} /> { item.action = value as ActionType; updateMe() }} style={{ flexGrow: 1, minWidth: rem(160) }} /> }} )); return <> <ThemeIcon radius="md" size="md" variant='filled' color='red' ><PiWallLight size={20} /></ThemeIcon><Space w="xs" />Firewall Rules {isMedium ? : } Enabled: Policy: setCurrentPolicy(value as ActionType)} /> {isMedium ? : } Rules: {rules.isLoading ? 0 : rules.data?.rules.length} queryClient.invalidateQueries({ queryKey: ["firewall"] })} size="lg" radius="md" variant="filled" loading={rules.isFetching}> setSettingsModal(true)} size="lg" radius="md" variant="filled"> Filtering Table: setSelectedTab(v == NFTables.MANGLE ? NFTables.MANGLE : NFTables.FILTER)}> Filter Mangle {items.length > 0 ? { if (!destination || destination.index == source.index) return handlers.reorder({ from: visibleRules[source.index].index, to: visibleRules[destination.index].index, }) }} > {(provided) => ( {items} {provided.placeholder} )} : <> Firewall Rules allows you to use nftables but through a web interface Add new rules, sort it and enable the firewall: be carefull, wrong rules could also drops out firegex access } setEnableFwModal(false)} action={enableFirewall} opened={enableFwModal} /> setApplyChangeModal(false)} action={applyChangesRaw} opened={applyChangeModal} /> setSettingsModal(false)} /> }