mirror of
https://github.com/tonarino/innernet.git
synced 2024-11-24 08:42:33 +02:00
client, server: invite expirations
The server now expects a UNIX timestamp after which the invitation will be expired. If a peer invite hasn't been redeemed after it expires, the server will clean up old entries and allow the IP to be re-allocated for a new invite. Closes #24
This commit is contained in:
parent
76500b3778
commit
2ce552cc36
8
Cargo.lock
generated
8
Cargo.lock
generated
@ -792,9 +792,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.5.3"
|
||||
version = "1.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce5f1ceb7f74abbce32601642fcf8e8508a8a8991e0621c7d750295b9095702b"
|
||||
checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@ -1220,9 +1220,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.2.1"
|
||||
version = "2.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ccd964113622c8e9322cfac19eb1004a07e636c545f325da085d5cdde6f1f8b"
|
||||
checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna",
|
||||
|
@ -135,7 +135,7 @@ mod tests {
|
||||
static ref BASE_PEERS: Vec<Peer> = vec![Peer {
|
||||
id: 0,
|
||||
contents: PeerContents {
|
||||
name: "blah".to_string(),
|
||||
name: "blah".parse().unwrap(),
|
||||
ip: "10.0.0.1".parse().unwrap(),
|
||||
cidr_id: 1,
|
||||
public_key: "abc".to_string(),
|
||||
@ -144,6 +144,7 @@ mod tests {
|
||||
is_disabled: false,
|
||||
is_redeemed: true,
|
||||
persistent_keepalive_interval: None,
|
||||
invite_expires: None,
|
||||
}
|
||||
}];
|
||||
static ref BASE_CIDRS: Vec<Cidr> = vec![Cidr {
|
||||
|
@ -38,6 +38,7 @@ SERVER_CONTAINER=$(cmd docker run -itd --rm \
|
||||
--cap-add NET_ADMIN \
|
||||
innernet-server)
|
||||
|
||||
info "server started as $SERVER_CONTAINER"
|
||||
info "Waiting for server to initialize."
|
||||
cmd sleep 10
|
||||
|
||||
@ -49,6 +50,7 @@ PEER1_CONTAINER=$(cmd docker create --rm -it \
|
||||
--env INTERFACE=evilcorp \
|
||||
--cap-add NET_ADMIN \
|
||||
innernet)
|
||||
info "peer1 started as $PEER1_CONTAINER"
|
||||
cmd docker cp "$tmp_dir/peer1.toml" "$PEER1_CONTAINER:/app/invite.toml"
|
||||
cmd docker start "$PEER1_CONTAINER"
|
||||
sleep 5
|
||||
@ -75,6 +77,7 @@ cmd docker exec "$PEER1_CONTAINER" innernet \
|
||||
--admin false \
|
||||
--auto-ip \
|
||||
--save-config "/app/peer2.toml" \
|
||||
--invite-expires "30d" \
|
||||
--yes
|
||||
cmd docker cp "$PEER1_CONTAINER:/app/peer2.toml" "$tmp_dir"
|
||||
|
||||
@ -85,6 +88,7 @@ PEER2_CONTAINER=$(docker create --rm -it \
|
||||
--cap-add NET_ADMIN \
|
||||
--env INTERFACE=evilcorp \
|
||||
innernet)
|
||||
info "peer2 started as $PEER2_CONTAINER"
|
||||
cmd docker cp "$tmp_dir/peer2.toml" "$PEER2_CONTAINER:/app/invite.toml"
|
||||
cmd docker start "$PEER2_CONTAINER"
|
||||
sleep 10
|
||||
|
@ -7,8 +7,19 @@ innernet-server new \
|
||||
--external-endpoint "172.18.1.1:51820" \
|
||||
--listen-port 51820
|
||||
|
||||
innernet-server add-cidr evilcorp --name "humans" --cidr "10.66.1.0/24" --parent "evilcorp" --yes
|
||||
innernet-server add-cidr evilcorp \
|
||||
--name "humans" \
|
||||
--cidr "10.66.1.0/24" \
|
||||
--parent "evilcorp" \
|
||||
--yes
|
||||
|
||||
innernet-server add-peer evilcorp --name "admin" --cidr "humans" --admin true --auto-ip --save-config "peer1.toml" --yes
|
||||
innernet-server add-peer evilcorp \
|
||||
--name "admin" \
|
||||
--cidr "humans" \
|
||||
--admin true \
|
||||
--auto-ip \
|
||||
--save-config "peer1.toml" \
|
||||
--invite-expires "30d" \
|
||||
--yes
|
||||
|
||||
innernet-server serve evilcorp
|
||||
|
@ -64,10 +64,10 @@ mod tests {
|
||||
use crate::{test, DatabasePeer};
|
||||
use anyhow::Result;
|
||||
use bytes::Buf;
|
||||
use shared::Cidr;
|
||||
use shared::{Cidr, Error};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_cidr_add() -> Result<()> {
|
||||
async fn test_cidr_add() -> Result<(), Error> {
|
||||
let server = test::Server::new()?;
|
||||
|
||||
let old_cidrs = DatabaseCidr::list(&server.db().lock())?;
|
||||
@ -95,7 +95,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_cidr_name_uniqueness() -> Result<()> {
|
||||
async fn test_cidr_name_uniqueness() -> Result<(), Error> {
|
||||
let server = test::Server::new()?;
|
||||
|
||||
let contents = CidrContents {
|
||||
@ -125,7 +125,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_cidr_create_auth() -> Result<()> {
|
||||
async fn test_cidr_create_auth() -> Result<(), Error> {
|
||||
let server = test::Server::new()?;
|
||||
|
||||
let contents = CidrContents {
|
||||
@ -143,7 +143,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_cidr_bad_parent() -> Result<()> {
|
||||
async fn test_cidr_bad_parent() -> Result<(), Error> {
|
||||
let server = test::Server::new()?;
|
||||
|
||||
let contents = CidrContents {
|
||||
@ -171,7 +171,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_cidr_overlap() -> Result<()> {
|
||||
async fn test_cidr_overlap() -> Result<(), Error> {
|
||||
let server = test::Server::new()?;
|
||||
|
||||
let contents = CidrContents {
|
||||
@ -188,7 +188,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_cidr_delete_fail_with_child_cidr() -> Result<()> {
|
||||
async fn test_cidr_delete_fail_with_child_cidr() -> Result<(), Error> {
|
||||
let server = test::Server::new()?;
|
||||
|
||||
let experimental_cidr = DatabaseCidr::create(
|
||||
@ -241,7 +241,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_cidr_delete_fail_with_peer_inside() -> Result<()> {
|
||||
async fn test_cidr_delete_fail_with_peer_inside() -> Result<(), Error> {
|
||||
let server = test::Server::new()?;
|
||||
|
||||
let experimental_cidr = DatabaseCidr::create(
|
||||
|
@ -94,12 +94,11 @@ mod handlers {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test;
|
||||
use anyhow::Result;
|
||||
use bytes::Buf;
|
||||
use shared::Peer;
|
||||
use shared::{Error, Peer};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_add_peer() -> Result<()> {
|
||||
async fn test_add_peer() -> Result<(), Error> {
|
||||
let server = test::Server::new()?;
|
||||
|
||||
let old_peers = DatabasePeer::list(&server.db().lock())?;
|
||||
@ -125,21 +124,13 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_add_peer_with_invalid_name() -> Result<()> {
|
||||
let server = test::Server::new()?;
|
||||
|
||||
let peer = test::developer_peer_contents("devel oper", "10.80.64.4")?;
|
||||
|
||||
let res = server
|
||||
.form_request(test::ADMIN_PEER_IP, "POST", "/v1/admin/peers", &peer)
|
||||
.await;
|
||||
|
||||
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
|
||||
async fn test_add_peer_with_invalid_name() -> Result<(), Error> {
|
||||
assert!(test::developer_peer_contents("devel oper", "10.80.64.4").is_err());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_add_peer_with_duplicate_name() -> Result<()> {
|
||||
async fn test_add_peer_with_duplicate_name() -> Result<(), Error> {
|
||||
let server = test::Server::new()?;
|
||||
|
||||
let old_peers = DatabasePeer::list(&server.db().lock())?;
|
||||
@ -161,7 +152,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_add_peer_with_duplicate_ip() -> Result<()> {
|
||||
async fn test_add_peer_with_duplicate_ip() -> Result<(), Error> {
|
||||
let server = test::Server::new()?;
|
||||
|
||||
let old_peers = DatabasePeer::list(&server.db().lock())?;
|
||||
@ -183,7 +174,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_add_peer_with_outside_cidr_range_ip() -> Result<()> {
|
||||
async fn test_add_peer_with_outside_cidr_range_ip() -> Result<(), Error> {
|
||||
let server = test::Server::new()?;
|
||||
|
||||
let old_peers = DatabasePeer::list(&server.db().lock())?;
|
||||
@ -217,7 +208,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_add_peer_from_non_admin() -> Result<()> {
|
||||
async fn test_add_peer_from_non_admin() -> Result<(), Error> {
|
||||
let server = test::Server::new()?;
|
||||
|
||||
let peer = test::developer_peer_contents("developer3", "10.80.64.4")?;
|
||||
@ -233,12 +224,12 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_update_peer_from_admin() -> Result<()> {
|
||||
async fn test_update_peer_from_admin() -> Result<(), Error> {
|
||||
let server = test::Server::new()?;
|
||||
let old_peer = DatabasePeer::get(&server.db.lock(), test::DEVELOPER1_PEER_ID)?;
|
||||
|
||||
let change = PeerContents {
|
||||
name: "new-peer-name".to_string(),
|
||||
name: "new-peer-name".parse()?,
|
||||
..old_peer.contents.clone()
|
||||
};
|
||||
|
||||
@ -255,12 +246,12 @@ mod tests {
|
||||
assert_eq!(res.status(), StatusCode::NO_CONTENT);
|
||||
|
||||
let new_peer = DatabasePeer::get(&server.db.lock(), test::DEVELOPER1_PEER_ID)?;
|
||||
assert_eq!(new_peer.name, "new-peer-name");
|
||||
assert_eq!(&*new_peer.name, "new-peer-name");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_update_peer_from_non_admin() -> Result<()> {
|
||||
async fn test_update_peer_from_non_admin() -> Result<(), Error> {
|
||||
let server = test::Server::new()?;
|
||||
|
||||
let peer = test::developer_peer_contents("developer3", "10.80.64.4")?;
|
||||
@ -281,7 +272,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_list_all_peers_from_admin() -> Result<()> {
|
||||
async fn test_list_all_peers_from_admin() -> Result<(), Error> {
|
||||
let server = test::Server::new()?;
|
||||
let res = server
|
||||
.request(test::ADMIN_PEER_IP, "GET", "/v1/admin/peers")
|
||||
@ -291,7 +282,7 @@ mod tests {
|
||||
|
||||
let whole_body = hyper::body::aggregate(res).await?;
|
||||
let peers: Vec<Peer> = serde_json::from_reader(whole_body.reader())?;
|
||||
let peer_names = peers.iter().map(|p| &p.contents.name).collect::<Vec<_>>();
|
||||
let peer_names = peers.iter().map(|p| &*p.contents.name).collect::<Vec<_>>();
|
||||
// An admin peer should see all the peers.
|
||||
assert_eq!(
|
||||
&[
|
||||
@ -309,7 +300,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_list_all_peers_from_non_admin() -> Result<()> {
|
||||
async fn test_list_all_peers_from_non_admin() -> Result<(), Error> {
|
||||
let server = test::Server::new()?;
|
||||
let res = server
|
||||
.request(test::DEVELOPER1_PEER_IP, "GET", "/v1/admin/peers")
|
||||
@ -321,7 +312,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_delete() -> Result<()> {
|
||||
async fn test_delete() -> Result<(), Error> {
|
||||
let server = test::Server::new()?;
|
||||
let old_peers = DatabasePeer::list(&server.db().lock())?;
|
||||
|
||||
@ -344,7 +335,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_delete_from_non_admin() -> Result<()> {
|
||||
async fn test_delete_from_non_admin() -> Result<(), Error> {
|
||||
let server = test::Server::new()?;
|
||||
|
||||
let old_peers = DatabasePeer::list(&server.db().lock())?;
|
||||
@ -367,7 +358,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_delete_unknown_id() -> Result<()> {
|
||||
async fn test_delete_unknown_id() -> Result<(), Error> {
|
||||
let server = test::Server::new()?;
|
||||
|
||||
let res = server
|
||||
|
@ -80,43 +80,37 @@ mod handlers {
|
||||
let old_public_key = wgctrl::Key::from_base64(&selected_peer.public_key)
|
||||
.map_err(|_| ServerError::WireGuard)?;
|
||||
|
||||
if selected_peer.is_redeemed {
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::GONE)
|
||||
.body(Body::empty())?)
|
||||
} else {
|
||||
selected_peer.redeem(&conn, &form.public_key)?;
|
||||
selected_peer.redeem(&conn, &form.public_key)?;
|
||||
|
||||
if cfg!(not(test)) {
|
||||
let interface = session.context.interface;
|
||||
if cfg!(not(test)) {
|
||||
let interface = session.context.interface;
|
||||
|
||||
// If we were to modify the WireGuard interface immediately, the HTTP response wouldn't
|
||||
// get through. Instead, we need to wait a reasonable amount for the HTTP response to
|
||||
// flush, then update the interface.
|
||||
//
|
||||
// The client is also expected to wait the same amount of time after receiving a success
|
||||
// response from /redeem.
|
||||
//
|
||||
// This might be avoidable if we were able to run code after we were certain the response
|
||||
// had flushed over the TCP socket, but that isn't easily accessible from this high-level
|
||||
// web framework.
|
||||
tokio::task::spawn(async move {
|
||||
tokio::time::sleep(*REDEEM_TRANSITION_WAIT).await;
|
||||
log::info!(
|
||||
"WireGuard: adding new peer {}, removing old pubkey {}",
|
||||
&*selected_peer,
|
||||
old_public_key.to_base64()
|
||||
);
|
||||
DeviceConfigBuilder::new()
|
||||
.remove_peer_by_key(&old_public_key)
|
||||
.add_peer((&*selected_peer).into())
|
||||
.apply(&interface)
|
||||
.map_err(|e| log::error!("{:?}", e))
|
||||
.ok();
|
||||
});
|
||||
}
|
||||
status_response(StatusCode::NO_CONTENT)
|
||||
// If we were to modify the WireGuard interface immediately, the HTTP response wouldn't
|
||||
// get through. Instead, we need to wait a reasonable amount for the HTTP response to
|
||||
// flush, then update the interface.
|
||||
//
|
||||
// The client is also expected to wait the same amount of time after receiving a success
|
||||
// response from /redeem.
|
||||
//
|
||||
// This might be avoidable if we were able to run code after we were certain the response
|
||||
// had flushed over the TCP socket, but that isn't easily accessible from this high-level
|
||||
// web framework.
|
||||
tokio::task::spawn(async move {
|
||||
tokio::time::sleep(*REDEEM_TRANSITION_WAIT).await;
|
||||
log::info!(
|
||||
"WireGuard: adding new peer {}, removing old pubkey {}",
|
||||
&*selected_peer,
|
||||
old_public_key.to_base64()
|
||||
);
|
||||
DeviceConfigBuilder::new()
|
||||
.remove_peer_by_key(&old_public_key)
|
||||
.add_peer((&*selected_peer).into())
|
||||
.apply(&interface)
|
||||
.map_err(|e| log::error!("{:?}", e))
|
||||
.ok();
|
||||
});
|
||||
}
|
||||
status_response(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
/// Redeems an invitation. An invitation includes a WireGuard keypair generated by either the server
|
||||
@ -147,14 +141,15 @@ mod handlers {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use super::*;
|
||||
use crate::{db::DatabaseAssociation, test};
|
||||
use anyhow::Result;
|
||||
use bytes::Buf;
|
||||
use shared::{AssociationContents, CidrContents, EndpointContents};
|
||||
use shared::{AssociationContents, CidrContents, EndpointContents, Error};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_state_from_developer1() -> Result<()> {
|
||||
async fn test_get_state_from_developer1() -> Result<(), Error> {
|
||||
let server = test::Server::new()?;
|
||||
let res = server
|
||||
.request(test::DEVELOPER1_PEER_IP, "GET", "/v1/user/state")
|
||||
@ -164,7 +159,7 @@ mod tests {
|
||||
|
||||
let whole_body = hyper::body::aggregate(res).await?;
|
||||
let State { peers, .. } = serde_json::from_reader(whole_body.reader())?;
|
||||
let mut peer_names = peers.iter().map(|p| &p.contents.name).collect::<Vec<_>>();
|
||||
let mut peer_names = peers.iter().map(|p| &*p.contents.name).collect::<Vec<_>>();
|
||||
peer_names.sort();
|
||||
// Developers should see only peers in infra CIDR and developer CIDR.
|
||||
assert_eq!(
|
||||
@ -176,7 +171,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_override_endpoint() -> Result<()> {
|
||||
async fn test_override_endpoint() -> Result<(), Error> {
|
||||
let server = test::Server::new()?;
|
||||
assert_eq!(
|
||||
server
|
||||
@ -222,7 +217,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_list_peers_from_unknown_ip() -> Result<()> {
|
||||
async fn test_list_peers_from_unknown_ip() -> Result<(), Error> {
|
||||
let server = test::Server::new()?;
|
||||
|
||||
// Request comes from an unknown IP.
|
||||
@ -234,7 +229,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_list_peers_for_developer_subcidr() -> Result<()> {
|
||||
async fn test_list_peers_for_developer_subcidr() -> Result<(), Error> {
|
||||
let server = test::Server::new()?;
|
||||
{
|
||||
let db = server.db.lock();
|
||||
@ -286,7 +281,7 @@ mod tests {
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
let whole_body = hyper::body::aggregate(res).await?;
|
||||
let State { peers, .. } = serde_json::from_reader(whole_body.reader())?;
|
||||
let mut peer_names = peers.iter().map(|p| &p.contents.name).collect::<Vec<_>>();
|
||||
let mut peer_names = peers.iter().map(|p| &*p.contents.name).collect::<Vec<_>>();
|
||||
peer_names.sort();
|
||||
// Developers should see only peers in infra CIDR and developer CIDR.
|
||||
assert_eq!(
|
||||
@ -304,7 +299,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_redeem() -> Result<()> {
|
||||
async fn test_redeem() -> Result<(), Error> {
|
||||
let server = test::Server::new()?;
|
||||
|
||||
let experimental_cidr = DatabaseCidr::create(
|
||||
@ -323,6 +318,7 @@ mod tests {
|
||||
false,
|
||||
)?;
|
||||
peer_contents.is_redeemed = false;
|
||||
peer_contents.invite_expires = Some(SystemTime::now() + Duration::from_secs(100));
|
||||
let _experiment_peer = DatabasePeer::create(&server.db().lock(), peer_contents)?;
|
||||
|
||||
// Step 1: Ensure that before redeeming, other endpoints aren't yet accessible.
|
||||
@ -363,4 +359,49 @@ mod tests {
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_redeem_expired() -> Result<(), Error> {
|
||||
let server = test::Server::new()?;
|
||||
|
||||
let experimental_cidr = DatabaseCidr::create(
|
||||
&server.db().lock(),
|
||||
CidrContents {
|
||||
name: "experimental".to_string(),
|
||||
cidr: test::EXPERIMENTAL_CIDR.parse()?,
|
||||
parent: Some(test::ROOT_CIDR_ID),
|
||||
},
|
||||
)?;
|
||||
|
||||
let mut peer_contents = test::peer_contents(
|
||||
"experiment-peer",
|
||||
test::EXPERIMENT_SUBCIDR_PEER_IP,
|
||||
experimental_cidr.id,
|
||||
false,
|
||||
)?;
|
||||
peer_contents.is_redeemed = false;
|
||||
peer_contents.invite_expires = Some(SystemTime::now() - Duration::from_secs(1));
|
||||
let _experiment_peer = DatabasePeer::create(&server.db().lock(), peer_contents)?;
|
||||
|
||||
// Step 1: Ensure that before redeeming, other endpoints aren't yet accessible.
|
||||
let res = server
|
||||
.request(test::EXPERIMENT_SUBCIDR_PEER_IP, "GET", "/v1/user/state")
|
||||
.await;
|
||||
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
|
||||
|
||||
// Step 2: Ensure that redemption works.
|
||||
let body = RedeemContents {
|
||||
public_key: "YBVIgpfLbi/knrMCTEb0L6eVy0daiZnJJQkxBK9s+2I=".into(),
|
||||
};
|
||||
let res = server
|
||||
.form_request(
|
||||
test::EXPERIMENT_SUBCIDR_PEER_IP,
|
||||
"POST",
|
||||
"/v1/user/redeem",
|
||||
&body,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -5,3 +5,30 @@ pub mod peer;
|
||||
pub use association::DatabaseAssociation;
|
||||
pub use cidr::DatabaseCidr;
|
||||
pub use peer::DatabasePeer;
|
||||
use rusqlite::params;
|
||||
|
||||
const INVITE_EXPIRATION_VERSION: usize = 1;
|
||||
|
||||
pub const CURRENT_VERSION: usize = INVITE_EXPIRATION_VERSION;
|
||||
|
||||
pub fn auto_migrate(conn: &rusqlite::Connection) -> Result<(), rusqlite::Error> {
|
||||
let old_version: usize = conn.pragma_query_value(None, "user_version", |r| r.get(0))?;
|
||||
|
||||
if old_version < INVITE_EXPIRATION_VERSION {
|
||||
conn.execute(
|
||||
"ALTER TABLE peers ADD COLUMN invite_expires INTEGER",
|
||||
params![],
|
||||
)?;
|
||||
}
|
||||
|
||||
conn.pragma_update(None, "user_version", &CURRENT_VERSION)?;
|
||||
if old_version != CURRENT_VERSION {
|
||||
log::info!(
|
||||
"migrated db version from {} to {}",
|
||||
old_version,
|
||||
CURRENT_VERSION
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ use shared::{Peer, PeerContents, PERSISTENT_KEEPALIVE_INTERVAL_SECS};
|
||||
use std::{
|
||||
net::IpAddr,
|
||||
ops::{Deref, DerefMut},
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
use structopt::lazy_static;
|
||||
|
||||
@ -20,6 +21,7 @@ pub static CREATE_TABLE_SQL: &str = "CREATE TABLE peers (
|
||||
is_admin INTEGER DEFAULT 0 NOT NULL, /* Admin capabilities are per-peer, not per-CIDR. */
|
||||
is_disabled INTEGER DEFAULT 0 NOT NULL, /* Is the peer disabled? (peers cannot be deleted) */
|
||||
is_redeemed INTEGER DEFAULT 0 NOT NULL, /* Has the peer redeemed their invite yet? */
|
||||
invite_expires INTEGER, /* The UNIX time that an invited peer can no longer redeem. */
|
||||
FOREIGN KEY (cidr_id)
|
||||
REFERENCES cidrs (id)
|
||||
ON UPDATE RESTRICT
|
||||
@ -68,6 +70,7 @@ impl DatabasePeer {
|
||||
is_admin,
|
||||
is_disabled,
|
||||
is_redeemed,
|
||||
invite_expires,
|
||||
..
|
||||
} = &contents;
|
||||
log::info!("creating peer {:?}", contents);
|
||||
@ -88,10 +91,15 @@ impl DatabasePeer {
|
||||
return Err(ServerError::InvalidQuery);
|
||||
}
|
||||
|
||||
let invite_expires = invite_expires
|
||||
.map(|t| t.duration_since(SystemTime::UNIX_EPOCH).ok())
|
||||
.flatten()
|
||||
.map(|t| t.as_secs());
|
||||
|
||||
conn.execute(
|
||||
"INSERT INTO peers (name, ip, cidr_id, public_key, endpoint, is_admin, is_disabled, is_redeemed) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
|
||||
"INSERT INTO peers (name, ip, cidr_id, public_key, endpoint, is_admin, is_disabled, is_redeemed, invite_expires) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
|
||||
params![
|
||||
name,
|
||||
&**name,
|
||||
ip.to_string(),
|
||||
cidr_id,
|
||||
&public_key,
|
||||
@ -99,6 +107,7 @@ impl DatabasePeer {
|
||||
is_admin,
|
||||
is_disabled,
|
||||
is_redeemed,
|
||||
invite_expires,
|
||||
],
|
||||
)?;
|
||||
let id = conn.last_insert_rowid();
|
||||
@ -137,7 +146,7 @@ impl DatabasePeer {
|
||||
is_disabled = ?4
|
||||
WHERE id = ?5",
|
||||
params![
|
||||
new_contents.name,
|
||||
&*new_contents.name,
|
||||
new_contents
|
||||
.endpoint
|
||||
.as_ref()
|
||||
@ -163,6 +172,14 @@ impl DatabasePeer {
|
||||
}
|
||||
|
||||
pub fn redeem(&mut self, conn: &Connection, pubkey: &str) -> Result<(), ServerError> {
|
||||
if self.is_redeemed {
|
||||
return Err(ServerError::Gone);
|
||||
}
|
||||
|
||||
if matches!(self.invite_expires, Some(time) if time < SystemTime::now()) {
|
||||
return Err(ServerError::Unauthorized);
|
||||
}
|
||||
|
||||
match conn.execute(
|
||||
"UPDATE peers SET is_redeemed = 1, public_key = ?1 WHERE id = ?2 AND is_redeemed = 0",
|
||||
params![pubkey, self.id],
|
||||
@ -178,7 +195,10 @@ impl DatabasePeer {
|
||||
|
||||
fn from_row(row: &rusqlite::Row) -> Result<Self, rusqlite::Error> {
|
||||
let id = row.get(0)?;
|
||||
let name = row.get(1)?;
|
||||
let name = row
|
||||
.get::<_, String>(1)?
|
||||
.parse()
|
||||
.map_err(|_| rusqlite::Error::ExecuteReturnedResults)?;
|
||||
let ip: IpAddr = row
|
||||
.get::<_, String>(2)?
|
||||
.parse()
|
||||
@ -191,6 +211,10 @@ impl DatabasePeer {
|
||||
let is_admin = row.get(6)?;
|
||||
let is_disabled = row.get(7)?;
|
||||
let is_redeemed = row.get(8)?;
|
||||
let invite_expires = row
|
||||
.get::<_, Option<u64>>(9)?
|
||||
.map(|unixtime| SystemTime::UNIX_EPOCH + Duration::from_secs(unixtime));
|
||||
|
||||
let persistent_keepalive_interval = Some(PERSISTENT_KEEPALIVE_INTERVAL_SECS);
|
||||
|
||||
Ok(Peer {
|
||||
@ -205,6 +229,7 @@ impl DatabasePeer {
|
||||
is_admin,
|
||||
is_disabled,
|
||||
is_redeemed,
|
||||
invite_expires,
|
||||
},
|
||||
}
|
||||
.into())
|
||||
@ -213,7 +238,7 @@ impl DatabasePeer {
|
||||
pub fn get(conn: &Connection, id: i64) -> Result<Self, ServerError> {
|
||||
let result = conn.query_row(
|
||||
"SELECT
|
||||
id, name, ip, cidr_id, public_key, endpoint, is_admin, is_disabled, is_redeemed
|
||||
id, name, ip, cidr_id, public_key, endpoint, is_admin, is_disabled, is_redeemed, invite_expires
|
||||
FROM peers
|
||||
WHERE id = ?1",
|
||||
params![id],
|
||||
@ -226,7 +251,7 @@ impl DatabasePeer {
|
||||
pub fn get_from_ip(conn: &Connection, ip: IpAddr) -> Result<Self, rusqlite::Error> {
|
||||
let result = conn.query_row(
|
||||
"SELECT
|
||||
id, name, ip, cidr_id, public_key, endpoint, is_admin, is_disabled, is_redeemed
|
||||
id, name, ip, cidr_id, public_key, endpoint, is_admin, is_disabled, is_redeemed, invite_expires
|
||||
FROM peers
|
||||
WHERE ip = ?1",
|
||||
params![ip.to_string()],
|
||||
@ -264,7 +289,7 @@ impl DatabasePeer {
|
||||
UNION
|
||||
SELECT id FROM cidrs, associated_subcidrs WHERE cidrs.parent=associated_subcidrs.cidr_id
|
||||
)
|
||||
SELECT DISTINCT peers.id, peers.name, peers.ip, peers.cidr_id, peers.public_key, peers.endpoint, peers.is_admin, peers.is_disabled, peers.is_redeemed
|
||||
SELECT DISTINCT peers.id, peers.name, peers.ip, peers.cidr_id, peers.public_key, peers.endpoint, peers.is_admin, peers.is_disabled, peers.is_redeemed, peers.invite_expires
|
||||
FROM peers
|
||||
JOIN associated_subcidrs ON peers.cidr_id=associated_subcidrs.cidr_id
|
||||
WHERE peers.is_disabled = 0 AND peers.is_redeemed = 1;",
|
||||
@ -277,10 +302,23 @@ impl DatabasePeer {
|
||||
|
||||
pub fn list(conn: &Connection) -> Result<Vec<Self>, ServerError> {
|
||||
let mut stmt = conn.prepare_cached(
|
||||
"SELECT id, name, ip, cidr_id, public_key, endpoint, is_admin, is_disabled, is_redeemed FROM peers",
|
||||
"SELECT id, name, ip, cidr_id, public_key, endpoint, is_admin, is_disabled, is_redeemed, invite_expires FROM peers",
|
||||
)?;
|
||||
let peer_iter = stmt.query_map(params![], Self::from_row)?;
|
||||
|
||||
Ok(peer_iter.collect::<Result<_, _>>()?)
|
||||
}
|
||||
|
||||
pub fn delete_expired_invites(conn: &Connection) -> Result<usize, ServerError> {
|
||||
let unix_now = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.expect("Something is horribly wrong with system time.");
|
||||
let deleted = conn.execute(
|
||||
"DELETE FROM peers
|
||||
WHERE is_redeemed = 0 AND invite_expires < ?1",
|
||||
params![unix_now.as_secs()],
|
||||
)?;
|
||||
|
||||
Ok(deleted)
|
||||
}
|
||||
}
|
||||
|
@ -1,63 +0,0 @@
|
||||
use parking_lot::RwLock;
|
||||
use wgctrl::{DeviceInfo, InterfaceName};
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
io,
|
||||
net::SocketAddr,
|
||||
sync::{
|
||||
mpsc::{sync_channel, SyncSender, TryRecvError},
|
||||
Arc,
|
||||
},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
pub struct Endpoints {
|
||||
pub endpoints: Arc<RwLock<HashMap<String, SocketAddr>>>,
|
||||
stop_tx: SyncSender<()>,
|
||||
}
|
||||
|
||||
impl std::ops::Deref for Endpoints {
|
||||
type Target = RwLock<HashMap<String, SocketAddr>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.endpoints
|
||||
}
|
||||
}
|
||||
|
||||
impl Endpoints {
|
||||
pub fn new(iface: &InterfaceName) -> Result<Self, io::Error> {
|
||||
let endpoints = Arc::new(RwLock::new(HashMap::new()));
|
||||
let (stop_tx, stop_rx) = sync_channel(1);
|
||||
|
||||
let iface = iface.to_owned();
|
||||
let thread_endpoints = endpoints.clone();
|
||||
log::info!("spawning endpoint watch thread.");
|
||||
if cfg!(not(test)) {
|
||||
thread::spawn(move || loop {
|
||||
if matches!(stop_rx.try_recv(), Ok(_) | Err(TryRecvError::Disconnected)) {
|
||||
break;
|
||||
}
|
||||
if let Ok(info) = DeviceInfo::get_by_name(&iface) {
|
||||
for peer in info.peers {
|
||||
if let Some(endpoint) = peer.config.endpoint {
|
||||
thread_endpoints
|
||||
.write()
|
||||
.insert(peer.config.public_key.to_base64(), endpoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
});
|
||||
}
|
||||
Ok(Self { endpoints, stop_tx })
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Endpoints {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.stop_tx.send(());
|
||||
}
|
||||
}
|
@ -14,6 +14,9 @@ pub enum ServerError {
|
||||
#[error("invalid query")]
|
||||
InvalidQuery,
|
||||
|
||||
#[error("endpoint gone")]
|
||||
Gone,
|
||||
|
||||
#[error("internal database error")]
|
||||
Database(#[from] rusqlite::Error),
|
||||
|
||||
@ -39,6 +42,7 @@ impl<'a> From<&'a ServerError> for StatusCode {
|
||||
match error {
|
||||
Unauthorized => StatusCode::UNAUTHORIZED,
|
||||
NotFound => StatusCode::NOT_FOUND,
|
||||
Gone => StatusCode::GONE,
|
||||
InvalidQuery | Json(_) => StatusCode::BAD_REQUEST,
|
||||
// Special-case the constraint violation situation.
|
||||
Database(rusqlite::Error::SqliteFailure(libsqlite3_sys::Error { code, .. }, ..))
|
||||
|
@ -4,8 +4,7 @@ use dialoguer::{theme::ColorfulTheme, Input};
|
||||
use indoc::printdoc;
|
||||
use rusqlite::{params, Connection};
|
||||
use shared::{
|
||||
prompts::{self, hostname_validator},
|
||||
CidrContents, Endpoint, PeerContents, PERSISTENT_KEEPALIVE_INTERVAL_SECS,
|
||||
prompts, CidrContents, Endpoint, Hostname, PeerContents, PERSISTENT_KEEPALIVE_INTERVAL_SECS,
|
||||
};
|
||||
use wgctrl::KeyPair;
|
||||
|
||||
@ -17,6 +16,8 @@ fn create_database<P: AsRef<Path>>(
|
||||
conn.execute(db::peer::CREATE_TABLE_SQL, params![])?;
|
||||
conn.execute(db::association::CREATE_TABLE_SQL, params![])?;
|
||||
conn.execute(db::cidr::CREATE_TABLE_SQL, params![])?;
|
||||
conn.pragma_update(None, "user_version", &db::CURRENT_VERSION)?;
|
||||
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
@ -24,7 +25,7 @@ fn create_database<P: AsRef<Path>>(
|
||||
pub struct InitializeOpts {
|
||||
/// The network name (ex: evilcorp)
|
||||
#[structopt(long)]
|
||||
pub network_name: Option<String>,
|
||||
pub network_name: Option<Hostname>,
|
||||
|
||||
/// The network CIDR (ex: 10.42.0.0/16)
|
||||
#[structopt(long)]
|
||||
@ -78,7 +79,7 @@ fn populate_database(conn: &Connection, db_init_data: DbInitData) -> Result<(),
|
||||
let _me = DatabasePeer::create(
|
||||
&conn,
|
||||
PeerContents {
|
||||
name: SERVER_NAME.into(),
|
||||
name: SERVER_NAME.parse()?,
|
||||
ip: db_init_data.our_ip,
|
||||
cidr_id: server_cidr.id,
|
||||
public_key: db_init_data.public_key_base64,
|
||||
@ -87,6 +88,7 @@ fn populate_database(conn: &Connection, db_init_data: DbInitData) -> Result<(),
|
||||
is_disabled: false,
|
||||
is_redeemed: true,
|
||||
persistent_keepalive_interval: Some(PERSISTENT_KEEPALIVE_INTERVAL_SECS),
|
||||
invite_expires: None,
|
||||
},
|
||||
)
|
||||
.map_err(|_| "failed to create innernet peer.".to_string())?;
|
||||
@ -104,13 +106,12 @@ pub fn init_wizard(conf: &ServerConfig, opts: InitializeOpts) -> Result<(), Erro
|
||||
)
|
||||
})?;
|
||||
|
||||
let name: String = if let Some(name) = opts.network_name {
|
||||
let name: Hostname = if let Some(name) = opts.network_name {
|
||||
name
|
||||
} else {
|
||||
println!("Here you'll specify the network CIDR, which will encompass the entire network.");
|
||||
Input::with_theme(&theme)
|
||||
.with_prompt("Network name")
|
||||
.validate_with(hostname_validator)
|
||||
.interact()?
|
||||
};
|
||||
|
||||
|
@ -3,12 +3,12 @@ use dialoguer::Confirm;
|
||||
use hyper::{http, server::conn::AddrStream, Body, Request, Response};
|
||||
use indoc::printdoc;
|
||||
use ipnetwork::IpNetwork;
|
||||
use parking_lot::Mutex;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use rusqlite::Connection;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use shared::{AddCidrOpts, AddPeerOpts, IoErrorContext, INNERNET_PUBKEY_HEADER};
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
collections::{HashMap, VecDeque},
|
||||
convert::TryInto,
|
||||
env,
|
||||
fs::File,
|
||||
@ -17,6 +17,7 @@ use std::{
|
||||
ops::Deref,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use structopt::StructOpt;
|
||||
use subtle::ConstantTimeEq;
|
||||
@ -24,7 +25,6 @@ use wgctrl::{DeviceConfigBuilder, DeviceInfo, InterfaceName, Key, PeerConfigBuil
|
||||
|
||||
pub mod api;
|
||||
pub mod db;
|
||||
pub mod endpoints;
|
||||
pub mod error;
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
@ -33,7 +33,6 @@ pub mod util;
|
||||
mod initialize;
|
||||
|
||||
use db::{DatabaseCidr, DatabasePeer};
|
||||
pub use endpoints::Endpoints;
|
||||
pub use error::ServerError;
|
||||
use initialize::InitializeOpts;
|
||||
use shared::{prompts, wg, CidrTree, Error, Interface, SERVER_CONFIG_DIR, SERVER_DATABASE_DIR};
|
||||
@ -81,11 +80,12 @@ enum Command {
|
||||
}
|
||||
|
||||
pub type Db = Arc<Mutex<Connection>>;
|
||||
pub type Endpoints = Arc<RwLock<HashMap<String, SocketAddr>>>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Context {
|
||||
pub db: Db,
|
||||
pub endpoints: Arc<Endpoints>,
|
||||
pub endpoints: Arc<RwLock<HashMap<String, SocketAddr>>>,
|
||||
pub interface: InterfaceName,
|
||||
pub public_key: Key,
|
||||
}
|
||||
@ -206,7 +206,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
},
|
||||
Command::Uninstall { interface } => uninstall(&interface, &conf)?,
|
||||
Command::Serve { interface } => serve(&interface, &conf).await?,
|
||||
Command::Serve { interface } => serve(*interface, &conf).await?,
|
||||
Command::AddPeer { interface, args } => add_peer(&interface, &conf, args)?,
|
||||
Command::AddCidr { interface, args } => add_cidr(&interface, &conf, args)?,
|
||||
}
|
||||
@ -230,7 +230,7 @@ fn open_database_connection(
|
||||
let conn = Connection::open(&database_path)?;
|
||||
// Foreign key constraints aren't on in SQLite by default. Enable.
|
||||
conn.pragma_update(None, "foreign_keys", &1)?;
|
||||
|
||||
db::auto_migrate(&conn)?;
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
@ -334,9 +334,45 @@ fn uninstall(interface: &InterfaceName, conf: &ServerConfig) -> Result<(), Error
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn serve(interface: &InterfaceName, conf: &ServerConfig) -> Result<(), Error> {
|
||||
let config = ConfigFile::from_file(conf.config_path(interface))?;
|
||||
let conn = open_database_connection(interface, conf)?;
|
||||
fn spawn_endpoint_refresher(interface: InterfaceName) -> Endpoints {
|
||||
let endpoints = Arc::new(RwLock::new(HashMap::new()));
|
||||
tokio::task::spawn({
|
||||
let endpoints = endpoints.clone();
|
||||
async move {
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(10));
|
||||
loop {
|
||||
interval.tick().await;
|
||||
if let Ok(info) = DeviceInfo::get_by_name(&interface) {
|
||||
for peer in info.peers {
|
||||
if let Some(endpoint) = peer.config.endpoint {
|
||||
endpoints
|
||||
.write()
|
||||
.insert(peer.config.public_key.to_base64(), endpoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
endpoints
|
||||
}
|
||||
|
||||
fn spawn_expired_invite_sweeper(db: Db) {
|
||||
tokio::task::spawn(async move {
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(10));
|
||||
loop {
|
||||
interval.tick().await;
|
||||
match DatabasePeer::delete_expired_invites(&db.lock()) {
|
||||
Ok(deleted) => log::info!("Deleted {} expired peer invitations.", deleted),
|
||||
Err(e) => log::error!("Failed to delete expired peer invitations: {}", e),
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn serve(interface: InterfaceName, conf: &ServerConfig) -> Result<(), Error> {
|
||||
let config = ConfigFile::from_file(conf.config_path(&interface))?;
|
||||
let conn = open_database_connection(&interface, conf)?;
|
||||
|
||||
let peers = DatabasePeer::list(&conn)?;
|
||||
let peer_configs = peers
|
||||
@ -346,7 +382,7 @@ async fn serve(interface: &InterfaceName, conf: &ServerConfig) -> Result<(), Err
|
||||
|
||||
log::info!("bringing up interface.");
|
||||
wg::up(
|
||||
interface,
|
||||
&interface,
|
||||
&config.private_key,
|
||||
IpNetwork::new(config.address, config.network_cidr_prefix)?,
|
||||
Some(config.listen_port),
|
||||
@ -357,22 +393,23 @@ async fn serve(interface: &InterfaceName, conf: &ServerConfig) -> Result<(), Err
|
||||
.add_peers(&peer_configs)
|
||||
.apply(&interface)?;
|
||||
|
||||
let endpoints = Arc::new(Endpoints::new(&interface)?);
|
||||
|
||||
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 endpoints = spawn_endpoint_refresher(interface);
|
||||
spawn_expired_invite_sweeper(db.clone());
|
||||
|
||||
let context = Context {
|
||||
db,
|
||||
interface: *interface,
|
||||
interface,
|
||||
endpoints,
|
||||
public_key,
|
||||
};
|
||||
|
||||
log::info!("innernet-server {} starting.", VERSION);
|
||||
|
||||
let listener = get_listener((config.address, config.listen_port).into(), interface)?;
|
||||
let listener = get_listener((config.address, config.listen_port).into(), &interface)?;
|
||||
|
||||
let make_svc = hyper::service::make_service_fn(move |socket: &AddrStream| {
|
||||
let remote_addr = socket.remote_addr();
|
||||
@ -492,7 +529,7 @@ mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
#[test]
|
||||
fn test_init_wizard() -> Result<()> {
|
||||
fn test_init_wizard() -> Result<(), Error> {
|
||||
// This runs init_wizard().
|
||||
let server = test::Server::new()?;
|
||||
|
||||
@ -502,7 +539,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_with_session_disguised_with_headers() -> Result<()> {
|
||||
async fn test_with_session_disguised_with_headers() -> Result<(), Error> {
|
||||
let server = test::Server::new()?;
|
||||
|
||||
let req = Request::builder()
|
||||
@ -525,7 +562,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_incorrect_public_key() -> Result<()> {
|
||||
async fn test_incorrect_public_key() -> Result<(), Error> {
|
||||
let server = test::Server::new()?;
|
||||
|
||||
let key = Key::generate_private().generate_public();
|
||||
@ -547,7 +584,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_unparseable_public_key() -> Result<()> {
|
||||
async fn test_unparseable_public_key() -> Result<(), Error> {
|
||||
let server = test::Server::new()?;
|
||||
|
||||
let req = Request::builder()
|
||||
|
@ -1,17 +1,16 @@
|
||||
#![allow(dead_code)]
|
||||
use crate::{
|
||||
db::{DatabaseCidr, DatabasePeer},
|
||||
endpoints::Endpoints,
|
||||
initialize::{init_wizard, InitializeOpts},
|
||||
Context, ServerConfig,
|
||||
Context, Db, Endpoints, ServerConfig,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::anyhow;
|
||||
use hyper::{header::HeaderValue, http, Body, Request, Response};
|
||||
use parking_lot::Mutex;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use rusqlite::Connection;
|
||||
use serde::Serialize;
|
||||
use shared::{Cidr, CidrContents, PeerContents};
|
||||
use std::{net::SocketAddr, path::PathBuf, sync::Arc};
|
||||
use shared::{Cidr, CidrContents, Error, PeerContents};
|
||||
use std::{collections::HashMap, net::SocketAddr, path::PathBuf, sync::Arc};
|
||||
use tempfile::TempDir;
|
||||
use wgctrl::{InterfaceName, Key, KeyPair};
|
||||
|
||||
@ -44,8 +43,8 @@ pub const USER1_PEER_ID: i64 = 5;
|
||||
pub const USER2_PEER_ID: i64 = 6;
|
||||
|
||||
pub struct Server {
|
||||
pub db: Arc<Mutex<Connection>>,
|
||||
endpoints: Arc<Endpoints>,
|
||||
pub db: Db,
|
||||
endpoints: Endpoints,
|
||||
interface: InterfaceName,
|
||||
conf: ServerConfig,
|
||||
public_key: Key,
|
||||
@ -54,7 +53,7 @@ pub struct Server {
|
||||
}
|
||||
|
||||
impl Server {
|
||||
pub fn new() -> Result<Self> {
|
||||
pub fn new() -> Result<Self, Error> {
|
||||
let test_dir = tempfile::tempdir()?;
|
||||
let test_dir_path = test_dir.path();
|
||||
|
||||
@ -68,7 +67,7 @@ impl Server {
|
||||
};
|
||||
|
||||
let opts = InitializeOpts {
|
||||
network_name: Some(interface.clone()),
|
||||
network_name: Some(interface.parse()?),
|
||||
network_cidr: Some(ROOT_CIDR.parse()?),
|
||||
external_endpoint: Some("155.155.155.155:54321".parse().unwrap()),
|
||||
listen_port: Some(54321),
|
||||
@ -116,7 +115,7 @@ impl Server {
|
||||
);
|
||||
|
||||
let db = Arc::new(Mutex::new(db));
|
||||
let endpoints = Arc::new(Endpoints::new(&interface)?);
|
||||
let endpoints = Arc::new(RwLock::new(HashMap::new()));
|
||||
|
||||
Ok(Self {
|
||||
conf,
|
||||
@ -192,7 +191,7 @@ impl Server {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_cidr(db: &Connection, name: &str, cidr_str: &str) -> Result<Cidr> {
|
||||
pub fn create_cidr(db: &Connection, name: &str, cidr_str: &str) -> Result<Cidr, Error> {
|
||||
let cidr = DatabaseCidr::create(
|
||||
db,
|
||||
CidrContents {
|
||||
@ -214,11 +213,11 @@ pub fn peer_contents(
|
||||
ip_str: &str,
|
||||
cidr_id: i64,
|
||||
is_admin: bool,
|
||||
) -> Result<PeerContents> {
|
||||
) -> Result<PeerContents, Error> {
|
||||
let public_key = KeyPair::generate().public;
|
||||
|
||||
Ok(PeerContents {
|
||||
name: name.to_string(),
|
||||
name: name.parse()?,
|
||||
ip: ip_str.parse()?,
|
||||
cidr_id,
|
||||
public_key: public_key.to_base64(),
|
||||
@ -227,21 +226,22 @@ pub fn peer_contents(
|
||||
persistent_keepalive_interval: None,
|
||||
is_disabled: false,
|
||||
is_redeemed: true,
|
||||
invite_expires: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn admin_peer_contents(name: &str, ip_str: &str) -> Result<PeerContents> {
|
||||
pub fn admin_peer_contents(name: &str, ip_str: &str) -> Result<PeerContents, Error> {
|
||||
peer_contents(name, ip_str, ADMIN_CIDR_ID, true)
|
||||
}
|
||||
|
||||
pub fn infra_peer_contents(name: &str, ip_str: &str) -> Result<PeerContents> {
|
||||
pub fn infra_peer_contents(name: &str, ip_str: &str) -> Result<PeerContents, Error> {
|
||||
peer_contents(name, ip_str, INFRA_CIDR_ID, false)
|
||||
}
|
||||
|
||||
pub fn developer_peer_contents(name: &str, ip_str: &str) -> Result<PeerContents> {
|
||||
pub fn developer_peer_contents(name: &str, ip_str: &str) -> Result<PeerContents, Error> {
|
||||
peer_contents(name, ip_str, DEVELOPER_CIDR_ID, false)
|
||||
}
|
||||
|
||||
pub fn user_peer_contents(name: &str, ip_str: &str) -> Result<PeerContents> {
|
||||
pub fn user_peer_contents(name: &str, ip_str: &str) -> Result<PeerContents, Error> {
|
||||
peer_contents(name, ip_str, USER_CIDR_ID, false)
|
||||
}
|
||||
|
@ -7,29 +7,14 @@ use colored::*;
|
||||
use dialoguer::{theme::ColorfulTheme, Confirm, Input, Select};
|
||||
use ipnetwork::IpNetwork;
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::{
|
||||
net::{IpAddr, SocketAddr},
|
||||
time::SystemTime,
|
||||
};
|
||||
use wgctrl::{InterfaceName, KeyPair};
|
||||
|
||||
lazy_static! {
|
||||
pub static ref THEME: ColorfulTheme = ColorfulTheme::default();
|
||||
|
||||
/// Regex to match the requirements of hostname(7), needed to have peers also be reachable hostnames.
|
||||
/// Note that the full length also must be maximum 63 characters, which this regex does not check.
|
||||
static ref PEER_NAME_REGEX: Regex = Regex::new(r"^([a-z0-9]-?)*[a-z0-9]$").unwrap();
|
||||
}
|
||||
|
||||
pub fn is_valid_hostname(name: &str) -> bool {
|
||||
name.len() < 64 && PEER_NAME_REGEX.is_match(name)
|
||||
}
|
||||
|
||||
#[allow(clippy::ptr_arg)]
|
||||
pub fn hostname_validator(name: &String) -> Result<(), &'static str> {
|
||||
if is_valid_hostname(name) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("not a valid hostname")
|
||||
}
|
||||
}
|
||||
|
||||
/// Bring up a prompt to create a new CIDR. Returns the peer request.
|
||||
@ -200,10 +185,7 @@ pub fn add_peer(
|
||||
let name = if let Some(ref name) = args.name {
|
||||
name.clone()
|
||||
} else {
|
||||
Input::with_theme(&*THEME)
|
||||
.with_prompt("Name")
|
||||
.validate_with(hostname_validator)
|
||||
.interact()?
|
||||
Input::with_theme(&*THEME).with_prompt("Name").interact()?
|
||||
};
|
||||
|
||||
let is_admin = if let Some(is_admin) = args.admin {
|
||||
@ -215,6 +197,15 @@ pub fn add_peer(
|
||||
.interact()?
|
||||
};
|
||||
|
||||
let invite_expires = if let Some(ref invite_expires) = args.invite_expires {
|
||||
invite_expires.clone()
|
||||
} else {
|
||||
Input::with_theme(&*THEME)
|
||||
.with_prompt("Invite expires after")
|
||||
.default("14d".parse()?)
|
||||
.interact()?
|
||||
};
|
||||
|
||||
let default_keypair = KeyPair::generate();
|
||||
let peer_request = PeerContents {
|
||||
name,
|
||||
@ -226,6 +217,7 @@ pub fn add_peer(
|
||||
is_disabled: false,
|
||||
is_redeemed: false,
|
||||
persistent_keepalive_interval: Some(PERSISTENT_KEEPALIVE_INTERVAL_SECS),
|
||||
invite_expires: Some(SystemTime::now() + invite_expires.into()),
|
||||
};
|
||||
|
||||
Ok(
|
||||
|
@ -1,5 +1,6 @@
|
||||
use crate::prompts::hostname_validator;
|
||||
use ipnetwork::IpNetwork;
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
fmt::{self, Display, Formatter},
|
||||
@ -7,6 +8,7 @@ use std::{
|
||||
ops::Deref,
|
||||
path::Path,
|
||||
str::FromStr,
|
||||
time::{Duration, SystemTime},
|
||||
vec,
|
||||
};
|
||||
use structopt::StructOpt;
|
||||
@ -22,8 +24,9 @@ impl FromStr for Interface {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(name: &str) -> Result<Self, Self::Err> {
|
||||
let name = name.to_string();
|
||||
hostname_validator(&name)?;
|
||||
if !Hostname::is_valid(name) {
|
||||
return Err("interface name is not a valid hostname".into());
|
||||
}
|
||||
let name = name
|
||||
.parse()
|
||||
.map_err(|e: InvalidInterfaceName| e.to_string())?;
|
||||
@ -284,7 +287,7 @@ pub struct InstallOpts {
|
||||
pub struct AddPeerOpts {
|
||||
/// Name of new peer
|
||||
#[structopt(long)]
|
||||
pub name: Option<String>,
|
||||
pub name: Option<Hostname>,
|
||||
|
||||
/// Specify desired IP of new peer (within parent CIDR)
|
||||
#[structopt(long, conflicts_with = "auto-ip")]
|
||||
@ -309,6 +312,10 @@ pub struct AddPeerOpts {
|
||||
/// Save the config to the given location
|
||||
#[structopt(long)]
|
||||
pub save_config: Option<String>,
|
||||
|
||||
/// Invite expiration period (eg. "30d", "7w", "2h", "60m", "1000s")
|
||||
#[structopt(long)]
|
||||
pub invite_expires: Option<Timestring>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, StructOpt)]
|
||||
@ -341,7 +348,7 @@ pub struct AddAssociationOpts {
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
pub struct PeerContents {
|
||||
pub name: String,
|
||||
pub name: Hostname,
|
||||
pub ip: IpAddr,
|
||||
pub cidr_id: i64,
|
||||
pub public_key: String,
|
||||
@ -350,6 +357,7 @@ pub struct PeerContents {
|
||||
pub is_admin: bool,
|
||||
pub is_disabled: bool,
|
||||
pub is_redeemed: bool,
|
||||
pub invite_expires: Option<SystemTime>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
@ -481,6 +489,93 @@ pub struct State {
|
||||
pub cidrs: Vec<Cidr>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Timestring {
|
||||
timestring: String,
|
||||
seconds: u64,
|
||||
}
|
||||
|
||||
impl Display for Timestring {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(&self.timestring)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Timestring {
|
||||
type Err = &'static str;
|
||||
|
||||
fn from_str(timestring: &str) -> Result<Self, Self::Err> {
|
||||
if timestring.len() < 2 {
|
||||
Err("timestring isn't long enough!")
|
||||
} else {
|
||||
let (n, suffix) = timestring.split_at(timestring.len() - 1);
|
||||
let n: u64 = n.parse().map_err(|_| {
|
||||
"invalid timestring (a number followed by a time unit character, eg. '15m')"
|
||||
})?;
|
||||
let multiplier = match suffix {
|
||||
"s" => Ok(1),
|
||||
"m" => Ok(60),
|
||||
"h" => Ok(60 * 60),
|
||||
"d" => Ok(60 * 60 * 24),
|
||||
"w" => Ok(60 * 60 * 24 * 7),
|
||||
_ => Err("invalid timestring suffix (must be one of 's', 'm', 'h', 'd', or 'w')"),
|
||||
}?;
|
||||
|
||||
Ok(Self {
|
||||
timestring: timestring.to_string(),
|
||||
seconds: n * multiplier,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Timestring> for Duration {
|
||||
fn from(timestring: Timestring) -> Self {
|
||||
Duration::from_secs(timestring.seconds)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Hostname(String);
|
||||
|
||||
lazy_static! {
|
||||
/// Regex to match the requirements of hostname(7), needed to have peers also be reachable hostnames.
|
||||
/// Note that the full length also must be maximum 63 characters, which this regex does not check.
|
||||
static ref HOSTNAME_REGEX: Regex = Regex::new(r"^([a-z0-9]-?)*[a-z0-9]$").unwrap();
|
||||
}
|
||||
|
||||
impl Hostname {
|
||||
pub fn is_valid(name: &str) -> bool {
|
||||
name.len() < 64 && HOSTNAME_REGEX.is_match(name)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Hostname {
|
||||
type Err = &'static str;
|
||||
|
||||
fn from_str(name: &str) -> Result<Self, Self::Err> {
|
||||
if Self::is_valid(name) {
|
||||
Ok(Self(name.to_string()))
|
||||
} else {
|
||||
Err("invalid hostname")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Hostname {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Hostname {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IoErrorContext<T> {
|
||||
fn with_path<P: AsRef<Path>>(self, path: P) -> Result<T, WrappedIoError>;
|
||||
fn with_str<S: Into<String>>(self, context: S) -> Result<T, WrappedIoError>;
|
||||
@ -526,7 +621,7 @@ mod tests {
|
||||
let peer = Peer {
|
||||
id: 1,
|
||||
contents: PeerContents {
|
||||
name: "peer1".to_owned(),
|
||||
name: "peer1".parse().unwrap(),
|
||||
ip,
|
||||
cidr_id: 1,
|
||||
public_key: PUBKEY.to_owned(),
|
||||
@ -535,6 +630,7 @@ mod tests {
|
||||
is_admin: false,
|
||||
is_disabled: false,
|
||||
is_redeemed: true,
|
||||
invite_expires: None,
|
||||
},
|
||||
};
|
||||
let builder =
|
||||
@ -552,7 +648,7 @@ mod tests {
|
||||
let peer = Peer {
|
||||
id: 1,
|
||||
contents: PeerContents {
|
||||
name: "peer1".to_owned(),
|
||||
name: "peer1".parse().unwrap(),
|
||||
ip,
|
||||
cidr_id: 1,
|
||||
public_key: PUBKEY.to_owned(),
|
||||
@ -561,6 +657,7 @@ mod tests {
|
||||
is_admin: false,
|
||||
is_disabled: false,
|
||||
is_redeemed: true,
|
||||
invite_expires: None,
|
||||
},
|
||||
};
|
||||
let builder =
|
||||
|
Loading…
Reference in New Issue
Block a user