{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.
pull/48/head
Jake McGinty 2021-04-09 13:48:00 +09:00 committed by GitHub
parent bcd68df772
commit a87d56cfc9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 311 additions and 165 deletions

1
Cargo.lock generated
View File

@ -1132,6 +1132,7 @@ dependencies = [
"shared",
"socket2",
"structopt",
"subtle",
"tempfile",
"thiserror",
"tokio",

View File

@ -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<PathBuf>) -> 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<Cidr> = http_get(&server.internal_endpoint, "/admin/cidrs")?;
let api = Api::new(&server);
let cidrs: Vec<Cidr> = 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<Cidr> = http_get(&server.internal_endpoint, "/admin/cidrs")?;
let cidrs: Vec<Cidr> = api.http("GET", "/admin/cidrs")?;
println!("Fetching peers");
let peers: Vec<Peer> = http_get(&server.internal_endpoint, "/admin/peers")?;
let peers: Vec<Peer> = 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<Peer> = http_get(&server.internal_endpoint, "/admin/peers")?;
let peers: Vec<Peer> = 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<Cidr> = http_get(&server.internal_endpoint, "/admin/cidrs")?;
let cidrs: Vec<Cidr> = 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<Cidr> = http_get(&server.internal_endpoint, "/admin/cidrs")?;
let cidrs: Vec<Cidr> = api.http("GET", "/admin/cidrs")?;
println!("Fetching associations");
let associations: Vec<Association> =
http_get(&server.internal_endpoint, "/admin/associations")?;
let associations: Vec<Association> = 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<Cidr> = http_get(&server.internal_endpoint, "/admin/cidrs")?;
let cidrs: Vec<Cidr> = api.http("GET", "/admin/cidrs")?;
println!("Fetching associations");
let associations: Vec<Association> =
http_get(&server.internal_endpoint, "/admin/associations")?;
let associations: Vec<Association> = 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),
)?;

View File

@ -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<T: DeserializeOwned>(server: &SocketAddr, endpoint: &str) -> Result<T, Error> {
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<S: Serialize, D: DeserializeOwned>(
server: &SocketAddr,
endpoint: &str,
form: S,
) -> Result<D, Error> {
let response = ureq::post(&format!("http://{}/v1{}", server, endpoint))
.send_json(serde_json::to_value(form)?)?;
process_response(response)
}
pub fn http_put<S: Serialize>(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<T: DeserializeOwned>(response: ureq::Response) -> Result<T, Error> {
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<T: DeserializeOwned>(&self, verb: &str, endpoint: &str) -> Result<T, Error> {
self.request::<(), _>(verb, endpoint, None)
}
pub fn http_form<S: Serialize, T: DeserializeOwned>(
&self,
verb: &str,
endpoint: &str,
form: S,
) -> Result<T, Error> {
self.request(verb, endpoint, Some(form))
}
fn request<S: Serialize, T: DeserializeOwned>(
&self,
verb: &str,
endpoint: &str,
form: Option<S>,
) -> Result<T, Error> {
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
))
})?)
}

View File

@ -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"] }

View File

@ -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)

View File

@ -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)

View File

@ -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;

View File

@ -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<Endpoints>,
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<Extract = (UnredeemedSession,), Error = warp::Rejection> + Clone {
warp::filters::addr::remote()
.and_then(move |addr: Option<SocketAddr>| {
get_session(context.clone(), addr.map(|addr| addr.ip()), false, false)
})
.map(|session| UnredeemedSession(session))
}
pub fn with_session(
context: Context,
) -> impl Filter<Extract = (Session,), Error = warp::Rejection> + Clone {
warp::filters::addr::remote().and_then(move |addr: Option<SocketAddr>| {
get_session(context.clone(), addr.map(|addr| addr.ip()), false, true)
})
}
pub fn with_admin_session(
context: Context,
) -> impl Filter<Extract = (AdminSession,), Error = warp::Rejection> + Clone {
warp::filters::addr::remote()
.and_then(move |addr: Option<SocketAddr>| {
get_session(context.clone(), addr.map(|addr| addr.ip()), true, true)
})
.map(|session| AdminSession(session))
}
pub fn form_body<T>() -> impl Filter<Extract = (T,), Error = warp::Rejection> + 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<Extract = (UnredeemedSession,), Error = warp::Rejection> + Clone {
filters::addr::remote()
.and(filters::header::header(INNERNET_PUBKEY_HEADER))
.and_then(move |addr: Option<SocketAddr>, 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<Extract = (Session,), Error = warp::Rejection> + Clone {
filters::addr::remote()
.and(filters::header::header(INNERNET_PUBKEY_HEADER))
.and_then(move |addr: Option<SocketAddr>, pubkey: String| {
get_session(
context.clone(),
addr.map(|addr| addr.ip()),
pubkey,
false,
true,
)
})
}
pub fn with_admin_session(
context: Context,
) -> impl Filter<Extract = (AdminSession,), Error = warp::Rejection> + Clone {
filters::addr::remote()
.and(filters::header::header(INNERNET_PUBKEY_HEADER))
.and_then(move |addr: Option<SocketAddr>, 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<IpAddr>,
pubkey: String,
admin_only: bool,
redeemed_only: bool,
) -> Result<Session, warp::Rejection> {
addr.map(|addr| -> Result<Session, ServerError> {
_get_session(context, addr, pubkey, admin_only, redeemed_only)
.map_err(|_| warp::reject::custom(ServerError::Unauthorized))
}
fn _get_session(
context: Context,
addr: Option<IpAddr>,
pubkey: String,
admin_only: bool,
redeemed_only: bool,
) -> Result<Session, Error> {
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(())
}
}

View File

@ -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<Endpoints>,
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<Cidr> {
@ -154,23 +177,6 @@ pub fn create_cidr(db: &Connection, name: &str, cidr_str: &str) -> Result<Cidr>
// 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,

View File

@ -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<u16>,
}
#[derive(Deserialize, Serialize, Debug)]
#[derive(Clone, Deserialize, Serialize, Debug)]
#[serde(rename_all = "kebab-case")]
pub struct ServerInfo {
/// The server's WireGuard public key

View File

@ -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<dyn std::error::Error>;

View File

@ -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 {