initial commit
Signed-off-by: Solomon Wagner <solow@solow.xyz>
This commit is contained in:
commit
21a4e01e81
94
.gitignore
vendored
Normal file
94
.gitignore
vendored
Normal file
@ -0,0 +1,94 @@
|
||||
# ---> Emacs
|
||||
# -*- mode: gitignore; -*-
|
||||
*~
|
||||
\#*\#
|
||||
/.emacs.desktop
|
||||
/.emacs.desktop.lock
|
||||
*.elc
|
||||
auto-save-list
|
||||
tramp
|
||||
.\#*
|
||||
|
||||
# Org-mode
|
||||
.org-id-locations
|
||||
*_archive
|
||||
|
||||
# flymake-mode
|
||||
*_flymake.*
|
||||
|
||||
# eshell files
|
||||
/eshell/history
|
||||
/eshell/lastdir
|
||||
|
||||
# elpa packages
|
||||
/elpa/
|
||||
|
||||
# reftex files
|
||||
*.rel
|
||||
|
||||
# AUCTeX auto folder
|
||||
/auto/
|
||||
|
||||
# cask packages
|
||||
.cask/
|
||||
dist/
|
||||
|
||||
# Flycheck
|
||||
flycheck_*.el
|
||||
|
||||
# server auth directory
|
||||
/server/
|
||||
|
||||
# projectiles files
|
||||
.projectile
|
||||
|
||||
# directory configuration
|
||||
.dir-locals.el
|
||||
|
||||
# network security
|
||||
/network-security.data
|
||||
|
||||
|
||||
# ---> Rust
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
debug/
|
||||
target/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
Cargo.lock
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
|
||||
# RustRover
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
# ---> Vim
|
||||
# Swap
|
||||
[._]*.s[a-v][a-z]
|
||||
!*.svg # comment out if you don't need vector files
|
||||
[._]*.sw[a-p]
|
||||
[._]s[a-rt-v][a-z]
|
||||
[._]ss[a-gi-z]
|
||||
[._]sw[a-p]
|
||||
|
||||
# Session
|
||||
Session.vim
|
||||
Sessionx.vim
|
||||
|
||||
# Temporary
|
||||
.netrwhist
|
||||
*~
|
||||
# Auto-generated tag files
|
||||
tags
|
||||
# Persistent undo
|
||||
[._]*.un~
|
||||
|
17
Cargo.toml
Normal file
17
Cargo.toml
Normal file
@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "swim-rs"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.96"
|
||||
clap = { version = "4.5.30", features = ["derive", "env"] }
|
||||
colored = "3.0.0"
|
||||
quinn = "0.11.6"
|
||||
serde = { version = "1.0.218", features = ["derive"] }
|
||||
serde_json = "1.0.139"
|
||||
tokio = { version = "1.43.0", features = ["full"] }
|
||||
tracing = "0.1.41"
|
||||
tracing-subscriber = { version = "0.3.19", features = ["fmt", "env-filter"] }
|
19
LICENSE
Normal file
19
LICENSE
Normal file
@ -0,0 +1,19 @@
|
||||
zlib License
|
||||
|
||||
This software is provided 'as-is', without any express or implied warranty. In
|
||||
no event will the authors be held liable for any damages arising from the use
|
||||
of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose, including
|
||||
commercial applications, and to alter it and redistribute it freely, subject to
|
||||
the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software in a
|
||||
product, an acknowledgment in the product documentation would be appreciated
|
||||
but is not required.
|
||||
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
|
||||
3. This notice may not be removed or altered from any source distribution.
|
237
src/main.rs
Normal file
237
src/main.rs
Normal file
@ -0,0 +1,237 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::{
|
||||
io::{AsyncBufReadExt, AsyncWriteExt, BufReader, Result},
|
||||
net::{UnixListener, UnixStream},
|
||||
sync::{self, mpsc, RwLock},
|
||||
};
|
||||
#[allow(unused_imports)]
|
||||
use tracing::{error, info, instrument, warn};
|
||||
|
||||
static LICENSE: &'static str = include_str!("../LICENSE");
|
||||
|
||||
const IPC_SOCKET_PATH: &str = "/tmp/swimd.sock";
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone)]
|
||||
enum SockType {
|
||||
Udp,
|
||||
Tcp,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone)]
|
||||
struct PortMapping {
|
||||
sock_type: SockType,
|
||||
from: u32,
|
||||
to: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
|
||||
struct Config {
|
||||
mappings: BTreeMap<u32, PortMapping>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct AppState {
|
||||
active_config: Arc<RwLock<Config>>,
|
||||
config: Arc<RwLock<Config>>,
|
||||
notify: sync::mpsc::Sender<()>, // Used to tell the ipc server to reload
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
enum ClientOp {
|
||||
Export,
|
||||
Import,
|
||||
Add(PortMapping),
|
||||
Del(Vec<u32>),
|
||||
Reload,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct IPCRequest {
|
||||
op: ClientOp,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
enum ClientResp<S: AsRef<str>> {
|
||||
Ok(Option<S>),
|
||||
Error(S),
|
||||
}
|
||||
|
||||
impl<S: AsRef<str> + Serialize> ClientResp<S> {
|
||||
async fn write_to<W>(&self, mut writer: W) -> Result<()>
|
||||
where
|
||||
W: AsyncWriteExt + std::marker::Unpin,
|
||||
{
|
||||
writer
|
||||
.write_all(serde_json::to_string(self)?.as_bytes())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn handle_ipc_client(mut conn: UnixStream, app: Arc<AppState>) {
|
||||
let config = &app.config;
|
||||
let (reader, mut writer) = conn.split();
|
||||
let mut reader = BufReader::new(reader);
|
||||
let mut buf = String::new();
|
||||
|
||||
if reader.read_line(&mut buf).await.unwrap() == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
serde_json::to_string(&*config.read().await).unwrap();
|
||||
|
||||
let resp: ClientResp<_> = match serde_json::from_str::<IPCRequest>(&buf) {
|
||||
Ok(req) => match req.op {
|
||||
ClientOp::Del(ni) => {
|
||||
let cfg = config.write().await;
|
||||
let km: Vec<String> = ni
|
||||
.iter()
|
||||
.filter(|i| cfg.mappings.contains_key(i))
|
||||
.map(|n| n.to_string())
|
||||
.collect();
|
||||
let kml = km.join(",");
|
||||
if !km.len() == 0 {
|
||||
error!("Bad rule ids when deleting: {}", kml);
|
||||
|
||||
ClientResp::Error(format!("Some route IDs do not exist: {}", kml))
|
||||
} else {
|
||||
for i in ni {
|
||||
info!("Removed ruleset {i}!");
|
||||
}
|
||||
ClientResp::Ok(Some(format!("Removed routes: {kml}")))
|
||||
}
|
||||
}
|
||||
ClientOp::Export => {
|
||||
let cfg = serde_json::to_string(&*config.read().await).unwrap();
|
||||
if let Err(e) = writer.write_all(cfg.as_bytes()).await {
|
||||
error!("Error writing to ipc socket! {e}");
|
||||
}
|
||||
ClientResp::Ok(None)
|
||||
}
|
||||
ClientOp::Import => todo!(),
|
||||
ClientOp::Add(port_mapping) => {
|
||||
let mut cfg = config.write().await;
|
||||
let next = cfg.mappings.keys().last().unwrap_or(&0) + 1;
|
||||
cfg.mappings.insert(next, port_mapping);
|
||||
ClientResp::Ok(None)
|
||||
}
|
||||
ClientOp::Reload => {
|
||||
app.notify.send(()).await.unwrap();
|
||||
ClientResp::Ok(None)
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Couldn't parse ipc request: {}", e);
|
||||
ClientResp::Error("Bad Request".to_string())
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = resp.write_to(writer).await {
|
||||
error!("Error sending response to client: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn ipc_server(app: Arc<AppState>, sock: UnixListener) {
|
||||
loop {
|
||||
match sock.accept().await {
|
||||
Ok((conn, _)) => {
|
||||
tokio::spawn(handle_ipc_client(conn, app.clone()));
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Couldn't accept socket connection: {}", e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn quic_server(app: Arc<AppState>, mut notify: mpsc::Receiver<()>) {
|
||||
while notify.recv().await.is_some() {
|
||||
let mut active = app.active_config.write().await;
|
||||
let cfg = app.config.read().await.clone();
|
||||
*active = cfg;
|
||||
info!("Reloaded config!");
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, clap::Args)]
|
||||
struct ConnectionAdd {
|
||||
#[arg(help = "The name of a registered remote")]
|
||||
remote: String,
|
||||
#[arg(short, long, help = "Specify a non-default port number")]
|
||||
port: u16,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
#[command(
|
||||
about = "Operations relating to tunnels",
|
||||
long_about = "Multiple port mappings can use the same tunnel. It is recommended to create separate tunnels for high traffic ports"
|
||||
)]
|
||||
enum Connection {
|
||||
Add(ConnectionAdd),
|
||||
List,
|
||||
Rm,
|
||||
Info,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum Commands {
|
||||
#[command(subcommand)]
|
||||
Connection(Connection),
|
||||
Remote,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author = "Solomon W.", version, about = "The tunneling firewall")]
|
||||
struct Cli {
|
||||
#[arg(long, help = "Run as the swim daemon")]
|
||||
daemonize: bool,
|
||||
#[arg(long, help = "Display the license")]
|
||||
license: bool,
|
||||
#[command(subcommand)]
|
||||
command: Option<Commands>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
if cli.license {
|
||||
println!("{}", LICENSE);
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
if cli.daemonize {
|
||||
let sock = UnixListener::bind(IPC_SOCKET_PATH).expect("Couldn't bind to unix socket!");
|
||||
let (tx, rx) = mpsc::channel(10);
|
||||
|
||||
let config = Config::default();
|
||||
|
||||
let app = Arc::new(AppState {
|
||||
config: Arc::new(RwLock::new(config)),
|
||||
notify: tx,
|
||||
active_config: Arc::new(RwLock::new(Config::default())),
|
||||
});
|
||||
|
||||
// TODO: Early config init here
|
||||
|
||||
let ipc_handle = tokio::spawn(ipc_server(app.clone(), sock));
|
||||
let quic_handle = tokio::spawn(quic_server(app.clone(), rx));
|
||||
|
||||
ipc_handle.await.unwrap();
|
||||
quic_handle.await.unwrap();
|
||||
}
|
||||
|
||||
if let Some(cmd) = cli.command {
|
||||
match cmd {
|
||||
Commands::Connection(_connection) => println!("Hello swim!"),
|
||||
_ => todo!("Not yet implemented!"),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user