diff options
author | Himbeer <himbeer@disroot.org> | 2024-08-20 15:27:11 +0200 |
---|---|---|
committer | Himbeer <himbeer@disroot.org> | 2024-08-20 15:27:11 +0200 |
commit | fd91cfa85a704cc02346aa7131901858e36a0fff (patch) | |
tree | ce98d8477c7a036548e25e3b467cf6e17146ed94 | |
parent | 4023acc763120b29190437f6622d3a2129cb1fbc (diff) |
Implement dynamic NPT for VPNs
-rw-r--r-- | Cargo.lock | 90 | ||||
-rw-r--r-- | Cargo.toml | 3 | ||||
-rw-r--r-- | src/error.rs | 4 | ||||
-rw-r--r-- | src/main.rs | 297 |
4 files changed, 377 insertions, 17 deletions
@@ -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]] @@ -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(()) } |