embassy-rs/embassy · error
read error
Error message
read error: {:?} What it means
The tuntap device panicked because reading a packet from the host's TUN device file failed with an error other than WouldBlock (WouldBlock is treated as 'no packet yet' and returns None). Since the smoltcp Device trait's receive() cannot return an error, any real I/O error (device closed, interface gone down, permission revoked) becomes a hard panic in the driver task.
Solutions
- Check kernel logs / errno: run with strace or add logging to identify the exact io::ErrorKind before it panics
- Ensure the tun interface exists for the process lifetime (do not let scripts or NetworkManager delete it); create it with 'ip tuntap add ... mode tun' owned by the running user
- Verify /dev/net/tun is available and permitted (inside containers: --device /dev/net/tun --cap-add NET_ADMIN)
- Shut down the network task before closing the tun fd, or keep the fd open for the process lifetime
- Patch the driver locally to log-and-continue or return None on non-fatal errors instead of panicking, if graceful degradation is required
Example fix
// before
Err(e) => panic!("read error: {:?}", e),
// after (local patch)
Err(e) => {
warn!("tun read error: {:?}", e);
None
} Defensive patterns
Strategy: try-catch
Validate before calling
// preflight: ensure tun interface and /dev/net/tun are usable before starting
// ip tuntap add mode tun dev tun0 user $USER && ip link set tun0 up
// test -c /dev/net/tun || { echo 'no /dev/net/tun'; exit 1; } Type guard
// Rust: distinguish benign vs fatal errors before they reach the driver
fn is_benign(e: &std::io::Error) -> bool {
matches!(e.kind(), std::io::ErrorKind::WouldBlock | std::io::ErrorKind::Interrupted)
} Try / catch
// run the network task under catch_unwind and restart cleanly
let result = std::panic::catch_unwind(AssertUnwindSafe(|| network_task()));
if result.is_err() {
error!("network task panicked; re-creating tun device and restarting");
} Prevention
- Create the tun interface with an owning user so runtime permission issues don't occur
- Prevent NetworkManager/scripts from deleting the interface while the app runs
- In containers, pass --device /dev/net/tun --cap-add NET_ADMIN
- Keep the tun fd open until the network task fully shuts down
- Run under catch_unwind with restart logic for resilience
When it happens
Trigger: The TUN file descriptor read fails with a non-WouldBlock error during receive(): the /dev/net/tun fd was closed, the tun interface was deleted (ip link del), the process lost permission to the device, or an unexpected errno (e.g., EBADF, ENODEV) occurred.
Common situations: Running the app without CAP_NET_ADMIN or outside the expected user/group so the TUN device misbehaves; a network manager or cleanup script deleting the tun interface at runtime; container restrictions blocking /dev/net/tun reads; app shutdown ordering closing the fd while the network task is still polling.
Understand the failure class
Background: "failed to read file", EACCES, ENOENT and "could not read <path>" errors: when a program can't read a file from disk — this error's family across 49 libraries.
Related errors
- transmit error
- Passphrase is too short or too long
- Boot prepare error
- Boot prepare error
- Boot prepare error
AI-assisted analysis of embassy-rs/embassy@463a07b963 (2026-09-10).
Data as JSON: /api/errors/8a4838dfb23e69d4.
Report an issue: GitHub.
Appendix: source
Thrown at embassy-net-tuntap/src/lib.rs:184
}
fn register_waker(&mut self, waker: &Waker) -> Result<(), NotSupported> {
let mut cx = Context::from_waker(waker);
let _ = self.device.poll_readable(&mut cx);
Ok(())
}
fn receive(&mut self) -> Option<PacketBuf> {
let mut buf = PacketBuf::try_new()?;
let mtu = self.device.get_ref().mtu.min(buf.capacity());
buf.set_len(mtu);
match unsafe { self.device.get_mut() }.read(&mut buf) {
Ok(n) => {
buf.set_len(n);
Some(buf)
}
Err(e) if e.kind() == io::ErrorKind::WouldBlock => None,
Err(e) => panic!("read error: {:?}", e),
}
}
fn can_transmit(&mut self) -> bool {
true
}
fn transmit(&mut self, buf: PacketBuf) -> Result<(), PacketBuf> {
// todo handle WouldBlock with async
match unsafe { self.device.get_mut() }.write(&buf) {
Ok(_) => {}
Err(e) if e.kind() == io::ErrorKind::WouldBlock => info!("transmit WouldBlock"),
Err(e) => panic!("transmit error: {:?}", e),
}
Ok(())
}
}
View on GitHub (pinned to 463a07b963)