From c880c5e6bb9962689bb22cbbc7d85ae0c86a445f Mon Sep 17 00:00:00 2001 From: Pierre Hubert Date: Wed, 3 Jan 2024 19:20:37 +0100 Subject: [PATCH] Create frames for network filters management --- virtweb_frontend/src/App.tsx | 8 + virtweb_frontend/src/api/NWFilterApi.ts | 152 +++++++++++++++++- virtweb_frontend/src/api/NetworksApi.ts | 12 +- .../src/routes/EditNWFilterRoute.tsx | 150 +++++++++++++++++ .../src/routes/NetworkFiltersListRoute.tsx | 12 +- .../src/routes/ViewNWFilterRoute.tsx | 65 ++++++++ .../src/widgets/nwfilter/NWFilterDetails.tsx | 12 ++ 7 files changed, 397 insertions(+), 14 deletions(-) create mode 100644 virtweb_frontend/src/routes/EditNWFilterRoute.tsx create mode 100644 virtweb_frontend/src/routes/ViewNWFilterRoute.tsx create mode 100644 virtweb_frontend/src/widgets/nwfilter/NWFilterDetails.tsx diff --git a/virtweb_frontend/src/App.tsx b/virtweb_frontend/src/App.tsx index 580545b..f4dcc20 100644 --- a/virtweb_frontend/src/App.tsx +++ b/virtweb_frontend/src/App.tsx @@ -27,6 +27,11 @@ import { BaseLoginPage } from "./widgets/BaseLoginPage"; import { ViewNetworkRoute } from "./routes/ViewNetworkRoute"; import { HomeRoute } from "./routes/HomeRoute"; import { NetworkFiltersListRoute } from "./routes/NetworkFiltersListRoute"; +import { ViewNWFilterRoute } from "./routes/ViewNWFilterRoute"; +import { + CreateNWFilterRoute, + EditNWFilterRoute, +} from "./routes/EditNWFilterRoute"; interface AuthContext { signedIn: boolean; @@ -63,6 +68,9 @@ export function App() { } /> } /> + } /> + } /> + } /> } /> } /> diff --git a/virtweb_frontend/src/api/NWFilterApi.ts b/virtweb_frontend/src/api/NWFilterApi.ts index 1cb9344..5b336a0 100644 --- a/virtweb_frontend/src/api/NWFilterApi.ts +++ b/virtweb_frontend/src/api/NWFilterApi.ts @@ -1,4 +1,5 @@ import { APIClient } from "./ApiClient"; +import { ServerApi } from "./ServerApi"; export interface NWFilterChain { protocol: string; @@ -18,8 +19,91 @@ export interface NWFSMac { comment?: string; } -// TODO : complete -export type NWFSelector = NWFSAll | NWFSMac; +export interface NWFSArpOrRARP { + srcmacaddr?: string; + srcmacmask?: string; + dstmacaddr?: string; + dstmacmask?: string; + arpsrcipaddr?: string; + arpsrcipmask?: number; + arpdstipaddr?: string; + arpdstipmask?: number; + comment?: string; +} + +export type NWFSArp = NWFSArpOrRARP & { + type: "arp"; +}; + +export type NWFSRArp = NWFSArpOrRARP & { + type: "rarp"; +}; + +export interface NWFSIPBase { + srcmacaddr?: string; + srcmacmask?: string; + dstmacaddr?: string; + dstmacmask?: string; + srcipaddr?: string; + srcipmask?: number; + dstipaddr?: string; + dstipmask?: number; + comment?: string; +} + +export type NFWSIPv4 = NWFSIPBase & { type: "ipv4" }; +export type NFWSIPv6 = NWFSIPBase & { type: "ipv6" }; + +export type Layer4State = + | "NEW" + | "ESTABLISHED" + | "RELATED" + | "INVALID" + | "NONE"; + +export interface NWFSLayer4Base { + srcmacaddr?: string; + srcipaddr?: string; + srcipmask?: number; + dstipaddr?: string; + dstipmask?: number; + srcipfrom?: string; + srcipto?: string; + dstipfrom?: string; + dstipto?: string; + srcportstart?: number; + srcportend?: number; + dstportstart?: number; + dstportend?: number; + state?: Layer4State; + comment?: string; +} + +export type NFWSTCPv4 = NWFSLayer4Base & { type: "tcp" }; +export type NFWSUDPv4 = NWFSLayer4Base & { type: "udp" }; +export type NFWSSCTPv4 = NWFSLayer4Base & { type: "sctp" }; +export type NFWSICMPv4 = NWFSLayer4Base & { type: "icmp" }; + +export type NFWSTCPv6 = NWFSLayer4Base & { type: "tcpipv6" }; +export type NFWSUDPv6 = NWFSLayer4Base & { type: "udpipv6" }; +export type NFWSSCTPv6 = NWFSLayer4Base & { type: "sctpipv6" }; +export type NFWSICMPv6 = NWFSLayer4Base & { type: "icmpipv6" }; + +export type NWFSelector = + | NWFSAll + | NWFSMac + | NWFSArp + | NWFSRArp + | NFWSIPv4 + | NFWSIPv6 + | NFWSTCPv4 + | NFWSUDPv4 + | NFWSSCTPv4 + | NFWSICMPv4 + | NFWSTCPv6 + | NFWSUDPv6 + | NFWSSCTPv6 + | NFWSICMPv6; export interface NWFilterRule { action: "drop" | "reject" | "accept" | "return" | "continue"; @@ -41,6 +125,10 @@ export function NWFilterURL(n: NWFilter, edit: boolean = false): string { return `/nwfilter/${n.uuid}${edit ? "/edit" : ""}`; } +export function NWFilterIsBuiltin(n: NWFilter): boolean { + return ServerApi.Config.builtin_nwfilter_rules.includes(n.name); +} + export class NWFilterApi { /** * Get the entire list of networks @@ -57,4 +145,64 @@ export class NWFilterApi { return list; } + + /** + * Get the information about a single network filter + */ + static async GetSingle(uuid: string): Promise { + return ( + await APIClient.exec({ + method: "GET", + uri: `/nwfilter/${uuid}`, + }) + ).data; + } + + /** + * Get the source XML configuration of a network filter for debugging purposes + */ + static async GetSingleXML(uuid: string): Promise { + return ( + await APIClient.exec({ + uri: `/nwfilter/${uuid}/src`, + method: "GET", + }) + ).data; + } + + /** + * Create a new network filter + */ + static async Create(n: NWFilter): Promise<{ uid: string }> { + return ( + await APIClient.exec({ + method: "POST", + uri: "/nwfilter/create", + jsonData: n, + }) + ).data; + } + + /** + * Update an existing network filter + */ + static async Update(n: NWFilter): Promise<{ uid: string }> { + return ( + await APIClient.exec({ + method: "PUT", + uri: `/nwfilter/${n.uuid}`, + jsonData: n, + }) + ).data; + } + + /** + * Delete a network filter + */ + static async Delete(n: NWFilter): Promise { + await APIClient.exec({ + method: "DELETE", + uri: `/nwfilter/${n.uuid}`, + }); + } } diff --git a/virtweb_frontend/src/api/NetworksApi.ts b/virtweb_frontend/src/api/NetworksApi.ts index 6c86c5d..43026fd 100644 --- a/virtweb_frontend/src/api/NetworksApi.ts +++ b/virtweb_frontend/src/api/NetworksApi.ts @@ -160,12 +160,10 @@ export class NetworkApi { /** * Delete a network */ - static async Delete(n: NetworkInfo): Promise { - return ( - await APIClient.exec({ - method: "DELETE", - uri: `/network/${n.uuid}`, - }) - ).data; + static async Delete(n: NetworkInfo): Promise { + await APIClient.exec({ + method: "DELETE", + uri: `/network/${n.uuid}`, + }); } } diff --git a/virtweb_frontend/src/routes/EditNWFilterRoute.tsx b/virtweb_frontend/src/routes/EditNWFilterRoute.tsx new file mode 100644 index 0000000..038f4df --- /dev/null +++ b/virtweb_frontend/src/routes/EditNWFilterRoute.tsx @@ -0,0 +1,150 @@ +import { Button } from "@mui/material"; +import React from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { useAlert } from "../hooks/providers/AlertDialogProvider"; +import { useLoadingMessage } from "../hooks/providers/LoadingMessageProvider"; +import { useSnackbar } from "../hooks/providers/SnackbarProvider"; +import { AsyncWidget } from "../widgets/AsyncWidget"; +import { ConfigImportExportButtons } from "../widgets/ConfigImportExportButtons"; +import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; +import { NWFilter, NWFilterApi, NWFilterURL } from "../api/NWFilterApi"; +import { NWFilterDetails } from "../widgets/nwfilter/NWFilterDetails"; + +export function CreateNWFilterRoute(): React.ReactElement { + const alert = useAlert(); + const snackbar = useSnackbar(); + const navigate = useNavigate(); + + const [nwfilter, setNWFilter] = React.useState({ + name: "my-filter", + join_filters: [], + rules: [], + }); + + const createNWFilter = async (n: NWFilter) => { + try { + const res = await NWFilterApi.Create(n); + snackbar("The network filter was successfully created!"); + navigate(`/nwfilter/${res.uid}`); + } catch (e) { + console.error(e); + alert(`Failed to create network filter!\n${e}`); + } + }; + + return ( + navigate("/nwfilter")} + onSave={createNWFilter} + onReplace={setNWFilter} + /> + ); +} + +export function EditNWFilterRoute(): React.ReactElement { + const alert = useAlert(); + const snackbar = useSnackbar(); + + const { uuid } = useParams(); + const navigate = useNavigate(); + + const [nwfilter, setNWFilter] = React.useState(); + + const load = async () => { + setNWFilter(await NWFilterApi.GetSingle(uuid!)); + }; + + const updateNetworkFilter = async (n: NWFilter) => { + try { + await NWFilterApi.Update(n); + snackbar("The network filter was successfully updated!"); + navigate(NWFilterURL(nwfilter!)); + } catch (e) { + console.error(e); + alert(`Failed to update network filter!\n${e}`); + } + }; + + return ( + ( + navigate(`/nwfilter/${uuid}`)} + onSave={updateNetworkFilter} + onReplace={setNWFilter} + /> + )} + /> + ); +} + +function EditNetworkFilterRouteInner(p: { + nwfilter: NWFilter; + creating: boolean; + onCancel: () => void; + onSave: (vm: NWFilter) => Promise; + onReplace: (vm: NWFilter) => void; +}): React.ReactElement { + const loadingMessage = useLoadingMessage(); + + const [changed, setChanged] = React.useState(false); + + const [, updateState] = React.useState(); + const forceUpdate = React.useCallback(() => updateState({}), []); + + const valueChanged = () => { + setChanged(true); + forceUpdate(); + }; + + const save = async () => { + loadingMessage.show("Saving network filter configuration..."); + await p.onSave(p.nwfilter); + loadingMessage.hide(); + }; + + return ( + + { + p.onReplace(c); + valueChanged(); + }} + /> + + {changed && ( + + )} + + + } + > + + + ); +} diff --git a/virtweb_frontend/src/routes/NetworkFiltersListRoute.tsx b/virtweb_frontend/src/routes/NetworkFiltersListRoute.tsx index a331857..c6e9f98 100644 --- a/virtweb_frontend/src/routes/NetworkFiltersListRoute.tsx +++ b/virtweb_frontend/src/routes/NetworkFiltersListRoute.tsx @@ -15,7 +15,12 @@ import { } from "@mui/material"; import React from "react"; import { useNavigate } from "react-router-dom"; -import { NWFilter, NWFilterApi, NWFilterURL } from "../api/NWFilterApi"; +import { + NWFilter, + NWFilterApi, + NWFilterIsBuiltin, + NWFilterURL, +} from "../api/NWFilterApi"; import { AsyncWidget } from "../widgets/AsyncWidget"; import { RouterLink } from "../widgets/RouterLink"; import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; @@ -61,10 +66,7 @@ function NetworkFiltersListRouteInner(p: { const onlyBuiltin = visibleFilters === VisibleFilters.Builtin; - return p.list.filter( - (f) => - ServerApi.Config.builtin_nwfilter_rules.includes(f.name) === onlyBuiltin - ); + return p.list.filter((f) => NWFilterIsBuiltin(f) === onlyBuiltin); }, [visibleFilters]); return ( diff --git a/virtweb_frontend/src/routes/ViewNWFilterRoute.tsx b/virtweb_frontend/src/routes/ViewNWFilterRoute.tsx new file mode 100644 index 0000000..5d9e5fa --- /dev/null +++ b/virtweb_frontend/src/routes/ViewNWFilterRoute.tsx @@ -0,0 +1,65 @@ +import { Button } from "@mui/material"; +import React from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { + NWFilter, + NWFilterApi, + NWFilterIsBuiltin, + NWFilterURL, +} from "../api/NWFilterApi"; +import { AsyncWidget } from "../widgets/AsyncWidget"; +import { ConfigImportExportButtons } from "../widgets/ConfigImportExportButtons"; +import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; +import { NWFilterDetails } from "../widgets/nwfilter/NWFilterDetails"; + +export function ViewNWFilterRoute() { + const { uuid } = useParams(); + + const [nwfilter, setNWFilter] = React.useState(); + + const load = async () => { + setNWFilter(await NWFilterApi.GetSingle(uuid!)); + }; + + return ( + } + /> + ); +} + +function ViewNetworkFilterRouteInner(p: { + nwfilter: NWFilter; +}): React.ReactElement { + const navigate = useNavigate(); + + return ( + + + + {!NWFilterIsBuiltin(p.nwfilter) && ( + + )} + + } + > + + + ); +} diff --git a/virtweb_frontend/src/widgets/nwfilter/NWFilterDetails.tsx b/virtweb_frontend/src/widgets/nwfilter/NWFilterDetails.tsx new file mode 100644 index 0000000..49165e8 --- /dev/null +++ b/virtweb_frontend/src/widgets/nwfilter/NWFilterDetails.tsx @@ -0,0 +1,12 @@ +import { ReactElement } from "react"; +import { NWFilter } from "../../api/NWFilterApi"; + +interface DetailsProps { + nwfilter: NWFilter; + editable: boolean; + onChange?: () => void; +} + +export function NWFilterDetails(p: DetailsProps): ReactElement { + return <>todo; +}