From f82925dbcb92dba488f2e27cdb1825be7f9a1b28 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 9 Jan 2024 21:57:18 +0100 Subject: [PATCH] Can configure network NAT settings from UI --- virtweb_backend/src/constants.rs | 3 + .../src/controllers/server_controller.rs | 5 + virtweb_backend/src/nat/nat_definition.rs | 7 + virtweb_frontend/src/api/NetworksApi.ts | 18 ++ virtweb_frontend/src/api/ServerApi.ts | 1 + .../widgets/forms/NetDHCPHostReservations.tsx | 6 + .../src/widgets/forms/NetNatConfiguration.tsx | 301 ++++++++++++++++++ .../src/widgets/forms/PortInput.tsx | 1 + .../src/widgets/forms/RadioGroupInput.tsx | 40 +++ .../src/widgets/forms/SelectInput.tsx | 7 +- .../src/widgets/net/NetworkDetails.tsx | 68 +++- 11 files changed, 446 insertions(+), 11 deletions(-) create mode 100644 virtweb_frontend/src/widgets/forms/NetNatConfiguration.tsx create mode 100644 virtweb_frontend/src/widgets/forms/RadioGroupInput.tsx diff --git a/virtweb_backend/src/constants.rs b/virtweb_backend/src/constants.rs index 4ba506d..3ad8bee 100644 --- a/virtweb_backend/src/constants.rs +++ b/virtweb_backend/src/constants.rs @@ -44,6 +44,9 @@ pub const DISK_SIZE_MIN: usize = 100; /// Disk size max (MB) pub const DISK_SIZE_MAX: usize = 1000 * 1000 * 2; +/// Net nat entry comment max size +pub const NET_NAT_COMMENT_MAX_SIZE: usize = 250; + /// Network mac address default prefix pub const NET_MAC_ADDR_PREFIX: &str = "52:54:00"; diff --git a/virtweb_backend/src/controllers/server_controller.rs b/virtweb_backend/src/controllers/server_controller.rs index 5d69687..1dbeeb9 100644 --- a/virtweb_backend/src/controllers/server_controller.rs +++ b/virtweb_backend/src/controllers/server_controller.rs @@ -43,6 +43,7 @@ struct ServerConstraints { disk_size: LenConstraints, net_name_size: LenConstraints, net_title_size: LenConstraints, + net_nat_comment_size: LenConstraints, dhcp_reservation_host_name: LenConstraints, nwfilter_name_size: LenConstraints, nwfilter_comment_size: LenConstraints, @@ -81,6 +82,10 @@ pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder { net_name_size: LenConstraints { min: 2, max: 50 }, net_title_size: LenConstraints { min: 0, max: 50 }, + net_nat_comment_size: LenConstraints { + min: 0, + max: constants::NET_NAT_COMMENT_MAX_SIZE, + }, dhcp_reservation_host_name: LenConstraints { min: 2, max: 250 }, diff --git a/virtweb_backend/src/nat/nat_definition.rs b/virtweb_backend/src/nat/nat_definition.rs index 63a372d..2952ed8 100644 --- a/virtweb_backend/src/nat/nat_definition.rs +++ b/virtweb_backend/src/nat/nat_definition.rs @@ -1,3 +1,4 @@ +use crate::constants; use crate::utils::net_utils; use std::net::{Ipv4Addr, Ipv6Addr}; @@ -64,6 +65,12 @@ impl Nat { return Err(NatDefError::InvalidNatDef("Invalid guest port!").into()); } + if let Some(comment) = &self.comment { + if comment.len() > constants::NET_NAT_COMMENT_MAX_SIZE { + return Err(NatDefError::InvalidNatDef("Comment is too large!").into()); + } + } + Ok(()) } } diff --git a/virtweb_frontend/src/api/NetworksApi.ts b/virtweb_frontend/src/api/NetworksApi.ts index 43026fd..575cbd0 100644 --- a/virtweb_frontend/src/api/NetworksApi.ts +++ b/virtweb_frontend/src/api/NetworksApi.ts @@ -13,10 +13,28 @@ export interface DHCPConfig { hosts: DHCPHost[]; } +export type NatSource = + | { type: "interface"; name: string } + | { type: "ip"; ip: string }; + +export type NatHostPort = + | { type: "single"; port: number } + | { type: "range"; start: number; end: number }; + +export interface NatEntry { + protocol: "TCP" | "UDP" | "Both"; + host_addr: NatSource; + host_port: NatHostPort; + guest_addr: string; + guest_port: number; + comment?: string; +} + export interface IpConfig { bridge_address: string; prefix: number; dhcp?: DHCPConfig; + nat?: NatEntry[]; } export interface NetworkInfo { diff --git a/virtweb_frontend/src/api/ServerApi.ts b/virtweb_frontend/src/api/ServerApi.ts index ac68612..8dda79d 100644 --- a/virtweb_frontend/src/api/ServerApi.ts +++ b/virtweb_frontend/src/api/ServerApi.ts @@ -21,6 +21,7 @@ export interface ServerConstraints { disk_size: LenConstraint; net_name_size: LenConstraint; net_title_size: LenConstraint; + net_nat_comment_size: LenConstraint; dhcp_reservation_host_name: LenConstraint; nwfilter_name_size: LenConstraint; nwfilter_comment_size: LenConstraint; diff --git a/virtweb_frontend/src/widgets/forms/NetDHCPHostReservations.tsx b/virtweb_frontend/src/widgets/forms/NetDHCPHostReservations.tsx index ba0bb37..565f059 100644 --- a/virtweb_frontend/src/widgets/forms/NetDHCPHostReservations.tsx +++ b/virtweb_frontend/src/widgets/forms/NetDHCPHostReservations.tsx @@ -11,6 +11,7 @@ import { ListItemText, Paper, Tooltip, + Typography, } from "@mui/material"; import { DHCPConfig, DHCPHost } from "../../api/NetworksApi"; import { ServerApi } from "../../api/ServerApi"; @@ -54,6 +55,11 @@ export function NetDHCPHostReservations(p: { ))} + {p.dhcp.hosts.length === 0 && ( + + You have not set any DHCP host reservations. + + )} {p.editable && ( )} diff --git a/virtweb_frontend/src/widgets/forms/NetNatConfiguration.tsx b/virtweb_frontend/src/widgets/forms/NetNatConfiguration.tsx new file mode 100644 index 0000000..c56cb20 --- /dev/null +++ b/virtweb_frontend/src/widgets/forms/NetNatConfiguration.tsx @@ -0,0 +1,301 @@ +import DeleteIcon from "@mui/icons-material/Delete"; +import { + Button, + Card, + CardActions, + CardContent, + Grid, + IconButton, + Tooltip, + Typography, +} from "@mui/material"; +import React, { PropsWithChildren } from "react"; +import { NatEntry } from "../../api/NetworksApi"; +import { ServerApi } from "../../api/ServerApi"; +import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider"; +import { IPInput } from "./IPInput"; +import { PortInput } from "./PortInput"; +import { RadioGroupInput } from "./RadioGroupInput"; +import { SelectInput } from "./SelectInput"; +import { TextInput } from "./TextInput"; + +export function NetNatConfiguration(p: { + editable: boolean; + nat: NatEntry[]; + nicsList: string[]; + onChange?: (nat: NatEntry[]) => void; + version: 4 | 6; +}): React.ReactElement { + const confirm = useConfirm(); + + const addEntry = () => { + p.nat.push({ + host_addr: { + type: "ip", + ip: p.version === 4 ? "10.0.0.1" : "fd00::", + }, + host_port: { type: "single", port: 80 }, + guest_addr: p.version === 4 ? "10.0.0.100" : "fd00::", + guest_port: 10, + protocol: "TCP", + }); + p.onChange?.(p.nat); + }; + + const onDelete = async (idx: number) => { + if (!(await confirm("Do you really want to delete this entry?"))) return; + + p.nat.splice(idx, 1); + p.onChange?.(p.nat); + }; + + return ( + <> + {p.nat.map((e, num) => ( + p.onChange?.(p.nat)} + onDelete={() => onDelete(num)} + /> + ))} + + {p.nat.length === 0 && ( + + You have not set any NAT entry yet. + + )} + + {p.editable && } + + ); +} + +function NatEntryForm(p: { + editable: boolean; + version: 4 | 6; + entry: NatEntry; + onChange?: () => void; + onDelete: () => void; + nicsList: string[]; +}): React.ReactElement { + const guestPortEnd = + p.entry.host_port.type === "range" + ? p.entry.host_port.end - p.entry.host_port.start + p.entry.guest_port + : undefined; + + return ( + + + + + { + p.entry.protocol = v as any; + p.onChange?.(); + }} + /> + + + { + p.entry.comment = v; + p.onChange?.(); + }} + size={ServerApi.Config.constraints.net_nat_comment_size} + /> + + + {/* Host conf */} + + { + p.entry.host_addr.type = v as any; + p.onChange?.(); + }} + /> + + {p.entry.host_addr.type === "ip" && ( + { + if (p.entry.host_addr.type === "ip") + p.entry.host_addr.ip = v!; + p.onChange?.(); + }} + /> + )} + + {p.entry.host_addr.type === "interface" && ( + { + return { + value: n, + }; + })} + onValueChange={(v) => { + if (p.entry.host_addr.type === "interface") + p.entry.host_addr.name = v!; + p.onChange?.(); + }} + /> + )} + + + + { + p.entry.guest_addr = v!; + p.onChange?.(); + }} + /> + + + + { + p.entry.host_port.type = v as any; + p.onChange?.(); + }} + /> + + {p.entry.host_port.type === "single" && ( + { + if (p.entry.host_port.type === "single") + p.entry.host_port.port = v!; + p.onChange?.(); + }} + /> + )} + + {p.entry.host_port.type === "range" && ( +
+ { + if (p.entry.host_port.type === "range") + p.entry.host_port.start = v!; + p.onChange?.(); + }} + /> + + { + if (p.entry.host_port.type === "range") + p.entry.host_port.end = v!; + p.onChange?.(); + }} + /> +
+ )} +
+ + +
+ { + p.entry.guest_port = v!; + p.onChange?.(); + }} + /> + {guestPortEnd && } + {guestPortEnd && ( + { + p.entry.guest_port = v!; + p.onChange?.(); + }} + /> + )} +
+
+
+
+ + {p.editable && ( + + + + + + )} + +
+ ); +} + +function NATEntryProp( + p: PropsWithChildren<{ label?: string }> +): React.ReactElement { + return ( + + {p.label && ( + + {p.label} + + )} + {p.children} + + ); +} + +function PortSpacer(): React.ReactElement { + return ; +} diff --git a/virtweb_frontend/src/widgets/forms/PortInput.tsx b/virtweb_frontend/src/widgets/forms/PortInput.tsx index aead89e..cde2d72 100644 --- a/virtweb_frontend/src/widgets/forms/PortInput.tsx +++ b/virtweb_frontend/src/widgets/forms/PortInput.tsx @@ -14,6 +14,7 @@ export function PortInput(p: { onValueChange={(v) => { p.onChange?.(sanitizePort(v)); }} + checkValue={(v) => Number(v) <= 65535} /> ); } diff --git a/virtweb_frontend/src/widgets/forms/RadioGroupInput.tsx b/virtweb_frontend/src/widgets/forms/RadioGroupInput.tsx new file mode 100644 index 0000000..6e25c9d --- /dev/null +++ b/virtweb_frontend/src/widgets/forms/RadioGroupInput.tsx @@ -0,0 +1,40 @@ +import { + RadioGroup, + FormControlLabel, + Radio, + FormControl, + FormLabel, +} from "@mui/material"; + +interface RadioGroupOption { + label: string; + value: string; +} + +export function RadioGroupInput(p: { + editable: boolean; + label?: string; + options: RadioGroupOption[]; + value: string; + onValueChange: (v: string) => void; +}): React.ReactElement { + return ( + + {p.label && {p.label}} + p.onValueChange?.(v)} + > + {p.options.map((o) => ( + } + label={o.label} + /> + ))} + + + ); +} diff --git a/virtweb_frontend/src/widgets/forms/SelectInput.tsx b/virtweb_frontend/src/widgets/forms/SelectInput.tsx index 87f8297..792a516 100644 --- a/virtweb_frontend/src/widgets/forms/SelectInput.tsx +++ b/virtweb_frontend/src/widgets/forms/SelectInput.tsx @@ -9,7 +9,7 @@ import { TextInput } from "./TextInput"; export interface SelectOption { value?: string; - label: string; + label?: string; description?: string; } @@ -23,9 +23,10 @@ export function SelectInput(p: { if (!p.editable && !p.value) return <>; if (!p.editable) { - const value = p.options.find((o) => o.value === p.value)?.label; + const value = p.options.find((o) => o.value === p.value)?.label ?? p.value; return ; } + return ( {p.label} @@ -41,7 +42,7 @@ export function SelectInput(p: { style={{ fontStyle: e.value === undefined ? "italic" : undefined }} >
- {e.label} + {e.label ?? e.value} {e.description && ( { p.net.ip_v4 = c; @@ -237,7 +238,7 @@ function NetworkDetailsTabIPv4(p: DetailsInnerProps): React.ReactElement { function NetworkDetailsTabIPv6(p: DetailsInnerProps): React.ReactElement { return ( { p.net.ip_v6 = c; @@ -253,6 +254,7 @@ function IPSection(p: { config?: IpConfig; onChange: (c: IpConfig | undefined) => void; version: 4 | 6; + nicsList: string[]; }): React.ReactElement { const confirm = useConfirm(); @@ -260,7 +262,7 @@ function IPSection(p: { if (!!p.config) { if ( !(await confirm( - `Do you really want to disable IPv${p.version} on this network?` + `Do you really want to disable IPv${p.version} on this network? Specific configuration will be deleted!` )) ) return; @@ -275,8 +277,8 @@ function IPSection(p: { }); }; - const toggleDHCP = (v: boolean) => { - if (v) + const toggleDHCP = async (v: boolean) => { + if (v) { p.config!.dhcp = p.version === 4 ? { @@ -285,7 +287,32 @@ function IPSection(p: { hosts: [], } : { start: "fd00::100", end: "fd00::f00", hosts: [] }; - else p.config!.dhcp = undefined; + } else { + if ( + !(await confirm( + `Do you really want to disable DHCPv${p.version} on this network? Specific configuration will be deleted!` + )) + ) + return; + p.config!.dhcp = undefined; + } + + p.onChange?.(p.config); + }; + + const toggleNAT = async (v: boolean) => { + if (v) { + p.config!.nat = []; + } else { + if ( + (p.config?.nat?.length ?? 0 > 0) && + !(await confirm( + `Do you really want to disable NAT port forwarding on this network? Specific configuration will be deleted!` + )) + ) + return; + p.config!.nat = undefined; + } p.onChange?.(p.config); }; @@ -384,6 +411,31 @@ function IPSection(p: { /> )} + + {p.config && (p.editable || p.config.nat) && ( + toggleNAT(val)} + /> + } + > + {p.config.nat && ( + { + p.config!.nat = n; + p.onChange?.(p.config); + }} + /> + )} + + )} ); }