diff --git a/crates/zotero/src/peer.rs b/crates/zotero/src/peer.rs index 00173c5c4..a9c2ccbe4 100644 --- a/crates/zotero/src/peer.rs +++ b/crates/zotero/src/peer.rs @@ -3,8 +3,9 @@ //! only connects to this computer (a loopback address), and on Linux checks that the program //! listening there runs as the same user, from the kernel's socket table (`/proc/net/tcp`): //! another account on a shared machine that took the port while Zotero wasn't running is -//! refused. Other systems have no such table to read without OS calls this crate doesn't make; -//! there the loopback rule is the guard (see `docs/zotero.md`). +//! refused. A listener the table doesn't show (WSL2 mirrored networking with Zotero on Windows, +//! a local proxy, a sandbox) leaves only the loopback rule, as on other systems, which have no +//! such table to read without OS calls this crate doesn't make (see `docs/zotero.md`). use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, TcpStream}; @@ -50,9 +51,9 @@ fn owner_check(_local: SocketAddr, _peer: SocketAddr) -> Result<(), ZoteroError> Ok(()) } -/// From a `/proc/net/tcp`-format socket table: whether the socket at `peer` connected to -/// `local` (the listener's side) belongs to the same user as ours. A peer that can't be found -/// is refused too: its owner can't be established. +/// From a `/proc/net/tcp`-format socket table: refuse when the socket at `peer` connected to +/// `local` (the listener's side) belongs to another user than ours. When either socket isn't +/// listed (or the table couldn't be read) the owner is unknown and the loopback rule decides. #[cfg_attr(not(any(target_os = "linux", target_os = "android")), allow(dead_code))] pub(crate) fn same_owner(table: &str, local: SocketAddr, peer: SocketAddr) -> Result<(), ZoteroError> { let key = |a: SocketAddr| (a.ip().to_canonical(), a.port()); @@ -76,7 +77,10 @@ pub(crate) fn same_owner(table: &str, local: SocketAddr, peer: SocketAddr) -> Re (Some(_), Some(b)) => { Err(ZoteroError::Refused(format!("the program on port {} runs as another user (uid {b}), so it isn't your Zotero", peer.1))) } - _ => Err(ZoteroError::Refused(format!("WordCraft couldn't tell which user's program is on port {}", peer.1))), + _ => { + log::debug!("Zotero: port {} isn't in this system's socket table; trusting the loopback address alone", peer.1); + Ok(()) + } } } @@ -142,15 +146,22 @@ mod tests { assert_eq!(same_owner(&same, local, peer), Ok(())); let other = format!("{HEADER}{}{}", row(local, peer, 1000), row(peer, local, 1001)); assert!(matches!(same_owner(&other, local, peer), Err(ZoteroError::Refused(e)) if e.contains("another user"))); - // Not found (or the table couldn't be read): the owner can't be established. - let missing = format!("{HEADER}{}", row(local, peer, 1000)); - assert!(matches!(same_owner(&missing, local, peer), Err(ZoteroError::Refused(_)))); - assert!(matches!(same_owner("", local, peer), Err(ZoteroError::Refused(_)))); // Junk lines are skipped. let junk = format!("{HEADER}garbage\n 1: zz:zz yy:yy 01 x x x x\n{}{}", row(local, peer, 7), row(peer, local, 7)); assert_eq!(same_owner(&junk, local, peer), Ok(())); } + /// WSL2 mirrored networking (Zotero on Windows), a local proxy or a sandbox: the listener + /// isn't in our table, so the loopback rule decides, as on other systems. + #[test] + fn an_unlisted_listener_falls_back_to_the_loopback_rule() { + let (local, peer) = addrs(); + let missing = format!("{HEADER}{}", row(local, peer, 1000)); + assert_eq!(same_owner(&missing, local, peer), Ok(())); + // The table couldn't be read. + assert_eq!(same_owner("", local, peer), Ok(())); + } + #[test] fn a_dual_stack_listener_is_matched_through_its_mapped_address() { let (local, peer) = addrs(); diff --git a/docs/zotero.md b/docs/zotero.md index ed486b4ff..601749d59 100644 --- a/docs/zotero.md +++ b/docs/zotero.md @@ -70,13 +70,15 @@ program can read the citations and edit the open document. WordCraft limits who is refused before connecting. - On Linux it checks, in the kernel's socket table (`/proc/net/tcp`, `/proc/net/tcp6`), that the program listening on the port runs as the same user as WordCraft, before sending anything. If - another account on a shared machine took the port while Zotero wasn't running, or the owner - can't be established (the table can't be read), WordCraft refuses with an error and the - document isn't touched. + another account on a shared machine took the port while Zotero wasn't running, WordCraft + refuses with an error and the document isn't touched. When the table doesn't show the + listener (WSL2 mirrored networking with Zotero running on Windows, a local proxy, a sandbox) + or can't be read, its owner is unknown and only the loopback rule applies, as below. - Remaining risk: macOS, Windows and the BSDs have no such table to read without OS calls - WordCraft doesn't make (it uses no `unsafe` code), so there only the loopback rule applies: - on a computer shared with other accounts, a program another user leaves on port 23116 while - Zotero is closed would be taken for Zotero. A program running as you is trusted on every + WordCraft doesn't make (it uses no `unsafe` code), so there only the loopback rule applies + (and on Linux when the listener isn't in the table): on a computer shared with other + accounts, a program another user leaves on port 23116 while Zotero is closed would be taken + for Zotero. A program running as you is trusted on every system; it could change your documents anyway. - `wordcraft-cli zotero` prints Zotero's messages (and `--trace` lines) with control characters, line breaks and backslashes shown as escapes (`\n`, `\u{1b}`, `\\`), so a peer