Working and okay-looking port forward/accept screen

This commit is contained in:
asonix 2020-09-08 20:26:40 -05:00
parent 04ece4cca8
commit ab42817c2b
7 changed files with 424 additions and 169 deletions

186
Cargo.lock generated
View file

@ -155,30 +155,30 @@ dependencies = [
"libc",
"once_cell",
"parking 2.0.0",
"polling",
"polling 0.1.9",
"socket2",
"vec-arena",
"vec-arena 0.5.2",
"wepoll-sys-stjepang",
"winapi",
]
[[package]]
name = "async-io"
version = "0.2.7"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9424bb88867b003ca32a1d99cf64595c5a310c7322891795f38aa061860d0af"
checksum = "016a7f0eda7091ef24ad8562d6503ad8da47af8c432d4d3fa440eea9e89055fe"
dependencies = [
"cfg-if",
"concurrent-queue",
"fastrand",
"futures-lite 1.0.0",
"futures-lite 1.3.0",
"libc",
"log",
"once_cell",
"parking 2.0.0",
"polling",
"polling 1.0.1",
"socket2",
"vec-arena",
"vec-arena 1.0.0",
"waker-fn",
"wepoll-sys-stjepang",
"winapi",
@ -186,24 +186,24 @@ dependencies = [
[[package]]
name = "async-mutex"
version = "1.2.0"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "065de1ccf10280d0d75c2f3a71a970ee1007c85c51aa3e7deee1df100f1dfadb"
checksum = "66941c2577c4fa351e4ce5fdde8f86c69b88d623f3b955be1bc7362a23434632"
dependencies = [
"event-listener",
]
[[package]]
name = "async-process"
version = "0.1.3"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23d510baf319291a41e6c3363df4408a3b196ad18fcb1305ae4e500f2eabe260"
checksum = "0bb915df28b8309139bd9c9c700d84c20e5c21385d05378caa84912332d0f6a1"
dependencies = [
"async-io 0.2.7",
"blocking 0.6.1",
"async-io 1.0.1",
"blocking 1.0.0",
"cfg-if",
"event-listener",
"futures-lite 1.0.0",
"futures-lite 1.3.0",
"once_cell",
"signal-hook",
"winapi",
@ -391,14 +391,14 @@ dependencies = [
[[package]]
name = "blocking"
version = "0.6.1"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f30e08a950487f80d2de5cbb72772c8bbaed6002dc8d979722dabd034ede18d"
checksum = "2640778f8053e72c11f621b0a5175a0560a269282aa98ed85107773ab8e2a556"
dependencies = [
"async-channel",
"atomic-waker",
"fastrand",
"futures-lite 1.0.0",
"futures-lite 1.3.0",
"once_cell",
"waker-fn",
]
@ -442,9 +442,6 @@ name = "cc"
version = "1.0.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66120af515773fb005778dc07c261bd201ec8ce50bd6e7144c927753fe013381"
dependencies = [
"jobserver",
]
[[package]]
name = "cfg-if"
@ -523,7 +520,7 @@ dependencies = [
"percent-encoding",
"rand",
"sha2",
"time 0.2.17",
"time 0.2.18",
"version_check",
]
@ -708,9 +705,9 @@ dependencies = [
[[package]]
name = "futures-lite"
version = "1.0.0"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dfd8ccee4974dccf68838bb2b9c90439238090061c82454af83866ac059eb9f"
checksum = "4fc6854fcb40c6446abf6043e82604e42567dcf3d652a5ff4e997fc36876414c"
dependencies = [
"fastrand",
"futures-core",
@ -813,12 +810,6 @@ version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aaf91faf136cb47367fa430cd46e37a788775e7fa104f8b4bcb3861dc389b724"
[[package]]
name = "glob"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
[[package]]
name = "gloo-timers"
version = "0.2.1"
@ -875,7 +866,7 @@ dependencies = [
"rand",
"serde 1.0.115",
"serde_json",
"serde_qs",
"serde_qs 0.6.1",
"serde_urlencoded",
"url",
]
@ -924,15 +915,6 @@ version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6"
[[package]]
name = "jobserver"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c71313ebb9439f74b00d9d2dcec36440beaf57a6aa0623068441dd7cd81a7f2"
dependencies = [
"libc",
]
[[package]]
name = "js-sys"
version = "0.3.44"
@ -1231,9 +1213,22 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "polling"
version = "0.1.8"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1e9fa0ab21ed700cf0c4ebec57ae5496bec942a0aef9545562979a9f75b97aa"
checksum = "8fffa183f6bd5f1a8a3e1f60ce2f8d5621e350eed84a62d6daaa5b9d1aaf6fbd"
dependencies = [
"cfg-if",
"libc",
"log",
"wepoll-sys-stjepang",
"winapi",
]
[[package]]
name = "polling"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0835fa5f9af34c170eb38638ae6bc88e1b11ecdd0b968c9d9de8e343450385eb"
dependencies = [
"cfg-if",
"libc",
@ -1365,15 +1360,17 @@ version = "0.1.0"
dependencies = [
"anyhow",
"async-process",
"blocking 0.6.1",
"blocking 1.0.0",
"config",
"futures-lite 1.0.0",
"futures-lite 1.3.0",
"mime",
"once_cell",
"regex",
"ructe",
"serde 1.0.115",
"serde_json",
"serde_qs 0.7.0",
"serde_with",
"sled",
"tide",
]
@ -1529,6 +1526,18 @@ dependencies = [
"serde 1.0.115",
]
[[package]]
name = "serde_qs"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9408a61dabe404c76cec504ec510f7d92f41dc0a9362a0db8ab73d141cfbf93f"
dependencies = [
"data-encoding",
"percent-encoding",
"serde 1.0.115",
"thiserror",
]
[[package]]
name = "serde_test"
version = "0.8.23"
@ -1550,6 +1559,27 @@ dependencies = [
"url",
]
[[package]]
name = "serde_with"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89d3d595d64120bbbc70b7f6d5ae63298b62a3d9f373ec2f56acf5365ca8a444"
dependencies = [
"serde 1.0.115",
"serde_with_macros",
]
[[package]]
name = "serde_with_macros"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4070d2c9b9d258465ad1d82aabb985b84cd9a3afa94da25ece5a9938ba5f1606"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "sha1"
version = "0.6.0"
@ -1609,7 +1639,6 @@ dependencies = [
"libc",
"log",
"parking_lot",
"zstd",
]
[[package]]
@ -1702,21 +1731,41 @@ checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0"
[[package]]
name = "subtle"
version = "2.2.3"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "502d53007c02d7605a05df1c1a73ee436952781653da5d0bf57ad608f66932c1"
checksum = "343f3f510c2915908f155e94f17220b19ccfacf2a64a2a5d8004f2c3e311e7fd"
[[package]]
name = "syn"
version = "1.0.39"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "891d8d6567fe7c7f8835a3a98af4208f3846fba258c1bc3c31d6e506239f11f9"
checksum = "963f7d3cc59b59b9325165add223142bbf1df27655d07789f109896d353d8350"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
]
[[package]]
name = "thiserror"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dfdd070ccd8ccb78f4ad66bf1982dc37f620ef696c6b5028fe2ed83dd3d0d08"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd80fc12f73063ac132ac92aceea36734f04a1d93c1240c6944e23a3b8841793"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "thread_local"
version = "1.0.1"
@ -1760,9 +1809,9 @@ dependencies = [
[[package]]
name = "time"
version = "0.2.17"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca7ec98a72285d12e0febb26f0847b12d54be24577618719df654c66cadab55d"
checksum = "12785163ae8a1cbb52a5db39af4a5baabd3fe49f07f76f952f89d7e89e5ce531"
dependencies = [
"const_fn",
"libc",
@ -1869,6 +1918,12 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cb18268690309760d59ee1a9b21132c126ba384f374c59a94db4bc03adeb561"
[[package]]
name = "vec-arena"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eafc1b9b2dfc6f5529177b62cf806484db55b32dc7c9658a118e11bbeb33061d"
[[package]]
name = "version_check"
version = "0.9.2"
@ -2010,34 +2065,3 @@ checksum = "39f0c922f1a334134dc2f7a8b67dc5d25f0735263feec974345ff706bcf20b0d"
dependencies = [
"linked-hash-map 0.5.3",
]
[[package]]
name = "zstd"
version = "0.5.3+zstd.1.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01b32eaf771efa709e8308605bbf9319bf485dc1503179ec0469b611937c0cd8"
dependencies = [
"zstd-safe",
]
[[package]]
name = "zstd-safe"
version = "2.0.5+zstd.1.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cfb642e0d27f64729a639c52db457e0ae906e7bc6f5fe8f5c453230400f1055"
dependencies = [
"libc",
"zstd-sys",
]
[[package]]
name = "zstd-sys"
version = "1.4.17+zstd.1.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b89249644df056b522696b1bb9e7c18c87e8ffa3e2f0dc3b0155875d6498f01b"
dependencies = [
"cc",
"glob",
"itertools",
"libc",
]

View file

@ -9,16 +9,18 @@ build = "src/build.rs"
[dependencies]
anyhow = "1.0"
async-process = "0.1.3"
blocking = "0.6.1"
async-process = "1.0.0"
blocking = "1.0.0"
config = { version = "0.10.1", features = ["toml"] }
futures-lite = "1.0.0"
futures-lite = "1.1.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"] }
serde_qs = "0.7"
serde_with = "1.4.0"
sled = "0.34.3"
tide = "0.13.0"
[build-dependencies]

View file

@ -0,0 +1,108 @@
body {
margin: 0;
background-color: #f5f5f5;
font-family: sans-serif;
padding: 16px 0 48px;
}
h1, h2, h3, h4, h5, h6 {
font-family: sans-serif;
font-weight: 500;
margin: 0;
}
section {
max-width: 900px;
margin: auto;
padding: 16px;
> h4 {
padding-bottom: 8px;
}
}
.table-wrapper {
background-color: #fff;
border: 1px solid #ddd;
border-radius: 2px;
}
table {
border-collapse: collapse;
width: 100%;
}
thead {
background-color: #fdf;
border-radius: 2px 2px 0 0;
tr {
border-top: none;
}
}
tr {
border-top: 1px solid #ddd;
}
th, td {
padding: 12px 16px;
}
th {
text-align: left;
}
th.port, td.port {
text-align: right;
}
td.delete {
text-align: right;
}
form {
background-color: #fff;
border: 1px solid #ddd;
border-radius: 2px;
.form-body {
padding: 16px;
}
.submit {
padding: 16px;
border-top: 1px solid #ddd;
}
label {
display: block;
padding: 12px 0;
}
}
@media(max-width: 650px) {
body {
padding: 8px 0 48px;
}
section {
padding: 8px 0;
> h1 {
padding: 0 16px;
}
> h4 {
padding: 8px 16px;
}
}
.table-wrapper {
overflow-x: auto;
}
table, form {
border-radius: 0;
}
}

View file

@ -8,7 +8,7 @@ pub(crate) enum Proto {
}
impl Proto {
fn as_str(&self) -> &'static str {
fn as_iptables_str(&self) -> &'static str {
match self {
Proto::Tcp => "tcp",
Proto::Udp => "udp",
@ -64,7 +64,7 @@ async fn input(
external_mask: u8,
func: impl Fn(&mut Command) -> &mut Command,
) -> Result<(), anyhow::Error> {
iptables(move |cmd| {
iptables_filter(move |cmd| {
func(cmd).args(&[
"INPUT",
"-d",
@ -115,7 +115,7 @@ pub(crate) async fn delete_forward_accept(
internal_interface,
proto,
external_port,
move |cmd| cmd.arg("-I"),
move |cmd| cmd.arg("-D"),
)
.await
}
@ -127,7 +127,7 @@ async fn forward(
external_port: u16,
func: impl Fn(&mut Command) -> &mut Command,
) -> Result<(), anyhow::Error> {
iptables(move |cmd| {
iptables_filter(move |cmd| {
func(cmd).args(&[
"FORWARD",
"-i",
@ -135,7 +135,7 @@ async fn forward(
"-o",
internal_interface,
"-p",
proto.as_str(),
proto.as_iptables_str(),
"--dport",
&external_port.to_string(),
"-m",
@ -150,28 +150,31 @@ async fn forward(
}
pub(crate) async fn forward_postrouting(
proto: Proto,
external_ip: Ipv4Addr,
external_port: u16,
destination_ip: Ipv4Addr,
) -> Result<(), anyhow::Error> {
forward_postrouting_snat(external_ip, external_port, destination_ip, |cmd| {
forward_postrouting_snat(proto, external_ip, external_port, destination_ip, |cmd| {
cmd.arg("-I")
})
.await
}
pub(crate) async fn delete_forward_postrouting(
proto: Proto,
external_ip: Ipv4Addr,
external_port: u16,
destination_ip: Ipv4Addr,
) -> Result<(), anyhow::Error> {
forward_postrouting_snat(external_ip, external_port, destination_ip, |cmd| {
forward_postrouting_snat(proto, external_ip, external_port, destination_ip, |cmd| {
cmd.arg("-D")
})
.await
}
async fn forward_postrouting_snat(
proto: Proto,
external_ip: Ipv4Addr,
external_port: u16,
destination_ip: Ipv4Addr,
@ -183,9 +186,9 @@ async fn forward_postrouting_snat(
"-d",
&destination_ip.to_string(),
"-p",
"tcp",
proto.as_iptables_str(),
"-m",
"tcp",
proto.as_iptables_str(),
"--dport",
&external_port.to_string(),
"-m",
@ -254,7 +257,7 @@ async fn forward_prerouting_dnat(
func(cmd).args(&[
"PREROUTING",
"-p",
proto.as_str(),
proto.as_iptables_str(),
"-d",
&format!("{}/{}", external_ip, external_mask),
"--dport",
@ -266,23 +269,21 @@ async fn forward_prerouting_dnat(
"-j",
"DNAT",
"--to",
format!("{}:{}", destination_ip, destination_port).as_str(),
&format!("{}:{}", destination_ip, destination_port),
])
})
.await
}
async fn iptables_nat<F>(func: F) -> Result<(), anyhow::Error>
where
F: Fn(&mut Command) -> &mut Command,
{
async fn iptables_nat(func: impl Fn(&mut Command) -> &mut Command) -> Result<(), anyhow::Error> {
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,
{
async fn iptables_filter(func: impl Fn(&mut Command) -> &mut Command) -> Result<(), anyhow::Error> {
iptables(move |cmd| func(cmd.args(&["-t", "filter"]))).await
}
async fn iptables(func: impl Fn(&mut Command) -> &mut Command) -> Result<(), anyhow::Error> {
let mut command = Command::new("iptables");
func(&mut command);

View file

@ -2,6 +2,7 @@ use blocking::unblock;
use futures_lite::*;
use once_cell::sync::Lazy;
use sled::Db;
use tide::log::{self, LogMiddleware};
include!(concat!(env!("OUT_DIR"), "/templates.rs"));
@ -9,7 +10,7 @@ mod iptables;
mod rules;
mod startup;
use self::{rules::Rule, startup::Interfaces};
use self::{rules::Rule, startup::Interfaces, templates::statics::StaticFile};
static INTERFACES: Lazy<Interfaces> = Lazy::new(|| {
let interfaces = Interfaces::init_blocking().unwrap();
@ -19,6 +20,8 @@ static INTERFACES: Lazy<Interfaces> = Lazy::new(|| {
static DB: Lazy<Db> = Lazy::new(|| sled::open("router-db-0-34-3").unwrap());
static QS: Lazy<serde_qs::Config> = Lazy::new(|| serde_qs::Config::new(3, false));
async fn rules_page(_: tide::Request<()>) -> tide::Result {
let mut html = Vec::new();
@ -37,9 +40,12 @@ async fn rules_page(_: tide::Request<()>) -> tide::Result {
}
async fn save_rule(mut req: tide::Request<()>) -> tide::Result {
let rule: Rule = req.body_form().await?;
let body_string = req.body_string().await?;
let rule: Rule = QS.deserialize_str(&body_string)?;
rules::save(&DB, &rule).await?;
rules::save(&DB, &rule)?;
rules::apply(&INTERFACES, rule).await?;
Ok(to_rules_page())
@ -47,7 +53,7 @@ async fn save_rule(mut req: tide::Request<()>) -> tide::Result {
async fn delete_rule(req: tide::Request<()>) -> tide::Result {
let id = req.param("id")?;
let rule = rules::delete(&DB, id)?;
let rule = rules::delete(&DB, id).await?;
rules::unset(&INTERFACES, rule).await?;
Ok(to_rules_page())
@ -59,15 +65,32 @@ fn to_rules_page() -> tide::Response {
.build()
}
async fn statics(req: tide::Request<()>) -> tide::Result {
let file: String = req.param("file")?;
if let Some(data) = StaticFile::get(&file) {
Ok(tide::Response::builder(200)
.header("Content-Type", data.mime.to_string())
.body(data.content)
.build())
} else {
Ok(tide::Response::builder(404).build())
}
}
fn main() -> Result<(), anyhow::Error> {
future::block_on(async {
println!("Hello, world!");
log::with_level(log::LevelFilter::Debug);
rules::apply_all(&DB, &INTERFACES).await?;
let mut app = tide::new();
app.with(LogMiddleware::new());
app.at("/static/:file").get(statics);
app.at("/rules").get(rules_page).post(save_rule);
app.at("/rules/:id").delete(delete_rule);
app.at("/rules/:id").get(delete_rule);
let listeners: Vec<String> = INTERFACES
.internal

View file

@ -2,10 +2,17 @@ use crate::{
iptables::{self, Proto},
startup::Interfaces,
};
use sled::Db;
use once_cell::sync::OnceCell;
use sled::{Db, Tree};
use std::net::Ipv4Addr;
#[derive(serde::Deserialize, serde::Serialize)]
static RULES_TREE: OnceCell<Tree> = OnceCell::new();
fn rules_tree(db: &Db) -> &'static Tree {
RULES_TREE.get_or_init(|| db.open_tree("rules").unwrap())
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct Rule {
pub(crate) proto: Proto,
pub(crate) port: u16,
@ -21,11 +28,15 @@ impl Rule {
}
}
#[derive(serde::Deserialize, serde::Serialize)]
#[derive(Debug, serde::Deserialize, serde::Serialize)]
#[serde(tag = "type")]
pub(crate) enum RuleKind {
Accept,
Forward { dest_ip: Ipv4Addr, dest_port: u16 },
Forward {
dest_ip: Ipv4Addr,
#[serde(with = "serde_with::rust::display_fromstr")]
dest_port: u16,
},
}
pub(crate) async fn apply_all(db: &Db, interfaces: &Interfaces) -> Result<(), anyhow::Error> {
@ -37,23 +48,31 @@ pub(crate) async fn apply_all(db: &Db, interfaces: &Interfaces) -> Result<(), an
}
pub(crate) fn read(db: &Db) -> Result<Vec<(String, Rule)>, anyhow::Error> {
db.iter()
rules_tree(db)
.iter()
.map(|res| {
let (id, rule) = res?;
let id = String::from_utf8_lossy(&id).to_string();
tide::log::debug!("id: {}", id);
tide::log::debug!("rule str: {}", String::from_utf8_lossy(&rule));
let rule: Rule = serde_json::from_slice(&rule)?;
tide::log::debug!("rule: {:?}", 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
pub(crate) async fn delete(db: &Db, rule_id: String) -> Result<Rule, anyhow::Error> {
let tree = rules_tree(db);
let rule = tree
.remove(rule_id.as_bytes())?
.ok_or(anyhow::anyhow!("No rule with id {}", rule_id))?;
tree.flush_async().await?;
let rule: Rule = serde_json::from_slice(&rule)?;
Ok(rule)
@ -89,19 +108,28 @@ pub(crate) async fn unset(interfaces: &Interfaces, rule: Rule) -> Result<(), any
dest_port,
)
.await?;
iptables::delete_forward_postrouting(interfaces.external.ip, rule.port, dest_ip)
.await?;
iptables::delete_forward_postrouting(
rule.proto,
interfaces.external.ip,
rule.port,
dest_ip,
)
.await?;
}
}
Ok(())
}
pub(crate) fn save(db: &Db, rule: &Rule) -> Result<(), anyhow::Error> {
pub(crate) async fn save(db: &Db, rule: &Rule) -> Result<(), anyhow::Error> {
let tree = rules_tree(db);
let s = serde_json::to_string(rule)?;
let id = db.generate_id()?;
db.insert(rule_id(id).as_bytes(), s.as_bytes())?;
tree.insert(rule_id(id).as_bytes(), s.as_bytes())?;
tree.flush_async().await?;
Ok(())
}
@ -136,7 +164,8 @@ pub(crate) async fn apply(interfaces: &Interfaces, rule: Rule) -> Result<(), any
dest_port,
)
.await?;
iptables::forward_postrouting(interfaces.external.ip, rule.port, dest_ip).await?;
iptables::forward_postrouting(rule.proto, interfaces.external.ip, rule.port, dest_ip)
.await?;
}
}
@ -146,3 +175,30 @@ pub(crate) async fn apply(interfaces: &Interfaces, rule: Rule) -> Result<(), any
fn rule_id(id: u64) -> String {
format!("rule-{}", id)
}
#[cfg(test)]
mod tests {
use crate::{
iptables::Proto,
rules::{Rule, RuleKind},
};
#[test]
fn can_serialize() {
let rule = Rule {
proto: Proto::Tcp,
port: 53,
kind: RuleKind::Forward {
dest_ip: "192.168.6.84".parse().unwrap(),
dest_port: 53,
},
};
let s = serde_qs::to_string(&rule).unwrap();
assert_eq!(
s,
"proto=Tcp&port=53&kind[type]=Forward&kind[dest_ip]=192.168.6.84&kind[dest_port]=53"
);
}
}

View file

@ -1,4 +1,7 @@
@use crate::rules::Rule;
@use crate::{
rules::Rule,
templates::statics::index_css,
};
@(rules: &[(String, Rule)])
@ -7,55 +10,93 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Rules</title>
<link rel="stylesheet" href="/static/@index_css.name" type="text/css" />
</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>
<section>
<h1>Rules</h1>
</section>
<section>
<div class="table-wrapper">
<table>
<thead>
<th>Kind</th>
<th>Protocol</th>
<th class="port">Port</th>
<th>Destination IP</th>
<th class="port">Destination Port</th>
<th></th>
</thead>
</tbody>
@for (id, rule) in rules {
<tr>
@if let Some((dest_ip, dest_port)) = rule.as_forward() {
<td>Forward</td>
<td>@rule.proto</td>
<td class="port">@rule.port</td>
<td>@dest_ip</td>
<td class="port">@dest_port</td>
} else {
<td>Accept</td>
<td>@rule.proto</td>
<td class="port">@rule.port</td>
<td></td>
<td></td>
}
<td class="delete"><a href="/rules/@id">Delete</a></td>
</tr>
}
</tbody>
</table>
</div>
</section>
<section>
<h4>Forward Port</h4>
<form method="POST" action="/rules">
<div class="form-body">
<select name="proto">
<option value="Tcp">TCP</option>
<option value="Udp">UDP</option>
</select>
<label for="port">
<h4>External Port</h4>
<input name="port" type="number" />
</label>
<input name="kind[type]" value="Forward" type="hidden" />
<label for="kind[dest_port]">
<h4>Internal Port</h4>
<input name="kind[dest_port]" type="number" />
</label>
<label for="kind[dest_ip]">
<h4>IP Address</h4>
<input name="kind[dest_ip]" type="text" />
</label>
</div>
<div class="submit">
<button type="submit">Forward!</button>
</div>
</form>
</section>
<section>
<h4>Accept Port</h4>
<form method="POST" action="/rules">
<div class="form-body">
<select name="proto">
<option value="Tcp">TCP</option>
<option value="Udp">UDP</option>
</select>
<label for="port">
<h4>External Port</h4>
<input name="port" type="number" />
</label>
<input name="kind[type]" value="Accept" type="hidden" />
</div>
<div class="submit">
<button type="submit">Accept!</button>
</div>
</form>
</section>
</body>
</html>