This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knot2 / crates / knot-ssh / src / lib.rs
13 kB 418 lines
1mod exec; 2mod identity; 3mod server; 4 5use std::borrow::Cow; 6use std::collections::BTreeSet; 7use std::net::SocketAddr; 8use std::path::Path; 9use std::sync::Arc; 10use std::time::Duration; 11 12use knot_atproto::Atproto; 13use knot_events::EventLog; 14use knot_git::{ArchiveLimit, Layout}; 15use knot_index::{Index, KeyTtl}; 16use knot_maintenance::MaintenanceHandle; 17use knot_pack::{MaxWireBytes, PackLimits}; 18use knot_postreceive::LanguagesPushBudget; 19use knot_runtime::{Clock, Entropy, HttpTransport, OsEntropy}; 20use knot_types::{AccountDid, ActorId, AdmissionPolicy, AppviewEndpoint, CiLogsAddr, KnotHostname}; 21use russh::keys::ssh_key::rand_core; 22use russh::keys::{Algorithm, PrivateKey, ssh_key}; 23use russh::server::{Config, Server as _}; 24use russh::{MethodKind, MethodSet, Preferred, compression}; 25use tokio::sync::Semaphore; 26use tokio_util::sync::CancellationToken; 27use tokio_util::task::TaskTracker; 28 29use knot_resource::{ 30 Burst, GlobalInflight, LimitConfig, PeerPacer, PerPeerInflight, PreAuthLimiter, RateLimit, 31 RefillMicros, ResolveSlots, Slots, SubjectPacer, 32}; 33use server::KnotSshServer; 34 35const MAX_INFLIGHT_PER_PEER: usize = 4; 36const MAX_INFLIGHT_LOOKUPS: usize = 16; 37const MAX_PREAUTH_LOOKUPS: usize = 4; 38const LOOKUP_BURST_PER_PEER: u32 = 8; 39const LOOKUP_REFILL_MICROS: u64 = 500_000; 40const LOOKUP_INFLIGHT_PER_PEER: usize = 2; 41const PROBE_BURST_PER_ACCOUNT: u32 = 1; 42const PROBE_REFILL_MICROS: u64 = 30_000_000; 43const MISS_BURST_PER_PEER: u32 = 1; 44const MISS_REFILL_MICROS: u64 = 120_000_000; 45const INACTIVITY_TIMEOUT: Duration = Duration::from_secs(120); 46const KEEPALIVE_INTERVAL: Duration = Duration::from_secs(30); 47const AUTH_REJECTION_TIME: Duration = Duration::from_millis(250); 48const DRAIN_GRACE: Duration = Duration::from_secs(30); 49const ACCEPT_BACKOFF: Duration = Duration::from_millis(250); 50 51#[derive(Debug, thiserror::Error)] 52pub enum SshError { 53 #[error("ssh host key {path}: {message}")] 54 HostKey { path: String, message: String }, 55 #[error("ssh server bind or serve: {0}")] 56 Serve(#[from] std::io::Error), 57} 58 59pub struct SshState<H, C> { 60 layout: Layout, 61 index: Arc<Index>, 62 atproto: Arc<Atproto<H, C>>, 63 knot_actor: ActorId, 64 events: Arc<EventLog<C>>, 65 hostname: KnotHostname, 66 appview: AppviewEndpoint, 67 admins: BTreeSet<AccountDid>, 68 admission: AdmissionPolicy, 69 limits: PackLimits, 70 max_pack_bytes: MaxWireBytes, 71 archive_limit: ArchiveLimit, 72 languages_push_budget: LanguagesPushBudget, 73 ci_logs: Option<CiLogsAddr>, 74 slots: Slots, 75 lookup_slots: ResolveSlots, 76 lookup_peers: Arc<PreAuthLimiter>, 77 probe_pace: SubjectPacer, 78 miss_pace: PeerPacer, 79 key_ttl: KeyTtl, 80 peer_slots: Arc<PreAuthLimiter>, 81 maintenance: MaintenanceHandle, 82 lfs: Option<LfsRuntime>, 83 catalog: Arc<knot_messages::Catalog>, 84} 85 86#[derive(Clone)] 87pub(crate) struct LfsRuntime { 88 pub(crate) handle: knot_lfs::LfsHandle, 89 pub(crate) slots: Arc<Semaphore>, 90 pub(crate) peer_slots: Arc<PreAuthLimiter>, 91} 92 93pub struct SshConfig<H, C> { 94 pub layout: Layout, 95 pub index: Arc<Index>, 96 pub atproto: Arc<Atproto<H, C>>, 97 pub knot_actor: ActorId, 98 pub events: Arc<EventLog<C>>, 99 pub hostname: KnotHostname, 100 pub appview: AppviewEndpoint, 101 pub admins: BTreeSet<AccountDid>, 102 pub admission: AdmissionPolicy, 103 pub max_pack_bytes: MaxWireBytes, 104 pub archive_limit: ArchiveLimit, 105 pub languages_push_budget: LanguagesPushBudget, 106 pub ci_logs: Option<CiLogsAddr>, 107} 108 109impl<H: HttpTransport, C: Clock> SshState<H, C> { 110 pub fn new(config: SshConfig<H, C>) -> Self { 111 let SshConfig { 112 layout, 113 index, 114 atproto, 115 knot_actor, 116 events, 117 hostname, 118 appview, 119 admins, 120 admission, 121 max_pack_bytes, 122 archive_limit, 123 languages_push_budget, 124 ci_logs, 125 } = config; 126 Self { 127 layout, 128 index, 129 atproto, 130 knot_actor, 131 events, 132 hostname, 133 appview, 134 admins, 135 admission, 136 limits: PackLimits::default(), 137 max_pack_bytes, 138 archive_limit, 139 languages_push_budget, 140 ci_logs, 141 slots: Slots::for_machine(), 142 lookup_slots: ResolveSlots::new(MAX_INFLIGHT_LOOKUPS), 143 lookup_peers: Arc::new(PreAuthLimiter::with_config(LimitConfig { 144 rate: Some(RateLimit { 145 burst: Burst::new(LOOKUP_BURST_PER_PEER), 146 refill: RefillMicros::new(LOOKUP_REFILL_MICROS), 147 }), 148 per_peer_inflight: Some(PerPeerInflight::new(LOOKUP_INFLIGHT_PER_PEER)), 149 global_inflight: Some(GlobalInflight::new(MAX_PREAUTH_LOOKUPS)), 150 })), 151 probe_pace: SubjectPacer::new(RateLimit { 152 burst: Burst::new(PROBE_BURST_PER_ACCOUNT), 153 refill: RefillMicros::new(PROBE_REFILL_MICROS), 154 }), 155 miss_pace: PeerPacer::new(RateLimit { 156 burst: Burst::new(MISS_BURST_PER_PEER), 157 refill: RefillMicros::new(MISS_REFILL_MICROS), 158 }), 159 key_ttl: KeyTtl::DEFAULT, 160 peer_slots: Arc::new(PreAuthLimiter::with_config(LimitConfig::per_peer_only( 161 PerPeerInflight::new(MAX_INFLIGHT_PER_PEER), 162 ))), 163 maintenance: MaintenanceHandle::disabled(), 164 lfs: None, 165 catalog: Arc::new(knot_messages::Catalog::defaults()), 166 } 167 } 168 169 pub fn with_catalog(mut self, catalog: Arc<knot_messages::Catalog>) -> Self { 170 self.catalog = catalog; 171 self 172 } 173 174 pub fn with_slots(mut self, slots: Slots) -> Self { 175 self.slots = slots; 176 self 177 } 178 179 pub fn with_key_ttl(mut self, ttl: KeyTtl) -> Self { 180 self.key_ttl = ttl; 181 self 182 } 183 184 pub fn with_maintenance(mut self, maintenance: MaintenanceHandle) -> Self { 185 self.maintenance = maintenance; 186 self 187 } 188 189 pub fn with_lfs(mut self, handle: knot_lfs::LfsHandle, max_transfers: usize) -> Self { 190 self.lfs = Some(LfsRuntime { 191 handle, 192 slots: Arc::new(Semaphore::new(max_transfers)), 193 peer_slots: Arc::new(PreAuthLimiter::with_config(LimitConfig::per_peer_only( 194 PerPeerInflight::new(max_transfers), 195 ))), 196 }); 197 self 198 } 199 200 pub fn with_limits(mut self, limits: PackLimits) -> Self { 201 self.limits = limits; 202 self 203 } 204} 205 206fn server_config(host_key: PrivateKey) -> Arc<Config> { 207 Arc::new(Config { 208 keys: vec![host_key], 209 methods: MethodSet::from(&[MethodKind::PublicKey][..]), 210 inactivity_timeout: Some(INACTIVITY_TIMEOUT), 211 keepalive_interval: Some(KEEPALIVE_INTERVAL), 212 auth_rejection_time: AUTH_REJECTION_TIME, 213 preferred: Preferred { 214 compression: Cow::Borrowed(&[compression::NONE]), 215 ..Preferred::DEFAULT 216 }, 217 ..Config::default() 218 }) 219} 220 221pub async fn serve<H: HttpTransport, C: Clock>( 222 addr: SocketAddr, 223 host_key: PrivateKey, 224 state: Arc<SshState<H, C>>, 225 shutdown: CancellationToken, 226) -> Result<(), SshError> { 227 let listener = tokio::net::TcpListener::bind(addr).await?; 228 serve_drained(listener, host_key, state, shutdown).await 229} 230 231pub async fn serve_on_socket<H: HttpTransport, C: Clock>( 232 listener: tokio::net::TcpListener, 233 host_key: PrivateKey, 234 state: Arc<SshState<H, C>>, 235) -> Result<(), SshError> { 236 serve_drained(listener, host_key, state, CancellationToken::new()).await 237} 238 239#[doc(hidden)] 240pub async fn serve_drained<H: HttpTransport, C: Clock>( 241 listener: tokio::net::TcpListener, 242 host_key: PrivateKey, 243 state: Arc<SshState<H, C>>, 244 shutdown: CancellationToken, 245) -> Result<(), SshError> { 246 let config = server_config(host_key); 247 let tracker = TaskTracker::new(); 248 let mut server = KnotSshServer { 249 state, 250 tracker: tracker.clone(), 251 }; 252 loop { 253 let accepted = tokio::select! { 254 () = shutdown.cancelled() => break, 255 accepted = listener.accept() => accepted, 256 }; 257 let (stream, peer) = match accepted { 258 Ok(pair) => pair, 259 Err(error) if is_connection_error(&error) => continue, 260 Err(error) => { 261 tracing::warn!("ssh accept failed, backing off: {error}"); 262 tokio::select! { 263 () = shutdown.cancelled() => break, 264 () = tokio::time::sleep(ACCEPT_BACKOFF) => {} 265 } 266 continue; 267 } 268 }; 269 let handler = server.new_client(Some(peer)); 270 let config = Arc::clone(&config); 271 tracker.spawn(async move { 272 if let Ok(session) = russh::server::run_stream(config, stream, handler).await { 273 let _ = session.await; 274 } 275 }); 276 } 277 tracker.close(); 278 let _ = tokio::time::timeout(DRAIN_GRACE, tracker.wait()).await; 279 Ok(()) 280} 281 282fn is_connection_error(error: &std::io::Error) -> bool { 283 matches!( 284 error.kind(), 285 std::io::ErrorKind::ConnectionRefused 286 | std::io::ErrorKind::ConnectionAborted 287 | std::io::ErrorKind::ConnectionReset 288 ) 289} 290 291pub fn load_or_create_host_key(path: &Path) -> Result<PrivateKey, SshError> { 292 let report = |message: String| SshError::HostKey { 293 path: path.display().to_string(), 294 message, 295 }; 296 let load = || { 297 ensure_secure_perms(path).map_err(&report)?; 298 russh::keys::load_secret_key(path, None).map_err(|error| report(error.to_string())) 299 }; 300 if path.exists() { 301 return load(); 302 } 303 let key = PrivateKey::random(&mut EntropyRng, Algorithm::Ed25519) 304 .map_err(|error| report(error.to_string()))?; 305 match persist_host_key(path, &key)? { 306 Claim::Won => Ok(key), 307 Claim::Lost => load(), 308 } 309} 310 311enum Claim { 312 Won, 313 Lost, 314} 315 316fn persist_host_key(path: &Path, key: &PrivateKey) -> Result<Claim, SshError> { 317 let report = |message: String| SshError::HostKey { 318 path: path.display().to_string(), 319 message, 320 }; 321 let pem = key 322 .to_openssh(ssh_key::LineEnding::LF) 323 .map_err(|error| report(error.to_string()))?; 324 if let Some(parent) = path.parent() { 325 std::fs::create_dir_all(parent).map_err(|error| report(error.to_string()))?; 326 } 327 let temp = unique_temp(path); 328 write_secret(&temp, pem.as_bytes()).map_err(|error| report(error.to_string()))?; 329 // `hard_link` instead of `rename` so that in case two knots 330 // are booting at the same time they don't get borked 331 // if one clobber's the other's key. here the loser reloads 332 // winner's key. 333 let claim = match std::fs::hard_link(&temp, path) { 334 Ok(()) => Claim::Won, 335 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => Claim::Lost, 336 Err(error) => { 337 let _ = std::fs::remove_file(&temp); 338 return Err(report(error.to_string())); 339 } 340 }; 341 let _ = std::fs::remove_file(&temp); 342 if let Some(parent) = path.parent() 343 && let Ok(dir) = std::fs::File::open(parent) 344 { 345 let _ = dir.sync_all(); 346 } 347 Ok(claim) 348} 349 350#[cfg(unix)] 351fn ensure_secure_perms(path: &Path) -> Result<(), String> { 352 use std::os::unix::fs::MetadataExt; 353 let mode = std::fs::metadata(path) 354 .map_err(|error| error.to_string())? 355 .mode(); 356 if mode & 0o077 != 0 { 357 return Err(format!( 358 "private host key is group or other accessible at mode {:o}, run chmod 600 on it", 359 mode & 0o777 360 )); 361 } 362 Ok(()) 363} 364 365#[cfg(not(unix))] 366fn ensure_secure_perms(_path: &Path) -> Result<(), String> { 367 Ok(()) 368} 369 370fn unique_temp(path: &Path) -> std::path::PathBuf { 371 use std::sync::atomic::{AtomicU64, Ordering}; 372 static COUNTER: AtomicU64 = AtomicU64::new(0); 373 let nonce = COUNTER.fetch_add(1, Ordering::Relaxed); 374 let stem = path 375 .file_name() 376 .and_then(|name| name.to_str()) 377 .unwrap_or("ssh_host_key"); 378 path.with_file_name(format!(".{stem}.{}.{nonce}.tmp", std::process::id())) 379} 380 381fn write_secret(path: &Path, bytes: &[u8]) -> std::io::Result<()> { 382 use std::io::Write; 383 let mut options = std::fs::OpenOptions::new(); 384 options.write(true).create_new(true); 385 #[cfg(unix)] 386 { 387 use std::os::unix::fs::OpenOptionsExt; 388 options.mode(0o600); 389 } 390 let mut file = options.open(path)?; 391 file.write_all(bytes)?; 392 file.sync_all() 393} 394 395struct EntropyRng; 396 397impl rand_core::TryRng for EntropyRng { 398 type Error = std::convert::Infallible; 399 400 fn try_next_u32(&mut self) -> Result<u32, Self::Error> { 401 let mut bytes = [0u8; 4]; 402 OsEntropy.fill(&mut bytes); 403 Ok(u32::from_le_bytes(bytes)) 404 } 405 406 fn try_next_u64(&mut self) -> Result<u64, Self::Error> { 407 let mut bytes = [0u8; 8]; 408 OsEntropy.fill(&mut bytes); 409 Ok(u64::from_le_bytes(bytes)) 410 } 411 412 fn try_fill_bytes(&mut self, dst: &mut [u8]) -> Result<(), Self::Error> { 413 OsEntropy.fill(dst); 414 Ok(()) 415 } 416} 417 418impl rand_core::TryCryptoRng for EntropyRng {}