diff --git a/Cargo.lock b/Cargo.lock index b51fc45..7a050b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,14 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +[[package]] +name = "aho-corasick" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81ce3d38065e618af2d7b77e10c5ad9a069859b4be3c2250f674af3840d9c8a5" +dependencies = [ + "memchr", +] + [[package]] name = "aho-corasick" version = "0.7.10" @@ -49,6 +58,12 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" +[[package]] +name = "bitflags" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dead7461c1127cf637931a1e50934eb6eee8bff2f74433ac7909e9afcee04a3" + [[package]] name = "bitflags" version = "1.2.1" @@ -127,8 +142,10 @@ dependencies = [ "anyhow", "futures", "igd", + "iptables", "log", "pretty_env_logger", + "regex 1.3.7", "reqwest", "serde", "serde-lexpr", @@ -160,7 +177,7 @@ dependencies = [ "atty", "humantime", "log", - "regex", + "regex 1.3.7", "termcolor", ] @@ -197,7 +214,7 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" dependencies = [ - "bitflags", + "bitflags 1.2.1", "fuchsia-zircon-sys", ] @@ -492,6 +509,17 @@ dependencies = [ "libc", ] +[[package]] +name = "iptables" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7910549cb022e3112eea631a4c3e62523281b6c61024b2c3a8d61da620010de" +dependencies = [ + "lazy_static 0.2.11", + "nix", + "regex 0.2.11", +] + [[package]] name = "itoa" version = "0.4.5" @@ -517,6 +545,12 @@ dependencies = [ "winapi-build", ] +[[package]] +name = "lazy_static" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f033c7ad61445c5b347c7382dd1237847eb1bce590fe50365dcb33d546be73" + [[package]] name = "lazy_static" version = "1.4.0" @@ -548,9 +582,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.66" +version = "0.2.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558" +checksum = "3baa92041a6fec78c687fa0cc2b3fae8884f743d672cf551bed1d6dac6988d0f" [[package]] name = "log" @@ -626,7 +660,7 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b2df1a4c22fd44a62147fd8f13dd0f95c9d8ca7b2610299b2a2f9cf8964274e" dependencies = [ - "lazy_static", + "lazy_static 1.4.0", "libc", "log", "openssl", @@ -649,6 +683,20 @@ dependencies = [ "winapi 0.3.8", ] +[[package]] +name = "nix" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d95c5fa8b641c10ad0b8887454ebaafa3c92b5cd5350f8fc693adafd178e7b" +dependencies = [ + "bitflags 0.4.0", + "cfg-if", + "libc", + "rustc_version", + "semver", + "void", +] + [[package]] name = "nom" version = "4.2.3" @@ -671,10 +719,10 @@ version = "0.10.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "973293749822d7dd6370d6da1e523b0d1db19f06c459134c658b2a4261378b52" dependencies = [ - "bitflags", + "bitflags 1.2.1", "cfg-if", "foreign-types", - "lazy_static", + "lazy_static 1.4.0", "libc", "openssl-sys", ] @@ -889,16 +937,38 @@ version = "0.1.56" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" +[[package]] +name = "regex" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9329abc99e39129fcceabd24cf5d85b4671ef7c29c50e972bc5afe32438ec384" +dependencies = [ + "aho-corasick 0.6.10", + "memchr", + "regex-syntax 0.5.6", + "thread_local 0.3.6", + "utf8-ranges", +] + [[package]] name = "regex" version = "1.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6020f034922e3194c711b82a627453881bc4682166cabb07134a10c26ba7692" dependencies = [ - "aho-corasick", + "aho-corasick 0.7.10", "memchr", - "regex-syntax", - "thread_local", + "regex-syntax 0.6.17", + "thread_local 1.0.1", +] + +[[package]] +name = "regex-syntax" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d707a4fa2637f2dca2ef9fd02225ec7661fe01a53623c1e6515b6916511f7a7" +dependencies = [ + "ucd-util", ] [[package]] @@ -932,7 +1002,7 @@ dependencies = [ "hyper", "hyper-tls", "js-sys", - "lazy_static", + "lazy_static 1.4.0", "log", "mime", "mime_guess", @@ -952,6 +1022,15 @@ dependencies = [ "winreg", ] +[[package]] +name = "rustc_version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f5376ea5e30ce23c03eb77cbe4962b988deead10910c372b226388b594c084" +dependencies = [ + "semver", +] + [[package]] name = "ryu" version = "1.0.2" @@ -964,7 +1043,7 @@ version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "507a9e6e8ffe0a4e0ebb9a10293e62fdf7657c06f1b8bb07a8fcf697d2abf295" dependencies = [ - "lazy_static", + "lazy_static 1.4.0", "winapi 0.3.8", ] @@ -989,6 +1068,12 @@ dependencies = [ "core-foundation-sys", ] +[[package]] +name = "semver" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4f410fedcf71af0345d7607d246e7ad15faaadd49d240ee3b24e5dc21a820ac" + [[package]] name = "serde" version = "1.0.107" @@ -1094,13 +1179,22 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "thread_local" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b" +dependencies = [ + "lazy_static 1.4.0", +] + [[package]] name = "thread_local" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" dependencies = [ - "lazy_static", + "lazy_static 1.4.0", ] [[package]] @@ -1116,14 +1210,14 @@ dependencies = [ [[package]] name = "tokio" -version = "0.2.11" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fdd17989496f49cdc57978c96f0c9fe5e4a58a8bddc6813c449a4624f6a030b" +checksum = "d099fa27b9702bed751524694adbe393e18b36b204da91eb1cbbbbb4a5ee2d58" dependencies = [ "bytes 0.5.4", "fnv", "iovec", - "lazy_static", + "lazy_static 1.4.0", "memchr", "mio", "pin-project-lite", @@ -1178,6 +1272,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e604eb7b43c06650e854be16a2a03155743d3752dd1c943f6829e26b7a36e382" +[[package]] +name = "ucd-util" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c85f514e095d348c279b1e5cd76795082cf15bd59b93207832abe0b1d8fed236" + [[package]] name = "unicase" version = "2.6.0" @@ -1239,6 +1339,12 @@ dependencies = [ "percent-encoding 2.1.0", ] +[[package]] +name = "utf8-ranges" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ae116fef2b7fea257ed6440d3cfcff7f190865f170cdad00bb6465bf18ecba" + [[package]] name = "vcpkg" version = "0.2.8" @@ -1257,6 +1363,12 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "078775d0255232fb988e6fccf26ddc9d1ac274299aaedcedce21c6f72cc533ce" +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + [[package]] name = "want" version = "0.3.0" @@ -1292,7 +1404,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11cdb95816290b525b32587d76419facd99662a07e59d3cdb560488a819d9a45" dependencies = [ "bumpalo", - "lazy_static", + "lazy_static 1.4.0", "log", "proc-macro2", "quote", @@ -1447,7 +1559,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c1cb601d29fe2c2ac60a2b2e5e293994d87a1f6fa9687a31a15270f909be9c2" dependencies = [ - "bitflags", + "bitflags 1.2.1", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index ce0fbc3..a2a9667 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,9 +11,11 @@ reqwest = { version = "0.10", features = ["json"] } igd = { version = "0.10.0", features = ["aio"] } log = "0.4" pretty_env_logger = "0.4" -tokio = "0.2.11" +tokio = "0.2" futures = "0.3.5" serde = { version = "1.0.107", features = ["derive"] } serde_json = "1.0.53" serde-lexpr = "0.1.1" anyhow = "1.0.28" +iptables = "0.2.2" +regex = "1" diff --git a/Dockerfile b/Dockerfile index 41c7da9..f34dd2c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,6 +18,6 @@ COPY ./src ./src RUN cargo build --release FROM debian:bullseye-slim -RUN apt-get update && apt-get install -y libssl1.1 +RUN apt-get update && apt-get install -y libssl1.1 iptables COPY --from=builder /srv/target/release/diplonat /usr/local/sbin/diplonat CMD ["/usr/local/sbin/diplonat"] diff --git a/README.md b/README.md index 518061b..3fb0f52 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Diplonat ## Feature set * [X] (Re)Configure NAT via UPNP/IGD (prio: high) - * [ ] (Re)Configure nftable (prio: low) + * [X] (Re)Configure iptables (prio: low) * [ ] (Re)Configure DNS via ??? (prio: low) ## Understand scope @@ -17,11 +17,24 @@ Diplonat ## Operate +You need to add the following to your nomad config file : + +``` +client { + [...] + + options { + docker.privileged.enabled = "true" + } +} +``` + + ```bash cargo build consul agent -dev # in a separate terminal -# adapt following values to your configuratio +# adapt following values to your configuration export DIPLONAT_PRIVATE_IP="192.168.0.18" export DIPLONAT_REFRESH_TIME="60" export DIPLONAT_EXPIRATION_TIME="300" diff --git a/src/consul_actor.rs b/src/consul_actor.rs index 1cbb1b8..ba5d704 100644 --- a/src/consul_actor.rs +++ b/src/consul_actor.rs @@ -8,11 +8,12 @@ use serde::{Serialize, Deserialize}; use serde_lexpr::{from_str,error}; use crate::messages; use crate::consul; +use std::collections::HashSet; #[derive(Serialize, Deserialize, Debug)] pub enum DiplonatParameter { - tcp_port(Vec), - udp_port(Vec) + tcp_port(HashSet), + udp_port(HashSet) } #[derive(Serialize, Deserialize, Debug)] @@ -53,8 +54,8 @@ fn to_parameters(catalog: &consul::CatalogNode) -> Vec { fn to_open_ports(params: &Vec) -> messages::PublicExposedPorts { let mut op = messages::PublicExposedPorts { - tcp_ports: Vec::new(), - udp_ports: Vec::new() + tcp_ports: HashSet::new(), + udp_ports: HashSet::new() }; for conf in params { @@ -73,8 +74,8 @@ fn to_open_ports(params: &Vec) -> messages::PublicExposedPorts { impl ConsulActor { pub fn new(url: &str, node: &str) -> Self { let (tx, rx) = watch::channel(messages::PublicExposedPorts{ - tcp_ports: Vec::new(), - udp_ports: Vec::new() + tcp_ports: HashSet::new(), + udp_ports: HashSet::new() }); return Self { diff --git a/src/diplonat.rs b/src/diplonat.rs index 1be00f7..798b779 100644 --- a/src/diplonat.rs +++ b/src/diplonat.rs @@ -1,13 +1,14 @@ use anyhow::Result; -use log::*; use tokio::try_join; use crate::consul_actor::ConsulActor; use crate::igd_actor::IgdActor; use crate::environment::Environment; +use crate::fw_actor::FirewallActor; pub struct Diplonat { consul: ConsulActor, - igd: IgdActor + igd: IgdActor, + firewall: FirewallActor } impl Diplonat { @@ -21,9 +22,15 @@ impl Diplonat { &ca.rx_open_ports ).await?; + let fw = FirewallActor::new( + env.refresh_time, + &ca.rx_open_ports + ).await?; + let ctx = Self { consul: ca, - igd: ia + igd: ia, + firewall: fw }; return Ok(ctx); @@ -32,7 +39,8 @@ impl Diplonat { pub async fn listen(&mut self) -> Result<()> { try_join!( self.consul.listen(), - self.igd.listen() + self.igd.listen(), + self.firewall.listen() )?; return Ok(()); diff --git a/src/fw.rs b/src/fw.rs new file mode 100644 index 0000000..bc4d119 --- /dev/null +++ b/src/fw.rs @@ -0,0 +1,81 @@ +use iptables; +use regex::Regex; +use std::collections::HashSet; +use crate::messages; +use anyhow::{Result,Context}; +use log::*; + +pub fn setup(ipt: &iptables::IPTables) -> Result<()> { + + // ensure we start from a clean state without any rule already set + cleanup(ipt)?; + + ipt.new_chain("filter", "DIPLONAT").context("Failed to create new chain")?; + ipt.insert_unique("filter", "INPUT", "-j DIPLONAT", 1).context("Failed to insert jump rule")?; + + Ok(()) +} + +pub fn open_ports(ipt: &iptables::IPTables, ports: messages::PublicExposedPorts) -> Result<()> { + for p in ports.tcp_ports { + ipt.append("filter", "DIPLONAT", &format!("-p tcp --dport {} -j ACCEPT", p)).context("Failed to insert port rule")?; + } + + for p in ports.udp_ports { + ipt.append("filter", "DIPLONAT", &format!("-p udp --dport {} -j ACCEPT", p)).context("Failed to insert port rule")?; + } + + Ok(()) +} + +pub fn get_opened_ports(ipt: &iptables::IPTables) -> Result { + let mut ports = messages::PublicExposedPorts { + tcp_ports: HashSet::new(), + udp_ports: HashSet::new() + }; + + let list = ipt.list("filter", "DIPLONAT")?; + let re = Regex::new(r"\-A.*? \-p (\w+).*\-\-dport (\d+).*?\-j ACCEPT").context("Regex matching open ports encountered an unexpected rule")?; + for i in list { + let caps = re.captures(&i); + match caps { + Some(c) => { + + if let (Some(raw_proto), Some(raw_port)) = (c.get(1), c.get(2)) { + + let proto = String::from(raw_proto.as_str()); + let number = String::from(raw_port.as_str()).parse::()?; + + if proto == "tcp" { + ports.tcp_ports.insert(number); + } else { + ports.udp_ports.insert(number); + } + + } else { + error!("Unexpected rule found in DIPLONAT chain") + } + + }, + _ => {} + } + } + + Ok(ports) +} + +pub fn cleanup(ipt: &iptables::IPTables) -> Result<()> { + + if ipt.chain_exists("filter", "DIPLONAT")? { + ipt.flush_chain("filter", "DIPLONAT").context("Failed to flush the DIPLONAT chain")?; + + if ipt.exists("filter", "INPUT", "-j DIPLONAT")? { + ipt.delete("filter", "INPUT", "-j DIPLONAT").context("Failed to delete jump rule")?; + } + + ipt.delete_chain("filter", "DIPLONAT").context("Failed to delete chain")?; + } + + Ok(()) +} + diff --git a/src/fw_actor.rs b/src/fw_actor.rs new file mode 100644 index 0000000..b5e4c7e --- /dev/null +++ b/src/fw_actor.rs @@ -0,0 +1,75 @@ +use anyhow::Result; +use tokio::{ + select, + sync::watch, + time::{ + self, + Duration +}}; +use log::*; + +use iptables; +use crate::messages; +use crate::fw; +use std::collections::HashSet; + +pub struct FirewallActor { + pub ipt: iptables::IPTables, + rx_ports: watch::Receiver, + last_ports: messages::PublicExposedPorts, + refresh: Duration +} + +impl FirewallActor { + pub async fn new(_refresh: Duration, rxp: &watch::Receiver) -> Result { + let ctx = Self { + ipt: iptables::new(false)?, + rx_ports: rxp.clone(), + last_ports: messages::PublicExposedPorts::new(), + refresh: _refresh, + }; + + fw::setup(&ctx.ipt)?; + + return Ok(ctx); + } + + pub async fn listen(&mut self) -> Result<()> { + let mut interval = time::interval(self.refresh); + loop { + // 1. Wait for an event + let new_ports = select! { + Some(ports) = self.rx_ports.recv() => Some(ports), + _ = interval.tick() => None, + else => return Ok(()) // Sender dropped, terminate loop. + }; + + // 2. Update last ports if needed + if let Some(p) = new_ports { self.last_ports = p; } + + // 3. Update firewall rules + match self.do_fw_update().await { + Ok(()) => debug!("Successfully updated firewall rules"), + Err(e) => error!("An error occured while updating firewall rules. {}", e), + } + } + } + + pub async fn do_fw_update(&self) -> Result<()> { + let curr_opened_ports = fw::get_opened_ports(&self.ipt)?; + + let diff_tcp = self.last_ports.tcp_ports.difference(&curr_opened_ports.tcp_ports).copied().collect::>(); + let diff_udp = self.last_ports.udp_ports.difference(&curr_opened_ports.udp_ports).copied().collect::>(); + + let ports_to_open = messages::PublicExposedPorts { + tcp_ports: diff_tcp, + udp_ports: diff_udp + }; + + fw::open_ports(&self.ipt, ports_to_open)?; + + return Ok(()); + } + +} + diff --git a/src/main.rs b/src/main.rs index a35916a..ca36c26 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,8 @@ mod consul; mod consul_actor; mod igd_actor; mod diplonat; +mod fw; +mod fw_actor; use log::*; use diplonat::Diplonat; @@ -12,7 +14,7 @@ use diplonat::Diplonat; async fn main() { pretty_env_logger::init(); info!("Starting Diplonat"); - + let mut diplo = Diplonat::new().await.expect("Setup failed"); diplo.listen().await.expect("A runtime error occured"); } diff --git a/src/messages.rs b/src/messages.rs index 31ed48f..09a7c14 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -1,14 +1,16 @@ -#[derive(Debug, Clone)] +use std::collections::HashSet; + +#[derive(Debug, Clone, PartialEq, Eq)] pub struct PublicExposedPorts { - pub tcp_ports: Vec, - pub udp_ports: Vec + pub tcp_ports: HashSet, + pub udp_ports: HashSet } impl PublicExposedPorts { pub fn new() -> Self { return Self { - tcp_ports: Vec::new(), - udp_ports: Vec::new() + tcp_ports: HashSet::new(), + udp_ports: HashSet::new() } } }