Almost working port forwarding, but foiled by serde-urlencoded once again

This commit is contained in:
asonix 2020-09-04 22:24:04 -05:00
commit 04ece4cca8
12 changed files with 3127 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

2043
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

26
Cargo.toml Normal file
View 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
View file

@ -0,0 +1,8 @@
[interface]
external = "eth[0-9]+"
internal = [
"enp1s0"
]
[network]
shared-internal = true

0
scss/index.scss Normal file
View file

10
src/build.rs Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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>