Compare commits

..

No commits in common. "c34578786eff176bb4a6bef9c0c907b8549f2ef2" and "9b640a555ae4a867e2050247d8c5b0997f87fab6" have entirely different histories.

22 changed files with 183 additions and 1228 deletions

3
.gitignore vendored
View file

@ -14,5 +14,4 @@ target/
tmp/ tmp/
*.db *.db
*.sock *.sock
*.sqlite3

881
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
[workspace] [workspace]
members = ["burrow", "server", "tun"] members = ["burrow", "tun"]
resolver = "2" resolver = "2"
exclude = ["burrow-gtk"] exclude = ["burrow-gtk"]

View file

@ -59,7 +59,7 @@ reqwest = { version = "0.12", default-features = false, features = [
] } ] }
rusqlite = { version = "0.31.0", features = ["blob"] } rusqlite = { version = "0.31.0", features = ["blob"] }
dotenv = "0.15.0" dotenv = "0.15.0"
tonic = "0.12.3" tonic = "0.12.0"
prost = "0.13.1" prost = "0.13.1"
prost-types = "0.13.1" prost-types = "0.13.1"
tokio-stream = "0.1" tokio-stream = "0.1"
@ -68,9 +68,6 @@ tower = "0.4.13"
hyper-util = "0.1.6" hyper-util = "0.1.6"
toml = "0.8.15" toml = "0.8.15"
rust-ini = "0.21.0" rust-ini = "0.21.0"
jwt-simple = "0.12.10"
config = "0.14.1"
dotenvy = "0.15.7"
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
caps = "0.5" caps = "0.5"
@ -99,4 +96,4 @@ bundled = ["rusqlite/bundled"]
[build-dependencies] [build-dependencies]
tonic-build = "0.12.3" tonic-build = "0.12.0"

View file

@ -1,7 +1,4 @@
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_build::configure().compile_protos( tonic_build::compile_protos("../proto/burrow.proto")?;
&["../proto/burrow.proto", "../proto/burrowweb.proto"],
&["../proto", "../proto"],
)?;
Ok(()) Ok(())
} }

2
burrow/src/auth/mod.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod client;
pub mod server;

View file

@ -1,5 +1,7 @@
use anyhow::Result; use anyhow::Result;
use crate::daemon::rpc::grpc_defs::{Network, NetworkType};
pub static PATH: &str = "./server.sqlite3"; pub static PATH: &str = "./server.sqlite3";
pub fn init_db() -> Result<()> { pub fn init_db() -> Result<()> {
@ -47,7 +49,7 @@ pub fn init_db() -> Result<()> {
} }
pub fn store_connection( pub fn store_connection(
openid_user: &super::providers::OpenIdUser, openid_user: super::providers::OpenIdUser,
openid_provider: &str, openid_provider: &str,
access_token: &str, access_token: &str,
refresh_token: Option<&str>, refresh_token: Option<&str>,
@ -82,32 +84,8 @@ pub fn store_device(
) -> Result<()> { ) -> Result<()> {
log::debug!("Storing openid user {:#?}", openid_user); log::debug!("Storing openid user {:#?}", openid_user);
let conn = rusqlite::Connection::open(PATH)?; let conn = rusqlite::Connection::open(PATH)?;
todo!();
conn.execute(
"INSERT INTO device (name, public_key, apns_token, user_id, ipv4, ipv6, access_token, refresh_token)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
())?;
Ok(())
}
pub fn delete_device(id: i64) -> Result<()> { // TODO
let conn = rusqlite::Connection::open(PATH)?;
conn.execute("DELETE FROM device WHERE id = ?", [id])?;
Ok(()) Ok(())
} }
pub fn list_devices(user_id: i64) -> Result<Vec<String>> {
let conn = rusqlite::Connection::open(PATH)?;
let mut stmt = conn.prepare("SELECT name FROM device WHERE user_id = ?")?;
let result: Vec<String> = stmt
.query_map([user_id], |row| {
let name: String = row.get(0)?;
Ok(name)
})?
.collect::<Result<Vec<_>, _>>()?;
Ok(result)
}

View file

@ -1,29 +1,32 @@
pub mod db; pub mod db;
pub mod grpc_defs;
mod grpc_server;
pub mod providers; pub mod providers;
pub mod settings;
use anyhow::Result; use anyhow::Result;
use grpc_defs::burrow_web_server::BurrowWebServer; use axum::{http::StatusCode, routing::post, Router};
use grpc_server::BurrowGrpcServer; use providers::slack::auth;
use tokio::signal; use tokio::signal;
use tonic::transport::Server;
pub async fn serve() -> Result<()> { pub async fn serve() -> Result<()> {
db::init_db()?; db::init_db()?;
let addr = "[::1]:8080".parse()?;
let app = Router::new()
.route("/slack-auth", post(auth))
.route("/device/new", post(device_new));
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap();
log::info!("Starting auth server on port 8080"); log::info!("Starting auth server on port 8080");
let burrow_grpc_server = BurrowGrpcServer::new()?; axum::serve(listener, app)
let svc = BurrowWebServer::new(burrow_grpc_server); .with_graceful_shutdown(shutdown_signal())
Server::builder() .await
.accept_http1(true) .unwrap();
.add_service(tonic_web::enable(svc))
.serve(addr)
.await?;
Ok(()) Ok(())
} }
async fn device_new() -> StatusCode {
StatusCode::OK
}
async fn shutdown_signal() { async fn shutdown_signal() {
let ctrl_c = async { let ctrl_c = async {
signal::ctrl_c() signal::ctrl_c()

View file

@ -0,0 +1,8 @@
pub mod slack;
pub use super::db;
#[derive(serde::Deserialize, Default, Debug)]
pub struct OpenIdUser {
pub sub: String,
pub name: String,
}

View file

@ -1,26 +1,24 @@
use anyhow::Result; use anyhow::Result;
use axum::{
extract::Json,
http::StatusCode,
routing::{get, post},
};
use reqwest::header::AUTHORIZATION; use reqwest::header::AUTHORIZATION;
use serde::Deserialize; use serde::Deserialize;
use super::db::store_connection; use super::db::store_connection;
use super::grpc_defs::{JwtInfo, SlackAuthRequest};
use super::KeypairT;
use tonic::{Request as TRequest, Response as TResponse, Result as TResult, Status as TStatus};
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct SlackToken { pub struct SlackToken {
slack_token: String, slack_token: String,
} }
pub async fn auth( pub async fn auth(Json(payload): Json<SlackToken>) -> (StatusCode, String) {
request: TRequest<SlackAuthRequest>, let slack_user = match fetch_slack_user(&payload.slack_token).await {
key_pair: &KeypairT,
) -> TResult<TResponse<JwtInfo>, TStatus> {
let slack_token = request.into_inner().slack_token;
let slack_user = match fetch_slack_user(&slack_token).await {
Ok(user) => user, Ok(user) => user,
Err(e) => { Err(e) => {
log::error!("Failed to fetch Slack user: {:?}", e); log::error!("Failed to fetch Slack user: {:?}", e);
return Err(TStatus::unauthenticated("Failed to fetch slack user")); return (StatusCode::UNAUTHORIZED, String::new());
} }
}; };
@ -30,18 +28,15 @@ pub async fn auth(
slack_user.sub slack_user.sub
); );
let _conn = match store_connection(&slack_user, "slack", &slack_token, None) { let conn = match store_connection(slack_user, "slack", &payload.slack_token, None) {
Ok(user) => user, Ok(user) => user,
Err(e) => { Err(e) => {
log::error!("Failed to fetch Slack user: {:?}", e); log::error!("Failed to fetch Slack user: {:?}", e);
return Err(TStatus::unauthenticated("Failed to store connection")); return (StatusCode::UNAUTHORIZED, String::new());
} }
}; };
Ok(TResponse::new( (StatusCode::OK, String::new())
JwtInfo::try_from_oid(slack_user, &key_pair)
.map_err(|e| TStatus::unauthenticated(format!("JWT Generation failed: {e}")))?,
))
} }
async fn fetch_slack_user(access_token: &str) -> Result<super::OpenIdUser> { async fn fetch_slack_user(access_token: &str) -> Result<super::OpenIdUser> {

View file

@ -5,13 +5,18 @@ pub mod wireguard;
mod daemon; mod daemon;
#[cfg(any(target_os = "linux", target_vendor = "apple"))] #[cfg(any(target_os = "linux", target_vendor = "apple"))]
pub mod database; pub mod database;
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
mod auth;
pub(crate) mod tracing; pub(crate) mod tracing;
#[cfg(target_vendor = "apple")] #[cfg(target_vendor = "apple")]
pub use daemon::apple::spawn_in_process; pub use daemon::apple::spawn_in_process;
#[cfg(any(target_os = "linux", target_vendor = "apple"))] #[cfg(any(target_os = "linux", target_vendor = "apple"))]
pub use daemon::{ pub use daemon::{
rpc::DaemonResponse, rpc::ServerInfo, DaemonClient, DaemonCommand, DaemonResponseData, rpc::DaemonResponse,
rpc::ServerInfo,
DaemonClient,
DaemonCommand,
DaemonResponseData,
DaemonStartOptions, DaemonStartOptions,
}; };

View file

@ -7,6 +7,9 @@ pub(crate) mod tracing;
#[cfg(any(target_os = "linux", target_vendor = "apple"))] #[cfg(any(target_os = "linux", target_vendor = "apple"))]
mod wireguard; mod wireguard;
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
mod auth;
#[cfg(any(target_os = "linux", target_vendor = "apple"))] #[cfg(any(target_os = "linux", target_vendor = "apple"))]
use daemon::{DaemonClient, DaemonCommand}; use daemon::{DaemonClient, DaemonCommand};
@ -49,6 +52,8 @@ enum Commands {
ServerConfig, ServerConfig,
/// Reload Config /// Reload Config
ReloadConfig(ReloadConfigArgs), ReloadConfig(ReloadConfigArgs),
/// Authentication server
AuthServer,
/// Server Status /// Server Status
ServerStatus, ServerStatus,
/// Tunnel Config /// Tunnel Config
@ -271,6 +276,7 @@ async fn main() -> Result<()> {
Commands::ServerInfo => try_serverinfo().await?, Commands::ServerInfo => try_serverinfo().await?,
Commands::ServerConfig => try_serverconfig().await?, Commands::ServerConfig => try_serverconfig().await?,
Commands::ReloadConfig(args) => try_reloadconfig(args.interface_id.clone()).await?, Commands::ReloadConfig(args) => try_reloadconfig(args.interface_id.clone()).await?,
Commands::AuthServer => crate::auth::server::serve().await?,
Commands::ServerStatus => try_serverstatus().await?, Commands::ServerStatus => try_serverstatus().await?,
Commands::TunnelConfig => try_tun_config().await?, Commands::TunnelConfig => try_tun_config().await?,
Commands::NetworkAdd(args) => { Commands::NetworkAdd(args) => {

View file

@ -2,6 +2,9 @@ syntax = "proto3";
package burrowweb; package burrowweb;
import "wireguard.proto";
// TODO: Frontend sends slack token receive JWT // TODO: Frontend sends slack token receive JWT
// TODO: create/delete/list routes // TODO: create/delete/list routes
@ -12,67 +15,15 @@ service BurrowWeb {
rpc CreateDevice (CreateDeviceRequest) returns (CreateDeviceResponse); rpc CreateDevice (CreateDeviceRequest) returns (CreateDeviceResponse);
rpc DeleteDevice (JWTInfo) returns (Empty); rpc DeleteDevice (JWTInfo) returns (Empty);
rpc ListDevices (JWTInfo) returns (ListDevicesResponse); rpc ListDevices (JWTInfo) returns (ListDevicesResponse);
rpc Status(Empty) returns (ServerStatus);
} }
message Peer {
string public_key = 1;
optional string preshared_key = 2;
repeated string allowed_ips = 3;
string endpoint = 4;
optional uint32 persistent_keepalive = 5;
optional string name = 6;
}
message InterfaceConfig {
// Does not include private key; the client is responsible for generating & persisting that
repeated string address = 1;
optional uint32 listen_port = 2;
repeated string dns = 3;
optional uint32 mtu = 4;
}
message Device {
int32 id = 1;
optional string name = 2;
string public_key = 3;
optional string apns_token = 4;
int32 user_id = 5;
string created_at = 6;
string ipv4 = 7;
string ipv6 = 8;
string access_token = 9;
string refresh_token = 10;
string expires_at = 11;
}
message User {
int32 id = 1;
string created_at = 2;
}
message UserConnection {
int32 user_id = 1;
string openid_provider = 2;
string openid_user_id = 3;
string openid_user_name = 4;
string access_token = 5;
string refresh_token = 6;
}
message Config {
InterfaceConfig interface = 1;
repeated Peer peers = 2;
}
message Empty {} message Empty {}
message SlackAuthRequest { message SlackAuthRequest {
string slack_token = 1; string slack_token = 1;
} }
message JWTInfo { message JWTInfo {
string jwt = 1; string jwt = 1;
} }
@ -83,13 +34,9 @@ message CreateDeviceRequest {
} }
message CreateDeviceResponse { message CreateDeviceResponse {
Config wg_config = 1; wireguard.Config wg_config = 1;
} }
message ListDevicesResponse { message ListDevicesResponse {
repeated Device devices = 1; repeated wireguard.Device devices = 1;
}
message ServerStatus {
string status = 1;
} }

53
proto/wireguard.proto Normal file
View file

@ -0,0 +1,53 @@
syntax = "proto3";
package wireguard;
message Peer {
string public_key = 1;
optional string preshared_key = 2;
repeated string allowed_ips = 3;
string endpoint = 4;
optional uint32 persistent_keepalive = 5;
optional string name = 6;
}
message InterfaceConfig {
// Does not include private key; the client is responsible for generating & persisting that
repeated string address = 1;
optional uint32 listen_port = 2;
repeated string dns = 3;
optional uint32 mtu = 4;
}
message Device {
int32 id = 1;
optional string name = 2;
string public_key = 3;
optional string apns_token = 4;
int32 user_id = 5;
string created_at = 6;
string ipv4 = 7;
string ipv6 = 8;
string access_token = 9;
string refresh_token = 10;
string expires_at = 11;
}
message User {
int32 id = 1;
string created_at = 2;
}
message UserConnection {
int32 user_id = 1;
string openid_provider = 2;
string openid_user_id = 3;
string openid_user_name = 4;
string access_token = 5;
string refresh_token = 6;
}
message Config {
InterfaceConfig interface = 1;
repeated Peer peers = 2;
}

View file

@ -1,41 +0,0 @@
[package]
name = "server"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1.0.93"
jwt-simple = "0.12.10"
log = "0.4.22"
reqwest = { version = "0.12.9", default-features = false, features = [
"json",
"rustls-tls",
] }
serde = "1.0.215"
serde_json = "1.0.133"
tokio = { version = "1.41.1", features = [
"rt",
"macros",
"sync",
"io-util",
"rt-multi-thread",
"signal",
"time",
"tracing",
"fs",
] }
tonic = "0.12.3"
clap = { version = "4.4", features = ["derive"] }
rusqlite = { version = "0.31.0", features = ["blob"] }
dotenvy = "0.15.7"
config = "0.14.1"
prost = "0.13.3"
prost-types = "0.13.3"
tonic-web = "0.12.3"
[features]
bundled = ["rusqlite/bundled"]
[build-dependencies]
tonic-build = "0.12.3"

View file

@ -1,4 +0,0 @@
fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_build::configure().compile_protos(&["../proto/burrowweb.proto"], &["../proto"])?;
Ok(())
}

View file

@ -1,52 +0,0 @@
pub mod client;
pub mod server;
use anyhow::Result;
use clap::{Args, Parser, Subcommand};
use server::{providers::gen_keypem, serve};
#[derive(Parser)]
#[command(name = "Burrow Server")]
#[command(author = "Hack Club <team@hackclub.com>")]
#[command(version = "0.1")]
#[command(
about = "Server for hosting auth logic of Burrow",
long_about = "Burrow is a 🚀 blazingly fast 🚀 tool designed to penetrate unnecessarily restrictive firewalls, providing teenagers worldwide with secure, less-filtered, and safe access to the internet!
It's being built by teenagers from Hack Club, in public! Check it out: https://github.com/hackclub/burrow
Spotted a bug? Please open an issue! https://github.com/hackclub/burrow/issues/new"
)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
StartServer,
#[command(name = "genkeys")]
GenKeys(GenKeyArgs),
}
#[derive(Args)]
pub struct GenKeyArgs {
#[arg(short, long, default_value = "false")]
pub raw: bool,
}
#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
match &cli.command {
Commands::GenKeys(args) => {
let pem = gen_keypem();
if args.raw {
println!(r"{pem:?}");
} else {
println!("Generated PEM:\n{pem}")
}
}
Commands::StartServer => {
serve().await?;
}
};
Ok(())
}

View file

@ -1,5 +0,0 @@
pub use burrowwebrpc::*;
pub mod burrowwebrpc {
tonic::include_proto!("burrowweb");
}

View file

@ -1,82 +0,0 @@
use std::sync::Arc;
use jwt_simple::prelude::Ed25519KeyPair;
use tonic::{Request, Response, Status};
use super::providers::{KeypairT, OpenIdUser};
use std::fmt::Debug;
use super::{
grpc_defs::{
burrowwebrpc::burrow_web_server::BurrowWeb, CreateDeviceRequest, CreateDeviceResponse,
Empty, JwtInfo, ListDevicesResponse, ServerStatus, SlackAuthRequest,
},
providers::slack::auth,
settings::BurrowAuthServerConfig,
};
#[derive(Clone)]
pub struct BurrowGrpcServer {
config: Arc<BurrowAuthServerConfig>,
jwt_keypair: Arc<KeypairT>,
}
impl Debug for BurrowGrpcServer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BurrowGrpcServer")
.field("config", &self.config)
.field("jwt_keypair", &"<redacted>")
.finish()
}
}
impl BurrowGrpcServer {
pub fn new() -> anyhow::Result<Self> {
let config = BurrowAuthServerConfig::new_dotenv()?;
let jwt_keypair = Ed25519KeyPair::from_pem(&config.jwt_pem)?;
Ok(Self {
config: Arc::new(config),
jwt_keypair: Arc::new(jwt_keypair),
})
}
}
#[tonic::async_trait]
impl BurrowWeb for BurrowGrpcServer {
async fn slack_auth(
&self,
request: Request<SlackAuthRequest>,
) -> Result<Response<JwtInfo>, Status> {
auth(request, &self.jwt_keypair).await
}
async fn create_device(
&self,
request: Request<CreateDeviceRequest>,
) -> Result<Response<CreateDeviceResponse>, Status> {
let req = request.into_inner();
let jwt = req
.jwt
.ok_or(Status::invalid_argument("JWT Not existent!"))?;
let oid_user = OpenIdUser::try_from_jwt(&jwt, &self.jwt_keypair)
.map_err(|e| Status::invalid_argument(e.to_string()))?;
todo!()
}
async fn delete_device(&self, request: Request<JwtInfo>) -> Result<Response<Empty>, Status> {
todo!()
}
async fn list_devices(
&self,
request: Request<JwtInfo>,
) -> Result<Response<ListDevicesResponse>, Status> {
todo!()
}
async fn status(&self, _req: Request<Empty>) -> Result<Response<ServerStatus>, Status> {
Ok(Response::new(ServerStatus {
status: "Nobody expects the Spanish Inquisition".into(),
}))
}
}

View file

@ -1,76 +0,0 @@
pub mod slack;
use self::grpc_defs::JwtInfo;
pub use super::{db, grpc_defs, settings::BurrowAuthServerConfig};
use anyhow::{anyhow, Result};
use jwt_simple::{
claims::{Claims, NoCustomClaims},
prelude::{Duration, Ed25519KeyPair, EdDSAKeyPairLike, EdDSAPublicKeyLike},
};
use serde::{Deserialize, Serialize};
pub type KeypairT = Ed25519KeyPair;
#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Eq, Clone)]
pub struct OpenIdUser {
pub sub: String,
pub name: String,
}
#[derive(Serialize, Deserialize, Debug)]
struct OpenIDCustomField {
pub name: String,
}
impl OpenIdUser {
pub fn try_from_jwt(jwt_info: &JwtInfo, keypair: &KeypairT) -> Result<Self> {
let claims = keypair
.public_key()
.verify_token::<OpenIDCustomField>(&jwt_info.jwt, None)?;
Ok(Self {
sub: claims.subject.ok_or(anyhow!("No Subject!"))?,
name: claims.custom.name,
})
}
}
impl JwtInfo {
fn try_from_oid(oid_user: OpenIdUser, keypair: &KeypairT) -> Result<Self> {
let claims = Claims::with_custom_claims(
OpenIDCustomField { name: oid_user.name },
Duration::from_days(10),
)
.with_subject(oid_user.sub);
let jwt = keypair.sign(claims)?;
Ok(Self { jwt })
}
}
pub fn gen_keypem() -> String {
let keypair = KeypairT::generate();
keypair.to_pem()
}
pub fn parse_keypem(pem: &String) -> Result<KeypairT> {
let keypair = KeypairT::from_pem(&pem)?;
Ok(keypair)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_jwt() -> Result<()> {
let key_pair = Ed25519KeyPair::generate();
let sample_usr = OpenIdUser {
sub: "Spanish".into(),
name: "Inquisition".into(),
};
let encoded = JwtInfo::try_from_oid(sample_usr.clone(), &key_pair)?;
println!("{}", encoded.jwt);
let decoded = OpenIdUser::try_from_jwt(&encoded, &key_pair)?;
assert_eq!(decoded, sample_usr);
Ok(())
}
}

View file

@ -1,22 +0,0 @@
use config::{Config, ConfigError, Environment};
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct BurrowAuthServerConfig {
pub jwt_pem: String,
}
impl BurrowAuthServerConfig {
pub fn new() -> Result<Self, ConfigError> {
let s = Config::builder()
.add_source(Environment::default())
.build()?;
s.try_deserialize()
}
/// Creates a new config that includes the dotenv
pub fn new_dotenv() -> Result<Self, ConfigError> {
dotenvy::dotenv().ok();
Self::new()
}
}