mirror of
https://git.proxmox.com/git/proxmox-firewall
synced 2025-10-04 10:08:57 +00:00
add proxmox-ve-rs crate - move proxmox-ve-config there
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com> Reviewed-by: Wolfgang Bumiller <w.bumiller@proxmox.com> [ TL: add dependency to d/control and update its version ] Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
This commit is contained in:
parent
bea3e651b4
commit
aa76920360
@ -1,7 +1,9 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"proxmox-ve-config",
|
|
||||||
"proxmox-nftables",
|
"proxmox-nftables",
|
||||||
"proxmox-firewall",
|
"proxmox-firewall",
|
||||||
]
|
]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
proxmox-ve-config = { version = "0.2.0" }
|
||||||
|
2
Makefile
2
Makefile
@ -29,7 +29,7 @@ cargo-build:
|
|||||||
build: $(BUILDDIR)
|
build: $(BUILDDIR)
|
||||||
$(BUILDDIR):
|
$(BUILDDIR):
|
||||||
rm -rf $@ $@.tmp; mkdir $@.tmp
|
rm -rf $@ $@.tmp; mkdir $@.tmp
|
||||||
cp -a proxmox-firewall proxmox-nftables proxmox-ve-config debian Cargo.toml Makefile defines.mk $@.tmp/
|
cp -a proxmox-firewall proxmox-nftables debian Cargo.toml Makefile defines.mk $@.tmp/
|
||||||
mv $@.tmp $@
|
mv $@.tmp $@
|
||||||
|
|
||||||
.PHONY: deb
|
.PHONY: deb
|
||||||
|
1
debian/control
vendored
1
debian/control
vendored
@ -13,6 +13,7 @@ Build-Depends: cargo:native,
|
|||||||
librust-proxmox-schema-3+default-dev (>= 3.1.2-~~),
|
librust-proxmox-schema-3+default-dev (>= 3.1.2-~~),
|
||||||
librust-proxmox-sortable-macro-dev,
|
librust-proxmox-sortable-macro-dev,
|
||||||
librust-proxmox-sys-dev (>= 0.6~),
|
librust-proxmox-sys-dev (>= 0.6~),
|
||||||
|
librust-proxmox-ve-config-dev (>= 0.2~),
|
||||||
librust-serde-1+default-dev,
|
librust-serde-1+default-dev,
|
||||||
librust-serde-1+derive-dev,
|
librust-serde-1+derive-dev,
|
||||||
librust-serde-json-1+default-dev,
|
librust-serde-json-1+default-dev,
|
||||||
|
@ -21,7 +21,7 @@ serde_json = "1"
|
|||||||
signal-hook = "0.3"
|
signal-hook = "0.3"
|
||||||
|
|
||||||
proxmox-nftables = { path = "../proxmox-nftables", features = ["config-ext"] }
|
proxmox-nftables = { path = "../proxmox-nftables", features = ["config-ext"] }
|
||||||
proxmox-ve-config = { path = "../proxmox-ve-config" }
|
proxmox-ve-config = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
insta = { version = "1.21", features = ["json"] }
|
insta = { version = "1.21", features = ["json"] }
|
||||||
|
@ -22,4 +22,4 @@ serde = { version = "1", features = [ "derive" ] }
|
|||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
serde_plain = "1"
|
serde_plain = "1"
|
||||||
|
|
||||||
proxmox-ve-config = { path = "../proxmox-ve-config", optional = true }
|
proxmox-ve-config = { workspace = true, optional = true }
|
||||||
|
@ -1,25 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "proxmox-ve-config"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
authors = [
|
|
||||||
"Wolfgang Bumiller <w.bumiller@proxmox.com>",
|
|
||||||
"Stefan Hanreich <s.hanreich@proxmox.com>",
|
|
||||||
"Proxmox Support Team <support@proxmox.com>",
|
|
||||||
]
|
|
||||||
description = "Proxmox VE config parsing"
|
|
||||||
license = "AGPL-3"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
log = "0.4"
|
|
||||||
anyhow = "1"
|
|
||||||
nix = "0.26"
|
|
||||||
|
|
||||||
serde = { version = "1", features = [ "derive" ] }
|
|
||||||
serde_json = "1"
|
|
||||||
serde_plain = "1"
|
|
||||||
serde_with = "3"
|
|
||||||
|
|
||||||
proxmox-schema = "3.1.2"
|
|
||||||
proxmox-sys = "0.6"
|
|
||||||
proxmox-sortable-macro = "0.1.3"
|
|
@ -1,52 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"name": "amanda",
|
|
||||||
"v4": true,
|
|
||||||
"v6": true,
|
|
||||||
"udp": 10080
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "ftp",
|
|
||||||
"v4": true,
|
|
||||||
"v6": true,
|
|
||||||
"tcp": 21
|
|
||||||
} ,
|
|
||||||
{
|
|
||||||
"name": "irc",
|
|
||||||
"v4": true,
|
|
||||||
"tcp": 6667
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "netbios-ns",
|
|
||||||
"v4": true,
|
|
||||||
"udp": 137
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "pptp",
|
|
||||||
"v4": true,
|
|
||||||
"tcp": 1723
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "sane",
|
|
||||||
"v4": true,
|
|
||||||
"v6": true,
|
|
||||||
"tcp": 6566
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "sip",
|
|
||||||
"v4": true,
|
|
||||||
"v6": true,
|
|
||||||
"udp": 5060
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "snmp",
|
|
||||||
"v4": true,
|
|
||||||
"udp": 161
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "tftp",
|
|
||||||
"v4": true,
|
|
||||||
"v6": true,
|
|
||||||
"udp": 69
|
|
||||||
}
|
|
||||||
]
|
|
@ -1,923 +0,0 @@
|
|||||||
{
|
|
||||||
"Amanda": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "10080",
|
|
||||||
"proto": "udp"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"dport": "10080",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Amanda Backup"
|
|
||||||
},
|
|
||||||
"Auth": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "113",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Auth (identd) traffic"
|
|
||||||
},
|
|
||||||
"BGP": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "179",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Border Gateway Protocol traffic"
|
|
||||||
},
|
|
||||||
"BitTorrent": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "6881:6889",
|
|
||||||
"proto": "tcp"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"dport": "6881",
|
|
||||||
"proto": "udp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "BitTorrent traffic for BitTorrent 3.1 and earlier"
|
|
||||||
},
|
|
||||||
"BitTorrent32": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "6881:6999",
|
|
||||||
"proto": "tcp"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"dport": "6881",
|
|
||||||
"proto": "udp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "BitTorrent traffic for BitTorrent 3.2 and later"
|
|
||||||
},
|
|
||||||
"CVS": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "2401",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Concurrent Versions System pserver traffic"
|
|
||||||
},
|
|
||||||
"Ceph": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "6789",
|
|
||||||
"proto": "tcp"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"dport": "3300",
|
|
||||||
"proto": "tcp"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"dport": "6800:7300",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Ceph Storage Cluster traffic (Ceph Monitors, OSD & MDS Daemons)"
|
|
||||||
},
|
|
||||||
"Citrix": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "1494",
|
|
||||||
"proto": "tcp"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"dport": "1604",
|
|
||||||
"proto": "udp"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"dport": "2598",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Citrix/ICA traffic (ICA, ICA Browser, CGP)"
|
|
||||||
},
|
|
||||||
"DAAP": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "3689",
|
|
||||||
"proto": "tcp"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"dport": "3689",
|
|
||||||
"proto": "udp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Digital Audio Access Protocol traffic (iTunes, Rythmbox daemons)"
|
|
||||||
},
|
|
||||||
"DCC": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "6277",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Distributed Checksum Clearinghouse spam filtering mechanism"
|
|
||||||
},
|
|
||||||
"DHCPfwd": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "67:68",
|
|
||||||
"proto": "udp",
|
|
||||||
"sport": "67:68"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Forwarded DHCP traffic"
|
|
||||||
},
|
|
||||||
"DHCPv6": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "546:547",
|
|
||||||
"proto": "udp",
|
|
||||||
"sport": "546:547"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "DHCPv6 traffic"
|
|
||||||
},
|
|
||||||
"DNS": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "53",
|
|
||||||
"proto": "udp"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"dport": "53",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Domain Name System traffic (upd and tcp)"
|
|
||||||
},
|
|
||||||
"Distcc": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "3632",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Distributed Compiler service"
|
|
||||||
},
|
|
||||||
"FTP": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "21",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "File Transfer Protocol"
|
|
||||||
},
|
|
||||||
"Finger": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "79",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Finger protocol (RFC 742)"
|
|
||||||
},
|
|
||||||
"GNUnet": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "2086",
|
|
||||||
"proto": "tcp"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"dport": "2086",
|
|
||||||
"proto": "udp"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"dport": "1080",
|
|
||||||
"proto": "tcp"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"dport": "1080",
|
|
||||||
"proto": "udp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "GNUnet secure peer-to-peer networking traffic"
|
|
||||||
},
|
|
||||||
"GRE": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"proto": "47"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Generic Routing Encapsulation tunneling protocol"
|
|
||||||
},
|
|
||||||
"Git": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "9418",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Git distributed revision control traffic"
|
|
||||||
},
|
|
||||||
"HKP": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "11371",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "OpenPGP HTTP key server protocol traffic"
|
|
||||||
},
|
|
||||||
"HTTP": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "80",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Hypertext Transfer Protocol (WWW)"
|
|
||||||
},
|
|
||||||
"HTTPS": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "443",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Hypertext Transfer Protocol (WWW) over SSL"
|
|
||||||
},
|
|
||||||
"HTTP/3": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "443",
|
|
||||||
"proto": "udp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Hypertext Transfer Protocol v3"
|
|
||||||
},
|
|
||||||
"ICPV2": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "3130",
|
|
||||||
"proto": "udp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Internet Cache Protocol V2 (Squid) traffic"
|
|
||||||
},
|
|
||||||
"ICQ": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "5190",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "AOL Instant Messenger traffic"
|
|
||||||
},
|
|
||||||
"IMAP": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "143",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Internet Message Access Protocol"
|
|
||||||
},
|
|
||||||
"IMAPS": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "993",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Internet Message Access Protocol over SSL"
|
|
||||||
},
|
|
||||||
"IPIP": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"proto": "94"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "IPIP capsulation traffic"
|
|
||||||
},
|
|
||||||
"IPsec": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "500",
|
|
||||||
"proto": "udp",
|
|
||||||
"sport": "500"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"proto": "50"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "IPsec traffic"
|
|
||||||
},
|
|
||||||
"IPsecah": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "500",
|
|
||||||
"proto": "udp",
|
|
||||||
"sport": "500"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"proto": "51"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "IPsec authentication (AH) traffic"
|
|
||||||
},
|
|
||||||
"IPsecnat": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "500",
|
|
||||||
"proto": "udp"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"dport": "4500",
|
|
||||||
"proto": "udp"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"proto": "50"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "IPsec traffic and Nat-Traversal"
|
|
||||||
},
|
|
||||||
"IRC": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "6667",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Internet Relay Chat traffic"
|
|
||||||
},
|
|
||||||
"Jetdirect": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "9100",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "HP Jetdirect printing"
|
|
||||||
},
|
|
||||||
"L2TP": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "1701",
|
|
||||||
"proto": "udp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Layer 2 Tunneling Protocol traffic"
|
|
||||||
},
|
|
||||||
"LDAP": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "389",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Lightweight Directory Access Protocol traffic"
|
|
||||||
},
|
|
||||||
"LDAPS": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "636",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Secure Lightweight Directory Access Protocol traffic"
|
|
||||||
},
|
|
||||||
"MDNS": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "5353",
|
|
||||||
"proto": "udp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Multicast DNS"
|
|
||||||
},
|
|
||||||
"MSNP": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "1863",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Microsoft Notification Protocol"
|
|
||||||
},
|
|
||||||
"MSSQL": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "1433",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Microsoft SQL Server"
|
|
||||||
},
|
|
||||||
"Mail": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "25",
|
|
||||||
"proto": "tcp"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"dport": "465",
|
|
||||||
"proto": "tcp"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"dport": "587",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Mail traffic (SMTP, SMTPS, Submission)"
|
|
||||||
},
|
|
||||||
"Munin": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "4949",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Munin networked resource monitoring traffic"
|
|
||||||
},
|
|
||||||
"MySQL": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "3306",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "MySQL server"
|
|
||||||
},
|
|
||||||
"NNTP": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "119",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "NNTP traffic (Usenet)."
|
|
||||||
},
|
|
||||||
"NNTPS": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "563",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Encrypted NNTP traffic (Usenet)"
|
|
||||||
},
|
|
||||||
"NTP": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "123",
|
|
||||||
"proto": "udp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Network Time Protocol (ntpd)"
|
|
||||||
},
|
|
||||||
"NeighborDiscovery": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "nd-router-solicit",
|
|
||||||
"proto": "icmpv6"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"dport": "nd-router-advert",
|
|
||||||
"proto": "icmpv6"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"dport": "nd-neighbor-solicit",
|
|
||||||
"proto": "icmpv6"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"dport": "nd-neighbor-advert",
|
|
||||||
"proto": "icmpv6"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "IPv6 neighbor solicitation, neighbor and router advertisement"
|
|
||||||
},
|
|
||||||
"OSPF": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"proto": "89"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "OSPF multicast traffic"
|
|
||||||
},
|
|
||||||
"OpenVPN": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "1194",
|
|
||||||
"proto": "udp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "OpenVPN traffic"
|
|
||||||
},
|
|
||||||
"PBS": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "8007",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Proxmox Backup Server"
|
|
||||||
},
|
|
||||||
"PCA": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "5632",
|
|
||||||
"proto": "udp"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"dport": "5631",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Symantec PCAnywere (tm)"
|
|
||||||
},
|
|
||||||
"PMG": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "8006",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Proxmox Mail Gateway web interface"
|
|
||||||
},
|
|
||||||
"POP3": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "110",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "POP3 traffic"
|
|
||||||
},
|
|
||||||
"POP3S": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "995",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Encrypted POP3 traffic"
|
|
||||||
},
|
|
||||||
"PPtP": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"proto": "47"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"dport": "1723",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Point-to-Point Tunneling Protocol"
|
|
||||||
},
|
|
||||||
"Ping": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "echo-request",
|
|
||||||
"proto": "icmp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "ICMP echo request"
|
|
||||||
},
|
|
||||||
"PostgreSQL": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "5432",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "PostgreSQL server"
|
|
||||||
},
|
|
||||||
"Printer": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "515",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Line Printer protocol printing"
|
|
||||||
},
|
|
||||||
"RDP": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "3389",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Microsoft Remote Desktop Protocol traffic"
|
|
||||||
},
|
|
||||||
"RIP": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "520",
|
|
||||||
"proto": "udp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Routing Information Protocol (bidirectional)"
|
|
||||||
},
|
|
||||||
"RNDC": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "953",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "BIND remote management protocol"
|
|
||||||
},
|
|
||||||
"Razor": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "2703",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Razor Antispam System"
|
|
||||||
},
|
|
||||||
"Rdate": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "37",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Remote time retrieval (rdate)"
|
|
||||||
},
|
|
||||||
"Rsync": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "873",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Rsync server"
|
|
||||||
},
|
|
||||||
"SANE": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "6566",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "SANE network scanning"
|
|
||||||
},
|
|
||||||
"SMB": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "135,445",
|
|
||||||
"proto": "udp"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"dport": "137:139",
|
|
||||||
"proto": "udp"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"dport": "1024:65535",
|
|
||||||
"proto": "udp",
|
|
||||||
"sport": "137"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"dport": "135,139,445",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Microsoft SMB traffic"
|
|
||||||
},
|
|
||||||
"SMBswat": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "901",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Samba Web Administration Tool"
|
|
||||||
},
|
|
||||||
"SMTP": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "25",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Simple Mail Transfer Protocol"
|
|
||||||
},
|
|
||||||
"SMTPS": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "465",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Encrypted Simple Mail Transfer Protocol"
|
|
||||||
},
|
|
||||||
"SNMP": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "161:162",
|
|
||||||
"proto": "udp"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"dport": "161",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Simple Network Management Protocol"
|
|
||||||
},
|
|
||||||
"SPAMD": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "783",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Spam Assassin SPAMD traffic"
|
|
||||||
},
|
|
||||||
"SPICEproxy": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "3128",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Proxmox VE SPICE display proxy traffic"
|
|
||||||
},
|
|
||||||
"SSH": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "22",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Secure shell traffic"
|
|
||||||
},
|
|
||||||
"SVN": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "3690",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Subversion server (svnserve)"
|
|
||||||
},
|
|
||||||
"SixXS": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "3874",
|
|
||||||
"proto": "tcp"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"dport": "3740",
|
|
||||||
"proto": "udp"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"proto": "41"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"dport": "5072,8374",
|
|
||||||
"proto": "udp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "SixXS IPv6 Deployment and Tunnel Broker"
|
|
||||||
},
|
|
||||||
"Squid": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "3128",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Squid web proxy traffic"
|
|
||||||
},
|
|
||||||
"Submission": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "587",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Mail message submission traffic"
|
|
||||||
},
|
|
||||||
"Syslog": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "514",
|
|
||||||
"proto": "udp"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"dport": "514",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Syslog protocol (RFC 5424) traffic"
|
|
||||||
},
|
|
||||||
"TFTP": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "69",
|
|
||||||
"proto": "udp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Trivial File Transfer Protocol traffic"
|
|
||||||
},
|
|
||||||
"Telnet": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "23",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Telnet traffic"
|
|
||||||
},
|
|
||||||
"Telnets": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "992",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Telnet over SSL"
|
|
||||||
},
|
|
||||||
"Time": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "37",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "RFC 868 Time protocol"
|
|
||||||
},
|
|
||||||
"Trcrt": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "33434:33524",
|
|
||||||
"proto": "udp"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"dport": "echo-request",
|
|
||||||
"proto": "icmp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Traceroute (for up to 30 hops) traffic"
|
|
||||||
},
|
|
||||||
"VNC": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "5900:5999",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "VNC traffic for VNC display's 0 - 99"
|
|
||||||
},
|
|
||||||
"VNCL": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "5500",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "VNC traffic from Vncservers to Vncviewers in listen mode"
|
|
||||||
},
|
|
||||||
"Web": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "80",
|
|
||||||
"proto": "tcp"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"dport": "443",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "WWW traffic (HTTP and HTTPS)"
|
|
||||||
},
|
|
||||||
"Webcache": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "8080",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Web Cache/Proxy traffic (port 8080)"
|
|
||||||
},
|
|
||||||
"Webmin": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "10000",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Webmin traffic"
|
|
||||||
},
|
|
||||||
"Whois": {
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"dport": "43",
|
|
||||||
"proto": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"desc": "Whois (nicname, RFC 3912) traffic"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,374 +0,0 @@
|
|||||||
use std::collections::BTreeMap;
|
|
||||||
use std::io;
|
|
||||||
|
|
||||||
use anyhow::Error;
|
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
use crate::firewall::common::ParserConfig;
|
|
||||||
use crate::firewall::types::ipset::{Ipset, IpsetScope};
|
|
||||||
use crate::firewall::types::log::LogRateLimit;
|
|
||||||
use crate::firewall::types::rule::{Direction, Verdict};
|
|
||||||
use crate::firewall::types::{Alias, Group, Rule};
|
|
||||||
|
|
||||||
use crate::firewall::parse::{serde_option_bool, serde_option_log_ratelimit};
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
pub struct Config {
|
|
||||||
pub(crate) config: super::common::Config<Options>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// default setting for [`Config::is_enabled()`]
|
|
||||||
pub const CLUSTER_ENABLED_DEFAULT: bool = false;
|
|
||||||
/// default setting for [`Config::ebtables()`]
|
|
||||||
pub const CLUSTER_EBTABLES_DEFAULT: bool = false;
|
|
||||||
/// default setting for [`Config::default_policy()`]
|
|
||||||
pub const CLUSTER_POLICY_IN_DEFAULT: Verdict = Verdict::Drop;
|
|
||||||
/// default setting for [`Config::default_policy()`]
|
|
||||||
pub const CLUSTER_POLICY_OUT_DEFAULT: Verdict = Verdict::Accept;
|
|
||||||
|
|
||||||
impl Config {
|
|
||||||
pub fn parse<R: io::BufRead>(input: R) -> Result<Self, Error> {
|
|
||||||
let parser_config = ParserConfig {
|
|
||||||
guest_iface_names: false,
|
|
||||||
ipset_scope: Some(IpsetScope::Datacenter),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
config: super::common::Config::parse(input, &parser_config)?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn rules(&self) -> &Vec<Rule> {
|
|
||||||
&self.config.rules
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn groups(&self) -> &BTreeMap<String, Group> {
|
|
||||||
&self.config.groups
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ipsets(&self) -> &BTreeMap<String, Ipset> {
|
|
||||||
&self.config.ipsets
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn alias(&self, name: &str) -> Option<&Alias> {
|
|
||||||
self.config.alias(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_enabled(&self) -> bool {
|
|
||||||
self.config
|
|
||||||
.options
|
|
||||||
.enable
|
|
||||||
.unwrap_or(CLUSTER_ENABLED_DEFAULT)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// returns the ebtables option from the cluster config or [`CLUSTER_EBTABLES_DEFAULT`] if
|
|
||||||
/// unset
|
|
||||||
///
|
|
||||||
/// this setting is leftover from the old firewall, but has no effect on the nftables firewall
|
|
||||||
pub fn ebtables(&self) -> bool {
|
|
||||||
self.config
|
|
||||||
.options
|
|
||||||
.ebtables
|
|
||||||
.unwrap_or(CLUSTER_EBTABLES_DEFAULT)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// returns policy_in / out or [`CLUSTER_POLICY_IN_DEFAULT`] / [`CLUSTER_POLICY_OUT_DEFAULT`] if
|
|
||||||
/// unset
|
|
||||||
pub fn default_policy(&self, dir: Direction) -> Verdict {
|
|
||||||
match dir {
|
|
||||||
Direction::In => self
|
|
||||||
.config
|
|
||||||
.options
|
|
||||||
.policy_in
|
|
||||||
.unwrap_or(CLUSTER_POLICY_IN_DEFAULT),
|
|
||||||
Direction::Out => self
|
|
||||||
.config
|
|
||||||
.options
|
|
||||||
.policy_out
|
|
||||||
.unwrap_or(CLUSTER_POLICY_OUT_DEFAULT),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// returns the rate_limit for logs or [`None`] if rate limiting is disabled
|
|
||||||
///
|
|
||||||
/// If there is no rate limit set, then [`LogRateLimit::default`] is used
|
|
||||||
pub fn log_ratelimit(&self) -> Option<LogRateLimit> {
|
|
||||||
let rate_limit = self
|
|
||||||
.config
|
|
||||||
.options
|
|
||||||
.log_ratelimit
|
|
||||||
.clone()
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
match rate_limit.enabled() {
|
|
||||||
true => Some(rate_limit),
|
|
||||||
false => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
pub struct Options {
|
|
||||||
#[serde(default, with = "serde_option_bool")]
|
|
||||||
enable: Option<bool>,
|
|
||||||
|
|
||||||
#[serde(default, with = "serde_option_bool")]
|
|
||||||
ebtables: Option<bool>,
|
|
||||||
|
|
||||||
#[serde(default, with = "serde_option_log_ratelimit")]
|
|
||||||
log_ratelimit: Option<LogRateLimit>,
|
|
||||||
|
|
||||||
policy_in: Option<Verdict>,
|
|
||||||
policy_out: Option<Verdict>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use crate::firewall::types::{
|
|
||||||
address::IpList,
|
|
||||||
alias::{AliasName, AliasScope},
|
|
||||||
ipset::{IpsetAddress, IpsetEntry},
|
|
||||||
log::{LogLevel, LogRateLimitTimescale},
|
|
||||||
rule::{Kind, RuleGroup},
|
|
||||||
rule_match::{
|
|
||||||
Icmpv6, Icmpv6Code, IpAddrMatch, IpMatch, Ports, Protocol, RuleMatch, Tcp, Udp,
|
|
||||||
},
|
|
||||||
Cidr,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_config() {
|
|
||||||
const CONFIG: &str = r#"
|
|
||||||
[OPTIONS]
|
|
||||||
enable: 1
|
|
||||||
log_ratelimit: 1,rate=10/second,burst=20
|
|
||||||
ebtables: 0
|
|
||||||
policy_in: REJECT
|
|
||||||
policy_out: REJECT
|
|
||||||
|
|
||||||
[ALIASES]
|
|
||||||
|
|
||||||
another 8.8.8.18
|
|
||||||
analias 7.7.0.0/16 # much
|
|
||||||
wide cccc::/64
|
|
||||||
|
|
||||||
[IPSET a-set]
|
|
||||||
|
|
||||||
!5.5.5.5
|
|
||||||
1.2.3.4/30
|
|
||||||
dc/analias # a comment
|
|
||||||
dc/wide
|
|
||||||
dddd::/96
|
|
||||||
|
|
||||||
[RULES]
|
|
||||||
|
|
||||||
GROUP tgr -i eth0 # acomm
|
|
||||||
IN ACCEPT -p udp -dport 33 -sport 22 -log warning
|
|
||||||
|
|
||||||
[group tgr] # comment for tgr
|
|
||||||
|
|
||||||
|OUT ACCEPT -source fe80::1/48 -dest dddd:3:3::9/64 -p icmpv6 -log nolog -icmp-type port-unreachable
|
|
||||||
OUT ACCEPT -p tcp -sport 33 -log nolog
|
|
||||||
IN BGP(REJECT) -log crit -source 1.2.3.4
|
|
||||||
"#;
|
|
||||||
|
|
||||||
let mut config = CONFIG.as_bytes();
|
|
||||||
let config = Config::parse(&mut config).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
config.config.options,
|
|
||||||
Options {
|
|
||||||
ebtables: Some(false),
|
|
||||||
enable: Some(true),
|
|
||||||
log_ratelimit: Some(LogRateLimit::new(
|
|
||||||
true,
|
|
||||||
10,
|
|
||||||
LogRateLimitTimescale::Second,
|
|
||||||
20
|
|
||||||
)),
|
|
||||||
policy_in: Some(Verdict::Reject),
|
|
||||||
policy_out: Some(Verdict::Reject),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(config.config.aliases.len(), 3);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
config.config.aliases["another"],
|
|
||||||
Alias::new("another", Cidr::new_v4([8, 8, 8, 18], 32).unwrap(), None),
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
config.config.aliases["analias"],
|
|
||||||
Alias::new(
|
|
||||||
"analias",
|
|
||||||
Cidr::new_v4([7, 7, 0, 0], 16).unwrap(),
|
|
||||||
"much".to_string()
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
config.config.aliases["wide"],
|
|
||||||
Alias::new(
|
|
||||||
"wide",
|
|
||||||
Cidr::new_v6(
|
|
||||||
[0xCCCC, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x000],
|
|
||||||
64
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
None
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(config.config.ipsets.len(), 1);
|
|
||||||
|
|
||||||
let mut ipset_elements = vec![
|
|
||||||
IpsetEntry {
|
|
||||||
nomatch: true,
|
|
||||||
address: Cidr::new_v4([5, 5, 5, 5], 32).unwrap().into(),
|
|
||||||
comment: None,
|
|
||||||
},
|
|
||||||
IpsetEntry {
|
|
||||||
nomatch: false,
|
|
||||||
address: Cidr::new_v4([1, 2, 3, 4], 30).unwrap().into(),
|
|
||||||
comment: None,
|
|
||||||
},
|
|
||||||
IpsetEntry {
|
|
||||||
nomatch: false,
|
|
||||||
address: IpsetAddress::Alias(AliasName::new(AliasScope::Datacenter, "analias")),
|
|
||||||
comment: Some("a comment".to_string()),
|
|
||||||
},
|
|
||||||
IpsetEntry {
|
|
||||||
nomatch: false,
|
|
||||||
address: IpsetAddress::Alias(AliasName::new(AliasScope::Datacenter, "wide")),
|
|
||||||
comment: None,
|
|
||||||
},
|
|
||||||
IpsetEntry {
|
|
||||||
nomatch: false,
|
|
||||||
address: Cidr::new_v6([0xdd, 0xdd, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 96)
|
|
||||||
.unwrap()
|
|
||||||
.into(),
|
|
||||||
comment: None,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
let mut ipset = Ipset::from_parts(IpsetScope::Datacenter, "a-set");
|
|
||||||
ipset.append(&mut ipset_elements);
|
|
||||||
|
|
||||||
assert_eq!(config.config.ipsets["a-set"], ipset,);
|
|
||||||
|
|
||||||
assert_eq!(config.config.rules.len(), 2);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
config.config.rules[0],
|
|
||||||
Rule {
|
|
||||||
disabled: false,
|
|
||||||
comment: Some("acomm".to_string()),
|
|
||||||
kind: Kind::Group(RuleGroup {
|
|
||||||
group: "tgr".to_string(),
|
|
||||||
iface: Some("eth0".to_string()),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
config.config.rules[1],
|
|
||||||
Rule {
|
|
||||||
disabled: false,
|
|
||||||
comment: None,
|
|
||||||
kind: Kind::Match(RuleMatch {
|
|
||||||
dir: Direction::In,
|
|
||||||
verdict: Verdict::Accept,
|
|
||||||
proto: Some(Protocol::Udp(Udp::new(Ports::from_u16(22, 33)))),
|
|
||||||
log: Some(LogLevel::Warning),
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(config.config.groups.len(), 1);
|
|
||||||
|
|
||||||
let entry = &config.config.groups["tgr"];
|
|
||||||
assert_eq!(entry.comment(), Some("comment for tgr"));
|
|
||||||
assert_eq!(entry.rules().len(), 3);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
entry.rules()[0],
|
|
||||||
Rule {
|
|
||||||
disabled: true,
|
|
||||||
comment: None,
|
|
||||||
kind: Kind::Match(RuleMatch {
|
|
||||||
dir: Direction::Out,
|
|
||||||
verdict: Verdict::Accept,
|
|
||||||
ip: Some(IpMatch {
|
|
||||||
src: Some(IpAddrMatch::Ip(IpList::from(
|
|
||||||
Cidr::new_v6(
|
|
||||||
[0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
|
|
||||||
48
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
))),
|
|
||||||
dst: Some(IpAddrMatch::Ip(IpList::from(
|
|
||||||
Cidr::new_v6(
|
|
||||||
[0xdd, 0xdd, 0, 3, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9],
|
|
||||||
64
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
))),
|
|
||||||
}),
|
|
||||||
proto: Some(Protocol::Icmpv6(Icmpv6::new_code(Icmpv6Code::Named(
|
|
||||||
"port-unreachable"
|
|
||||||
)))),
|
|
||||||
log: Some(LogLevel::Nolog),
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
entry.rules()[1],
|
|
||||||
Rule {
|
|
||||||
disabled: false,
|
|
||||||
comment: None,
|
|
||||||
kind: Kind::Match(RuleMatch {
|
|
||||||
dir: Direction::Out,
|
|
||||||
verdict: Verdict::Accept,
|
|
||||||
proto: Some(Protocol::Tcp(Tcp::new(Ports::from_u16(33, None)))),
|
|
||||||
log: Some(LogLevel::Nolog),
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
entry.rules()[2],
|
|
||||||
Rule {
|
|
||||||
disabled: false,
|
|
||||||
comment: None,
|
|
||||||
kind: Kind::Match(RuleMatch {
|
|
||||||
dir: Direction::In,
|
|
||||||
verdict: Verdict::Reject,
|
|
||||||
log: Some(LogLevel::Critical),
|
|
||||||
fw_macro: Some("BGP".to_string()),
|
|
||||||
ip: Some(IpMatch {
|
|
||||||
src: Some(IpAddrMatch::Ip(IpList::from(
|
|
||||||
Cidr::new_v4([1, 2, 3, 4], 32).unwrap()
|
|
||||||
))),
|
|
||||||
dst: None,
|
|
||||||
}),
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
let empty_config = Config::parse("".as_bytes()).expect("empty config is invalid");
|
|
||||||
|
|
||||||
assert_eq!(empty_config.config.options, Options::default());
|
|
||||||
assert!(empty_config.config.rules.is_empty());
|
|
||||||
assert!(empty_config.config.aliases.is_empty());
|
|
||||||
assert!(empty_config.config.ipsets.is_empty());
|
|
||||||
assert!(empty_config.config.groups.is_empty());
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,184 +0,0 @@
|
|||||||
use std::collections::{BTreeMap, HashMap};
|
|
||||||
use std::io;
|
|
||||||
|
|
||||||
use anyhow::{bail, format_err, Error};
|
|
||||||
use serde::de::IntoDeserializer;
|
|
||||||
|
|
||||||
use crate::firewall::parse::{parse_named_section_tail, split_key_value, SomeString};
|
|
||||||
use crate::firewall::types::ipset::{IpsetName, IpsetScope};
|
|
||||||
use crate::firewall::types::{Alias, Group, Ipset, Rule};
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
pub struct Config<O>
|
|
||||||
where
|
|
||||||
O: Default + std::fmt::Debug + serde::de::DeserializeOwned,
|
|
||||||
{
|
|
||||||
pub(crate) options: O,
|
|
||||||
pub(crate) rules: Vec<Rule>,
|
|
||||||
pub(crate) aliases: BTreeMap<String, Alias>,
|
|
||||||
pub(crate) ipsets: BTreeMap<String, Ipset>,
|
|
||||||
pub(crate) groups: BTreeMap<String, Group>,
|
|
||||||
}
|
|
||||||
|
|
||||||
enum Sec {
|
|
||||||
None,
|
|
||||||
Options,
|
|
||||||
Aliases,
|
|
||||||
Rules,
|
|
||||||
Ipset(String, Ipset),
|
|
||||||
Group(String, Group),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct ParserConfig {
|
|
||||||
/// Network interfaces must be of the form `netX`.
|
|
||||||
pub guest_iface_names: bool,
|
|
||||||
pub ipset_scope: Option<IpsetScope>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<O> Config<O>
|
|
||||||
where
|
|
||||||
O: Default + std::fmt::Debug + serde::de::DeserializeOwned,
|
|
||||||
{
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse<R: io::BufRead>(input: R, parser_cfg: &ParserConfig) -> Result<Self, Error> {
|
|
||||||
let mut section = Sec::None;
|
|
||||||
|
|
||||||
let mut this = Self::new();
|
|
||||||
let mut options = HashMap::new();
|
|
||||||
|
|
||||||
for line in input.lines() {
|
|
||||||
let line = line?;
|
|
||||||
let line = line.trim();
|
|
||||||
|
|
||||||
if line.is_empty() || line.starts_with('#') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
log::trace!("parsing config line {line}");
|
|
||||||
|
|
||||||
if line.eq_ignore_ascii_case("[OPTIONS]") {
|
|
||||||
this.set_section(&mut section, Sec::Options)?;
|
|
||||||
} else if line.eq_ignore_ascii_case("[ALIASES]") {
|
|
||||||
this.set_section(&mut section, Sec::Aliases)?;
|
|
||||||
} else if line.eq_ignore_ascii_case("[RULES]") {
|
|
||||||
this.set_section(&mut section, Sec::Rules)?;
|
|
||||||
} else if let Some(line) = line.strip_prefix("[IPSET") {
|
|
||||||
let (name, comment) = parse_named_section_tail("ipset", line)?;
|
|
||||||
|
|
||||||
let scope = parser_cfg.ipset_scope.ok_or_else(|| {
|
|
||||||
format_err!("IPSET in config, but no scope set in parser config")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let ipset_name = IpsetName::new(scope, name.to_string());
|
|
||||||
let mut ipset = Ipset::new(ipset_name);
|
|
||||||
ipset.comment = comment.map(str::to_owned);
|
|
||||||
|
|
||||||
this.set_section(&mut section, Sec::Ipset(name.to_string(), ipset))?;
|
|
||||||
} else if let Some(line) = line.strip_prefix("[group") {
|
|
||||||
let (name, comment) = parse_named_section_tail("group", line)?;
|
|
||||||
let mut group = Group::new();
|
|
||||||
|
|
||||||
group.set_comment(comment.map(str::to_owned));
|
|
||||||
|
|
||||||
this.set_section(&mut section, Sec::Group(name.to_owned(), group))?;
|
|
||||||
} else if line.starts_with('[') {
|
|
||||||
bail!("invalid section {line:?}");
|
|
||||||
} else {
|
|
||||||
match &mut section {
|
|
||||||
Sec::None => bail!("config line with no section: {line:?}"),
|
|
||||||
Sec::Options => Self::parse_option(line, &mut options)?,
|
|
||||||
Sec::Aliases => this.parse_alias(line)?,
|
|
||||||
Sec::Rules => this.parse_rule(line, parser_cfg)?,
|
|
||||||
Sec::Ipset(_name, ipset) => ipset.parse_entry(line)?,
|
|
||||||
Sec::Group(_name, group) => group.parse_entry(line)?,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.set_section(&mut section, Sec::None)?;
|
|
||||||
|
|
||||||
this.options = O::deserialize(IntoDeserializer::<
|
|
||||||
'_,
|
|
||||||
crate::firewall::parse::SerdeStringError,
|
|
||||||
>::into_deserializer(options))?;
|
|
||||||
|
|
||||||
Ok(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_option(line: &str, options: &mut HashMap<String, SomeString>) -> Result<(), Error> {
|
|
||||||
let (key, value) = split_key_value(line)
|
|
||||||
.ok_or_else(|| format_err!("expected colon separated key and value, found {line:?}"))?;
|
|
||||||
|
|
||||||
if options.insert(key.to_string(), value.into()).is_some() {
|
|
||||||
bail!("duplicate option {key:?}");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_alias(&mut self, line: &str) -> Result<(), Error> {
|
|
||||||
let alias: Alias = line.parse()?;
|
|
||||||
|
|
||||||
if self
|
|
||||||
.aliases
|
|
||||||
.insert(alias.name().to_string(), alias)
|
|
||||||
.is_some()
|
|
||||||
{
|
|
||||||
bail!("duplicate alias: {line}");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_rule(&mut self, line: &str, parser_cfg: &ParserConfig) -> Result<(), Error> {
|
|
||||||
let rule: Rule = line.parse()?;
|
|
||||||
|
|
||||||
if parser_cfg.guest_iface_names {
|
|
||||||
if let Some(iface) = rule.iface() {
|
|
||||||
let _ = iface
|
|
||||||
.strip_prefix("net")
|
|
||||||
.ok_or_else(|| {
|
|
||||||
format_err!("interface name must be of the form \"net<number>\"")
|
|
||||||
})?
|
|
||||||
.parse::<u16>()
|
|
||||||
.map_err(|_| {
|
|
||||||
format_err!("interface name must be of the form \"net<number>\"")
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.rules.push(rule);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_section(&mut self, sec: &mut Sec, to: Sec) -> Result<(), Error> {
|
|
||||||
let prev = std::mem::replace(sec, to);
|
|
||||||
|
|
||||||
match prev {
|
|
||||||
Sec::Ipset(name, ipset) => {
|
|
||||||
if self.ipsets.insert(name.clone(), ipset).is_some() {
|
|
||||||
bail!("duplicate ipset: {name:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Sec::Group(name, group) => {
|
|
||||||
if self.groups.insert(name.clone(), group).is_some() {
|
|
||||||
bail!("duplicate group: {name:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ipsets(&self) -> &BTreeMap<String, Ipset> {
|
|
||||||
&self.ipsets
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn alias(&self, name: &str) -> Option<&Alias> {
|
|
||||||
self.aliases.get(name)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,115 +0,0 @@
|
|||||||
use anyhow::{bail, Error};
|
|
||||||
use serde::Deserialize;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::sync::OnceLock;
|
|
||||||
|
|
||||||
use crate::firewall::types::address::Family;
|
|
||||||
use crate::firewall::types::rule_match::{Ports, Protocol, Tcp, Udp};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
|
||||||
pub struct CtHelperMacroJson {
|
|
||||||
pub v4: Option<bool>,
|
|
||||||
pub v6: Option<bool>,
|
|
||||||
pub name: String,
|
|
||||||
pub tcp: Option<u16>,
|
|
||||||
pub udp: Option<u16>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<CtHelperMacroJson> for CtHelperMacro {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(value: CtHelperMacroJson) -> Result<Self, Self::Error> {
|
|
||||||
if value.tcp.is_none() && value.udp.is_none() {
|
|
||||||
bail!("Neither TCP nor UDP port set in CT helper!");
|
|
||||||
}
|
|
||||||
|
|
||||||
let family = match (value.v4, value.v6) {
|
|
||||||
(Some(true), Some(true)) => None,
|
|
||||||
(Some(true), _) => Some(Family::V4),
|
|
||||||
(_, Some(true)) => Some(Family::V6),
|
|
||||||
_ => bail!("Neither v4 nor v6 set in CT Helper Macro!"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut ct_helper = CtHelperMacro {
|
|
||||||
family,
|
|
||||||
name: value.name,
|
|
||||||
tcp: None,
|
|
||||||
udp: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(dport) = value.tcp {
|
|
||||||
let ports = Ports::from_u16(None, dport);
|
|
||||||
ct_helper.tcp = Some(Tcp::new(ports).into());
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(dport) = value.udp {
|
|
||||||
let ports = Ports::from_u16(None, dport);
|
|
||||||
ct_helper.udp = Some(Udp::new(ports).into());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(ct_helper)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
|
||||||
#[serde(try_from = "CtHelperMacroJson")]
|
|
||||||
pub struct CtHelperMacro {
|
|
||||||
family: Option<Family>,
|
|
||||||
name: String,
|
|
||||||
tcp: Option<Protocol>,
|
|
||||||
udp: Option<Protocol>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CtHelperMacro {
|
|
||||||
fn helper_name(&self, protocol: &str) -> String {
|
|
||||||
format!("helper-{}-{protocol}", self.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tcp_helper_name(&self) -> String {
|
|
||||||
self.helper_name("tcp")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn udp_helper_name(&self) -> String {
|
|
||||||
self.helper_name("udp")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn family(&self) -> Option<Family> {
|
|
||||||
self.family
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn name(&self) -> &str {
|
|
||||||
self.name.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tcp(&self) -> Option<&Protocol> {
|
|
||||||
self.tcp.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn udp(&self) -> Option<&Protocol> {
|
|
||||||
self.udp.as_ref()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn hashmap() -> &'static HashMap<String, CtHelperMacro> {
|
|
||||||
const MACROS: &str = include_str!("../../resources/ct_helper.json");
|
|
||||||
static HASHMAP: OnceLock<HashMap<String, CtHelperMacro>> = OnceLock::new();
|
|
||||||
|
|
||||||
HASHMAP.get_or_init(|| {
|
|
||||||
let macro_data: Vec<CtHelperMacro> = match serde_json::from_str(MACROS) {
|
|
||||||
Ok(data) => data,
|
|
||||||
Err(err) => {
|
|
||||||
log::error!("could not load data for ct helpers: {err}");
|
|
||||||
Vec::new()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
macro_data
|
|
||||||
.into_iter()
|
|
||||||
.map(|elem| (elem.name.clone(), elem))
|
|
||||||
.collect()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_cthelper(name: &str) -> Option<&'static CtHelperMacro> {
|
|
||||||
hashmap().get(name)
|
|
||||||
}
|
|
@ -1,69 +0,0 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use serde::Deserialize;
|
|
||||||
use std::sync::OnceLock;
|
|
||||||
|
|
||||||
use crate::firewall::types::rule_match::Protocol;
|
|
||||||
|
|
||||||
use super::types::rule_match::RuleOptions;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Deserialize)]
|
|
||||||
struct FwMacroData {
|
|
||||||
#[serde(rename = "desc")]
|
|
||||||
pub description: &'static str,
|
|
||||||
pub code: Vec<RuleOptions>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
|
||||||
pub struct FwMacro {
|
|
||||||
pub _description: &'static str,
|
|
||||||
pub code: Vec<Protocol>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn macros() -> &'static HashMap<String, FwMacro> {
|
|
||||||
const MACROS: &str = include_str!("../../resources/macros.json");
|
|
||||||
static HASHMAP: OnceLock<HashMap<String, FwMacro>> = OnceLock::new();
|
|
||||||
|
|
||||||
HASHMAP.get_or_init(|| {
|
|
||||||
let macro_data: HashMap<String, FwMacroData> = match serde_json::from_str(MACROS) {
|
|
||||||
Ok(m) => m,
|
|
||||||
Err(err) => {
|
|
||||||
log::error!("could not load data for macros: {err}");
|
|
||||||
HashMap::new()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut macros = HashMap::new();
|
|
||||||
|
|
||||||
'outer: for (name, data) in macro_data {
|
|
||||||
let mut code = Vec::new();
|
|
||||||
|
|
||||||
for c in data.code {
|
|
||||||
match Protocol::from_options(&c) {
|
|
||||||
Ok(Some(p)) => code.push(p),
|
|
||||||
Ok(None) => {
|
|
||||||
continue 'outer;
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
log::error!("could not parse data for macro {name}: {err}");
|
|
||||||
continue 'outer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
macros.insert(
|
|
||||||
name,
|
|
||||||
FwMacro {
|
|
||||||
_description: data.description,
|
|
||||||
code,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
macros
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_macro(name: &str) -> Option<&'static FwMacro> {
|
|
||||||
macros().get(name)
|
|
||||||
}
|
|
@ -1,237 +0,0 @@
|
|||||||
use std::collections::BTreeMap;
|
|
||||||
use std::io;
|
|
||||||
|
|
||||||
use crate::guest::types::Vmid;
|
|
||||||
use crate::guest::vm::NetworkConfig;
|
|
||||||
|
|
||||||
use crate::firewall::types::alias::{Alias, AliasName};
|
|
||||||
use crate::firewall::types::ipset::IpsetScope;
|
|
||||||
use crate::firewall::types::log::LogLevel;
|
|
||||||
use crate::firewall::types::rule::{Direction, Rule, Verdict};
|
|
||||||
use crate::firewall::types::Ipset;
|
|
||||||
|
|
||||||
use anyhow::{bail, Error};
|
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
use crate::firewall::parse::serde_option_bool;
|
|
||||||
|
|
||||||
/// default return value for [`Config::is_enabled()`]
|
|
||||||
pub const GUEST_ENABLED_DEFAULT: bool = false;
|
|
||||||
/// default return value for [`Config::allow_ndp()`]
|
|
||||||
pub const GUEST_ALLOW_NDP_DEFAULT: bool = true;
|
|
||||||
/// default return value for [`Config::allow_dhcp()`]
|
|
||||||
pub const GUEST_ALLOW_DHCP_DEFAULT: bool = true;
|
|
||||||
/// default return value for [`Config::allow_ra()`]
|
|
||||||
pub const GUEST_ALLOW_RA_DEFAULT: bool = false;
|
|
||||||
/// default return value for [`Config::macfilter()`]
|
|
||||||
pub const GUEST_MACFILTER_DEFAULT: bool = true;
|
|
||||||
/// default return value for [`Config::ipfilter()`]
|
|
||||||
pub const GUEST_IPFILTER_DEFAULT: bool = false;
|
|
||||||
/// default return value for [`Config::default_policy()`]
|
|
||||||
pub const GUEST_POLICY_IN_DEFAULT: Verdict = Verdict::Drop;
|
|
||||||
/// default return value for [`Config::default_policy()`]
|
|
||||||
pub const GUEST_POLICY_OUT_DEFAULT: Verdict = Verdict::Accept;
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
pub struct Options {
|
|
||||||
#[serde(default, with = "serde_option_bool")]
|
|
||||||
dhcp: Option<bool>,
|
|
||||||
|
|
||||||
#[serde(default, with = "serde_option_bool")]
|
|
||||||
enable: Option<bool>,
|
|
||||||
|
|
||||||
#[serde(default, with = "serde_option_bool")]
|
|
||||||
ipfilter: Option<bool>,
|
|
||||||
|
|
||||||
#[serde(default, with = "serde_option_bool")]
|
|
||||||
ndp: Option<bool>,
|
|
||||||
|
|
||||||
#[serde(default, with = "serde_option_bool")]
|
|
||||||
radv: Option<bool>,
|
|
||||||
|
|
||||||
log_level_in: Option<LogLevel>,
|
|
||||||
log_level_out: Option<LogLevel>,
|
|
||||||
|
|
||||||
#[serde(default, with = "serde_option_bool")]
|
|
||||||
macfilter: Option<bool>,
|
|
||||||
|
|
||||||
#[serde(rename = "policy_in")]
|
|
||||||
policy_in: Option<Verdict>,
|
|
||||||
|
|
||||||
#[serde(rename = "policy_out")]
|
|
||||||
policy_out: Option<Verdict>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Config {
|
|
||||||
vmid: Vmid,
|
|
||||||
|
|
||||||
/// The interface prefix: "veth" for containers, "tap" for VMs.
|
|
||||||
iface_prefix: &'static str,
|
|
||||||
|
|
||||||
network_config: NetworkConfig,
|
|
||||||
config: super::common::Config<Options>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Config {
|
|
||||||
pub fn parse<T: io::BufRead, U: io::BufRead>(
|
|
||||||
vmid: &Vmid,
|
|
||||||
iface_prefix: &'static str,
|
|
||||||
firewall_input: T,
|
|
||||||
network_input: U,
|
|
||||||
) -> Result<Self, Error> {
|
|
||||||
let parser_cfg = super::common::ParserConfig {
|
|
||||||
guest_iface_names: true,
|
|
||||||
ipset_scope: Some(IpsetScope::Guest),
|
|
||||||
};
|
|
||||||
|
|
||||||
let config = super::common::Config::parse(firewall_input, &parser_cfg)?;
|
|
||||||
if !config.groups.is_empty() {
|
|
||||||
bail!("guest firewall config cannot declare groups");
|
|
||||||
}
|
|
||||||
|
|
||||||
let network_config = NetworkConfig::parse(network_input)?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
vmid: *vmid,
|
|
||||||
iface_prefix,
|
|
||||||
config,
|
|
||||||
network_config,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn vmid(&self) -> Vmid {
|
|
||||||
self.vmid
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn alias(&self, name: &AliasName) -> Option<&Alias> {
|
|
||||||
self.config.alias(name.name())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn iface_name_by_key(&self, key: &str) -> Result<String, Error> {
|
|
||||||
let index = NetworkConfig::index_from_net_key(key)?;
|
|
||||||
Ok(format!("{}{}i{index}", self.iface_prefix, self.vmid))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn iface_name_by_index(&self, index: i64) -> String {
|
|
||||||
format!("{}{}i{index}", self.iface_prefix, self.vmid)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// returns the value of the enabled config key or [`GUEST_ENABLED_DEFAULT`] if unset
|
|
||||||
pub fn is_enabled(&self) -> bool {
|
|
||||||
self.config.options.enable.unwrap_or(GUEST_ENABLED_DEFAULT)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn rules(&self) -> &[Rule] {
|
|
||||||
&self.config.rules
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn log_level(&self, dir: Direction) -> LogLevel {
|
|
||||||
match dir {
|
|
||||||
Direction::In => self.config.options.log_level_in.unwrap_or_default(),
|
|
||||||
Direction::Out => self.config.options.log_level_out.unwrap_or_default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// returns the value of the ndp config key or [`GUEST_ALLOW_NDP_DEFAULT`] if unset
|
|
||||||
pub fn allow_ndp(&self) -> bool {
|
|
||||||
self.config.options.ndp.unwrap_or(GUEST_ALLOW_NDP_DEFAULT)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// returns the value of the dhcp config key or [`GUEST_ALLOW_DHCP_DEFAULT`] if unset
|
|
||||||
pub fn allow_dhcp(&self) -> bool {
|
|
||||||
self.config.options.dhcp.unwrap_or(GUEST_ALLOW_DHCP_DEFAULT)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// returns the value of the radv config key or [`GUEST_ALLOW_RA_DEFAULT`] if unset
|
|
||||||
pub fn allow_ra(&self) -> bool {
|
|
||||||
self.config.options.radv.unwrap_or(GUEST_ALLOW_RA_DEFAULT)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// returns the value of the macfilter config key or [`GUEST_MACFILTER_DEFAULT`] if unset
|
|
||||||
pub fn macfilter(&self) -> bool {
|
|
||||||
self.config
|
|
||||||
.options
|
|
||||||
.macfilter
|
|
||||||
.unwrap_or(GUEST_MACFILTER_DEFAULT)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// returns the value of the ipfilter config key or [`GUEST_IPFILTER_DEFAULT`] if unset
|
|
||||||
pub fn ipfilter(&self) -> bool {
|
|
||||||
self.config
|
|
||||||
.options
|
|
||||||
.ipfilter
|
|
||||||
.unwrap_or(GUEST_IPFILTER_DEFAULT)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// returns the value of the policy_in/out config key or
|
|
||||||
/// [`GUEST_POLICY_IN_DEFAULT`] / [`GUEST_POLICY_OUT_DEFAULT`] if unset
|
|
||||||
pub fn default_policy(&self, dir: Direction) -> Verdict {
|
|
||||||
match dir {
|
|
||||||
Direction::In => self
|
|
||||||
.config
|
|
||||||
.options
|
|
||||||
.policy_in
|
|
||||||
.unwrap_or(GUEST_POLICY_IN_DEFAULT),
|
|
||||||
Direction::Out => self
|
|
||||||
.config
|
|
||||||
.options
|
|
||||||
.policy_out
|
|
||||||
.unwrap_or(GUEST_POLICY_OUT_DEFAULT),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn network_config(&self) -> &NetworkConfig {
|
|
||||||
&self.network_config
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ipsets(&self) -> &BTreeMap<String, Ipset> {
|
|
||||||
self.config.ipsets()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_config() {
|
|
||||||
// most of the stuff is already tested in cluster parsing, only testing
|
|
||||||
// guest specific options here
|
|
||||||
const CONFIG: &str = r#"
|
|
||||||
[OPTIONS]
|
|
||||||
enable: 1
|
|
||||||
dhcp: 1
|
|
||||||
ipfilter: 0
|
|
||||||
log_level_in: emerg
|
|
||||||
log_level_out: crit
|
|
||||||
macfilter: 0
|
|
||||||
ndp:1
|
|
||||||
radv:1
|
|
||||||
policy_in: REJECT
|
|
||||||
policy_out: REJECT
|
|
||||||
"#;
|
|
||||||
|
|
||||||
let config = CONFIG.as_bytes();
|
|
||||||
let network_config: Vec<u8> = Vec::new();
|
|
||||||
let config =
|
|
||||||
Config::parse(&Vmid::new(100), "tap", config, network_config.as_slice()).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
config.config.options,
|
|
||||||
Options {
|
|
||||||
dhcp: Some(true),
|
|
||||||
enable: Some(true),
|
|
||||||
ipfilter: Some(false),
|
|
||||||
ndp: Some(true),
|
|
||||||
radv: Some(true),
|
|
||||||
log_level_in: Some(LogLevel::Emergency),
|
|
||||||
log_level_out: Some(LogLevel::Critical),
|
|
||||||
macfilter: Some(false),
|
|
||||||
policy_in: Some(Verdict::Reject),
|
|
||||||
policy_out: Some(Verdict::Reject),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,372 +0,0 @@
|
|||||||
use std::io;
|
|
||||||
use std::net::IpAddr;
|
|
||||||
|
|
||||||
use anyhow::{bail, Error};
|
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
use crate::host::utils::{host_ips, network_interface_cidrs};
|
|
||||||
use proxmox_sys::nodename;
|
|
||||||
|
|
||||||
use crate::firewall::parse;
|
|
||||||
use crate::firewall::types::log::LogLevel;
|
|
||||||
use crate::firewall::types::rule::Direction;
|
|
||||||
use crate::firewall::types::{Alias, Cidr, Rule};
|
|
||||||
|
|
||||||
/// default setting for the enabled key
|
|
||||||
pub const HOST_ENABLED_DEFAULT: bool = true;
|
|
||||||
/// default setting for the nftables key
|
|
||||||
pub const HOST_NFTABLES_DEFAULT: bool = false;
|
|
||||||
/// default return value for [`Config::allow_ndp()`]
|
|
||||||
pub const HOST_ALLOW_NDP_DEFAULT: bool = true;
|
|
||||||
/// default return value for [`Config::block_smurfs()`]
|
|
||||||
pub const HOST_BLOCK_SMURFS_DEFAULT: bool = true;
|
|
||||||
/// default return value for [`Config::block_synflood()`]
|
|
||||||
pub const HOST_BLOCK_SYNFLOOD_DEFAULT: bool = false;
|
|
||||||
/// default rate limit for synflood rule (packets / second)
|
|
||||||
pub const HOST_BLOCK_SYNFLOOD_RATE_DEFAULT: i64 = 200;
|
|
||||||
/// default rate limit for synflood rule (packets / second)
|
|
||||||
pub const HOST_BLOCK_SYNFLOOD_BURST_DEFAULT: i64 = 1000;
|
|
||||||
/// default return value for [`Config::block_invalid_tcp()`]
|
|
||||||
pub const HOST_BLOCK_INVALID_TCP_DEFAULT: bool = false;
|
|
||||||
/// default return value for [`Config::block_invalid_conntrack()`]
|
|
||||||
pub const HOST_BLOCK_INVALID_CONNTRACK: bool = false;
|
|
||||||
/// default setting for logging of invalid conntrack entries
|
|
||||||
pub const HOST_LOG_INVALID_CONNTRACK: bool = false;
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
pub struct Options {
|
|
||||||
#[serde(default, with = "parse::serde_option_bool")]
|
|
||||||
enable: Option<bool>,
|
|
||||||
|
|
||||||
#[serde(default, with = "parse::serde_option_bool")]
|
|
||||||
nftables: Option<bool>,
|
|
||||||
|
|
||||||
log_level_in: Option<LogLevel>,
|
|
||||||
log_level_out: Option<LogLevel>,
|
|
||||||
|
|
||||||
#[serde(default, with = "parse::serde_option_bool")]
|
|
||||||
log_nf_conntrack: Option<bool>,
|
|
||||||
#[serde(default, with = "parse::serde_option_bool")]
|
|
||||||
ndp: Option<bool>,
|
|
||||||
|
|
||||||
#[serde(default, with = "parse::serde_option_bool")]
|
|
||||||
nf_conntrack_allow_invalid: Option<bool>,
|
|
||||||
|
|
||||||
// is Option<Vec<>> for easier deserialization
|
|
||||||
#[serde(default, with = "parse::serde_option_conntrack_helpers")]
|
|
||||||
nf_conntrack_helpers: Option<Vec<String>>,
|
|
||||||
|
|
||||||
#[serde(default, with = "parse::serde_option_number")]
|
|
||||||
nf_conntrack_max: Option<i64>,
|
|
||||||
#[serde(default, with = "parse::serde_option_number")]
|
|
||||||
nf_conntrack_tcp_timeout_established: Option<i64>,
|
|
||||||
#[serde(default, with = "parse::serde_option_number")]
|
|
||||||
nf_conntrack_tcp_timeout_syn_recv: Option<i64>,
|
|
||||||
|
|
||||||
#[serde(default, with = "parse::serde_option_bool")]
|
|
||||||
nosmurfs: Option<bool>,
|
|
||||||
|
|
||||||
#[serde(default, with = "parse::serde_option_bool")]
|
|
||||||
protection_synflood: Option<bool>,
|
|
||||||
#[serde(default, with = "parse::serde_option_number")]
|
|
||||||
protection_synflood_burst: Option<i64>,
|
|
||||||
#[serde(default, with = "parse::serde_option_number")]
|
|
||||||
protection_synflood_rate: Option<i64>,
|
|
||||||
|
|
||||||
smurf_log_level: Option<LogLevel>,
|
|
||||||
tcp_flags_log_level: Option<LogLevel>,
|
|
||||||
|
|
||||||
#[serde(default, with = "parse::serde_option_bool")]
|
|
||||||
tcpflags: Option<bool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
pub struct Config {
|
|
||||||
pub(crate) config: super::common::Config<Options>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Config {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
config: Default::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse<R: io::BufRead>(input: R) -> Result<Self, Error> {
|
|
||||||
let config = super::common::Config::parse(input, &Default::default())?;
|
|
||||||
|
|
||||||
if !config.groups.is_empty() {
|
|
||||||
bail!("host firewall config cannot declare groups");
|
|
||||||
}
|
|
||||||
|
|
||||||
if !config.aliases.is_empty() {
|
|
||||||
bail!("host firewall config cannot declare aliases");
|
|
||||||
}
|
|
||||||
|
|
||||||
if !config.ipsets.is_empty() {
|
|
||||||
bail!("host firewall config cannot declare ipsets");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Self { config })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn rules(&self) -> &[Rule] {
|
|
||||||
&self.config.rules
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn management_ips() -> Result<Vec<Cidr>, Error> {
|
|
||||||
let mut management_cidrs = Vec::new();
|
|
||||||
|
|
||||||
for host_ip in host_ips() {
|
|
||||||
for network_interface_cidr in network_interface_cidrs() {
|
|
||||||
match (host_ip, network_interface_cidr) {
|
|
||||||
(IpAddr::V4(ip), Cidr::Ipv4(cidr)) => {
|
|
||||||
if cidr.contains_address(&ip) {
|
|
||||||
management_cidrs.push(network_interface_cidr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(IpAddr::V6(ip), Cidr::Ipv6(cidr)) => {
|
|
||||||
if cidr.contains_address(&ip) {
|
|
||||||
management_cidrs.push(network_interface_cidr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => continue,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(management_cidrs)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn hostname() -> &'static str {
|
|
||||||
nodename()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_alias(&self, name: &str) -> Option<&Alias> {
|
|
||||||
self.config.alias(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// returns value of enabled key or [`HOST_ENABLED_DEFAULT`] if unset
|
|
||||||
pub fn is_enabled(&self) -> bool {
|
|
||||||
self.config.options.enable.unwrap_or(HOST_ENABLED_DEFAULT)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// returns value of nftables key or [`HOST_NFTABLES_DEFAULT`] if unset
|
|
||||||
pub fn nftables(&self) -> bool {
|
|
||||||
self.config
|
|
||||||
.options
|
|
||||||
.nftables
|
|
||||||
.unwrap_or(HOST_NFTABLES_DEFAULT)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// returns value of ndp key or [`HOST_ALLOW_NDP_DEFAULT`] if unset
|
|
||||||
pub fn allow_ndp(&self) -> bool {
|
|
||||||
self.config.options.ndp.unwrap_or(HOST_ALLOW_NDP_DEFAULT)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// returns value of nosmurfs key or [`HOST_BLOCK_SMURFS_DEFAULT`] if unset
|
|
||||||
pub fn block_smurfs(&self) -> bool {
|
|
||||||
self.config
|
|
||||||
.options
|
|
||||||
.nosmurfs
|
|
||||||
.unwrap_or(HOST_BLOCK_SMURFS_DEFAULT)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// returns the log level for the smurf protection rule
|
|
||||||
///
|
|
||||||
/// If there is no log level set, it returns [`LogLevel::default()`]
|
|
||||||
pub fn block_smurfs_log_level(&self) -> LogLevel {
|
|
||||||
self.config.options.smurf_log_level.unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// returns value of protection_synflood key or [`HOST_BLOCK_SYNFLOOD_DEFAULT`] if unset
|
|
||||||
pub fn block_synflood(&self) -> bool {
|
|
||||||
self.config
|
|
||||||
.options
|
|
||||||
.protection_synflood
|
|
||||||
.unwrap_or(HOST_BLOCK_SYNFLOOD_DEFAULT)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// returns value of protection_synflood_rate key or [`HOST_BLOCK_SYNFLOOD_RATE_DEFAULT`] if
|
|
||||||
/// unset
|
|
||||||
pub fn synflood_rate(&self) -> i64 {
|
|
||||||
self.config
|
|
||||||
.options
|
|
||||||
.protection_synflood_rate
|
|
||||||
.unwrap_or(HOST_BLOCK_SYNFLOOD_RATE_DEFAULT)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// returns value of protection_synflood_burst key or [`HOST_BLOCK_SYNFLOOD_BURST_DEFAULT`] if
|
|
||||||
/// unset
|
|
||||||
pub fn synflood_burst(&self) -> i64 {
|
|
||||||
self.config
|
|
||||||
.options
|
|
||||||
.protection_synflood_burst
|
|
||||||
.unwrap_or(HOST_BLOCK_SYNFLOOD_BURST_DEFAULT)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// returns value of tcpflags key or [`HOST_BLOCK_INVALID_TCP_DEFAULT`] if unset
|
|
||||||
pub fn block_invalid_tcp(&self) -> bool {
|
|
||||||
self.config
|
|
||||||
.options
|
|
||||||
.tcpflags
|
|
||||||
.unwrap_or(HOST_BLOCK_INVALID_TCP_DEFAULT)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// returns the log level for the block invalid TCP packets rule
|
|
||||||
///
|
|
||||||
/// If there is no log level set, it returns [`LogLevel::default()`]
|
|
||||||
pub fn block_invalid_tcp_log_level(&self) -> LogLevel {
|
|
||||||
self.config.options.tcp_flags_log_level.unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// returns value of nf_conntrack_allow_invalid key or [`HOST_BLOCK_INVALID_CONNTRACK`] if
|
|
||||||
/// unset
|
|
||||||
pub fn block_invalid_conntrack(&self) -> bool {
|
|
||||||
!self
|
|
||||||
.config
|
|
||||||
.options
|
|
||||||
.nf_conntrack_allow_invalid
|
|
||||||
.unwrap_or(HOST_BLOCK_INVALID_CONNTRACK)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn nf_conntrack_max(&self) -> Option<i64> {
|
|
||||||
self.config.options.nf_conntrack_max
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn nf_conntrack_tcp_timeout_established(&self) -> Option<i64> {
|
|
||||||
self.config.options.nf_conntrack_tcp_timeout_established
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn nf_conntrack_tcp_timeout_syn_recv(&self) -> Option<i64> {
|
|
||||||
self.config.options.nf_conntrack_tcp_timeout_syn_recv
|
|
||||||
}
|
|
||||||
|
|
||||||
/// returns value of log_nf_conntrack key or [`HOST_LOG_INVALID_CONNTRACK`] if unset
|
|
||||||
pub fn log_nf_conntrack(&self) -> bool {
|
|
||||||
self.config
|
|
||||||
.options
|
|
||||||
.log_nf_conntrack
|
|
||||||
.unwrap_or(HOST_LOG_INVALID_CONNTRACK)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn conntrack_helpers(&self) -> Option<&Vec<String>> {
|
|
||||||
self.config.options.nf_conntrack_helpers.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// returns the log level for the given direction
|
|
||||||
///
|
|
||||||
/// If there is no log level set it returns [`LogLevel::default()`]
|
|
||||||
pub fn log_level(&self, dir: Direction) -> LogLevel {
|
|
||||||
match dir {
|
|
||||||
Direction::In => self.config.options.log_level_in.unwrap_or_default(),
|
|
||||||
Direction::Out => self.config.options.log_level_out.unwrap_or_default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use crate::firewall::types::{
|
|
||||||
log::LogLevel,
|
|
||||||
rule::{Kind, RuleGroup, Verdict},
|
|
||||||
rule_match::{Ports, Protocol, RuleMatch, Udp},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_config() {
|
|
||||||
const CONFIG: &str = r#"
|
|
||||||
[OPTIONS]
|
|
||||||
enable: 1
|
|
||||||
nftables: 1
|
|
||||||
log_level_in: debug
|
|
||||||
log_level_out: emerg
|
|
||||||
log_nf_conntrack: 0
|
|
||||||
ndp: 1
|
|
||||||
nf_conntrack_allow_invalid: yes
|
|
||||||
nf_conntrack_helpers: ftp
|
|
||||||
nf_conntrack_max: 44000
|
|
||||||
nf_conntrack_tcp_timeout_established: 500000
|
|
||||||
nf_conntrack_tcp_timeout_syn_recv: 44
|
|
||||||
nosmurfs: no
|
|
||||||
protection_synflood: 1
|
|
||||||
protection_synflood_burst: 2500
|
|
||||||
protection_synflood_rate: 300
|
|
||||||
smurf_log_level: notice
|
|
||||||
tcp_flags_log_level: nolog
|
|
||||||
tcpflags: yes
|
|
||||||
|
|
||||||
[RULES]
|
|
||||||
|
|
||||||
GROUP tgr -i eth0 # acomm
|
|
||||||
IN ACCEPT -p udp -dport 33 -sport 22 -log warning
|
|
||||||
|
|
||||||
"#;
|
|
||||||
|
|
||||||
let mut config = CONFIG.as_bytes();
|
|
||||||
let config = Config::parse(&mut config).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
config.config.options,
|
|
||||||
Options {
|
|
||||||
enable: Some(true),
|
|
||||||
nftables: Some(true),
|
|
||||||
log_level_in: Some(LogLevel::Debug),
|
|
||||||
log_level_out: Some(LogLevel::Emergency),
|
|
||||||
log_nf_conntrack: Some(false),
|
|
||||||
ndp: Some(true),
|
|
||||||
nf_conntrack_allow_invalid: Some(true),
|
|
||||||
nf_conntrack_helpers: Some(vec!["ftp".to_string()]),
|
|
||||||
nf_conntrack_max: Some(44000),
|
|
||||||
nf_conntrack_tcp_timeout_established: Some(500000),
|
|
||||||
nf_conntrack_tcp_timeout_syn_recv: Some(44),
|
|
||||||
nosmurfs: Some(false),
|
|
||||||
protection_synflood: Some(true),
|
|
||||||
protection_synflood_burst: Some(2500),
|
|
||||||
protection_synflood_rate: Some(300),
|
|
||||||
smurf_log_level: Some(LogLevel::Notice),
|
|
||||||
tcp_flags_log_level: Some(LogLevel::Nolog),
|
|
||||||
tcpflags: Some(true),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(config.config.rules.len(), 2);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
config.config.rules[0],
|
|
||||||
Rule {
|
|
||||||
disabled: false,
|
|
||||||
comment: Some("acomm".to_string()),
|
|
||||||
kind: Kind::Group(RuleGroup {
|
|
||||||
group: "tgr".to_string(),
|
|
||||||
iface: Some("eth0".to_string()),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
config.config.rules[1],
|
|
||||||
Rule {
|
|
||||||
disabled: false,
|
|
||||||
comment: None,
|
|
||||||
kind: Kind::Match(RuleMatch {
|
|
||||||
dir: Direction::In,
|
|
||||||
verdict: Verdict::Accept,
|
|
||||||
proto: Some(Protocol::Udp(Udp::new(Ports::from_u16(22, 33)))),
|
|
||||||
log: Some(LogLevel::Warning),
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
Config::parse("[ALIASES]\ntest 127.0.0.1".as_bytes())
|
|
||||||
.expect_err("host config cannot contain aliases");
|
|
||||||
|
|
||||||
Config::parse("[GROUP test]".as_bytes()).expect_err("host config cannot contain groups");
|
|
||||||
|
|
||||||
Config::parse("[IPSET test]".as_bytes()).expect_err("host config cannot contain ipsets");
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
pub mod cluster;
|
|
||||||
pub mod common;
|
|
||||||
pub mod ct_helper;
|
|
||||||
pub mod fw_macros;
|
|
||||||
pub mod guest;
|
|
||||||
pub mod host;
|
|
||||||
pub mod ports;
|
|
||||||
pub mod types;
|
|
||||||
|
|
||||||
pub(crate) mod parse;
|
|
@ -1,494 +0,0 @@
|
|||||||
use std::fmt;
|
|
||||||
|
|
||||||
use anyhow::{bail, format_err, Error};
|
|
||||||
|
|
||||||
const NAME_SPECIAL_CHARACTERS: [u8; 2] = [b'-', b'_'];
|
|
||||||
|
|
||||||
/// Parses out a "name" which can be alphanumeric and include dashes.
|
|
||||||
///
|
|
||||||
/// Returns `None` if the name part would be empty.
|
|
||||||
///
|
|
||||||
/// Returns a tuple with the name and the remainder (not trimmed).
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
/// ```ignore
|
|
||||||
/// assert_eq!(match_name("some-name someremainder"), Some(("some-name", " someremainder")));
|
|
||||||
/// assert_eq!(match_name("some-name@someremainder"), Some(("some-name", "@someremainder")));
|
|
||||||
/// assert_eq!(match_name(""), None);
|
|
||||||
/// assert_eq!(match_name(" someremainder"), None);
|
|
||||||
/// ```
|
|
||||||
pub fn match_name(line: &str) -> Option<(&str, &str)> {
|
|
||||||
if !line.starts_with(|c: char| c.is_ascii_alphabetic()) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let end = line
|
|
||||||
.as_bytes()
|
|
||||||
.iter()
|
|
||||||
.position(|&b| !(b.is_ascii_alphanumeric() || NAME_SPECIAL_CHARACTERS.contains(&b)));
|
|
||||||
|
|
||||||
let (name, rest) = match end {
|
|
||||||
Some(end) => line.split_at(end),
|
|
||||||
None => (line, ""),
|
|
||||||
};
|
|
||||||
|
|
||||||
if name.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some((name, rest))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parses up to the next whitespace character or end of the string.
|
|
||||||
///
|
|
||||||
/// Returns `None` if the non-whitespace part would be empty.
|
|
||||||
///
|
|
||||||
/// Returns a tuple containing the parsed section and the *trimmed* remainder.
|
|
||||||
pub fn match_non_whitespace(line: &str) -> Option<(&str, &str)> {
|
|
||||||
let (text, rest) = line
|
|
||||||
.as_bytes()
|
|
||||||
.iter()
|
|
||||||
.position(|&b| b.is_ascii_whitespace())
|
|
||||||
.map(|pos| {
|
|
||||||
let (a, b) = line.split_at(pos);
|
|
||||||
(a, b.trim_start())
|
|
||||||
})
|
|
||||||
.unwrap_or((line, ""));
|
|
||||||
if text.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some((text, rest))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// parses out all digits and returns the remainder
|
|
||||||
///
|
|
||||||
/// returns [`None`] if the digit part would be empty
|
|
||||||
///
|
|
||||||
/// Returns a tuple with the digits and the remainder (not trimmed).
|
|
||||||
pub fn match_digits(line: &str) -> Option<(&str, &str)> {
|
|
||||||
let split_position = line.as_bytes().iter().position(|&b| !b.is_ascii_digit());
|
|
||||||
|
|
||||||
let (digits, rest) = match split_position {
|
|
||||||
Some(pos) => line.split_at(pos),
|
|
||||||
None => (line, ""),
|
|
||||||
};
|
|
||||||
|
|
||||||
if !digits.is_empty() {
|
|
||||||
return Some((digits, rest));
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Separate a `key: value` line, trimming whitespace.
|
|
||||||
///
|
|
||||||
/// Returns `None` if the `key` would be empty.
|
|
||||||
pub fn split_key_value(line: &str) -> Option<(&str, &str)> {
|
|
||||||
line.split_once(':')
|
|
||||||
.map(|(key, value)| (key.trim(), value.trim()))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse a boolean.
|
|
||||||
///
|
|
||||||
/// values that parse as [`false`]: 0, false, off, no
|
|
||||||
/// values that parse as [`true`]: 1, true, on, yes
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
/// ```ignore
|
|
||||||
/// assert_eq!(parse_bool("false"), Ok(false));
|
|
||||||
/// assert_eq!(parse_bool("on"), Ok(true));
|
|
||||||
/// assert!(parse_bool("proxmox").is_err());
|
|
||||||
/// ```
|
|
||||||
pub fn parse_bool(value: &str) -> Result<bool, Error> {
|
|
||||||
Ok(
|
|
||||||
if value == "0"
|
|
||||||
|| value.eq_ignore_ascii_case("false")
|
|
||||||
|| value.eq_ignore_ascii_case("off")
|
|
||||||
|| value.eq_ignore_ascii_case("no")
|
|
||||||
{
|
|
||||||
false
|
|
||||||
} else if value == "1"
|
|
||||||
|| value.eq_ignore_ascii_case("true")
|
|
||||||
|| value.eq_ignore_ascii_case("on")
|
|
||||||
|| value.eq_ignore_ascii_case("yes")
|
|
||||||
{
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
bail!("not a boolean: {value:?}");
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse the *remainder* of a section line, that is `<whitespace>NAME] #optional comment`.
|
|
||||||
/// The `kind` parameter is used for error messages and should be the section type.
|
|
||||||
///
|
|
||||||
/// Return the name and the optional comment.
|
|
||||||
pub fn parse_named_section_tail<'a>(
|
|
||||||
kind: &'static str,
|
|
||||||
line: &'a str,
|
|
||||||
) -> Result<(&'a str, Option<&'a str>), Error> {
|
|
||||||
if line.is_empty() || !line.as_bytes()[0].is_ascii_whitespace() {
|
|
||||||
bail!("incomplete {kind} section");
|
|
||||||
}
|
|
||||||
|
|
||||||
let line = line.trim_start();
|
|
||||||
let (name, line) = match_name(line)
|
|
||||||
.ok_or_else(|| format_err!("expected a name for the {kind} at {line:?}"))?;
|
|
||||||
|
|
||||||
let line = line
|
|
||||||
.strip_prefix(']')
|
|
||||||
.ok_or_else(|| format_err!("expected closing ']' in {kind} section header"))?
|
|
||||||
.trim_start();
|
|
||||||
|
|
||||||
Ok(match line.strip_prefix('#') {
|
|
||||||
Some(comment) => (name, Some(comment.trim())),
|
|
||||||
None if !line.is_empty() => bail!("trailing characters after {kind} section: {line:?}"),
|
|
||||||
None => (name, None),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// parses a number from a string OR number
|
|
||||||
pub mod serde_option_number {
|
|
||||||
use std::fmt;
|
|
||||||
|
|
||||||
use serde::de::{Deserializer, Error, Visitor};
|
|
||||||
|
|
||||||
pub fn deserialize<'de, D: Deserializer<'de>>(
|
|
||||||
deserializer: D,
|
|
||||||
) -> Result<Option<i64>, D::Error> {
|
|
||||||
struct V;
|
|
||||||
|
|
||||||
impl<'de> Visitor<'de> for V {
|
|
||||||
type Value = Option<i64>;
|
|
||||||
|
|
||||||
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
f.write_str("a numerical value")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
|
|
||||||
v.parse().map_err(E::custom).map(Some)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_none<E: Error>(self) -> Result<Self::Value, E> {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
deserializer.deserialize_any(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deserializer.deserialize_any(V)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// parses a bool from a string OR bool
|
|
||||||
pub mod serde_option_bool {
|
|
||||||
use std::fmt;
|
|
||||||
|
|
||||||
use serde::de::{Deserializer, Error, Visitor};
|
|
||||||
|
|
||||||
pub fn deserialize<'de, D: Deserializer<'de>>(
|
|
||||||
deserializer: D,
|
|
||||||
) -> Result<Option<bool>, D::Error> {
|
|
||||||
struct V;
|
|
||||||
|
|
||||||
impl<'de> Visitor<'de> for V {
|
|
||||||
type Value = Option<bool>;
|
|
||||||
|
|
||||||
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
f.write_str("a boolean-like value")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_bool<E: Error>(self, v: bool) -> Result<Self::Value, E> {
|
|
||||||
Ok(Some(v))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
|
|
||||||
super::parse_bool(v).map_err(E::custom).map(Some)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_none<E: Error>(self) -> Result<Self::Value, E> {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
deserializer.deserialize_any(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deserializer.deserialize_any(V)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// parses a comma_separated list of strings
|
|
||||||
pub mod serde_option_conntrack_helpers {
|
|
||||||
use std::fmt;
|
|
||||||
|
|
||||||
use serde::de::{Deserializer, Error, Visitor};
|
|
||||||
|
|
||||||
pub fn deserialize<'de, D: Deserializer<'de>>(
|
|
||||||
deserializer: D,
|
|
||||||
) -> Result<Option<Vec<String>>, D::Error> {
|
|
||||||
struct V;
|
|
||||||
|
|
||||||
impl<'de> Visitor<'de> for V {
|
|
||||||
type Value = Option<Vec<String>>;
|
|
||||||
|
|
||||||
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
f.write_str("A list of conntrack helpers")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
|
|
||||||
if v.is_empty() {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Some(v.split(',').map(String::from).collect()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_none<E: Error>(self) -> Result<Self::Value, E> {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
deserializer.deserialize_any(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deserializer.deserialize_any(V)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// parses a log_ratelimit string: '[enable=]<1|0> [,burst=<integer>] [,rate=<rate>]'
|
|
||||||
pub mod serde_option_log_ratelimit {
|
|
||||||
use std::fmt;
|
|
||||||
|
|
||||||
use serde::de::{Deserializer, Error, Visitor};
|
|
||||||
|
|
||||||
use crate::firewall::types::log::LogRateLimit;
|
|
||||||
|
|
||||||
pub fn deserialize<'de, D: Deserializer<'de>>(
|
|
||||||
deserializer: D,
|
|
||||||
) -> Result<Option<LogRateLimit>, D::Error> {
|
|
||||||
struct V;
|
|
||||||
|
|
||||||
impl<'de> Visitor<'de> for V {
|
|
||||||
type Value = Option<LogRateLimit>;
|
|
||||||
|
|
||||||
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
f.write_str("a boolean-like value")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
|
|
||||||
v.parse().map_err(E::custom).map(Some)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_none<E: Error>(self) -> Result<Self::Value, E> {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
deserializer.deserialize_any(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deserializer.deserialize_any(V)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `&str` deserializer which also accepts an `Option`.
|
|
||||||
///
|
|
||||||
/// Serde's `StringDeserializer` does not.
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
|
||||||
pub struct SomeStrDeserializer<'a, E>(serde::de::value::StrDeserializer<'a, E>);
|
|
||||||
|
|
||||||
impl<'de, 'a, E> serde::de::Deserializer<'de> for SomeStrDeserializer<'a, E>
|
|
||||||
where
|
|
||||||
E: serde::de::Error,
|
|
||||||
{
|
|
||||||
type Error = E;
|
|
||||||
|
|
||||||
fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
|
||||||
where
|
|
||||||
V: serde::de::Visitor<'de>,
|
|
||||||
{
|
|
||||||
self.0.deserialize_any(visitor)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
|
||||||
where
|
|
||||||
V: serde::de::Visitor<'de>,
|
|
||||||
{
|
|
||||||
visitor.visit_some(self.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
|
||||||
where
|
|
||||||
V: serde::de::Visitor<'de>,
|
|
||||||
{
|
|
||||||
self.0.deserialize_str(visitor)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_string<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
|
||||||
where
|
|
||||||
V: serde::de::Visitor<'de>,
|
|
||||||
{
|
|
||||||
self.0.deserialize_string(visitor)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_enum<V>(
|
|
||||||
self,
|
|
||||||
_name: &str,
|
|
||||||
_variants: &'static [&'static str],
|
|
||||||
visitor: V,
|
|
||||||
) -> Result<V::Value, Self::Error>
|
|
||||||
where
|
|
||||||
V: serde::de::Visitor<'de>,
|
|
||||||
{
|
|
||||||
visitor.visit_enum(self.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
serde::forward_to_deserialize_any! {
|
|
||||||
bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char
|
|
||||||
bytes byte_buf unit unit_struct newtype_struct seq tuple
|
|
||||||
tuple_struct map struct identifier ignored_any
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `&str` wrapper which implements `IntoDeserializer` via `SomeStrDeserializer`.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct SomeStr<'a>(pub &'a str);
|
|
||||||
|
|
||||||
impl<'a> From<&'a str> for SomeStr<'a> {
|
|
||||||
fn from(s: &'a str) -> Self {
|
|
||||||
Self(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de, 'a, E> serde::de::IntoDeserializer<'de, E> for SomeStr<'a>
|
|
||||||
where
|
|
||||||
E: serde::de::Error,
|
|
||||||
{
|
|
||||||
type Deserializer = SomeStrDeserializer<'a, E>;
|
|
||||||
|
|
||||||
fn into_deserializer(self) -> Self::Deserializer {
|
|
||||||
SomeStrDeserializer(self.0.into_deserializer())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `String` deserializer which also accepts an `Option`.
|
|
||||||
///
|
|
||||||
/// Serde's `StringDeserializer` does not.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct SomeStringDeserializer<E>(serde::de::value::StringDeserializer<E>);
|
|
||||||
|
|
||||||
impl<'de, E> serde::de::Deserializer<'de> for SomeStringDeserializer<E>
|
|
||||||
where
|
|
||||||
E: serde::de::Error,
|
|
||||||
{
|
|
||||||
type Error = E;
|
|
||||||
|
|
||||||
fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
|
||||||
where
|
|
||||||
V: serde::de::Visitor<'de>,
|
|
||||||
{
|
|
||||||
self.0.deserialize_any(visitor)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
|
||||||
where
|
|
||||||
V: serde::de::Visitor<'de>,
|
|
||||||
{
|
|
||||||
visitor.visit_some(self.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
|
||||||
where
|
|
||||||
V: serde::de::Visitor<'de>,
|
|
||||||
{
|
|
||||||
self.0.deserialize_str(visitor)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_string<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
|
||||||
where
|
|
||||||
V: serde::de::Visitor<'de>,
|
|
||||||
{
|
|
||||||
self.0.deserialize_string(visitor)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_enum<V>(
|
|
||||||
self,
|
|
||||||
_name: &str,
|
|
||||||
_variants: &'static [&'static str],
|
|
||||||
visitor: V,
|
|
||||||
) -> Result<V::Value, Self::Error>
|
|
||||||
where
|
|
||||||
V: serde::de::Visitor<'de>,
|
|
||||||
{
|
|
||||||
visitor.visit_enum(self.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
serde::forward_to_deserialize_any! {
|
|
||||||
bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char
|
|
||||||
bytes byte_buf unit unit_struct newtype_struct seq tuple
|
|
||||||
tuple_struct map struct identifier ignored_any
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `&str` wrapper which implements `IntoDeserializer` via `SomeStringDeserializer`.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct SomeString(pub String);
|
|
||||||
|
|
||||||
impl From<&str> for SomeString {
|
|
||||||
fn from(s: &str) -> Self {
|
|
||||||
Self::from(s.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<String> for SomeString {
|
|
||||||
fn from(s: String) -> Self {
|
|
||||||
Self(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de, E> serde::de::IntoDeserializer<'de, E> for SomeString
|
|
||||||
where
|
|
||||||
E: serde::de::Error,
|
|
||||||
{
|
|
||||||
type Deserializer = SomeStringDeserializer<E>;
|
|
||||||
|
|
||||||
fn into_deserializer(self) -> Self::Deserializer {
|
|
||||||
SomeStringDeserializer(self.0.into_deserializer())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct SerdeStringError(String);
|
|
||||||
|
|
||||||
impl std::error::Error for SerdeStringError {}
|
|
||||||
|
|
||||||
impl fmt::Display for SerdeStringError {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
f.write_str(&self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl serde::de::Error for SerdeStringError {
|
|
||||||
fn custom<T: fmt::Display>(msg: T) -> Self {
|
|
||||||
Self(msg.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,80 +0,0 @@
|
|||||||
use anyhow::{format_err, Error};
|
|
||||||
use std::sync::OnceLock;
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
struct NamedPorts {
|
|
||||||
ports: std::collections::HashMap<String, u16>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NamedPorts {
|
|
||||||
fn new() -> Self {
|
|
||||||
use std::io::BufRead;
|
|
||||||
|
|
||||||
log::trace!("loading /etc/services");
|
|
||||||
|
|
||||||
let mut this = Self::default();
|
|
||||||
|
|
||||||
let file = match std::fs::File::open("/etc/services") {
|
|
||||||
Ok(file) => file,
|
|
||||||
Err(_) => return this,
|
|
||||||
};
|
|
||||||
|
|
||||||
for line in std::io::BufReader::new(file).lines() {
|
|
||||||
let line = match line {
|
|
||||||
Ok(line) => line,
|
|
||||||
Err(_) => break,
|
|
||||||
};
|
|
||||||
|
|
||||||
let line = line.trim_start();
|
|
||||||
|
|
||||||
if line.is_empty() || line.starts_with('#') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut parts = line.split_ascii_whitespace();
|
|
||||||
|
|
||||||
let name = match parts.next() {
|
|
||||||
None => continue,
|
|
||||||
Some(name) => name.to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let proto: u16 = match parts.next() {
|
|
||||||
None => continue,
|
|
||||||
Some(proto) => match proto.split('/').next() {
|
|
||||||
None => continue,
|
|
||||||
Some(num) => match num.parse() {
|
|
||||||
Ok(num) => num,
|
|
||||||
Err(_) => continue,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
this.ports.insert(name, proto);
|
|
||||||
for alias in parts {
|
|
||||||
if alias.starts_with('#') {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
this.ports.insert(alias.to_string(), proto);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this
|
|
||||||
}
|
|
||||||
|
|
||||||
fn find(&self, name: &str) -> Option<u16> {
|
|
||||||
self.ports.get(name).copied()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn named_ports() -> &'static NamedPorts {
|
|
||||||
static NAMED_PORTS: OnceLock<NamedPorts> = OnceLock::new();
|
|
||||||
|
|
||||||
NAMED_PORTS.get_or_init(NamedPorts::new)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse a named port with the help of `/etc/services`.
|
|
||||||
pub fn parse_named_port(name: &str) -> Result<u16, Error> {
|
|
||||||
named_ports()
|
|
||||||
.find(name)
|
|
||||||
.ok_or_else(|| format_err!("unknown port name {name:?}"))
|
|
||||||
}
|
|
@ -1,615 +0,0 @@
|
|||||||
use std::fmt;
|
|
||||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
|
||||||
use std::ops::Deref;
|
|
||||||
|
|
||||||
use anyhow::{bail, format_err, Error};
|
|
||||||
use serde_with::DeserializeFromStr;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
||||||
pub enum Family {
|
|
||||||
V4,
|
|
||||||
V6,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for Family {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
Family::V4 => f.write_str("Ipv4"),
|
|
||||||
Family::V6 => f.write_str("Ipv6"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
pub enum Cidr {
|
|
||||||
Ipv4(Ipv4Cidr),
|
|
||||||
Ipv6(Ipv6Cidr),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Cidr {
|
|
||||||
pub fn new_v4(addr: impl Into<Ipv4Addr>, mask: u8) -> Result<Self, Error> {
|
|
||||||
Ok(Cidr::Ipv4(Ipv4Cidr::new(addr, mask)?))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_v6(addr: impl Into<Ipv6Addr>, mask: u8) -> Result<Self, Error> {
|
|
||||||
Ok(Cidr::Ipv6(Ipv6Cidr::new(addr, mask)?))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn family(&self) -> Family {
|
|
||||||
match self {
|
|
||||||
Cidr::Ipv4(_) => Family::V4,
|
|
||||||
Cidr::Ipv6(_) => Family::V6,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_ipv4(&self) -> bool {
|
|
||||||
matches!(self, Cidr::Ipv4(_))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_ipv6(&self) -> bool {
|
|
||||||
matches!(self, Cidr::Ipv6(_))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for Cidr {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
Self::Ipv4(ip) => f.write_str(ip.to_string().as_str()),
|
|
||||||
Self::Ipv6(ip) => f.write_str(ip.to_string().as_str()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::str::FromStr for Cidr {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Error> {
|
|
||||||
if let Ok(ip) = s.parse::<Ipv4Cidr>() {
|
|
||||||
return Ok(Cidr::Ipv4(ip));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(ip) = s.parse::<Ipv6Cidr>() {
|
|
||||||
return Ok(Cidr::Ipv6(ip));
|
|
||||||
}
|
|
||||||
|
|
||||||
bail!("invalid ip address or CIDR: {s:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Ipv4Cidr> for Cidr {
|
|
||||||
fn from(cidr: Ipv4Cidr) -> Self {
|
|
||||||
Cidr::Ipv4(cidr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Ipv6Cidr> for Cidr {
|
|
||||||
fn from(cidr: Ipv6Cidr) -> Self {
|
|
||||||
Cidr::Ipv6(cidr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const IPV4_LENGTH: u8 = 32;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
pub struct Ipv4Cidr {
|
|
||||||
addr: Ipv4Addr,
|
|
||||||
mask: u8,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Ipv4Cidr {
|
|
||||||
pub fn new(addr: impl Into<Ipv4Addr>, mask: u8) -> Result<Self, Error> {
|
|
||||||
if mask > 32 {
|
|
||||||
bail!("mask out of range for ipv4 cidr ({mask})");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
addr: addr.into(),
|
|
||||||
mask,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn contains_address(&self, other: &Ipv4Addr) -> bool {
|
|
||||||
let bits = u32::from_be_bytes(self.addr.octets());
|
|
||||||
let other_bits = u32::from_be_bytes(other.octets());
|
|
||||||
|
|
||||||
let shift_amount: u32 = IPV4_LENGTH.saturating_sub(self.mask).into();
|
|
||||||
|
|
||||||
bits.checked_shr(shift_amount).unwrap_or(0)
|
|
||||||
== other_bits.checked_shr(shift_amount).unwrap_or(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn address(&self) -> &Ipv4Addr {
|
|
||||||
&self.addr
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn mask(&self) -> u8 {
|
|
||||||
self.mask
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Into<Ipv4Addr>> From<T> for Ipv4Cidr {
|
|
||||||
fn from(value: T) -> Self {
|
|
||||||
Self {
|
|
||||||
addr: value.into(),
|
|
||||||
mask: 32,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::str::FromStr for Ipv4Cidr {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Error> {
|
|
||||||
Ok(match s.find('/') {
|
|
||||||
None => Self {
|
|
||||||
addr: s.parse()?,
|
|
||||||
mask: 32,
|
|
||||||
},
|
|
||||||
Some(pos) => {
|
|
||||||
let mask: u8 = s[(pos + 1)..]
|
|
||||||
.parse()
|
|
||||||
.map_err(|_| format_err!("invalid mask in ipv4 cidr: {s:?}"))?;
|
|
||||||
|
|
||||||
Self::new(s[..pos].parse::<Ipv4Addr>()?, mask)?
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for Ipv4Cidr {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(f, "{}/{}", &self.addr, self.mask)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const IPV6_LENGTH: u8 = 128;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
pub struct Ipv6Cidr {
|
|
||||||
addr: Ipv6Addr,
|
|
||||||
mask: u8,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Ipv6Cidr {
|
|
||||||
pub fn new(addr: impl Into<Ipv6Addr>, mask: u8) -> Result<Self, Error> {
|
|
||||||
if mask > IPV6_LENGTH {
|
|
||||||
bail!("mask out of range for ipv6 cidr");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
addr: addr.into(),
|
|
||||||
mask,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn contains_address(&self, other: &Ipv6Addr) -> bool {
|
|
||||||
let bits = u128::from_be_bytes(self.addr.octets());
|
|
||||||
let other_bits = u128::from_be_bytes(other.octets());
|
|
||||||
|
|
||||||
let shift_amount: u32 = IPV6_LENGTH.saturating_sub(self.mask).into();
|
|
||||||
|
|
||||||
bits.checked_shr(shift_amount).unwrap_or(0)
|
|
||||||
== other_bits.checked_shr(shift_amount).unwrap_or(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn address(&self) -> &Ipv6Addr {
|
|
||||||
&self.addr
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn mask(&self) -> u8 {
|
|
||||||
self.mask
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::str::FromStr for Ipv6Cidr {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Error> {
|
|
||||||
Ok(match s.find('/') {
|
|
||||||
None => Self {
|
|
||||||
addr: s.parse()?,
|
|
||||||
mask: 128,
|
|
||||||
},
|
|
||||||
Some(pos) => {
|
|
||||||
let mask: u8 = s[(pos + 1)..]
|
|
||||||
.parse()
|
|
||||||
.map_err(|_| format_err!("invalid mask in ipv6 cidr: {s:?}"))?;
|
|
||||||
|
|
||||||
Self::new(s[..pos].parse::<Ipv6Addr>()?, mask)?
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for Ipv6Cidr {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(f, "{}/{}", &self.addr, self.mask)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Into<Ipv6Addr>> From<T> for Ipv6Cidr {
|
|
||||||
fn from(addr: T) -> Self {
|
|
||||||
Self {
|
|
||||||
addr: addr.into(),
|
|
||||||
mask: 128,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
pub enum IpEntry {
|
|
||||||
Cidr(Cidr),
|
|
||||||
Range(IpAddr, IpAddr),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::str::FromStr for IpEntry {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Error> {
|
|
||||||
if s.is_empty() {
|
|
||||||
bail!("Empty IP specification!")
|
|
||||||
}
|
|
||||||
|
|
||||||
let entries: Vec<&str> = s
|
|
||||||
.split('-')
|
|
||||||
.take(3) // so we can check whether there are too many
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
match entries.as_slice() {
|
|
||||||
[cidr] => Ok(IpEntry::Cidr(cidr.parse()?)),
|
|
||||||
[beg, end] => {
|
|
||||||
if let Ok(beg) = beg.parse::<Ipv4Addr>() {
|
|
||||||
if let Ok(end) = end.parse::<Ipv4Addr>() {
|
|
||||||
if beg < end {
|
|
||||||
return Ok(IpEntry::Range(beg.into(), end.into()));
|
|
||||||
}
|
|
||||||
|
|
||||||
bail!("start address is greater than end address!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(beg) = beg.parse::<Ipv6Addr>() {
|
|
||||||
if let Ok(end) = end.parse::<Ipv6Addr>() {
|
|
||||||
if beg < end {
|
|
||||||
return Ok(IpEntry::Range(beg.into(), end.into()));
|
|
||||||
}
|
|
||||||
|
|
||||||
bail!("start address is greater than end address!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bail!("start and end are not valid IP addresses of the same type!")
|
|
||||||
}
|
|
||||||
_ => bail!("Invalid amount of elements in IpEntry!"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for IpEntry {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
Self::Cidr(ip) => write!(f, "{ip}"),
|
|
||||||
Self::Range(beg, end) => write!(f, "{beg}-{end}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IpEntry {
|
|
||||||
fn family(&self) -> Family {
|
|
||||||
match self {
|
|
||||||
Self::Cidr(cidr) => cidr.family(),
|
|
||||||
Self::Range(start, end) => {
|
|
||||||
if start.is_ipv4() && end.is_ipv4() {
|
|
||||||
return Family::V4;
|
|
||||||
}
|
|
||||||
|
|
||||||
if start.is_ipv6() && end.is_ipv6() {
|
|
||||||
return Family::V6;
|
|
||||||
}
|
|
||||||
|
|
||||||
// should never be reached due to constructors validating that
|
|
||||||
// start type == end type
|
|
||||||
unreachable!("invalid IP entry")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Cidr> for IpEntry {
|
|
||||||
fn from(value: Cidr) -> Self {
|
|
||||||
IpEntry::Cidr(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, DeserializeFromStr)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
pub struct IpList {
|
|
||||||
// guaranteed to have the same family
|
|
||||||
entries: Vec<IpEntry>,
|
|
||||||
family: Family,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for IpList {
|
|
||||||
type Target = Vec<IpEntry>;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.entries
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Into<IpEntry>> From<T> for IpList {
|
|
||||||
fn from(value: T) -> Self {
|
|
||||||
let entry = value.into();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
family: entry.family(),
|
|
||||||
entries: vec![entry],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::str::FromStr for IpList {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Error> {
|
|
||||||
if s.is_empty() {
|
|
||||||
bail!("Empty IP specification!")
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut entries = Vec::new();
|
|
||||||
let mut current_family = None;
|
|
||||||
|
|
||||||
for element in s.split(',') {
|
|
||||||
let entry: IpEntry = element.parse()?;
|
|
||||||
|
|
||||||
if let Some(family) = current_family {
|
|
||||||
if family != entry.family() {
|
|
||||||
bail!("Incompatible families in IPList!")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
current_family = Some(entry.family());
|
|
||||||
}
|
|
||||||
|
|
||||||
entries.push(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
if entries.is_empty() {
|
|
||||||
bail!("empty ip list")
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(IpList {
|
|
||||||
entries,
|
|
||||||
family: current_family.unwrap(), // must be set due to length check above
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IpList {
|
|
||||||
pub fn new(entries: Vec<IpEntry>) -> Result<Self, Error> {
|
|
||||||
let family = entries.iter().try_fold(None, |result, entry| {
|
|
||||||
if let Some(family) = result {
|
|
||||||
if entry.family() != family {
|
|
||||||
bail!("non-matching families in entries list");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Some(family))
|
|
||||||
} else {
|
|
||||||
Ok(Some(entry.family()))
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if let Some(family) = family {
|
|
||||||
return Ok(Self { entries, family });
|
|
||||||
}
|
|
||||||
|
|
||||||
bail!("no elements in ip list entries");
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn family(&self) -> Family {
|
|
||||||
self.family
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_v4_cidr() {
|
|
||||||
let mut cidr: Ipv4Cidr = "0.0.0.0/0".parse().expect("valid IPv4 CIDR");
|
|
||||||
|
|
||||||
assert_eq!(cidr.addr, Ipv4Addr::new(0, 0, 0, 0));
|
|
||||||
assert_eq!(cidr.mask, 0);
|
|
||||||
|
|
||||||
assert!(cidr.contains_address(&Ipv4Addr::new(0, 0, 0, 0)));
|
|
||||||
assert!(cidr.contains_address(&Ipv4Addr::new(255, 255, 255, 255)));
|
|
||||||
|
|
||||||
cidr = "192.168.100.1".parse().expect("valid IPv4 CIDR");
|
|
||||||
|
|
||||||
assert_eq!(cidr.addr, Ipv4Addr::new(192, 168, 100, 1));
|
|
||||||
assert_eq!(cidr.mask, 32);
|
|
||||||
|
|
||||||
assert!(cidr.contains_address(&Ipv4Addr::new(192, 168, 100, 1)));
|
|
||||||
assert!(!cidr.contains_address(&Ipv4Addr::new(192, 168, 100, 2)));
|
|
||||||
assert!(!cidr.contains_address(&Ipv4Addr::new(192, 168, 100, 0)));
|
|
||||||
|
|
||||||
cidr = "10.100.5.0/24".parse().expect("valid IPv4 CIDR");
|
|
||||||
|
|
||||||
assert_eq!(cidr.mask, 24);
|
|
||||||
|
|
||||||
assert!(cidr.contains_address(&Ipv4Addr::new(10, 100, 5, 0)));
|
|
||||||
assert!(cidr.contains_address(&Ipv4Addr::new(10, 100, 5, 1)));
|
|
||||||
assert!(cidr.contains_address(&Ipv4Addr::new(10, 100, 5, 100)));
|
|
||||||
assert!(cidr.contains_address(&Ipv4Addr::new(10, 100, 5, 255)));
|
|
||||||
assert!(!cidr.contains_address(&Ipv4Addr::new(10, 100, 4, 255)));
|
|
||||||
assert!(!cidr.contains_address(&Ipv4Addr::new(10, 100, 6, 0)));
|
|
||||||
|
|
||||||
"0.0.0.0/-1".parse::<Ipv4Cidr>().unwrap_err();
|
|
||||||
"0.0.0.0/33".parse::<Ipv4Cidr>().unwrap_err();
|
|
||||||
"256.256.256.256/10".parse::<Ipv4Cidr>().unwrap_err();
|
|
||||||
|
|
||||||
"fe80::1/64".parse::<Ipv4Cidr>().unwrap_err();
|
|
||||||
"qweasd".parse::<Ipv4Cidr>().unwrap_err();
|
|
||||||
"".parse::<Ipv4Cidr>().unwrap_err();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_v6_cidr() {
|
|
||||||
let mut cidr: Ipv6Cidr = "abab::1/64".parse().expect("valid IPv6 CIDR");
|
|
||||||
|
|
||||||
assert_eq!(cidr.addr, Ipv6Addr::new(0xABAB, 0, 0, 0, 0, 0, 0, 1));
|
|
||||||
assert_eq!(cidr.mask, 64);
|
|
||||||
|
|
||||||
assert!(cidr.contains_address(&Ipv6Addr::new(0xABAB, 0, 0, 0, 0, 0, 0, 0)));
|
|
||||||
assert!(cidr.contains_address(&Ipv6Addr::new(
|
|
||||||
0xABAB, 0, 0, 0, 0xAAAA, 0xAAAA, 0xAAAA, 0xAAAA
|
|
||||||
)));
|
|
||||||
assert!(cidr.contains_address(&Ipv6Addr::new(
|
|
||||||
0xABAB, 0, 0, 0, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF
|
|
||||||
)));
|
|
||||||
assert!(!cidr.contains_address(&Ipv6Addr::new(0xABAB, 0, 0, 1, 0, 0, 0, 0)));
|
|
||||||
assert!(!cidr.contains_address(&Ipv6Addr::new(
|
|
||||||
0xABAA, 0, 0, 0, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF
|
|
||||||
)));
|
|
||||||
|
|
||||||
cidr = "eeee::1".parse().expect("valid IPv6 CIDR");
|
|
||||||
|
|
||||||
assert_eq!(cidr.mask, 128);
|
|
||||||
|
|
||||||
assert!(cidr.contains_address(&Ipv6Addr::new(0xEEEE, 0, 0, 0, 0, 0, 0, 1)));
|
|
||||||
assert!(!cidr.contains_address(&Ipv6Addr::new(
|
|
||||||
0xEEED, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF
|
|
||||||
)));
|
|
||||||
assert!(!cidr.contains_address(&Ipv6Addr::new(0xEEEE, 0, 0, 0, 0, 0, 0, 0)));
|
|
||||||
|
|
||||||
"eeee::1/-1".parse::<Ipv6Cidr>().unwrap_err();
|
|
||||||
"eeee::1/129".parse::<Ipv6Cidr>().unwrap_err();
|
|
||||||
"gggg::1/64".parse::<Ipv6Cidr>().unwrap_err();
|
|
||||||
|
|
||||||
"192.168.0.1".parse::<Ipv6Cidr>().unwrap_err();
|
|
||||||
"qweasd".parse::<Ipv6Cidr>().unwrap_err();
|
|
||||||
"".parse::<Ipv6Cidr>().unwrap_err();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_ip_entry() {
|
|
||||||
let mut entry: IpEntry = "10.0.0.1".parse().expect("valid IP entry");
|
|
||||||
|
|
||||||
assert_eq!(entry, Cidr::new_v4([10, 0, 0, 1], 32).unwrap().into());
|
|
||||||
|
|
||||||
entry = "10.0.0.0/16".parse().expect("valid IP entry");
|
|
||||||
|
|
||||||
assert_eq!(entry, Cidr::new_v4([10, 0, 0, 0], 16).unwrap().into());
|
|
||||||
|
|
||||||
entry = "192.168.0.1-192.168.99.255"
|
|
||||||
.parse()
|
|
||||||
.expect("valid IP entry");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
entry,
|
|
||||||
IpEntry::Range([192, 168, 0, 1].into(), [192, 168, 99, 255].into())
|
|
||||||
);
|
|
||||||
|
|
||||||
entry = "fe80::1".parse().expect("valid IP entry");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
entry,
|
|
||||||
Cidr::new_v6([0xFE80, 0, 0, 0, 0, 0, 0, 1], 128)
|
|
||||||
.unwrap()
|
|
||||||
.into()
|
|
||||||
);
|
|
||||||
|
|
||||||
entry = "fe80::1/48".parse().expect("valid IP entry");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
entry,
|
|
||||||
Cidr::new_v6([0xFE80, 0, 0, 0, 0, 0, 0, 1], 48)
|
|
||||||
.unwrap()
|
|
||||||
.into()
|
|
||||||
);
|
|
||||||
|
|
||||||
entry = "fd80::1-fd80::ffff".parse().expect("valid IP entry");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
entry,
|
|
||||||
IpEntry::Range(
|
|
||||||
[0xFD80, 0, 0, 0, 0, 0, 0, 1].into(),
|
|
||||||
[0xFD80, 0, 0, 0, 0, 0, 0, 0xFFFF].into(),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
"192.168.100.0-192.168.99.255"
|
|
||||||
.parse::<IpEntry>()
|
|
||||||
.unwrap_err();
|
|
||||||
"192.168.100.0-fe80::1".parse::<IpEntry>().unwrap_err();
|
|
||||||
"192.168.100.0-192.168.200.0/16"
|
|
||||||
.parse::<IpEntry>()
|
|
||||||
.unwrap_err();
|
|
||||||
"192.168.100.0-192.168.200.0-192.168.250.0"
|
|
||||||
.parse::<IpEntry>()
|
|
||||||
.unwrap_err();
|
|
||||||
"qweasd".parse::<IpEntry>().unwrap_err();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_ip_list() {
|
|
||||||
let mut ip_list: IpList = "192.168.0.1,192.168.100.0/24,172.16.0.0-172.32.255.255"
|
|
||||||
.parse()
|
|
||||||
.expect("valid IP list");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
ip_list,
|
|
||||||
IpList {
|
|
||||||
entries: vec![
|
|
||||||
IpEntry::Cidr(Cidr::new_v4([192, 168, 0, 1], 32).unwrap()),
|
|
||||||
IpEntry::Cidr(Cidr::new_v4([192, 168, 100, 0], 24).unwrap()),
|
|
||||||
IpEntry::Range([172, 16, 0, 0].into(), [172, 32, 255, 255].into()),
|
|
||||||
],
|
|
||||||
family: Family::V4,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
ip_list = "fe80::1/64".parse().expect("valid IP list");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
ip_list,
|
|
||||||
IpList {
|
|
||||||
entries: vec![IpEntry::Cidr(
|
|
||||||
Cidr::new_v6([0xFE80, 0, 0, 0, 0, 0, 0, 1], 64).unwrap()
|
|
||||||
),],
|
|
||||||
family: Family::V6,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
"192.168.0.1,fe80::1".parse::<IpList>().unwrap_err();
|
|
||||||
|
|
||||||
"".parse::<IpList>().unwrap_err();
|
|
||||||
"proxmox".parse::<IpList>().unwrap_err();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_construct_ip_list() {
|
|
||||||
let mut ip_list = IpList::new(vec![Cidr::new_v4([10, 0, 0, 0], 8).unwrap().into()])
|
|
||||||
.expect("valid ip list");
|
|
||||||
|
|
||||||
assert_eq!(ip_list.family(), Family::V4);
|
|
||||||
|
|
||||||
ip_list =
|
|
||||||
IpList::new(vec![Cidr::new_v6([0x000; 8], 8).unwrap().into()]).expect("valid ip list");
|
|
||||||
|
|
||||||
assert_eq!(ip_list.family(), Family::V6);
|
|
||||||
|
|
||||||
IpList::new(vec![]).expect_err("empty ip list is invalid");
|
|
||||||
|
|
||||||
IpList::new(vec![
|
|
||||||
Cidr::new_v4([10, 0, 0, 0], 8).unwrap().into(),
|
|
||||||
Cidr::new_v6([0x0000; 8], 8).unwrap().into(),
|
|
||||||
])
|
|
||||||
.expect_err("cannot mix ip families in ip list");
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,174 +0,0 @@
|
|||||||
use std::fmt::Display;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use anyhow::{bail, format_err, Error};
|
|
||||||
use serde_with::DeserializeFromStr;
|
|
||||||
|
|
||||||
use crate::firewall::parse::{match_name, match_non_whitespace};
|
|
||||||
use crate::firewall::types::address::Cidr;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
pub enum AliasScope {
|
|
||||||
Datacenter,
|
|
||||||
Guest,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for AliasScope {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
Ok(match s {
|
|
||||||
"dc" => AliasScope::Datacenter,
|
|
||||||
"guest" => AliasScope::Guest,
|
|
||||||
_ => bail!("invalid scope for alias: {s}"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for AliasScope {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.write_str(match self {
|
|
||||||
AliasScope::Datacenter => "dc",
|
|
||||||
AliasScope::Guest => "guest",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, DeserializeFromStr)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
pub struct AliasName {
|
|
||||||
scope: AliasScope,
|
|
||||||
name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for AliasName {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.write_fmt(format_args!("{}/{}", self.scope, self.name))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for AliasName {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
match s.split_once('/') {
|
|
||||||
Some((prefix, name)) if !name.is_empty() => Ok(Self {
|
|
||||||
scope: prefix.parse()?,
|
|
||||||
name: name.to_string(),
|
|
||||||
}),
|
|
||||||
_ => {
|
|
||||||
bail!("Invalid Alias name!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AliasName {
|
|
||||||
pub fn new(scope: AliasScope, name: impl Into<String>) -> Self {
|
|
||||||
Self {
|
|
||||||
scope,
|
|
||||||
name: name.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn name(&self) -> &str {
|
|
||||||
&self.name
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn scope(&self) -> &AliasScope {
|
|
||||||
&self.scope
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
pub struct Alias {
|
|
||||||
name: String,
|
|
||||||
address: Cidr,
|
|
||||||
comment: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Alias {
|
|
||||||
pub fn new(
|
|
||||||
name: impl Into<String>,
|
|
||||||
address: impl Into<Cidr>,
|
|
||||||
comment: impl Into<Option<String>>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
name: name.into(),
|
|
||||||
address: address.into(),
|
|
||||||
comment: comment.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn name(&self) -> &str {
|
|
||||||
&self.name
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn address(&self) -> &Cidr {
|
|
||||||
&self.address
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn comment(&self) -> Option<&str> {
|
|
||||||
self.comment.as_deref()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for Alias {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
let (name, line) =
|
|
||||||
match_name(s.trim_start()).ok_or_else(|| format_err!("expected an alias name"))?;
|
|
||||||
|
|
||||||
let (address, line) = match_non_whitespace(line.trim_start())
|
|
||||||
.ok_or_else(|| format_err!("expected a value for alias {name:?}"))?;
|
|
||||||
|
|
||||||
let address: Cidr = address.parse()?;
|
|
||||||
|
|
||||||
let line = line.trim_start();
|
|
||||||
|
|
||||||
let comment = match line.strip_prefix('#') {
|
|
||||||
Some(comment) => Some(comment.trim().to_string()),
|
|
||||||
None if !line.is_empty() => bail!("trailing characters in alias: {line:?}"),
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Alias {
|
|
||||||
name: name.to_string(),
|
|
||||||
address,
|
|
||||||
comment,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_alias() {
|
|
||||||
for alias in [
|
|
||||||
"local_network 10.0.0.0/32",
|
|
||||||
"test-_123-___-a---- 10.0.0.1/32",
|
|
||||||
] {
|
|
||||||
alias.parse::<Alias>().expect("valid alias");
|
|
||||||
}
|
|
||||||
|
|
||||||
for alias in ["-- 10.0.0.1/32", "0asd 10.0.0.1/32", "__test 10.0.0.0/32"] {
|
|
||||||
alias.parse::<Alias>().expect_err("invalid alias");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_alias_name() {
|
|
||||||
for name in ["dc/proxmox_123", "guest/proxmox-123"] {
|
|
||||||
name.parse::<AliasName>().expect("valid alias name");
|
|
||||||
}
|
|
||||||
|
|
||||||
for name in ["proxmox/proxmox_123", "guests/proxmox-123", "dc/", "/name"] {
|
|
||||||
name.parse::<AliasName>().expect_err("invalid alias name");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
use anyhow::Error;
|
|
||||||
|
|
||||||
use crate::firewall::types::Rule;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
pub struct Group {
|
|
||||||
rules: Vec<Rule>,
|
|
||||||
comment: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Group {
|
|
||||||
pub const fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
rules: Vec::new(),
|
|
||||||
comment: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn rules(&self) -> &Vec<Rule> {
|
|
||||||
&self.rules
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn comment(&self) -> Option<&str> {
|
|
||||||
self.comment.as_deref()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_comment(&mut self, comment: Option<String>) {
|
|
||||||
self.comment = comment;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn parse_entry(&mut self, line: &str) -> Result<(), Error> {
|
|
||||||
self.rules.push(line.parse()?);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,349 +0,0 @@
|
|||||||
use core::fmt::Display;
|
|
||||||
use std::ops::{Deref, DerefMut};
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use anyhow::{bail, format_err, Error};
|
|
||||||
use serde_with::DeserializeFromStr;
|
|
||||||
|
|
||||||
use crate::firewall::parse::match_non_whitespace;
|
|
||||||
use crate::firewall::types::address::Cidr;
|
|
||||||
use crate::firewall::types::alias::AliasName;
|
|
||||||
use crate::guest::vm::NetworkConfig;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
|
||||||
pub enum IpsetScope {
|
|
||||||
Datacenter,
|
|
||||||
Guest,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for IpsetScope {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
Ok(match s {
|
|
||||||
"+dc" => IpsetScope::Datacenter,
|
|
||||||
"+guest" => IpsetScope::Guest,
|
|
||||||
_ => bail!("invalid scope for ipset: {s}"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for IpsetScope {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
let prefix = match self {
|
|
||||||
Self::Datacenter => "dc",
|
|
||||||
Self::Guest => "guest",
|
|
||||||
};
|
|
||||||
|
|
||||||
f.write_str(prefix)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, DeserializeFromStr)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
pub struct IpsetName {
|
|
||||||
pub scope: IpsetScope,
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IpsetName {
|
|
||||||
pub fn new(scope: IpsetScope, name: impl Into<String>) -> Self {
|
|
||||||
Self {
|
|
||||||
scope,
|
|
||||||
name: name.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn name(&self) -> &str {
|
|
||||||
&self.name
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn scope(&self) -> IpsetScope {
|
|
||||||
self.scope
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for IpsetName {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
match s.split_once('/') {
|
|
||||||
Some((prefix, name)) if !name.is_empty() => Ok(Self {
|
|
||||||
scope: prefix.parse()?,
|
|
||||||
name: name.to_string(),
|
|
||||||
}),
|
|
||||||
_ => {
|
|
||||||
bail!("Invalid IPSet name: {s}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for IpsetName {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "{}/{}", self.scope, self.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
pub enum IpsetAddress {
|
|
||||||
Alias(AliasName),
|
|
||||||
Cidr(Cidr),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for IpsetAddress {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Error> {
|
|
||||||
if let Ok(cidr) = s.parse() {
|
|
||||||
return Ok(IpsetAddress::Cidr(cidr));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(name) = s.parse() {
|
|
||||||
return Ok(IpsetAddress::Alias(name));
|
|
||||||
}
|
|
||||||
|
|
||||||
bail!("Invalid address in IPSet: {s}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Into<Cidr>> From<T> for IpsetAddress {
|
|
||||||
fn from(cidr: T) -> Self {
|
|
||||||
IpsetAddress::Cidr(cidr.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
pub struct IpsetEntry {
|
|
||||||
pub nomatch: bool,
|
|
||||||
pub address: IpsetAddress,
|
|
||||||
pub comment: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Into<IpsetAddress>> From<T> for IpsetEntry {
|
|
||||||
fn from(value: T) -> Self {
|
|
||||||
Self {
|
|
||||||
nomatch: false,
|
|
||||||
address: value.into(),
|
|
||||||
comment: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for IpsetEntry {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(line: &str) -> Result<Self, Error> {
|
|
||||||
let line = line.trim_start();
|
|
||||||
|
|
||||||
let (nomatch, line) = match line.strip_prefix('!') {
|
|
||||||
Some(line) => (true, line),
|
|
||||||
None => (false, line),
|
|
||||||
};
|
|
||||||
|
|
||||||
let (address, line) =
|
|
||||||
match_non_whitespace(line.trim_start()).ok_or_else(|| format_err!("missing value"))?;
|
|
||||||
|
|
||||||
let address: IpsetAddress = address.parse()?;
|
|
||||||
let line = line.trim_start();
|
|
||||||
|
|
||||||
let comment = match line.strip_prefix('#') {
|
|
||||||
Some(comment) => Some(comment.trim().to_string()),
|
|
||||||
None if !line.is_empty() => bail!("trailing characters in ipset entry: {line:?}"),
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
nomatch,
|
|
||||||
address,
|
|
||||||
comment,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
pub struct Ipfilter<'a> {
|
|
||||||
index: i64,
|
|
||||||
ipset: &'a Ipset,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Ipfilter<'_> {
|
|
||||||
pub fn index(&self) -> i64 {
|
|
||||||
self.index
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ipset(&self) -> &Ipset {
|
|
||||||
self.ipset
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn name_for_index(index: i64) -> String {
|
|
||||||
format!("ipfilter-net{index}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
pub struct Ipset {
|
|
||||||
pub name: IpsetName,
|
|
||||||
set: Vec<IpsetEntry>,
|
|
||||||
pub comment: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Ipset {
|
|
||||||
pub const fn new(name: IpsetName) -> Self {
|
|
||||||
Self {
|
|
||||||
name,
|
|
||||||
set: Vec::new(),
|
|
||||||
comment: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn name(&self) -> &IpsetName {
|
|
||||||
&self.name
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_parts(scope: IpsetScope, name: impl Into<String>) -> Self {
|
|
||||||
Self::new(IpsetName::new(scope, name))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn parse_entry(&mut self, line: &str) -> Result<(), Error> {
|
|
||||||
self.set.push(line.parse()?);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ipfilter(&self) -> Option<Ipfilter> {
|
|
||||||
if self.name.scope() != IpsetScope::Guest {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let name = self.name.name();
|
|
||||||
|
|
||||||
if let Some(key) = name.strip_prefix("ipfilter-") {
|
|
||||||
let id = NetworkConfig::index_from_net_key(key);
|
|
||||||
|
|
||||||
if let Ok(id) = id {
|
|
||||||
return Some(Ipfilter {
|
|
||||||
index: id,
|
|
||||||
ipset: self,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for Ipset {
|
|
||||||
type Target = Vec<IpsetEntry>;
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.set
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DerefMut for Ipset {
|
|
||||||
#[inline]
|
|
||||||
fn deref_mut(&mut self) -> &mut Vec<IpsetEntry> {
|
|
||||||
&mut self.set
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_ipset_name() {
|
|
||||||
for test_case in [
|
|
||||||
("+dc/proxmox-123", IpsetScope::Datacenter, "proxmox-123"),
|
|
||||||
("+guest/proxmox_123", IpsetScope::Guest, "proxmox_123"),
|
|
||||||
] {
|
|
||||||
let ipset_name = test_case.0.parse::<IpsetName>().expect("valid ipset name");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
ipset_name,
|
|
||||||
IpsetName {
|
|
||||||
scope: test_case.1,
|
|
||||||
name: test_case.2.to_string(),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
for name in ["+dc/", "+guests/proxmox_123", "guest/proxmox_123"] {
|
|
||||||
name.parse::<IpsetName>().expect_err("invalid ipset name");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_ipset_address() {
|
|
||||||
let mut ipset_address = "10.0.0.1"
|
|
||||||
.parse::<IpsetAddress>()
|
|
||||||
.expect("valid ipset address");
|
|
||||||
assert!(matches!(ipset_address, IpsetAddress::Cidr(Cidr::Ipv4(..))));
|
|
||||||
|
|
||||||
ipset_address = "fe80::1/64"
|
|
||||||
.parse::<IpsetAddress>()
|
|
||||||
.expect("valid ipset address");
|
|
||||||
assert!(matches!(ipset_address, IpsetAddress::Cidr(Cidr::Ipv6(..))));
|
|
||||||
|
|
||||||
ipset_address = "dc/proxmox-123"
|
|
||||||
.parse::<IpsetAddress>()
|
|
||||||
.expect("valid ipset address");
|
|
||||||
assert!(matches!(ipset_address, IpsetAddress::Alias(..)));
|
|
||||||
|
|
||||||
ipset_address = "guest/proxmox_123"
|
|
||||||
.parse::<IpsetAddress>()
|
|
||||||
.expect("valid ipset address");
|
|
||||||
assert!(matches!(ipset_address, IpsetAddress::Alias(..)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_ipfilter() {
|
|
||||||
let mut ipset = Ipset::from_parts(IpsetScope::Guest, "ipfilter-net0");
|
|
||||||
ipset.ipfilter().expect("is an ipfilter");
|
|
||||||
|
|
||||||
ipset = Ipset::from_parts(IpsetScope::Guest, "ipfilter-qwe");
|
|
||||||
assert!(ipset.ipfilter().is_none());
|
|
||||||
|
|
||||||
ipset = Ipset::from_parts(IpsetScope::Guest, "proxmox");
|
|
||||||
assert!(ipset.ipfilter().is_none());
|
|
||||||
|
|
||||||
ipset = Ipset::from_parts(IpsetScope::Datacenter, "ipfilter-net0");
|
|
||||||
assert!(ipset.ipfilter().is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_ipset_entry() {
|
|
||||||
let mut entry = "!10.0.0.1 # qweqweasd"
|
|
||||||
.parse::<IpsetEntry>()
|
|
||||||
.expect("valid ipset entry");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
entry,
|
|
||||||
IpsetEntry {
|
|
||||||
nomatch: true,
|
|
||||||
comment: Some("qweqweasd".to_string()),
|
|
||||||
address: IpsetAddress::Cidr(Cidr::new_v4([10, 0, 0, 1], 32).unwrap())
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
entry = "fe80::1/48"
|
|
||||||
.parse::<IpsetEntry>()
|
|
||||||
.expect("valid ipset entry");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
entry,
|
|
||||||
IpsetEntry {
|
|
||||||
nomatch: false,
|
|
||||||
comment: None,
|
|
||||||
address: IpsetAddress::Cidr(
|
|
||||||
Cidr::new_v6([0xFE80, 0, 0, 0, 0, 0, 0, 1], 48).unwrap()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,222 +0,0 @@
|
|||||||
use std::fmt;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use crate::firewall::parse::parse_bool;
|
|
||||||
use anyhow::{bail, Error};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Deserialize, Serialize, Default)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
#[serde(rename_all = "lowercase")]
|
|
||||||
pub enum LogRateLimitTimescale {
|
|
||||||
#[default]
|
|
||||||
Second,
|
|
||||||
Minute,
|
|
||||||
Hour,
|
|
||||||
Day,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for LogRateLimitTimescale {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(str: &str) -> Result<Self, Error> {
|
|
||||||
match str {
|
|
||||||
"second" => Ok(LogRateLimitTimescale::Second),
|
|
||||||
"minute" => Ok(LogRateLimitTimescale::Minute),
|
|
||||||
"hour" => Ok(LogRateLimitTimescale::Hour),
|
|
||||||
"day" => Ok(LogRateLimitTimescale::Day),
|
|
||||||
_ => bail!("Invalid time scale provided"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
pub struct LogRateLimit {
|
|
||||||
enabled: bool,
|
|
||||||
rate: i64, // in packets
|
|
||||||
per: LogRateLimitTimescale,
|
|
||||||
burst: i64, // in packets
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LogRateLimit {
|
|
||||||
pub fn new(enabled: bool, rate: i64, per: LogRateLimitTimescale, burst: i64) -> Self {
|
|
||||||
Self {
|
|
||||||
enabled,
|
|
||||||
rate,
|
|
||||||
per,
|
|
||||||
burst,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn enabled(&self) -> bool {
|
|
||||||
self.enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn rate(&self) -> i64 {
|
|
||||||
self.rate
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn burst(&self) -> i64 {
|
|
||||||
self.burst
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn per(&self) -> LogRateLimitTimescale {
|
|
||||||
self.per
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for LogRateLimit {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
enabled: true,
|
|
||||||
rate: 1,
|
|
||||||
burst: 5,
|
|
||||||
per: LogRateLimitTimescale::Second,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for LogRateLimit {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(str: &str) -> Result<Self, Error> {
|
|
||||||
let mut limit = Self::default();
|
|
||||||
|
|
||||||
for element in str.split(',') {
|
|
||||||
match element.split_once('=') {
|
|
||||||
None => {
|
|
||||||
limit.enabled = parse_bool(element)?;
|
|
||||||
}
|
|
||||||
Some((key, value)) if !key.is_empty() && !value.is_empty() => match key {
|
|
||||||
"enable" => limit.enabled = parse_bool(value)?,
|
|
||||||
"burst" => limit.burst = i64::from_str(value)?,
|
|
||||||
"rate" => match value.split_once('/') {
|
|
||||||
None => {
|
|
||||||
limit.rate = i64::from_str(value)?;
|
|
||||||
}
|
|
||||||
Some((rate, unit)) => {
|
|
||||||
if unit.is_empty() {
|
|
||||||
bail!("empty unit specification")
|
|
||||||
}
|
|
||||||
|
|
||||||
limit.rate = i64::from_str(rate)?;
|
|
||||||
limit.per = LogRateLimitTimescale::from_str(unit)?;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_ => bail!("Invalid value for Key found in log_ratelimit!"),
|
|
||||||
},
|
|
||||||
_ => bail!("invalid value in log_ratelimit"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(limit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Default)]
|
|
||||||
pub enum LogLevel {
|
|
||||||
#[default]
|
|
||||||
Nolog,
|
|
||||||
Emergency,
|
|
||||||
Alert,
|
|
||||||
Critical,
|
|
||||||
Error,
|
|
||||||
Warning,
|
|
||||||
Notice,
|
|
||||||
Info,
|
|
||||||
Debug,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::str::FromStr for LogLevel {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Error> {
|
|
||||||
Ok(match s {
|
|
||||||
"nolog" => LogLevel::Nolog,
|
|
||||||
"emerg" => LogLevel::Emergency,
|
|
||||||
"alert" => LogLevel::Alert,
|
|
||||||
"crit" => LogLevel::Critical,
|
|
||||||
"err" => LogLevel::Error,
|
|
||||||
"warn" => LogLevel::Warning,
|
|
||||||
"warning" => LogLevel::Warning,
|
|
||||||
"notice" => LogLevel::Notice,
|
|
||||||
"info" => LogLevel::Info,
|
|
||||||
"debug" => LogLevel::Debug,
|
|
||||||
_ => bail!("invalid log level {s:?}"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for LogLevel {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
f.write_str(match self {
|
|
||||||
LogLevel::Nolog => "nolog",
|
|
||||||
LogLevel::Emergency => "emerg",
|
|
||||||
LogLevel::Alert => "alert",
|
|
||||||
LogLevel::Critical => "crit",
|
|
||||||
LogLevel::Error => "err",
|
|
||||||
LogLevel::Warning => "warn",
|
|
||||||
LogLevel::Notice => "notice",
|
|
||||||
LogLevel::Info => "info",
|
|
||||||
LogLevel::Debug => "debug",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
serde_plain::derive_deserialize_from_fromstr!(LogLevel, "valid log level");
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_rate_limit() {
|
|
||||||
let mut parsed_rate_limit = "1,burst=123,rate=44"
|
|
||||||
.parse::<LogRateLimit>()
|
|
||||||
.expect("valid rate limit");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
parsed_rate_limit,
|
|
||||||
LogRateLimit {
|
|
||||||
enabled: true,
|
|
||||||
burst: 123,
|
|
||||||
rate: 44,
|
|
||||||
per: LogRateLimitTimescale::Second,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
parsed_rate_limit = "1".parse::<LogRateLimit>().expect("valid rate limit");
|
|
||||||
|
|
||||||
assert_eq!(parsed_rate_limit, LogRateLimit::default());
|
|
||||||
|
|
||||||
parsed_rate_limit = "enable=0,rate=123/hour"
|
|
||||||
.parse::<LogRateLimit>()
|
|
||||||
.expect("valid rate limit");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
parsed_rate_limit,
|
|
||||||
LogRateLimit {
|
|
||||||
enabled: false,
|
|
||||||
burst: 5,
|
|
||||||
rate: 123,
|
|
||||||
per: LogRateLimitTimescale::Hour,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
"2".parse::<LogRateLimit>()
|
|
||||||
.expect_err("invalid value for enable");
|
|
||||||
|
|
||||||
"enabled=0,rate=123"
|
|
||||||
.parse::<LogRateLimit>()
|
|
||||||
.expect_err("invalid key in log ratelimit");
|
|
||||||
|
|
||||||
"enable=0,rate=123,"
|
|
||||||
.parse::<LogRateLimit>()
|
|
||||||
.expect_err("trailing comma in log rate limit specification");
|
|
||||||
|
|
||||||
"enable=0,rate=123/proxmox,"
|
|
||||||
.parse::<LogRateLimit>()
|
|
||||||
.expect_err("invalid unit for rate");
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
pub mod address;
|
|
||||||
pub mod alias;
|
|
||||||
pub mod group;
|
|
||||||
pub mod ipset;
|
|
||||||
pub mod log;
|
|
||||||
pub mod port;
|
|
||||||
pub mod rule;
|
|
||||||
pub mod rule_match;
|
|
||||||
|
|
||||||
pub use address::Cidr;
|
|
||||||
pub use alias::Alias;
|
|
||||||
pub use group::Group;
|
|
||||||
pub use ipset::Ipset;
|
|
||||||
pub use rule::Rule;
|
|
@ -1,181 +0,0 @@
|
|||||||
use std::fmt;
|
|
||||||
use std::ops::Deref;
|
|
||||||
|
|
||||||
use anyhow::{bail, Error};
|
|
||||||
use serde_with::DeserializeFromStr;
|
|
||||||
|
|
||||||
use crate::firewall::ports::parse_named_port;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
pub enum PortEntry {
|
|
||||||
Port(u16),
|
|
||||||
Range(u16, u16),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for PortEntry {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
Self::Port(p) => write!(f, "{p}"),
|
|
||||||
Self::Range(beg, end) => write!(f, "{beg}-{end}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_port(port: &str) -> Result<u16, Error> {
|
|
||||||
if let Ok(port) = port.parse::<u16>() {
|
|
||||||
return Ok(port);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(port) = parse_named_port(port) {
|
|
||||||
return Ok(port);
|
|
||||||
}
|
|
||||||
|
|
||||||
bail!("invalid port specification: {port}")
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::str::FromStr for PortEntry {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
Ok(match s.trim().split_once(':') {
|
|
||||||
None => PortEntry::from(parse_port(s)?),
|
|
||||||
Some((first, second)) => {
|
|
||||||
PortEntry::try_from((parse_port(first)?, parse_port(second)?))?
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<u16> for PortEntry {
|
|
||||||
fn from(port: u16) -> Self {
|
|
||||||
PortEntry::Port(port)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<(u16, u16)> for PortEntry {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(ports: (u16, u16)) -> Result<Self, Error> {
|
|
||||||
if ports.0 > ports.1 {
|
|
||||||
bail!("start port is greater than end port!");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(PortEntry::Range(ports.0, ports.1))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, DeserializeFromStr)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
pub struct PortList(pub(crate) Vec<PortEntry>);
|
|
||||||
|
|
||||||
impl FromIterator<PortEntry> for PortList {
|
|
||||||
fn from_iter<T: IntoIterator<Item = PortEntry>>(iter: T) -> Self {
|
|
||||||
Self(iter.into_iter().collect())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Into<PortEntry>> From<T> for PortList {
|
|
||||||
fn from(value: T) -> Self {
|
|
||||||
Self(vec![value.into()])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for PortList {
|
|
||||||
type Target = Vec<PortEntry>;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::str::FromStr for PortList {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Error> {
|
|
||||||
if s.is_empty() {
|
|
||||||
bail!("empty port specification");
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut entries = Vec::new();
|
|
||||||
|
|
||||||
for entry in s.trim().split(',') {
|
|
||||||
entries.push(entry.parse()?);
|
|
||||||
}
|
|
||||||
|
|
||||||
if entries.is_empty() {
|
|
||||||
bail!("invalid empty port list");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Self(entries))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for PortList {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
use fmt::Write;
|
|
||||||
if self.0.len() > 1 {
|
|
||||||
f.write_char('{')?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut comma = '\0';
|
|
||||||
for entry in &self.0 {
|
|
||||||
if std::mem::replace(&mut comma, ',') != '\0' {
|
|
||||||
f.write_char(comma)?;
|
|
||||||
}
|
|
||||||
fmt::Display::fmt(entry, f)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.0.len() > 1 {
|
|
||||||
f.write_char('}')?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_port_entry() {
|
|
||||||
let mut port_entry: PortEntry = "12345".parse().expect("valid port entry");
|
|
||||||
assert_eq!(port_entry, PortEntry::from(12345));
|
|
||||||
|
|
||||||
port_entry = "0:65535".parse().expect("valid port entry");
|
|
||||||
assert_eq!(port_entry, PortEntry::try_from((0, 65535)).unwrap());
|
|
||||||
|
|
||||||
"65536".parse::<PortEntry>().unwrap_err();
|
|
||||||
"100:100000".parse::<PortEntry>().unwrap_err();
|
|
||||||
"qweasd".parse::<PortEntry>().unwrap_err();
|
|
||||||
"".parse::<PortEntry>().unwrap_err();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_port_list() {
|
|
||||||
let mut port_list: PortList = "12345".parse().expect("valid port list");
|
|
||||||
assert_eq!(port_list, PortList::from(12345));
|
|
||||||
|
|
||||||
port_list = "12345,0:65535,1337,ssh:80,https"
|
|
||||||
.parse()
|
|
||||||
.expect("valid port list");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
port_list,
|
|
||||||
PortList(vec![
|
|
||||||
PortEntry::from(12345),
|
|
||||||
PortEntry::try_from((0, 65535)).unwrap(),
|
|
||||||
PortEntry::from(1337),
|
|
||||||
PortEntry::try_from((22, 80)).unwrap(),
|
|
||||||
PortEntry::from(443),
|
|
||||||
])
|
|
||||||
);
|
|
||||||
|
|
||||||
"0::1337".parse::<PortList>().unwrap_err();
|
|
||||||
"0:1337,".parse::<PortList>().unwrap_err();
|
|
||||||
"70000".parse::<PortList>().unwrap_err();
|
|
||||||
"qweasd".parse::<PortList>().unwrap_err();
|
|
||||||
"".parse::<PortList>().unwrap_err();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,412 +0,0 @@
|
|||||||
use core::fmt::Display;
|
|
||||||
use std::fmt;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use anyhow::{bail, ensure, format_err, Error};
|
|
||||||
|
|
||||||
use crate::firewall::parse::match_name;
|
|
||||||
use crate::firewall::types::rule_match::RuleMatch;
|
|
||||||
use crate::firewall::types::rule_match::RuleOptions;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
|
||||||
pub enum Direction {
|
|
||||||
#[default]
|
|
||||||
In,
|
|
||||||
Out,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::str::FromStr for Direction {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Error> {
|
|
||||||
for (name, dir) in [("IN", Direction::In), ("OUT", Direction::Out)] {
|
|
||||||
if s.eq_ignore_ascii_case(name) {
|
|
||||||
return Ok(dir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bail!("invalid direction: {s:?}, expect 'IN' or 'OUT'");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
serde_plain::derive_deserialize_from_fromstr!(Direction, "valid packet direction");
|
|
||||||
|
|
||||||
impl fmt::Display for Direction {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
Direction::In => f.write_str("in"),
|
|
||||||
Direction::Out => f.write_str("out"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
|
||||||
pub enum Verdict {
|
|
||||||
Accept,
|
|
||||||
Reject,
|
|
||||||
#[default]
|
|
||||||
Drop,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::str::FromStr for Verdict {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Error> {
|
|
||||||
for (name, verdict) in [
|
|
||||||
("ACCEPT", Verdict::Accept),
|
|
||||||
("REJECT", Verdict::Reject),
|
|
||||||
("DROP", Verdict::Drop),
|
|
||||||
] {
|
|
||||||
if s.eq_ignore_ascii_case(name) {
|
|
||||||
return Ok(verdict);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
bail!("invalid verdict {s:?}, expected one of 'ACCEPT', 'REJECT' or 'DROP'");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Verdict {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
let string = match self {
|
|
||||||
Verdict::Accept => "ACCEPT",
|
|
||||||
Verdict::Drop => "DROP",
|
|
||||||
Verdict::Reject => "REJECT",
|
|
||||||
};
|
|
||||||
|
|
||||||
write!(f, "{string}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
serde_plain::derive_deserialize_from_fromstr!(Verdict, "valid verdict");
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
pub struct Rule {
|
|
||||||
pub(crate) disabled: bool,
|
|
||||||
pub(crate) kind: Kind,
|
|
||||||
pub(crate) comment: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::ops::Deref for Rule {
|
|
||||||
type Target = Kind;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.kind
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::ops::DerefMut for Rule {
|
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
||||||
&mut self.kind
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for Rule {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
|
||||||
if input.contains(['\n', '\r']) {
|
|
||||||
bail!("rule must not contain any newlines!");
|
|
||||||
}
|
|
||||||
|
|
||||||
let (line, comment) = match input.rsplit_once('#') {
|
|
||||||
Some((line, comment)) if !comment.is_empty() => (line.trim(), Some(comment.trim())),
|
|
||||||
_ => (input.trim(), None),
|
|
||||||
};
|
|
||||||
|
|
||||||
let (disabled, line) = match line.strip_prefix('|') {
|
|
||||||
Some(line) => (true, line.trim_start()),
|
|
||||||
None => (false, line),
|
|
||||||
};
|
|
||||||
|
|
||||||
// todo: case insensitive?
|
|
||||||
let kind = if line.starts_with("GROUP") {
|
|
||||||
Kind::from(line.parse::<RuleGroup>()?)
|
|
||||||
} else {
|
|
||||||
Kind::from(line.parse::<RuleMatch>()?)
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
disabled,
|
|
||||||
comment: comment.map(str::to_string),
|
|
||||||
kind,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Rule {
|
|
||||||
pub fn iface(&self) -> Option<&str> {
|
|
||||||
match &self.kind {
|
|
||||||
Kind::Group(group) => group.iface(),
|
|
||||||
Kind::Match(rule) => rule.iface(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn disabled(&self) -> bool {
|
|
||||||
self.disabled
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn kind(&self) -> &Kind {
|
|
||||||
&self.kind
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn comment(&self) -> Option<&str> {
|
|
||||||
self.comment.as_deref()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
pub enum Kind {
|
|
||||||
Group(RuleGroup),
|
|
||||||
Match(RuleMatch),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Kind {
|
|
||||||
pub fn is_group(&self) -> bool {
|
|
||||||
matches!(self, Kind::Group(_))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_match(&self) -> bool {
|
|
||||||
matches!(self, Kind::Match(_))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<RuleGroup> for Kind {
|
|
||||||
fn from(value: RuleGroup) -> Self {
|
|
||||||
Kind::Group(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<RuleMatch> for Kind {
|
|
||||||
fn from(value: RuleMatch) -> Self {
|
|
||||||
Kind::Match(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
pub struct RuleGroup {
|
|
||||||
pub(crate) group: String,
|
|
||||||
pub(crate) iface: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RuleGroup {
|
|
||||||
pub(crate) fn from_options(group: String, options: RuleOptions) -> Result<Self, Error> {
|
|
||||||
ensure!(
|
|
||||||
options.proto.is_none()
|
|
||||||
&& options.dport.is_none()
|
|
||||||
&& options.sport.is_none()
|
|
||||||
&& options.dest.is_none()
|
|
||||||
&& options.source.is_none()
|
|
||||||
&& options.log.is_none()
|
|
||||||
&& options.icmp_type.is_none(),
|
|
||||||
"only interface parameter is permitted for group rules"
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
group,
|
|
||||||
iface: options.iface,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn group(&self) -> &str {
|
|
||||||
&self.group
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn iface(&self) -> Option<&str> {
|
|
||||||
self.iface.as_deref()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for RuleGroup {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
|
||||||
let (keyword, rest) = match_name(input)
|
|
||||||
.ok_or_else(|| format_err!("expected a leading keyword in rule group"))?;
|
|
||||||
|
|
||||||
if !keyword.eq_ignore_ascii_case("group") {
|
|
||||||
bail!("Expected keyword GROUP")
|
|
||||||
}
|
|
||||||
|
|
||||||
let (name, rest) =
|
|
||||||
match_name(rest.trim()).ok_or_else(|| format_err!("expected a name for rule group"))?;
|
|
||||||
|
|
||||||
let options = rest.trim_start().parse()?;
|
|
||||||
|
|
||||||
Self::from_options(name.to_string(), options)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use crate::firewall::types::{
|
|
||||||
address::{IpEntry, IpList},
|
|
||||||
alias::{AliasName, AliasScope},
|
|
||||||
ipset::{IpsetName, IpsetScope},
|
|
||||||
log::LogLevel,
|
|
||||||
rule_match::{Icmp, IcmpCode, IpAddrMatch, IpMatch, Ports, Protocol, Udp},
|
|
||||||
Cidr,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_rule() {
|
|
||||||
let mut rule: Rule = "|GROUP tgr -i eth0 # acomm".parse().expect("valid rule");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
rule,
|
|
||||||
Rule {
|
|
||||||
disabled: true,
|
|
||||||
comment: Some("acomm".to_string()),
|
|
||||||
kind: Kind::Group(RuleGroup {
|
|
||||||
group: "tgr".to_string(),
|
|
||||||
iface: Some("eth0".to_string()),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
rule = "IN ACCEPT -p udp -dport 33 -sport 22 -log warning"
|
|
||||||
.parse()
|
|
||||||
.expect("valid rule");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
rule,
|
|
||||||
Rule {
|
|
||||||
disabled: false,
|
|
||||||
comment: None,
|
|
||||||
kind: Kind::Match(RuleMatch {
|
|
||||||
dir: Direction::In,
|
|
||||||
verdict: Verdict::Accept,
|
|
||||||
proto: Some(Udp::new(Ports::from_u16(22, 33)).into()),
|
|
||||||
log: Some(LogLevel::Warning),
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
rule = "IN ACCEPT --proto udp -i eth0".parse().expect("valid rule");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
rule,
|
|
||||||
Rule {
|
|
||||||
disabled: false,
|
|
||||||
comment: None,
|
|
||||||
kind: Kind::Match(RuleMatch {
|
|
||||||
dir: Direction::In,
|
|
||||||
verdict: Verdict::Accept,
|
|
||||||
proto: Some(Udp::new(Ports::new(None, None)).into()),
|
|
||||||
iface: Some("eth0".to_string()),
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
rule = " OUT DROP \
|
|
||||||
-source 10.0.0.0/24 -dest 20.0.0.0-20.255.255.255,192.168.0.0/16 \
|
|
||||||
-p icmp -log nolog -icmp-type port-unreachable "
|
|
||||||
.parse()
|
|
||||||
.expect("valid rule");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
rule,
|
|
||||||
Rule {
|
|
||||||
disabled: false,
|
|
||||||
comment: None,
|
|
||||||
kind: Kind::Match(RuleMatch {
|
|
||||||
dir: Direction::Out,
|
|
||||||
verdict: Verdict::Drop,
|
|
||||||
ip: IpMatch::new(
|
|
||||||
IpAddrMatch::Ip(IpList::from(Cidr::new_v4([10, 0, 0, 0], 24).unwrap())),
|
|
||||||
IpAddrMatch::Ip(
|
|
||||||
IpList::new(vec![
|
|
||||||
IpEntry::Range([20, 0, 0, 0].into(), [20, 255, 255, 255].into()),
|
|
||||||
IpEntry::Cidr(Cidr::new_v4([192, 168, 0, 0], 16).unwrap()),
|
|
||||||
])
|
|
||||||
.unwrap()
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.ok(),
|
|
||||||
proto: Some(Protocol::Icmp(Icmp::new_code(IcmpCode::Named(
|
|
||||||
"port-unreachable"
|
|
||||||
)))),
|
|
||||||
log: Some(LogLevel::Nolog),
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
rule = "IN BGP(ACCEPT) --log crit --iface eth0"
|
|
||||||
.parse()
|
|
||||||
.expect("valid rule");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
rule,
|
|
||||||
Rule {
|
|
||||||
disabled: false,
|
|
||||||
comment: None,
|
|
||||||
kind: Kind::Match(RuleMatch {
|
|
||||||
dir: Direction::In,
|
|
||||||
verdict: Verdict::Accept,
|
|
||||||
log: Some(LogLevel::Critical),
|
|
||||||
fw_macro: Some("BGP".to_string()),
|
|
||||||
iface: Some("eth0".to_string()),
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
rule = "IN ACCEPT --source dc/test --dest +dc/test"
|
|
||||||
.parse()
|
|
||||||
.expect("valid rule");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
rule,
|
|
||||||
Rule {
|
|
||||||
disabled: false,
|
|
||||||
comment: None,
|
|
||||||
kind: Kind::Match(RuleMatch {
|
|
||||||
dir: Direction::In,
|
|
||||||
verdict: Verdict::Accept,
|
|
||||||
ip: Some(
|
|
||||||
IpMatch::new(
|
|
||||||
IpAddrMatch::Alias(AliasName::new(AliasScope::Datacenter, "test")),
|
|
||||||
IpAddrMatch::Set(IpsetName::new(IpsetScope::Datacenter, "test"),),
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
),
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
rule = "IN REJECT".parse().expect("valid rule");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
rule,
|
|
||||||
Rule {
|
|
||||||
disabled: false,
|
|
||||||
comment: None,
|
|
||||||
kind: Kind::Match(RuleMatch {
|
|
||||||
dir: Direction::In,
|
|
||||||
verdict: Verdict::Reject,
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
"IN DROP ---log crit"
|
|
||||||
.parse::<Rule>()
|
|
||||||
.expect_err("too many dashes in option");
|
|
||||||
|
|
||||||
"IN DROP --log --iface eth0"
|
|
||||||
.parse::<Rule>()
|
|
||||||
.expect_err("no value for option");
|
|
||||||
|
|
||||||
"IN DROP --log crit --iface"
|
|
||||||
.parse::<Rule>()
|
|
||||||
.expect_err("no value for option");
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,977 +0,0 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
use std::fmt;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
use anyhow::{bail, format_err, Error};
|
|
||||||
use serde::de::IntoDeserializer;
|
|
||||||
|
|
||||||
use proxmox_sortable_macro::sortable;
|
|
||||||
|
|
||||||
use crate::firewall::parse::{match_name, match_non_whitespace, SomeStr};
|
|
||||||
use crate::firewall::types::address::{Family, IpList};
|
|
||||||
use crate::firewall::types::alias::AliasName;
|
|
||||||
use crate::firewall::types::ipset::IpsetName;
|
|
||||||
use crate::firewall::types::log::LogLevel;
|
|
||||||
use crate::firewall::types::port::PortList;
|
|
||||||
use crate::firewall::types::rule::{Direction, Verdict};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Deserialize)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
|
|
||||||
pub(crate) struct RuleOptions {
|
|
||||||
#[serde(alias = "p")]
|
|
||||||
pub(crate) proto: Option<String>,
|
|
||||||
|
|
||||||
pub(crate) dport: Option<String>,
|
|
||||||
pub(crate) sport: Option<String>,
|
|
||||||
|
|
||||||
pub(crate) dest: Option<String>,
|
|
||||||
pub(crate) source: Option<String>,
|
|
||||||
|
|
||||||
#[serde(alias = "i")]
|
|
||||||
pub(crate) iface: Option<String>,
|
|
||||||
|
|
||||||
pub(crate) log: Option<LogLevel>,
|
|
||||||
pub(crate) icmp_type: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for RuleOptions {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(mut line: &str) -> Result<Self, Self::Err> {
|
|
||||||
let mut options = HashMap::new();
|
|
||||||
|
|
||||||
loop {
|
|
||||||
line = line.trim_start();
|
|
||||||
|
|
||||||
if line.is_empty() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
line = line
|
|
||||||
.strip_prefix('-')
|
|
||||||
.ok_or_else(|| format_err!("expected an option starting with '-'"))?;
|
|
||||||
|
|
||||||
// second dash is optional
|
|
||||||
line = line.strip_prefix('-').unwrap_or(line);
|
|
||||||
|
|
||||||
let param;
|
|
||||||
(param, line) = match_name(line)
|
|
||||||
.ok_or_else(|| format_err!("expected a parameter name after '-'"))?;
|
|
||||||
|
|
||||||
let value;
|
|
||||||
(value, line) = match_non_whitespace(line.trim_start())
|
|
||||||
.ok_or_else(|| format_err!("expected a value for {param:?}"))?;
|
|
||||||
|
|
||||||
if options.insert(param, SomeStr(value)).is_some() {
|
|
||||||
bail!("duplicate option in rule: {param}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(RuleOptions::deserialize(IntoDeserializer::<
|
|
||||||
'_,
|
|
||||||
crate::firewall::parse::SerdeStringError,
|
|
||||||
>::into_deserializer(
|
|
||||||
options
|
|
||||||
))?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
pub struct RuleMatch {
|
|
||||||
pub(crate) dir: Direction,
|
|
||||||
pub(crate) verdict: Verdict,
|
|
||||||
pub(crate) fw_macro: Option<String>,
|
|
||||||
|
|
||||||
pub(crate) iface: Option<String>,
|
|
||||||
pub(crate) log: Option<LogLevel>,
|
|
||||||
pub(crate) ip: Option<IpMatch>,
|
|
||||||
pub(crate) proto: Option<Protocol>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RuleMatch {
|
|
||||||
pub(crate) fn from_options(
|
|
||||||
dir: Direction,
|
|
||||||
verdict: Verdict,
|
|
||||||
fw_macro: impl Into<Option<String>>,
|
|
||||||
options: RuleOptions,
|
|
||||||
) -> Result<Self, Error> {
|
|
||||||
if options.dport.is_some() && options.icmp_type.is_some() {
|
|
||||||
bail!("dport and icmp-type are mutually exclusive");
|
|
||||||
}
|
|
||||||
|
|
||||||
let ip = IpMatch::from_options(&options)?;
|
|
||||||
let proto = Protocol::from_options(&options)?;
|
|
||||||
|
|
||||||
// todo: check protocol & IP Version compatibility
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
dir,
|
|
||||||
verdict,
|
|
||||||
fw_macro: fw_macro.into(),
|
|
||||||
iface: options.iface,
|
|
||||||
log: options.log,
|
|
||||||
ip,
|
|
||||||
proto,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn direction(&self) -> Direction {
|
|
||||||
self.dir
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn iface(&self) -> Option<&str> {
|
|
||||||
self.iface.as_deref()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn verdict(&self) -> Verdict {
|
|
||||||
self.verdict
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fw_macro(&self) -> Option<&str> {
|
|
||||||
self.fw_macro.as_deref()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn log(&self) -> Option<LogLevel> {
|
|
||||||
self.log
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ip(&self) -> Option<&IpMatch> {
|
|
||||||
self.ip.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn proto(&self) -> Option<&Protocol> {
|
|
||||||
self.proto.as_ref()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns `(Macro name, Verdict, RestOfTheLine)`.
|
|
||||||
fn parse_action(line: &str) -> Result<(Option<&str>, Verdict, &str), Error> {
|
|
||||||
let (verdict, line) =
|
|
||||||
match_name(line).ok_or_else(|| format_err!("expected a verdict or macro name"))?;
|
|
||||||
|
|
||||||
Ok(if let Some(line) = line.strip_prefix('(') {
|
|
||||||
// <macro>(<verdict>)
|
|
||||||
|
|
||||||
let macro_name = verdict;
|
|
||||||
let (verdict, line) = match_name(line).ok_or_else(|| format_err!("expected a verdict"))?;
|
|
||||||
let line = line
|
|
||||||
.strip_prefix(')')
|
|
||||||
.ok_or_else(|| format_err!("expected closing ')' after verdict"))?;
|
|
||||||
|
|
||||||
let verdict: Verdict = verdict.parse()?;
|
|
||||||
|
|
||||||
(Some(macro_name), verdict, line.trim_start())
|
|
||||||
} else {
|
|
||||||
(None, verdict.parse()?, line.trim_start())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for RuleMatch {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(line: &str) -> Result<Self, Self::Err> {
|
|
||||||
let (dir, rest) = match_name(line).ok_or_else(|| format_err!("expected a direction"))?;
|
|
||||||
|
|
||||||
let direction: Direction = dir.parse()?;
|
|
||||||
|
|
||||||
let (fw_macro, verdict, rest) = parse_action(rest.trim_start())?;
|
|
||||||
|
|
||||||
let options: RuleOptions = rest.trim_start().parse()?;
|
|
||||||
|
|
||||||
Self::from_options(direction, verdict, fw_macro.map(str::to_string), options)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
pub struct IpMatch {
|
|
||||||
pub(crate) src: Option<IpAddrMatch>,
|
|
||||||
pub(crate) dst: Option<IpAddrMatch>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IpMatch {
|
|
||||||
pub fn new(
|
|
||||||
src: impl Into<Option<IpAddrMatch>>,
|
|
||||||
dst: impl Into<Option<IpAddrMatch>>,
|
|
||||||
) -> Result<Self, Error> {
|
|
||||||
let source = src.into();
|
|
||||||
let dest = dst.into();
|
|
||||||
|
|
||||||
if source.is_none() && dest.is_none() {
|
|
||||||
bail!("either src or dst must be set")
|
|
||||||
}
|
|
||||||
|
|
||||||
if let (Some(IpAddrMatch::Ip(src)), Some(IpAddrMatch::Ip(dst))) = (&source, &dest) {
|
|
||||||
if src.family() != dst.family() {
|
|
||||||
bail!("src and dst family must be equal")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let ip_match = Self {
|
|
||||||
src: source,
|
|
||||||
dst: dest,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(ip_match)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_options(options: &RuleOptions) -> Result<Option<Self>, Error> {
|
|
||||||
let src = options
|
|
||||||
.source
|
|
||||||
.as_ref()
|
|
||||||
.map(|elem| elem.parse::<IpAddrMatch>())
|
|
||||||
.transpose()?;
|
|
||||||
|
|
||||||
let dst = options
|
|
||||||
.dest
|
|
||||||
.as_ref()
|
|
||||||
.map(|elem| elem.parse::<IpAddrMatch>())
|
|
||||||
.transpose()?;
|
|
||||||
|
|
||||||
if src.is_some() || dst.is_some() {
|
|
||||||
Ok(Some(IpMatch::new(src, dst)?))
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn src(&self) -> Option<&IpAddrMatch> {
|
|
||||||
self.src.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn dst(&self) -> Option<&IpAddrMatch> {
|
|
||||||
self.dst.as_ref()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
pub enum IpAddrMatch {
|
|
||||||
Ip(IpList),
|
|
||||||
Set(IpsetName),
|
|
||||||
Alias(AliasName),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IpAddrMatch {
|
|
||||||
pub fn family(&self) -> Option<Family> {
|
|
||||||
if let IpAddrMatch::Ip(list) = self {
|
|
||||||
return Some(list.family());
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for IpAddrMatch {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(value: &str) -> Result<Self, Error> {
|
|
||||||
if value.is_empty() {
|
|
||||||
bail!("empty IP specification");
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(ip_list) = value.parse() {
|
|
||||||
return Ok(IpAddrMatch::Ip(ip_list));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(ipset) = value.parse() {
|
|
||||||
return Ok(IpAddrMatch::Set(ipset));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(name) = value.parse() {
|
|
||||||
return Ok(IpAddrMatch::Alias(name));
|
|
||||||
}
|
|
||||||
|
|
||||||
bail!("invalid IP specification: {value}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
pub enum Protocol {
|
|
||||||
Dccp(Ports),
|
|
||||||
Sctp(Sctp),
|
|
||||||
Tcp(Tcp),
|
|
||||||
Udp(Udp),
|
|
||||||
UdpLite(Ports),
|
|
||||||
Icmp(Icmp),
|
|
||||||
Icmpv6(Icmpv6),
|
|
||||||
Named(String),
|
|
||||||
Numeric(u8),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Protocol {
|
|
||||||
pub(crate) fn from_options(options: &RuleOptions) -> Result<Option<Self>, Error> {
|
|
||||||
let proto = match options.proto.as_deref() {
|
|
||||||
Some(p) => p,
|
|
||||||
None => return Ok(None),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Some(match proto {
|
|
||||||
"dccp" | "33" => Protocol::Dccp(Ports::from_options(options)?),
|
|
||||||
"sctp" | "132" => Protocol::Sctp(Sctp::from_options(options)?),
|
|
||||||
"tcp" | "6" => Protocol::Tcp(Tcp::from_options(options)?),
|
|
||||||
"udp" | "17" => Protocol::Udp(Udp::from_options(options)?),
|
|
||||||
"udplite" | "136" => Protocol::UdpLite(Ports::from_options(options)?),
|
|
||||||
"icmp" | "1" => Protocol::Icmp(Icmp::from_options(options)?),
|
|
||||||
"ipv6-icmp" | "icmpv6" | "58" => Protocol::Icmpv6(Icmpv6::from_options(options)?),
|
|
||||||
other => match other.parse::<u8>() {
|
|
||||||
Ok(num) => Protocol::Numeric(num),
|
|
||||||
Err(_) => Protocol::Named(other.to_string()),
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn family(&self) -> Option<Family> {
|
|
||||||
match self {
|
|
||||||
Self::Icmp(_) => Some(Family::V4),
|
|
||||||
Self::Icmpv6(_) => Some(Family::V6),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
pub struct Udp {
|
|
||||||
ports: Ports,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Udp {
|
|
||||||
fn from_options(options: &RuleOptions) -> Result<Self, Error> {
|
|
||||||
Ok(Self {
|
|
||||||
ports: Ports::from_options(options)?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new(ports: Ports) -> Self {
|
|
||||||
Self { ports }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ports(&self) -> &Ports {
|
|
||||||
&self.ports
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Udp> for Protocol {
|
|
||||||
fn from(value: Udp) -> Self {
|
|
||||||
Protocol::Udp(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
pub struct Ports {
|
|
||||||
sport: Option<PortList>,
|
|
||||||
dport: Option<PortList>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Ports {
|
|
||||||
pub fn new(sport: impl Into<Option<PortList>>, dport: impl Into<Option<PortList>>) -> Self {
|
|
||||||
Self {
|
|
||||||
sport: sport.into(),
|
|
||||||
dport: dport.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_options(options: &RuleOptions) -> Result<Self, Error> {
|
|
||||||
Ok(Self {
|
|
||||||
sport: options.sport.as_deref().map(|s| s.parse()).transpose()?,
|
|
||||||
dport: options.dport.as_deref().map(|s| s.parse()).transpose()?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_u16(sport: impl Into<Option<u16>>, dport: impl Into<Option<u16>>) -> Self {
|
|
||||||
Self::new(
|
|
||||||
sport.into().map(PortList::from),
|
|
||||||
dport.into().map(PortList::from),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sport(&self) -> Option<&PortList> {
|
|
||||||
self.sport.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn dport(&self) -> Option<&PortList> {
|
|
||||||
self.dport.as_ref()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
pub struct Tcp {
|
|
||||||
ports: Ports,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Tcp {
|
|
||||||
pub fn new(ports: Ports) -> Self {
|
|
||||||
Self { ports }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_options(options: &RuleOptions) -> Result<Self, Error> {
|
|
||||||
Ok(Self {
|
|
||||||
ports: Ports::from_options(options)?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ports(&self) -> &Ports {
|
|
||||||
&self.ports
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Tcp> for Protocol {
|
|
||||||
fn from(value: Tcp) -> Self {
|
|
||||||
Protocol::Tcp(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
pub struct Sctp {
|
|
||||||
ports: Ports,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Sctp {
|
|
||||||
fn from_options(options: &RuleOptions) -> Result<Self, Error> {
|
|
||||||
Ok(Self {
|
|
||||||
ports: Ports::from_options(options)?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ports(&self) -> &Ports {
|
|
||||||
&self.ports
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
pub struct Icmp {
|
|
||||||
ty: Option<IcmpType>,
|
|
||||||
code: Option<IcmpCode>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Icmp {
|
|
||||||
pub fn new_ty(ty: IcmpType) -> Self {
|
|
||||||
Self {
|
|
||||||
ty: Some(ty),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_code(code: IcmpCode) -> Self {
|
|
||||||
Self {
|
|
||||||
code: Some(code),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_options(options: &RuleOptions) -> Result<Self, Error> {
|
|
||||||
if let Some(ty) = &options.icmp_type {
|
|
||||||
return ty.parse();
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Self::default())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ty(&self) -> Option<&IcmpType> {
|
|
||||||
self.ty.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn code(&self) -> Option<&IcmpCode> {
|
|
||||||
self.code.as_ref()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for Icmp {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
let mut this = Self::default();
|
|
||||||
|
|
||||||
if let Ok(ty) = s.parse() {
|
|
||||||
this.ty = Some(ty);
|
|
||||||
return Ok(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(code) = s.parse() {
|
|
||||||
this.code = Some(code);
|
|
||||||
return Ok(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
bail!("supplied string is neither a valid icmp type nor code");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
pub enum IcmpType {
|
|
||||||
Numeric(u8),
|
|
||||||
Named(&'static str),
|
|
||||||
Any,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[sortable]
|
|
||||||
const ICMP_TYPES: [(&str, u8); 15] = sorted!([
|
|
||||||
("address-mask-reply", 18),
|
|
||||||
("address-mask-request", 17),
|
|
||||||
("destination-unreachable", 3),
|
|
||||||
("echo-reply", 0),
|
|
||||||
("echo-request", 8),
|
|
||||||
("info-reply", 16),
|
|
||||||
("info-request", 15),
|
|
||||||
("parameter-problem", 12),
|
|
||||||
("redirect", 5),
|
|
||||||
("router-advertisement", 9),
|
|
||||||
("router-solicitation", 10),
|
|
||||||
("source-quench", 4),
|
|
||||||
("time-exceeded", 11),
|
|
||||||
("timestamp-reply", 14),
|
|
||||||
("timestamp-request", 13),
|
|
||||||
]);
|
|
||||||
|
|
||||||
impl std::str::FromStr for IcmpType {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Error> {
|
|
||||||
if s.eq_ignore_ascii_case("any") {
|
|
||||||
return Ok(Self::Any);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(ty) = s.trim().parse::<u8>() {
|
|
||||||
return Ok(Self::Numeric(ty));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(index) = ICMP_TYPES.binary_search_by(|v| v.0.cmp(s)) {
|
|
||||||
return Ok(Self::Named(ICMP_TYPES[index].0));
|
|
||||||
}
|
|
||||||
|
|
||||||
bail!("{s:?} is not a valid icmp type");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for IcmpType {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
IcmpType::Numeric(ty) => write!(f, "{ty}"),
|
|
||||||
IcmpType::Named(ty) => write!(f, "{ty}"),
|
|
||||||
IcmpType::Any => write!(f, "any"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
pub enum IcmpCode {
|
|
||||||
Numeric(u8),
|
|
||||||
Named(&'static str),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[sortable]
|
|
||||||
const ICMP_CODES: [(&str, u8); 7] = sorted!([
|
|
||||||
("admin-prohibited", 13),
|
|
||||||
("host-prohibited", 10),
|
|
||||||
("host-unreachable", 1),
|
|
||||||
("net-prohibited", 9),
|
|
||||||
("net-unreachable", 0),
|
|
||||||
("port-unreachable", 3),
|
|
||||||
("prot-unreachable", 2),
|
|
||||||
]);
|
|
||||||
|
|
||||||
impl std::str::FromStr for IcmpCode {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Error> {
|
|
||||||
if let Ok(code) = s.trim().parse::<u8>() {
|
|
||||||
return Ok(Self::Numeric(code));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(index) = ICMP_CODES.binary_search_by(|v| v.0.cmp(s)) {
|
|
||||||
return Ok(Self::Named(ICMP_CODES[index].0));
|
|
||||||
}
|
|
||||||
|
|
||||||
bail!("{s:?} is not a valid icmp code");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for IcmpCode {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
IcmpCode::Numeric(code) => write!(f, "{code}"),
|
|
||||||
IcmpCode::Named(code) => write!(f, "{code}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
pub struct Icmpv6 {
|
|
||||||
pub ty: Option<Icmpv6Type>,
|
|
||||||
pub code: Option<Icmpv6Code>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Icmpv6 {
|
|
||||||
pub fn new_ty(ty: Icmpv6Type) -> Self {
|
|
||||||
Self {
|
|
||||||
ty: Some(ty),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_code(code: Icmpv6Code) -> Self {
|
|
||||||
Self {
|
|
||||||
code: Some(code),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_options(options: &RuleOptions) -> Result<Self, Error> {
|
|
||||||
if let Some(ty) = &options.icmp_type {
|
|
||||||
return ty.parse();
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Self::default())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ty(&self) -> Option<&Icmpv6Type> {
|
|
||||||
self.ty.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn code(&self) -> Option<&Icmpv6Code> {
|
|
||||||
self.code.as_ref()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for Icmpv6 {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
let mut this = Self::default();
|
|
||||||
|
|
||||||
if let Ok(ty) = s.parse() {
|
|
||||||
this.ty = Some(ty);
|
|
||||||
return Ok(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(code) = s.parse() {
|
|
||||||
this.code = Some(code);
|
|
||||||
return Ok(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
bail!("supplied string is neither a valid icmpv6 type nor code");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
pub enum Icmpv6Type {
|
|
||||||
Numeric(u8),
|
|
||||||
Named(&'static str),
|
|
||||||
Any,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[sortable]
|
|
||||||
const ICMPV6_TYPES: [(&str, u8); 19] = sorted!([
|
|
||||||
("destination-unreachable", 1),
|
|
||||||
("echo-reply", 129),
|
|
||||||
("echo-request", 128),
|
|
||||||
("ind-neighbor-advert", 142),
|
|
||||||
("ind-neighbor-solicit", 141),
|
|
||||||
("mld-listener-done", 132),
|
|
||||||
("mld-listener-query", 130),
|
|
||||||
("mld-listener-reduction", 132),
|
|
||||||
("mld-listener-report", 131),
|
|
||||||
("mld2-listener-report", 143),
|
|
||||||
("nd-neighbor-advert", 136),
|
|
||||||
("nd-neighbor-solicit", 135),
|
|
||||||
("nd-redirect", 137),
|
|
||||||
("nd-router-advert", 134),
|
|
||||||
("nd-router-solicit", 133),
|
|
||||||
("packet-too-big", 2),
|
|
||||||
("parameter-problem", 4),
|
|
||||||
("router-renumbering", 138),
|
|
||||||
("time-exceeded", 3),
|
|
||||||
]);
|
|
||||||
|
|
||||||
impl std::str::FromStr for Icmpv6Type {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Error> {
|
|
||||||
if s.eq_ignore_ascii_case("any") {
|
|
||||||
return Ok(Self::Any);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(ty) = s.trim().parse::<u8>() {
|
|
||||||
return Ok(Self::Numeric(ty));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(index) = ICMPV6_TYPES.binary_search_by(|v| v.0.cmp(s)) {
|
|
||||||
return Ok(Self::Named(ICMPV6_TYPES[index].0));
|
|
||||||
}
|
|
||||||
|
|
||||||
bail!("{s:?} is not a valid icmpv6 type");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for Icmpv6Type {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
Icmpv6Type::Numeric(ty) => write!(f, "{ty}"),
|
|
||||||
Icmpv6Type::Named(ty) => write!(f, "{ty}"),
|
|
||||||
Icmpv6Type::Any => write!(f, "any"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
pub enum Icmpv6Code {
|
|
||||||
Numeric(u8),
|
|
||||||
Named(&'static str),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[sortable]
|
|
||||||
const ICMPV6_CODES: [(&str, u8); 6] = sorted!([
|
|
||||||
("addr-unreachable", 3),
|
|
||||||
("admin-prohibited", 1),
|
|
||||||
("no-route", 0),
|
|
||||||
("policy-fail", 5),
|
|
||||||
("port-unreachable", 4),
|
|
||||||
("reject-route", 6),
|
|
||||||
]);
|
|
||||||
|
|
||||||
impl std::str::FromStr for Icmpv6Code {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Error> {
|
|
||||||
if let Ok(code) = s.trim().parse::<u8>() {
|
|
||||||
return Ok(Self::Numeric(code));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(index) = ICMPV6_CODES.binary_search_by(|v| v.0.cmp(s)) {
|
|
||||||
return Ok(Self::Named(ICMPV6_CODES[index].0));
|
|
||||||
}
|
|
||||||
|
|
||||||
bail!("{s:?} is not a valid icmpv6 code");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for Icmpv6Code {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
Icmpv6Code::Numeric(code) => write!(f, "{code}"),
|
|
||||||
Icmpv6Code::Named(code) => write!(f, "{code}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use crate::firewall::types::{alias::AliasScope::Guest, Cidr};
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_action() {
|
|
||||||
assert_eq!(parse_action("REJECT").unwrap(), (None, Verdict::Reject, ""));
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
parse_action("SSH(ACCEPT) qweasd").unwrap(),
|
|
||||||
(Some("SSH"), Verdict::Accept, "qweasd")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_ip_addr_match() {
|
|
||||||
for input in [
|
|
||||||
"10.0.0.0/8",
|
|
||||||
"10.0.0.0/8,192.168.0.0-192.168.255.255,172.16.0.1",
|
|
||||||
"dc/test",
|
|
||||||
"+guest/proxmox",
|
|
||||||
] {
|
|
||||||
input.parse::<IpAddrMatch>().expect("valid ip match");
|
|
||||||
}
|
|
||||||
|
|
||||||
for input in [
|
|
||||||
"10.0.0.0/",
|
|
||||||
"10.0.0.0/8,192.168.256.0-192.168.255.255,172.16.0.1",
|
|
||||||
"dcc/test",
|
|
||||||
"+guest/",
|
|
||||||
"",
|
|
||||||
] {
|
|
||||||
input.parse::<IpAddrMatch>().expect_err("invalid ip match");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_options() {
|
|
||||||
let mut options: RuleOptions =
|
|
||||||
"-p udp --sport 123 --dport 234 -source 127.0.0.1 --dest 127.0.0.1 -i ens1 --log crit"
|
|
||||||
.parse()
|
|
||||||
.expect("valid option string");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
options,
|
|
||||||
RuleOptions {
|
|
||||||
proto: Some("udp".to_string()),
|
|
||||||
sport: Some("123".to_string()),
|
|
||||||
dport: Some("234".to_string()),
|
|
||||||
source: Some("127.0.0.1".to_string()),
|
|
||||||
dest: Some("127.0.0.1".to_string()),
|
|
||||||
iface: Some("ens1".to_string()),
|
|
||||||
log: Some(LogLevel::Critical),
|
|
||||||
icmp_type: None,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
options = "".parse().expect("valid option string");
|
|
||||||
|
|
||||||
assert_eq!(options, RuleOptions::default(),);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_construct_ip_match() {
|
|
||||||
IpMatch::new(
|
|
||||||
IpAddrMatch::Ip(IpList::from(Cidr::new_v4([10, 0, 0, 0], 8).unwrap())),
|
|
||||||
IpAddrMatch::Ip(IpList::from(Cidr::new_v4([10, 0, 0, 0], 8).unwrap())),
|
|
||||||
)
|
|
||||||
.expect("valid ip match");
|
|
||||||
|
|
||||||
IpMatch::new(
|
|
||||||
IpAddrMatch::Ip(IpList::from(Cidr::new_v4([10, 0, 0, 0], 8).unwrap())),
|
|
||||||
IpAddrMatch::Alias(AliasName::new(Guest, "test")),
|
|
||||||
)
|
|
||||||
.expect("valid ip match");
|
|
||||||
|
|
||||||
IpMatch::new(
|
|
||||||
IpAddrMatch::Ip(IpList::from(Cidr::new_v4([10, 0, 0, 0], 8).unwrap())),
|
|
||||||
IpAddrMatch::Ip(IpList::from(Cidr::new_v6([0x0000; 8], 8).unwrap())),
|
|
||||||
)
|
|
||||||
.expect_err("cannot mix ip families");
|
|
||||||
|
|
||||||
IpMatch::new(None, None).expect_err("at least one ip must be set");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_from_options() {
|
|
||||||
let mut options = RuleOptions {
|
|
||||||
proto: Some("tcp".to_string()),
|
|
||||||
sport: Some("123".to_string()),
|
|
||||||
dport: Some("234".to_string()),
|
|
||||||
source: Some("192.168.0.1".to_string()),
|
|
||||||
dest: Some("10.0.0.1".to_string()),
|
|
||||||
iface: Some("eth123".to_string()),
|
|
||||||
log: Some(LogLevel::Error),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
Protocol::from_options(&options).unwrap().unwrap(),
|
|
||||||
Protocol::Tcp(Tcp::new(Ports::from_u16(123, 234))),
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
IpMatch::from_options(&options).unwrap().unwrap(),
|
|
||||||
IpMatch::new(
|
|
||||||
IpAddrMatch::Ip(IpList::from(Cidr::new_v4([192, 168, 0, 1], 32).unwrap()),),
|
|
||||||
IpAddrMatch::Ip(IpList::from(Cidr::new_v4([10, 0, 0, 1], 32).unwrap()),)
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
|
|
||||||
options = RuleOptions::default();
|
|
||||||
|
|
||||||
assert_eq!(Protocol::from_options(&options).unwrap(), None,);
|
|
||||||
|
|
||||||
assert_eq!(IpMatch::from_options(&options).unwrap(), None,);
|
|
||||||
|
|
||||||
options = RuleOptions {
|
|
||||||
proto: Some("tcp".to_string()),
|
|
||||||
sport: Some("qwe".to_string()),
|
|
||||||
source: Some("qwe".to_string()),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
Protocol::from_options(&options).expect_err("invalid source port");
|
|
||||||
|
|
||||||
IpMatch::from_options(&options).expect_err("invalid source address");
|
|
||||||
|
|
||||||
options = RuleOptions {
|
|
||||||
icmp_type: Some("port-unreachable".to_string()),
|
|
||||||
dport: Some("123".to_string()),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
RuleMatch::from_options(Direction::In, Verdict::Drop, None, options)
|
|
||||||
.expect_err("cannot mix dport and icmp-type");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_icmp() {
|
|
||||||
let mut icmp: Icmp = "info-request".parse().expect("valid icmp type");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
icmp,
|
|
||||||
Icmp {
|
|
||||||
ty: Some(IcmpType::Named("info-request")),
|
|
||||||
code: None
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
icmp = "12".parse().expect("valid icmp type");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
icmp,
|
|
||||||
Icmp {
|
|
||||||
ty: Some(IcmpType::Numeric(12)),
|
|
||||||
code: None
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
icmp = "port-unreachable".parse().expect("valid icmp code");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
icmp,
|
|
||||||
Icmp {
|
|
||||||
ty: None,
|
|
||||||
code: Some(IcmpCode::Named("port-unreachable"))
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_icmp6() {
|
|
||||||
let mut icmp: Icmpv6 = "echo-reply".parse().expect("valid icmpv6 type");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
icmp,
|
|
||||||
Icmpv6 {
|
|
||||||
ty: Some(Icmpv6Type::Named("echo-reply")),
|
|
||||||
code: None
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
icmp = "12".parse().expect("valid icmpv6 type");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
icmp,
|
|
||||||
Icmpv6 {
|
|
||||||
ty: Some(Icmpv6Type::Numeric(12)),
|
|
||||||
code: None
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
icmp = "admin-prohibited".parse().expect("valid icmpv6 code");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
icmp,
|
|
||||||
Icmpv6 {
|
|
||||||
ty: None,
|
|
||||||
code: Some(Icmpv6Code::Named("admin-prohibited"))
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,115 +0,0 @@
|
|||||||
use core::ops::Deref;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use anyhow::{Context, Error};
|
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
use proxmox_sys::nodename;
|
|
||||||
use types::Vmid;
|
|
||||||
|
|
||||||
pub mod types;
|
|
||||||
pub mod vm;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize)]
|
|
||||||
pub enum GuestType {
|
|
||||||
#[serde(rename = "qemu")]
|
|
||||||
Vm,
|
|
||||||
#[serde(rename = "lxc")]
|
|
||||||
Ct,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GuestType {
|
|
||||||
pub fn iface_prefix(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
GuestType::Vm => "tap",
|
|
||||||
GuestType::Ct => "veth",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn config_folder(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
GuestType::Vm => "qemu-server",
|
|
||||||
GuestType::Ct => "lxc",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct GuestEntry {
|
|
||||||
node: String,
|
|
||||||
|
|
||||||
#[serde(rename = "type")]
|
|
||||||
ty: GuestType,
|
|
||||||
|
|
||||||
#[serde(rename = "version")]
|
|
||||||
_version: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GuestEntry {
|
|
||||||
pub fn new(node: String, ty: GuestType) -> Self {
|
|
||||||
Self {
|
|
||||||
node,
|
|
||||||
ty,
|
|
||||||
_version: Default::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_local(&self) -> bool {
|
|
||||||
nodename() == self.node
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ty(&self) -> &GuestType {
|
|
||||||
&self.ty
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const VMLIST_CONFIG_PATH: &str = "/etc/pve/.vmlist";
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct GuestMap {
|
|
||||||
#[serde(rename = "version")]
|
|
||||||
_version: usize,
|
|
||||||
#[serde(rename = "ids", default)]
|
|
||||||
guests: HashMap<Vmid, GuestEntry>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<HashMap<Vmid, GuestEntry>> for GuestMap {
|
|
||||||
fn from(guests: HashMap<Vmid, GuestEntry>) -> Self {
|
|
||||||
Self {
|
|
||||||
guests,
|
|
||||||
_version: Default::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for GuestMap {
|
|
||||||
type Target = HashMap<Vmid, GuestEntry>;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.guests
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GuestMap {
|
|
||||||
pub fn new() -> Result<Self, Error> {
|
|
||||||
let data = std::fs::read(VMLIST_CONFIG_PATH)
|
|
||||||
.with_context(|| format!("failed to read guest map from {VMLIST_CONFIG_PATH}"))?;
|
|
||||||
|
|
||||||
serde_json::from_slice(&data).with_context(|| "failed to parse guest map".to_owned())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn firewall_config_path(vmid: &Vmid) -> String {
|
|
||||||
format!("/etc/pve/firewall/{}.fw", vmid)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// returns the local configuration path for a given Vmid.
|
|
||||||
///
|
|
||||||
/// The caller must ensure that the given Vmid exists and is local to the node
|
|
||||||
pub fn config_path(vmid: &Vmid, entry: &GuestEntry) -> String {
|
|
||||||
format!(
|
|
||||||
"/etc/pve/local/{}/{}.conf",
|
|
||||||
entry.ty().config_folder(),
|
|
||||||
vmid
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,38 +0,0 @@
|
|||||||
use std::fmt;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use anyhow::{format_err, Error};
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)]
|
|
||||||
pub struct Vmid(u32);
|
|
||||||
|
|
||||||
impl Vmid {
|
|
||||||
pub fn new(id: u32) -> Self {
|
|
||||||
Vmid(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<u32> for Vmid {
|
|
||||||
fn from(value: u32) -> Self {
|
|
||||||
Self::new(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for Vmid {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
fmt::Display::fmt(&self.0, f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for Vmid {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
Ok(Self(
|
|
||||||
s.parse()
|
|
||||||
.map_err(|_| format_err!("not a valid vmid: {s:?}"))?,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
serde_plain::derive_deserialize_from_fromstr!(Vmid, "valid vmid");
|
|
@ -1,510 +0,0 @@
|
|||||||
use anyhow::{bail, Error};
|
|
||||||
use core::fmt::Display;
|
|
||||||
use std::io;
|
|
||||||
use std::str::FromStr;
|
|
||||||
use std::{collections::HashMap, net::Ipv6Addr};
|
|
||||||
|
|
||||||
use proxmox_schema::property_string::PropertyIterator;
|
|
||||||
|
|
||||||
use crate::firewall::parse::{match_digits, parse_bool};
|
|
||||||
use crate::firewall::types::address::{Ipv4Cidr, Ipv6Cidr};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
pub struct MacAddress([u8; 6]);
|
|
||||||
|
|
||||||
static LOCAL_PART: [u8; 8] = [0xFE, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
|
|
||||||
static EUI64_MIDDLE_PART: [u8; 2] = [0xFF, 0xFE];
|
|
||||||
|
|
||||||
impl MacAddress {
|
|
||||||
/// generates a link local IPv6-address according to RFC 4291 (Appendix A)
|
|
||||||
pub fn eui64_link_local_address(&self) -> Ipv6Addr {
|
|
||||||
let head = &self.0[..3];
|
|
||||||
let tail = &self.0[3..];
|
|
||||||
|
|
||||||
let mut eui64_address: Vec<u8> = LOCAL_PART
|
|
||||||
.iter()
|
|
||||||
.chain(head.iter())
|
|
||||||
.chain(EUI64_MIDDLE_PART.iter())
|
|
||||||
.chain(tail.iter())
|
|
||||||
.copied()
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// we need to flip the 7th bit of the first eui64 byte
|
|
||||||
eui64_address[8] ^= 0x02;
|
|
||||||
|
|
||||||
Ipv6Addr::from(
|
|
||||||
TryInto::<[u8; 16]>::try_into(eui64_address).expect("is an u8 array with 16 entries"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for MacAddress {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
let split = s.split(':');
|
|
||||||
|
|
||||||
let parsed = split
|
|
||||||
.into_iter()
|
|
||||||
.map(|elem| u8::from_str_radix(elem, 16))
|
|
||||||
.collect::<Result<Vec<u8>, _>>()
|
|
||||||
.map_err(Error::msg)?;
|
|
||||||
|
|
||||||
if parsed.len() != 6 {
|
|
||||||
bail!("Invalid amount of elements in MAC address!");
|
|
||||||
}
|
|
||||||
|
|
||||||
let address = &parsed.as_slice()[0..6];
|
|
||||||
Ok(Self(address.try_into().unwrap()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for MacAddress {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"{:<02X}:{:<02X}:{:<02X}:{:<02X}:{:<02X}:{:<02X}",
|
|
||||||
self.0[0], self.0[1], self.0[2], self.0[3], self.0[4], self.0[5]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
pub enum NetworkDeviceModel {
|
|
||||||
VirtIO,
|
|
||||||
Veth,
|
|
||||||
E1000,
|
|
||||||
Vmxnet3,
|
|
||||||
RTL8139,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for NetworkDeviceModel {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
match s {
|
|
||||||
"virtio" => Ok(NetworkDeviceModel::VirtIO),
|
|
||||||
"e1000" => Ok(NetworkDeviceModel::E1000),
|
|
||||||
"rtl8139" => Ok(NetworkDeviceModel::RTL8139),
|
|
||||||
"vmxnet3" => Ok(NetworkDeviceModel::Vmxnet3),
|
|
||||||
"veth" => Ok(NetworkDeviceModel::Veth),
|
|
||||||
_ => bail!("Invalid network device model: {s}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
pub struct NetworkDevice {
|
|
||||||
model: NetworkDeviceModel,
|
|
||||||
mac_address: MacAddress,
|
|
||||||
firewall: bool,
|
|
||||||
ip: Option<Ipv4Cidr>,
|
|
||||||
ip6: Option<Ipv6Cidr>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NetworkDevice {
|
|
||||||
pub fn model(&self) -> NetworkDeviceModel {
|
|
||||||
self.model
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn mac_address(&self) -> &MacAddress {
|
|
||||||
&self.mac_address
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ip(&self) -> Option<&Ipv4Cidr> {
|
|
||||||
self.ip.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ip6(&self) -> Option<&Ipv6Cidr> {
|
|
||||||
self.ip6.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn has_firewall(&self) -> bool {
|
|
||||||
self.firewall
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for NetworkDevice {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
let (mut ty, mut hwaddr, mut firewall, mut ip, mut ip6) = (None, None, true, None, None);
|
|
||||||
|
|
||||||
for entry in PropertyIterator::new(s) {
|
|
||||||
let (key, value) = entry.unwrap();
|
|
||||||
|
|
||||||
if let Some(key) = key {
|
|
||||||
match key {
|
|
||||||
"type" | "model" => {
|
|
||||||
ty = Some(NetworkDeviceModel::from_str(&value)?);
|
|
||||||
}
|
|
||||||
"hwaddr" | "macaddr" => {
|
|
||||||
hwaddr = Some(MacAddress::from_str(&value)?);
|
|
||||||
}
|
|
||||||
"firewall" => {
|
|
||||||
firewall = parse_bool(&value)?;
|
|
||||||
}
|
|
||||||
"ip" => {
|
|
||||||
if value == "dhcp" {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
ip = Some(Ipv4Cidr::from_str(&value)?);
|
|
||||||
}
|
|
||||||
"ip6" => {
|
|
||||||
if value == "dhcp" || value == "auto" {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
ip6 = Some(Ipv6Cidr::from_str(&value)?);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
if let Ok(model) = NetworkDeviceModel::from_str(key) {
|
|
||||||
ty = Some(model);
|
|
||||||
hwaddr = Some(MacAddress::from_str(&value)?);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let (Some(ty), Some(hwaddr)) = (ty, hwaddr) {
|
|
||||||
return Ok(NetworkDevice {
|
|
||||||
model: ty,
|
|
||||||
mac_address: hwaddr,
|
|
||||||
firewall,
|
|
||||||
ip,
|
|
||||||
ip6,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
bail!("No valid network device detected in string {s}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
|
||||||
pub struct NetworkConfig {
|
|
||||||
network_devices: HashMap<i64, NetworkDevice>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NetworkConfig {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn index_from_net_key(key: &str) -> Result<i64, Error> {
|
|
||||||
if let Some(digits) = key.strip_prefix("net") {
|
|
||||||
if let Some((digits, rest)) = match_digits(digits) {
|
|
||||||
let index: i64 = digits.parse()?;
|
|
||||||
|
|
||||||
if (0..31).contains(&index) && rest.is_empty() {
|
|
||||||
return Ok(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bail!("No index found in net key string: {key}")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn network_devices(&self) -> &HashMap<i64, NetworkDevice> {
|
|
||||||
&self.network_devices
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse<R: io::BufRead>(input: R) -> Result<Self, Error> {
|
|
||||||
let mut network_devices = HashMap::new();
|
|
||||||
|
|
||||||
for line in input.lines() {
|
|
||||||
let line = line?;
|
|
||||||
let line = line.trim();
|
|
||||||
|
|
||||||
if line.is_empty() || line.starts_with('#') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if line.starts_with('[') {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if line.starts_with("net") {
|
|
||||||
log::trace!("parsing net config line: {line}");
|
|
||||||
|
|
||||||
if let Some((mut key, mut value)) = line.split_once(':') {
|
|
||||||
if key.is_empty() || value.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
key = key.trim();
|
|
||||||
value = value.trim();
|
|
||||||
|
|
||||||
if let Ok(index) = Self::index_from_net_key(key) {
|
|
||||||
let network_device = NetworkDevice::from_str(value)?;
|
|
||||||
|
|
||||||
let exists = network_devices.insert(index, network_device);
|
|
||||||
|
|
||||||
if exists.is_some() {
|
|
||||||
bail!("Duplicated config key detected: {key}");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
bail!("Encountered invalid net key in cfg: {key}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Self { network_devices })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_mac_address() {
|
|
||||||
for input in [
|
|
||||||
"aa:aa:aa:11:22:33",
|
|
||||||
"AA:BB:FF:11:22:33",
|
|
||||||
"bc:24:11:AA:bb:Ef",
|
|
||||||
] {
|
|
||||||
let mac_address = input.parse::<MacAddress>().expect("valid mac address");
|
|
||||||
|
|
||||||
assert_eq!(input.to_uppercase(), mac_address.to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
for input in [
|
|
||||||
"aa:aa:aa:11:22:33:aa",
|
|
||||||
"AA:BB:FF:11:22",
|
|
||||||
"AA:BB:GG:11:22:33",
|
|
||||||
"AABBGG112233",
|
|
||||||
"",
|
|
||||||
] {
|
|
||||||
input
|
|
||||||
.parse::<MacAddress>()
|
|
||||||
.expect_err("invalid mac address");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_eui64_link_local_address() {
|
|
||||||
let mac_address: MacAddress = "BC:24:11:49:8D:75".parse().expect("valid MAC address");
|
|
||||||
|
|
||||||
let link_local_address =
|
|
||||||
Ipv6Addr::from_str("fe80::be24:11ff:fe49:8d75").expect("valid IPv6 address");
|
|
||||||
|
|
||||||
assert_eq!(link_local_address, mac_address.eui64_link_local_address());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_network_device() {
|
|
||||||
let mut network_device: NetworkDevice =
|
|
||||||
"virtio=AA:AA:AA:17:19:81,bridge=public,firewall=1,queues=4"
|
|
||||||
.parse()
|
|
||||||
.expect("valid network configuration");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
network_device,
|
|
||||||
NetworkDevice {
|
|
||||||
model: NetworkDeviceModel::VirtIO,
|
|
||||||
mac_address: MacAddress([0xAA, 0xAA, 0xAA, 0x17, 0x19, 0x81]),
|
|
||||||
firewall: true,
|
|
||||||
ip: None,
|
|
||||||
ip6: None,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
network_device = "model=virtio,macaddr=AA:AA:AA:17:19:81,bridge=public,firewall=1,queues=4"
|
|
||||||
.parse()
|
|
||||||
.expect("valid network configuration");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
network_device,
|
|
||||||
NetworkDevice {
|
|
||||||
model: NetworkDeviceModel::VirtIO,
|
|
||||||
mac_address: MacAddress([0xAA, 0xAA, 0xAA, 0x17, 0x19, 0x81]),
|
|
||||||
firewall: true,
|
|
||||||
ip: None,
|
|
||||||
ip6: None,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
network_device =
|
|
||||||
"name=eth0,bridge=public,firewall=0,hwaddr=AA:AA:AA:E2:3E:24,ip=dhcp,type=veth"
|
|
||||||
.parse()
|
|
||||||
.expect("valid network configuration");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
network_device,
|
|
||||||
NetworkDevice {
|
|
||||||
model: NetworkDeviceModel::Veth,
|
|
||||||
mac_address: MacAddress([0xAA, 0xAA, 0xAA, 0xE2, 0x3E, 0x24]),
|
|
||||||
firewall: false,
|
|
||||||
ip: None,
|
|
||||||
ip6: None,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
"model=virtio"
|
|
||||||
.parse::<NetworkDevice>()
|
|
||||||
.expect_err("invalid network configuration");
|
|
||||||
|
|
||||||
"bridge=public,firewall=0"
|
|
||||||
.parse::<NetworkDevice>()
|
|
||||||
.expect_err("invalid network configuration");
|
|
||||||
|
|
||||||
"".parse::<NetworkDevice>()
|
|
||||||
.expect_err("invalid network configuration");
|
|
||||||
|
|
||||||
"name=eth0,bridge=public,firewall=0,hwaddr=AA:AA:AG:E2:3E:24,ip=dhcp,type=veth"
|
|
||||||
.parse::<NetworkDevice>()
|
|
||||||
.expect_err("invalid network configuration");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_network_confg() {
|
|
||||||
let mut guest_config = "\
|
|
||||||
boot: order=scsi0;net0
|
|
||||||
cores: 4
|
|
||||||
cpu: host
|
|
||||||
memory: 8192
|
|
||||||
meta: creation-qemu=8.0.2,ctime=1700141675
|
|
||||||
name: hoan-sdn
|
|
||||||
net0: virtio=AA:BB:CC:F2:FE:75,bridge=public
|
|
||||||
numa: 0
|
|
||||||
ostype: l26
|
|
||||||
parent: uwu
|
|
||||||
scsi0: local-lvm:vm-999-disk-0,discard=on,iothread=1,size=32G
|
|
||||||
scsihw: virtio-scsi-single
|
|
||||||
smbios1: uuid=addb0cc6-0393-4269-a504-1eb46604cb8a
|
|
||||||
sockets: 1
|
|
||||||
vmgenid: 13bcbb05-3608-4d74-bf4f-d5d20c3538e8
|
|
||||||
|
|
||||||
[snapshot]
|
|
||||||
boot: order=scsi0;ide2;net0
|
|
||||||
cores: 4
|
|
||||||
cpu: x86-64-v2-AES
|
|
||||||
ide2: NFS-iso:iso/proxmox-ve_8.0-2.iso,media=cdrom,size=1166488K
|
|
||||||
memory: 8192
|
|
||||||
meta: creation-qemu=8.0.2,ctime=1700141675
|
|
||||||
name: test
|
|
||||||
net2: virtio=AA:AA:AA:F2:FE:75,bridge=public,firewall=1
|
|
||||||
numa: 0
|
|
||||||
ostype: l26
|
|
||||||
parent: pre-SDN
|
|
||||||
scsi0: local-lvm:vm-999-disk-0,discard=on,iothread=1,size=32G
|
|
||||||
scsihw: virtio-scsi-single
|
|
||||||
smbios1: uuid=addb0cc6-0393-4269-a504-1eb46604cb8a
|
|
||||||
snaptime: 1700143513
|
|
||||||
sockets: 1
|
|
||||||
vmgenid: 706fbe99-d28b-4047-a9cd-3677c859ca8a
|
|
||||||
|
|
||||||
[snapshott]
|
|
||||||
boot: order=scsi0;ide2;net0
|
|
||||||
cores: 4
|
|
||||||
cpu: host
|
|
||||||
ide2: NFS-iso:iso/proxmox-ve_8.0-2.iso,media=cdrom,size=1166488K
|
|
||||||
memory: 8192
|
|
||||||
meta: creation-qemu=8.0.2,ctime=1700141675
|
|
||||||
name: hoan-sdn
|
|
||||||
net0: virtio=AA:AA:FF:F2:FE:75,bridge=public,firewall=0
|
|
||||||
numa: 0
|
|
||||||
ostype: l26
|
|
||||||
parent: SDN
|
|
||||||
scsi0: local-lvm:vm-999-disk-0,discard=on,iothread=1,size=32G
|
|
||||||
scsihw: virtio-scsi-single
|
|
||||||
smbios1: uuid=addb0cc6-0393-4269-a504-1eb46604cb8a
|
|
||||||
snaptime: 1700158473
|
|
||||||
sockets: 1
|
|
||||||
vmgenid: 706fbe99-d28b-4047-a9cd-3677c859ca8a"
|
|
||||||
.as_bytes();
|
|
||||||
|
|
||||||
let mut network_config: NetworkConfig =
|
|
||||||
NetworkConfig::parse(guest_config).expect("valid network configuration");
|
|
||||||
|
|
||||||
assert_eq!(network_config.network_devices().len(), 1);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
network_config.network_devices()[&0],
|
|
||||||
NetworkDevice {
|
|
||||||
model: NetworkDeviceModel::VirtIO,
|
|
||||||
mac_address: MacAddress([0xAA, 0xBB, 0xCC, 0xF2, 0xFE, 0x75]),
|
|
||||||
firewall: true,
|
|
||||||
ip: None,
|
|
||||||
ip6: None,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
guest_config = "\
|
|
||||||
arch: amd64
|
|
||||||
cores: 1
|
|
||||||
features: nesting=1
|
|
||||||
hostname: dnsct
|
|
||||||
memory: 512
|
|
||||||
net0: name=eth0,bridge=data,firewall=1,hwaddr=BC:24:11:47:83:11,ip=dhcp,type=veth
|
|
||||||
net2: name=eth0,bridge=data,firewall=0,hwaddr=BC:24:11:47:83:12,ip=123.123.123.123/24,type=veth
|
|
||||||
net5: name=eth0,bridge=data,firewall=1,hwaddr=BC:24:11:47:83:13,ip6=fd80::1/64,type=veth
|
|
||||||
ostype: alpine
|
|
||||||
rootfs: local-lvm:vm-10001-disk-0,size=1G
|
|
||||||
swap: 512
|
|
||||||
unprivileged: 1"
|
|
||||||
.as_bytes();
|
|
||||||
|
|
||||||
network_config = NetworkConfig::parse(guest_config).expect("valid network configuration");
|
|
||||||
|
|
||||||
assert_eq!(network_config.network_devices().len(), 3);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
network_config.network_devices()[&0],
|
|
||||||
NetworkDevice {
|
|
||||||
model: NetworkDeviceModel::Veth,
|
|
||||||
mac_address: MacAddress([0xBC, 0x24, 0x11, 0x47, 0x83, 0x11]),
|
|
||||||
firewall: true,
|
|
||||||
ip: None,
|
|
||||||
ip6: None,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
network_config.network_devices()[&2],
|
|
||||||
NetworkDevice {
|
|
||||||
model: NetworkDeviceModel::Veth,
|
|
||||||
mac_address: MacAddress([0xBC, 0x24, 0x11, 0x47, 0x83, 0x12]),
|
|
||||||
firewall: false,
|
|
||||||
ip: Some(Ipv4Cidr::from_str("123.123.123.123/24").expect("valid ipv4")),
|
|
||||||
ip6: None,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
network_config.network_devices()[&5],
|
|
||||||
NetworkDevice {
|
|
||||||
model: NetworkDeviceModel::Veth,
|
|
||||||
mac_address: MacAddress([0xBC, 0x24, 0x11, 0x47, 0x83, 0x13]),
|
|
||||||
firewall: true,
|
|
||||||
ip: None,
|
|
||||||
ip6: Some(Ipv6Cidr::from_str("fd80::1/64").expect("valid ipv6")),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
NetworkConfig::parse(
|
|
||||||
"netqwe: name=eth0,bridge=data,firewall=1,hwaddr=BC:24:11:47:83:11,ip=dhcp,type=veth"
|
|
||||||
.as_bytes(),
|
|
||||||
)
|
|
||||||
.expect_err("invalid net key");
|
|
||||||
|
|
||||||
NetworkConfig::parse(
|
|
||||||
"net0 name=eth0,bridge=data,firewall=1,hwaddr=BC:24:11:47:83:11,ip=dhcp,type=veth"
|
|
||||||
.as_bytes(),
|
|
||||||
)
|
|
||||||
.expect_err("invalid net key");
|
|
||||||
|
|
||||||
NetworkConfig::parse(
|
|
||||||
"net33: name=eth0,bridge=data,firewall=1,hwaddr=BC:24:11:47:83:11,ip=dhcp,type=veth"
|
|
||||||
.as_bytes(),
|
|
||||||
)
|
|
||||||
.expect_err("invalid net key");
|
|
||||||
}
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
pub mod utils;
|
|
@ -1,70 +0,0 @@
|
|||||||
use std::net::{IpAddr, ToSocketAddrs};
|
|
||||||
|
|
||||||
use crate::firewall::types::Cidr;
|
|
||||||
|
|
||||||
use nix::sys::socket::{AddressFamily, SockaddrLike};
|
|
||||||
use proxmox_sys::nodename;
|
|
||||||
|
|
||||||
/// gets a list of IPs that the hostname of this node resolves to
|
|
||||||
///
|
|
||||||
/// panics if the local hostname is not resolvable
|
|
||||||
pub fn host_ips() -> Vec<IpAddr> {
|
|
||||||
let hostname = nodename();
|
|
||||||
|
|
||||||
log::trace!("resolving hostname");
|
|
||||||
|
|
||||||
format!("{hostname}:0")
|
|
||||||
.to_socket_addrs()
|
|
||||||
.expect("local hostname is resolvable")
|
|
||||||
.map(|addr| addr.ip())
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// gets a list of all configured CIDRs on all network interfaces of this host
|
|
||||||
///
|
|
||||||
/// panics if unable to query the current network configuration
|
|
||||||
pub fn network_interface_cidrs() -> Vec<Cidr> {
|
|
||||||
use nix::ifaddrs::getifaddrs;
|
|
||||||
|
|
||||||
log::trace!("reading networking interface list");
|
|
||||||
|
|
||||||
let mut cidrs = Vec::new();
|
|
||||||
|
|
||||||
let interfaces = getifaddrs().expect("should be able to query network interfaces");
|
|
||||||
|
|
||||||
for interface in interfaces {
|
|
||||||
if let (Some(address), Some(netmask)) = (interface.address, interface.netmask) {
|
|
||||||
match (address.family(), netmask.family()) {
|
|
||||||
(Some(AddressFamily::Inet), Some(AddressFamily::Inet)) => {
|
|
||||||
let address = address.as_sockaddr_in().expect("is an IPv4 address").ip();
|
|
||||||
|
|
||||||
let netmask = netmask
|
|
||||||
.as_sockaddr_in()
|
|
||||||
.expect("is an IPv4 address")
|
|
||||||
.ip()
|
|
||||||
.count_ones()
|
|
||||||
.try_into()
|
|
||||||
.expect("count_ones of u32 is < u8_max");
|
|
||||||
|
|
||||||
cidrs.push(Cidr::new_v4(address, netmask).expect("netmask is valid"));
|
|
||||||
}
|
|
||||||
(Some(AddressFamily::Inet6), Some(AddressFamily::Inet6)) => {
|
|
||||||
let address = address.as_sockaddr_in6().expect("is an IPv6 address").ip();
|
|
||||||
|
|
||||||
let netmask_address =
|
|
||||||
netmask.as_sockaddr_in6().expect("is an IPv6 address").ip();
|
|
||||||
|
|
||||||
let netmask = u128::from_be_bytes(netmask_address.octets())
|
|
||||||
.count_ones()
|
|
||||||
.try_into()
|
|
||||||
.expect("count_ones of u128 is < u8_max");
|
|
||||||
|
|
||||||
cidrs.push(Cidr::new_v6(address, netmask).expect("netmask is valid"));
|
|
||||||
}
|
|
||||||
_ => continue,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cidrs
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
pub mod firewall;
|
|
||||||
pub mod guest;
|
|
||||||
pub mod host;
|
|
Loading…
Reference in New Issue
Block a user