innernet/server/src/db/peer.rs

354 lines
13 KiB
Rust

use super::DatabaseCidr;
use crate::ServerError;
use lazy_static::lazy_static;
use regex::Regex;
use rusqlite::{params, types::Type, Connection};
use shared::{Peer, PeerContents, PERSISTENT_KEEPALIVE_INTERVAL_SECS};
use std::{
net::IpAddr,
ops::{Deref, DerefMut},
time::{Duration, SystemTime},
};
use structopt::lazy_static;
pub static CREATE_TABLE_SQL: &str = "CREATE TABLE peers (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE, /* The canonical name for the peer in canonical hostname(7) format. */
ip TEXT NOT NULL UNIQUE, /* The WireGuard-internal IP address assigned to the peer. */
public_key TEXT NOT NULL UNIQUE, /* The WireGuard public key of the peer. */
endpoint TEXT, /* The optional external endpoint ([ip]:[port]) of the peer. */
cidr_id INTEGER NOT NULL, /* The ID of the peer's parent 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_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. */
candidates TEXT, /* A list of additional endpoints that peers can use to connect. */
FOREIGN KEY (cidr_id)
REFERENCES cidrs (id)
ON UPDATE RESTRICT
ON DELETE RESTRICT
)";
pub static COLUMNS: &[&str] = &[
"id",
"name",
"ip",
"cidr_id",
"public_key",
"endpoint",
"is_admin",
"is_disabled",
"is_redeemed",
"invite_expires",
"candidates",
];
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 PEER_NAME_REGEX: Regex = Regex::new(r"^([a-z0-9]-?)*[a-z0-9]$").unwrap();
}
#[derive(Debug)]
pub struct DatabasePeer {
pub inner: Peer,
}
impl From<Peer> for DatabasePeer {
fn from(inner: Peer) -> Self {
Self { inner }
}
}
impl Deref for DatabasePeer {
type Target = Peer;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl DerefMut for DatabasePeer {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
impl DatabasePeer {
pub fn create(conn: &Connection, contents: PeerContents) -> Result<Self, ServerError> {
let PeerContents {
name,
ip,
cidr_id,
public_key,
endpoint,
is_admin,
is_disabled,
is_redeemed,
invite_expires,
candidates,
..
} = &contents;
log::info!("creating peer {:?}", contents);
if !Self::is_valid_name(name) {
log::warn!("peer name is invalid, must conform to hostname(7) requirements.");
return Err(ServerError::InvalidQuery);
}
let cidr = DatabaseCidr::get(conn, *cidr_id)?;
if !cidr.cidr.contains(*ip) {
log::warn!("tried to add peer with IP outside of parent CIDR range.");
return Err(ServerError::InvalidQuery);
}
if !cidr.cidr.is_assignable(*ip) {
println!("Peer IP cannot be the network or broadcast IP of CIDRs with network prefixes under 31.");
return Err(ServerError::InvalidQuery);
}
let invite_expires = invite_expires
.map(|t| t.duration_since(SystemTime::UNIX_EPOCH).ok())
.flatten()
.map(|t| t.as_secs());
let candidates = serde_json::to_string(candidates)?;
conn.execute(
&format!(
"INSERT INTO peers ({}) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
COLUMNS[1..].join(", ")
),
params![
&**name,
ip.to_string(),
cidr_id,
&public_key,
endpoint.as_ref().map(|endpoint| endpoint.to_string()),
is_admin,
is_disabled,
is_redeemed,
invite_expires,
candidates,
],
)?;
let id = conn.last_insert_rowid();
Ok(Peer { id, contents }.into())
}
fn is_valid_name(name: &str) -> bool {
name.len() < 64 && PEER_NAME_REGEX.is_match(name)
}
/// Update self with new contents, validating them and updating the backend in the process.
pub fn update(&mut self, conn: &Connection, contents: PeerContents) -> Result<(), ServerError> {
if !Self::is_valid_name(&contents.name) {
log::warn!("peer name is invalid, must conform to hostname(7) requirements.");
return Err(ServerError::InvalidQuery);
}
// We will only allow updates of certain fields at this point, disregarding any requests
// for changes of IP address, public key, or parent CIDR, for security reasons.
//
// In the future, we may allow re-assignments of peers to new CIDRs, but it's easiest to
// disregard that case for now to prevent possible attacks.
let new_contents = PeerContents {
name: contents.name,
endpoint: contents.endpoint,
is_admin: contents.is_admin,
is_disabled: contents.is_disabled,
candidates: contents.candidates,
..self.contents.clone()
};
let new_candidates = serde_json::to_string(&new_contents.candidates)?;
conn.execute(
"UPDATE peers SET
name = ?2,
endpoint = ?3,
is_admin = ?4,
is_disabled = ?5,
candidates = ?6
WHERE id = ?1",
params![
self.id,
&*new_contents.name,
new_contents
.endpoint
.as_ref()
.map(|endpoint| endpoint.to_string()),
new_contents.is_admin,
new_contents.is_disabled,
new_candidates,
],
)?;
self.contents = new_contents;
Ok(())
}
pub fn disable(conn: &Connection, id: i64) -> Result<(), ServerError> {
match conn.execute(
"UPDATE peers SET is_disabled = 1 WHERE id = ?1",
params![id],
)? {
0 => Err(ServerError::NotFound),
_ => Ok(()),
}
}
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],
)? {
0 => Err(ServerError::NotFound),
_ => {
self.contents.public_key = pubkey.into();
self.contents.is_redeemed = true;
Ok(())
},
}
}
fn from_row(row: &rusqlite::Row) -> Result<Self, rusqlite::Error> {
let id = row.get(0)?;
let name = row
.get::<_, String>(1)?
.parse()
.map_err(|_| rusqlite::Error::InvalidColumnType(1, "hostname".into(), Type::Text))?;
let ip: IpAddr = row
.get::<_, String>(2)?
.parse()
.map_err(|_| rusqlite::Error::InvalidColumnType(2, "ip".into(), Type::Text))?;
let cidr_id = row.get(3)?;
let public_key = row.get(4)?;
let endpoint = row
.get::<_, Option<String>>(5)?
.and_then(|endpoint| endpoint.parse().ok());
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 candidates = if let Some(candidates) = row.get::<_, Option<String>>(10)? {
serde_json::from_str(&candidates).map_err(|_| {
rusqlite::Error::InvalidColumnType(10, "candidates (json)".into(), Type::Text)
})?
} else {
vec![]
};
let persistent_keepalive_interval = Some(PERSISTENT_KEEPALIVE_INTERVAL_SECS);
Ok(Peer {
id,
contents: PeerContents {
name,
ip,
cidr_id,
public_key,
endpoint,
persistent_keepalive_interval,
is_admin,
is_disabled,
is_redeemed,
invite_expires,
candidates,
},
}
.into())
}
pub fn get(conn: &Connection, id: i64) -> Result<Self, ServerError> {
let result = conn.query_row(
&format!("SELECT {} FROM peers WHERE id = ?1", COLUMNS.join(", ")),
params![id],
Self::from_row,
)?;
Ok(result)
}
pub fn get_from_ip(conn: &Connection, ip: IpAddr) -> Result<Self, rusqlite::Error> {
let result = conn.query_row(
&format!("SELECT {} FROM peers WHERE ip = ?1", COLUMNS.join(", ")),
params![ip.to_string()],
Self::from_row,
)?;
Ok(result)
}
pub fn get_all_allowed_peers(&self, conn: &Connection) -> Result<Vec<Self>, ServerError> {
// This query is a handful, so an explanation of what's happening, and what each CTE does (https://sqlite.org/lang_with.html):
//
// 1. parent_of: Enumerate all ancestor CIDRs of the CIDR associated with peer.
// 2. associated: Enumerate all auth associations between any of the above enumerated CIDRs.
// 3. associated_subcidrs: For each association, list all peers by enumerating down each
// associated CIDR's children and listing any peers belonging to them.
//
// NOTE that a forced association is created with the special "infra" CIDR with id 2 (1 being the root).
let mut stmt = conn.prepare_cached(
&format!("WITH
parent_of(id, parent) AS (
SELECT id, parent FROM cidrs WHERE id = ?1
UNION ALL
SELECT cidrs.id, cidrs.parent FROM cidrs JOIN parent_of ON parent_of.parent = cidrs.id
),
associated(cidr_id) as (
SELECT associations.cidr_id_2 FROM associations, parent_of WHERE associations.cidr_id_1 = parent_of.id
UNION
SELECT associations.cidr_id_1 FROM associations, parent_of WHERE associations.cidr_id_2 = parent_of.id
),
associated_subcidrs(cidr_id) AS (
VALUES(?1), (2)
UNION
SELECT cidr_id FROM associated
UNION
SELECT id FROM cidrs, associated_subcidrs WHERE cidrs.parent=associated_subcidrs.cidr_id
)
SELECT DISTINCT {}
FROM peers
JOIN associated_subcidrs ON peers.cidr_id=associated_subcidrs.cidr_id
WHERE peers.is_disabled = 0 AND peers.is_redeemed = 1;",
COLUMNS.iter().map(|col| format!("peers.{}", col)).collect::<Vec<_>>().join(", ")
),
)?;
let peers = stmt
.query_map(params![self.cidr_id], Self::from_row)?
.collect::<Result<_, _>>()?;
Ok(peers)
}
pub fn list(conn: &Connection) -> Result<Vec<Self>, ServerError> {
let mut stmt = conn.prepare_cached(&format!("SELECT {} FROM peers", COLUMNS.join(", ")))?;
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)
}
}