Files
VirtWeb/virtweb_frontend/src/widgets/forms/NetNatConfiguration.tsx
2024-01-11 20:00:44 +01:00

312 lines
9.0 KiB
TypeScript

import DeleteIcon from "@mui/icons-material/Delete";
import {
Alert,
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_ip: {
type: "ip",
ip: p.version === 4 ? "10.0.0.1" : "fd00::",
},
host_port: { type: "single", port: 80 },
guest_ip: 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) => (
<NatEntryForm
key={num}
{...p}
entry={e}
onChange={() => p.onChange?.(p.nat)}
onDelete={() => onDelete(num)}
/>
))}
{p.nat.length === 0 && (
<Typography style={{ textAlign: "center" }}>
You have not set any NAT entry yet.
</Typography>
)}
{p.editable && <Button onClick={addEntry}>Add a new entry</Button>}
</>
);
}
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 (
<Card style={{ margin: "30px" }} elevation={3}>
<CardContent>
<Grid container>
<NATEntryProp>
<SelectInput
{...p}
label="Protocol"
options={[
{ value: "TCP" },
{ value: "UDP" },
{ label: "TCP & UDP", value: "Both" },
]}
value={p.entry.protocol}
onValueChange={(v) => {
p.entry.protocol = v as any;
p.onChange?.();
}}
/>
</NATEntryProp>
<NATEntryProp>
<TextInput
{...p}
label="Comment"
value={p.entry.comment}
onValueChange={(v) => {
p.entry.comment = v;
p.onChange?.();
}}
size={ServerApi.Config.constraints.net_nat_comment_size}
/>
</NATEntryProp>
{/* Host conf */}
<NATEntryProp label="Host configuration">
<SelectInput
{...p}
label="Host IP address specification"
options={[
{
label: "Specific IP",
value: "ip",
description: "Use a pre-defined IP address",
},
{
label: "Network interface",
value: "interface",
description:
"Use active IP addresses on the selected network interface during network startup to determine host adddress",
},
]}
value={p.entry.host_ip.type}
onValueChange={(v) => {
p.entry.host_ip.type = v as any;
p.onChange?.();
}}
/>
{p.entry.host_ip.type === "ip" && (
<IPInput
{...p}
label="Host IP address"
value={p.entry.host_ip.ip}
onValueChange={(v) => {
if (p.entry.host_ip.type === "ip") p.entry.host_ip.ip = v!;
p.onChange?.();
}}
/>
)}
{p.entry.host_ip.type === "interface" && (
<>
{p.editable && (
<Alert severity="warning" style={{ margin: "10px 0px" }}>
Warning! All IP addresses may not be inferred on reboot due
to the fact that the network hook might be executed before
the network interfaces are fully configured. This might lead
to incomplete ports exposition!
</Alert>
)}
<SelectInput
{...p}
label="Network interface"
value={p.entry.host_ip.name}
options={p.nicsList.map((n) => {
return {
value: n,
};
})}
onValueChange={(v) => {
if (p.entry.host_ip.type === "interface")
p.entry.host_ip.name = v!;
p.onChange?.();
}}
/>
</>
)}
</NATEntryProp>
<NATEntryProp label="Target guest configuration">
<IPInput
{...p}
label="Guest IP"
value={p.entry.guest_ip}
onValueChange={(v) => {
p.entry.guest_ip = v!;
p.onChange?.();
}}
/>
</NATEntryProp>
<NATEntryProp>
<RadioGroupInput
{...p}
options={[
{ label: "Single port", value: "single" },
{ label: "Range of ports", value: "range" },
]}
value={p.entry.host_port.type}
onValueChange={(v) => {
p.entry.host_port.type = v as any;
p.onChange?.();
}}
/>
{p.entry.host_port.type === "single" && (
<PortInput
{...p}
label="Host port"
value={p.entry.host_port.port}
onChange={(v) => {
if (p.entry.host_port.type === "single")
p.entry.host_port.port = v!;
p.onChange?.();
}}
/>
)}
{p.entry.host_port.type === "range" && (
<div style={{ display: "flex" }}>
<PortInput
{...p}
label="Host port start"
value={p.entry.host_port.start}
onChange={(v) => {
if (p.entry.host_port.type === "range")
p.entry.host_port.start = v!;
p.onChange?.();
}}
/>
<PortSpacer />
<PortInput
{...p}
label="Host port end"
value={p.entry.host_port.end}
onChange={(v) => {
if (p.entry.host_port.type === "range")
p.entry.host_port.end = v!;
p.onChange?.();
}}
/>
</div>
)}
</NATEntryProp>
<NATEntryProp>
<div style={{ display: "flex", height: "100%", alignItems: "end" }}>
<PortInput
{...p}
label={`Guest port ${guestPortEnd ? "start" : ""}`}
value={p.entry.guest_port}
onChange={(v) => {
p.entry.guest_port = v!;
p.onChange?.();
}}
/>
{guestPortEnd && <PortSpacer />}
{guestPortEnd && (
<PortInput
editable={false}
label={`Guest port end`}
value={guestPortEnd}
onChange={(v) => {
p.entry.guest_port = v!;
p.onChange?.();
}}
/>
)}
</div>
</NATEntryProp>
</Grid>
</CardContent>
<CardActions>
{p.editable && (
<Tooltip title="Remove the entry">
<IconButton color="error" onClick={p.onDelete}>
<DeleteIcon />
</IconButton>
</Tooltip>
)}
</CardActions>
</Card>
);
}
function NATEntryProp(
p: PropsWithChildren<{ label?: string }>
): React.ReactElement {
return (
<Grid item sm={12} md={6} style={{ padding: "20px" }}>
{p.label && (
<Typography variant="h6" style={{ marginBottom: "10px" }}>
{p.label}
</Typography>
)}
{p.children}
</Grid>
);
}
function PortSpacer(): React.ReactElement {
return <span style={{ width: "20px" }}></span>;
}