From a87d56cfc967e8d3c68245950cba7ca3cb11cecc Mon Sep 17 00:00:00 2001 From: Jake McGinty Date: Fri, 9 Apr 2021 13:48:00 +0900 Subject: [PATCH] {client,server}: send and require a header that contains the server public key This is a stop-gap CSRF protection mechanism from unsophisticated attacks. It's to be considered a temporary solution until a more complete one can be implemented, but it should be sufficient in most cases for the time being. See https://github.com/tonarino/innernet/issues/38 for further discussion. --- Cargo.lock | 1 + client/src/main.rs | 60 +++++----- client/src/util.rs | 86 ++++++++------ server/Cargo.toml | 1 + server/src/api/admin/cidr.rs | 33 ++++-- server/src/api/admin/peer.rs | 45 +++++--- server/src/api/user.rs | 30 +++-- server/src/main.rs | 167 +++++++++++++++++++++------- server/src/test.rs | 42 ++++--- shared/src/interface_config.rs | 6 +- shared/src/lib.rs | 3 +- wgctrl-rs/src/backends/userspace.rs | 2 +- 12 files changed, 311 insertions(+), 165 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3adaf25..372a0f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1132,6 +1132,7 @@ dependencies = [ "shared", "socket2", "structopt", + "subtle", "tempfile", "thiserror", "tokio", diff --git a/client/src/main.rs b/client/src/main.rs index 8def1b3..867782b 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -21,7 +21,7 @@ mod util; use data_store::DataStore; use shared::{wg, Error}; -use util::{http_delete, http_get, http_post, http_put, human_duration, human_size}; +use util::{human_duration, human_size, Api}; #[derive(Debug, StructOpt)] #[structopt(name = "innernet", about)] @@ -216,8 +216,8 @@ fn install(invite: &Path, hosts_file: Option) -> Result<(), Error> { "[*]".dimmed(), &config.server.internal_endpoint ); - http_post( - &config.server.internal_endpoint, + Api::new(&config.server).http_form( + "POST", "/user/redeem", RedeemContents { public_key: keypair.public.to_base64(), @@ -334,7 +334,7 @@ fn fetch( println!("{} fetching state from server.", "[*]".dimmed()); let mut store = DataStore::open_or_create(&interface)?; - let State { peers, cidrs } = http_get(&config.server.internal_endpoint, "/user/state")?; + let State { peers, cidrs } = Api::new(&config.server).http("GET", "/user/state")?; let device_info = DeviceInfo::get_by_name(&interface)?; let interface_public_key = device_info @@ -419,12 +419,13 @@ fn fetch( fn add_cidr(interface: &InterfaceName) -> Result<(), Error> { let InterfaceConfig { server, .. } = InterfaceConfig::from_interface(interface)?; println!("Fetching CIDRs"); - let cidrs: Vec = http_get(&server.internal_endpoint, "/admin/cidrs")?; + let api = Api::new(&server); + let cidrs: Vec = api.http("GET", "/admin/cidrs")?; let cidr_request = prompts::add_cidr(&cidrs)?; println!("Creating CIDR..."); - let cidr: Cidr = http_post(&server.internal_endpoint, "/admin/cidrs", cidr_request)?; + let cidr: Cidr = api.http_form("POST", "/admin/cidrs", cidr_request)?; printdoc!( " @@ -443,15 +444,17 @@ fn add_cidr(interface: &InterfaceName) -> Result<(), Error> { fn add_peer(interface: &InterfaceName) -> Result<(), Error> { let InterfaceConfig { server, .. } = InterfaceConfig::from_interface(interface)?; + let api = Api::new(&server); + println!("Fetching CIDRs"); - let cidrs: Vec = http_get(&server.internal_endpoint, "/admin/cidrs")?; + let cidrs: Vec = api.http("GET", "/admin/cidrs")?; println!("Fetching peers"); - let peers: Vec = http_get(&server.internal_endpoint, "/admin/peers")?; + let peers: Vec = api.http("GET", "/admin/peers")?; let cidr_tree = CidrTree::new(&cidrs[..]); if let Some((peer_request, keypair)) = prompts::add_peer(&peers, &cidr_tree)? { println!("Creating peer..."); - let peer: Peer = http_post(&server.internal_endpoint, "/admin/peers", peer_request)?; + let peer: Peer = api.http_form("POST", "/admin/peers", peer_request)?; let server_peer = peers.iter().find(|p| p.id == 1).unwrap(); prompts::save_peer_invitation( interface, @@ -470,17 +473,15 @@ fn add_peer(interface: &InterfaceName) -> Result<(), Error> { fn enable_or_disable_peer(interface: &InterfaceName, enable: bool) -> Result<(), Error> { let InterfaceConfig { server, .. } = InterfaceConfig::from_interface(interface)?; + let api = Api::new(&server); + println!("Fetching peers."); - let peers: Vec = http_get(&server.internal_endpoint, "/admin/peers")?; + let peers: Vec = api.http("GET", "/admin/peers")?; if let Some(peer) = prompts::enable_or_disable_peer(&peers[..], enable)? { let Peer { id, mut contents } = peer; contents.is_disabled = !enable; - http_put( - &server.internal_endpoint, - &format!("/admin/peers/{}", id), - contents, - )?; + api.http_form("PUT", &format!("/admin/peers/{}", id), contents)?; } else { println!("exited without disabling peer."); } @@ -490,13 +491,14 @@ fn enable_or_disable_peer(interface: &InterfaceName, enable: bool) -> Result<(), fn add_association(interface: &InterfaceName) -> Result<(), Error> { let InterfaceConfig { server, .. } = InterfaceConfig::from_interface(interface)?; + let api = Api::new(&server); println!("Fetching CIDRs"); - let cidrs: Vec = http_get(&server.internal_endpoint, "/admin/cidrs")?; + let cidrs: Vec = api.http("GET", "/admin/cidrs")?; if let Some((cidr1, cidr2)) = prompts::add_association(&cidrs[..])? { - http_post( - &server.internal_endpoint, + api.http_form( + "POST", "/admin/associations", AssociationContents { cidr_id_1: cidr1.id, @@ -512,18 +514,15 @@ fn add_association(interface: &InterfaceName) -> Result<(), Error> { fn delete_association(interface: &InterfaceName) -> Result<(), Error> { let InterfaceConfig { server, .. } = InterfaceConfig::from_interface(interface)?; + let api = Api::new(&server); println!("Fetching CIDRs"); - let cidrs: Vec = http_get(&server.internal_endpoint, "/admin/cidrs")?; + let cidrs: Vec = api.http("GET", "/admin/cidrs")?; println!("Fetching associations"); - let associations: Vec = - http_get(&server.internal_endpoint, "/admin/associations")?; + let associations: Vec = api.http("GET", "/admin/associations")?; if let Some(association) = prompts::delete_association(&associations[..], &cidrs[..])? { - http_delete( - &server.internal_endpoint, - &format!("/admin/associations/{}", association.id), - )?; + api.http("DELETE", &format!("/admin/associations/{}", association.id))?; } else { println!("exited without adding association."); } @@ -533,11 +532,12 @@ fn delete_association(interface: &InterfaceName) -> Result<(), Error> { fn list_associations(interface: &InterfaceName) -> Result<(), Error> { let InterfaceConfig { server, .. } = InterfaceConfig::from_interface(interface)?; + let api = Api::new(&server); + println!("Fetching CIDRs"); - let cidrs: Vec = http_get(&server.internal_endpoint, "/admin/cidrs")?; + let cidrs: Vec = api.http("GET", "/admin/cidrs")?; println!("Fetching associations"); - let associations: Vec = - http_get(&server.internal_endpoint, "/admin/associations")?; + let associations: Vec = api.http("GET", "/admin/associations")?; for association in associations { println!( @@ -590,8 +590,8 @@ fn override_endpoint(interface: &InterfaceName, unset: bool) -> Result<(), Error if let Some(endpoint) = prompts::override_endpoint(unset)? { println!("Updating endpoint."); - http_put( - &config.server.internal_endpoint, + Api::new(&config.server).http_form( + "PUT", "/user/endpoint", EndpointContents::from(endpoint), )?; diff --git a/client/src/util.rs b/client/src/util.rs index ec9feda..e4ae6eb 100644 --- a/client/src/util.rs +++ b/client/src/util.rs @@ -1,7 +1,8 @@ use crate::{ClientError, Error}; use colored::*; use serde::{de::DeserializeOwned, Serialize}; -use std::{net::SocketAddr, time::Duration}; +use shared::{interface_config::ServerInfo, INNERNET_PUBKEY_HEADER}; +use std::time::Duration; pub fn human_duration(duration: Duration) -> String { match duration.as_secs() { @@ -46,41 +47,56 @@ pub fn human_size(bytes: u64) -> String { } } -pub fn http_get(server: &SocketAddr, endpoint: &str) -> Result { - let response = ureq::get(&format!("http://{}/v1{}", server, endpoint)).call()?; - process_response(response) +pub struct Api<'a> { + server: &'a ServerInfo, } -pub fn http_delete(server: &SocketAddr, endpoint: &str) -> Result<(), Error> { - ureq::get(&format!("http://{}/v1{}", server, endpoint)).call()?; - Ok(()) -} - -pub fn http_post( - server: &SocketAddr, - endpoint: &str, - form: S, -) -> Result { - let response = ureq::post(&format!("http://{}/v1{}", server, endpoint)) - .send_json(serde_json::to_value(form)?)?; - process_response(response) -} - -pub fn http_put(server: &SocketAddr, endpoint: &str, form: S) -> Result<(), Error> { - ureq::put(&format!("http://{}/v1{}", server, endpoint)) - .send_json(serde_json::to_value(form)?)?; - Ok(()) -} - -fn process_response(response: ureq::Response) -> Result { - let mut response = response.into_string()?; - if response.is_empty() { - response = "null".into(); +impl<'a> Api<'a> { + pub fn new(server: &'a ServerInfo) -> Self { + Self { server } + } + + pub fn http(&self, verb: &str, endpoint: &str) -> Result { + self.request::<(), _>(verb, endpoint, None) + } + + pub fn http_form( + &self, + verb: &str, + endpoint: &str, + form: S, + ) -> Result { + self.request(verb, endpoint, Some(form)) + } + + fn request( + &self, + verb: &str, + endpoint: &str, + form: Option, + ) -> Result { + let request = ureq::request( + verb, + &format!("http://{}/v1{}", self.server.internal_endpoint, endpoint), + ) + .set(INNERNET_PUBKEY_HEADER, &self.server.public_key); + + let response = if let Some(form) = form { + request.send_json(serde_json::to_value(form)?)? + } else { + request.call()? + }; + + let mut response = response.into_string()?; + // A little trick for serde to parse an empty response as `()`. + if response.is_empty() { + response = "null".into(); + } + Ok(serde_json::from_str(&response).map_err(|e| { + ClientError(format!( + "failed to deserialize JSON response from the server: {}, response={}", + e, &response + )) + })?) } - Ok(serde_json::from_str(&response).map_err(|e| { - ClientError(format!( - "failed to deserialize JSON response from the server: {}, response={}", - e, &response - )) - })?) } diff --git a/server/Cargo.toml b/server/Cargo.toml index 2b4061c..264aae5 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -30,6 +30,7 @@ rusqlite = "0.24" serde = { version = "1", features = ["derive"] } serde_json = "1" shared = { path = "../shared" } +subtle = "2" structopt = "0.3" thiserror = "1" tokio = { version = "1", features = ["macros", "rt-multi-thread"] } diff --git a/server/src/api/admin/cidr.rs b/server/src/api/admin/cidr.rs index f145102..39a6b02 100644 --- a/server/src/api/admin/cidr.rs +++ b/server/src/api/admin/cidr.rs @@ -104,7 +104,8 @@ mod tests { }; let filter = crate::routes(server.context()); - let res = test::post_request_from_ip(test::ADMIN_PEER_IP) + let res = server + .post_request_from_ip(test::ADMIN_PEER_IP) .path("/v1/admin/cidrs") .body(serde_json::to_string(&contents)?) .reply(&filter) @@ -132,7 +133,8 @@ mod tests { }; let filter = crate::routes(server.context()); - let res = test::post_request_from_ip(test::ADMIN_PEER_IP) + let res = server + .post_request_from_ip(test::ADMIN_PEER_IP) .path("/v1/admin/cidrs") .body(serde_json::to_string(&contents)?) .reply(&filter) @@ -145,7 +147,8 @@ mod tests { cidr: test::EXPERIMENTAL_SUBCIDR.parse()?, parent: Some(cidr_res.id), }; - let res = test::post_request_from_ip(test::ADMIN_PEER_IP) + let res = server + .post_request_from_ip(test::ADMIN_PEER_IP) .path("/v1/admin/cidrs") .body(serde_json::to_string(&contents)?) .reply(&filter) @@ -166,7 +169,8 @@ mod tests { }; let filter = crate::routes(server.context()); - let res = test::post_request_from_ip(test::USER1_PEER_IP) + let res = server + .post_request_from_ip(test::USER1_PEER_IP) .path("/v1/admin/cidrs") .body(serde_json::to_string(&contents)?) .reply(&filter) @@ -186,7 +190,8 @@ mod tests { parent: Some(test::ROOT_CIDR_ID), }; let filter = crate::routes(server.context()); - let res = test::post_request_from_ip(test::ADMIN_PEER_IP) + let res = server + .post_request_from_ip(test::ADMIN_PEER_IP) .path("/v1/admin/cidrs") .body(serde_json::to_string(&contents)?) .reply(&filter) @@ -200,7 +205,8 @@ mod tests { }; let filter = crate::routes(server.context()); - let res = test::post_request_from_ip(test::ADMIN_PEER_IP) + let res = server + .post_request_from_ip(test::ADMIN_PEER_IP) .path("/v1/admin/cidrs") .body(serde_json::to_string(&contents)?) .reply(&filter) @@ -220,7 +226,8 @@ mod tests { parent: Some(test::ROOT_CIDR_ID), }; let filter = crate::routes(server.context()); - let res = test::post_request_from_ip(test::ADMIN_PEER_IP) + let res = server + .post_request_from_ip(test::ADMIN_PEER_IP) .path("/v1/admin/cidrs") .body(serde_json::to_string(&contents)?) .reply(&filter) @@ -253,7 +260,8 @@ mod tests { let filter = crate::routes(server.context()); - let res = test::request_from_ip(test::ADMIN_PEER_IP) + let res = server + .request_from_ip(test::ADMIN_PEER_IP) .method("DELETE") .path(&format!("/v1/admin/cidrs/{}", experimental_cidr.id)) .reply(&filter) @@ -261,7 +269,8 @@ mod tests { // Should fail because child CIDR exists. assert_eq!(res.status(), StatusCode::BAD_REQUEST); - let res = test::request_from_ip(test::ADMIN_PEER_IP) + let res = server + .request_from_ip(test::ADMIN_PEER_IP) .method("DELETE") .path(&format!("/v1/admin/cidrs/{}", experimental_subcidr.id)) .reply(&filter) @@ -269,7 +278,8 @@ mod tests { // Deleting child "leaf" CIDR should fail because peer exists inside it. assert_eq!(res.status(), StatusCode::NO_CONTENT); - let res = test::request_from_ip(test::ADMIN_PEER_IP) + let res = server + .request_from_ip(test::ADMIN_PEER_IP) .method("DELETE") .path(&format!("/v1/admin/cidrs/{}", experimental_cidr.id)) .reply(&filter) @@ -304,7 +314,8 @@ mod tests { let filter = crate::routes(server.context()); - let res = test::request_from_ip(test::ADMIN_PEER_IP) + let res = server + .request_from_ip(test::ADMIN_PEER_IP) .method("DELETE") .path(&format!("/v1/admin/cidrs/{}", experimental_cidr.id)) .reply(&filter) diff --git a/server/src/api/admin/peer.rs b/server/src/api/admin/peer.rs index ab46778..5c356ab 100644 --- a/server/src/api/admin/peer.rs +++ b/server/src/api/admin/peer.rs @@ -147,7 +147,8 @@ mod tests { let peer = test::developer_peer_contents("developer3", "10.80.64.4")?; let filter = crate::routes(server.context()); - let res = test::post_request_from_ip(test::ADMIN_PEER_IP) + let res = server + .post_request_from_ip(test::ADMIN_PEER_IP) .path("/v1/admin/peers") .body(serde_json::to_string(&peer)?) .reply(&filter) @@ -172,7 +173,8 @@ mod tests { let peer = test::developer_peer_contents("devel oper", "10.80.64.4")?; let filter = crate::routes(server.context()); - let res = test::post_request_from_ip(test::ADMIN_PEER_IP) + let res = server + .post_request_from_ip(test::ADMIN_PEER_IP) .path("/v1/admin/peers") .body(serde_json::to_string(&peer)?) .reply(&filter) @@ -192,7 +194,8 @@ mod tests { let peer = test::developer_peer_contents("developer2", "10.80.64.4")?; let filter = crate::routes(server.context()); - let res = test::post_request_from_ip(test::ADMIN_PEER_IP) + let res = server + .post_request_from_ip(test::ADMIN_PEER_IP) .path("/v1/admin/peers") .body(serde_json::to_string(&peer)?) .reply(&filter) @@ -217,7 +220,8 @@ mod tests { let peer = test::developer_peer_contents("developer3", "10.80.64.3")?; let filter = crate::routes(server.context()); - let res = test::post_request_from_ip(test::ADMIN_PEER_IP) + let res = server + .post_request_from_ip(test::ADMIN_PEER_IP) .path("/v1/admin/peers") .body(serde_json::to_string(&peer)?) .reply(&filter) @@ -241,7 +245,8 @@ mod tests { // Try to add IP outside of the CIDR network. let peer = test::developer_peer_contents("developer3", "10.80.65.4")?; - let res = test::post_request_from_ip(test::ADMIN_PEER_IP) + let res = server + .post_request_from_ip(test::ADMIN_PEER_IP) .path("/v1/admin/peers") .body(serde_json::to_string(&peer)?) .reply(&filter) @@ -250,7 +255,8 @@ mod tests { // Try to use the network address as peer IP. let peer = test::developer_peer_contents("developer3", "10.80.64.0")?; - let res = test::post_request_from_ip(test::ADMIN_PEER_IP) + let res = server + .post_request_from_ip(test::ADMIN_PEER_IP) .path("/v1/admin/peers") .body(serde_json::to_string(&peer)?) .reply(&filter) @@ -259,7 +265,8 @@ mod tests { // Try to use the broadcast address as peer IP. let peer = test::developer_peer_contents("developer3", "10.80.64.255")?; - let res = test::post_request_from_ip(test::ADMIN_PEER_IP) + let res = server + .post_request_from_ip(test::ADMIN_PEER_IP) .path("/v1/admin/peers") .body(serde_json::to_string(&peer)?) .reply(&filter) @@ -281,7 +288,8 @@ mod tests { // Try to create a new developer peer from a user peer. let filter = crate::routes(server.context()); - let res = test::post_request_from_ip(test::USER1_PEER_IP) + let res = server + .post_request_from_ip(test::USER1_PEER_IP) .path("/v1/admin/peers") .body(serde_json::to_string(&peer)?) .reply(&filter) @@ -304,7 +312,8 @@ mod tests { // Try to create a new developer peer from a user peer. let filter = crate::routes(server.context()); - let res = test::put_request_from_ip(test::ADMIN_PEER_IP) + let res = server + .put_request_from_ip(test::ADMIN_PEER_IP) .path(&format!("/v1/admin/peers/{}", test::DEVELOPER1_PEER_ID)) .body(serde_json::to_string(&change)?) .reply(&filter) @@ -325,7 +334,8 @@ mod tests { // Try to create a new developer peer from a user peer. let filter = crate::routes(server.context()); - let res = test::put_request_from_ip(test::USER1_PEER_IP) + let res = server + .put_request_from_ip(test::USER1_PEER_IP) .path(&format!("/v1/admin/peers/{}", test::ADMIN_PEER_ID)) .body(serde_json::to_string(&peer)?) .reply(&filter) @@ -340,7 +350,8 @@ mod tests { async fn test_list_all_peers_from_admin() -> Result<()> { let server = test::Server::new()?; let filter = crate::routes(server.context()); - let res = test::request_from_ip(test::ADMIN_PEER_IP) + let res = server + .request_from_ip(test::ADMIN_PEER_IP) .path("/v1/admin/peers") .reply(&filter) .await; @@ -369,7 +380,8 @@ mod tests { async fn test_list_all_peers_from_non_admin() -> Result<()> { let server = test::Server::new()?; let filter = crate::routes(server.context()); - let res = test::request_from_ip(test::DEVELOPER1_PEER_IP) + let res = server + .request_from_ip(test::DEVELOPER1_PEER_IP) .path("/v1/admin/peers") .reply(&filter) .await; @@ -386,7 +398,8 @@ mod tests { let old_peers = DatabasePeer::list(&server.db().lock())?; - let res = test::request_from_ip(test::ADMIN_PEER_IP) + let res = server + .request_from_ip(test::ADMIN_PEER_IP) .method("DELETE") .path(&format!("/v1/admin/peers/{}", test::USER1_PEER_ID)) .reply(&filter) @@ -409,7 +422,8 @@ mod tests { let old_peers = DatabasePeer::list(&server.db().lock())?; - let res = test::request_from_ip(test::DEVELOPER1_PEER_IP) + let res = server + .request_from_ip(test::DEVELOPER1_PEER_IP) .method("DELETE") .path(&format!("/v1/admin/peers/{}", test::USER1_PEER_ID)) .reply(&filter) @@ -429,7 +443,8 @@ mod tests { let server = test::Server::new()?; let filter = crate::routes(server.context()); - let res = test::request_from_ip(test::ADMIN_PEER_IP) + let res = server + .request_from_ip(test::ADMIN_PEER_IP) .method("DELETE") .path(&format!("/v1/admin/peers/{}", test::USER1_PEER_ID + 100)) .reply(&filter) diff --git a/server/src/api/user.rs b/server/src/api/user.rs index 7d1dd1d..b7c5929 100644 --- a/server/src/api/user.rs +++ b/server/src/api/user.rs @@ -171,7 +171,8 @@ mod tests { async fn test_get_state_from_developer1() -> Result<()> { let server = test::Server::new()?; let filter = crate::routes(server.context()); - let res = test::request_from_ip(test::DEVELOPER1_PEER_IP) + let res = server + .request_from_ip(test::DEVELOPER1_PEER_IP) .path("/v1/user/state") .reply(&filter) .await; @@ -195,7 +196,8 @@ mod tests { let server = test::Server::new()?; let filter = crate::routes(server.context()); assert_eq!( - test::put_request_from_ip(test::DEVELOPER1_PEER_IP) + server + .put_request_from_ip(test::DEVELOPER1_PEER_IP) .path("/v1/user/endpoint") .body(serde_json::to_string(&EndpointContents::Set( "1.1.1.1:51820".parse()? @@ -208,7 +210,8 @@ mod tests { println!("{}", serde_json::to_string(&EndpointContents::Unset)?); assert_eq!( - test::put_request_from_ip(test::DEVELOPER1_PEER_IP) + server + .put_request_from_ip(test::DEVELOPER1_PEER_IP) .path("/v1/user/endpoint") .body(serde_json::to_string(&EndpointContents::Unset)?) .reply(&filter) @@ -218,7 +221,8 @@ mod tests { ); assert_eq!( - test::put_request_from_ip(test::DEVELOPER1_PEER_IP) + server + .put_request_from_ip(test::DEVELOPER1_PEER_IP) .path("/v1/user/endpoint") .body("endpoint=blah") .reply(&filter) @@ -236,7 +240,8 @@ mod tests { let filter = crate::routes(server.context()); // Request comes from an unknown IP. - let res = test::request_from_ip("10.80.80.80") + let res = server + .request_from_ip("10.80.80.80") .path("/v1/user/state") .reply(&filter) .await; @@ -296,7 +301,8 @@ mod tests { } for ip in &[test::DEVELOPER1_PEER_IP, test::EXPERIMENT_SUBCIDR_PEER_IP] { - let res = test::request_from_ip(ip) + let res = server + .request_from_ip(ip) .path("/v1/user/state") .reply(&filter) .await; @@ -344,7 +350,8 @@ mod tests { let filter = crate::routes(server.context()); // Step 1: Ensure that before redeeming, other endpoints aren't yet accessible. - let res = test::request_from_ip(test::EXPERIMENT_SUBCIDR_PEER_IP) + let res = server + .request_from_ip(test::EXPERIMENT_SUBCIDR_PEER_IP) .path("/v1/user/state") .reply(&filter) .await; @@ -354,7 +361,8 @@ mod tests { let body = RedeemContents { public_key: "YBVIgpfLbi/knrMCTEb0L6eVy0daiZnJJQkxBK9s+2I=".into(), }; - let res = test::post_request_from_ip(test::EXPERIMENT_SUBCIDR_PEER_IP) + let res = server + .post_request_from_ip(test::EXPERIMENT_SUBCIDR_PEER_IP) .path("/v1/user/redeem") .body(serde_json::to_string(&body)?) .reply(&filter) @@ -362,7 +370,8 @@ mod tests { assert!(res.status().is_success()); // Step 3: Ensure that a second attempt at redemption DOESN'T work. - let res = test::post_request_from_ip(test::EXPERIMENT_SUBCIDR_PEER_IP) + let res = server + .post_request_from_ip(test::EXPERIMENT_SUBCIDR_PEER_IP) .path("/v1/user/redeem") .body(serde_json::to_string(&body)?) .reply(&filter) @@ -370,7 +379,8 @@ mod tests { assert!(res.status().is_client_error()); // Step 3: Ensure that after redemption, fetching state works. - let res = test::request_from_ip(test::EXPERIMENT_SUBCIDR_PEER_IP) + let res = server + .request_from_ip(test::EXPERIMENT_SUBCIDR_PEER_IP) .path("/v1/user/state") .reply(&filter) .await; diff --git a/server/src/main.rs b/server/src/main.rs index 55b6639..899dcba 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -6,7 +6,7 @@ use ipnetwork::IpNetwork; use parking_lot::Mutex; use rusqlite::Connection; use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use shared::IoErrorContext; +use shared::{IoErrorContext, INNERNET_PUBKEY_HEADER}; use std::{ convert::Infallible, env, @@ -18,8 +18,9 @@ use std::{ sync::Arc, }; use structopt::StructOpt; -use warp::Filter; -use wgctrl::{DeviceConfigBuilder, DeviceInfo, InterfaceName, PeerConfigBuilder}; +use subtle::ConstantTimeEq; +use warp::{filters, Filter}; +use wgctrl::{DeviceConfigBuilder, DeviceInfo, InterfaceName, Key, PeerConfigBuilder}; pub mod api; pub mod db; @@ -68,6 +69,7 @@ pub struct Context { pub db: Db, pub endpoints: Arc, pub interface: InterfaceName, + pub public_key: Key, } pub struct Session { @@ -297,11 +299,13 @@ async fn serve(interface: &InterfaceName, conf: &ServerConfig) -> Result<(), Err log::info!("{} peers added to wireguard interface.", peers.len()); + let public_key = wgctrl::Key::from_base64(&config.private_key)?.generate_public(); let db = Arc::new(Mutex::new(conn)); let context = Context { db, interface: *interface, endpoints, + public_key, }; log::info!("innernet-server {} starting.", VERSION); @@ -363,34 +367,6 @@ pub fn routes( .recover(handle_rejection) } -pub fn with_unredeemed_session( - context: Context, -) -> impl Filter + Clone { - warp::filters::addr::remote() - .and_then(move |addr: Option| { - get_session(context.clone(), addr.map(|addr| addr.ip()), false, false) - }) - .map(|session| UnredeemedSession(session)) -} - -pub fn with_session( - context: Context, -) -> impl Filter + Clone { - warp::filters::addr::remote().and_then(move |addr: Option| { - get_session(context.clone(), addr.map(|addr| addr.ip()), false, true) - }) -} - -pub fn with_admin_session( - context: Context, -) -> impl Filter + Clone { - warp::filters::addr::remote() - .and_then(move |addr: Option| { - get_session(context.clone(), addr.map(|addr| addr.ip()), true, true) - }) - .map(|session| AdminSession(session)) -} - pub fn form_body() -> impl Filter + Clone where T: DeserializeOwned + Send, @@ -398,24 +374,88 @@ where warp::body::content_length_limit(1024 * 16).and(warp::body::json()) } +pub fn with_unredeemed_session( + context: Context, +) -> impl Filter + Clone { + filters::addr::remote() + .and(filters::header::header(INNERNET_PUBKEY_HEADER)) + .and_then(move |addr: Option, pubkey: String| { + get_session( + context.clone(), + addr.map(|addr| addr.ip()), + pubkey, + false, + false, + ) + }) + .map(|session| UnredeemedSession(session)) +} + +pub fn with_session( + context: Context, +) -> impl Filter + Clone { + filters::addr::remote() + .and(filters::header::header(INNERNET_PUBKEY_HEADER)) + .and_then(move |addr: Option, pubkey: String| { + get_session( + context.clone(), + addr.map(|addr| addr.ip()), + pubkey, + false, + true, + ) + }) +} + +pub fn with_admin_session( + context: Context, +) -> impl Filter + Clone { + filters::addr::remote() + .and(filters::header::header(INNERNET_PUBKEY_HEADER)) + .and_then(move |addr: Option, pubkey: String| { + get_session( + context.clone(), + addr.map(|addr| addr.ip()), + pubkey, + true, + true, + ) + }) + .map(|session| AdminSession(session)) +} + async fn get_session( context: Context, addr: Option, + pubkey: String, admin_only: bool, redeemed_only: bool, ) -> Result { - addr.map(|addr| -> Result { + _get_session(context, addr, pubkey, admin_only, redeemed_only) + .map_err(|_| warp::reject::custom(ServerError::Unauthorized)) +} + +fn _get_session( + context: Context, + addr: Option, + pubkey: String, + admin_only: bool, + redeemed_only: bool, +) -> Result { + let pubkey = Key::from_base64(&pubkey)?; + if pubkey.0.ct_eq(&context.public_key.0).into() { + let addr = addr.ok_or(ServerError::NotFound)?; let peer = DatabasePeer::get_from_ip(&context.db.lock(), addr)?; - if !peer.is_disabled && (!admin_only || peer.is_admin) && (!redeemed_only || peer.is_redeemed) { - Ok(Session { context, peer }) - } else { - Err(ServerError::Unauthorized) + if !peer.is_disabled + && (!admin_only || peer.is_admin) + && (!redeemed_only || peer.is_redeemed) + { + return Ok(Session { context, peer }); } - }) - .map(|session| session.ok()) - .flatten() // If no IP address is found, reject. - .ok_or_else(|| { warp::reject::custom(ServerError::Unauthorized)}) + } + + Err(ServerError::Unauthorized.into()) } #[cfg(test)] @@ -442,7 +482,8 @@ mod tests { let filter = routes(server.context()); // Request from an unknown IP, trying to disguise as an admin using HTTP headers. - let res = test::request_from_ip("10.80.80.80") + let res = server + .request_from_ip("10.80.80.80") .path("/v1/admin/peers") .header("Forwarded", format!("for={}", test::ADMIN_PEER_IP)) .header("X-Forwarded-For", test::ADMIN_PEER_IP) @@ -457,4 +498,48 @@ mod tests { Ok(()) } + + #[tokio::test] + async fn test_incorrect_public_key() -> Result<()> { + let server = test::Server::new()?; + let filter = routes(server.context()); + + let key = Key::generate_private().generate_public(); + + // Request from an unknown IP, trying to disguise as an admin using HTTP headers. + let res = server + .request_from_ip("10.80.80.80") + .path("/v1/admin/peers") + .header(shared::INNERNET_PUBKEY_HEADER, key.to_base64()) + .reply(&filter) + .await; + + // addr::remote() filter only look at remote_addr from TCP socket. + // HTTP headers are not considered. This also means that innernet + // server would not function behind an HTTP proxy. + assert_eq!(res.status(), StatusCode::UNAUTHORIZED); + + Ok(()) + } + + #[tokio::test] + async fn test_unparseable_public_key() -> Result<()> { + let server = test::Server::new()?; + let filter = routes(server.context()); + + // Request from an unknown IP, trying to disguise as an admin using HTTP headers. + let res = server + .request_from_ip("10.80.80.80") + .path("/v1/admin/peers") + .header(shared::INNERNET_PUBKEY_HEADER, "") + .reply(&filter) + .await; + + // addr::remote() filter only look at remote_addr from TCP socket. + // HTTP headers are not considered. This also means that innernet + // server would not function behind an HTTP proxy. + assert_eq!(res.status(), StatusCode::UNAUTHORIZED); + + Ok(()) + } } diff --git a/server/src/test.rs b/server/src/test.rs index d389320..696bf2d 100644 --- a/server/src/test.rs +++ b/server/src/test.rs @@ -12,7 +12,7 @@ use shared::{Cidr, CidrContents, PeerContents}; use std::{net::SocketAddr, path::PathBuf, sync::Arc}; use tempfile::TempDir; use warp::test::RequestBuilder; -use wgctrl::{InterfaceName, KeyPair}; +use wgctrl::{InterfaceName, Key, KeyPair}; pub const ROOT_CIDR: &str = "10.80.0.0/15"; pub const SERVER_CIDR: &str = "10.80.0.1/32"; @@ -47,6 +47,7 @@ pub struct Server { endpoints: Arc, interface: InterfaceName, conf: ServerConfig, + public_key: Key, // The directory will be removed during destruction. _test_dir: TempDir, } @@ -56,6 +57,7 @@ impl Server { let test_dir = tempfile::tempdir()?; let test_dir_path = test_dir.path(); + let public_key = Key::generate_private().generate_public(); // Run the init wizard to initialize the database and create basic // cidrs and peers. let interface = "test".to_string(); @@ -116,6 +118,7 @@ impl Server { db, endpoints, interface, + public_key, _test_dir: test_dir, }) } @@ -129,12 +132,32 @@ impl Server { db: self.db.clone(), interface: self.interface.clone(), endpoints: self.endpoints.clone(), + public_key: self.public_key.clone(), } } pub fn wg_conf_path(&self) -> PathBuf { self.conf.config_path(&self.interface) } + + pub fn request_from_ip(&self, ip_str: &str) -> RequestBuilder { + let port = 54321u16; + warp::test::request() + .remote_addr(SocketAddr::new(ip_str.parse().unwrap(), port)) + .header(shared::INNERNET_PUBKEY_HEADER, self.public_key.to_base64()) + } + + pub fn post_request_from_ip(&self, ip_str: &str) -> RequestBuilder { + self.request_from_ip(ip_str) + .method("POST") + .header("Content-Type", "application/json") + } + + pub fn put_request_from_ip(&self, ip_str: &str) -> RequestBuilder { + self.request_from_ip(ip_str) + .method("PUT") + .header("Content-Type", "application/json") + } } pub fn create_cidr(db: &Connection, name: &str, cidr_str: &str) -> Result { @@ -154,23 +177,6 @@ pub fn create_cidr(db: &Connection, name: &str, cidr_str: &str) -> Result // Below are helper functions for writing tests. // -pub fn request_from_ip(ip_str: &str) -> RequestBuilder { - let port = 54321u16; - warp::test::request().remote_addr(SocketAddr::new(ip_str.parse().unwrap(), port)) -} - -pub fn post_request_from_ip(ip_str: &str) -> RequestBuilder { - request_from_ip(ip_str) - .method("POST") - .header("Content-Type", "application/json") -} - -pub fn put_request_from_ip(ip_str: &str) -> RequestBuilder { - request_from_ip(ip_str) - .method("PUT") - .header("Content-Type", "application/json") -} - pub fn peer_contents( name: &str, ip_str: &str, diff --git a/shared/src/interface_config.rs b/shared/src/interface_config.rs index 053691c..f7b2426 100644 --- a/shared/src/interface_config.rs +++ b/shared/src/interface_config.rs @@ -11,7 +11,7 @@ use std::{ }; use wgctrl::InterfaceName; -#[derive(Deserialize, Serialize, Debug)] +#[derive(Clone, Deserialize, Serialize, Debug)] #[serde(rename_all = "kebab-case")] pub struct InterfaceConfig { /// The information to bring up the interface. @@ -21,7 +21,7 @@ pub struct InterfaceConfig { pub server: ServerInfo, } -#[derive(Deserialize, Serialize, Debug)] +#[derive(Clone, Deserialize, Serialize, Debug)] #[serde(rename_all = "kebab-case")] pub struct InterfaceInfo { /// The interface name (i.e. "tonari") @@ -38,7 +38,7 @@ pub struct InterfaceInfo { pub listen_port: Option, } -#[derive(Deserialize, Serialize, Debug)] +#[derive(Clone, Deserialize, Serialize, Debug)] #[serde(rename_all = "kebab-case")] pub struct ServerInfo { /// The server's WireGuard public key diff --git a/shared/src/lib.rs b/shared/src/lib.rs index 0b0e723..e7a5039 100644 --- a/shared/src/lib.rs +++ b/shared/src/lib.rs @@ -27,7 +27,8 @@ lazy_static! { pub static ref REDEEM_TRANSITION_WAIT: Duration = Duration::from_secs(5); } -pub static PERSISTENT_KEEPALIVE_INTERVAL_SECS: u16 = 25; +pub const PERSISTENT_KEEPALIVE_INTERVAL_SECS: u16 = 25; +pub const INNERNET_PUBKEY_HEADER: &str = "X-Innernet-Server-Key"; pub type Error = Box; diff --git a/wgctrl-rs/src/backends/userspace.rs b/wgctrl-rs/src/backends/userspace.rs index 107696b..a1eb698 100644 --- a/wgctrl-rs/src/backends/userspace.rs +++ b/wgctrl-rs/src/backends/userspace.rs @@ -366,7 +366,7 @@ pub fn apply(builder: DeviceConfigBuilder, iface: &InterfaceName) -> io::Result< /// `Key`s, especially ones created from external data. #[cfg(not(target_os = "linux"))] #[derive(PartialEq, Eq, Clone)] -pub struct Key([u8; 32]); +pub struct Key(pub [u8; 32]); #[cfg(not(target_os = "linux"))] impl Key {