aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHimbeer <himbeer@disroot.org>2024-08-20 15:27:11 +0200
committerHimbeer <himbeer@disroot.org>2024-08-20 15:27:11 +0200
commitfd91cfa85a704cc02346aa7131901858e36a0fff (patch)
treece98d8477c7a036548e25e3b467cf6e17146ed94
parent4023acc763120b29190437f6622d3a2129cb1fbc (diff)
Implement dynamic NPT for VPNs
-rw-r--r--Cargo.lock90
-rw-r--r--Cargo.toml3
-rw-r--r--src/error.rs4
-rw-r--r--src/main.rs297
4 files changed, 377 insertions, 17 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 65fe6df..dc5f985 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -66,7 +66,7 @@ dependencies = [
"regex",
"rustc-hash",
"shlex",
- "syn 2.0.36",
+ "syn 2.0.75",
"which",
]
@@ -194,6 +194,12 @@ dependencies = [
]
[[package]]
+name = "itoa"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
+
+[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -314,7 +320,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d"
dependencies = [
"proc-macro2",
- "syn 2.0.36",
+ "syn 2.0.75",
]
[[package]]
@@ -343,18 +349,18 @@ dependencies = [
[[package]]
name = "proc-macro2"
-version = "1.0.67"
+version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328"
+checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
-version = "1.0.28"
+version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488"
+checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
dependencies = [
"proc-macro2",
]
@@ -382,11 +388,22 @@ version = "0.7.0-dev"
dependencies = [
"failure",
"ipnetwork",
+ "rsdsl_pd_config",
"rustables",
+ "serde_json",
+ "signal-hook",
"thiserror",
]
[[package]]
+name = "rsdsl_pd_config"
+version = "0.1.0"
+source = "git+https://github.com/rsdsl/pd_config.git#6cf4dbd588730396d5d03f5d01ad88ff37bf79b0"
+dependencies = [
+ "serde",
+]
+
+[[package]]
name = "rustables"
version = "0.14.0"
source = "git+https://github.com/rsdsl/rustables.git#8ee510d4cde3aed93b9ebde46938e9a7df51290c"
@@ -440,10 +457,42 @@ dependencies = [
]
[[package]]
+name = "ryu"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
+
+[[package]]
name = "serde"
-version = "1.0.183"
+version = "1.0.208"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.208"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32ac8da02677876d532745a130fc9d8e6edfa81a269b107c5b00829b91d8eb3c"
+checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.75",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.125"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed"
+dependencies = [
+ "itoa",
+ "memchr",
+ "ryu",
+ "serde",
+]
[[package]]
name = "shlex"
@@ -452,6 +501,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380"
[[package]]
+name = "signal-hook"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
+dependencies = [
+ "libc",
+ "signal-hook-registry",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
+dependencies = [
+ "libc",
+]
+
+[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -464,9 +532,9 @@ dependencies = [
[[package]]
name = "syn"
-version = "2.0.36"
+version = "2.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "91e02e55d62894af2a08aca894c6577281f76769ba47c94d5756bec8ac6e7373"
+checksum = "f6af063034fc1935ede7be0122941bafa9bacb949334d090b77ca98b5817c7d9"
dependencies = [
"proc-macro2",
"quote",
@@ -502,7 +570,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.36",
+ "syn 2.0.75",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index cca6ed6..1a15758 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -8,5 +8,8 @@ edition = "2021"
[dependencies]
failure = "0.1.8"
ipnetwork = "0.20.0"
+rsdsl_pd_config = { git = "https://github.com/rsdsl/pd_config.git", version = "0.1.0" }
rustables = { git = "https://github.com/rsdsl/rustables.git", version = "0.14.0" }
+serde_json = "1.0"
+signal-hook = "0.3.17"
thiserror = "1.0"
diff --git a/src/error.rs b/src/error.rs
index 3431850..4fcba44 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -2,6 +2,8 @@ use thiserror::Error;
#[derive(Debug, Error)]
pub enum Error {
+ #[error("io: {0}")]
+ Io(#[from] std::io::Error),
#[error("parse ip address: {0}")]
AddrParse(#[from] std::net::AddrParseError),
#[error("ipnetwork: {0}")]
@@ -10,6 +12,8 @@ pub enum Error {
RustablesBuilder(#[from] rustables::error::BuilderError),
#[error("rustables query: {0}")]
RustablesQuery(#[from] rustables::error::QueryError),
+ #[error("serde_json: {0}")]
+ SerdeJson(#[from] serde_json::Error),
}
pub type Result<T> = std::result::Result<T, Error>;
diff --git a/src/main.rs b/src/main.rs
index 6398e66..5d660f9 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,14 +1,32 @@
use rsdsl_netfilterd::error::Result;
-use std::net::Ipv4Addr;
-use std::thread;
-use std::time::Duration;
+use std::fs::File;
+use std::net::{Ipv4Addr, Ipv6Addr};
-use ipnetwork::Ipv4Network;
+use ipnetwork::{IpNetwork, Ipv4Network, Ipv6Network};
+use rsdsl_pd_config::PdConfig;
+use rustables::expr::{
+ Bitwise, HighLevelPayload, IPv6HeaderField, Nat, NatType, NetworkHeaderField, Register,
+};
use rustables::{
Batch, Chain, ChainPolicy, ChainType, Hook, HookClass, MsgType, Protocol, ProtocolFamily, Rule,
Table,
};
+use signal_hook::{consts::SIGUSR1, iterator::Signals};
+
+const ULA: Ipv6Addr = Ipv6Addr::new(0xfd0b, 0x9272, 0x534e, 0, 0, 0, 0, 0);
+const VPN_ULA: Ipv6Addr = Ipv6Addr::new(0xfd0b, 0x9272, 0x534e, 6, 0, 0, 0, 0);
+const EXPOSED_VPN_ULA: Ipv6Addr = Ipv6Addr::new(0xfd0b, 0x9272, 0x534e, 7, 0, 0, 0, 0);
+
+#[derive(Debug)]
+struct Npt {
+ postrouting: Chain,
+ prerouting: Chain,
+ map_vpn_to_gua: Rule,
+ map_exposed_vpn_to_gua: Rule,
+ map_gua_to_vpn: Rule,
+ map_gua_to_exposed_vpn: Rule,
+}
fn nat() -> Result<()> {
let mut batch = Batch::new();
@@ -395,6 +413,244 @@ fn filter() -> Result<()> {
Ok(())
}
+fn enable_npt(prefix: Ipv6Addr) -> Result<Npt> {
+ let vpn_net = IpNetwork::V6(Ipv6Network::new(VPN_ULA, 64).unwrap());
+ let exposed_vpn_net: IpNetwork = IpNetwork::V6(Ipv6Network::new(EXPOSED_VPN_ULA, 64).unwrap());
+
+ let ifid_mask = vec![
+ 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ ];
+ let everything_mask = vec![0xff; 16];
+
+ let mut vpn_segments = prefix.segments();
+ vpn_segments[3] += 6;
+ let vpn_prefix = Ipv6Addr::from(vpn_segments);
+ let vpn_subnet = IpNetwork::V6(Ipv6Network::new(vpn_prefix, 64).unwrap());
+
+ let mut exposed_vpn_segments = prefix.segments();
+ exposed_vpn_segments[3] += 7;
+ let exposed_vpn_prefix = Ipv6Addr::from(exposed_vpn_segments);
+ let exposed_vpn_subnet = IpNetwork::V6(Ipv6Network::new(exposed_vpn_prefix, 64).unwrap());
+
+ let mut batch = Batch::new();
+
+ let npt = Table::new(ProtocolFamily::Ipv6).with_name("npt");
+ batch.add(&npt, MsgType::Add);
+
+ // +-------------------+
+ // | POSTROUTING chain |
+ // +-------------------+
+
+ let mut postrouting = Chain::new(&npt).with_name("POSTROUTING");
+
+ postrouting.set_type(ChainType::Nat);
+ postrouting.set_hook(Hook::new(HookClass::PostRouting, 100));
+ postrouting.set_policy(ChainPolicy::Accept);
+
+ batch.add(&postrouting, MsgType::Add);
+
+ let map_vpn_to_gua = Rule::new(&postrouting)?
+ .oface("ppp0")?
+ .snetwork(vpn_net)?
+ .with_expr(
+ HighLevelPayload::Network(NetworkHeaderField::IPv6(IPv6HeaderField::Saddr)).build(),
+ )
+ .with_expr(Bitwise::new(ifid_mask.clone(), 0u128.to_be_bytes())?)
+ .with_expr(Bitwise::new(everything_mask.clone(), vpn_prefix.octets())?)
+ .with_expr(Nat {
+ nat_type: Some(NatType::SNat),
+ family: Some(ProtocolFamily::Ipv6),
+ ip_register: Some(Register::Reg1),
+ port_register: None,
+ });
+ batch.add(&map_vpn_to_gua, MsgType::Add);
+
+ let map_exposed_vpn_to_gua = Rule::new(&postrouting)?
+ .oface("ppp0")?
+ .snetwork(exposed_vpn_net)?
+ .with_expr(
+ HighLevelPayload::Network(NetworkHeaderField::IPv6(IPv6HeaderField::Saddr)).build(),
+ )
+ .with_expr(Bitwise::new(ifid_mask.clone(), 0u128.to_be_bytes())?)
+ .with_expr(Bitwise::new(
+ everything_mask.clone(),
+ exposed_vpn_prefix.octets(),
+ )?)
+ .with_expr(Nat {
+ nat_type: Some(NatType::SNat),
+ family: Some(ProtocolFamily::Ipv6),
+ ip_register: Some(Register::Reg1),
+ port_register: None,
+ });
+ batch.add(&map_exposed_vpn_to_gua, MsgType::Add);
+
+ // +------------------+
+ // | PREROUTING chain |
+ // +------------------+
+
+ let mut prerouting = Chain::new(&npt).with_name("PREROUTING");
+
+ prerouting.set_type(ChainType::Nat);
+ prerouting.set_hook(Hook::new(HookClass::PreRouting, -100));
+ prerouting.set_policy(ChainPolicy::Accept);
+
+ batch.add(&prerouting, MsgType::Add);
+
+ let map_gua_to_vpn = Rule::new(&prerouting)?
+ .iface("ppp0")?
+ .dnetwork(vpn_subnet)?
+ .with_expr(
+ HighLevelPayload::Network(NetworkHeaderField::IPv6(IPv6HeaderField::Daddr)).build(),
+ )
+ .with_expr(Bitwise::new(ifid_mask.clone(), 0u128.to_be_bytes())?)
+ .with_expr(Bitwise::new(everything_mask.clone(), VPN_ULA.octets())?)
+ .with_expr(Nat {
+ nat_type: Some(NatType::DNat),
+ family: Some(ProtocolFamily::Ipv6),
+ ip_register: Some(Register::Reg1),
+ port_register: None,
+ });
+ batch.add(&map_gua_to_vpn, MsgType::Add);
+
+ let map_gua_to_exposed_vpn = Rule::new(&prerouting)?
+ .iface("ppp0")?
+ .dnetwork(exposed_vpn_subnet)?
+ .with_expr(
+ HighLevelPayload::Network(NetworkHeaderField::IPv6(IPv6HeaderField::Daddr)).build(),
+ )
+ .with_expr(Bitwise::new(ifid_mask, 0u128.to_be_bytes())?)
+ .with_expr(Bitwise::new(everything_mask, EXPOSED_VPN_ULA.octets())?)
+ .with_expr(Nat {
+ nat_type: Some(NatType::DNat),
+ family: Some(ProtocolFamily::Ipv6),
+ ip_register: Some(Register::Reg1),
+ port_register: None,
+ });
+ batch.add(&map_gua_to_exposed_vpn, MsgType::Add);
+
+ batch.send()?;
+ Ok(Npt {
+ postrouting,
+ prerouting,
+ map_vpn_to_gua,
+ map_exposed_vpn_to_gua,
+ map_gua_to_vpn,
+ map_gua_to_exposed_vpn,
+ })
+}
+
+fn update_npt(npt: &mut Npt, prefix: Ipv6Addr) -> Result<()> {
+ let vpn_net = IpNetwork::V6(Ipv6Network::new(VPN_ULA, 64).unwrap());
+ let exposed_vpn_net: IpNetwork = IpNetwork::V6(Ipv6Network::new(EXPOSED_VPN_ULA, 64).unwrap());
+
+ let ifid_mask = vec![
+ 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ ];
+ let everything_mask = vec![0xff; 16];
+
+ let mut vpn_segments = prefix.segments();
+ vpn_segments[3] += 6;
+ let vpn_prefix = Ipv6Addr::from(vpn_segments);
+ let vpn_subnet = IpNetwork::V6(Ipv6Network::new(vpn_prefix, 64).unwrap());
+
+ let mut exposed_vpn_segments = prefix.segments();
+ exposed_vpn_segments[3] += 7;
+ let exposed_vpn_prefix = Ipv6Addr::from(exposed_vpn_segments);
+ let exposed_vpn_subnet = IpNetwork::V6(Ipv6Network::new(exposed_vpn_prefix, 64).unwrap());
+
+ let mut batch = Batch::new();
+
+ batch.add(&npt.map_vpn_to_gua, MsgType::Del);
+ batch.add(&npt.map_exposed_vpn_to_gua, MsgType::Del);
+ batch.add(&npt.map_gua_to_vpn, MsgType::Del);
+ batch.add(&npt.map_gua_to_exposed_vpn, MsgType::Del);
+
+ // +-------------------+
+ // | POSTROUTING chain |
+ // +-------------------+
+
+ npt.map_vpn_to_gua = Rule::new(&npt.postrouting)?
+ .oface("ppp0")?
+ .snetwork(vpn_net)?
+ .with_expr(
+ HighLevelPayload::Network(NetworkHeaderField::IPv6(IPv6HeaderField::Saddr)).build(),
+ )
+ .with_expr(Bitwise::new(ifid_mask.clone(), 0u128.to_be_bytes())?)
+ .with_expr(Bitwise::new(everything_mask.clone(), vpn_prefix.octets())?)
+ .with_expr(Nat {
+ nat_type: Some(NatType::SNat),
+ family: Some(ProtocolFamily::Ipv6),
+ ip_register: Some(Register::Reg1),
+ port_register: None,
+ });
+ batch.add(&npt.map_vpn_to_gua, MsgType::Add);
+
+ npt.map_exposed_vpn_to_gua = Rule::new(&npt.postrouting)?
+ .oface("ppp0")?
+ .snetwork(exposed_vpn_net)?
+ .with_expr(
+ HighLevelPayload::Network(NetworkHeaderField::IPv6(IPv6HeaderField::Saddr)).build(),
+ )
+ .with_expr(Bitwise::new(ifid_mask.clone(), 0u128.to_be_bytes())?)
+ .with_expr(Bitwise::new(
+ everything_mask.clone(),
+ exposed_vpn_prefix.octets(),
+ )?)
+ .with_expr(Nat {
+ nat_type: Some(NatType::SNat),
+ family: Some(ProtocolFamily::Ipv6),
+ ip_register: Some(Register::Reg1),
+ port_register: None,
+ });
+ batch.add(&npt.map_exposed_vpn_to_gua, MsgType::Add);
+
+ // +------------------+
+ // | PREROUTING chain |
+ // +------------------+
+
+ npt.map_gua_to_vpn = Rule::new(&npt.prerouting)?
+ .iface("ppp0")?
+ .dnetwork(vpn_subnet)?
+ .with_expr(
+ HighLevelPayload::Network(NetworkHeaderField::IPv6(IPv6HeaderField::Daddr)).build(),
+ )
+ .with_expr(Bitwise::new(ifid_mask.clone(), 0u128.to_be_bytes())?)
+ .with_expr(Bitwise::new(everything_mask.clone(), VPN_ULA.octets())?)
+ .with_expr(Nat {
+ nat_type: Some(NatType::DNat),
+ family: Some(ProtocolFamily::Ipv6),
+ ip_register: Some(Register::Reg1),
+ port_register: None,
+ });
+ batch.add(&npt.map_gua_to_vpn, MsgType::Add);
+
+ npt.map_gua_to_exposed_vpn = Rule::new(&npt.prerouting)?
+ .iface("ppp0")?
+ .dnetwork(exposed_vpn_subnet)?
+ .with_expr(
+ HighLevelPayload::Network(NetworkHeaderField::IPv6(IPv6HeaderField::Daddr)).build(),
+ )
+ .with_expr(Bitwise::new(ifid_mask, 0u128.to_be_bytes())?)
+ .with_expr(Bitwise::new(everything_mask, EXPOSED_VPN_ULA.octets())?)
+ .with_expr(Nat {
+ nat_type: Some(NatType::DNat),
+ family: Some(ProtocolFamily::Ipv6),
+ ip_register: Some(Register::Reg1),
+ port_register: None,
+ });
+ batch.add(&npt.map_gua_to_exposed_vpn, MsgType::Add);
+
+ batch.send()?;
+ Ok(())
+}
+
+fn read_prefix() -> Result<Ipv6Addr> {
+ let mut file = File::open(rsdsl_pd_config::LOCATION)?;
+ let pdconfig: PdConfig = serde_json::from_reader(&mut file)?;
+
+ Ok(pdconfig.prefix)
+}
+
fn main() -> Result<()> {
match nat() {
Ok(_) => println!("enable nat"),
@@ -412,7 +668,36 @@ fn main() -> Result<()> {
}
}
- loop {
- thread::sleep(Duration::MAX);
+ let prefix = read_prefix().unwrap_or(ULA);
+
+ let mut npt = match enable_npt(prefix) {
+ Ok(npt) => {
+ println!("enable npt");
+ npt
+ }
+ Err(e) => {
+ println!("can't enable npt: {}", e);
+ return Err(e);
+ }
+ };
+
+ let mut signals = match Signals::new([SIGUSR1]) {
+ Ok(signals) => signals,
+ Err(e) => {
+ println!("signal handling: {}", e);
+ return Err(e.into());
+ }
+ };
+
+ for _ in signals.forever() {
+ let prefix = read_prefix().unwrap_or(ULA);
+
+ match update_npt(&mut npt, prefix) {
+ Ok(_) => println!("update npt"),
+ Err(e) => println!("can't update npt: {}", e),
+ }
}
+
+ println!("no more signals");
+ Ok(())
}