use crate::core::grants::Grants;
use crate::enroll::Tokens;
use crate::protocol::PeerMessage;
use iroh::EndpointId;
use std::collections::BTreeMap;
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
pub struct ConnId(pub u64);
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum Admission {
Added,
Code,
Key,
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum Refusal {
NotAuthorized,
EnrollTimeout,
NotGranted,
}
const SHORT_KEY: usize = 8;
pub fn default_name(key: &EndpointId) -> String {
key.to_string().chars().take(SHORT_KEY).collect()
}
impl Admission {
pub fn as_str(self) -> &'static str {
match self {
Admission::Added => "added",
Admission::Code => "code",
Admission::Key => "key",
}
}
}
impl Refusal {
pub fn as_str(self) -> &'static str {
match self {
Refusal::NotAuthorized => "not authorized",
Refusal::EnrollTimeout => "enroll timeout",
Refusal::NotGranted => "not granted",
}
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum Action {
Admit {
conn: ConnId,
peer: EndpointId,
replacing: bool,
},
Refuse { conn: ConnId, reason: Refusal },
Announce { peer: EndpointId, ports: Vec<u16> },
ApplyPorts { peer: EndpointId, ports: Vec<u16> },
ServeTunnel { port: u16 },
RejectTunnel { reason: Refusal },
PersistPin {
key: EndpointId,
label: Option<String>,
},
DropPin { key: EndpointId },
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Admitted {
pub label: Option<String>,
pub admission: Admission,
}
#[derive(Clone, PartialEq, Eq, Debug, Default)]
struct Pending {
early_ports: Option<Vec<u16>>,
}
#[derive(Default)]
pub struct Session {
grants: Grants,
tokens: Tokens,
peers: BTreeMap<EndpointId, Admitted>,
pending: BTreeMap<ConnId, Pending>,
}
impl Session {
pub fn new() -> Self {
Self::default()
}
pub fn admit_known(
&mut self,
key: EndpointId,
label: Option<String>,
admission: Admission,
) -> bool {
if self.peers.contains_key(&key) {
return false;
}
self.peers.insert(key, Admitted { label, admission });
true
}
pub fn on_inbound(&mut self, conn: ConnId, remote: EndpointId) -> Vec<Action> {
if self.peers.contains_key(&remote) {
return vec![
Action::Admit {
conn,
peer: remote,
replacing: true,
},
self.announce(remote),
];
}
self.pending.insert(conn, Pending::default());
Vec::new()
}
pub fn on_unadmitted(
&mut self,
conn: ConnId,
remote: EndpointId,
msg: PeerMessage,
) -> Vec<Action> {
match msg {
PeerMessage::Enroll { token, version: _ } => match self.tokens.claim(&token) {
Some(claimed) => {
let early = self
.pending
.remove(&conn)
.and_then(|p| p.early_ports)
.unwrap_or_default();
self.peers.insert(
remote,
Admitted {
label: claimed.name.clone(),
admission: Admission::Code,
},
);
for port in &claimed.ports {
self.grants.add(*port, remote);
}
let mut actions = vec![
Action::Admit {
conn,
peer: remote,
replacing: false,
},
Action::PersistPin {
key: remote,
label: claimed.name,
},
];
if !early.is_empty() {
actions.push(Action::ApplyPorts {
peer: remote,
ports: early,
});
}
actions.push(self.announce(remote));
actions
}
None => {
self.pending.remove(&conn);
vec![Action::Refuse {
conn,
reason: Refusal::NotAuthorized,
}]
}
},
PeerMessage::ExposedPorts(ports) => {
if let Some(p) = self.pending.get_mut(&conn) {
p.early_ports = Some(ports);
}
Vec::new()
}
_ => Vec::new(),
}
}
pub fn on_enroll_timeout(&mut self, conn: ConnId) -> Vec<Action> {
if self.pending.remove(&conn).is_none() {
return Vec::new();
}
vec![Action::Refuse {
conn,
reason: Refusal::EnrollTimeout,
}]
}
pub fn evict(&mut self, peer: &EndpointId) -> Vec<Action> {
if self.peers.remove(peer).is_none() {
return Vec::new();
}
self.grants.revoke_grantee(peer);
vec![Action::DropPin { key: *peer }]
}
pub fn on_tunnel(&self, peer: &EndpointId, port: u16) -> Action {
if self.grants.allows(port, peer) {
Action::ServeTunnel { port }
} else {
Action::RejectTunnel {
reason: Refusal::NotGranted,
}
}
}
pub fn announce(&self, peer: EndpointId) -> Action {
Action::Announce {
peer,
ports: self.grants.ports_for(&peer),
}
}
pub fn expose(&mut self, port: u16, to: &[EndpointId]) -> Vec<Action> {
for grantee in to {
self.grants.add(port, *grantee);
}
self.announce_all()
}
pub fn unexpose(&mut self, port: u16, to: Option<EndpointId>) -> Vec<Action> {
self.grants.remove(port, to);
self.announce_all()
}
fn announce_all(&self) -> Vec<Action> {
self.peers.keys().map(|k| self.announce(*k)).collect()
}
pub fn peer_keys(&self) -> Vec<EndpointId> {
self.peers.keys().copied().collect()
}
pub fn admission_of(&self, peer: &EndpointId) -> Option<Admission> {
self.peers.get(peer).map(|p| p.admission)
}
pub fn label_of(&self, peer: &EndpointId) -> Option<String> {
self.peers.get(peer).and_then(|p| p.label.clone())
}
pub fn mint_token(&self, name: Option<String>, ports: Vec<u16>) -> String {
self.tokens.mint(name, ports)
}
pub fn granted_ports(&self) -> Vec<u16> {
self.grants.ports()
}
pub fn all_grants(&self) -> Vec<(u16, EndpointId)> {
self.grants.all()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn key(n: u8) -> EndpointId {
let mut bytes = [0u8; 32];
bytes[0] = n;
iroh::SecretKey::from_bytes(&bytes).public()
}
fn session_with(peer: EndpointId) -> Session {
let mut s = Session::new();
s.admit_known(peer, Some("peer".into()), Admission::Added);
s
}
#[test]
fn tunnel_needs_a_grant() {
let a = key(1);
let s = session_with(a);
assert_eq!(
s.on_tunnel(&a, 4000),
Action::RejectTunnel {
reason: Refusal::NotGranted
}
);
}
#[test]
fn tunnel_is_served_once_granted() {
let a = key(1);
let mut s = session_with(a);
s.expose(4000, &[a]);
assert_eq!(s.on_tunnel(&a, 4000), Action::ServeTunnel { port: 4000 });
}
#[test]
fn a_grant_to_one_peer_is_not_usable_by_another() {
let (a, b) = (key(1), key(2));
let mut s = session_with(a);
s.admit_known(b, None, Admission::Added);
s.expose(4000, &[a]);
assert_eq!(
s.on_tunnel(&b, 4000),
Action::RejectTunnel {
reason: Refusal::NotGranted
}
);
}
#[test]
fn a_grant_covers_only_the_port_named() {
let a = key(1);
let mut s = session_with(a);
s.expose(4000, &[a]);
assert_eq!(
s.on_tunnel(&a, 4001),
Action::RejectTunnel {
reason: Refusal::NotGranted
}
);
}
#[test]
fn a_peer_is_told_only_its_own_grants() {
let (a, b) = (key(1), key(2));
let mut s = session_with(a);
s.admit_known(b, None, Admission::Added);
s.expose(4000, &[a]);
s.expose(4001, &[b]);
assert_eq!(
s.announce(a),
Action::Announce {
peer: a,
ports: vec![4000]
}
);
}
#[test]
fn revocation_announces_an_empty_list() {
let a = key(1);
let mut s = session_with(a);
s.expose(4000, &[a]);
assert_eq!(
s.unexpose(4000, None),
vec![Action::Announce {
peer: a,
ports: vec![]
}]
);
}
#[test]
fn revoking_one_grantee_leaves_the_other() {
let (a, b) = (key(1), key(2));
let mut s = session_with(a);
s.admit_known(b, None, Admission::Added);
s.expose(4000, &[a, b]);
s.unexpose(4000, Some(a));
assert_eq!(
s.on_tunnel(&a, 4000),
Action::RejectTunnel {
reason: Refusal::NotGranted
}
);
assert_eq!(s.on_tunnel(&b, 4000), Action::ServeTunnel { port: 4000 });
}
#[test]
fn an_unknown_peer_is_not_admitted_on_arrival() {
let mut s = Session::new();
assert_eq!(s.on_inbound(ConnId(1), key(9)), vec![]);
}
#[test]
fn an_unknown_peer_with_no_claim_is_refused_on_timeout() {
let mut s = Session::new();
let conn = ConnId(1);
s.on_inbound(conn, key(9));
assert_eq!(
s.on_enroll_timeout(conn),
vec![Action::Refuse {
conn,
reason: Refusal::EnrollTimeout
}]
);
assert!(s.admission_of(&key(9)).is_none());
}
#[test]
fn a_bad_code_is_refused() {
let mut s = Session::new();
let (conn, k) = (ConnId(1), key(9));
s.on_inbound(conn, k);
let actions = s.on_unadmitted(
conn,
k,
PeerMessage::Enroll {
token: "nope".into(),
version: None,
},
);
assert_eq!(
actions,
vec![Action::Refuse {
conn,
reason: Refusal::NotAuthorized
}]
);
assert!(s.admission_of(&k).is_none());
}
#[test]
fn a_valid_code_admits_and_pins() {
let mut s = Session::new();
let (conn, k) = (ConnId(1), key(9));
let token = s.mint_token(Some("rustdev".into()), vec![]);
s.on_inbound(conn, k);
let actions = s.on_unadmitted(
conn,
k,
PeerMessage::Enroll {
token,
version: None,
},
);
assert_eq!(
actions,
vec![
Action::Admit {
conn,
peer: k,
replacing: false
},
Action::PersistPin {
key: k,
label: Some("rustdev".into())
},
Action::Announce {
peer: k,
ports: vec![]
},
]
);
assert_eq!(s.admission_of(&k), Some(Admission::Code));
assert_eq!(s.label_of(&k), Some("rustdev".into()));
}
#[test]
fn a_code_is_single_use() {
let mut s = Session::new();
let token = s.mint_token(Some("rustdev".into()), vec![]);
let first = key(1);
s.on_inbound(ConnId(1), first);
s.on_unadmitted(
ConnId(1),
first,
PeerMessage::Enroll {
token: token.clone(),
version: None,
},
);
let second = key(2);
s.on_inbound(ConnId(2), second);
let actions = s.on_unadmitted(
ConnId(2),
second,
PeerMessage::Enroll {
token,
version: None,
},
);
assert_eq!(
actions,
vec![Action::Refuse {
conn: ConnId(2),
reason: Refusal::NotAuthorized
}]
);
assert!(s.admission_of(&second).is_none());
}
#[test]
fn an_unadmitted_peer_announcing_ports_is_not_admitted_by_it() {
let mut s = Session::new();
let (conn, k) = (ConnId(1), key(9));
s.on_inbound(conn, k);
assert_eq!(
s.on_unadmitted(conn, k, PeerMessage::ExposedPorts(vec![4000])),
vec![]
);
assert!(s.admission_of(&k).is_none());
}
#[test]
fn ports_announced_before_the_claim_are_applied_after_it() {
let mut s = Session::new();
let (conn, k) = (ConnId(1), key(9));
let token = s.mint_token(Some("rustdev".into()), vec![]);
s.on_inbound(conn, k);
s.on_unadmitted(conn, k, PeerMessage::ExposedPorts(vec![4000, 4001]));
let actions = s.on_unadmitted(
conn,
k,
PeerMessage::Enroll {
token,
version: None,
},
);
assert!(actions.contains(&Action::ApplyPorts {
peer: k,
ports: vec![4000, 4001]
}));
}
#[test]
fn a_known_peer_is_admitted_with_no_claim() {
let a = key(1);
let mut s = session_with(a);
let conn = ConnId(1);
assert_eq!(
s.on_inbound(conn, a),
vec![
Action::Admit {
conn,
peer: a,
replacing: true
},
Action::Announce {
peer: a,
ports: vec![]
},
]
);
}
#[test]
fn admitting_a_known_peer_twice_changes_nothing() {
let a = key(1);
let mut s = session_with(a);
assert!(!s.admit_known(a, Some("other".into()), Admission::Key));
assert_eq!(s.label_of(&a), Some("peer".into()));
assert_eq!(s.admission_of(&a), Some(Admission::Added));
}
#[test]
fn admission_records_how_the_peer_arrived() {
let mut s = Session::new();
s.admit_known(key(1), None, Admission::Key);
assert_eq!(s.admission_of(&key(1)), Some(Admission::Key));
}
#[test]
fn eviction_revokes_every_grant_naming_the_peer() {
let (a, b) = (key(1), key(2));
let mut s = session_with(a);
s.admit_known(b, None, Admission::Added);
s.expose(4000, &[a, b]);
s.expose(4001, &[a]);
s.evict(&a);
assert!(s.admission_of(&a).is_none());
assert_eq!(
s.on_tunnel(&a, 4000),
Action::RejectTunnel {
reason: Refusal::NotGranted
}
);
assert_eq!(s.granted_ports(), vec![4000]);
}
#[test]
fn evicting_an_unknown_peer_does_nothing() {
let mut s = Session::new();
assert_eq!(s.evict(&key(1)), vec![]);
}
#[test]
fn an_evicted_peer_is_no_longer_admitted_on_arrival() {
let a = key(1);
let mut s = session_with(a);
s.evict(&a);
assert_eq!(s.on_inbound(ConnId(1), a), vec![]);
}
#[test]
fn an_invitation_can_carry_a_grant() {
let mut s = Session::new();
let (conn, k) = (ConnId(1), key(9));
let token = s.mint_token(None, vec![8080]);
s.on_inbound(conn, k);
let actions = s.on_unadmitted(
conn,
k,
PeerMessage::Enroll {
token,
version: None,
},
);
assert!(actions.contains(&Action::Announce {
peer: k,
ports: vec![8080]
}));
assert_eq!(s.on_tunnel(&k, 8080), Action::ServeTunnel { port: 8080 });
}
#[test]
fn a_carried_grant_names_only_the_claimer() {
let (a, b) = (key(1), key(2));
let mut s = session_with(a);
let token = s.mint_token(None, vec![8080]);
s.on_inbound(ConnId(1), b);
s.on_unadmitted(
ConnId(1),
b,
PeerMessage::Enroll {
token,
version: None,
},
);
assert_eq!(s.on_tunnel(&b, 8080), Action::ServeTunnel { port: 8080 });
assert_eq!(
s.on_tunnel(&a, 8080),
Action::RejectTunnel {
reason: Refusal::NotGranted
}
);
}
#[test]
fn an_unnamed_peer_falls_back_to_a_short_key() {
let k = key(1);
let name = default_name(&k);
assert_eq!(name.len(), SHORT_KEY);
assert!(k.to_string().starts_with(&name));
}
#[test]
fn short_names_differ_between_peers() {
assert_ne!(default_name(&key(1)), default_name(&key(2)));
}
}