use std::io::{self, Write};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, TcpStream, ToSocketAddrs, UdpSocket};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use purecrypto::ec::x25519::X25519PrivateKey;
use purecrypto::rng::OsRng;
use purecrypto::zeroize::Zeroizing;
use super::server::unix_ms;
use super::wire::{self, Accept, KnockFields, MAX_DATAGRAM};
use crate::error::{Error, Result};
use crate::hostkey::HostKey;
pub struct KnockIdentity<'a> {
pub signer: &'a dyn HostKey,
pub sig1: Option<Vec<u8>>,
}
impl core::fmt::Debug for KnockIdentity<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("KnockIdentity")
.field("algorithm", &self.signer.algorithm())
.field("sig1_cached", &self.sig1.is_some())
.finish()
}
}
#[derive(Debug)]
pub struct KnockRequest<'a> {
pub user: &'a str,
pub identities: Vec<KnockIdentity<'a>>,
pub known_host_keys: Vec<Vec<u8>>,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct KnockClientConfig {
pub retransmit: Vec<Duration>,
pub give_up: Duration,
}
impl Default for KnockClientConfig {
fn default() -> Self {
Self {
retransmit: vec![Duration::from_millis(500), Duration::from_millis(1500)],
give_up: Duration::from_millis(3000),
}
}
}
pub struct KnockAccepted {
pub target: SocketAddr,
pub host_key_blob: Vec<u8>,
pub host_key_known: bool,
pub identity: usize,
pub expires: Instant,
token: Zeroizing<[u8; 32]>,
}
impl core::fmt::Debug for KnockAccepted {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("KnockAccepted")
.field("target", &self.target)
.field("host_key_known", &self.host_key_known)
.field("identity", &self.identity)
.finish_non_exhaustive()
}
}
impl KnockAccepted {
pub fn connect(&self, timeout: Option<Duration>) -> io::Result<TcpStream> {
let mut s = match timeout {
Some(t) => TcpStream::connect_timeout(&self.target, t)?,
None => TcpStream::connect(self.target)?,
};
s.set_nodelay(true)?;
s.write_all(&self.token[..])?;
Ok(s)
}
#[cfg(feature = "client")]
pub fn pin(&self, cfg: &mut crate::client::Config) {
cfg.pinned_host_key = Some(self.host_key_blob.clone());
}
}
fn host_key_rank(blob: &[u8]) -> u8 {
match crate::format::Reader::new(blob).read_string() {
Ok(b"ssh-ed25519") => 0,
Ok(n) if n.starts_with(b"ecdsa-") => 1,
_ => 2,
}
}
struct Sent {
datagram: Vec<u8>,
hash: [u8; 32],
eph: X25519PrivateKey,
identity: usize,
}
pub fn knock(
addr: SocketAddr,
req: &mut KnockRequest<'_>,
cfg: &KnockClientConfig,
abort: &dyn Fn() -> bool,
) -> Result<Option<KnockAccepted>> {
let mut known: Vec<&Vec<u8>> = req
.known_host_keys
.iter()
.filter(|b| wire::max_sig_blob_len(b).is_some())
.collect();
known.sort_by_key(|b| host_key_rank(b));
let expected = known.first().map(|b| (*b).clone());
let (host_fp, min_len) = match &expected {
Some(blob) => (
Some(wire::key_fp(blob)),
wire::accept_len_bound(blob, false).unwrap_or(0),
),
None => (None, MAX_DATAGRAM),
};
let mut sent: Vec<Sent> = Vec::new();
let mut seen_fps: Vec<[u8; 32]> = Vec::new();
let nonce = unix_ms();
for (i, id) in req.identities.iter_mut().enumerate() {
let blob = id.signer.public_blob();
if wire::max_sig_blob_len(&blob).is_none() {
continue;
}
let fp = wire::key_fp(&blob);
if seen_fps.contains(&fp) {
continue;
}
seen_fps.push(fp);
if id.sig1.is_none() {
let msg = wire::knock_id_message(req.user, &fp);
id.sig1 = Some(wire::sign(id.signer, wire::NS_KNOCK_ID, &msg)?);
}
let eph = X25519PrivateKey::generate(&mut OsRng);
let fields = KnockFields {
user: req.user,
key_fp: fp,
sig1: id.sig1.as_deref().unwrap_or_default(),
nonce,
eph_c: eph.public_key(),
host_fp,
};
let datagram = match fields.build(id.signer, min_len) {
Ok(d) => d,
Err(Error::Unsupported(_)) => continue,
Err(e) => return Err(e),
};
let hash = wire::knock_hash(&datagram);
sent.push(Sent {
datagram,
hash,
eph,
identity: i,
});
}
if sent.is_empty() {
return Ok(None);
}
let local: SocketAddr = match addr.ip() {
IpAddr::V4(_) => (Ipv4Addr::UNSPECIFIED, 0).into(),
IpAddr::V6(_) => (Ipv6Addr::UNSPECIFIED, 0).into(),
};
let udp = UdpSocket::bind(local)?;
udp.connect(addr)?;
let start = Instant::now();
let send_all = |udp: &UdpSocket| {
for s in &sent {
let _ = udp.send(&s.datagram);
}
};
send_all(&udp);
let mut schedule = cfg.retransmit.iter().copied().peekable();
let mut buf = [0u8; MAX_DATAGRAM + 1];
loop {
let elapsed = start.elapsed();
if elapsed >= cfg.give_up || abort() {
return Ok(None);
}
if let Some(at) = schedule.peek()
&& elapsed >= *at
{
schedule.next();
send_all(&udp);
continue;
}
let next = schedule
.peek()
.copied()
.unwrap_or(cfg.give_up)
.min(cfg.give_up);
let wait = next
.saturating_sub(elapsed)
.clamp(Duration::from_millis(1), Duration::from_millis(50));
udp.set_read_timeout(Some(wait))?;
let n = match udp.recv(&mut buf) {
Ok(n) => n,
Err(e)
if matches!(
e.kind(),
io::ErrorKind::WouldBlock
| io::ErrorKind::TimedOut
| io::ErrorKind::Interrupted
) =>
{
continue;
}
Err(e)
if matches!(
e.kind(),
io::ErrorKind::ConnectionRefused | io::ErrorKind::ConnectionReset
) =>
{
return Ok(None);
}
Err(e) => return Err(e.into()),
};
if n > MAX_DATAGRAM {
continue;
}
if let Some(acc) = check_accept(&buf[..n], &sent, expected.as_deref(), addr) {
return Ok(Some(acc));
}
}
}
fn check_accept(
datagram: &[u8],
sent: &[Sent],
expected: Option<&[u8]>,
addr: SocketAddr,
) -> Option<KnockAccepted> {
let a = Accept::parse(datagram).ok()?;
let s = sent.iter().find(|s| s.hash == a.knock_hash)?;
let (blob, known) = match (expected, a.hostkey) {
(Some(exp), None) => (exp.to_vec(), true),
(Some(exp), Some(full)) if full == exp => (exp.to_vec(), true),
(Some(_), Some(_)) => return None,
(None, Some(full)) => (full.to_vec(), false),
(None, None) => return None,
};
wire::verify(&blob, wire::NS_ACCEPT, a.signed, a.sig_host).ok()?;
let shared = Zeroizing::new(s.eph.diffie_hellman(&a.eph_s).ok()?);
let token = wire::open_token(&shared, &a.knock_hash, &a.eph_s, a.port);
Some(KnockAccepted {
target: SocketAddr::new(addr.ip(), a.port),
host_key_blob: blob,
host_key_known: known,
identity: s.identity,
expires: Instant::now() + Duration::from_secs(a.window as u64),
token,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum KnockMode {
Never,
#[default]
Auto,
Always,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct KnockConnectOptions {
pub mode: KnockMode,
pub fallback_delay: Duration,
pub connect_timeout: Option<Duration>,
pub knock_port: Option<u16>,
pub client: KnockClientConfig,
}
impl Default for KnockConnectOptions {
fn default() -> Self {
Self {
mode: KnockMode::Auto,
fallback_delay: Duration::from_millis(1500),
connect_timeout: None,
knock_port: None,
client: KnockClientConfig::default(),
}
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum Connected {
Direct(TcpStream),
Knocked {
stream: TcpStream,
accepted: KnockAccepted,
},
}
impl Connected {
pub fn stream(&self) -> &TcpStream {
match self {
Connected::Direct(s) | Connected::Knocked { stream: s, .. } => s,
}
}
}
fn tcp_connect(addr: SocketAddr, timeout: Option<Duration>) -> io::Result<TcpStream> {
match timeout {
Some(t) => TcpStream::connect_timeout(&addr, t),
None => TcpStream::connect(addr),
}
}
pub fn connect_or_knock(
host: &str,
port: u16,
req: &mut KnockRequest<'_>,
opts: &KnockConnectOptions,
) -> Result<Connected> {
let addrs: Vec<SocketAddr> = (host, port).to_socket_addrs()?.collect();
let timeout = opts.connect_timeout;
connect_or_knock_addrs(
&addrs,
req,
opts,
Arc::new(move |a| tcp_connect(a, timeout)),
)
}
pub type TcpDialer = Arc<dyn Fn(SocketAddr) -> io::Result<TcpStream> + Send + Sync>;
pub fn connect_or_knock_addrs(
addrs: &[SocketAddr],
req: &mut KnockRequest<'_>,
opts: &KnockConnectOptions,
dial: TcpDialer,
) -> Result<Connected> {
let mut last_err: Option<Error> = None;
for &addr in addrs {
let knock_addr = SocketAddr::new(addr.ip(), opts.knock_port.unwrap_or(addr.port()));
let res = match opts.mode {
KnockMode::Never => dial(addr).map(Connected::Direct).map_err(Error::from),
KnockMode::Always => match knock(knock_addr, req, &opts.client, &|| false) {
Ok(Some(acc)) => knocked(acc, opts),
Ok(None) => Err(Error::Io(io::Error::new(
io::ErrorKind::ConnectionRefused,
"knock: no answer",
))),
Err(e) => Err(e),
},
KnockMode::Auto => auto_one(addr, knock_addr, req, opts, dial.clone()),
};
match res {
Ok(c) => return Ok(c),
Err(e) => last_err = Some(e),
}
}
Err(last_err.unwrap_or_else(|| {
Error::Io(io::Error::new(
io::ErrorKind::NotFound,
"no addresses to connect to",
))
}))
}
fn knocked(acc: KnockAccepted, opts: &KnockConnectOptions) -> Result<Connected> {
let stream = acc.connect(opts.connect_timeout)?;
Ok(Connected::Knocked {
stream,
accepted: acc,
})
}
type Slot = Arc<Mutex<Option<io::Result<TcpStream>>>>;
fn auto_one(
addr: SocketAddr,
knock_addr: SocketAddr,
req: &mut KnockRequest<'_>,
opts: &KnockConnectOptions,
dial: TcpDialer,
) -> Result<Connected> {
let slot: Slot = Arc::new(Mutex::new(None));
let (tx, rx) = std::sync::mpsc::channel::<()>();
let thread_slot = slot.clone();
let handle = std::thread::spawn(move || {
let r = dial(addr);
if let Ok(mut g) = thread_slot.lock() {
*g = Some(r);
}
let _ = tx.send(());
});
let take = |slot: &Slot| slot.lock().ok().and_then(|mut g| g.take());
let _ = rx.recv_timeout(opts.fallback_delay);
match take(&slot) {
Some(Ok(s)) => {
let _ = handle.join();
return Ok(Connected::Direct(s));
}
Some(Err(e)) => {
let _ = handle.join();
return match knock(knock_addr, req, &opts.client, &|| false)? {
Some(acc) => knocked(acc, opts),
None => Err(e.into()),
};
}
None => {}
}
let tcp_ok = || {
slot.lock()
.map(|g| matches!(*g, Some(Ok(_))))
.unwrap_or(false)
};
let knocked_acc = knock(knock_addr, req, &opts.client, &tcp_ok);
if let Ok(Some(acc)) = knocked_acc {
drop(handle);
return knocked(acc, opts);
}
let _ = handle.join();
match take(&slot) {
Some(Ok(s)) => Ok(Connected::Direct(s)),
Some(Err(e)) => Err(e.into()),
None => match knocked_acc {
Err(e) => Err(e),
_ => Err(Error::Io(io::Error::other("connect thread vanished"))),
},
}
}