Compare commits
21 Commits
Author | SHA1 | Date | |
---|---|---|---|
524ab50df7 | |||
8cd32d35e2 | |||
307e5d1b50 | |||
ff66a5cf97 | |||
dcf6cdab9b | |||
2649bfbd25 | |||
3eab3ba4b5 | |||
975b4ab395 | |||
c40ee037da | |||
719ab3b265 | |||
ad45c0d654 | |||
7d7a052f5f | |||
aafa4bf145 | |||
baa0adf529 | |||
fdd005a3ec | |||
ed48b22f7f | |||
a7bfb80547 | |||
0710c61909 | |||
85dcb06014 | |||
c880c5e6bb | |||
22f5acd0ff |
19
virtweb_backend/Cargo.lock
generated
19
virtweb_backend/Cargo.lock
generated
@ -2159,18 +2159,6 @@ dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde-xml-rs"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb3aa78ecda1ebc9ec9847d5d3aba7d618823446a049ba2491940506da6e2782"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
"thiserror",
|
||||
"xml-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.193"
|
||||
@ -2677,7 +2665,6 @@ dependencies = [
|
||||
"reqwest",
|
||||
"rust-embed",
|
||||
"serde",
|
||||
"serde-xml-rs",
|
||||
"serde_json",
|
||||
"sysinfo",
|
||||
"tempfile",
|
||||
@ -2981,12 +2968,6 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xml-rs"
|
||||
version = "0.8.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fcb9cbac069e033553e8bb871be2fbdffcab578eb25bd0f7c508cedc6dcd75a"
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.31"
|
||||
|
@ -22,7 +22,6 @@ actix-web-actors = "4.2.0"
|
||||
actix-http = "3.4.0"
|
||||
serde = { version = "1.0.193", features = ["derive"] }
|
||||
serde_json = "1.0.108"
|
||||
serde-xml-rs = "0.6.0"
|
||||
quick-xml = { version = "0.31.0", features = ["serialize", "overlapped-lists"] }
|
||||
futures-util = "0.3.28"
|
||||
anyhow = "1.0.75"
|
||||
|
@ -74,3 +74,6 @@ pub const BUILTIN_NETWORK_FILTER_RULES: [&str; 24] = [
|
||||
"qemu-announce-self",
|
||||
"qemu-announce-self-rarp",
|
||||
];
|
||||
|
||||
/// List of valid network chains
|
||||
pub const NETWORK_CHAINS: [&str; 8] = ["root", "mac", "stp", "vlan", "arp", "rarp", "ipv4", "ipv6"];
|
||||
|
@ -15,8 +15,9 @@ struct StaticConfig {
|
||||
oidc_auth_enabled: bool,
|
||||
iso_mimetypes: &'static [&'static str],
|
||||
net_mac_prefix: &'static str,
|
||||
builtin_nwfilter_rules: &'static [&'static str],
|
||||
nwfilter_chains: &'static [&'static str],
|
||||
constraints: ServerConstraints,
|
||||
builtin_network_rules: &'static [&'static str],
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
@ -25,6 +26,12 @@ struct LenConstraints {
|
||||
max: usize,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct SLenConstraints {
|
||||
min: i64,
|
||||
max: i64,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct ServerConstraints {
|
||||
iso_max_size: usize,
|
||||
@ -37,6 +44,10 @@ struct ServerConstraints {
|
||||
net_name_size: LenConstraints,
|
||||
net_title_size: LenConstraints,
|
||||
dhcp_reservation_host_name: LenConstraints,
|
||||
nwfilter_name_size: LenConstraints,
|
||||
nwfilter_comment_size: LenConstraints,
|
||||
nwfilter_priority: SLenConstraints,
|
||||
nwfilter_selectors_count: LenConstraints,
|
||||
}
|
||||
|
||||
pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder {
|
||||
@ -46,7 +57,8 @@ pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder {
|
||||
oidc_auth_enabled: !AppConfig::get().disable_oidc,
|
||||
iso_mimetypes: &constants::ALLOWED_ISO_MIME_TYPES,
|
||||
net_mac_prefix: constants::NET_MAC_ADDR_PREFIX,
|
||||
builtin_network_rules: &constants::BUILTIN_NETWORK_FILTER_RULES,
|
||||
builtin_nwfilter_rules: &constants::BUILTIN_NETWORK_FILTER_RULES,
|
||||
nwfilter_chains: &constants::NETWORK_CHAINS,
|
||||
constraints: ServerConstraints {
|
||||
iso_max_size: constants::ISO_MAX_SIZE,
|
||||
|
||||
@ -71,6 +83,14 @@ pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder {
|
||||
net_title_size: LenConstraints { min: 0, max: 50 },
|
||||
|
||||
dhcp_reservation_host_name: LenConstraints { min: 2, max: 250 },
|
||||
|
||||
nwfilter_name_size: LenConstraints { min: 2, max: 250 },
|
||||
nwfilter_comment_size: LenConstraints { min: 0, max: 256 },
|
||||
nwfilter_priority: SLenConstraints {
|
||||
min: -1000,
|
||||
max: 1000,
|
||||
},
|
||||
nwfilter_selectors_count: LenConstraints { min: 0, max: 1 },
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -9,10 +9,6 @@ pub struct NetworkFilterRefXML {
|
||||
pub filter: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
||||
#[serde(rename = "all")]
|
||||
pub struct NetworkFilterRuleProtocolAll {}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
||||
#[serde(rename = "mac")]
|
||||
pub struct NetworkFilterRuleProtocolMac {
|
||||
@ -47,7 +43,6 @@ pub struct NetworkFilterRuleProtocolArpXML {
|
||||
pub arpdstipaddr: Option<String>,
|
||||
#[serde(rename = "@arpdstipmask", skip_serializing_if = "Option::is_none")]
|
||||
pub arpdstipmask: Option<u8>,
|
||||
|
||||
#[serde(rename = "@comment", skip_serializing_if = "Option::is_none")]
|
||||
pub comment: Option<String>,
|
||||
}
|
||||
@ -111,7 +106,37 @@ pub struct NetworkFilterRuleProtocolLayer4<IPv> {
|
||||
pub dstportend: Option<u16>,
|
||||
#[serde(rename = "@state", skip_serializing_if = "Option::is_none")]
|
||||
pub state: Option<String>,
|
||||
#[serde(rename = "@comment", skip_serializing_if = "Option::is_none")]
|
||||
pub comment: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
||||
#[serde(rename = "all")]
|
||||
pub struct NetworkFilterRuleProtocolAllXML<IPv> {
|
||||
#[serde(rename = "@srcmacaddr", skip_serializing_if = "Option::is_none")]
|
||||
pub srcmacaddr: Option<String>,
|
||||
#[serde(rename = "@srcipaddr", skip_serializing_if = "Option::is_none")]
|
||||
pub srcipaddr: Option<IPv>,
|
||||
#[serde(rename = "@srcipmask", skip_serializing_if = "Option::is_none")]
|
||||
pub srcipmask: Option<u8>,
|
||||
#[serde(rename = "@dstipaddr", skip_serializing_if = "Option::is_none")]
|
||||
pub dstipaddr: Option<IPv>,
|
||||
#[serde(rename = "@dstipmask", skip_serializing_if = "Option::is_none")]
|
||||
pub dstipmask: Option<u8>,
|
||||
/// Start of range of source IP address
|
||||
#[serde(rename = "@srcipfrom", skip_serializing_if = "Option::is_none")]
|
||||
pub srcipfrom: Option<IPv>,
|
||||
/// End of range of source IP address
|
||||
#[serde(rename = "@srcipto", skip_serializing_if = "Option::is_none")]
|
||||
pub srcipto: Option<IPv>,
|
||||
/// Start of range of destination IP address
|
||||
#[serde(rename = "@dstipfrom", skip_serializing_if = "Option::is_none")]
|
||||
pub dstipfrom: Option<IPv>,
|
||||
/// End of range of destination IP address
|
||||
#[serde(rename = "@dstipto", skip_serializing_if = "Option::is_none")]
|
||||
pub dstipto: Option<IPv>,
|
||||
#[serde(rename = "@state", skip_serializing_if = "Option::is_none")]
|
||||
pub state: Option<String>,
|
||||
#[serde(rename = "@comment", skip_serializing_if = "Option::is_none")]
|
||||
pub comment: Option<String>,
|
||||
}
|
||||
@ -126,10 +151,6 @@ pub struct NetworkFilterRuleXML {
|
||||
#[serde(rename = "@priority")]
|
||||
pub priority: Option<i32>,
|
||||
|
||||
/// Match all protocols
|
||||
#[serde(default, rename = "all", skip_serializing_if = "Vec::is_empty")]
|
||||
pub all_selectors: Vec<NetworkFilterRuleProtocolAll>,
|
||||
|
||||
/// Match mac protocol
|
||||
#[serde(default, rename = "mac", skip_serializing_if = "Vec::is_empty")]
|
||||
pub mac_selectors: Vec<NetworkFilterRuleProtocolMac>,
|
||||
@ -166,6 +187,10 @@ pub struct NetworkFilterRuleXML {
|
||||
#[serde(default, rename = "icmp", skip_serializing_if = "Vec::is_empty")]
|
||||
pub icmp_selectors: Vec<NetworkFilterRuleProtocolLayer4<Ipv4Addr>>,
|
||||
|
||||
/// Match all protocols
|
||||
#[serde(default, rename = "all", skip_serializing_if = "Vec::is_empty")]
|
||||
pub all_selectors: Vec<NetworkFilterRuleProtocolAllXML<Ipv4Addr>>,
|
||||
|
||||
/// Match TCP IPv6 protocol
|
||||
#[serde(default, rename = "tcp-ipv6", skip_serializing_if = "Vec::is_empty")]
|
||||
pub tcp_ipv6_selectors: Vec<NetworkFilterRuleProtocolLayer4<Ipv6Addr>>,
|
||||
@ -181,6 +206,10 @@ pub struct NetworkFilterRuleXML {
|
||||
/// Match ICMP IPv6 protocol
|
||||
#[serde(default, rename = "icmpv6", skip_serializing_if = "Vec::is_empty")]
|
||||
pub imcp_ipv6_selectors: Vec<NetworkFilterRuleProtocolLayer4<Ipv6Addr>>,
|
||||
|
||||
/// Match all ipv6 protocols
|
||||
#[serde(default, rename = "all-ipv6", skip_serializing_if = "Vec::is_empty")]
|
||||
pub all_ipv6_selectors: Vec<NetworkFilterRuleProtocolAllXML<Ipv6Addr>>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
||||
|
@ -1,5 +1,5 @@
|
||||
use crate::libvirt_lib_structures::nwfilter::{
|
||||
NetworkFilterRefXML, NetworkFilterRuleProtocolAll, NetworkFilterRuleProtocolArpXML,
|
||||
NetworkFilterRefXML, NetworkFilterRuleProtocolAllXML, NetworkFilterRuleProtocolArpXML,
|
||||
NetworkFilterRuleProtocolIpvx, NetworkFilterRuleProtocolLayer4, NetworkFilterRuleProtocolMac,
|
||||
NetworkFilterRuleXML, NetworkFilterXML,
|
||||
};
|
||||
@ -366,10 +366,28 @@ pub struct NetworkFilterSelectorLayer4<IPv> {
|
||||
comment: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||
pub struct NetworkSelectorAll<IPv> {
|
||||
comment: Option<String>,
|
||||
srcmacaddr: Option<NetworkFilterMacAddressOrVar>,
|
||||
srcipaddr: Option<IPv>,
|
||||
srcipmask: Option<u8>,
|
||||
dstipaddr: Option<IPv>,
|
||||
dstipmask: Option<u8>,
|
||||
/// Start of range of source IP address
|
||||
srcipfrom: Option<IPv>,
|
||||
/// End of range of source IP address
|
||||
srcipto: Option<IPv>,
|
||||
/// Start of range of destination IP address
|
||||
dstipfrom: Option<IPv>,
|
||||
/// End of range of destination IP address
|
||||
dstipto: Option<IPv>,
|
||||
state: Option<Layer4State>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||
#[serde(tag = "type", rename_all = "lowercase")]
|
||||
pub enum NetworkFilterSelector {
|
||||
All,
|
||||
Mac(NetworkSelectorMac),
|
||||
Arp(NetworkSelectorARP),
|
||||
Rarp(NetworkSelectorARP),
|
||||
@ -379,10 +397,12 @@ pub enum NetworkFilterSelector {
|
||||
UDP(NetworkFilterSelectorLayer4<Ipv4Addr>),
|
||||
SCTP(NetworkFilterSelectorLayer4<Ipv4Addr>),
|
||||
ICMP(NetworkFilterSelectorLayer4<Ipv4Addr>),
|
||||
All(NetworkSelectorAll<Ipv4Addr>),
|
||||
TCPipv6(NetworkFilterSelectorLayer4<Ipv6Addr>),
|
||||
UDPipv6(NetworkFilterSelectorLayer4<Ipv6Addr>),
|
||||
SCTPipv6(NetworkFilterSelectorLayer4<Ipv6Addr>),
|
||||
ICMPipv6(NetworkFilterSelectorLayer4<Ipv6Addr>),
|
||||
Allipv6(NetworkSelectorAll<Ipv6Addr>),
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||
@ -410,10 +430,6 @@ pub struct NetworkFilter {
|
||||
}
|
||||
|
||||
impl NetworkFilter {
|
||||
fn lib2rest_process_all_rule(_n: &NetworkFilterRuleProtocolAll) -> NetworkFilterSelector {
|
||||
NetworkFilterSelector::All
|
||||
}
|
||||
|
||||
fn lib2rest_process_mac_rule(n: &NetworkFilterRuleProtocolMac) -> NetworkFilterSelector {
|
||||
NetworkFilterSelector::Mac(NetworkSelectorMac {
|
||||
src_mac_addr: n.srcmacaddr.as_ref().map(|v| v.into()),
|
||||
@ -476,21 +492,30 @@ impl NetworkFilter {
|
||||
})
|
||||
}
|
||||
|
||||
fn lib2rest_process_all_rule<IPv: Copy>(
|
||||
n: &NetworkFilterRuleProtocolAllXML<IPv>,
|
||||
) -> anyhow::Result<NetworkSelectorAll<IPv>> {
|
||||
Ok(NetworkSelectorAll {
|
||||
srcmacaddr: n.srcmacaddr.as_ref().map(|v| v.into()),
|
||||
srcipaddr: n.srcipaddr,
|
||||
srcipmask: n.srcipmask,
|
||||
dstipaddr: n.dstipaddr,
|
||||
dstipmask: n.dstipmask,
|
||||
srcipfrom: n.srcipfrom,
|
||||
srcipto: n.srcipto,
|
||||
dstipfrom: n.dstipfrom,
|
||||
dstipto: n.dstipto,
|
||||
state: n.state.as_deref().map(Layer4State::from_xml).transpose()?,
|
||||
comment: n.comment.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn lib2rest(xml: NetworkFilterXML) -> anyhow::Result<Self> {
|
||||
let mut rules = Vec::with_capacity(xml.rules.len());
|
||||
for rule in &xml.rules {
|
||||
let mut selectors = Vec::new();
|
||||
|
||||
// All selector
|
||||
selectors.append(
|
||||
&mut rule
|
||||
.all_selectors
|
||||
.iter()
|
||||
.map(Self::lib2rest_process_all_rule)
|
||||
.collect(),
|
||||
);
|
||||
|
||||
// Mac rules
|
||||
// Mac selectors
|
||||
selectors.append(
|
||||
&mut rule
|
||||
.mac_selectors
|
||||
@ -499,7 +524,7 @@ impl NetworkFilter {
|
||||
.collect(),
|
||||
);
|
||||
|
||||
// ARP - RARP rules
|
||||
// ARP - RARP selectors
|
||||
selectors.append(
|
||||
&mut rule
|
||||
.arp_selectors
|
||||
@ -515,7 +540,7 @@ impl NetworkFilter {
|
||||
.collect(),
|
||||
);
|
||||
|
||||
// IPv4 - IPv6 rules
|
||||
// IPv4 - IPv6 selectors
|
||||
selectors.append(
|
||||
&mut rule
|
||||
.ipv4_selectors
|
||||
@ -531,7 +556,7 @@ impl NetworkFilter {
|
||||
.collect(),
|
||||
);
|
||||
|
||||
// Layer 4 protocols
|
||||
// Layer 4 protocols selectors
|
||||
selectors.append(
|
||||
&mut rule
|
||||
.tcp_selectors
|
||||
@ -622,6 +647,31 @@ impl NetworkFilter {
|
||||
.collect::<Result<Vec<_>, anyhow::Error>>()?,
|
||||
);
|
||||
|
||||
// All selectors
|
||||
selectors.append(
|
||||
&mut rule
|
||||
.all_selectors
|
||||
.iter()
|
||||
.map(|r| {
|
||||
Ok(NetworkFilterSelector::All(Self::lib2rest_process_all_rule(
|
||||
r,
|
||||
)?))
|
||||
})
|
||||
.collect::<Result<Vec<_>, anyhow::Error>>()?,
|
||||
);
|
||||
|
||||
selectors.append(
|
||||
&mut rule
|
||||
.all_ipv6_selectors
|
||||
.iter()
|
||||
.map(|r| {
|
||||
Ok(NetworkFilterSelector::Allipv6(
|
||||
Self::lib2rest_process_all_rule(r)?,
|
||||
))
|
||||
})
|
||||
.collect::<Result<Vec<_>, anyhow::Error>>()?,
|
||||
);
|
||||
|
||||
rules.push(NetworkFilterRule {
|
||||
action: NetworkFilterAction::from_xml(&rule.action)?,
|
||||
direction: NetworkFilterDirection::from_xml(&rule.direction)?,
|
||||
@ -704,6 +754,26 @@ impl NetworkFilter {
|
||||
})
|
||||
}
|
||||
|
||||
fn rest2lib_process_all_selector<IPv: Copy>(
|
||||
selector: &NetworkSelectorAll<IPv>,
|
||||
) -> anyhow::Result<NetworkFilterRuleProtocolAllXML<IPv>> {
|
||||
Ok(NetworkFilterRuleProtocolAllXML {
|
||||
srcmacaddr: extract_mac_address_or_var(&selector.srcmacaddr)?,
|
||||
srcipaddr: selector.srcipaddr,
|
||||
// This IP mask is not checked
|
||||
srcipmask: selector.srcipmask,
|
||||
dstipaddr: selector.dstipaddr,
|
||||
// This IP mask is not checked
|
||||
dstipmask: selector.dstipmask,
|
||||
srcipfrom: selector.srcipfrom,
|
||||
srcipto: selector.srcipto,
|
||||
dstipfrom: selector.dstipfrom,
|
||||
dstipto: selector.dstipto,
|
||||
state: selector.state.map(|s| s.to_xml()),
|
||||
comment: extract_nw_filter_comment(&selector.comment)?,
|
||||
})
|
||||
}
|
||||
|
||||
fn rest2lib_process_rule(rule: &NetworkFilterRule) -> anyhow::Result<NetworkFilterRuleXML> {
|
||||
let mut rule_xml = NetworkFilterRuleXML {
|
||||
action: rule.action.to_xml(),
|
||||
@ -714,10 +784,6 @@ impl NetworkFilter {
|
||||
|
||||
for sel in &rule.selectors {
|
||||
match sel {
|
||||
NetworkFilterSelector::All => {
|
||||
rule_xml.all_selectors.push(NetworkFilterRuleProtocolAll {});
|
||||
}
|
||||
|
||||
NetworkFilterSelector::Mac(mac) => {
|
||||
rule_xml.mac_selectors.push(NetworkFilterRuleProtocolMac {
|
||||
srcmacaddr: extract_mac_address_or_var(&mac.src_mac_addr)?,
|
||||
@ -733,6 +799,7 @@ impl NetworkFilter {
|
||||
.arp_selectors
|
||||
.push(Self::rest2lib_process_arp_selector(a)?);
|
||||
}
|
||||
|
||||
NetworkFilterSelector::Rarp(a) => {
|
||||
rule_xml
|
||||
.rarp_selectors
|
||||
@ -742,7 +809,6 @@ impl NetworkFilter {
|
||||
NetworkFilterSelector::IPv4(ip) => rule_xml
|
||||
.ipv4_selectors
|
||||
.push(Self::rest2lib_process_ip_selector(ip)?),
|
||||
|
||||
NetworkFilterSelector::IPv6(ip) => rule_xml
|
||||
.ipv6_selectors
|
||||
.push(Self::rest2lib_process_ip_selector(ip)?),
|
||||
@ -763,6 +829,12 @@ impl NetworkFilter {
|
||||
.icmp_selectors
|
||||
.push(Self::rest2lib_process_layer4_selector(icmp)?),
|
||||
|
||||
NetworkFilterSelector::All(all) => {
|
||||
rule_xml
|
||||
.all_selectors
|
||||
.push(Self::rest2lib_process_all_selector(all)?);
|
||||
}
|
||||
|
||||
NetworkFilterSelector::TCPipv6(tcpv6) => rule_xml
|
||||
.tcp_ipv6_selectors
|
||||
.push(Self::rest2lib_process_layer4_selector(tcpv6)?),
|
||||
@ -778,6 +850,12 @@ impl NetworkFilter {
|
||||
NetworkFilterSelector::ICMPipv6(icmpv6) => rule_xml
|
||||
.imcp_ipv6_selectors
|
||||
.push(Self::rest2lib_process_layer4_selector(icmpv6)?),
|
||||
|
||||
NetworkFilterSelector::Allipv6(all) => {
|
||||
rule_xml
|
||||
.all_ipv6_selectors
|
||||
.push(Self::rest2lib_process_all_selector(all)?);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -26,6 +26,12 @@ import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage";
|
||||
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;
|
||||
@ -61,6 +67,11 @@ export function App() {
|
||||
<Route path="net/:uuid" element={<ViewNetworkRoute />} />
|
||||
<Route path="net/:uuid/edit" element={<EditNetworkRoute />} />
|
||||
|
||||
<Route path="nwfilter" element={<NetworkFiltersListRoute />} />
|
||||
<Route path="nwfilter/new" element={<CreateNWFilterRoute />} />
|
||||
<Route path="nwfilter/:uuid" element={<ViewNWFilterRoute />} />
|
||||
<Route path="nwfilter/:uuid/edit" element={<EditNWFilterRoute />} />
|
||||
|
||||
<Route path="sysinfo" element={<SysInfoRoute />} />
|
||||
<Route path="*" element={<NotFoundRoute />} />
|
||||
</Route>
|
||||
|
@ -1,14 +1,11 @@
|
||||
import { APIClient } from "./ApiClient";
|
||||
import { ServerApi } from "./ServerApi";
|
||||
|
||||
export interface NWFilterChain {
|
||||
protocol: string;
|
||||
suffix?: string;
|
||||
}
|
||||
|
||||
export interface NWFSAll {
|
||||
type: "all";
|
||||
}
|
||||
|
||||
export interface NWFSMac {
|
||||
type: "mac";
|
||||
src_mac_addr?: string;
|
||||
@ -18,8 +15,114 @@ 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 interface NWFSAllBase {
|
||||
srcmacaddr?: string;
|
||||
srcipaddr?: string;
|
||||
srcipmask?: number;
|
||||
dstipaddr?: string;
|
||||
dstipmask?: number;
|
||||
srcipfrom?: string;
|
||||
srcipto?: string;
|
||||
dstipfrom?: string;
|
||||
dstipto?: string;
|
||||
state?: Layer4State;
|
||||
comment?: string;
|
||||
}
|
||||
|
||||
export type NWFSAll = NWFSAllBase & {
|
||||
type: "all";
|
||||
};
|
||||
|
||||
export type NWFSAllIPv6 = NWFSAllBase & {
|
||||
type: "allipv6";
|
||||
};
|
||||
|
||||
export type NWFSelector =
|
||||
| NWFSMac
|
||||
| NWFSArp
|
||||
| NWFSRArp
|
||||
| NFWSIPv4
|
||||
| NFWSIPv6
|
||||
| NFWSTCPv4
|
||||
| NFWSUDPv4
|
||||
| NFWSSCTPv4
|
||||
| NFWSICMPv4
|
||||
| NWFSAll
|
||||
| NFWSTCPv6
|
||||
| NFWSUDPv6
|
||||
| NFWSSCTPv6
|
||||
| NFWSICMPv6
|
||||
| NWFSAllIPv6;
|
||||
|
||||
export interface NWFilterRule {
|
||||
action: "drop" | "reject" | "accept" | "return" | "continue";
|
||||
@ -30,13 +133,21 @@ export interface NWFilterRule {
|
||||
|
||||
export interface NWFilter {
|
||||
name: string;
|
||||
uuid?: string;
|
||||
chain?: NWFilterChain;
|
||||
priority?: number;
|
||||
uuid?: string;
|
||||
join_filters: string[];
|
||||
rules: NWFilterRule[];
|
||||
}
|
||||
|
||||
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
|
||||
@ -53,4 +164,64 @@ export class NWFilterApi {
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the information about a single network filter
|
||||
*/
|
||||
static async GetSingle(uuid: string): Promise<NWFilter> {
|
||||
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<string> {
|
||||
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<void> {
|
||||
await APIClient.exec({
|
||||
method: "DELETE",
|
||||
uri: `/nwfilter/${n.uuid}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -39,10 +39,6 @@ export function NetworkURL(n: NetworkInfo, edit: boolean = false): string {
|
||||
return `/net/${n.uuid}${edit ? "/edit" : ""}`;
|
||||
}
|
||||
|
||||
export function NetworkXMLURL(n: NetworkInfo): string {
|
||||
return `/net/${n.uuid}/xml`;
|
||||
}
|
||||
|
||||
export class NetworkApi {
|
||||
/**
|
||||
* Create a new network
|
||||
@ -164,12 +160,10 @@ export class NetworkApi {
|
||||
/**
|
||||
* Delete a network
|
||||
*/
|
||||
static async Delete(n: NetworkInfo): Promise<NetworkInfo[]> {
|
||||
return (
|
||||
static async Delete(n: NetworkInfo): Promise<void> {
|
||||
await APIClient.exec({
|
||||
method: "DELETE",
|
||||
uri: `/network/${n.uuid}`,
|
||||
})
|
||||
).data;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,8 @@ export interface ServerConfig {
|
||||
oidc_auth_enabled: boolean;
|
||||
iso_mimetypes: string[];
|
||||
net_mac_prefix: string;
|
||||
builtin_nwfilter_rules: string[];
|
||||
nwfilter_chains: string[];
|
||||
constraints: ServerConstraints;
|
||||
}
|
||||
|
||||
@ -20,6 +22,10 @@ export interface ServerConstraints {
|
||||
net_name_size: LenConstraint;
|
||||
net_title_size: LenConstraint;
|
||||
dhcp_reservation_host_name: LenConstraint;
|
||||
nwfilter_name_size: LenConstraint;
|
||||
nwfilter_comment_size: LenConstraint;
|
||||
nwfilter_priority: LenConstraint;
|
||||
nwfilter_selectors_count: LenConstraint;
|
||||
}
|
||||
|
||||
export interface LenConstraint {
|
||||
|
@ -133,10 +133,6 @@ export class VMInfo implements VMInfoInterface {
|
||||
get VNCURL(): string {
|
||||
return `/vm/${this.uuid}/vnc`;
|
||||
}
|
||||
|
||||
get XMLURL(): string {
|
||||
return `/vm/${this.uuid}/xml`;
|
||||
}
|
||||
}
|
||||
|
||||
export class VMApi {
|
||||
|
151
virtweb_frontend/src/routes/EditNWFilterRoute.tsx
Normal file
151
virtweb_frontend/src/routes/EditNWFilterRoute.tsx
Normal file
@ -0,0 +1,151 @@
|
||||
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<NWFilter>({
|
||||
name: "my-filter",
|
||||
chain: { protocol: "root" },
|
||||
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 (
|
||||
<EditNetworkFilterRouteInner
|
||||
nwfilter={nwfilter}
|
||||
creating={true}
|
||||
onCancel={() => 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<NWFilter | undefined>();
|
||||
|
||||
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 (
|
||||
<AsyncWidget
|
||||
loadKey={uuid}
|
||||
ready={nwfilter !== undefined}
|
||||
errMsg="Failed to fetch network filter information!"
|
||||
load={load}
|
||||
build={() => (
|
||||
<EditNetworkFilterRouteInner
|
||||
nwfilter={nwfilter!}
|
||||
creating={false}
|
||||
onCancel={() => navigate(`/nwfilter/${uuid}`)}
|
||||
onSave={updateNetworkFilter}
|
||||
onReplace={setNWFilter}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function EditNetworkFilterRouteInner(p: {
|
||||
nwfilter: NWFilter;
|
||||
creating: boolean;
|
||||
onCancel: () => void;
|
||||
onSave: (vm: NWFilter) => Promise<void>;
|
||||
onReplace: (vm: NWFilter) => void;
|
||||
}): React.ReactElement {
|
||||
const loadingMessage = useLoadingMessage();
|
||||
|
||||
const [changed, setChanged] = React.useState(false);
|
||||
|
||||
const [, updateState] = React.useState<any>();
|
||||
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 (
|
||||
<VirtWebRouteContainer
|
||||
label={p.creating ? "Create a Network Filter" : "Edit Network Filter"}
|
||||
actions={
|
||||
<span>
|
||||
<ConfigImportExportButtons
|
||||
currentConf={p.nwfilter}
|
||||
filename={`nwfilter-${p.nwfilter.name}.json`}
|
||||
importConf={(c) => {
|
||||
p.onReplace(c);
|
||||
valueChanged();
|
||||
}}
|
||||
/>
|
||||
|
||||
{changed && (
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={save}
|
||||
style={{ marginRight: "10px" }}
|
||||
>
|
||||
{p.creating ? "Create" : "Save"}
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={p.onCancel} variant="outlined">
|
||||
Cancel
|
||||
</Button>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<NWFilterDetails
|
||||
nwfilter={p.nwfilter}
|
||||
editable={true}
|
||||
onChange={valueChanged}
|
||||
/>
|
||||
</VirtWebRouteContainer>
|
||||
);
|
||||
}
|
154
virtweb_frontend/src/routes/NetworkFiltersListRoute.tsx
Normal file
154
virtweb_frontend/src/routes/NetworkFiltersListRoute.tsx
Normal file
@ -0,0 +1,154 @@
|
||||
import VisibilityIcon from "@mui/icons-material/Visibility";
|
||||
import {
|
||||
Button,
|
||||
IconButton,
|
||||
Paper,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
ToggleButton,
|
||||
ToggleButtonGroup,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
NWFilter,
|
||||
NWFilterApi,
|
||||
NWFilterIsBuiltin,
|
||||
NWFilterURL,
|
||||
} from "../api/NWFilterApi";
|
||||
import { AsyncWidget } from "../widgets/AsyncWidget";
|
||||
import { RouterLink } from "../widgets/RouterLink";
|
||||
import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer";
|
||||
|
||||
export function NetworkFiltersListRoute(): React.ReactElement {
|
||||
const [list, setList] = React.useState<NWFilter[] | undefined>();
|
||||
|
||||
const [count] = React.useState(1);
|
||||
|
||||
const load = async () => {
|
||||
setList(await NWFilterApi.GetList());
|
||||
};
|
||||
|
||||
return (
|
||||
<AsyncWidget
|
||||
loadKey={count}
|
||||
load={load}
|
||||
ready={list !== undefined}
|
||||
errMsg="Failed to load the list of networks!"
|
||||
build={() => <NetworkFiltersListRouteInner list={list!} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
enum VisibleFilters {
|
||||
All,
|
||||
Builtin,
|
||||
Custom,
|
||||
}
|
||||
|
||||
function NetworkFiltersListRouteInner(p: {
|
||||
list: NWFilter[];
|
||||
}): React.ReactElement {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [visibleFilters, setVisibleFilters] = React.useState(
|
||||
VisibleFilters.All
|
||||
);
|
||||
|
||||
const filteredList = React.useMemo(() => {
|
||||
if (visibleFilters === VisibleFilters.All) return p.list;
|
||||
|
||||
const onlyBuiltin = visibleFilters === VisibleFilters.Builtin;
|
||||
|
||||
return p.list.filter((f) => NWFilterIsBuiltin(f) === onlyBuiltin);
|
||||
}, [visibleFilters]);
|
||||
|
||||
return (
|
||||
<VirtWebRouteContainer
|
||||
label="Network filters"
|
||||
actions={
|
||||
<>
|
||||
<span style={{ flex: 10 }}></span>
|
||||
<ToggleButtonGroup
|
||||
size="small"
|
||||
value={visibleFilters}
|
||||
exclusive
|
||||
onChange={(_ev, v) => setVisibleFilters(v)}
|
||||
aria-label="visible filters"
|
||||
>
|
||||
<ToggleButton value={VisibleFilters.All}>All</ToggleButton>
|
||||
<ToggleButton value={VisibleFilters.Builtin}>Builtin</ToggleButton>
|
||||
<ToggleButton value={VisibleFilters.Custom}>Custom</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
<span style={{ flex: 2 }}></span>
|
||||
|
||||
<RouterLink to="/nwfilter/new">
|
||||
<Button>New</Button>
|
||||
</RouterLink>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Name</TableCell>
|
||||
<TableCell>Chain</TableCell>
|
||||
<TableCell>Priority</TableCell>
|
||||
<TableCell>Referenced filters</TableCell>
|
||||
<TableCell># of rules</TableCell>
|
||||
<TableCell>Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{filteredList.map((t) => {
|
||||
return (
|
||||
<TableRow
|
||||
key={t.uuid}
|
||||
hover
|
||||
onDoubleClick={() => navigate(NWFilterURL(t))}
|
||||
>
|
||||
<TableCell>{t.name}</TableCell>
|
||||
<TableCell>
|
||||
{t.chain?.protocol ?? (
|
||||
<Typography style={{ fontStyle: "italic" }}>
|
||||
None
|
||||
</Typography>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{t.priority ?? (
|
||||
<Typography style={{ fontStyle: "italic" }}>
|
||||
None
|
||||
</Typography>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<ul>
|
||||
{t.join_filters.map((f, n) => (
|
||||
<li key={n}>{f}</li>
|
||||
))}
|
||||
</ul>
|
||||
</TableCell>
|
||||
<TableCell>{t.rules.length}</TableCell>
|
||||
<TableCell>
|
||||
<RouterLink to={NWFilterURL(t)}>
|
||||
<IconButton>
|
||||
<VisibilityIcon />
|
||||
</IconButton>
|
||||
</RouterLink>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</VirtWebRouteContainer>
|
||||
);
|
||||
}
|
65
virtweb_frontend/src/routes/ViewNWFilterRoute.tsx
Normal file
65
virtweb_frontend/src/routes/ViewNWFilterRoute.tsx
Normal file
@ -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<NWFilter | undefined>();
|
||||
|
||||
const load = async () => {
|
||||
setNWFilter(await NWFilterApi.GetSingle(uuid!));
|
||||
};
|
||||
|
||||
return (
|
||||
<AsyncWidget
|
||||
loadKey={uuid}
|
||||
ready={nwfilter !== undefined}
|
||||
errMsg="Failed to fetch network filter information!"
|
||||
load={load}
|
||||
build={() => <ViewNetworkFilterRouteInner nwfilter={nwfilter!} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ViewNetworkFilterRouteInner(p: {
|
||||
nwfilter: NWFilter;
|
||||
}): React.ReactElement {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<VirtWebRouteContainer
|
||||
label={`Network filter ${p.nwfilter.name}`}
|
||||
actions={
|
||||
<span style={{ display: "flex", alignItems: "center" }}>
|
||||
<ConfigImportExportButtons
|
||||
filename={`nwfilter-${p.nwfilter.name}.json`}
|
||||
currentConf={p.nwfilter}
|
||||
/>
|
||||
|
||||
{!NWFilterIsBuiltin(p.nwfilter) && (
|
||||
<Button
|
||||
variant="contained"
|
||||
style={{ marginLeft: "15px" }}
|
||||
onClick={() => navigate(NWFilterURL(p.nwfilter, true))}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
)}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<NWFilterDetails nwfilter={p.nwfilter} editable={false} />
|
||||
</VirtWebRouteContainer>
|
||||
);
|
||||
}
|
5
virtweb_frontend/src/utils/DebugUtils.ts
Normal file
5
virtweb_frontend/src/utils/DebugUtils.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export function isDebug(): boolean {
|
||||
return (
|
||||
!import.meta.env.NODE_ENV || import.meta.env.NODE_ENV === "development"
|
||||
);
|
||||
}
|
@ -3,7 +3,8 @@ import {
|
||||
mdiDisc,
|
||||
mdiHome,
|
||||
mdiInformation,
|
||||
mdiLan
|
||||
mdiLan,
|
||||
mdiSecurityNetwork,
|
||||
} from "@mdi/js";
|
||||
import Icon from "@mdi/react";
|
||||
import {
|
||||
@ -15,6 +16,7 @@ import {
|
||||
ListItemText,
|
||||
} from "@mui/material";
|
||||
import { Outlet, useLocation } from "react-router-dom";
|
||||
import { isDebug } from "../utils/DebugUtils";
|
||||
import { RouterLink } from "./RouterLink";
|
||||
import { VirtWebAppBar } from "./VirtWebAppBar";
|
||||
|
||||
@ -60,6 +62,11 @@ export function BaseAuthenticatedPage(): React.ReactElement {
|
||||
uri="/net"
|
||||
icon={<Icon path={mdiLan} size={1} />}
|
||||
/>
|
||||
<NavLink
|
||||
label="Network filters"
|
||||
uri="/nwfilter"
|
||||
icon={<Icon path={mdiSecurityNetwork} size={1} />}
|
||||
/>
|
||||
<NavLink
|
||||
label="ISO files"
|
||||
uri="/iso"
|
||||
|
@ -1,3 +1,4 @@
|
||||
import React from "react";
|
||||
import { TextInput } from "./TextInput";
|
||||
|
||||
export function IPInput(p: {
|
||||
@ -18,6 +19,47 @@ export function IPInput(p: {
|
||||
);
|
||||
}
|
||||
|
||||
export function IPInputWithMask(p: {
|
||||
label: string;
|
||||
editable: boolean;
|
||||
ip?: string;
|
||||
mask?: number;
|
||||
onValueChange?: (ip?: string, mask?: number) => void;
|
||||
version: 4 | 6;
|
||||
}): React.ReactElement {
|
||||
const showSlash = React.useRef(!!p.mask);
|
||||
|
||||
const currValue =
|
||||
(p.ip ?? "") + (p.mask || showSlash.current ? "/" : "") + (p.mask ?? "");
|
||||
|
||||
const { onValueChange, ...props } = p;
|
||||
return (
|
||||
<TextInput
|
||||
onValueChange={(v) => {
|
||||
showSlash.current = false;
|
||||
if (!v) {
|
||||
onValueChange?.(undefined, undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const split = v?.split("/");
|
||||
const ip =
|
||||
p.version === 4 ? sanitizeIpV4(split[0]) : sanitizeIpV6(split[0]);
|
||||
let mask = undefined;
|
||||
|
||||
if (split.length > 1) {
|
||||
showSlash.current = true;
|
||||
mask = sanitizeMask(p.version, split[1]);
|
||||
}
|
||||
|
||||
onValueChange?.(ip, mask);
|
||||
}}
|
||||
value={currValue}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function sanitizeIpV4(s: string | undefined): string | undefined {
|
||||
if (s === "" || s === undefined) return s;
|
||||
|
||||
@ -77,3 +119,15 @@ function sanitizeIpV6(s: string | undefined): string | undefined {
|
||||
|
||||
return needAnotherIteration ? sanitizeIpV6(res) : res;
|
||||
}
|
||||
|
||||
function sanitizeMask(version: 4 | 6, mask?: string): number | undefined {
|
||||
if (!mask) return undefined;
|
||||
|
||||
const value = Math.floor(Number(mask));
|
||||
|
||||
if (version === 4) {
|
||||
return value < 0 || value > 32 ? 32 : value;
|
||||
} else {
|
||||
return value < 0 || value > 64 ? 64 : value;
|
||||
}
|
||||
}
|
||||
|
27
virtweb_frontend/src/widgets/forms/NWFConnStateInput.tsx
Normal file
27
virtweb_frontend/src/widgets/forms/NWFConnStateInput.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { Layer4State } from "../../api/NWFilterApi";
|
||||
import { SelectInput } from "./SelectInput";
|
||||
|
||||
export function NWFConnStateInput(p: {
|
||||
editable: boolean;
|
||||
value?: Layer4State;
|
||||
onChange: (s?: Layer4State) => void;
|
||||
}): React.ReactElement {
|
||||
return (
|
||||
<SelectInput
|
||||
{...p}
|
||||
label="Connection state"
|
||||
value={p.value}
|
||||
onValueChange={(s) => {
|
||||
p.onChange?.(s as any);
|
||||
}}
|
||||
options={[
|
||||
{ label: "None", value: undefined },
|
||||
{ label: "NEW", value: "NEW" },
|
||||
{ label: "ESTABLISHED", value: "ESTABLISHED" },
|
||||
{ label: "RELATED", value: "RELATED" },
|
||||
{ label: "INVALID", value: "INVALID" },
|
||||
{ label: "NONE", value: "NONE" },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { NWFilter, NWFilterURL } from "../../api/NWFilterApi";
|
||||
import { NWFilterItem } from "../nwfilter/NWFilterItem";
|
||||
import { NWFilterSelectInput } from "./NWFilterSelectInput";
|
||||
|
||||
export function NWFSelectReferencedFilters(p: {
|
||||
editable: boolean;
|
||||
selected: string[];
|
||||
nwFiltersList: NWFilter[];
|
||||
onChange?: () => void;
|
||||
excludedFilters?: string[];
|
||||
}): React.ReactElement {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const nwfilters = React.useMemo(
|
||||
() =>
|
||||
p.excludedFilters
|
||||
? p.nwFiltersList.filter((f) => !p.excludedFilters!.includes(f.name))
|
||||
: p.nwFiltersList,
|
||||
[p.excludedFilters]
|
||||
);
|
||||
|
||||
const selectedFilters = React.useMemo(
|
||||
() => p.selected.map((f) => p.nwFiltersList.find((s) => s.name === f)),
|
||||
[p.selected.length]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{selectedFilters.map((entry, n) => (
|
||||
<NWFilterItem
|
||||
key={n}
|
||||
value={entry}
|
||||
onDelete={
|
||||
p.editable
|
||||
? () => {
|
||||
p.selected.splice(n, 1);
|
||||
p.onChange?.();
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onClick={
|
||||
!p.editable && entry
|
||||
? () => navigate(NWFilterURL(entry))
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
|
||||
{p.editable && (
|
||||
<NWFilterSelectInput
|
||||
editable={p.editable}
|
||||
label="Attach a new filter"
|
||||
canBeNull={false}
|
||||
nwfilters={nwfilters}
|
||||
value={""}
|
||||
onChange={(f) => {
|
||||
p.selected.push(f!);
|
||||
p.onChange?.();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
22
virtweb_frontend/src/widgets/forms/NWFilterPriorityInput.tsx
Normal file
22
virtweb_frontend/src/widgets/forms/NWFilterPriorityInput.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { ServerApi } from "../../api/ServerApi";
|
||||
import { TextInput } from "./TextInput";
|
||||
|
||||
export function NWFilterPriorityInput(p: {
|
||||
editable: boolean;
|
||||
label: string;
|
||||
value?: number;
|
||||
onChange: (priority?: number) => void;
|
||||
}): React.ReactElement {
|
||||
return (
|
||||
<TextInput
|
||||
{...p}
|
||||
value={p.value?.toString()}
|
||||
type="number"
|
||||
onValueChange={(v) => {
|
||||
p.onChange?.(v && v !== "" ? Number(v) : undefined);
|
||||
}}
|
||||
size={ServerApi.Config.constraints.nwfilter_priority}
|
||||
helperText="A lower priority value is accessed before one with a higher value"
|
||||
/>
|
||||
);
|
||||
}
|
709
virtweb_frontend/src/widgets/forms/NWFilterRules.tsx
Normal file
709
virtweb_frontend/src/widgets/forms/NWFilterRules.tsx
Normal file
@ -0,0 +1,709 @@
|
||||
import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward";
|
||||
import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import PlaylistAddIcon from "@mui/icons-material/PlaylistAdd";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardActions,
|
||||
CardContent,
|
||||
IconButton,
|
||||
Paper,
|
||||
Tooltip,
|
||||
} from "@mui/material";
|
||||
import {
|
||||
NWFSAllBase,
|
||||
NWFSArpOrRARP,
|
||||
NWFSIPBase,
|
||||
NWFSLayer4Base,
|
||||
NWFSMac,
|
||||
NWFSelector,
|
||||
NWFilterRule,
|
||||
} from "../../api/NWFilterApi";
|
||||
import { ServerApi } from "../../api/ServerApi";
|
||||
import { EditSection } from "./EditSection";
|
||||
import { IPInput, IPInputWithMask } from "./IPInput";
|
||||
import { MACInput } from "./MACInput";
|
||||
import { NWFConnStateInput } from "./NWFConnStateInput";
|
||||
import { NWFilterPriorityInput } from "./NWFilterPriorityInput";
|
||||
import { PortInput } from "./PortInput";
|
||||
import { SelectInput } from "./SelectInput";
|
||||
import { TextInput } from "./TextInput";
|
||||
|
||||
export function NWFilterRules(p: {
|
||||
editable: boolean;
|
||||
rules: NWFilterRule[];
|
||||
onChange?: () => void;
|
||||
}): React.ReactElement {
|
||||
const addRule = () => {
|
||||
p.rules.push({
|
||||
action: "drop",
|
||||
direction: "inout",
|
||||
selectors: [],
|
||||
});
|
||||
p.onChange?.();
|
||||
};
|
||||
|
||||
const swapRules = (f: number, s: number) => {
|
||||
const swap = p.rules[f];
|
||||
p.rules[f] = p.rules[s];
|
||||
p.rules[s] = swap;
|
||||
p.onChange?.();
|
||||
};
|
||||
|
||||
const deleteRule = (num: number) => {
|
||||
p.rules.splice(num, 1);
|
||||
p.onChange?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<EditSection title="Rules">
|
||||
{p.rules.map((r, n) => (
|
||||
<NWRuleEdit
|
||||
key={n}
|
||||
rule={r}
|
||||
onDelete={() => {
|
||||
deleteRule(n);
|
||||
}}
|
||||
onGoDown={
|
||||
n < p.rules.length - 1 ? () => swapRules(n, n + 1) : undefined
|
||||
}
|
||||
onGoUp={n > 0 ? () => swapRules(n, n - 1) : undefined}
|
||||
{...p}
|
||||
/>
|
||||
))}
|
||||
|
||||
<div style={{ textAlign: "right" }}>
|
||||
{p.editable && <Button onClick={addRule}>Add a new rule</Button>}
|
||||
</div>
|
||||
</EditSection>
|
||||
);
|
||||
}
|
||||
|
||||
function NWRuleEdit(p: {
|
||||
editable: boolean;
|
||||
rule: NWFilterRule;
|
||||
onChange?: () => void;
|
||||
onGoUp?: () => void;
|
||||
onGoDown?: () => void;
|
||||
onDelete: () => void;
|
||||
}): React.ReactElement {
|
||||
const addSelector = () => {
|
||||
p.rule.selectors.push({
|
||||
type: "all",
|
||||
});
|
||||
p.onChange?.();
|
||||
};
|
||||
|
||||
const deleteSelector = (num: number) => {
|
||||
p.rule.selectors.splice(num, 1);
|
||||
p.onChange?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<Card style={{ margin: "30px" }} elevation={3}>
|
||||
<CardContent>
|
||||
<div style={{ display: "flex" }}>
|
||||
<SelectInput
|
||||
editable={p.editable}
|
||||
label="Action"
|
||||
value={p.rule.action}
|
||||
onValueChange={(v) => {
|
||||
p.rule.action = v as any;
|
||||
p.onChange?.();
|
||||
}}
|
||||
options={[
|
||||
{ label: "drop", value: "drop" },
|
||||
{ label: "reject", value: "reject" },
|
||||
{ label: "accept", value: "accept" },
|
||||
{ label: "return", value: "return" },
|
||||
{ label: "continue", value: "continue" },
|
||||
]}
|
||||
/>
|
||||
<span style={{ width: "20px" }}></span>
|
||||
<SelectInput
|
||||
editable={p.editable}
|
||||
label="Direction"
|
||||
value={p.rule.direction}
|
||||
onValueChange={(v) => {
|
||||
p.rule.direction = v as any;
|
||||
p.onChange?.();
|
||||
}}
|
||||
options={[
|
||||
{ label: "in", value: "in" },
|
||||
{ label: "out", value: "out" },
|
||||
{ label: "inout", value: "inout" },
|
||||
]}
|
||||
/>
|
||||
<span style={{ width: "20px" }}></span>
|
||||
<NWFilterPriorityInput
|
||||
{...p}
|
||||
label="Priority"
|
||||
value={p.rule.priority}
|
||||
onChange={(v) => {
|
||||
p.rule.priority = v;
|
||||
p.onChange?.();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{p.rule.selectors.map((s, n) => (
|
||||
<NWFSelectorEdit
|
||||
key={n}
|
||||
editable={p.editable}
|
||||
onChange={p.onChange}
|
||||
selector={s}
|
||||
onDelete={() => deleteSelector(n)}
|
||||
/>
|
||||
))}
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
{p.editable && (
|
||||
<div style={{ display: "flex", width: "100%" }}>
|
||||
<Tooltip title="Remove the rule">
|
||||
<IconButton color="error" onClick={p.onDelete}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<span style={{ flex: 1 }}></span>
|
||||
|
||||
{ServerApi.Config.constraints.nwfilter_selectors_count.max >
|
||||
p.rule.selectors.length && (
|
||||
<Tooltip title="Add a selector">
|
||||
<IconButton onClick={addSelector}>
|
||||
<PlaylistAddIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{p.onGoUp && (
|
||||
<Tooltip title="Move rule upward">
|
||||
<IconButton onClick={p.onGoUp}>
|
||||
<ArrowUpwardIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{p.onGoDown && (
|
||||
<Tooltip title="Move rule downward">
|
||||
<IconButton onClick={p.onGoDown}>
|
||||
<ArrowDownwardIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardActions>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function NWFSelectorEdit(p: {
|
||||
editable: boolean;
|
||||
selector: NWFSelector;
|
||||
onDelete: () => void;
|
||||
onChange?: () => void;
|
||||
}): React.ReactElement {
|
||||
return (
|
||||
<Paper elevation={10} style={{ padding: "10px" }}>
|
||||
<div style={{ display: "flex", width: "100%" }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<SelectInput
|
||||
editable={p.editable}
|
||||
label="Type"
|
||||
onValueChange={(v) => {
|
||||
p.selector.type = v! as any;
|
||||
p.onChange?.();
|
||||
}}
|
||||
value={p.selector.type}
|
||||
options={[
|
||||
{ label: "MAC (Ethernet)", value: "mac" },
|
||||
|
||||
{ label: "ARP", value: "arp" },
|
||||
{ label: "RARP", value: "rarp" },
|
||||
|
||||
{ label: "IPv4", value: "ipv4" },
|
||||
{ label: "IPv6", value: "ipv6" },
|
||||
|
||||
{ label: "TCP over IPv4", value: "tcp" },
|
||||
{ label: "UDP over IPv4", value: "udp" },
|
||||
{ label: "SCTP over IPv4", value: "sctp" },
|
||||
{ label: "ICMPv4", value: "icmp" },
|
||||
|
||||
{ label: "All over IPv4", value: "all" },
|
||||
|
||||
{ label: "TCP over IPv6", value: "tcpipv6" },
|
||||
{ label: "UDP over IPv6", value: "udpipv6" },
|
||||
{ label: "SCTP over IPv6", value: "sctpipv6" },
|
||||
{ label: "ICMPv6", value: "icmpipv6" },
|
||||
|
||||
{ label: "All over IPv6", value: "allipv6" },
|
||||
]}
|
||||
/>
|
||||
|
||||
{p.selector.type === "mac" && (
|
||||
<NWFSelectorMac {...p} selector={p.selector} />
|
||||
)}
|
||||
|
||||
{(p.selector.type === "arp" || p.selector.type === "rarp") && (
|
||||
<NWFSelectorArp {...p} selector={p.selector} />
|
||||
)}
|
||||
|
||||
{p.selector.type === "ipv4" && (
|
||||
<NWFSelectorIP {...p} selector={p.selector} version={4} />
|
||||
)}
|
||||
|
||||
{p.selector.type === "ipv6" && (
|
||||
<NWFSelectorIP {...p} selector={p.selector} version={6} />
|
||||
)}
|
||||
|
||||
{(p.selector.type === "tcp" ||
|
||||
p.selector.type === "udp" ||
|
||||
p.selector.type === "sctp" ||
|
||||
p.selector.type === "icmp") && (
|
||||
<NWFSelectorLayer4 {...p} selector={p.selector} version={4} />
|
||||
)}
|
||||
|
||||
{p.selector.type === "all" && (
|
||||
<NWFSelectorAll {...p} selector={p.selector} version={4} />
|
||||
)}
|
||||
|
||||
{(p.selector.type === "tcpipv6" ||
|
||||
p.selector.type === "udpipv6" ||
|
||||
p.selector.type === "sctpipv6" ||
|
||||
p.selector.type === "icmpipv6") && (
|
||||
<NWFSelectorLayer4 {...p} selector={p.selector} version={6} />
|
||||
)}
|
||||
|
||||
{p.selector.type === "allipv6" && (
|
||||
<NWFSelectorAll {...p} selector={p.selector} version={6} />
|
||||
)}
|
||||
|
||||
<TextInput
|
||||
editable={p.editable}
|
||||
label="Comment"
|
||||
value={p.selector.comment}
|
||||
onValueChange={(v) => {
|
||||
p.selector.comment = v;
|
||||
p.onChange?.();
|
||||
}}
|
||||
size={ServerApi.Config.constraints.nwfilter_comment_size}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{p.editable && (
|
||||
<div style={{ display: "flex", justifyContent: "center" }}>
|
||||
<Tooltip title="Remove the selector">
|
||||
<IconButton color="error" onClick={p.onDelete}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
interface SpecificSelectorEditor<E> {
|
||||
editable: boolean;
|
||||
selector: E;
|
||||
onChange?: () => void;
|
||||
}
|
||||
|
||||
interface SpecificSelectorEditorWithIPVersion<E>
|
||||
extends SpecificSelectorEditor<E> {
|
||||
version: 4 | 6;
|
||||
}
|
||||
|
||||
function NWFSelectorMac(
|
||||
p: SpecificSelectorEditor<NWFSMac>
|
||||
): React.ReactElement {
|
||||
return (
|
||||
<>
|
||||
<MACInput
|
||||
{...p}
|
||||
label="Src mac address"
|
||||
value={p.selector.src_mac_addr}
|
||||
onValueChange={(v) => {
|
||||
p.selector.src_mac_addr = v;
|
||||
p.onChange?.();
|
||||
}}
|
||||
/>
|
||||
<MACInput
|
||||
{...p}
|
||||
label="Src mac mask"
|
||||
value={p.selector.src_mac_mask}
|
||||
onValueChange={(v) => {
|
||||
p.selector.src_mac_mask = v;
|
||||
p.onChange?.();
|
||||
}}
|
||||
/>
|
||||
<MACInput
|
||||
{...p}
|
||||
label="Dst mac address"
|
||||
value={p.selector.dst_mac_addr}
|
||||
onValueChange={(v) => {
|
||||
p.selector.dst_mac_addr = v;
|
||||
p.onChange?.();
|
||||
}}
|
||||
/>
|
||||
<MACInput
|
||||
{...p}
|
||||
label="Dst mac mask"
|
||||
value={p.selector.dst_mac_mask}
|
||||
onValueChange={(v) => {
|
||||
p.selector.dst_mac_mask = v;
|
||||
p.onChange?.();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function NWFSelectorArp(
|
||||
p: SpecificSelectorEditor<NWFSArpOrRARP>
|
||||
): React.ReactElement {
|
||||
return (
|
||||
<>
|
||||
<MACInput
|
||||
{...p}
|
||||
label="Src mac address"
|
||||
value={p.selector.srcmacaddr}
|
||||
onValueChange={(v) => {
|
||||
p.selector.srcmacaddr = v;
|
||||
p.onChange?.();
|
||||
}}
|
||||
/>
|
||||
<MACInput
|
||||
{...p}
|
||||
label="Src mac mask"
|
||||
value={p.selector.srcmacmask}
|
||||
onValueChange={(v) => {
|
||||
p.selector.srcmacmask = v;
|
||||
p.onChange?.();
|
||||
}}
|
||||
/>
|
||||
<MACInput
|
||||
{...p}
|
||||
label="Dst mac address"
|
||||
value={p.selector.dstmacaddr}
|
||||
onValueChange={(v) => {
|
||||
p.selector.dstmacaddr = v;
|
||||
p.onChange?.();
|
||||
}}
|
||||
/>
|
||||
<MACInput
|
||||
{...p}
|
||||
label="Dst mac mask"
|
||||
value={p.selector.dstmacmask}
|
||||
onValueChange={(v) => {
|
||||
p.selector.dstmacmask = v;
|
||||
p.onChange?.();
|
||||
}}
|
||||
/>
|
||||
<IPInputWithMask
|
||||
{...p}
|
||||
label="ARP src ip"
|
||||
ip={p.selector.arpsrcipaddr}
|
||||
mask={p.selector.arpsrcipmask}
|
||||
version={4}
|
||||
onValueChange={(ip, mask) => {
|
||||
p.selector.arpsrcipaddr = ip;
|
||||
p.selector.arpsrcipmask = mask;
|
||||
p.onChange?.();
|
||||
}}
|
||||
/>
|
||||
<IPInputWithMask
|
||||
{...p}
|
||||
label="ARP dst ip"
|
||||
ip={p.selector.arpdstipaddr}
|
||||
mask={p.selector.arpdstipmask}
|
||||
version={4}
|
||||
onValueChange={(ip, mask) => {
|
||||
p.selector.arpdstipaddr = ip;
|
||||
p.selector.arpdstipmask = mask;
|
||||
p.onChange?.();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function NWFSelectorIP(
|
||||
p: SpecificSelectorEditorWithIPVersion<NWFSIPBase>
|
||||
): React.ReactElement {
|
||||
return (
|
||||
<>
|
||||
<MACInput
|
||||
{...p}
|
||||
label="Src mac address"
|
||||
value={p.selector.srcmacaddr}
|
||||
onValueChange={(v) => {
|
||||
p.selector.srcmacaddr = v;
|
||||
p.onChange?.();
|
||||
}}
|
||||
/>
|
||||
<MACInput
|
||||
{...p}
|
||||
label="Src mac mask"
|
||||
value={p.selector.srcmacmask}
|
||||
onValueChange={(v) => {
|
||||
p.selector.srcmacmask = v;
|
||||
p.onChange?.();
|
||||
}}
|
||||
/>
|
||||
<MACInput
|
||||
{...p}
|
||||
label="Dst mac address"
|
||||
value={p.selector.dstmacaddr}
|
||||
onValueChange={(v) => {
|
||||
p.selector.dstmacaddr = v;
|
||||
p.onChange?.();
|
||||
}}
|
||||
/>
|
||||
<MACInput
|
||||
{...p}
|
||||
label="Dst mac mask"
|
||||
value={p.selector.dstmacmask}
|
||||
onValueChange={(v) => {
|
||||
p.selector.dstmacmask = v;
|
||||
p.onChange?.();
|
||||
}}
|
||||
/>
|
||||
<IPInputWithMask
|
||||
{...p}
|
||||
label="Source IP address / mask"
|
||||
ip={p.selector.srcipaddr}
|
||||
mask={p.selector.srcipmask}
|
||||
version={p.version}
|
||||
onValueChange={(ip, mask) => {
|
||||
p.selector.srcipaddr = ip;
|
||||
p.selector.srcipmask = mask;
|
||||
p.onChange?.();
|
||||
}}
|
||||
/>
|
||||
<IPInputWithMask
|
||||
{...p}
|
||||
label="Destination IP address / mask"
|
||||
ip={p.selector.dstipaddr}
|
||||
mask={p.selector.dstipmask}
|
||||
version={p.version}
|
||||
onValueChange={(ip, mask) => {
|
||||
p.selector.dstipaddr = ip;
|
||||
p.selector.dstipmask = mask;
|
||||
p.onChange?.();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function NWFSelectorLayer4(
|
||||
p: SpecificSelectorEditorWithIPVersion<NWFSLayer4Base>
|
||||
): React.ReactElement {
|
||||
return (
|
||||
<>
|
||||
<MACInput
|
||||
{...p}
|
||||
label="Src mac address"
|
||||
value={p.selector.srcmacaddr}
|
||||
onValueChange={(v) => {
|
||||
p.selector.srcmacaddr = v;
|
||||
p.onChange?.();
|
||||
}}
|
||||
/>
|
||||
<IPInputWithMask
|
||||
{...p}
|
||||
label="Source IP address / mask"
|
||||
ip={p.selector.srcipaddr}
|
||||
mask={p.selector.srcipmask}
|
||||
version={p.version}
|
||||
onValueChange={(ip, mask) => {
|
||||
p.selector.srcipaddr = ip;
|
||||
p.selector.srcipmask = mask;
|
||||
p.onChange?.();
|
||||
}}
|
||||
/>
|
||||
<IPInputWithMask
|
||||
{...p}
|
||||
label="Destination IP address / mask"
|
||||
ip={p.selector.dstipaddr}
|
||||
mask={p.selector.dstipmask}
|
||||
version={p.version}
|
||||
onValueChange={(ip, mask) => {
|
||||
p.selector.dstipaddr = ip;
|
||||
p.selector.dstipmask = mask;
|
||||
p.onChange?.();
|
||||
}}
|
||||
/>
|
||||
<IPInput
|
||||
{...p}
|
||||
label="Source IP from"
|
||||
value={p.selector.srcipfrom}
|
||||
onValueChange={(ip) => {
|
||||
p.selector.srcipfrom = ip;
|
||||
p.onChange?.();
|
||||
}}
|
||||
/>
|
||||
<IPInput
|
||||
{...p}
|
||||
label="Source IP to"
|
||||
value={p.selector.srcipto}
|
||||
onValueChange={(ip) => {
|
||||
p.selector.srcipto = ip;
|
||||
p.onChange?.();
|
||||
}}
|
||||
/>
|
||||
<IPInput
|
||||
{...p}
|
||||
label="Destination IP from"
|
||||
value={p.selector.dstipfrom}
|
||||
onValueChange={(ip) => {
|
||||
p.selector.dstipfrom = ip;
|
||||
p.onChange?.();
|
||||
}}
|
||||
/>
|
||||
<IPInput
|
||||
{...p}
|
||||
label="Destination IP to"
|
||||
value={p.selector.dstipto}
|
||||
onValueChange={(ip) => {
|
||||
p.selector.dstipto = ip;
|
||||
p.onChange?.();
|
||||
}}
|
||||
/>
|
||||
<PortInput
|
||||
{...p}
|
||||
label="Source port start"
|
||||
value={p.selector.srcportstart}
|
||||
onChange={(port) => {
|
||||
p.selector.srcportstart = port;
|
||||
p.onChange?.();
|
||||
}}
|
||||
/>
|
||||
<PortInput
|
||||
{...p}
|
||||
label="Source port end"
|
||||
value={p.selector.srcportend}
|
||||
onChange={(port) => {
|
||||
p.selector.srcportend = port;
|
||||
p.onChange?.();
|
||||
}}
|
||||
/>
|
||||
<PortInput
|
||||
{...p}
|
||||
label="Destination port start"
|
||||
value={p.selector.dstportstart}
|
||||
onChange={(port) => {
|
||||
p.selector.dstportstart = port;
|
||||
p.onChange?.();
|
||||
}}
|
||||
/>
|
||||
<PortInput
|
||||
{...p}
|
||||
label="Destination port end"
|
||||
value={p.selector.dstportend}
|
||||
onChange={(port) => {
|
||||
p.selector.dstportend = port;
|
||||
p.onChange?.();
|
||||
}}
|
||||
/>
|
||||
<NWFConnStateInput
|
||||
{...p}
|
||||
value={p.selector.state}
|
||||
onChange={(v) => {
|
||||
p.selector.state = v;
|
||||
p.onChange?.();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function NWFSelectorAll(
|
||||
p: SpecificSelectorEditorWithIPVersion<NWFSAllBase>
|
||||
): React.ReactElement {
|
||||
return (
|
||||
<>
|
||||
<MACInput
|
||||
{...p}
|
||||
label="Src mac address"
|
||||
value={p.selector.srcmacaddr}
|
||||
onValueChange={(v) => {
|
||||
p.selector.srcmacaddr = v;
|
||||
p.onChange?.();
|
||||
}}
|
||||
/>
|
||||
<IPInputWithMask
|
||||
{...p}
|
||||
label="Source IP address / mask"
|
||||
ip={p.selector.srcipaddr}
|
||||
mask={p.selector.srcipmask}
|
||||
version={p.version}
|
||||
onValueChange={(ip, mask) => {
|
||||
p.selector.srcipaddr = ip;
|
||||
p.selector.srcipmask = mask;
|
||||
p.onChange?.();
|
||||
}}
|
||||
/>
|
||||
<IPInputWithMask
|
||||
{...p}
|
||||
label="Destination IP address / mask"
|
||||
ip={p.selector.dstipaddr}
|
||||
mask={p.selector.dstipmask}
|
||||
version={p.version}
|
||||
onValueChange={(ip, mask) => {
|
||||
p.selector.dstipaddr = ip;
|
||||
p.selector.dstipmask = mask;
|
||||
p.onChange?.();
|
||||
}}
|
||||
/>
|
||||
<IPInput
|
||||
{...p}
|
||||
label="Source IP from"
|
||||
value={p.selector.srcipfrom}
|
||||
onValueChange={(ip) => {
|
||||
p.selector.srcipfrom = ip;
|
||||
p.onChange?.();
|
||||
}}
|
||||
/>
|
||||
<IPInput
|
||||
{...p}
|
||||
label="Source IP to"
|
||||
value={p.selector.srcipto}
|
||||
onValueChange={(ip) => {
|
||||
p.selector.srcipto = ip;
|
||||
p.onChange?.();
|
||||
}}
|
||||
/>
|
||||
<IPInput
|
||||
{...p}
|
||||
label="Destination IP from"
|
||||
value={p.selector.dstipfrom}
|
||||
onValueChange={(ip) => {
|
||||
p.selector.dstipfrom = ip;
|
||||
p.onChange?.();
|
||||
}}
|
||||
/>
|
||||
<IPInput
|
||||
{...p}
|
||||
label="Destination IP to"
|
||||
value={p.selector.dstipto}
|
||||
onValueChange={(ip) => {
|
||||
p.selector.dstipto = ip;
|
||||
p.onChange?.();
|
||||
}}
|
||||
/>
|
||||
<NWFConnStateInput
|
||||
{...p}
|
||||
value={p.selector.state}
|
||||
onChange={(v) => {
|
||||
p.selector.state = v;
|
||||
p.onChange?.();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
63
virtweb_frontend/src/widgets/forms/NWFilterSelectInput.tsx
Normal file
63
virtweb_frontend/src/widgets/forms/NWFilterSelectInput.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
import { Autocomplete, TextField } from "@mui/material";
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { NWFilter, NWFilterURL } from "../../api/NWFilterApi";
|
||||
import { NWFilterItem } from "../nwfilter/NWFilterItem";
|
||||
|
||||
export function NWFilterSelectInput(p: {
|
||||
editable: boolean;
|
||||
label?: string;
|
||||
nwfilters: NWFilter[];
|
||||
value?: string;
|
||||
onChange?: (name?: string) => void;
|
||||
canBeNull: boolean;
|
||||
}): React.ReactElement {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
const selectedValue = p.nwfilters.find((o) => o.name === p.value);
|
||||
if (!p.editable && !selectedValue) return <></>;
|
||||
|
||||
if (selectedValue)
|
||||
return (
|
||||
<NWFilterItem
|
||||
value={selectedValue}
|
||||
onDelete={p.editable ? () => p.onChange?.(undefined) : undefined}
|
||||
onClick={
|
||||
!p.editable && selectedValue
|
||||
? () => navigate(NWFilterURL(selectedValue))
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
open={open}
|
||||
onOpen={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
onClose={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
readOnly={!p.editable}
|
||||
options={[...(p.canBeNull ? [undefined] : []), ...p.nwfilters]}
|
||||
getOptionLabel={(o) => o?.name ?? "Unspecified"}
|
||||
value={selectedValue}
|
||||
renderInput={(params) => (
|
||||
<TextField {...params} variant="standard" label={p.label} />
|
||||
)}
|
||||
renderOption={(_props, option, _state) => (
|
||||
<NWFilterItem
|
||||
dense
|
||||
onClick={() => {
|
||||
p.onChange?.(option?.name);
|
||||
setOpen(false);
|
||||
}}
|
||||
value={option}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
@ -14,11 +14,11 @@ import {
|
||||
import { DHCPConfig, DHCPHost } from "../../api/NetworksApi";
|
||||
import { ServerApi } from "../../api/ServerApi";
|
||||
import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider";
|
||||
import { IPInput } from "../forms/IPInput";
|
||||
import { MACInput } from "../forms/MACInput";
|
||||
import { TextInput } from "../forms/TextInput";
|
||||
import { IPInput } from "./IPInput";
|
||||
import { MACInput } from "./MACInput";
|
||||
import { TextInput } from "./TextInput";
|
||||
|
||||
export function DHCPHostReservations(p: {
|
||||
export function NetDHCPHostReservations(p: {
|
||||
editable: boolean;
|
||||
dhcp: DHCPConfig;
|
||||
version: 4 | 6;
|
28
virtweb_frontend/src/widgets/forms/PortInput.tsx
Normal file
28
virtweb_frontend/src/widgets/forms/PortInput.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import { TextInput } from "./TextInput";
|
||||
|
||||
export function PortInput(p: {
|
||||
editable: boolean;
|
||||
label: string;
|
||||
value?: number;
|
||||
onChange: (value: number | undefined) => void;
|
||||
}): React.ReactElement {
|
||||
return (
|
||||
<TextInput
|
||||
{...p}
|
||||
value={p.value?.toString() ?? ""}
|
||||
type="number"
|
||||
onValueChange={(v) => {
|
||||
p.onChange?.(sanitizePort(v));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function sanitizePort(port?: string): number | undefined {
|
||||
if (port === undefined) return undefined;
|
||||
const val = Number(port);
|
||||
|
||||
if (val < 0) return 0;
|
||||
if (val > 65535) return 65535;
|
||||
return val;
|
||||
}
|
@ -16,6 +16,7 @@ export function TextInput(p: {
|
||||
maxRows?: number;
|
||||
type?: React.HTMLInputTypeAttribute;
|
||||
style?: React.CSSProperties;
|
||||
helperText?: string;
|
||||
}): React.ReactElement {
|
||||
if (!p.editable && (p.value ?? "") === "") return <></>;
|
||||
|
||||
@ -54,7 +55,7 @@ export function TextInput(p: {
|
||||
minRows={p.minRows}
|
||||
maxRows={p.maxRows}
|
||||
error={valueError !== undefined}
|
||||
helperText={valueError}
|
||||
helperText={valueError ?? p.helperText}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -17,10 +17,11 @@ import { ServerApi } from "../../api/ServerApi";
|
||||
import { VMInfo, VMNetInterface } from "../../api/VMApi";
|
||||
import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider";
|
||||
import { randomMacAddress } from "../../utils/RandUtils";
|
||||
import { EditSection } from "./EditSection";
|
||||
import { MACInput } from "./MACInput";
|
||||
import { NWFilterSelectInput } from "./NWFilterSelectInput";
|
||||
import { SelectInput } from "./SelectInput";
|
||||
import { VMNetworkFilterParameters } from "./VMNetworkFilterParameters";
|
||||
import { EditSection } from "./EditSection";
|
||||
|
||||
export function VMNetworksList(p: {
|
||||
vm: VMInfo;
|
||||
@ -174,11 +175,11 @@ function NetworkInfoWidget(p: {
|
||||
/>
|
||||
|
||||
{/* Network Filter */}
|
||||
<SelectInput
|
||||
<NWFilterSelectInput
|
||||
editable={p.editable}
|
||||
label="Network filter"
|
||||
value={p.network.nwfilterref?.name}
|
||||
onValueChange={(v) => {
|
||||
onChange={(v) => {
|
||||
if (v && !p.network.nwfilterref) {
|
||||
p.network.nwfilterref = { name: v, parameters: [] };
|
||||
} else if (v) {
|
||||
@ -188,16 +189,8 @@ function NetworkInfoWidget(p: {
|
||||
}
|
||||
p.onChange?.();
|
||||
}}
|
||||
options={[
|
||||
{ label: "No network filer", value: undefined },
|
||||
...p.networkFiltersList.map((v) => {
|
||||
return {
|
||||
value: v.name,
|
||||
label: `${v.name} (${v.chain?.protocol ?? "unspecified"})`,
|
||||
description: `${v.rules.length} rules - ${v.join_filters.length} joint filters`,
|
||||
};
|
||||
}),
|
||||
]}
|
||||
canBeNull={true}
|
||||
nwfilters={p.networkFiltersList}
|
||||
/>
|
||||
|
||||
{p.network.nwfilterref && (
|
||||
|
@ -13,7 +13,7 @@ import { IPInput } from "../forms/IPInput";
|
||||
import { ResAutostartInput } from "../forms/ResAutostartInput";
|
||||
import { SelectInput } from "../forms/SelectInput";
|
||||
import { TextInput } from "../forms/TextInput";
|
||||
import { DHCPHostReservations } from "./DHCPHostReservations";
|
||||
import { NetDHCPHostReservations } from "../forms/NetDHCPHostReservations";
|
||||
import { XMLAsyncWidget } from "../XMLWidget";
|
||||
|
||||
interface DetailsProps {
|
||||
@ -23,10 +23,10 @@ interface DetailsProps {
|
||||
}
|
||||
|
||||
export function NetworkDetails(p: DetailsProps): React.ReactElement {
|
||||
const [cardsList, setCardsList] = React.useState<string[] | any>();
|
||||
const [nicsList, setNicsList] = React.useState<string[] | any>();
|
||||
|
||||
const load = async () => {
|
||||
setCardsList(await ServerApi.GetNetworksList());
|
||||
setNicsList(await ServerApi.GetNetworksList());
|
||||
};
|
||||
|
||||
return (
|
||||
@ -34,7 +34,7 @@ export function NetworkDetails(p: DetailsProps): React.ReactElement {
|
||||
loadKey={"1"}
|
||||
load={load}
|
||||
errMsg="Failed to load the list of host network cards!"
|
||||
build={() => <NetworkDetailsInner cardsList={cardsList} {...p} />}
|
||||
build={() => <NetworkDetailsInner nicsList={nicsList} {...p} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -47,7 +47,7 @@ enum NetTab {
|
||||
Danger,
|
||||
}
|
||||
|
||||
type DetailsInnerProps = DetailsProps & { cardsList: string[] };
|
||||
type DetailsInnerProps = DetailsProps & { nicsList: string[] };
|
||||
|
||||
function NetworkDetailsInner(p: DetailsInnerProps): React.ReactElement {
|
||||
const [currTab, setCurrTab] = React.useState(NetTab.General);
|
||||
@ -176,7 +176,7 @@ function NetworkDetailsTabGeneral(p: DetailsInnerProps): React.ReactElement {
|
||||
value={p.net.device}
|
||||
options={[
|
||||
{ label: "Default interface", value: undefined },
|
||||
...p.cardsList.map((d) => {
|
||||
...p.nicsList.map((d) => {
|
||||
return { label: d, value: d };
|
||||
}),
|
||||
]}
|
||||
@ -374,7 +374,7 @@ function IPSection(p: {
|
||||
|
||||
{p.config?.dhcp && (p.editable || p.config.dhcp.hosts.length > 0) && (
|
||||
<EditSection title="DHCP hosts reservations">
|
||||
<DHCPHostReservations
|
||||
<NetDHCPHostReservations
|
||||
{...p}
|
||||
dhcp={p.config.dhcp}
|
||||
onChange={(d) => {
|
||||
|
218
virtweb_frontend/src/widgets/nwfilter/NWFilterDetails.tsx
Normal file
218
virtweb_frontend/src/widgets/nwfilter/NWFilterDetails.tsx
Normal file
@ -0,0 +1,218 @@
|
||||
import { Button, Grid } from "@mui/material";
|
||||
import React, { ReactElement } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
NWFilter,
|
||||
NWFilterApi,
|
||||
NWFilterIsBuiltin,
|
||||
} from "../../api/NWFilterApi";
|
||||
import { useAlert } from "../../hooks/providers/AlertDialogProvider";
|
||||
import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider";
|
||||
import { useSnackbar } from "../../hooks/providers/SnackbarProvider";
|
||||
import { AsyncWidget } from "../AsyncWidget";
|
||||
import { TabsWidget } from "../TabsWidget";
|
||||
import { XMLAsyncWidget } from "../XMLWidget";
|
||||
import { EditSection } from "../forms/EditSection";
|
||||
import { TextInput } from "../forms/TextInput";
|
||||
import { ServerApi } from "../../api/ServerApi";
|
||||
import { SelectInput } from "../forms/SelectInput";
|
||||
import { NWFSelectReferencedFilters } from "../forms/NWFSelectReferencedFilters";
|
||||
import { NWFilterRules } from "../forms/NWFilterRules";
|
||||
import { NWFilterPriorityInput } from "../forms/NWFilterPriorityInput";
|
||||
|
||||
interface DetailsProps {
|
||||
nwfilter: NWFilter;
|
||||
editable: boolean;
|
||||
onChange?: () => void;
|
||||
}
|
||||
|
||||
export function NWFilterDetails(p: DetailsProps): ReactElement {
|
||||
const [nwFiltersList, setNwFiltersList] = React.useState<NWFilter[] | any>();
|
||||
|
||||
const load = async () => {
|
||||
setNwFiltersList(await NWFilterApi.GetList());
|
||||
};
|
||||
|
||||
return (
|
||||
<AsyncWidget
|
||||
loadKey={p.nwfilter.uuid}
|
||||
load={load}
|
||||
errMsg="Failed to load the list of network filters!"
|
||||
build={() => (
|
||||
<NetworkFilterDetailsInner nwFiltersList={nwFiltersList} {...p} />
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
type InnerDetailsProps = DetailsProps & { nwFiltersList: NWFilter[] };
|
||||
|
||||
enum NetFilterTab {
|
||||
General = 0,
|
||||
Rules,
|
||||
XML,
|
||||
Danger,
|
||||
}
|
||||
|
||||
export function NetworkFilterDetailsInner(
|
||||
p: InnerDetailsProps
|
||||
): React.ReactElement {
|
||||
const [currTab, setCurrTab] = React.useState(NetFilterTab.General);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TabsWidget
|
||||
currTab={currTab}
|
||||
onTabChange={setCurrTab}
|
||||
options={[
|
||||
{ label: "General", value: NetFilterTab.General, visible: true },
|
||||
{
|
||||
label: "Rules",
|
||||
value: NetFilterTab.Rules,
|
||||
visible: p.editable || p.nwfilter.rules.length > 0,
|
||||
},
|
||||
|
||||
{
|
||||
label: "XML",
|
||||
value: NetFilterTab.XML,
|
||||
visible: !p.editable,
|
||||
},
|
||||
{
|
||||
label: "Danger zone",
|
||||
value: NetFilterTab.Danger,
|
||||
color: "red",
|
||||
visible: !p.editable && !NWFilterIsBuiltin(p.nwfilter),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{currTab === NetFilterTab.General && (
|
||||
<NetworkFilterDetailsTabGeneral {...p} />
|
||||
)}
|
||||
{currTab === NetFilterTab.Rules && (
|
||||
<NetworkFilterDetailsTabRules {...p} />
|
||||
)}
|
||||
{currTab === NetFilterTab.XML && <NetworkFilterDetailsTabXML {...p} />}
|
||||
{currTab === NetFilterTab.Danger && (
|
||||
<NetworkFilterDetailsTabDanger {...p} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function NetworkFilterDetailsTabGeneral(
|
||||
p: InnerDetailsProps
|
||||
): React.ReactElement {
|
||||
return (
|
||||
<Grid container spacing={2}>
|
||||
{/* Metadata section */}
|
||||
<EditSection title="Metadata">
|
||||
<TextInput
|
||||
label="Name"
|
||||
editable={p.editable}
|
||||
value={p.nwfilter.name}
|
||||
onValueChange={(v) => {
|
||||
p.nwfilter.name = v ?? "";
|
||||
p.onChange?.();
|
||||
}}
|
||||
checkValue={(v) => /^[a-zA-Z0-9\_\-]+$/.test(v)}
|
||||
size={ServerApi.Config.constraints.nwfilter_name_size}
|
||||
/>
|
||||
|
||||
<TextInput label="UUID" editable={false} value={p.nwfilter.uuid} />
|
||||
|
||||
<SelectInput
|
||||
label="Chain"
|
||||
editable={p.editable}
|
||||
value={p.nwfilter.chain?.protocol}
|
||||
onValueChange={(v) => {
|
||||
p.nwfilter.chain = v ? { protocol: v } : undefined;
|
||||
p.onChange?.();
|
||||
}}
|
||||
options={ServerApi.Config.nwfilter_chains.map((c) => {
|
||||
return { label: c, value: c };
|
||||
})}
|
||||
/>
|
||||
|
||||
<NWFilterPriorityInput
|
||||
{...p}
|
||||
label="Priority"
|
||||
value={p.nwfilter.priority}
|
||||
onChange={(pri) => {
|
||||
p.nwfilter.priority = pri;
|
||||
p.onChange?.();
|
||||
}}
|
||||
/>
|
||||
</EditSection>
|
||||
|
||||
{/* Referenced filters */}
|
||||
{(p.editable || p.nwfilter.join_filters.length > 0) && (
|
||||
<EditSection title="Referenced filters">
|
||||
<NWFSelectReferencedFilters
|
||||
selected={p.nwfilter.join_filters}
|
||||
excludedFilters={[p.nwfilter.name]}
|
||||
{...p}
|
||||
/>
|
||||
</EditSection>
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
function NetworkFilterDetailsTabRules(
|
||||
p: InnerDetailsProps
|
||||
): React.ReactElement {
|
||||
return (
|
||||
<NWFilterRules
|
||||
editable={p.editable}
|
||||
rules={p.nwfilter.rules}
|
||||
onChange={p.onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function NetworkFilterDetailsTabXML(p: InnerDetailsProps): React.ReactElement {
|
||||
return (
|
||||
<XMLAsyncWidget
|
||||
errMsg="Failed to load network filter XML definition!"
|
||||
identifier={p.nwfilter.uuid!}
|
||||
load={() => NWFilterApi.GetSingleXML(p.nwfilter.uuid!)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function NetworkFilterDetailsTabDanger(
|
||||
p: InnerDetailsProps
|
||||
): React.ReactElement {
|
||||
const confirm = useConfirm();
|
||||
const snackbar = useSnackbar();
|
||||
const alert = useAlert();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const requestDelete = async () => {
|
||||
try {
|
||||
if (
|
||||
!(await confirm(
|
||||
"Do you really want to delete this network filter?",
|
||||
`Delete network filter ${p.nwfilter.name}`,
|
||||
"Delete"
|
||||
))
|
||||
)
|
||||
return;
|
||||
|
||||
await NWFilterApi.Delete(p.nwfilter);
|
||||
|
||||
navigate("/nwfilter");
|
||||
snackbar("The network filter was successfully deleted!");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert(`Failed to delete the network filter!\n${e}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Button color="error" onClick={requestDelete}>
|
||||
Delete this network filter
|
||||
</Button>
|
||||
);
|
||||
}
|
71
virtweb_frontend/src/widgets/nwfilter/NWFilterItem.tsx
Normal file
71
virtweb_frontend/src/widgets/nwfilter/NWFilterItem.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
import { mdiSecurityNetwork } from "@mdi/js";
|
||||
import Icon from "@mdi/react";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
IconButton,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemButton,
|
||||
ListItemText,
|
||||
} from "@mui/material";
|
||||
import { NWFilter } from "../../api/NWFilterApi";
|
||||
|
||||
export function NWFilterItem(p: {
|
||||
value?: NWFilter;
|
||||
onClick?: () => void;
|
||||
dense?: boolean;
|
||||
onDelete?: () => void;
|
||||
}): React.ReactElement {
|
||||
const specs = [];
|
||||
if (p.value) {
|
||||
if (p.value.rules.length === 1) specs.push(`1 rule`);
|
||||
else if (p.value.rules.length > 1)
|
||||
specs.push(`${p.value.rules.length} rules`);
|
||||
|
||||
if (p.value.join_filters.length === 1) specs.push(`1 joint filter`);
|
||||
else if (p.value.join_filters.length > 1)
|
||||
specs.push(`${p.value.join_filters.length} joint filters`);
|
||||
|
||||
if (p.value.priority) specs.push(`priority: ${p.value.priority}`);
|
||||
}
|
||||
const inner = (
|
||||
<>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<Icon path={mdiSecurityNetwork} />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={
|
||||
p.value
|
||||
? `${p.value.name} (${p.value.chain?.protocol ?? "unspecified"})`
|
||||
: "Unspecified"
|
||||
}
|
||||
secondary={specs.join(" / ")}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
if (p.onClick)
|
||||
return (
|
||||
<ListItemButton onClick={p.onClick} dense={p.dense}>
|
||||
{inner}
|
||||
</ListItemButton>
|
||||
);
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
secondaryAction={
|
||||
p.onDelete ? (
|
||||
<IconButton onClick={p.onDelete}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
{inner}
|
||||
</ListItem>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user