From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id F09681FF141 for ; Fri, 13 Feb 2026 15:36:18 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 9B5646EFE; Fri, 13 Feb 2026 15:37:01 +0100 (CET) From: Christoph Heiss To: pve-devel@lists.proxmox.com Subject: [PATCH proxmox v2 6/8] wireguard: init configuration support crate Date: Fri, 13 Feb 2026 15:35:59 +0100 Message-ID: <20260213143601.1424613-7-c.heiss@proxmox.com> X-Mailer: git-send-email 2.52.0 In-Reply-To: <20260213143601.1424613-1-c.heiss@proxmox.com> References: <20260213143601.1424613-1-c.heiss@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1770993390897 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.051 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment KAM_SHORT 0.001 Use of a URL Shortener for very short URL SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record WEIRD_PORT 0.001 Uses non-standard port number for HTTP Message-ID-Hash: OGDGX77QVS623IOC7D4WHETS2DIUOJFY X-Message-ID-Hash: OGDGX77QVS623IOC7D4WHETS2DIUOJFY X-MailFrom: c.heiss@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: This introduces a new crate, `proxmox-wireguard`. It provides: - a slight abstraction over raw ED25519 keys and keyspairs, as used by WireGuard - keypair generation - wg(8) configuration support, by providing the necessary structs and INI serialization thereof Signed-off-by: Christoph Heiss --- Changes v1 -> v2: * implement `AsRef<{Private,Preshared}Key>` instead of .raw() method * scope key generation (and thus proxmox-sys usage) behind a feature flag, makes it possible to use this crate in e.g. wasm context * expand test cases a bit Cargo.toml | 1 + proxmox-wireguard/Cargo.toml | 25 ++ proxmox-wireguard/debian/changelog | 5 + proxmox-wireguard/debian/control | 67 +++++ proxmox-wireguard/debian/copyright | 18 ++ proxmox-wireguard/debian/debcargo.toml | 7 + proxmox-wireguard/src/lib.rs | 391 +++++++++++++++++++++++++ 7 files changed, 514 insertions(+) create mode 100644 proxmox-wireguard/Cargo.toml create mode 100644 proxmox-wireguard/debian/changelog create mode 100644 proxmox-wireguard/debian/control create mode 100644 proxmox-wireguard/debian/copyright create mode 100644 proxmox-wireguard/debian/debcargo.toml create mode 100644 proxmox-wireguard/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index f650a9f7..40d370ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,7 @@ members = [ "proxmox-time-api", "proxmox-upgrade-checks", "proxmox-uuid", + "proxmox-wireguard", "proxmox-worker-task", "pbs-api-types", "pve-api-types", diff --git a/proxmox-wireguard/Cargo.toml b/proxmox-wireguard/Cargo.toml new file mode 100644 index 00000000..b48ed238 --- /dev/null +++ b/proxmox-wireguard/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "proxmox-wireguard" +description = "WireGuard configuration support" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +license.workspace = true +homepage.workspace = true +exclude.workspace = true +rust-version.workspace = true + +[dependencies] +ed25519-dalek = "2.1" +serde = { workspace = true, features = [ "derive" ] } +thiserror.workspace = true +proxmox-serde = { workspace = true, features = [ "ini-ser" ] } +proxmox-network-types.workspace = true +proxmox-sys = { workspace = true, optional = true } + +[dev-dependencies] +pretty_assertions.workspace = true + +[features] +default = ["key-generation"] +key-generation = ["dep:proxmox-sys"] diff --git a/proxmox-wireguard/debian/changelog b/proxmox-wireguard/debian/changelog new file mode 100644 index 00000000..5a079507 --- /dev/null +++ b/proxmox-wireguard/debian/changelog @@ -0,0 +1,5 @@ +rust-proxmox-wireguard (0.1.0-1) unstable; urgency=medium + + * Initial release. + + -- Proxmox Support Team Thu, 13 Feb 2026 15:10:12 +0200 diff --git a/proxmox-wireguard/debian/control b/proxmox-wireguard/debian/control new file mode 100644 index 00000000..9c4c46cd --- /dev/null +++ b/proxmox-wireguard/debian/control @@ -0,0 +1,67 @@ +Source: rust-proxmox-wireguard +Section: rust +Priority: optional +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo +Build-Depends-Arch: cargo:native , + rustc:native (>= 1.82) , + libstd-rust-dev , + librust-ed25519-dalek-2+default-dev (>= 2.1-~~) , + librust-proxmox-network-types-0.1+default-dev , + librust-proxmox-serde-1+default-dev , + librust-proxmox-serde-1+ini-ser-dev , + librust-proxmox-serde-1+serde-json-dev , + librust-proxmox-sys-1+default-dev , + librust-serde-1+default-dev , + librust-serde-1+derive-dev , + librust-thiserror-2+default-dev +Maintainer: Proxmox Support Team +Standards-Version: 4.7.2 +Vcs-Git: git://git.proxmox.com/git/proxmox-ve-rs.git +Vcs-Browser: https://git.proxmox.com/?p=proxmox-ve-rs.git +Homepage: https://proxmox.com +X-Cargo-Crate: proxmox-wireguard + +Package: librust-proxmox-wireguard-dev +Architecture: any +Multi-Arch: same +Depends: + ${misc:Depends}, + librust-ed25519-dalek-2+default-dev (>= 2.1-~~), + librust-proxmox-network-types-0.1+default-dev, + librust-proxmox-serde-1+default-dev, + librust-proxmox-serde-1+ini-ser-dev, + librust-proxmox-serde-1+serde-json-dev, + librust-serde-1+default-dev, + librust-serde-1+derive-dev, + librust-thiserror-2+default-dev +Recommends: + librust-proxmox-wireguard+key-generation-dev (= ${binary:Version}) +Provides: + librust-proxmox-wireguard-0-dev (= ${binary:Version}), + librust-proxmox-wireguard-0.1-dev (= ${binary:Version}), + librust-proxmox-wireguard-0.1.0-dev (= ${binary:Version}) +Description: WireGuard configuration support - Rust source code + Source code for Debianized Rust crate "proxmox-wireguard" + +Package: librust-proxmox-wireguard+key-generation-dev +Architecture: any +Multi-Arch: same +Depends: + ${misc:Depends}, + librust-proxmox-wireguard-dev (= ${binary:Version}), + librust-proxmox-sys-1+default-dev +Provides: + librust-proxmox-wireguard+default-dev (= ${binary:Version}), + librust-proxmox-wireguard-0+key-generation-dev (= ${binary:Version}), + librust-proxmox-wireguard-0+default-dev (= ${binary:Version}), + librust-proxmox-wireguard-0.1+key-generation-dev (= ${binary:Version}), + librust-proxmox-wireguard-0.1+default-dev (= ${binary:Version}), + librust-proxmox-wireguard-0.1.0+key-generation-dev (= ${binary:Version}), + librust-proxmox-wireguard-0.1.0+default-dev (= ${binary:Version}) +Description: WireGuard configuration support - feature "key-generation" and 1 more + This metapackage enables feature "key-generation" for the Rust proxmox- + wireguard crate, by pulling in any additional dependencies needed by that + feature. + . + Additionally, this package also provides the "default" feature. diff --git a/proxmox-wireguard/debian/copyright b/proxmox-wireguard/debian/copyright new file mode 100644 index 00000000..1ea8a56b --- /dev/null +++ b/proxmox-wireguard/debian/copyright @@ -0,0 +1,18 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ + +Files: + * +Copyright: 2019 - 2025 Proxmox Server Solutions GmbH +License: AGPL-3.0-or-later + This program is free software: you can redistribute it and/or modify it under + the terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or (at your option) any + later version. + . + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + details. + . + You should have received a copy of the GNU Affero General Public License along + with this program. If not, see . diff --git a/proxmox-wireguard/debian/debcargo.toml b/proxmox-wireguard/debian/debcargo.toml new file mode 100644 index 00000000..87a787e6 --- /dev/null +++ b/proxmox-wireguard/debian/debcargo.toml @@ -0,0 +1,7 @@ +overlay = "." +crate_src_path = ".." +maintainer = "Proxmox Support Team " + +[source] +vcs_git = "git://git.proxmox.com/git/proxmox-ve-rs.git" +vcs_browser = "https://git.proxmox.com/?p=proxmox-ve-rs.git" diff --git a/proxmox-wireguard/src/lib.rs b/proxmox-wireguard/src/lib.rs new file mode 100644 index 00000000..a3a517b7 --- /dev/null +++ b/proxmox-wireguard/src/lib.rs @@ -0,0 +1,391 @@ +//! Implements a interface for handling WireGuard configurations and serializing them into the +//! INI-style format as described in wg(8) "CONFIGURATION FILE FORMAT". +//! +//! WireGuard keys are 32-bytes securely-generated values, encoded as base64 +//! for any usage where users might come in contact with them. +//! +//! [`PrivateKey`], [`PublicKey`] and [`PresharedKey`] implement all the needed +//! key primitives. +//! +//! By design there is no key pair, as keys should be treated as opaque from a +//! configuration perspective and not worked with. + +#![forbid(unsafe_code, missing_docs)] + +use ed25519_dalek::SigningKey; +use serde::{Deserialize, Serialize}; +use std::fmt; + +use proxmox_network_types::{endpoint::ServiceEndpoint, ip_address::Cidr}; + +/// Possible error when handling WireGuard configurations. +#[derive(thiserror::Error, Debug, PartialEq, Clone)] +pub enum Error { + /// (Private) key generation failed + #[error("failed to generate private key: {0}")] + KeyGenFailed(String), + /// Serialization to the WireGuard INI format failed + #[error("failed to serialize config: {0}")] + SerializationFailed(String), +} + +impl From for Error { + fn from(err: proxmox_serde::ini::Error) -> Self { + Self::SerializationFailed(err.to_string()) + } +} + +/// Public key of a WireGuard peer. +#[derive(Clone, Copy, Deserialize, Serialize, Hash, Debug)] +#[serde(transparent)] +pub struct PublicKey( + #[serde(with = "proxmox_serde::byte_array_as_base64")] [u8; ed25519_dalek::PUBLIC_KEY_LENGTH], +); + +/// Private key of a WireGuard peer. +#[derive(Serialize)] +#[serde(transparent)] +pub struct PrivateKey( + #[serde(with = "proxmox_serde::byte_array_as_base64")] ed25519_dalek::SecretKey, +); + +impl fmt::Debug for PrivateKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "") + } +} + +impl PrivateKey { + /// Length of the raw private key data in bytes. + pub const RAW_LENGTH: usize = ed25519_dalek::SECRET_KEY_LENGTH; + + /// Generates a new private key suitable for use with WireGuard. + #[cfg(feature = "key-generation")] + pub fn generate() -> Result { + generate_key().map(Self) + } + + /// Calculates the public key from the private key. + pub fn public_key(&self) -> PublicKey { + PublicKey( + ed25519_dalek::SigningKey::from_bytes(&self.0) + .verifying_key() + .to_bytes(), + ) + } + + /// Builds a new [`PrivateKey`] from raw key material. + #[must_use] + pub fn from_raw(data: ed25519_dalek::SecretKey) -> Self { + // [`SigningKey`] takes care of correct key clamping. + Self(SigningKey::from(&data).to_bytes()) + } +} + +impl From for PrivateKey { + fn from(value: ed25519_dalek::SecretKey) -> Self { + Self(value) + } +} + +impl AsRef for PrivateKey { + /// Returns the raw private key material. + fn as_ref(&self) -> &ed25519_dalek::SecretKey { + &self.0 + } +} + +/// Preshared key between two WireGuard peers. +#[derive(Clone, Deserialize, Serialize)] +#[serde(transparent)] +pub struct PresharedKey( + #[serde(with = "proxmox_serde::byte_array_as_base64")] ed25519_dalek::SecretKey, +); + +impl fmt::Debug for PresharedKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "") + } +} + +impl PresharedKey { + /// Length of the raw private key data in bytes. + pub const RAW_LENGTH: usize = ed25519_dalek::SECRET_KEY_LENGTH; + + /// Generates a new preshared key suitable for use with WireGuard. + #[cfg(feature = "key-generation")] + pub fn generate() -> Result { + generate_key().map(Self) + } + + /// Builds a new [`PrivateKey`] from raw key material. + #[must_use] + pub fn from_raw(data: ed25519_dalek::SecretKey) -> Self { + // [`SigningKey`] takes care of correct key clamping. + Self(SigningKey::from(&data).to_bytes()) + } +} + +impl AsRef for PresharedKey { + /// Returns the raw preshared key material. + fn as_ref(&self) -> &ed25519_dalek::SecretKey { + &self.0 + } +} + +/// A single WireGuard peer. +#[derive(Serialize, Debug)] +#[serde(rename_all = "PascalCase")] +pub struct WireGuardPeer { + /// Public key, matching the private key of of the remote peer. + pub public_key: PublicKey, + /// Additional key preshared between two peers. Adds an additional layer of symmetric-key + /// cryptography to be mixed into the already existing public-key cryptography, for + /// post-quantum resistance. + pub preshared_key: PresharedKey, + /// List of IPv4/v6 CIDRs from which incoming traffic for this peer is allowed and to which + /// outgoing traffic for this peer is directed. The catch-all 0.0.0.0/0 may be specified for + /// matching all IPv4 addresses, and ::/0 may be specified for matching all IPv6 addresses. + #[serde(rename = "AllowedIPs", skip_serializing_if = "Vec::is_empty")] + pub allowed_ips: Vec, + /// Remote peer endpoint address to connect to. Optional; only needed on the connecting side. + #[serde(skip_serializing_if = "Option::is_none")] + pub endpoint: Option, + /// A seconds interval, between 1 and 65535 inclusive, of how often to send an authenticated + /// empty packet to the peer for the purpose of keeping a stateful firewall or NAT mapping + /// valid persistently. For example, if the interface very rarely sends traffic, but it might + /// at anytime receive traffic from a peer, and it is behind NAT, the interface might benefit + /// from having a persistent keepalive interval of 25 seconds. If unset or set to 0, it is + /// turned off. + #[serde(skip_serializing_if = "persistent_keepalive_is_off")] + pub persistent_keepalive: Option, +} + +/// Determines whether the given `PersistentKeepalive` value means that it is +/// turned off. Useful for usage with serde's `skip_serializing_if`. +fn persistent_keepalive_is_off(value: &Option) -> bool { + value.map(|v| v == 0).unwrap_or(true) +} + +/// Properties of a WireGuard interface. +#[derive(Serialize, Debug)] +#[serde(rename_all = "PascalCase")] +pub struct WireGuardInterface { + /// Private key for this interface. + pub private_key: PrivateKey, + /// Port to listen on. Optional; if not specified, chosen randomly. Only needed on the "server" + /// side. + #[serde(skip_serializing_if = "Option::is_none")] + pub listen_port: Option, + /// Fwmark for outgoing packets. + #[serde(skip_serializing_if = "Option::is_none")] + pub fw_mark: Option, +} + +/// Top-level WireGuard configuration for WireGuard network interface. Holds all +/// parameters for the interface itself, as well as its remote peers. +#[derive(Serialize, Debug)] +#[serde(rename_all = "PascalCase")] +pub struct WireGuardConfig { + /// The WireGuard-specific network interface configuration. + pub interface: WireGuardInterface, + /// Peers for this WireGuard interface. + #[serde(rename = "Peer")] + pub peers: Vec, +} + +impl WireGuardConfig { + /// Generate a raw, INI-style configuration file as accepted by wg(8). + pub fn to_raw_config(self) -> Result { + Ok(proxmox_serde::ini::to_string(&self)?) + } +} + +/// Generates a new ED25519 private key. +#[cfg(feature = "key-generation")] +fn generate_key() -> Result { + let mut secret = ed25519_dalek::SecretKey::default(); + proxmox_sys::linux::fill_with_random_data(&mut secret) + .map_err(|err| Error::KeyGenFailed(err.to_string()))?; + + // [`SigningKey`] takes care of correct key clamping. + Ok(SigningKey::from(&secret).to_bytes()) +} + +#[cfg(test)] +mod tests { + use std::net::Ipv4Addr; + + use proxmox_network_types::ip_address::Cidr; + + use crate::{PresharedKey, PrivateKey, WireGuardConfig, WireGuardInterface, WireGuardPeer}; + + fn mock_private_key(v: u8) -> PrivateKey { + let base = v * 32; + PrivateKey((base..base + 32).collect::>().try_into().unwrap()) + } + + fn mock_preshared_key(v: u8) -> PresharedKey { + let base = v * 32; + PresharedKey((base..base + 32).collect::>().try_into().unwrap()) + } + + #[test] + fn single_peer() { + let config = WireGuardConfig { + interface: WireGuardInterface { + private_key: mock_private_key(0), + listen_port: Some(51820), + fw_mark: Some(127), + }, + peers: vec![WireGuardPeer { + public_key: mock_private_key(1).public_key(), + preshared_key: mock_preshared_key(1), + allowed_ips: vec![Cidr::new_v4(Ipv4Addr::new(192, 168, 0, 0), 24).unwrap()], + endpoint: Some("foo.example.com:51820".parse().unwrap()), + persistent_keepalive: Some(25), + }], + }; + + pretty_assertions::assert_eq!( + config.to_raw_config().unwrap(), + "[Interface] +PrivateKey = AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8= +ListenPort = 51820 +FwMark = 127 + +[Peer] +PublicKey = Kay64UG8yvCyLhqU000LxzYeUm0L/hLIl5S8kyKWbdc= +PresharedKey = ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8= +AllowedIPs = 192.168.0.0/24 +Endpoint = foo.example.com:51820 +PersistentKeepalive = 25 +" + ); + } + + #[test] + fn multiple_peers() { + let config = WireGuardConfig { + interface: WireGuardInterface { + private_key: mock_private_key(0), + listen_port: Some(51820), + fw_mark: None, + }, + peers: vec![ + WireGuardPeer { + public_key: mock_private_key(1).public_key(), + preshared_key: mock_preshared_key(1), + allowed_ips: vec![Cidr::new_v4(Ipv4Addr::new(192, 168, 0, 0), 24).unwrap()], + endpoint: Some("foo.example.com:51820".parse().unwrap()), + persistent_keepalive: None, + }, + WireGuardPeer { + public_key: mock_private_key(2).public_key(), + preshared_key: mock_preshared_key(2), + allowed_ips: vec![Cidr::new_v4(Ipv4Addr::new(192, 168, 1, 0), 24).unwrap()], + endpoint: None, + persistent_keepalive: Some(25), + }, + WireGuardPeer { + public_key: mock_private_key(3).public_key(), + preshared_key: mock_preshared_key(3), + allowed_ips: vec![Cidr::new_v4(Ipv4Addr::new(192, 168, 2, 0), 24).unwrap()], + endpoint: None, + persistent_keepalive: None, + }, + WireGuardPeer { + public_key: mock_private_key(4).public_key(), + preshared_key: Some(mock_preshared_key(4)), + allowed_ips: vec![], + endpoint: Some("10.0.0.1:51820".parse().unwrap()), + persistent_keepalive: Some(25), + }, + ], + }; + + pretty_assertions::assert_eq!( + config.to_raw_config().unwrap(), + "[Interface] +PrivateKey = AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8= +ListenPort = 51820 + +[Peer] +PublicKey = Kay64UG8yvCyLhqU000LxzYeUm0L/hLIl5S8kyKWbdc= +PresharedKey = ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8= +AllowedIPs = 192.168.0.0/24 +Endpoint = foo.example.com:51820 + +[Peer] +PublicKey = JUO5L/EJVRFHatyDadtt3JM2ZaEZeN2hQE7hBmypVZ0= +PresharedKey = QEFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaW1xdXl8= +AllowedIPs = 192.168.1.0/24 +PersistentKeepalive = 25 + +[Peer] +PublicKey = F0VTtFbd38aQjsqxwQH+arIeK6oGF3lbfUOmNIKZP9U= +PresharedKey = YGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn8= +AllowedIPs = 192.168.2.0/24 + +[Peer] +PublicKey = zRSzf5VulTGU/3+3Oz2B3MVh1hp1OAlLfD4aZD7l86o= +PresharedKey = gIGCg4SFhoeIiYqLjI2Oj5CRkpOUlZaXmJmam5ydnp8= +Endpoint = 10.0.0.1:51820 +PersistentKeepalive = 25 +" + ); + } + + #[test] + fn non_listening_peer() { + let config = WireGuardConfig { + interface: WireGuardInterface { + private_key: mock_private_key(0), + listen_port: None, + fw_mark: None, + }, + peers: vec![WireGuardPeer { + public_key: mock_private_key(1).public_key(), + preshared_key: Some(mock_preshared_key(1)), + allowed_ips: vec![Cidr::new_v4(Ipv4Addr::new(192, 168, 0, 0), 24).unwrap()], + endpoint: Some("10.0.0.1:51820".parse().unwrap()), + persistent_keepalive: Some(25), + }], + }; + + pretty_assertions::assert_eq!( + config.to_raw_config().unwrap(), + "[Interface] +PrivateKey = AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8= + +[Peer] +PublicKey = Kay64UG8yvCyLhqU000LxzYeUm0L/hLIl5S8kyKWbdc= +PresharedKey = ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8= +AllowedIPs = 192.168.0.0/24 +Endpoint = 10.0.0.1:51820 +PersistentKeepalive = 25 +" + ); + } + + #[test] + fn empty_peers() { + let config = WireGuardConfig { + interface: WireGuardInterface { + private_key: mock_private_key(0), + listen_port: Some(51830), + fw_mark: Some(240), + }, + peers: vec![], + }; + + pretty_assertions::assert_eq!( + config.to_raw_config().unwrap(), + "[Interface] +PrivateKey = AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8= +ListenPort = 51830 +FwMark = 240 +" + ); + } +} -- 2.52.0