initial commit

Signed-off-by: Solomon Wagner <solow@solow.xyz>
This commit is contained in:
Solomon W. 2025-02-23 22:12:08 -05:00
commit 21a4e01e81
5 changed files with 370 additions and 0 deletions

94
.gitignore vendored Normal file
View 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
View 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
View 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.

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# swim
A traffic routing proxy

237
src/main.rs Normal file
View 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(())
}