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>;
+}