diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 3e52e48..a1374f2 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -52,12 +52,12 @@ jobs: run: | mkdir -p artifacts find ./target/release -name waygate-server -exec cp {} artifacts/ \; - find ./target/release -name generate-keys -exec cp {} artifacts/ \; + find ./target/release -name waygate-generate-keys -exec cp {} artifacts/ \; find ./target/release -name waygate-server.exe -exec cp {} artifacts/ \; - find ./target/release -name generate-keys.exe -exec cp {} artifacts/ \; + find ./target/release -name waygate-generate-keys.exe -exec cp {} artifacts/ \; find ./target/release -name libsteam_api.so -exec cp {} artifacts/ \; find ./target/release -name steam_api64.dll -exec cp {} artifacts/ \; - cp config.toml artifacts/ + cp announcements.toml artifacts/ cp logging.toml artifacts/ cp steam_appid.txt artifacts/ shell: bash diff --git a/Cargo.lock b/Cargo.lock index 0338483..41c4e66 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -2883,6 +2883,14 @@ dependencies = [ "tracing", ] +[[package]] +name = "waygate-generate-keys" +version = "0.1.0" +dependencies = [ + "base64 0.22.1", + "libsodium-sys-stable", +] + [[package]] name = "waygate-message" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 77c8be6..812e07f 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ - "server", + "crates/server", + "crates/generate-keys", "crates/wire", "crates/pool", "crates/message", @@ -66,6 +67,18 @@ features = [ "rustls-tls-native-roots", ] +[workspace.dependencies.libsodium-sys-stable] +version = "1" +features = [ + "fetch-latest", +] + +[workspace.dependencies.waygate-server] +path = "crates/server" + +[workspace.dependencies.waygate-generate-keys] +path = "crates/generate-keys" + [workspace.dependencies.waygate-pool] path = "crates/pool" diff --git a/README.md b/README.md index 1f4dba7..d26fe0d 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,95 @@ Over the past year testing with about 30-40 people no ones has reported getting banned from official online play, however there is no guarantee this server is completely safe to use. +## Setup +### Server setup +The server requires a postgresql database to store messages, bloodstains, ghosts +and more. I am not going to cover how to set up a postgresql database as enough +places cover this exact procedure. + +### Keys +The game uses a set of fixed keys to kick off the connection and perform the key +exchange. You will need to generate a key pair for your server and client. You +do this once during setup of the server. + +In order to generate a keypair you invoke: +```bash +$ ./waygate-generate-keys +``` + +This will print a valid keypair alongside instructions on applying the keypair +to your setup: +``` +# Pass this to your servers launch command +--client-public-key = "8oWXtzzyvMwg0DZTUxdRP/HzDnDhlw8J1ZyXiB2Giks=" +--server-secret-key = "XzUteM9hPf2n/XUg8L2ImxIaRUGusUpqYVFDnEY0Egs=" + +# Add this to your clients's waygate.toml +client_secret_key = "VD7xDTwd6+kt9zg+3qzVaKnxfIvVIwSG8JM1cc8Eeu0=" +server_public_key = "5HCUh3iOJiPwsEPvln0QFnmx9sFaQrzDI9XopTa532c=" +``` + +### Running the server +Grab the [latest release](https://github.com/vswarte/waygate-server/releases) +for your platform and invoke it as such: + +```bash +$ ./waygate-server \ + --bind 0.0.0.0:10901 \ + --api-bind 0.0.0.0:10902 \ + --api-key \ + --database "" \ + --client-public-key "" \ + --server-secret-key "" +``` + +| Parameter | Env variable | Description | +|-----------------------|-----------------------------|----------------------------------------------------| +| `--bind` | `WAYGATE_BIND` | Specifies the binding address for the game server. | +| `--api-bind` | `WAYGATE_API_BIND` | Specifies the binding address for the api server. | +| `--api-key` | `WAYGATE_API_KEY` | Specifies API authentication key. Keep secret. | +| `--database` | `WAYGATE_DATABASE` | Specifies the database URL to be used | +| `--client-public-key` | `WAYGATE_CLIENT_PUBLIC_KEY` | Specifies the KX client public key. Keep secret. | +| `--server-secret-key` | `WAYGATE_SERVER_SECRET_KEY` | Specifies the KX server secret key. Keep secret. | + +#### Database URL +The `--database` parameter expects a database URL like so: `postgresql://:@/`. + +#### Setting up the client +// TBD + +### Additional configuration +#### Logging +The logging setup is configured with `logging.toml`. Under the hood it's log4rs, +which has its [own manual](https://docs.rs/log4rs/latest/log4rs/config/index.html) on the logging options. + +#### Announcements +The announcements are set in `announcements.toml`. + +### API +The server also spins up a JSON HTTP API that allows people to do automated healthchecks, +broadcast messages and more in the future. This HTTP server is bound seperately +from the game server dictated by the `--api-bind` parameter. + +API authentication is regulated by the `--api-key` which requires you to specify +a key that must be matched on incoming HTTP requests. You can use random.org or +a password generator to derive a secure API key. + +Example healtcheck call: +```bash +$ curl -X GET http://localhost:10902/health \ + --header "X-Auth-Token: " \ + --header "Content-Type: application/json" +``` + +Example announcement call: +```bash +$ curl -v -X POST http://localhost:10902/notify/message \ + --header "X-Auth-Token: " \ + --header "Content-Type: application/json" \ + --data '{"message":"Test Announcement"}' +``` + ## What's working? What needs to be done? - [x] Summoning per sign - [x] Quickmatches (arena) @@ -41,13 +130,6 @@ A lot of the development happened as I was reversing the game, so a few parts need rewriting to be more maintainable. The database needs indices as an optimization, etc -## Installation -The server requires a postgresql database to store messages, bloodstains, ghosts -and more. I am not going to cover how to set up a postgresql database as enough -places cover this exact procedure. - -// TBD - # Credits The most important section of this README, this project took a long time to execute and there's still a bit to go. Without these people this project @@ -59,6 +141,6 @@ would've remained on my disk forever. - Steelovsky for testing - Mintal for testing - Metalcrow for testing - - Dasaav for testing - Shion for testing + - Dasaav for testing - auramalexander (Dylan) for name diff --git a/config.toml b/config.toml deleted file mode 100644 index 024fa66..0000000 --- a/config.toml +++ /dev/null @@ -1,10 +0,0 @@ -# General -bind = "0.0.0.0:10901" -database_url = "postgresql://postgres:test123@localhost/waygate" -client_public_key = "hQBVt5lmrykxDhcPkCCgNa7YXWYO+OmRPPDTjPugoS4=" -server_secret_key = "pkryIBfz7CeyxKbN787xd2j7XUr123F+T0FuRcBwW7s=" - -# API -api_bind = "0.0.0.0:10902" -api_key = "KxPRMAxKxFM8" - diff --git a/crates/api/src/auth.rs b/crates/api/src/auth.rs index 9f7585f..76704ca 100644 --- a/crates/api/src/auth.rs +++ b/crates/api/src/auth.rs @@ -4,12 +4,19 @@ use actix_web::{ http::header::HeaderValue, Error }; -use waygate_config::GENERAL; use std::future::{ready, Ready}; use std::task::{Context, Poll}; use std::pin::Pin; -pub struct CheckKey; +pub struct CheckKey { + api_key: String, +} + +impl CheckKey { + pub fn new(api_key: &str) -> Self { + Self { api_key: api_key.to_string() } + } +} impl Transform for CheckKey where @@ -23,11 +30,13 @@ where type Future = Ready>; fn new_transform(&self, service: S) -> Self::Future { - ready(Ok(CheckKeyMiddleware { service })) + let api_key = self.api_key.clone(); + ready(Ok(CheckKeyMiddleware { service, api_key })) } } pub struct CheckKeyMiddleware { + api_key: String, service: S, } @@ -45,8 +54,7 @@ where } fn call(&self, req: ServiceRequest) -> Self::Future { - let key = GENERAL.get().unwrap().api_key.as_str(); - let expected = HeaderValue::from_str(key).unwrap(); + let expected = HeaderValue::from_str(&self.api_key).unwrap(); let authorized = req.headers() .get("X-Auth-Token") .map(|v| v == expected) diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index 9054bb4..fe097e2 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -12,11 +12,12 @@ pub enum ApiError { Start(#[from] std::io::Error), } -pub async fn serve_api(bind: &str, key: &str) -> Result<(), ApiError> { +pub async fn serve_api(bind: &str, api_key: &str) -> Result<(), ApiError> { + let api_key = api_key.to_string(); Ok( - HttpServer::new(|| { + HttpServer::new(move || { App::new() - .wrap(CheckKey) + .wrap(CheckKey::new(&api_key)) .service(health) .service(notify::notify_message) }) diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index a08ede5..317488d 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -4,16 +4,6 @@ use std::fs::read_to_string; use serde::Deserialize; -#[derive(Debug, Deserialize)] -pub struct GeneralConfig { - pub bind: String, - pub database_url: String, - pub client_public_key: String, - pub server_secret_key: String, - pub api_bind: String, - pub api_key: String, -} - #[derive(Debug, Deserialize)] pub struct AnnouncementConfig { pub announcements: Vec, @@ -28,16 +18,9 @@ pub struct AnnouncementItem { pub body: String, } -pub static GENERAL: OnceLock = OnceLock::new(); pub static ANNOUNCEMENTS: OnceLock = OnceLock::new(); pub fn init_config() -> Result<(), Box>{ - let general = toml::from_str( - &read_to_string("./config.toml")? - )?; - let _ = GENERAL.set(general); - - let announcements = toml::from_str( &read_to_string("./announcements.toml")? )?; diff --git a/crates/connection/Cargo.toml b/crates/connection/Cargo.toml index 2c62e0d..fc4b6a7 100644 --- a/crates/connection/Cargo.toml +++ b/crates/connection/Cargo.toml @@ -14,10 +14,7 @@ features = [ ] [dependencies.libsodium-sys-stable] -version = "1" -features = [ - "fetch-latest", -] +workspace = true [dependencies.tracing] workspace = true diff --git a/crates/connection/src/crypto.rs b/crates/connection/src/crypto.rs index deb6c44..bf2d55a 100644 --- a/crates/connection/src/crypto.rs +++ b/crates/connection/src/crypto.rs @@ -1,5 +1,6 @@ use std::ffi::c_void; use std::io::{self, Read}; +use std::sync::OnceLock; use libsodium_sys::{ crypto_kx_PUBLICKEYBYTES, crypto_kx_SECRETKEYBYTES, @@ -17,11 +18,8 @@ use libsodium_sys::{ use thiserror::Error; use tokio::io::AsyncWriteExt; -use waygate_config::GENERAL; -use base64::prelude::*; - -use crate::ClientError; +use crate::{ClientError, KX_CLIENT_PUBLIC_KEY, KX_SERVER_SECRET_KEY}; pub const PUBLICKEYBYTES: usize = crypto_kx_PUBLICKEYBYTES as usize; pub const SECRETKEYBYTES: usize = crypto_kx_SECRETKEYBYTES as usize; @@ -151,10 +149,6 @@ impl ClientCrypto { let (nonce, mac, mut payload) = Self::split_nonce_mac_and_payload(r) .await.map_err(ClientError::Io)?; - let config = GENERAL.get().unwrap(); - let client_public_key = BASE64_STANDARD.decode(&config.client_public_key)?; - let server_secret_key = BASE64_STANDARD.decode(&config.server_secret_key)?; - let result = unsafe { libsodium_sys::crypto_box_open_detached( payload.as_mut_ptr(), @@ -162,8 +156,8 @@ impl ClientCrypto { mac.as_ptr(), payload.len() as u64, nonce.as_ptr(), - client_public_key.as_ptr(), - server_secret_key.as_ptr(), + KX_CLIENT_PUBLIC_KEY.get().unwrap().as_ptr(), + KX_SERVER_SECRET_KEY.get().unwrap().as_ptr(), ) }; @@ -195,10 +189,6 @@ impl ClientCrypto { randombytes_buf(bootstrap_nonce.as_mut_ptr() as *mut c_void, NONCEBYTES); }; - let config = GENERAL.get().unwrap(); - let client_public_key = BASE64_STANDARD.decode(&config.client_public_key)?; - let server_secret_key = BASE64_STANDARD.decode(&config.server_secret_key)?; - let mut mac = [0u8; MACBYTES]; let result = unsafe { crypto_box_detached( @@ -207,8 +197,8 @@ impl ClientCrypto { payload.as_ptr(), payload.len() as u64, bootstrap_nonce.as_ptr(), - client_public_key.as_ptr(), - server_secret_key.as_ptr(), + KX_CLIENT_PUBLIC_KEY.get().unwrap().as_ptr(), + KX_SERVER_SECRET_KEY.get().unwrap().as_ptr(), ) }; diff --git a/crates/connection/src/lib.rs b/crates/connection/src/lib.rs index f52a933..a1386fd 100644 --- a/crates/connection/src/lib.rs +++ b/crates/connection/src/lib.rs @@ -4,8 +4,27 @@ mod transport; mod client; mod session; +use std::sync::OnceLock; + +use base64::prelude::*; pub use push::*; pub use crypto::*; pub use transport::*; pub use client::*; pub use session::*; + +pub(crate) static KX_CLIENT_PUBLIC_KEY: OnceLock> = OnceLock::new(); +pub(crate) static KX_SERVER_SECRET_KEY: OnceLock> = OnceLock::new(); + +pub fn init_crypto( + client_public_key: &str, + server_secret_key: &str, +) -> Result<(), base64::DecodeError> { + let client_public_key = BASE64_STANDARD.decode(client_public_key)?; + KX_CLIENT_PUBLIC_KEY.set(client_public_key).expect("init_crypto called twice?"); + + let server_secret_key = BASE64_STANDARD.decode(server_secret_key)?; + KX_SERVER_SECRET_KEY.set(server_secret_key).expect("init_crypto called twice?"); + + Ok(()) +} diff --git a/crates/generate-keys/Cargo.toml b/crates/generate-keys/Cargo.toml new file mode 100644 index 0000000..80edf1f --- /dev/null +++ b/crates/generate-keys/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "waygate-generate-keys" +version = "0.1.0" +edition = "2021" + +[dependencies.base64] +workspace = true + +[dependencies.libsodium-sys-stable] +workspace = true diff --git a/crates/generate-keys/src/main.rs b/crates/generate-keys/src/main.rs new file mode 100644 index 0000000..c8585da --- /dev/null +++ b/crates/generate-keys/src/main.rs @@ -0,0 +1,35 @@ +use base64::prelude::*; + +use libsodium_sys::{ + crypto_box_keypair, + crypto_box_PUBLICKEYBYTES, + crypto_box_SECRETKEYBYTES, +}; + +fn main() { + let (client_public, client_secret) = generate_keypair(); + let (server_public, server_secret) = generate_keypair(); + + println!("# Pass this to your servers launch command"); + println!("--client-public-key = \"{}\"", BASE64_STANDARD.encode(client_public)); + println!("--server-secret-key = \"{}\"", BASE64_STANDARD.encode(server_secret)); + println!(); + println!("# Add this to your clients's waygate.toml"); + println!("client_secret_key = \"{}\"", BASE64_STANDARD.encode(client_secret)); + println!("server_public_key = \"{}\"", BASE64_STANDARD.encode(server_public)); +} + +fn generate_keypair() -> ( + [u8; crypto_box_PUBLICKEYBYTES as usize], + [u8; crypto_box_SECRETKEYBYTES as usize], +) { + let mut pk = [0u8; crypto_box_PUBLICKEYBYTES as usize]; + let mut sk = [0u8; crypto_box_SECRETKEYBYTES as usize]; + + let result = unsafe { crypto_box_keypair(pk.as_mut_ptr(), sk.as_mut_ptr()) }; + if result != 0x0 { + panic!("Could not generate keypair"); + } + + (pk, sk) +} diff --git a/server/Cargo.toml b/crates/server/Cargo.toml similarity index 100% rename from server/Cargo.toml rename to crates/server/Cargo.toml diff --git a/server/src/main.rs b/crates/server/src/main.rs similarity index 85% rename from server/src/main.rs rename to crates/server/src/main.rs index c9f5ffd..b902cda 100755 --- a/server/src/main.rs +++ b/crates/server/src/main.rs @@ -8,7 +8,7 @@ use tokio::net::{TcpListener, TcpStream}; use tungstenite::handshake::server::{Request, Response}; use waygate_api::serve_api; use waygate_config::init_config; -use waygate_connection::{new_client, ClientError, TransportError}; +use waygate_connection::{init_crypto, new_client, ClientError, TransportError}; use waygate_database::init_database; use waygate_steam::{begin_session, init_steamworks}; use waygate_rpc::handle_request; @@ -32,6 +32,16 @@ struct Args { /// Database URL pointing to the postgresql instance. #[arg(long, env("WAYGATE_DATABASE"))] database: String, + + /// Key used by server to encrypt KX messages going to client. + /// This key should be kept secret. + #[arg(long, env("WAYGATE_CLIENT_PUBLIC_KEY"))] + client_public_key: String, + + /// Key used by server to decrypt incoming KX messages from the client. + /// This key should be kept secret. + #[arg(long, env("WAYGATE_SERVER_SECRET_KEY"))] + server_secret_key: String, } #[tokio::main] @@ -40,13 +50,18 @@ async fn main() -> Result<(), io::Error> { log4rs::init_file("logging.toml", Default::default()).unwrap(); - init_config().expect("Could not initialize config"); + init_config() + .expect("Could not initialize config"); + + init_crypto(args.client_public_key.as_str(), args.server_secret_key.as_str()) + .expect("Could not initialize crypto"); init_database(args.database.as_str()) .await .expect("Could not initialize database"); - init_steamworks().expect("Could not initialize steam"); + init_steamworks() + .expect("Could not initialize steam"); tokio::select! { _ = serve_api( diff --git a/logging.toml b/logging.toml index 69cffc9..c99b59e 100644 --- a/logging.toml +++ b/logging.toml @@ -1,5 +1,5 @@ [root] -level = "debug" +level = "info" # Specifies what appenders to use appenders = ["stdout", "file"] diff --git a/server/src/api.rs b/server/src/api.rs deleted file mode 100644 index 8b13789..0000000 --- a/server/src/api.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/server/src/bin/generate-keys.rs b/server/src/bin/generate-keys.rs deleted file mode 100644 index b1f52c7..0000000 --- a/server/src/bin/generate-keys.rs +++ /dev/null @@ -1,35 +0,0 @@ -// use base64::prelude::*; -// -// use libsodium_sys::{ -// crypto_box_keypair, -// crypto_box_PUBLICKEYBYTES, -// crypto_box_SECRETKEYBYTES, -// }; -// -fn main() { -// let (client_public, client_secret) = generate_keypair(); -// let (server_public, server_secret) = generate_keypair(); -// -// println!("# Add this to server's general.toml"); -// println!("client_public_key = \"{}\"", BASE64_STANDARD.encode(client_public)); -// println!("server_secret_key = \"{}\"", BASE64_STANDARD.encode(server_secret)); -// println!(); -// println!("# Add this to clients's waygate.toml"); -// println!("client_secret_key = \"{}\"", BASE64_STANDARD.encode(client_secret)); -// println!("server_public_key = \"{}\"", BASE64_STANDARD.encode(server_public)); -} -// -// fn generate_keypair() -> ( -// [u8; crypto_box_PUBLICKEYBYTES as usize], -// [u8; crypto_box_SECRETKEYBYTES as usize], -// ) { -// let mut pk = [0u8; crypto_box_PUBLICKEYBYTES as usize]; -// let mut sk = [0u8; crypto_box_SECRETKEYBYTES as usize]; -// -// let result = unsafe { crypto_box_keypair(pk.as_mut_ptr(), sk.as_mut_ptr()) }; -// if result != 0x0 { -// panic!("Could not generate keypair"); -// } -// -// (pk, sk) -// }