Almost working port forwarding, but foiled by serde-urlencoded once again
This commit is contained in:
commit
04ece4cca8
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/target
|
2043
Cargo.lock
generated
Normal file
2043
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
26
Cargo.toml
Normal file
26
Cargo.toml
Normal file
|
@ -0,0 +1,26 @@
|
|||
[package]
|
||||
name = "router"
|
||||
version = "0.1.0"
|
||||
authors = ["asonix <asonix@asonix.dog>"]
|
||||
edition = "2018"
|
||||
build = "src/build.rs"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
async-process = "0.1.3"
|
||||
blocking = "0.6.1"
|
||||
config = { version = "0.10.1", features = ["toml"] }
|
||||
futures-lite = "1.0.0"
|
||||
mime = "0.3"
|
||||
once_cell = "1.4.1"
|
||||
regex = "1.3.9"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
sled = { version = "0.34.3", features = ["compression"] }
|
||||
tide = "0.13.0"
|
||||
|
||||
[build-dependencies]
|
||||
anyhow = "1.0"
|
||||
ructe = { version = "0.12.0", features = ["sass", "mime03"] }
|
8
config.toml
Normal file
8
config.toml
Normal file
|
@ -0,0 +1,8 @@
|
|||
[interface]
|
||||
external = "eth[0-9]+"
|
||||
internal = [
|
||||
"enp1s0"
|
||||
]
|
||||
|
||||
[network]
|
||||
shared-internal = true
|
0
scss/index.scss
Normal file
0
scss/index.scss
Normal file
10
src/build.rs
Normal file
10
src/build.rs
Normal file
|
@ -0,0 +1,10 @@
|
|||
use ructe::Ructe;
|
||||
|
||||
fn main() -> Result<(), anyhow::Error> {
|
||||
let mut ructe = Ructe::from_env()?;
|
||||
let mut statics = ructe.statics()?;
|
||||
statics.add_sass_file("scss/index.scss")?;
|
||||
ructe.compile_templates("templates")?;
|
||||
|
||||
Ok(())
|
||||
}
|
302
src/iptables.rs
Normal file
302
src/iptables.rs
Normal file
|
@ -0,0 +1,302 @@
|
|||
use async_process::Command;
|
||||
use std::{fmt, net::Ipv4Addr};
|
||||
|
||||
#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize)]
|
||||
pub(crate) enum Proto {
|
||||
Tcp,
|
||||
Udp,
|
||||
}
|
||||
|
||||
impl Proto {
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Proto::Tcp => "tcp",
|
||||
Proto::Udp => "udp",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Proto {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Proto::Tcp => write!(f, "TCP"),
|
||||
Proto::Udp => write!(f, "UDP"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn input_accept(
|
||||
external_interface: &str,
|
||||
external_ip: Ipv4Addr,
|
||||
external_port: u16,
|
||||
external_mask: u8,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
input(
|
||||
external_interface,
|
||||
external_ip,
|
||||
external_port,
|
||||
external_mask,
|
||||
move |cmd| cmd.arg("-I"),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_input_accept(
|
||||
external_interface: &str,
|
||||
external_ip: Ipv4Addr,
|
||||
external_port: u16,
|
||||
external_mask: u8,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
input(
|
||||
external_interface,
|
||||
external_ip,
|
||||
external_port,
|
||||
external_mask,
|
||||
move |cmd| cmd.arg("-D"),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn input(
|
||||
external_interface: &str,
|
||||
external_ip: Ipv4Addr,
|
||||
external_port: u16,
|
||||
external_mask: u8,
|
||||
func: impl Fn(&mut Command) -> &mut Command,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
iptables(move |cmd| {
|
||||
func(cmd).args(&[
|
||||
"INPUT",
|
||||
"-d",
|
||||
&format!("{}/{}", external_ip, external_mask),
|
||||
"-i",
|
||||
external_interface,
|
||||
"-p",
|
||||
"tcp",
|
||||
"-m",
|
||||
"conntrack",
|
||||
"--ctstate",
|
||||
"NEW,RELATED,ESTABLISHED",
|
||||
"-m",
|
||||
"tcp",
|
||||
"--dport",
|
||||
&external_port.to_string(),
|
||||
"-j",
|
||||
"ACCEPT",
|
||||
])
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn forward_accept(
|
||||
external_interface: &str,
|
||||
internal_interface: &str,
|
||||
proto: Proto,
|
||||
external_port: u16,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
forward(
|
||||
external_interface,
|
||||
internal_interface,
|
||||
proto,
|
||||
external_port,
|
||||
move |cmd| cmd.arg("-I"),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_forward_accept(
|
||||
external_interface: &str,
|
||||
internal_interface: &str,
|
||||
proto: Proto,
|
||||
external_port: u16,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
forward(
|
||||
external_interface,
|
||||
internal_interface,
|
||||
proto,
|
||||
external_port,
|
||||
move |cmd| cmd.arg("-I"),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn forward(
|
||||
external_interface: &str,
|
||||
internal_interface: &str,
|
||||
proto: Proto,
|
||||
external_port: u16,
|
||||
func: impl Fn(&mut Command) -> &mut Command,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
iptables(move |cmd| {
|
||||
func(cmd).args(&[
|
||||
"FORWARD",
|
||||
"-i",
|
||||
external_interface,
|
||||
"-o",
|
||||
internal_interface,
|
||||
"-p",
|
||||
proto.as_str(),
|
||||
"--dport",
|
||||
&external_port.to_string(),
|
||||
"-m",
|
||||
"conntrack",
|
||||
"--ctstate",
|
||||
"NEW,ESTABLISHED,RELATED",
|
||||
"-j",
|
||||
"ACCEPT",
|
||||
])
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn forward_postrouting(
|
||||
external_ip: Ipv4Addr,
|
||||
external_port: u16,
|
||||
destination_ip: Ipv4Addr,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
forward_postrouting_snat(external_ip, external_port, destination_ip, |cmd| {
|
||||
cmd.arg("-I")
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_forward_postrouting(
|
||||
external_ip: Ipv4Addr,
|
||||
external_port: u16,
|
||||
destination_ip: Ipv4Addr,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
forward_postrouting_snat(external_ip, external_port, destination_ip, |cmd| {
|
||||
cmd.arg("-D")
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn forward_postrouting_snat(
|
||||
external_ip: Ipv4Addr,
|
||||
external_port: u16,
|
||||
destination_ip: Ipv4Addr,
|
||||
func: impl Fn(&mut Command) -> &mut Command,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
iptables_nat(move |cmd| {
|
||||
func(cmd).args(&[
|
||||
"POSTROUTING",
|
||||
"-d",
|
||||
&destination_ip.to_string(),
|
||||
"-p",
|
||||
"tcp",
|
||||
"-m",
|
||||
"tcp",
|
||||
"--dport",
|
||||
&external_port.to_string(),
|
||||
"-m",
|
||||
"conntrack",
|
||||
"--ctstate",
|
||||
"NEW,RELATED,ESTABLISHED",
|
||||
"-j",
|
||||
"SNAT",
|
||||
"--to-source",
|
||||
&external_ip.to_string(),
|
||||
])
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn forward_prerouting(
|
||||
proto: Proto,
|
||||
external_ip: Ipv4Addr,
|
||||
external_mask: u8,
|
||||
external_port: u16,
|
||||
destination_ip: Ipv4Addr,
|
||||
destination_port: u16,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
forward_prerouting_dnat(
|
||||
proto,
|
||||
external_ip,
|
||||
external_mask,
|
||||
external_port,
|
||||
destination_ip,
|
||||
destination_port,
|
||||
|cmd| cmd.arg("-I"),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_forward_prerouting(
|
||||
proto: Proto,
|
||||
external_ip: Ipv4Addr,
|
||||
external_mask: u8,
|
||||
external_port: u16,
|
||||
destination_ip: Ipv4Addr,
|
||||
destination_port: u16,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
forward_prerouting_dnat(
|
||||
proto,
|
||||
external_ip,
|
||||
external_mask,
|
||||
external_port,
|
||||
destination_ip,
|
||||
destination_port,
|
||||
|cmd| cmd.arg("-D"),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn forward_prerouting_dnat(
|
||||
proto: Proto,
|
||||
external_ip: Ipv4Addr,
|
||||
external_mask: u8,
|
||||
external_port: u16,
|
||||
destination_ip: Ipv4Addr,
|
||||
destination_port: u16,
|
||||
func: impl Fn(&mut Command) -> &mut Command,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
iptables_nat(move |cmd| {
|
||||
func(cmd).args(&[
|
||||
"PREROUTING",
|
||||
"-p",
|
||||
proto.as_str(),
|
||||
"-d",
|
||||
&format!("{}/{}", external_ip, external_mask),
|
||||
"--dport",
|
||||
&external_port.to_string(),
|
||||
"-m",
|
||||
"conntrack",
|
||||
"--ctstate",
|
||||
"NEW,ESTABLISHED,RELATED",
|
||||
"-j",
|
||||
"DNAT",
|
||||
"--to",
|
||||
format!("{}:{}", destination_ip, destination_port).as_str(),
|
||||
])
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn iptables_nat<F>(func: F) -> Result<(), anyhow::Error>
|
||||
where
|
||||
F: Fn(&mut Command) -> &mut Command,
|
||||
{
|
||||
iptables(move |cmd| func(cmd.args(&["-t", "nat"]))).await
|
||||
}
|
||||
|
||||
async fn iptables<F>(func: F) -> Result<(), anyhow::Error>
|
||||
where
|
||||
F: Fn(&mut Command) -> &mut Command,
|
||||
{
|
||||
let mut command = Command::new("iptables");
|
||||
|
||||
func(&mut command);
|
||||
|
||||
let mut child = command.spawn()?;
|
||||
|
||||
let status = child.status().await?;
|
||||
|
||||
if !status.success() {
|
||||
return Err(anyhow::Error::msg(format!(
|
||||
"Command failed with status {}",
|
||||
status
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
84
src/main.rs
Normal file
84
src/main.rs
Normal file
|
@ -0,0 +1,84 @@
|
|||
use blocking::unblock;
|
||||
use futures_lite::*;
|
||||
use once_cell::sync::Lazy;
|
||||
use sled::Db;
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/templates.rs"));
|
||||
|
||||
mod iptables;
|
||||
mod rules;
|
||||
mod startup;
|
||||
|
||||
use self::{rules::Rule, startup::Interfaces};
|
||||
|
||||
static INTERFACES: Lazy<Interfaces> = Lazy::new(|| {
|
||||
let interfaces = Interfaces::init_blocking().unwrap();
|
||||
interfaces.reset_blocking().unwrap();
|
||||
interfaces
|
||||
});
|
||||
|
||||
static DB: Lazy<Db> = Lazy::new(|| sled::open("router-db-0-34-3").unwrap());
|
||||
|
||||
async fn rules_page(_: tide::Request<()>) -> tide::Result {
|
||||
let mut html = Vec::new();
|
||||
|
||||
let rules = unblock(move || rules::read(&DB)).await?;
|
||||
|
||||
templates::rules(&mut html, &rules)?;
|
||||
|
||||
Ok(tide::Response::builder(200)
|
||||
.body(html)
|
||||
.content_type(
|
||||
"text/html;charset=utf-8"
|
||||
.parse::<tide::http::Mime>()
|
||||
.unwrap(),
|
||||
)
|
||||
.build())
|
||||
}
|
||||
|
||||
async fn save_rule(mut req: tide::Request<()>) -> tide::Result {
|
||||
let rule: Rule = req.body_form().await?;
|
||||
|
||||
rules::save(&DB, &rule)?;
|
||||
rules::apply(&INTERFACES, rule).await?;
|
||||
|
||||
Ok(to_rules_page())
|
||||
}
|
||||
|
||||
async fn delete_rule(req: tide::Request<()>) -> tide::Result {
|
||||
let id = req.param("id")?;
|
||||
let rule = rules::delete(&DB, id)?;
|
||||
rules::unset(&INTERFACES, rule).await?;
|
||||
|
||||
Ok(to_rules_page())
|
||||
}
|
||||
|
||||
fn to_rules_page() -> tide::Response {
|
||||
tide::Response::builder(301)
|
||||
.header("Location", "/rules")
|
||||
.build()
|
||||
}
|
||||
|
||||
fn main() -> Result<(), anyhow::Error> {
|
||||
future::block_on(async {
|
||||
println!("Hello, world!");
|
||||
|
||||
rules::apply_all(&DB, &INTERFACES).await?;
|
||||
|
||||
let mut app = tide::new();
|
||||
app.at("/rules").get(rules_page).post(save_rule);
|
||||
app.at("/rules/:id").delete(delete_rule);
|
||||
|
||||
let listeners: Vec<String> = INTERFACES
|
||||
.internal
|
||||
.iter()
|
||||
.map(|info| format!("{}:8080", info.ip))
|
||||
.collect();
|
||||
|
||||
app.listen(listeners).await?;
|
||||
|
||||
Ok(()) as Result<(), anyhow::Error>
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
148
src/rules.rs
Normal file
148
src/rules.rs
Normal file
|
@ -0,0 +1,148 @@
|
|||
use crate::{
|
||||
iptables::{self, Proto},
|
||||
startup::Interfaces,
|
||||
};
|
||||
use sled::Db;
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
pub struct Rule {
|
||||
pub(crate) proto: Proto,
|
||||
pub(crate) port: u16,
|
||||
pub(crate) kind: RuleKind,
|
||||
}
|
||||
|
||||
impl Rule {
|
||||
pub(crate) fn as_forward(&self) -> Option<(Ipv4Addr, u16)> {
|
||||
match &self.kind {
|
||||
RuleKind::Forward { dest_ip, dest_port } => Some((*dest_ip, *dest_port)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub(crate) enum RuleKind {
|
||||
Accept,
|
||||
Forward { dest_ip: Ipv4Addr, dest_port: u16 },
|
||||
}
|
||||
|
||||
pub(crate) async fn apply_all(db: &Db, interfaces: &Interfaces) -> Result<(), anyhow::Error> {
|
||||
for (_, rule) in read(db)? {
|
||||
apply(interfaces, rule).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn read(db: &Db) -> Result<Vec<(String, Rule)>, anyhow::Error> {
|
||||
db.iter()
|
||||
.map(|res| {
|
||||
let (id, rule) = res?;
|
||||
|
||||
let id = String::from_utf8_lossy(&id).to_string();
|
||||
let rule: Rule = serde_json::from_slice(&rule)?;
|
||||
|
||||
Ok((id, rule)) as Result<(String, Rule), anyhow::Error>
|
||||
})
|
||||
.collect::<Result<Vec<_>, anyhow::Error>>()
|
||||
}
|
||||
|
||||
pub(crate) fn delete(db: &Db, rule_id: String) -> Result<Rule, anyhow::Error> {
|
||||
let rule = db
|
||||
.remove(rule_id.as_bytes())?
|
||||
.ok_or(anyhow::anyhow!("No rule with id {}", rule_id))?;
|
||||
|
||||
let rule: Rule = serde_json::from_slice(&rule)?;
|
||||
|
||||
Ok(rule)
|
||||
}
|
||||
|
||||
pub(crate) async fn unset(interfaces: &Interfaces, rule: Rule) -> Result<(), anyhow::Error> {
|
||||
match rule.kind {
|
||||
RuleKind::Accept => {
|
||||
iptables::delete_input_accept(
|
||||
&interfaces.external.interface,
|
||||
interfaces.external.ip,
|
||||
rule.port,
|
||||
interfaces.external.mask,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
RuleKind::Forward { dest_ip, dest_port } => {
|
||||
for info in &interfaces.internal {
|
||||
iptables::delete_forward_accept(
|
||||
&interfaces.external.interface,
|
||||
&info.interface,
|
||||
rule.proto,
|
||||
rule.port,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
iptables::delete_forward_prerouting(
|
||||
rule.proto,
|
||||
interfaces.external.ip,
|
||||
interfaces.external.mask,
|
||||
rule.port,
|
||||
dest_ip,
|
||||
dest_port,
|
||||
)
|
||||
.await?;
|
||||
iptables::delete_forward_postrouting(interfaces.external.ip, rule.port, dest_ip)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn save(db: &Db, rule: &Rule) -> Result<(), anyhow::Error> {
|
||||
let s = serde_json::to_string(rule)?;
|
||||
let id = db.generate_id()?;
|
||||
|
||||
db.insert(rule_id(id).as_bytes(), s.as_bytes())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn apply(interfaces: &Interfaces, rule: Rule) -> Result<(), anyhow::Error> {
|
||||
match rule.kind {
|
||||
RuleKind::Accept => {
|
||||
iptables::input_accept(
|
||||
&interfaces.external.interface,
|
||||
interfaces.external.ip,
|
||||
rule.port,
|
||||
interfaces.external.mask,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
RuleKind::Forward { dest_ip, dest_port } => {
|
||||
for info in &interfaces.internal {
|
||||
iptables::forward_accept(
|
||||
&interfaces.external.interface,
|
||||
&info.interface,
|
||||
rule.proto,
|
||||
rule.port,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
iptables::forward_prerouting(
|
||||
rule.proto,
|
||||
interfaces.external.ip,
|
||||
interfaces.external.mask,
|
||||
rule.port,
|
||||
dest_ip,
|
||||
dest_port,
|
||||
)
|
||||
.await?;
|
||||
iptables::forward_postrouting(interfaces.external.ip, rule.port, dest_ip).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn rule_id(id: u64) -> String {
|
||||
format!("rule-{}", id)
|
||||
}
|
185
src/startup/mod.rs
Normal file
185
src/startup/mod.rs
Normal file
|
@ -0,0 +1,185 @@
|
|||
use anyhow::anyhow;
|
||||
use regex::Regex;
|
||||
use std::{
|
||||
io::Write,
|
||||
net::Ipv4Addr,
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
|
||||
mod preload;
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct Config {
|
||||
interface: InterfaceConfig,
|
||||
network: NetworkConfig,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct InterfaceConfig {
|
||||
external: String,
|
||||
internal: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
struct NetworkConfig {
|
||||
shared_internal: bool,
|
||||
}
|
||||
|
||||
pub(crate) struct Interfaces {
|
||||
pub(crate) external: InterfaceInfo,
|
||||
pub(crate) internal: Vec<InterfaceInfo>,
|
||||
shared_internal: bool,
|
||||
}
|
||||
|
||||
pub(crate) struct InterfaceInfo {
|
||||
pub(crate) interface: String,
|
||||
pub(crate) ip: Ipv4Addr,
|
||||
pub(crate) mask: u8,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
fn read() -> Result<Self, anyhow::Error> {
|
||||
let mut config = config::Config::new();
|
||||
config.merge(config::File::with_name("config.toml"))?;
|
||||
|
||||
Ok(config.try_into()?)
|
||||
}
|
||||
}
|
||||
|
||||
impl Interfaces {
|
||||
pub(crate) fn init_blocking() -> Result<Self, anyhow::Error> {
|
||||
let config = Config::read()?;
|
||||
|
||||
let output = Command::new("ip").arg("addr").output()?;
|
||||
let output = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
let external = parse_interface_info(&output, &config.interface.external)?
|
||||
.next()
|
||||
.ok_or(anyhow!(
|
||||
"Failed to parse IP for interface {}",
|
||||
config.interface.external,
|
||||
))?;
|
||||
|
||||
let mut internal = Vec::new();
|
||||
|
||||
for iface in &config.interface.internal {
|
||||
internal.extend(parse_interface_info(&output, &iface)?);
|
||||
}
|
||||
|
||||
if internal.len() == 0 {
|
||||
return Err(anyhow!(
|
||||
"No internal interfaces found for {:?}",
|
||||
config.interface.internal
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Interfaces {
|
||||
external,
|
||||
internal,
|
||||
shared_internal: config.network.shared_internal,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn reset_blocking(&self) -> Result<(), anyhow::Error> {
|
||||
let firewall_rules = self::preload::firewall_rules(self);
|
||||
|
||||
println!("LOADING RULES");
|
||||
println!("{}", firewall_rules);
|
||||
|
||||
let mut child = Command::new("iptables-restore")
|
||||
.stdin(Stdio::piped())
|
||||
.spawn()?;
|
||||
|
||||
let stdin = child
|
||||
.stdin
|
||||
.as_mut()
|
||||
.expect("Failed to open STDIN (shouldn't happen)");
|
||||
stdin.write_all(firewall_rules.as_bytes())?;
|
||||
|
||||
child.wait()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_interface_info<'a>(
|
||||
output: &'a str,
|
||||
interface: &str,
|
||||
) -> Result<impl Iterator<Item = InterfaceInfo> + 'a, anyhow::Error> {
|
||||
let iface_regx = Regex::new(interface)?;
|
||||
let ip_regex = Regex::new(r"([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})/([0-9]+)")?;
|
||||
|
||||
let iter = output.split('\n').filter_map(move |line| {
|
||||
let iface_capture = iface_regx.captures_iter(line).next()?;
|
||||
let iface_match = iface_capture.get(0)?;
|
||||
|
||||
let ip_capture = ip_regex.captures_iter(line).next()?;
|
||||
let ip_match = ip_capture.get(1)?;
|
||||
let mask_match = ip_capture.get(2)?;
|
||||
|
||||
let ip = ip_match.as_str().parse().ok()?;
|
||||
let mask = mask_match.as_str().parse().ok()?;
|
||||
|
||||
Some(InterfaceInfo {
|
||||
interface: iface_match.as_str().to_owned(),
|
||||
mask,
|
||||
ip,
|
||||
})
|
||||
});
|
||||
|
||||
Ok(iter)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::parse_interface_info;
|
||||
|
||||
const OUTPUT: &'static str = r#"1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
|
||||
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
|
||||
inet 127.0.0.1/8 scope host lo
|
||||
valid_lft forever preferred_lft forever
|
||||
inet6 ::1/128 scope host
|
||||
valid_lft forever preferred_lft forever
|
||||
2: enp1s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
|
||||
link/ether 24:4b:fe:37:ad:b1 brd ff:ff:ff:ff:ff:ff
|
||||
inet 192.168.6.1/24 brd 192.168.5.255 scope global enp1s0
|
||||
valid_lft forever preferred_lft forever
|
||||
inet6 fe80::264b:feff:fe37:adb1/64 scope link
|
||||
valid_lft forever preferred_lft forever
|
||||
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
|
||||
link/ether ca:b8:a7:51:07:3d brd ff:ff:ff:ff:ff:ff
|
||||
inet 136.49.5.58/20 brd 136.49.15.255 scope global dynamic eth1
|
||||
valid_lft 85973sec preferred_lft 85973sec
|
||||
inet6 fe80::c8b8:a7ff:fe51:73d/64 scope link
|
||||
valid_lft forever preferred_lft forever
|
||||
4: wg0: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000
|
||||
link/none
|
||||
inet 192.168.5.0/24 scope global wg0
|
||||
valid_lft forever preferred_lft forever
|
||||
"#;
|
||||
|
||||
#[test]
|
||||
fn parses_external_interface() {
|
||||
let info = parse_interface_info(OUTPUT, "eth[0-9]+")
|
||||
.unwrap()
|
||||
.next()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(info.interface, "eth1");
|
||||
assert_eq!(info.ip.to_string(), "136.49.5.58");
|
||||
assert_eq!(info.mask, 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_internal_interface() {
|
||||
let info = parse_interface_info(OUTPUT, "enp1s0")
|
||||
.unwrap()
|
||||
.next()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(info.interface, "enp1s0");
|
||||
assert_eq!(info.ip.to_string(), "192.168.6.1");
|
||||
assert_eq!(info.mask, 24);
|
||||
}
|
||||
}
|
259
src/startup/preload.rs
Normal file
259
src/startup/preload.rs
Normal file
|
@ -0,0 +1,259 @@
|
|||
use crate::startup::Interfaces;
|
||||
|
||||
static UNIVERSE: &'static str = "0.0.0.0/0";
|
||||
|
||||
pub(crate) fn firewall_rules(interfaces: &Interfaces) -> String {
|
||||
filter(interfaces) + "\n" + &nat(interfaces)
|
||||
}
|
||||
|
||||
// FILTER table rules
|
||||
fn filter(interfaces: &Interfaces) -> String {
|
||||
let mut filter = String::from(
|
||||
r#"*filter
|
||||
:INPUT DROP [0:0]
|
||||
:FORWARD DROP [0:0]
|
||||
:OUTPUT DROP [0:0]
|
||||
|
||||
"#,
|
||||
);
|
||||
|
||||
// INPUT: Incoming traffic from various interfaces
|
||||
|
||||
// Accept everything on loopback
|
||||
filter += &format!(
|
||||
"-A INPUT -i lo -s {universe} -d {universe} -j ACCEPT\n",
|
||||
universe = UNIVERSE
|
||||
);
|
||||
|
||||
// Allow internal machines to connect to anything
|
||||
for iface in &interfaces.internal {
|
||||
filter += &format!(
|
||||
"-A INPUT -i {intif} -s {intip}/{intmask} -d {universe} -j ACCEPT\n",
|
||||
intif = iface.interface,
|
||||
intip = iface.ip,
|
||||
intmask = iface.mask,
|
||||
universe = UNIVERSE
|
||||
);
|
||||
}
|
||||
|
||||
// Disallow IP spoofing, internal IPs should only come from the internal network
|
||||
// If an internal IP is seen by the external interface, it's BAD!!!!
|
||||
for iface in &interfaces.internal {
|
||||
filter += &format!(
|
||||
"-A INPUT -i {extif} -s {intip}/{intmask} -d {universe} -j REJECT\n",
|
||||
extif = interfaces.external.interface,
|
||||
intip = iface.ip,
|
||||
intmask = iface.mask,
|
||||
universe = UNIVERSE,
|
||||
);
|
||||
}
|
||||
|
||||
// Allow ICMP traffic on external interface
|
||||
filter += &format!(
|
||||
"-A INPUT -i {extif} -p ICMP -s {universe} -d {extip}/{extmask} -j ACCEPT\n",
|
||||
extif = interfaces.external.interface,
|
||||
universe = UNIVERSE,
|
||||
extip = interfaces.external.ip,
|
||||
extmask = interfaces.external.mask,
|
||||
);
|
||||
|
||||
// Don't prevent existing connections for continuing
|
||||
filter += &format!(
|
||||
"-A INPUT -i {extif} -s {universe} -d {extip}/{extmask} -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT\n",
|
||||
extif = interfaces.external.interface,
|
||||
universe = UNIVERSE,
|
||||
extip = interfaces.external.ip,
|
||||
extmask = interfaces.external.mask,
|
||||
);
|
||||
|
||||
// Allow DHCP traffic
|
||||
for iface in &interfaces.internal {
|
||||
filter += &format!(
|
||||
"-A INPUT -i {intif} -p tcp --sport 68 --dport 67 -j ACCEPT\n",
|
||||
intif = iface.interface,
|
||||
);
|
||||
|
||||
filter += &format!(
|
||||
"-A INPUT -i {intif} -p udp --sport 68 --dport 67 -j ACCEPT\n",
|
||||
intif = iface.interface,
|
||||
);
|
||||
}
|
||||
|
||||
filter += &format!(
|
||||
"-A INPUT -s {universe} -d {universe} -j REJECT\n",
|
||||
universe = UNIVERSE
|
||||
);
|
||||
|
||||
// OUTPUT: Outgoing traffic from vairous interfaces
|
||||
|
||||
// netfilter bug workaround
|
||||
filter += "-A OUTPUT -m conntrack -p icmp --ctstate INVALID -j DROP\n";
|
||||
|
||||
// Accept everything on loopback
|
||||
filter += &format!(
|
||||
"-A OUTPUT -o lo -s {universe} -d {universe} -j ACCEPT\n",
|
||||
universe = UNIVERSE,
|
||||
);
|
||||
|
||||
if interfaces.shared_internal {
|
||||
for iface in &interfaces.internal {
|
||||
// jface (jeans iface)
|
||||
for jface in &interfaces.internal {
|
||||
// Allow internal traffic across all internal interfaces
|
||||
filter += &format!(
|
||||
"-A OUTPUT -o {intif} -s {extip}/{extmask} -d {jntip}/{jntmask} -j ACCEPT\n",
|
||||
intif = iface.interface,
|
||||
extip = interfaces.external.ip,
|
||||
extmask = interfaces.external.mask,
|
||||
jntip = jface.ip, // jeans IP
|
||||
jntmask = jface.mask, // jeans mask
|
||||
);
|
||||
|
||||
// Allow internal traffic from self to internal networks
|
||||
filter += &format!(
|
||||
"-A OUTPUT -o {intif} -s {intip}/32 -d {jntip}/{jntmask} -j ACCEPT\n",
|
||||
intif = iface.interface,
|
||||
intip = iface.ip,
|
||||
jntip = jface.ip, // jeans IP
|
||||
jntmask = jface.mask, // jeans mask
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for iface in &interfaces.internal {
|
||||
// Allow internal traffic only on network associated with interface
|
||||
filter += &format!(
|
||||
"-A OUTPUT -o {intif} -s {extip}/{extmask} -d {intip}/{intmask} -j ACCEPT\n",
|
||||
intif = iface.interface,
|
||||
extip = interfaces.external.ip,
|
||||
extmask = interfaces.external.mask,
|
||||
intip = iface.ip,
|
||||
intmask = iface.mask,
|
||||
);
|
||||
|
||||
// Allow traffic from self to networks associated with interface
|
||||
filter += &format!(
|
||||
"-A OUTPUT -o {intif} -s {intip}/32 -d {intip}/{intmask} -j ACCEPT\n",
|
||||
intif = iface.interface,
|
||||
intip = iface.ip,
|
||||
intmask = iface.mask,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for iface in &interfaces.internal {
|
||||
// Deny traffic to internal networks on external interface
|
||||
filter += &format!(
|
||||
"-A OUTPUT -o {extif} -s {universe} -d {intip}/{intmask} -j REJECT\n",
|
||||
extif = interfaces.external.interface,
|
||||
universe = UNIVERSE,
|
||||
intip = iface.ip,
|
||||
intmask = iface.mask,
|
||||
);
|
||||
}
|
||||
|
||||
// Allow traffic out from external interface to anywhere
|
||||
filter += &format!(
|
||||
"-A OUTPUT -o {extif} -s {extip}/{extmask} -d {universe} -j ACCEPT\n",
|
||||
extif = interfaces.external.interface,
|
||||
extip = interfaces.external.ip,
|
||||
extmask = interfaces.external.mask,
|
||||
universe = UNIVERSE,
|
||||
);
|
||||
|
||||
for iface in &interfaces.internal {
|
||||
// Allow DHCP traffic out from internal interfaces
|
||||
filter += &format!(
|
||||
"-A OUTPUT -o {intif} -p tcp -s {intip} --sport 67 -d 255.255.255.255 --dport 68 -j ACCEPT\n",
|
||||
intif = iface.interface,
|
||||
intip = iface.ip,
|
||||
);
|
||||
|
||||
filter += &format!(
|
||||
"-A OUTPUT -o {intif} -p udp -s {intip} --sport 67 -d 255.255.255.255 --dport 68 -j ACCEPT\n",
|
||||
intif = iface.interface,
|
||||
intip = iface.ip,
|
||||
);
|
||||
}
|
||||
|
||||
// Reject traffic we don't care about
|
||||
filter += &format!(
|
||||
"-A OUTPUT -s {universe} -d {universe} -j REJECT\n",
|
||||
universe = UNIVERSE
|
||||
);
|
||||
|
||||
// FORWARD: Forwarding traffic across interfaces
|
||||
|
||||
// Accept TCP packets
|
||||
for iface in &interfaces.internal {
|
||||
filter += &format!(
|
||||
"-A FORWARD -i {extif} -o {intif} -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT\n",
|
||||
extif = interfaces.external.interface,
|
||||
intif = iface.interface,
|
||||
);
|
||||
}
|
||||
|
||||
if interfaces.shared_internal {
|
||||
for iface in &interfaces.internal {
|
||||
// jface (jeans interface)
|
||||
for jface in &interfaces.internal {
|
||||
// Allow packets across internal interfaces
|
||||
filter += &format!(
|
||||
"-A FORWARD -i {intif} -o {jntif} -j ACCEPT\n",
|
||||
intif = iface.interface,
|
||||
jntif = jface.interface, // jntif (jeans intif)
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for iface in &interfaces.internal {
|
||||
// Allow packets across internal interface
|
||||
filter += &format!(
|
||||
"-A FORWARD -i {intif} -o {intif} -j ACCEPT\n",
|
||||
intif = iface.interface,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Forward packets to the internet
|
||||
for iface in &interfaces.internal {
|
||||
filter += &format!(
|
||||
"-A FORWARD -i {intif} -o {extif} -j ACCEPT\n",
|
||||
intif = iface.interface,
|
||||
extif = interfaces.external.interface,
|
||||
);
|
||||
}
|
||||
|
||||
filter += "-A FORWARD -j REJECT\n";
|
||||
filter += "COMMIT\n";
|
||||
|
||||
filter
|
||||
}
|
||||
|
||||
// NAT Table rules
|
||||
fn nat(interfaces: &Interfaces) -> String {
|
||||
let mut nat = String::from(
|
||||
r#"*nat
|
||||
:PREROUTING ACCEPT [0:0]
|
||||
:POSTROUTING ACCEPT [0:0]
|
||||
:OUTPUT ACCEPT [0:0]
|
||||
|
||||
"#,
|
||||
);
|
||||
|
||||
nat += &format!(
|
||||
"-A POSTROUTING -o {extif} -j MASQUERADE\n",
|
||||
extif = interfaces.external.interface
|
||||
);
|
||||
|
||||
nat += "COMMIT\n";
|
||||
|
||||
nat
|
||||
}
|
||||
|
||||
// TODO: SSH
|
||||
// # Internal interface, SSH traffic accepted on port 3128
|
||||
// -A INPUT -i $INTIF -p tcp --dport 3128 -j ACCEPT
|
||||
//
|
||||
// # External interface, SSH traffic allowed on port 3128
|
||||
// -A INPUT -i $EXTIF -m conntrack -p tcp -s $UNIVERSE -d $EXTIP --dport 3128 -j ACCEPT
|
61
templates/rules.rs.html
Normal file
61
templates/rules.rs.html
Normal file
|
@ -0,0 +1,61 @@
|
|||
@use crate::rules::Rule;
|
||||
|
||||
@(rules: &[(String, Rule)])
|
||||
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Rules</title>
|
||||
</head>
|
||||
<body>
|
||||
<table>
|
||||
<thead>
|
||||
<th>Kind</th>
|
||||
<th>Protocol</th>
|
||||
<th>Port</th>
|
||||
<th>Destination</th>
|
||||
<th></th>
|
||||
</thead>
|
||||
</tbody>
|
||||
@for (id, rule) in rules {
|
||||
@if let Some((dest_ip, dest_port)) = rule.as_forward() {
|
||||
<td>Forward</td>
|
||||
<td>@rule.proto</td>
|
||||
<td>@rule.port</td>
|
||||
<td>@dest_ip</td>
|
||||
<td>@dest_port</td>
|
||||
} else {
|
||||
<td>Accept</td>
|
||||
<td>@rule.proto</td>
|
||||
<td>@rule.port</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
}
|
||||
<td><a href="/rules/@id">Delete</a></td>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
<form method="POST" action="/rules">
|
||||
<select name="proto">
|
||||
<option value="Tcp">TCP</option>
|
||||
<option value="Udp">UDP</option>
|
||||
</select>
|
||||
<label for="ext_port">
|
||||
<h4>External Port</h4>
|
||||
<input name="ext_port" type="integer" />
|
||||
</label>
|
||||
<input name="kind[type]" value="Forward" type="hidden" />
|
||||
<label for="kind[dest_port]">
|
||||
<h4>Internal Port</h4>
|
||||
<input name="kind[dest_port]" type="integer" />
|
||||
</label>
|
||||
<label for="kind[dest_ip]">
|
||||
<h4>IP Address</h4>
|
||||
<input name="kind[dest_ip]" type="text" />
|
||||
</label>
|
||||
<button type="submit">Forward!</button>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in a new issue