1use std::ffi::OsString;
2use std::fs::{self, File, Metadata, OpenOptions, Permissions};
3use std::io;
4use std::mem::MaybeUninit;
5use std::os::fd::AsRawFd;
6use std::os::unix::fs::{FileTypeExt, MetadataExt, OpenOptionsExt, PermissionsExt};
7use std::path::{Path, PathBuf};
8use std::time::Duration;
9
10use tokio::net::{UnixListener, UnixStream};
11use tokio::time::{sleep, timeout};
12
13const CONNECT_RETRIES: usize = 100;
14const CONNECT_RETRY_DELAY: Duration = Duration::from_millis(20);
15const STALE_SOCKET_PROBE_TIMEOUT: Duration = Duration::from_millis(250);
16
17pub type LocalServerStream = UnixStream;
18pub type LocalClientStream = UnixStream;
19
20#[derive(Debug)]
21pub struct OwnerOnlyLocalListener {
22 listener: UnixListener,
23 endpoint: PathBuf,
24 identity: FileIdentity,
25 lock: EndpointLock,
26}
27
28impl OwnerOnlyLocalListener {
29 pub async fn bind(endpoint: impl AsRef<Path>) -> io::Result<Self> {
30 let owner_uid = current_euid();
31 let endpoint = validated_endpoint(endpoint.as_ref(), owner_uid)?;
32 let lock = EndpointLock::acquire(&endpoint, owner_uid)?;
33 remove_stale_socket(&endpoint, owner_uid, &lock).await?;
34
35 let listener = UnixListener::bind(&endpoint)?;
36 validate_listener(&listener)?;
37 let initial_metadata = match owned_socket_metadata(&endpoint, owner_uid) {
38 Ok(metadata) => metadata,
39 Err(error) => {
40 drop(listener);
41 return Err(error);
42 }
43 };
44 let initial_identity = FileIdentity::from_metadata(&initial_metadata);
45
46 if let Err(error) = fs::set_permissions(&endpoint, Permissions::from_mode(0o600)) {
47 drop(listener);
48 remove_socket_if_same(&endpoint, initial_identity, &lock);
49 return Err(error);
50 }
51
52 let metadata = match owned_socket_metadata(&endpoint, owner_uid) {
53 Ok(metadata)
54 if FileIdentity::from_metadata(&metadata) == initial_identity
55 && metadata.permissions().mode() & 0o7777 == 0o600 =>
56 {
57 metadata
58 }
59 Ok(_) => {
60 drop(listener);
61 remove_socket_if_same(&endpoint, initial_identity, &lock);
62 return Err(io::Error::new(
63 io::ErrorKind::PermissionDenied,
64 format!(
65 "local socket changed while securing {}",
66 endpoint.display()
67 ),
68 ));
69 }
70 Err(error) => {
71 drop(listener);
72 remove_socket_if_same(&endpoint, initial_identity, &lock);
73 return Err(error);
74 }
75 };
76
77 Ok(Self {
78 listener,
79 endpoint,
80 identity: FileIdentity::from_metadata(&metadata),
81 lock,
82 })
83 }
84
85 pub async fn accept(&mut self) -> io::Result<LocalServerStream> {
86 loop {
87 let (stream, _) = self.listener.accept().await?;
88 let peer_uid = stream.peer_cred()?.uid();
89 if trusted_peer_uid(peer_uid, self.identity.uid) {
90 return Ok(stream);
91 }
92 drop(stream);
93 }
94 }
95}
96
97impl Drop for OwnerOnlyLocalListener {
98 fn drop(&mut self) {
99 remove_socket_if_same(&self.endpoint, self.identity, &self.lock);
100 }
101}
102
103pub async fn connect_local_stream(endpoint: impl AsRef<Path>) -> io::Result<LocalClientStream> {
104 let owner_uid = current_euid();
105 let endpoint = validated_endpoint(endpoint.as_ref(), owner_uid)?;
106 let mut last_error = None;
107
108 for attempt in 0..CONNECT_RETRIES {
109 match UnixStream::connect(&endpoint).await {
110 Ok(stream) => {
111 let peer_uid = stream.peer_cred()?.uid();
112 if trusted_peer_uid(peer_uid, owner_uid) {
113 return Ok(stream);
114 }
115 return Err(io::Error::new(
116 io::ErrorKind::PermissionDenied,
117 format!(
118 "local socket peer for {} has untrusted uid {peer_uid}",
119 endpoint.display()
120 ),
121 ));
122 }
123 Err(error) if retryable_connect_error(&error) => {
124 let has_more_attempts = attempt + 1 < CONNECT_RETRIES;
125 last_error = Some(error);
126 if has_more_attempts {
127 sleep(CONNECT_RETRY_DELAY).await;
128 }
129 }
130 Err(error) => return Err(error),
131 }
132 }
133
134 Err(last_error.unwrap_or_else(|| {
135 io::Error::new(
136 io::ErrorKind::NotFound,
137 format!("local socket was not available at {}", endpoint.display()),
138 )
139 }))
140}
141
142#[derive(Clone, Copy, Debug, Eq, PartialEq)]
143struct FileIdentity {
144 device: u64,
145 inode: u64,
146 uid: u32,
147}
148
149impl FileIdentity {
150 fn from_metadata(metadata: &Metadata) -> Self {
151 Self {
152 device: metadata.dev(),
153 inode: metadata.ino(),
154 uid: metadata.uid(),
155 }
156 }
157}
158
159#[derive(Debug)]
160struct EndpointLock {
161 _file: File,
162}
163
164impl EndpointLock {
165 fn acquire(endpoint: &Path, owner_uid: u32) -> io::Result<Self> {
166 let path = endpoint_lock_path(endpoint);
167 let file = OpenOptions::new()
168 .read(true)
169 .write(true)
170 .create(true)
171 .mode(0o600)
172 .custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK)
173 .open(&path)?;
174 let metadata = file.metadata()?;
175 if !metadata.file_type().is_file() {
176 return Err(io::Error::new(
177 io::ErrorKind::InvalidInput,
178 format!("local socket lock is not a regular file: {}", path.display()),
179 ));
180 }
181 if metadata.uid() != owner_uid {
182 return Err(io::Error::new(
183 io::ErrorKind::PermissionDenied,
184 format!(
185 "local socket lock {} is owned by uid {}, expected {owner_uid}",
186 path.display(),
187 metadata.uid()
188 ),
189 ));
190 }
191 let identity = FileIdentity::from_metadata(&metadata);
192 let path_metadata = fs::symlink_metadata(&path)?;
193 if !path_metadata.file_type().is_file()
194 || FileIdentity::from_metadata(&path_metadata) != identity
195 {
196 return Err(io::Error::new(
197 io::ErrorKind::PermissionDenied,
198 format!("local socket lock path changed: {}", path.display()),
199 ));
200 }
201
202 let result = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
203 if result != 0 {
204 let error = io::Error::last_os_error();
205 let raw_error = error.raw_os_error();
206 if raw_error == Some(libc::EWOULDBLOCK) || raw_error == Some(libc::EAGAIN) {
207 return Err(io::Error::new(
208 io::ErrorKind::AddrInUse,
209 format!("local socket lock is already held: {}", path.display()),
210 ));
211 }
212 return Err(error);
213 }
214
215 file.set_permissions(Permissions::from_mode(0o600))?;
216 let secured_metadata = file.metadata()?;
217 let secured_path_metadata = fs::symlink_metadata(&path)?;
218 let secured_identity = FileIdentity::from_metadata(&secured_metadata);
219 if !secured_metadata.file_type().is_file()
220 || secured_metadata.uid() != owner_uid
221 || secured_metadata.permissions().mode() & 0o7777 != 0o600
222 || secured_identity != identity
223 || !secured_path_metadata.file_type().is_file()
224 || FileIdentity::from_metadata(&secured_path_metadata) != identity
225 {
226 return Err(io::Error::new(
227 io::ErrorKind::PermissionDenied,
228 format!("local socket lock is not securely fixed: {}", path.display()),
229 ));
230 }
231
232 Ok(Self { _file: file })
233 }
234}
235
236fn current_euid() -> u32 {
237 unsafe { libc::geteuid() }
238}
239
240fn trusted_peer_uid(peer_uid: u32, owner_uid: u32) -> bool {
244 peer_uid == owner_uid || peer_uid == 0
245}
246
247fn validated_endpoint(endpoint: &Path, owner_uid: u32) -> io::Result<PathBuf> {
248 if !endpoint.is_absolute() {
249 return Err(io::Error::new(
250 io::ErrorKind::InvalidInput,
251 format!("local socket endpoint must be absolute: {}", endpoint.display()),
252 ));
253 }
254 let file_name = endpoint.file_name().ok_or_else(|| {
255 io::Error::new(
256 io::ErrorKind::InvalidInput,
257 format!(
258 "local socket endpoint must end in a filename: {}",
259 endpoint.display()
260 ),
261 )
262 })?;
263 let parent = endpoint.parent().ok_or_else(|| {
264 io::Error::new(
265 io::ErrorKind::InvalidInput,
266 format!(
267 "local socket endpoint has no parent directory: {}",
268 endpoint.display()
269 ),
270 )
271 })?;
272 let parent = fs::canonicalize(parent)?;
273 let metadata = fs::symlink_metadata(&parent)?;
274 if !metadata.file_type().is_dir() {
275 return Err(io::Error::new(
276 io::ErrorKind::InvalidInput,
277 format!("local socket parent is not a directory: {}", parent.display()),
278 ));
279 }
280 if metadata.uid() != owner_uid {
281 return Err(io::Error::new(
282 io::ErrorKind::PermissionDenied,
283 format!(
284 "local socket parent {} is owned by uid {}, expected {owner_uid}",
285 parent.display(),
286 metadata.uid()
287 ),
288 ));
289 }
290 let mode = metadata.permissions().mode() & 0o7777;
291 if mode != 0o700 {
292 return Err(io::Error::new(
293 io::ErrorKind::PermissionDenied,
294 format!(
295 "local socket parent {} has mode {mode:04o}, expected 0700",
296 parent.display()
297 ),
298 ));
299 }
300
301 Ok(parent.join(file_name))
302}
303
304fn endpoint_lock_path(endpoint: &Path) -> PathBuf {
305 let mut lock_name = OsString::from(".");
306 lock_name.push(
307 endpoint
308 .file_name()
309 .expect("validated endpoint has a filename"),
310 );
311 lock_name.push(".gate4agent.lock");
312 endpoint.with_file_name(lock_name)
313}
314
315fn validate_listener(listener: &UnixListener) -> io::Result<()> {
316 let mut stat = MaybeUninit::<libc::stat>::uninit();
317 if unsafe { libc::fstat(listener.as_raw_fd(), stat.as_mut_ptr()) } != 0 {
318 return Err(io::Error::last_os_error());
319 }
320 let stat = unsafe { stat.assume_init() };
321 if stat.st_mode & libc::S_IFMT != libc::S_IFSOCK {
322 return Err(io::Error::new(
323 io::ErrorKind::PermissionDenied,
324 "bound listener file descriptor is not a Unix socket",
325 ));
326 }
327 Ok(())
328}
329
330async fn remove_stale_socket(
331 endpoint: &Path,
332 owner_uid: u32,
333 _lock: &EndpointLock,
334) -> io::Result<()> {
335 let metadata = match fs::symlink_metadata(endpoint) {
336 Ok(metadata) => metadata,
337 Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(()),
338 Err(error) => return Err(error),
339 };
340 if !metadata.file_type().is_socket() {
341 return Err(io::Error::new(
342 io::ErrorKind::AlreadyExists,
343 format!(
344 "refusing to replace non-socket endpoint {}",
345 endpoint.display()
346 ),
347 ));
348 }
349 if metadata.uid() != owner_uid {
350 return Err(io::Error::new(
351 io::ErrorKind::PermissionDenied,
352 format!(
353 "refusing to replace socket {} owned by uid {}",
354 endpoint.display(),
355 metadata.uid()
356 ),
357 ));
358 }
359 let original_identity = FileIdentity::from_metadata(&metadata);
360
361 match timeout(STALE_SOCKET_PROBE_TIMEOUT, UnixStream::connect(endpoint)).await {
362 Ok(Ok(stream)) => {
363 drop(stream);
364 return Err(live_socket_collision(endpoint));
365 }
366 Err(_) => return Err(live_socket_collision(endpoint)),
367 Ok(Err(error)) if error.kind() == io::ErrorKind::ConnectionRefused => {}
368 Ok(Err(error)) if error.kind() == io::ErrorKind::NotFound => return Ok(()),
369 Ok(Err(error)) => return Err(error),
370 }
371
372 let current_metadata = match fs::symlink_metadata(endpoint) {
373 Ok(metadata) => metadata,
374 Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(()),
375 Err(error) => return Err(error),
376 };
377 let unchanged_owned_socket = current_metadata.file_type().is_socket()
378 && current_metadata.uid() == owner_uid
379 && FileIdentity::from_metadata(¤t_metadata) == original_identity;
380 if !unchanged_owned_socket {
381 return Err(io::Error::new(
382 io::ErrorKind::AlreadyExists,
383 format!(
384 "local socket endpoint changed during stale probe: {}",
385 endpoint.display()
386 ),
387 ));
388 }
389
390 fs::remove_file(endpoint)
391}
392
393fn owned_socket_metadata(endpoint: &Path, owner_uid: u32) -> io::Result<Metadata> {
394 let metadata = fs::symlink_metadata(endpoint)?;
395 if !metadata.file_type().is_socket() || metadata.uid() != owner_uid {
396 return Err(io::Error::new(
397 io::ErrorKind::PermissionDenied,
398 format!(
399 "bound endpoint is not an owned socket: {}",
400 endpoint.display()
401 ),
402 ));
403 }
404 Ok(metadata)
405}
406
407fn remove_socket_if_same(endpoint: &Path, identity: FileIdentity, _lock: &EndpointLock) {
408 let Ok(metadata) = fs::symlink_metadata(endpoint) else {
409 return;
410 };
411 if metadata.file_type().is_socket()
412 && FileIdentity::from_metadata(&metadata) == identity
413 {
414 let _ = fs::remove_file(endpoint);
415 }
416}
417
418fn retryable_connect_error(error: &io::Error) -> bool {
419 matches!(
420 error.kind(),
421 io::ErrorKind::NotFound | io::ErrorKind::ConnectionRefused
422 )
423}
424
425fn live_socket_collision(endpoint: &Path) -> io::Error {
426 io::Error::new(
427 io::ErrorKind::AddrInUse,
428 format!("live local socket already exists at {}", endpoint.display()),
429 )
430}
431
432#[cfg(test)]
433mod tests {
434 use super::*;
435 use std::fs::File;
436 use std::os::unix::fs::symlink;
437 use std::sync::atomic::{AtomicU64, Ordering};
438 use tokio::io::{AsyncReadExt, AsyncWriteExt};
439
440 static TEST_SEQUENCE: AtomicU64 = AtomicU64::new(0);
441
442 struct TestDirectory {
443 path: PathBuf,
444 }
445
446 impl TestDirectory {
447 fn new() -> Self {
448 let base = fs::canonicalize(std::env::temp_dir()).expect("canonical temp directory");
449 for _ in 0..100 {
450 let sequence = TEST_SEQUENCE.fetch_add(1, Ordering::Relaxed);
451 let path = base.join(format!("g{:x}-{sequence:x}", std::process::id()));
452 match fs::create_dir(&path) {
453 Ok(()) => {
454 fs::set_permissions(&path, Permissions::from_mode(0o700))
455 .expect("secure test directory");
456 return Self { path };
457 }
458 Err(error) if error.kind() == io::ErrorKind::AlreadyExists => continue,
459 Err(error) => panic!("create test directory: {error}"),
460 }
461 }
462 panic!("allocate unique short test directory")
463 }
464
465 fn endpoint(&self, name: &str) -> PathBuf {
466 self.path.join(name)
467 }
468 }
469
470 impl Drop for TestDirectory {
471 fn drop(&mut self) {
472 if let Ok(entries) = fs::read_dir(&self.path) {
473 for entry in entries.flatten() {
474 match entry.file_type() {
475 Ok(file_type) if file_type.is_dir() => {
476 let _ = fs::remove_dir(entry.path());
477 }
478 _ => {
479 let _ = fs::remove_file(entry.path());
480 }
481 }
482 }
483 }
484 let _ = fs::remove_dir(&self.path);
485 }
486 }
487
488 #[tokio::test(flavor = "current_thread")]
489 async fn owner_only_socket_has_secure_modes_and_roundtrips() {
490 let directory = TestDirectory::new();
491 let endpoint = directory.endpoint("n.sock");
492 let lock_path = endpoint_lock_path(&endpoint);
493 assert_eq!(
494 lock_path.file_name(),
495 Some(std::ffi::OsStr::new(".n.sock.gate4agent.lock"))
496 );
497 let mut listener = OwnerOnlyLocalListener::bind(&endpoint)
498 .await
499 .expect("bind owner-only socket");
500
501 assert_eq!(
502 fs::metadata(&directory.path)
503 .expect("stat parent")
504 .permissions()
505 .mode()
506 & 0o7777,
507 0o700
508 );
509 assert_eq!(
510 fs::symlink_metadata(&endpoint)
511 .expect("stat socket")
512 .permissions()
513 .mode()
514 & 0o7777,
515 0o600
516 );
517 let lock_metadata = fs::symlink_metadata(&lock_path).expect("stat endpoint lock");
518 assert!(lock_metadata.file_type().is_file());
519 assert_eq!(lock_metadata.uid(), current_euid());
520 assert_eq!(lock_metadata.permissions().mode() & 0o7777, 0o600);
521
522 let server = tokio::spawn(async move {
523 let mut stream = listener.accept().await.expect("accept owner client");
524 let mut request = [0_u8; 4];
525 stream.read_exact(&mut request).await.expect("read request");
526 assert_eq!(&request, b"ping");
527 stream.write_all(b"pong").await.expect("write response");
528 });
529 let mut client = connect_local_stream(&endpoint)
530 .await
531 .expect("connect owner client");
532 client.write_all(b"ping").await.expect("write request");
533 let mut response = [0_u8; 4];
534 client.read_exact(&mut response).await.expect("read response");
535 assert_eq!(&response, b"pong");
536 server.await.expect("join server");
537 assert!(lock_path.is_file(), "listener drop must retain lock file");
538 }
539
540 #[tokio::test(flavor = "current_thread")]
541 async fn cancelled_accept_keeps_listener_usable() {
542 let directory = TestDirectory::new();
543 let endpoint = directory.endpoint("n.sock");
544 let mut listener = OwnerOnlyLocalListener::bind(&endpoint)
545 .await
546 .expect("bind owner-only socket");
547
548 timeout(Duration::from_millis(10), listener.accept())
549 .await
550 .expect_err("accept remains pending without a client");
551 let client = connect_local_stream(&endpoint)
552 .await
553 .expect("connect after cancelled accept");
554 let server = listener.accept().await.expect("accept after cancellation");
555 drop(client);
556 drop(server);
557 }
558
559 #[tokio::test(flavor = "current_thread")]
560 async fn second_listener_lock_collision_does_not_remove_live_socket() {
561 let directory = TestDirectory::new();
562 let endpoint = directory.endpoint("n.sock");
563 let listener = OwnerOnlyLocalListener::bind(&endpoint)
564 .await
565 .expect("bind first listener");
566 let identity = FileIdentity::from_metadata(
567 &fs::symlink_metadata(&endpoint).expect("stat first socket"),
568 );
569 let lock_path = endpoint_lock_path(&endpoint);
570 let lock_identity = FileIdentity::from_metadata(
571 &fs::symlink_metadata(&lock_path).expect("stat held endpoint lock"),
572 );
573
574 let error = OwnerOnlyLocalListener::bind(&endpoint)
575 .await
576 .expect_err("reject live collision");
577 assert_eq!(error.kind(), io::ErrorKind::AddrInUse);
578 assert_eq!(
579 FileIdentity::from_metadata(
580 &fs::symlink_metadata(&endpoint).expect("live socket remains")
581 ),
582 identity
583 );
584 assert_eq!(
585 FileIdentity::from_metadata(
586 &fs::symlink_metadata(&lock_path).expect("held endpoint lock remains")
587 ),
588 lock_identity
589 );
590 drop(listener);
591 }
592
593 #[tokio::test(flavor = "current_thread")]
594 async fn endpoint_specific_locks_allow_two_listeners_in_one_parent() {
595 let directory = TestDirectory::new();
596 let node_endpoint = directory.endpoint("n.sock");
597 let c2_endpoint = directory.endpoint("c.sock");
598 assert_ne!(
599 endpoint_lock_path(&node_endpoint),
600 endpoint_lock_path(&c2_endpoint)
601 );
602
603 let node_listener = OwnerOnlyLocalListener::bind(&node_endpoint)
604 .await
605 .expect("bind node listener");
606 let c2_listener = OwnerOnlyLocalListener::bind(&c2_endpoint)
607 .await
608 .expect("bind C2 listener in same parent");
609 assert!(node_endpoint.exists());
610 assert!(c2_endpoint.exists());
611 drop(c2_listener);
612 drop(node_listener);
613 }
614
615 #[tokio::test(flavor = "current_thread")]
616 async fn safely_recovers_owned_stale_socket() {
617 let directory = TestDirectory::new();
618 let endpoint = directory.endpoint("n.sock");
619 let stale = std::os::unix::net::UnixListener::bind(&endpoint)
620 .expect("bind stale socket fixture");
621 drop(stale);
622 let mut listener = OwnerOnlyLocalListener::bind(&endpoint)
623 .await
624 .expect("recover stale socket");
625 let client = connect_local_stream(&endpoint)
626 .await
627 .expect("connect to recovered socket");
628 let server = listener
629 .accept()
630 .await
631 .expect("accept on recovered socket");
632 drop(client);
633 drop(server);
634 drop(listener);
635 }
636
637 #[tokio::test(flavor = "current_thread")]
638 async fn refuses_regular_file_and_symlink_endpoints() {
639 let directory = TestDirectory::new();
640 let regular = directory.endpoint("regular");
641 File::create(®ular).expect("create regular endpoint fixture");
642 let regular_error = OwnerOnlyLocalListener::bind(®ular)
643 .await
644 .expect_err("refuse regular file");
645 assert_eq!(regular_error.kind(), io::ErrorKind::AlreadyExists);
646 assert!(regular.is_file());
647
648 let target = directory.endpoint("target");
649 File::create(&target).expect("create symlink target");
650 let link = directory.endpoint("link.sock");
651 symlink(&target, &link).expect("create endpoint symlink");
652 let symlink_error = OwnerOnlyLocalListener::bind(&link)
653 .await
654 .expect_err("refuse endpoint symlink");
655 assert_eq!(symlink_error.kind(), io::ErrorKind::AlreadyExists);
656 assert!(
657 fs::symlink_metadata(&link)
658 .expect("symlink remains")
659 .file_type()
660 .is_symlink()
661 );
662 assert!(target.is_file());
663 }
664
665 #[tokio::test(flavor = "current_thread")]
666 async fn rejects_relative_endpoint_and_insecure_parent_mode() {
667 let relative_error = OwnerOnlyLocalListener::bind(Path::new("n.sock"))
668 .await
669 .expect_err("reject relative endpoint");
670 assert_eq!(relative_error.kind(), io::ErrorKind::InvalidInput);
671
672 let directory = TestDirectory::new();
673 fs::set_permissions(&directory.path, Permissions::from_mode(0o750))
674 .expect("make parent insecure");
675 let endpoint = directory.endpoint("n.sock");
676 let mode_error = OwnerOnlyLocalListener::bind(&endpoint)
677 .await
678 .expect_err("reject insecure parent mode");
679 assert_eq!(mode_error.kind(), io::ErrorKind::PermissionDenied);
680 assert!(!endpoint.exists());
681 fs::set_permissions(&directory.path, Permissions::from_mode(0o700))
682 .expect("restore parent mode for cleanup");
683 }
684
685 #[tokio::test(flavor = "current_thread")]
686 async fn refuses_symlink_and_nonregular_endpoint_locks() {
687 let directory = TestDirectory::new();
688
689 let symlink_endpoint = directory.endpoint("s.sock");
690 let symlink_lock = endpoint_lock_path(&symlink_endpoint);
691 let target = directory.endpoint("lock-target");
692 File::create(&target).expect("create lock symlink target");
693 symlink(&target, &symlink_lock).expect("create malicious lock symlink");
694 OwnerOnlyLocalListener::bind(&symlink_endpoint)
695 .await
696 .expect_err("refuse symlink lock");
697 assert!(
698 fs::symlink_metadata(&symlink_lock)
699 .expect("malicious lock symlink remains")
700 .file_type()
701 .is_symlink()
702 );
703 assert!(!symlink_endpoint.exists());
704
705 let directory_endpoint = directory.endpoint("d.sock");
706 let directory_lock = endpoint_lock_path(&directory_endpoint);
707 fs::create_dir(&directory_lock).expect("create nonregular lock fixture");
708 OwnerOnlyLocalListener::bind(&directory_endpoint)
709 .await
710 .expect_err("refuse nonregular lock");
711 assert!(directory_lock.is_dir());
712 assert!(!directory_endpoint.exists());
713 }
714
715 #[tokio::test(flavor = "current_thread")]
716 async fn drop_preserves_replacement_socket_inode() {
717 let directory = TestDirectory::new();
718 let endpoint = directory.endpoint("n.sock");
719 let lock_path = endpoint_lock_path(&endpoint);
720 let listener = OwnerOnlyLocalListener::bind(&endpoint)
721 .await
722 .expect("bind managed socket");
723 fs::remove_file(&endpoint).expect("unlink managed socket fixture");
724 let replacement = std::os::unix::net::UnixListener::bind(&endpoint)
725 .expect("bind replacement socket");
726 let replacement_identity = FileIdentity::from_metadata(
727 &fs::symlink_metadata(&endpoint).expect("stat replacement socket"),
728 );
729
730 let collision = OwnerOnlyLocalListener::bind(&endpoint)
731 .await
732 .expect_err("held endpoint lock blocks bind after path replacement");
733 assert_eq!(collision.kind(), io::ErrorKind::AddrInUse);
734 assert_eq!(
735 FileIdentity::from_metadata(
736 &fs::symlink_metadata(&endpoint).expect("replacement survives collision")
737 ),
738 replacement_identity
739 );
740
741 drop(listener);
742 assert_eq!(
743 FileIdentity::from_metadata(
744 &fs::symlink_metadata(&endpoint).expect("replacement socket remains")
745 ),
746 replacement_identity
747 );
748 assert!(lock_path.is_file(), "replacement race must retain lock file");
749 drop(replacement);
750 }
751
752 #[tokio::test(flavor = "current_thread")]
753 async fn drop_cleans_socket_and_allows_rebind() {
754 let directory = TestDirectory::new();
755 let endpoint = directory.endpoint("n.sock");
756 let lock_path = endpoint_lock_path(&endpoint);
757 let listener = OwnerOnlyLocalListener::bind(&endpoint)
758 .await
759 .expect("bind first socket");
760 assert!(endpoint.exists());
761 drop(listener);
762 assert!(!endpoint.exists());
763 let lock_identity = FileIdentity::from_metadata(
764 &fs::symlink_metadata(&lock_path).expect("lock remains after cleanup"),
765 );
766
767 let rebound = OwnerOnlyLocalListener::bind(&endpoint)
768 .await
769 .expect("rebind after cleanup");
770 assert!(endpoint.exists());
771 drop(rebound);
772 assert!(!endpoint.exists());
773 assert_eq!(
774 FileIdentity::from_metadata(
775 &fs::symlink_metadata(&lock_path).expect("same lock remains after rebind")
776 ),
777 lock_identity
778 );
779 }
780}