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 #24pull/72/head
parent
76500b3778
commit
2ce552cc36
|
@ -792,9 +792,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.5.3"
|
version = "1.5.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ce5f1ceb7f74abbce32601642fcf8e8508a8a8991e0621c7d750295b9095702b"
|
checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
|
@ -1220,9 +1220,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.2.1"
|
version = "2.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9ccd964113622c8e9322cfac19eb1004a07e636c545f325da085d5cdde6f1f8b"
|
checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
"idna",
|
"idna",
|
||||||
|
|
|
@ -135,7 +135,7 @@ mod tests {
|
||||||
static ref BASE_PEERS: Vec<Peer> = vec![Peer {
|
static ref BASE_PEERS: Vec<Peer> = vec![Peer {
|
||||||
id: 0,
|
id: 0,
|
||||||
contents: PeerContents {
|
contents: PeerContents {
|
||||||
name: "blah".to_string(),
|
name: "blah".parse().unwrap(),
|
||||||
ip: "10.0.0.1".parse().unwrap(),
|
ip: "10.0.0.1".parse().unwrap(),
|
||||||
cidr_id: 1,
|
cidr_id: 1,
|
||||||
public_key: "abc".to_string(),
|
public_key: "abc".to_string(),
|
||||||
|
@ -144,6 +144,7 @@ mod tests {
|
||||||
is_disabled: false,
|
is_disabled: false,
|
||||||
is_redeemed: true,
|
is_redeemed: true,
|
||||||
persistent_keepalive_interval: None,
|
persistent_keepalive_interval: None,
|
||||||
|
invite_expires: None,
|
||||||
}
|
}
|
||||||
}];
|
}];
|
||||||
static ref BASE_CIDRS: Vec<Cidr> = vec![Cidr {
|
static ref BASE_CIDRS: Vec<Cidr> = vec![Cidr {
|
||||||
|
|
|
@ -38,6 +38,7 @@ SERVER_CONTAINER=$(cmd docker run -itd --rm \
|
||||||
--cap-add NET_ADMIN \
|
--cap-add NET_ADMIN \
|
||||||
innernet-server)
|
innernet-server)
|
||||||
|
|
||||||
|
info "server started as $SERVER_CONTAINER"
|
||||||
info "Waiting for server to initialize."
|
info "Waiting for server to initialize."
|
||||||
cmd sleep 10
|
cmd sleep 10
|
||||||
|
|
||||||
|
@ -49,6 +50,7 @@ PEER1_CONTAINER=$(cmd docker create --rm -it \
|
||||||
--env INTERFACE=evilcorp \
|
--env INTERFACE=evilcorp \
|
||||||
--cap-add NET_ADMIN \
|
--cap-add NET_ADMIN \
|
||||||
innernet)
|
innernet)
|
||||||
|
info "peer1 started as $PEER1_CONTAINER"
|
||||||
cmd docker cp "$tmp_dir/peer1.toml" "$PEER1_CONTAINER:/app/invite.toml"
|
cmd docker cp "$tmp_dir/peer1.toml" "$PEER1_CONTAINER:/app/invite.toml"
|
||||||
cmd docker start "$PEER1_CONTAINER"
|
cmd docker start "$PEER1_CONTAINER"
|
||||||
sleep 5
|
sleep 5
|
||||||
|
@ -75,6 +77,7 @@ cmd docker exec "$PEER1_CONTAINER" innernet \
|
||||||
--admin false \
|
--admin false \
|
||||||
--auto-ip \
|
--auto-ip \
|
||||||
--save-config "/app/peer2.toml" \
|
--save-config "/app/peer2.toml" \
|
||||||
|
--invite-expires "30d" \
|
||||||
--yes
|
--yes
|
||||||
cmd docker cp "$PEER1_CONTAINER:/app/peer2.toml" "$tmp_dir"
|
cmd docker cp "$PEER1_CONTAINER:/app/peer2.toml" "$tmp_dir"
|
||||||
|
|
||||||
|
@ -85,6 +88,7 @@ PEER2_CONTAINER=$(docker create --rm -it \
|
||||||
--cap-add NET_ADMIN \
|
--cap-add NET_ADMIN \
|
||||||
--env INTERFACE=evilcorp \
|
--env INTERFACE=evilcorp \
|
||||||
innernet)
|
innernet)
|
||||||
|
info "peer2 started as $PEER2_CONTAINER"
|
||||||
cmd docker cp "$tmp_dir/peer2.toml" "$PEER2_CONTAINER:/app/invite.toml"
|
cmd docker cp "$tmp_dir/peer2.toml" "$PEER2_CONTAINER:/app/invite.toml"
|
||||||
cmd docker start "$PEER2_CONTAINER"
|
cmd docker start "$PEER2_CONTAINER"
|
||||||
sleep 10
|
sleep 10
|
||||||
|
|
|
@ -7,8 +7,19 @@ innernet-server new \
|
||||||
--external-endpoint "172.18.1.1:51820" \
|
--external-endpoint "172.18.1.1:51820" \
|
||||||
--listen-port 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
|
innernet-server serve evilcorp
|
||||||
|
|
|
@ -64,10 +64,10 @@ mod tests {
|
||||||
use crate::{test, DatabasePeer};
|
use crate::{test, DatabasePeer};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use bytes::Buf;
|
use bytes::Buf;
|
||||||
use shared::Cidr;
|
use shared::{Cidr, Error};
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_cidr_add() -> Result<()> {
|
async fn test_cidr_add() -> Result<(), Error> {
|
||||||
let server = test::Server::new()?;
|
let server = test::Server::new()?;
|
||||||
|
|
||||||
let old_cidrs = DatabaseCidr::list(&server.db().lock())?;
|
let old_cidrs = DatabaseCidr::list(&server.db().lock())?;
|
||||||
|
@ -95,7 +95,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_cidr_name_uniqueness() -> Result<()> {
|
async fn test_cidr_name_uniqueness() -> Result<(), Error> {
|
||||||
let server = test::Server::new()?;
|
let server = test::Server::new()?;
|
||||||
|
|
||||||
let contents = CidrContents {
|
let contents = CidrContents {
|
||||||
|
@ -125,7 +125,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_cidr_create_auth() -> Result<()> {
|
async fn test_cidr_create_auth() -> Result<(), Error> {
|
||||||
let server = test::Server::new()?;
|
let server = test::Server::new()?;
|
||||||
|
|
||||||
let contents = CidrContents {
|
let contents = CidrContents {
|
||||||
|
@ -143,7 +143,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_cidr_bad_parent() -> Result<()> {
|
async fn test_cidr_bad_parent() -> Result<(), Error> {
|
||||||
let server = test::Server::new()?;
|
let server = test::Server::new()?;
|
||||||
|
|
||||||
let contents = CidrContents {
|
let contents = CidrContents {
|
||||||
|
@ -171,7 +171,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_cidr_overlap() -> Result<()> {
|
async fn test_cidr_overlap() -> Result<(), Error> {
|
||||||
let server = test::Server::new()?;
|
let server = test::Server::new()?;
|
||||||
|
|
||||||
let contents = CidrContents {
|
let contents = CidrContents {
|
||||||
|
@ -188,7 +188,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[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 server = test::Server::new()?;
|
||||||
|
|
||||||
let experimental_cidr = DatabaseCidr::create(
|
let experimental_cidr = DatabaseCidr::create(
|
||||||
|
@ -241,7 +241,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[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 server = test::Server::new()?;
|
||||||
|
|
||||||
let experimental_cidr = DatabaseCidr::create(
|
let experimental_cidr = DatabaseCidr::create(
|
||||||
|
|
|
@ -94,12 +94,11 @@ mod handlers {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::test;
|
use crate::test;
|
||||||
use anyhow::Result;
|
|
||||||
use bytes::Buf;
|
use bytes::Buf;
|
||||||
use shared::Peer;
|
use shared::{Error, Peer};
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_add_peer() -> Result<()> {
|
async fn test_add_peer() -> Result<(), Error> {
|
||||||
let server = test::Server::new()?;
|
let server = test::Server::new()?;
|
||||||
|
|
||||||
let old_peers = DatabasePeer::list(&server.db().lock())?;
|
let old_peers = DatabasePeer::list(&server.db().lock())?;
|
||||||
|
@ -125,21 +124,13 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_add_peer_with_invalid_name() -> Result<()> {
|
async fn test_add_peer_with_invalid_name() -> Result<(), Error> {
|
||||||
let server = test::Server::new()?;
|
assert!(test::developer_peer_contents("devel oper", "10.80.64.4").is_err());
|
||||||
|
|
||||||
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);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[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 server = test::Server::new()?;
|
||||||
|
|
||||||
let old_peers = DatabasePeer::list(&server.db().lock())?;
|
let old_peers = DatabasePeer::list(&server.db().lock())?;
|
||||||
|
@ -161,7 +152,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[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 server = test::Server::new()?;
|
||||||
|
|
||||||
let old_peers = DatabasePeer::list(&server.db().lock())?;
|
let old_peers = DatabasePeer::list(&server.db().lock())?;
|
||||||
|
@ -183,7 +174,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[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 server = test::Server::new()?;
|
||||||
|
|
||||||
let old_peers = DatabasePeer::list(&server.db().lock())?;
|
let old_peers = DatabasePeer::list(&server.db().lock())?;
|
||||||
|
@ -217,7 +208,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[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 server = test::Server::new()?;
|
||||||
|
|
||||||
let peer = test::developer_peer_contents("developer3", "10.80.64.4")?;
|
let peer = test::developer_peer_contents("developer3", "10.80.64.4")?;
|
||||||
|
@ -233,12 +224,12 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[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 server = test::Server::new()?;
|
||||||
let old_peer = DatabasePeer::get(&server.db.lock(), test::DEVELOPER1_PEER_ID)?;
|
let old_peer = DatabasePeer::get(&server.db.lock(), test::DEVELOPER1_PEER_ID)?;
|
||||||
|
|
||||||
let change = PeerContents {
|
let change = PeerContents {
|
||||||
name: "new-peer-name".to_string(),
|
name: "new-peer-name".parse()?,
|
||||||
..old_peer.contents.clone()
|
..old_peer.contents.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -255,12 +246,12 @@ mod tests {
|
||||||
assert_eq!(res.status(), StatusCode::NO_CONTENT);
|
assert_eq!(res.status(), StatusCode::NO_CONTENT);
|
||||||
|
|
||||||
let new_peer = DatabasePeer::get(&server.db.lock(), test::DEVELOPER1_PEER_ID)?;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[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 server = test::Server::new()?;
|
||||||
|
|
||||||
let peer = test::developer_peer_contents("developer3", "10.80.64.4")?;
|
let peer = test::developer_peer_contents("developer3", "10.80.64.4")?;
|
||||||
|
@ -281,7 +272,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[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 server = test::Server::new()?;
|
||||||
let res = server
|
let res = server
|
||||||
.request(test::ADMIN_PEER_IP, "GET", "/v1/admin/peers")
|
.request(test::ADMIN_PEER_IP, "GET", "/v1/admin/peers")
|
||||||
|
@ -291,7 +282,7 @@ mod tests {
|
||||||
|
|
||||||
let whole_body = hyper::body::aggregate(res).await?;
|
let whole_body = hyper::body::aggregate(res).await?;
|
||||||
let peers: Vec<Peer> = serde_json::from_reader(whole_body.reader())?;
|
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.
|
// An admin peer should see all the peers.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
&[
|
&[
|
||||||
|
@ -309,7 +300,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[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 server = test::Server::new()?;
|
||||||
let res = server
|
let res = server
|
||||||
.request(test::DEVELOPER1_PEER_IP, "GET", "/v1/admin/peers")
|
.request(test::DEVELOPER1_PEER_IP, "GET", "/v1/admin/peers")
|
||||||
|
@ -321,7 +312,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_delete() -> Result<()> {
|
async fn test_delete() -> Result<(), Error> {
|
||||||
let server = test::Server::new()?;
|
let server = test::Server::new()?;
|
||||||
let old_peers = DatabasePeer::list(&server.db().lock())?;
|
let old_peers = DatabasePeer::list(&server.db().lock())?;
|
||||||
|
|
||||||
|
@ -344,7 +335,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[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 server = test::Server::new()?;
|
||||||
|
|
||||||
let old_peers = DatabasePeer::list(&server.db().lock())?;
|
let old_peers = DatabasePeer::list(&server.db().lock())?;
|
||||||
|
@ -367,7 +358,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_delete_unknown_id() -> Result<()> {
|
async fn test_delete_unknown_id() -> Result<(), Error> {
|
||||||
let server = test::Server::new()?;
|
let server = test::Server::new()?;
|
||||||
|
|
||||||
let res = server
|
let res = server
|
||||||
|
|
|
@ -80,43 +80,37 @@ mod handlers {
|
||||||
let old_public_key = wgctrl::Key::from_base64(&selected_peer.public_key)
|
let old_public_key = wgctrl::Key::from_base64(&selected_peer.public_key)
|
||||||
.map_err(|_| ServerError::WireGuard)?;
|
.map_err(|_| ServerError::WireGuard)?;
|
||||||
|
|
||||||
if selected_peer.is_redeemed {
|
selected_peer.redeem(&conn, &form.public_key)?;
|
||||||
Ok(Response::builder()
|
|
||||||
.status(StatusCode::GONE)
|
|
||||||
.body(Body::empty())?)
|
|
||||||
} else {
|
|
||||||
selected_peer.redeem(&conn, &form.public_key)?;
|
|
||||||
|
|
||||||
if cfg!(not(test)) {
|
if cfg!(not(test)) {
|
||||||
let interface = session.context.interface;
|
let interface = session.context.interface;
|
||||||
|
|
||||||
// If we were to modify the WireGuard interface immediately, the HTTP response wouldn't
|
// 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
|
// get through. Instead, we need to wait a reasonable amount for the HTTP response to
|
||||||
// flush, then update the interface.
|
// flush, then update the interface.
|
||||||
//
|
//
|
||||||
// The client is also expected to wait the same amount of time after receiving a success
|
// The client is also expected to wait the same amount of time after receiving a success
|
||||||
// response from /redeem.
|
// response from /redeem.
|
||||||
//
|
//
|
||||||
// This might be avoidable if we were able to run code after we were certain the response
|
// 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
|
// had flushed over the TCP socket, but that isn't easily accessible from this high-level
|
||||||
// web framework.
|
// web framework.
|
||||||
tokio::task::spawn(async move {
|
tokio::task::spawn(async move {
|
||||||
tokio::time::sleep(*REDEEM_TRANSITION_WAIT).await;
|
tokio::time::sleep(*REDEEM_TRANSITION_WAIT).await;
|
||||||
log::info!(
|
log::info!(
|
||||||
"WireGuard: adding new peer {}, removing old pubkey {}",
|
"WireGuard: adding new peer {}, removing old pubkey {}",
|
||||||
&*selected_peer,
|
&*selected_peer,
|
||||||
old_public_key.to_base64()
|
old_public_key.to_base64()
|
||||||
);
|
);
|
||||||
DeviceConfigBuilder::new()
|
DeviceConfigBuilder::new()
|
||||||
.remove_peer_by_key(&old_public_key)
|
.remove_peer_by_key(&old_public_key)
|
||||||
.add_peer((&*selected_peer).into())
|
.add_peer((&*selected_peer).into())
|
||||||
.apply(&interface)
|
.apply(&interface)
|
||||||
.map_err(|e| log::error!("{:?}", e))
|
.map_err(|e| log::error!("{:?}", e))
|
||||||
.ok();
|
.ok();
|
||||||
});
|
});
|
||||||
}
|
|
||||||
status_response(StatusCode::NO_CONTENT)
|
|
||||||
}
|
}
|
||||||
|
status_response(StatusCode::NO_CONTENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Redeems an invitation. An invitation includes a WireGuard keypair generated by either the server
|
/// Redeems an invitation. An invitation includes a WireGuard keypair generated by either the server
|
||||||
|
@ -147,14 +141,15 @@ mod handlers {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use std::time::{Duration, SystemTime};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{db::DatabaseAssociation, test};
|
use crate::{db::DatabaseAssociation, test};
|
||||||
use anyhow::Result;
|
|
||||||
use bytes::Buf;
|
use bytes::Buf;
|
||||||
use shared::{AssociationContents, CidrContents, EndpointContents};
|
use shared::{AssociationContents, CidrContents, EndpointContents, Error};
|
||||||
|
|
||||||
#[tokio::test]
|
#[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 server = test::Server::new()?;
|
||||||
let res = server
|
let res = server
|
||||||
.request(test::DEVELOPER1_PEER_IP, "GET", "/v1/user/state")
|
.request(test::DEVELOPER1_PEER_IP, "GET", "/v1/user/state")
|
||||||
|
@ -164,7 +159,7 @@ mod tests {
|
||||||
|
|
||||||
let whole_body = hyper::body::aggregate(res).await?;
|
let whole_body = hyper::body::aggregate(res).await?;
|
||||||
let State { peers, .. } = serde_json::from_reader(whole_body.reader())?;
|
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();
|
peer_names.sort();
|
||||||
// Developers should see only peers in infra CIDR and developer CIDR.
|
// Developers should see only peers in infra CIDR and developer CIDR.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -176,7 +171,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_override_endpoint() -> Result<()> {
|
async fn test_override_endpoint() -> Result<(), Error> {
|
||||||
let server = test::Server::new()?;
|
let server = test::Server::new()?;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
server
|
server
|
||||||
|
@ -222,7 +217,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[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()?;
|
let server = test::Server::new()?;
|
||||||
|
|
||||||
// Request comes from an unknown IP.
|
// Request comes from an unknown IP.
|
||||||
|
@ -234,7 +229,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[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 server = test::Server::new()?;
|
||||||
{
|
{
|
||||||
let db = server.db.lock();
|
let db = server.db.lock();
|
||||||
|
@ -286,7 +281,7 @@ mod tests {
|
||||||
assert_eq!(res.status(), StatusCode::OK);
|
assert_eq!(res.status(), StatusCode::OK);
|
||||||
let whole_body = hyper::body::aggregate(res).await?;
|
let whole_body = hyper::body::aggregate(res).await?;
|
||||||
let State { peers, .. } = serde_json::from_reader(whole_body.reader())?;
|
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();
|
peer_names.sort();
|
||||||
// Developers should see only peers in infra CIDR and developer CIDR.
|
// Developers should see only peers in infra CIDR and developer CIDR.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -304,7 +299,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_redeem() -> Result<()> {
|
async fn test_redeem() -> Result<(), Error> {
|
||||||
let server = test::Server::new()?;
|
let server = test::Server::new()?;
|
||||||
|
|
||||||
let experimental_cidr = DatabaseCidr::create(
|
let experimental_cidr = DatabaseCidr::create(
|
||||||
|
@ -323,6 +318,7 @@ mod tests {
|
||||||
false,
|
false,
|
||||||
)?;
|
)?;
|
||||||
peer_contents.is_redeemed = 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)?;
|
let _experiment_peer = DatabasePeer::create(&server.db().lock(), peer_contents)?;
|
||||||
|
|
||||||
// Step 1: Ensure that before redeeming, other endpoints aren't yet accessible.
|
// Step 1: Ensure that before redeeming, other endpoints aren't yet accessible.
|
||||||
|
@ -363,4 +359,49 @@ mod tests {
|
||||||
assert_eq!(res.status(), StatusCode::OK);
|
assert_eq!(res.status(), StatusCode::OK);
|
||||||
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 association::DatabaseAssociation;
|
||||||
pub use cidr::DatabaseCidr;
|
pub use cidr::DatabaseCidr;
|
||||||
pub use peer::DatabasePeer;
|
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::{
|
use std::{
|
||||||
net::IpAddr,
|
net::IpAddr,
|
||||||
ops::{Deref, DerefMut},
|
ops::{Deref, DerefMut},
|
||||||
|
time::{Duration, SystemTime},
|
||||||
};
|
};
|
||||||
use structopt::lazy_static;
|
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_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_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? */
|
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)
|
FOREIGN KEY (cidr_id)
|
||||||
REFERENCES cidrs (id)
|
REFERENCES cidrs (id)
|
||||||
ON UPDATE RESTRICT
|
ON UPDATE RESTRICT
|
||||||
|
@ -68,6 +70,7 @@ impl DatabasePeer {
|
||||||
is_admin,
|
is_admin,
|
||||||
is_disabled,
|
is_disabled,
|
||||||
is_redeemed,
|
is_redeemed,
|
||||||
|
invite_expires,
|
||||||
..
|
..
|
||||||
} = &contents;
|
} = &contents;
|
||||||
log::info!("creating peer {:?}", contents);
|
log::info!("creating peer {:?}", contents);
|
||||||
|
@ -88,10 +91,15 @@ impl DatabasePeer {
|
||||||
return Err(ServerError::InvalidQuery);
|
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(
|
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![
|
params![
|
||||||
name,
|
&**name,
|
||||||
ip.to_string(),
|
ip.to_string(),
|
||||||
cidr_id,
|
cidr_id,
|
||||||
&public_key,
|
&public_key,
|
||||||
|
@ -99,6 +107,7 @@ impl DatabasePeer {
|
||||||
is_admin,
|
is_admin,
|
||||||
is_disabled,
|
is_disabled,
|
||||||
is_redeemed,
|
is_redeemed,
|
||||||
|
invite_expires,
|
||||||
],
|
],
|
||||||
)?;
|
)?;
|
||||||
let id = conn.last_insert_rowid();
|
let id = conn.last_insert_rowid();
|
||||||
|
@ -137,7 +146,7 @@ impl DatabasePeer {
|
||||||
is_disabled = ?4
|
is_disabled = ?4
|
||||||
WHERE id = ?5",
|
WHERE id = ?5",
|
||||||
params![
|
params![
|
||||||
new_contents.name,
|
&*new_contents.name,
|
||||||
new_contents
|
new_contents
|
||||||
.endpoint
|
.endpoint
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
@ -163,6 +172,14 @@ impl DatabasePeer {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn redeem(&mut self, conn: &Connection, pubkey: &str) -> Result<(), ServerError> {
|
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(
|
match conn.execute(
|
||||||
"UPDATE peers SET is_redeemed = 1, public_key = ?1 WHERE id = ?2 AND is_redeemed = 0",
|
"UPDATE peers SET is_redeemed = 1, public_key = ?1 WHERE id = ?2 AND is_redeemed = 0",
|
||||||
params![pubkey, self.id],
|
params![pubkey, self.id],
|
||||||
|
@ -178,7 +195,10 @@ impl DatabasePeer {
|
||||||
|
|
||||||
fn from_row(row: &rusqlite::Row) -> Result<Self, rusqlite::Error> {
|
fn from_row(row: &rusqlite::Row) -> Result<Self, rusqlite::Error> {
|
||||||
let id = row.get(0)?;
|
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
|
let ip: IpAddr = row
|
||||||
.get::<_, String>(2)?
|
.get::<_, String>(2)?
|
||||||
.parse()
|
.parse()
|
||||||
|
@ -191,6 +211,10 @@ impl DatabasePeer {
|
||||||
let is_admin = row.get(6)?;
|
let is_admin = row.get(6)?;
|
||||||
let is_disabled = row.get(7)?;
|
let is_disabled = row.get(7)?;
|
||||||
let is_redeemed = row.get(8)?;
|
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);
|
let persistent_keepalive_interval = Some(PERSISTENT_KEEPALIVE_INTERVAL_SECS);
|
||||||
|
|
||||||
Ok(Peer {
|
Ok(Peer {
|
||||||
|
@ -205,6 +229,7 @@ impl DatabasePeer {
|
||||||
is_admin,
|
is_admin,
|
||||||
is_disabled,
|
is_disabled,
|
||||||
is_redeemed,
|
is_redeemed,
|
||||||
|
invite_expires,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
.into())
|
.into())
|
||||||
|
@ -213,7 +238,7 @@ impl DatabasePeer {
|
||||||
pub fn get(conn: &Connection, id: i64) -> Result<Self, ServerError> {
|
pub fn get(conn: &Connection, id: i64) -> Result<Self, ServerError> {
|
||||||
let result = conn.query_row(
|
let result = conn.query_row(
|
||||||
"SELECT
|
"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
|
FROM peers
|
||||||
WHERE id = ?1",
|
WHERE id = ?1",
|
||||||
params![id],
|
params![id],
|
||||||
|
@ -226,7 +251,7 @@ impl DatabasePeer {
|
||||||
pub fn get_from_ip(conn: &Connection, ip: IpAddr) -> Result<Self, rusqlite::Error> {
|
pub fn get_from_ip(conn: &Connection, ip: IpAddr) -> Result<Self, rusqlite::Error> {
|
||||||
let result = conn.query_row(
|
let result = conn.query_row(
|
||||||
"SELECT
|
"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
|
FROM peers
|
||||||
WHERE ip = ?1",
|
WHERE ip = ?1",
|
||||||
params![ip.to_string()],
|
params![ip.to_string()],
|
||||||
|
@ -264,7 +289,7 @@ impl DatabasePeer {
|
||||||
UNION
|
UNION
|
||||||
SELECT id FROM cidrs, associated_subcidrs WHERE cidrs.parent=associated_subcidrs.cidr_id
|
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
|
FROM peers
|
||||||
JOIN associated_subcidrs ON peers.cidr_id=associated_subcidrs.cidr_id
|
JOIN associated_subcidrs ON peers.cidr_id=associated_subcidrs.cidr_id
|
||||||
WHERE peers.is_disabled = 0 AND peers.is_redeemed = 1;",
|
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> {
|
pub fn list(conn: &Connection) -> Result<Vec<Self>, ServerError> {
|
||||||
let mut stmt = conn.prepare_cached(
|
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)?;
|
let peer_iter = stmt.query_map(params![], Self::from_row)?;
|
||||||
|
|
||||||
Ok(peer_iter.collect::<Result<_, _>>()?)
|
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")]
|
#[error("invalid query")]
|
||||||
InvalidQuery,
|
InvalidQuery,
|
||||||
|
|
||||||
|
#[error("endpoint gone")]
|
||||||
|
Gone,
|
||||||
|
|
||||||
#[error("internal database error")]
|
#[error("internal database error")]
|
||||||
Database(#[from] rusqlite::Error),
|
Database(#[from] rusqlite::Error),
|
||||||
|
|
||||||
|
@ -39,6 +42,7 @@ impl<'a> From<&'a ServerError> for StatusCode {
|
||||||
match error {
|
match error {
|
||||||
Unauthorized => StatusCode::UNAUTHORIZED,
|
Unauthorized => StatusCode::UNAUTHORIZED,
|
||||||
NotFound => StatusCode::NOT_FOUND,
|
NotFound => StatusCode::NOT_FOUND,
|
||||||
|
Gone => StatusCode::GONE,
|
||||||
InvalidQuery | Json(_) => StatusCode::BAD_REQUEST,
|
InvalidQuery | Json(_) => StatusCode::BAD_REQUEST,
|
||||||
// Special-case the constraint violation situation.
|
// Special-case the constraint violation situation.
|
||||||
Database(rusqlite::Error::SqliteFailure(libsqlite3_sys::Error { code, .. }, ..))
|
Database(rusqlite::Error::SqliteFailure(libsqlite3_sys::Error { code, .. }, ..))
|
||||||
|
|
|
@ -4,8 +4,7 @@ use dialoguer::{theme::ColorfulTheme, Input};
|
||||||
use indoc::printdoc;
|
use indoc::printdoc;
|
||||||
use rusqlite::{params, Connection};
|
use rusqlite::{params, Connection};
|
||||||
use shared::{
|
use shared::{
|
||||||
prompts::{self, hostname_validator},
|
prompts, CidrContents, Endpoint, Hostname, PeerContents, PERSISTENT_KEEPALIVE_INTERVAL_SECS,
|
||||||
CidrContents, Endpoint, PeerContents, PERSISTENT_KEEPALIVE_INTERVAL_SECS,
|
|
||||||
};
|
};
|
||||||
use wgctrl::KeyPair;
|
use wgctrl::KeyPair;
|
||||||
|
|
||||||
|
@ -17,6 +16,8 @@ fn create_database<P: AsRef<Path>>(
|
||||||
conn.execute(db::peer::CREATE_TABLE_SQL, params![])?;
|
conn.execute(db::peer::CREATE_TABLE_SQL, params![])?;
|
||||||
conn.execute(db::association::CREATE_TABLE_SQL, params![])?;
|
conn.execute(db::association::CREATE_TABLE_SQL, params![])?;
|
||||||
conn.execute(db::cidr::CREATE_TABLE_SQL, params![])?;
|
conn.execute(db::cidr::CREATE_TABLE_SQL, params![])?;
|
||||||
|
conn.pragma_update(None, "user_version", &db::CURRENT_VERSION)?;
|
||||||
|
|
||||||
Ok(conn)
|
Ok(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,7 +25,7 @@ fn create_database<P: AsRef<Path>>(
|
||||||
pub struct InitializeOpts {
|
pub struct InitializeOpts {
|
||||||
/// The network name (ex: evilcorp)
|
/// The network name (ex: evilcorp)
|
||||||
#[structopt(long)]
|
#[structopt(long)]
|
||||||
pub network_name: Option<String>,
|
pub network_name: Option<Hostname>,
|
||||||
|
|
||||||
/// The network CIDR (ex: 10.42.0.0/16)
|
/// The network CIDR (ex: 10.42.0.0/16)
|
||||||
#[structopt(long)]
|
#[structopt(long)]
|
||||||
|
@ -78,7 +79,7 @@ fn populate_database(conn: &Connection, db_init_data: DbInitData) -> Result<(),
|
||||||
let _me = DatabasePeer::create(
|
let _me = DatabasePeer::create(
|
||||||
&conn,
|
&conn,
|
||||||
PeerContents {
|
PeerContents {
|
||||||
name: SERVER_NAME.into(),
|
name: SERVER_NAME.parse()?,
|
||||||
ip: db_init_data.our_ip,
|
ip: db_init_data.our_ip,
|
||||||
cidr_id: server_cidr.id,
|
cidr_id: server_cidr.id,
|
||||||
public_key: db_init_data.public_key_base64,
|
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_disabled: false,
|
||||||
is_redeemed: true,
|
is_redeemed: true,
|
||||||
persistent_keepalive_interval: Some(PERSISTENT_KEEPALIVE_INTERVAL_SECS),
|
persistent_keepalive_interval: Some(PERSISTENT_KEEPALIVE_INTERVAL_SECS),
|
||||||
|
invite_expires: None,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.map_err(|_| "failed to create innernet peer.".to_string())?;
|
.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
|
name
|
||||||
} else {
|
} else {
|
||||||
println!("Here you'll specify the network CIDR, which will encompass the entire network.");
|
println!("Here you'll specify the network CIDR, which will encompass the entire network.");
|
||||||
Input::with_theme(&theme)
|
Input::with_theme(&theme)
|
||||||
.with_prompt("Network name")
|
.with_prompt("Network name")
|
||||||
.validate_with(hostname_validator)
|
|
||||||
.interact()?
|
.interact()?
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -3,12 +3,12 @@ use dialoguer::Confirm;
|
||||||
use hyper::{http, server::conn::AddrStream, Body, Request, Response};
|
use hyper::{http, server::conn::AddrStream, Body, Request, Response};
|
||||||
use indoc::printdoc;
|
use indoc::printdoc;
|
||||||
use ipnetwork::IpNetwork;
|
use ipnetwork::IpNetwork;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::{Mutex, RwLock};
|
||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use shared::{AddCidrOpts, AddPeerOpts, IoErrorContext, INNERNET_PUBKEY_HEADER};
|
use shared::{AddCidrOpts, AddPeerOpts, IoErrorContext, INNERNET_PUBKEY_HEADER};
|
||||||
use std::{
|
use std::{
|
||||||
collections::VecDeque,
|
collections::{HashMap, VecDeque},
|
||||||
convert::TryInto,
|
convert::TryInto,
|
||||||
env,
|
env,
|
||||||
fs::File,
|
fs::File,
|
||||||
|
@ -17,6 +17,7 @@ use std::{
|
||||||
ops::Deref,
|
ops::Deref,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
|
time::Duration,
|
||||||
};
|
};
|
||||||
use structopt::StructOpt;
|
use structopt::StructOpt;
|
||||||
use subtle::ConstantTimeEq;
|
use subtle::ConstantTimeEq;
|
||||||
|
@ -24,7 +25,6 @@ use wgctrl::{DeviceConfigBuilder, DeviceInfo, InterfaceName, Key, PeerConfigBuil
|
||||||
|
|
||||||
pub mod api;
|
pub mod api;
|
||||||
pub mod db;
|
pub mod db;
|
||||||
pub mod endpoints;
|
|
||||||
pub mod error;
|
pub mod error;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test;
|
mod test;
|
||||||
|
@ -33,7 +33,6 @@ pub mod util;
|
||||||
mod initialize;
|
mod initialize;
|
||||||
|
|
||||||
use db::{DatabaseCidr, DatabasePeer};
|
use db::{DatabaseCidr, DatabasePeer};
|
||||||
pub use endpoints::Endpoints;
|
|
||||||
pub use error::ServerError;
|
pub use error::ServerError;
|
||||||
use initialize::InitializeOpts;
|
use initialize::InitializeOpts;
|
||||||
use shared::{prompts, wg, CidrTree, Error, Interface, SERVER_CONFIG_DIR, SERVER_DATABASE_DIR};
|
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 Db = Arc<Mutex<Connection>>;
|
||||||
|
pub type Endpoints = Arc<RwLock<HashMap<String, SocketAddr>>>;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Context {
|
pub struct Context {
|
||||||
pub db: Db,
|
pub db: Db,
|
||||||
pub endpoints: Arc<Endpoints>,
|
pub endpoints: Arc<RwLock<HashMap<String, SocketAddr>>>,
|
||||||
pub interface: InterfaceName,
|
pub interface: InterfaceName,
|
||||||
pub public_key: Key,
|
pub public_key: Key,
|
||||||
}
|
}
|
||||||
|
@ -206,7 +206,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Command::Uninstall { interface } => uninstall(&interface, &conf)?,
|
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::AddPeer { interface, args } => add_peer(&interface, &conf, args)?,
|
||||||
Command::AddCidr { interface, args } => add_cidr(&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)?;
|
let conn = Connection::open(&database_path)?;
|
||||||
// Foreign key constraints aren't on in SQLite by default. Enable.
|
// Foreign key constraints aren't on in SQLite by default. Enable.
|
||||||
conn.pragma_update(None, "foreign_keys", &1)?;
|
conn.pragma_update(None, "foreign_keys", &1)?;
|
||||||
|
db::auto_migrate(&conn)?;
|
||||||
Ok(conn)
|
Ok(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -334,9 +334,45 @@ fn uninstall(interface: &InterfaceName, conf: &ServerConfig) -> Result<(), Error
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn serve(interface: &InterfaceName, conf: &ServerConfig) -> Result<(), Error> {
|
fn spawn_endpoint_refresher(interface: InterfaceName) -> Endpoints {
|
||||||
let config = ConfigFile::from_file(conf.config_path(interface))?;
|
let endpoints = Arc::new(RwLock::new(HashMap::new()));
|
||||||
let conn = open_database_connection(interface, conf)?;
|
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 peers = DatabasePeer::list(&conn)?;
|
||||||
let peer_configs = peers
|
let peer_configs = peers
|
||||||
|
@ -346,7 +382,7 @@ async fn serve(interface: &InterfaceName, conf: &ServerConfig) -> Result<(), Err
|
||||||
|
|
||||||
log::info!("bringing up interface.");
|
log::info!("bringing up interface.");
|
||||||
wg::up(
|
wg::up(
|
||||||
interface,
|
&interface,
|
||||||
&config.private_key,
|
&config.private_key,
|
||||||
IpNetwork::new(config.address, config.network_cidr_prefix)?,
|
IpNetwork::new(config.address, config.network_cidr_prefix)?,
|
||||||
Some(config.listen_port),
|
Some(config.listen_port),
|
||||||
|
@ -357,22 +393,23 @@ async fn serve(interface: &InterfaceName, conf: &ServerConfig) -> Result<(), Err
|
||||||
.add_peers(&peer_configs)
|
.add_peers(&peer_configs)
|
||||||
.apply(&interface)?;
|
.apply(&interface)?;
|
||||||
|
|
||||||
let endpoints = Arc::new(Endpoints::new(&interface)?);
|
|
||||||
|
|
||||||
log::info!("{} peers added to wireguard interface.", peers.len());
|
log::info!("{} peers added to wireguard interface.", peers.len());
|
||||||
|
|
||||||
let public_key = wgctrl::Key::from_base64(&config.private_key)?.generate_public();
|
let public_key = wgctrl::Key::from_base64(&config.private_key)?.generate_public();
|
||||||
let db = Arc::new(Mutex::new(conn));
|
let db = Arc::new(Mutex::new(conn));
|
||||||
|
let endpoints = spawn_endpoint_refresher(interface);
|
||||||
|
spawn_expired_invite_sweeper(db.clone());
|
||||||
|
|
||||||
let context = Context {
|
let context = Context {
|
||||||
db,
|
db,
|
||||||
interface: *interface,
|
interface,
|
||||||
endpoints,
|
endpoints,
|
||||||
public_key,
|
public_key,
|
||||||
};
|
};
|
||||||
|
|
||||||
log::info!("innernet-server {} starting.", VERSION);
|
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 make_svc = hyper::service::make_service_fn(move |socket: &AddrStream| {
|
||||||
let remote_addr = socket.remote_addr();
|
let remote_addr = socket.remote_addr();
|
||||||
|
@ -492,7 +529,7 @@ mod tests {
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_init_wizard() -> Result<()> {
|
fn test_init_wizard() -> Result<(), Error> {
|
||||||
// This runs init_wizard().
|
// This runs init_wizard().
|
||||||
let server = test::Server::new()?;
|
let server = test::Server::new()?;
|
||||||
|
|
||||||
|
@ -502,7 +539,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[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 server = test::Server::new()?;
|
||||||
|
|
||||||
let req = Request::builder()
|
let req = Request::builder()
|
||||||
|
@ -525,7 +562,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_incorrect_public_key() -> Result<()> {
|
async fn test_incorrect_public_key() -> Result<(), Error> {
|
||||||
let server = test::Server::new()?;
|
let server = test::Server::new()?;
|
||||||
|
|
||||||
let key = Key::generate_private().generate_public();
|
let key = Key::generate_private().generate_public();
|
||||||
|
@ -547,7 +584,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_unparseable_public_key() -> Result<()> {
|
async fn test_unparseable_public_key() -> Result<(), Error> {
|
||||||
let server = test::Server::new()?;
|
let server = test::Server::new()?;
|
||||||
|
|
||||||
let req = Request::builder()
|
let req = Request::builder()
|
||||||
|
|
|
@ -1,17 +1,16 @@
|
||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
use crate::{
|
use crate::{
|
||||||
db::{DatabaseCidr, DatabasePeer},
|
db::{DatabaseCidr, DatabasePeer},
|
||||||
endpoints::Endpoints,
|
|
||||||
initialize::{init_wizard, InitializeOpts},
|
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 hyper::{header::HeaderValue, http, Body, Request, Response};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::{Mutex, RwLock};
|
||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use shared::{Cidr, CidrContents, PeerContents};
|
use shared::{Cidr, CidrContents, Error, PeerContents};
|
||||||
use std::{net::SocketAddr, path::PathBuf, sync::Arc};
|
use std::{collections::HashMap, net::SocketAddr, path::PathBuf, sync::Arc};
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
use wgctrl::{InterfaceName, Key, KeyPair};
|
use wgctrl::{InterfaceName, Key, KeyPair};
|
||||||
|
|
||||||
|
@ -44,8 +43,8 @@ pub const USER1_PEER_ID: i64 = 5;
|
||||||
pub const USER2_PEER_ID: i64 = 6;
|
pub const USER2_PEER_ID: i64 = 6;
|
||||||
|
|
||||||
pub struct Server {
|
pub struct Server {
|
||||||
pub db: Arc<Mutex<Connection>>,
|
pub db: Db,
|
||||||
endpoints: Arc<Endpoints>,
|
endpoints: Endpoints,
|
||||||
interface: InterfaceName,
|
interface: InterfaceName,
|
||||||
conf: ServerConfig,
|
conf: ServerConfig,
|
||||||
public_key: Key,
|
public_key: Key,
|
||||||
|
@ -54,7 +53,7 @@ pub struct Server {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Server {
|
impl Server {
|
||||||
pub fn new() -> Result<Self> {
|
pub fn new() -> Result<Self, Error> {
|
||||||
let test_dir = tempfile::tempdir()?;
|
let test_dir = tempfile::tempdir()?;
|
||||||
let test_dir_path = test_dir.path();
|
let test_dir_path = test_dir.path();
|
||||||
|
|
||||||
|
@ -68,7 +67,7 @@ impl Server {
|
||||||
};
|
};
|
||||||
|
|
||||||
let opts = InitializeOpts {
|
let opts = InitializeOpts {
|
||||||
network_name: Some(interface.clone()),
|
network_name: Some(interface.parse()?),
|
||||||
network_cidr: Some(ROOT_CIDR.parse()?),
|
network_cidr: Some(ROOT_CIDR.parse()?),
|
||||||
external_endpoint: Some("155.155.155.155:54321".parse().unwrap()),
|
external_endpoint: Some("155.155.155.155:54321".parse().unwrap()),
|
||||||
listen_port: Some(54321),
|
listen_port: Some(54321),
|
||||||
|
@ -116,7 +115,7 @@ impl Server {
|
||||||
);
|
);
|
||||||
|
|
||||||
let db = Arc::new(Mutex::new(db));
|
let db = Arc::new(Mutex::new(db));
|
||||||
let endpoints = Arc::new(Endpoints::new(&interface)?);
|
let endpoints = Arc::new(RwLock::new(HashMap::new()));
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
conf,
|
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(
|
let cidr = DatabaseCidr::create(
|
||||||
db,
|
db,
|
||||||
CidrContents {
|
CidrContents {
|
||||||
|
@ -214,11 +213,11 @@ pub fn peer_contents(
|
||||||
ip_str: &str,
|
ip_str: &str,
|
||||||
cidr_id: i64,
|
cidr_id: i64,
|
||||||
is_admin: bool,
|
is_admin: bool,
|
||||||
) -> Result<PeerContents> {
|
) -> Result<PeerContents, Error> {
|
||||||
let public_key = KeyPair::generate().public;
|
let public_key = KeyPair::generate().public;
|
||||||
|
|
||||||
Ok(PeerContents {
|
Ok(PeerContents {
|
||||||
name: name.to_string(),
|
name: name.parse()?,
|
||||||
ip: ip_str.parse()?,
|
ip: ip_str.parse()?,
|
||||||
cidr_id,
|
cidr_id,
|
||||||
public_key: public_key.to_base64(),
|
public_key: public_key.to_base64(),
|
||||||
|
@ -227,21 +226,22 @@ pub fn peer_contents(
|
||||||
persistent_keepalive_interval: None,
|
persistent_keepalive_interval: None,
|
||||||
is_disabled: false,
|
is_disabled: false,
|
||||||
is_redeemed: true,
|
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)
|
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)
|
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)
|
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)
|
peer_contents(name, ip_str, USER_CIDR_ID, false)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,29 +7,14 @@ use colored::*;
|
||||||
use dialoguer::{theme::ColorfulTheme, Confirm, Input, Select};
|
use dialoguer::{theme::ColorfulTheme, Confirm, Input, Select};
|
||||||
use ipnetwork::IpNetwork;
|
use ipnetwork::IpNetwork;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use regex::Regex;
|
use std::{
|
||||||
use std::net::{IpAddr, SocketAddr};
|
net::{IpAddr, SocketAddr},
|
||||||
|
time::SystemTime,
|
||||||
|
};
|
||||||
use wgctrl::{InterfaceName, KeyPair};
|
use wgctrl::{InterfaceName, KeyPair};
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
pub static ref THEME: ColorfulTheme = ColorfulTheme::default();
|
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.
|
/// 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 {
|
let name = if let Some(ref name) = args.name {
|
||||||
name.clone()
|
name.clone()
|
||||||
} else {
|
} else {
|
||||||
Input::with_theme(&*THEME)
|
Input::with_theme(&*THEME).with_prompt("Name").interact()?
|
||||||
.with_prompt("Name")
|
|
||||||
.validate_with(hostname_validator)
|
|
||||||
.interact()?
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let is_admin = if let Some(is_admin) = args.admin {
|
let is_admin = if let Some(is_admin) = args.admin {
|
||||||
|
@ -215,6 +197,15 @@ pub fn add_peer(
|
||||||
.interact()?
|
.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 default_keypair = KeyPair::generate();
|
||||||
let peer_request = PeerContents {
|
let peer_request = PeerContents {
|
||||||
name,
|
name,
|
||||||
|
@ -226,6 +217,7 @@ pub fn add_peer(
|
||||||
is_disabled: false,
|
is_disabled: false,
|
||||||
is_redeemed: false,
|
is_redeemed: false,
|
||||||
persistent_keepalive_interval: Some(PERSISTENT_KEEPALIVE_INTERVAL_SECS),
|
persistent_keepalive_interval: Some(PERSISTENT_KEEPALIVE_INTERVAL_SECS),
|
||||||
|
invite_expires: Some(SystemTime::now() + invite_expires.into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(
|
Ok(
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::prompts::hostname_validator;
|
|
||||||
use ipnetwork::IpNetwork;
|
use ipnetwork::IpNetwork;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use regex::Regex;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
fmt::{self, Display, Formatter},
|
fmt::{self, Display, Formatter},
|
||||||
|
@ -7,6 +8,7 @@ use std::{
|
||||||
ops::Deref,
|
ops::Deref,
|
||||||
path::Path,
|
path::Path,
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
|
time::{Duration, SystemTime},
|
||||||
vec,
|
vec,
|
||||||
};
|
};
|
||||||
use structopt::StructOpt;
|
use structopt::StructOpt;
|
||||||
|
@ -22,8 +24,9 @@ impl FromStr for Interface {
|
||||||
type Err = String;
|
type Err = String;
|
||||||
|
|
||||||
fn from_str(name: &str) -> Result<Self, Self::Err> {
|
fn from_str(name: &str) -> Result<Self, Self::Err> {
|
||||||
let name = name.to_string();
|
if !Hostname::is_valid(name) {
|
||||||
hostname_validator(&name)?;
|
return Err("interface name is not a valid hostname".into());
|
||||||
|
}
|
||||||
let name = name
|
let name = name
|
||||||
.parse()
|
.parse()
|
||||||
.map_err(|e: InvalidInterfaceName| e.to_string())?;
|
.map_err(|e: InvalidInterfaceName| e.to_string())?;
|
||||||
|
@ -284,7 +287,7 @@ pub struct InstallOpts {
|
||||||
pub struct AddPeerOpts {
|
pub struct AddPeerOpts {
|
||||||
/// Name of new peer
|
/// Name of new peer
|
||||||
#[structopt(long)]
|
#[structopt(long)]
|
||||||
pub name: Option<String>,
|
pub name: Option<Hostname>,
|
||||||
|
|
||||||
/// Specify desired IP of new peer (within parent CIDR)
|
/// Specify desired IP of new peer (within parent CIDR)
|
||||||
#[structopt(long, conflicts_with = "auto-ip")]
|
#[structopt(long, conflicts_with = "auto-ip")]
|
||||||
|
@ -309,6 +312,10 @@ pub struct AddPeerOpts {
|
||||||
/// Save the config to the given location
|
/// Save the config to the given location
|
||||||
#[structopt(long)]
|
#[structopt(long)]
|
||||||
pub save_config: Option<String>,
|
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)]
|
#[derive(Debug, Clone, PartialEq, StructOpt)]
|
||||||
|
@ -341,7 +348,7 @@ pub struct AddAssociationOpts {
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||||
pub struct PeerContents {
|
pub struct PeerContents {
|
||||||
pub name: String,
|
pub name: Hostname,
|
||||||
pub ip: IpAddr,
|
pub ip: IpAddr,
|
||||||
pub cidr_id: i64,
|
pub cidr_id: i64,
|
||||||
pub public_key: String,
|
pub public_key: String,
|
||||||
|
@ -350,6 +357,7 @@ pub struct PeerContents {
|
||||||
pub is_admin: bool,
|
pub is_admin: bool,
|
||||||
pub is_disabled: bool,
|
pub is_disabled: bool,
|
||||||
pub is_redeemed: bool,
|
pub is_redeemed: bool,
|
||||||
|
pub invite_expires: Option<SystemTime>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||||
|
@ -481,6 +489,93 @@ pub struct State {
|
||||||
pub cidrs: Vec<Cidr>,
|
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> {
|
pub trait IoErrorContext<T> {
|
||||||
fn with_path<P: AsRef<Path>>(self, path: P) -> Result<T, WrappedIoError>;
|
fn with_path<P: AsRef<Path>>(self, path: P) -> Result<T, WrappedIoError>;
|
||||||
fn with_str<S: Into<String>>(self, context: S) -> Result<T, WrappedIoError>;
|
fn with_str<S: Into<String>>(self, context: S) -> Result<T, WrappedIoError>;
|
||||||
|
@ -526,7 +621,7 @@ mod tests {
|
||||||
let peer = Peer {
|
let peer = Peer {
|
||||||
id: 1,
|
id: 1,
|
||||||
contents: PeerContents {
|
contents: PeerContents {
|
||||||
name: "peer1".to_owned(),
|
name: "peer1".parse().unwrap(),
|
||||||
ip,
|
ip,
|
||||||
cidr_id: 1,
|
cidr_id: 1,
|
||||||
public_key: PUBKEY.to_owned(),
|
public_key: PUBKEY.to_owned(),
|
||||||
|
@ -535,6 +630,7 @@ mod tests {
|
||||||
is_admin: false,
|
is_admin: false,
|
||||||
is_disabled: false,
|
is_disabled: false,
|
||||||
is_redeemed: true,
|
is_redeemed: true,
|
||||||
|
invite_expires: None,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
let builder =
|
let builder =
|
||||||
|
@ -552,7 +648,7 @@ mod tests {
|
||||||
let peer = Peer {
|
let peer = Peer {
|
||||||
id: 1,
|
id: 1,
|
||||||
contents: PeerContents {
|
contents: PeerContents {
|
||||||
name: "peer1".to_owned(),
|
name: "peer1".parse().unwrap(),
|
||||||
ip,
|
ip,
|
||||||
cidr_id: 1,
|
cidr_id: 1,
|
||||||
public_key: PUBKEY.to_owned(),
|
public_key: PUBKEY.to_owned(),
|
||||||
|
@ -561,6 +657,7 @@ mod tests {
|
||||||
is_admin: false,
|
is_admin: false,
|
||||||
is_disabled: false,
|
is_disabled: false,
|
||||||
is_redeemed: true,
|
is_redeemed: true,
|
||||||
|
invite_expires: None,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
let builder =
|
let builder =
|
||||||
|
|
Loading…
Reference in New Issue