Skip to content

Commit 44adb43

Browse files
authored
feat: ESP32 support (#116)
## Description Adds esp32 support and CI ## Breaking Changes None ## Notes & open questions Note: the posix_minimal impl for now just adds stubs that don't do nothing whatsoever, which works fine for esp32. We could and should add a poll based simple implementation later, but maybe not in this PR. I got a WIP PR here: #117, but still need to thoroughly check it. But just this one is all that is needed to get it to work on esp32. The actual interface enumeration is just nice to have and can come later. ## Change checklist - [x] Self-review. - [ ] Documentation updates following the [style guide](https://rust-lang.github.io/rfcs/1574-more-api-documentation-conventions.html#appendix-a-full-conventions-text), if relevant. - [x] Tests if relevant. - [x] All breaking changes documented.
1 parent aea20a2 commit 44adb43

11 files changed

Lines changed: 233 additions & 10 deletions

File tree

.github/workflows/ci.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,23 @@ jobs:
151151
env:
152152
RUST_LOG: ${{ runner.debug && 'TRACE' || 'DEBUG' }}
153153

154+
esp32_check:
155+
name: ESP32-C3 build check
156+
if: "github.event_name != 'pull_request' || ! contains(github.event.pull_request.labels.*.name, 'flaky-test')"
157+
timeout-minutes: 30
158+
runs-on: ubuntu-latest
159+
env:
160+
RUSTC_WRAPPER: "sccache"
161+
SCCACHE_GHA_ENABLED: "on"
162+
steps:
163+
- uses: actions/checkout@v6
164+
- uses: dtolnay/rust-toolchain@nightly
165+
with:
166+
components: rust-src
167+
- uses: mozilla-actions/sccache-action@v0.0.9
168+
- name: Check workspace for ESP32-C3
169+
run: cargo check -Z build-std=std,panic_abort --target riscv32imc-esp-espidf --workspace --no-default-features
170+
154171
wasm_test:
155172
name: Build & test wasm32 for browsers
156173
runs-on: ubuntu-latest

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

netwatch/Cargo.toml

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,19 +34,18 @@ tracing = "0.1"
3434

3535
# non-browser dependencies
3636
[target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dependencies]
37+
ipnet = "2"
3738
noq-udp = "=1.0.0-rc.1"
3839
libc = "0.2.139"
39-
netdev = "0.44.0"
4040
socket2 = { version = "0.6", features = ["all"] }
41+
tokio = { version = "1", features = ["rt", "net"] }
42+
43+
# netdev is available on all non-embedded, non-wasm platforms
44+
[target.'cfg(not(any(target_os = "espidf", all(target_family = "wasm", target_os = "unknown"))))'.dependencies]
45+
netdev = "0.44.0"
4146
tokio = { version = "1", features = [
42-
"io-util",
43-
"macros",
44-
"sync",
45-
"rt",
46-
"net",
4747
"fs",
4848
"io-std",
49-
"time",
5049
] }
5150

5251
[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies]

netwatch/build.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,7 @@ fn main() {
55
cfg_aliases! {
66
// Convenience aliases
77
wasm_browser: { all(target_family = "wasm", target_os = "unknown") },
8+
// Limited POSIX platforms (not wasm)
9+
posix_minimal: { target_os = "espidf" },
810
}
911
}

netwatch/src/interfaces.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ mod linux;
1717
#[cfg(target_os = "windows")]
1818
mod windows;
1919

20+
pub(crate) use ipnet::{Ipv4Net, Ipv6Net};
2021
pub use netdev::interface::ipv6_addr_flags::Ipv6AddrFlags;
21-
pub(crate) use netdev::ipnet::{Ipv4Net, Ipv6Net};
2222

2323
#[cfg(any(
2424
target_os = "freebsd",
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
//! Fallback interfaces implementation for platforms without `netdev`.
2+
//! Provides stub types — no network interface enumeration available.
3+
4+
use std::{collections::HashMap, fmt, net::IpAddr};
5+
6+
pub(crate) use ipnet::{Ipv4Net, Ipv6Net};
7+
use n0_future::time::Instant;
8+
9+
use crate::ip::LocalAddresses;
10+
11+
/// Represents a network interface (stub).
12+
#[derive(Clone, Debug, PartialEq, Eq)]
13+
pub struct Interface;
14+
15+
impl fmt::Display for Interface {
16+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
17+
write!(f, "unknown")
18+
}
19+
}
20+
21+
impl Interface {
22+
/// A list of all ip addresses of this interface.
23+
pub fn addrs(&self) -> impl Iterator<Item = IpNet> + '_ {
24+
std::iter::empty()
25+
}
26+
}
27+
28+
/// State flags for a single IPv6 address.
29+
///
30+
/// Hand-kept mirror of netdev's `Ipv6AddrFlags`, so the `interfaces` API is
31+
/// identical on platforms built without `netdev` (e.g. esp-idf). All fields
32+
/// default to `false` when the platform does not provide the information.
33+
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
34+
pub struct Ipv6AddrFlags {
35+
/// Preferred lifetime expired; should not be used for new connections.
36+
pub deprecated: bool,
37+
/// Privacy address ([RFC 4941](https://datatracker.ietf.org/doc/html/rfc4941)).
38+
pub temporary: bool,
39+
/// Undergoing duplicate address detection.
40+
pub tentative: bool,
41+
/// Duplicate address detection failed.
42+
pub duplicated: bool,
43+
/// Manually configured, not from SLAAC.
44+
pub permanent: bool,
45+
}
46+
47+
/// Structure of an IP network, either IPv4 or IPv6.
48+
///
49+
/// The shape mirrors the `netdev`-based `IpNet` so downstream code is
50+
/// platform-agnostic.
51+
#[derive(Clone, Debug)]
52+
pub enum IpNet {
53+
/// Structure of IPv4 Network.
54+
V4(Ipv4Net),
55+
/// Structure of IPv6 Network.
56+
V6 {
57+
/// The actual network address.
58+
net: Ipv6Net,
59+
/// IPv6 scope ID
60+
scope_id: u32,
61+
/// IPv6 address flags.
62+
flags: Ipv6AddrFlags,
63+
},
64+
}
65+
66+
impl PartialEq for IpNet {
67+
fn eq(&self, other: &Self) -> bool {
68+
match (self, other) {
69+
(IpNet::V4(a), IpNet::V4(b)) => {
70+
a.addr() == b.addr()
71+
&& a.prefix_len() == b.prefix_len()
72+
&& a.netmask() == b.netmask()
73+
}
74+
(
75+
IpNet::V6 {
76+
net: net_a,
77+
scope_id: scope_id_a,
78+
flags: flags_a,
79+
},
80+
IpNet::V6 {
81+
net: net_b,
82+
scope_id: scope_id_b,
83+
flags: flags_b,
84+
},
85+
) => {
86+
net_a.addr() == net_b.addr()
87+
&& net_a.prefix_len() == net_b.prefix_len()
88+
&& net_a.netmask() == net_b.netmask()
89+
&& scope_id_a == scope_id_b
90+
&& flags_a == flags_b
91+
}
92+
_ => false,
93+
}
94+
}
95+
}
96+
impl Eq for IpNet {}
97+
98+
impl IpNet {
99+
/// The IP address of this structure.
100+
pub fn addr(&self) -> IpAddr {
101+
match self {
102+
IpNet::V4(a) => IpAddr::V4(a.addr()),
103+
IpNet::V6 { net, .. } => IpAddr::V6(net.addr()),
104+
}
105+
}
106+
}
107+
108+
/// The router/gateway of the local network (stub — always returns `None`).
109+
#[derive(Debug, Clone)]
110+
pub struct HomeRouter {
111+
/// Ip of the router.
112+
pub gateway: IpAddr,
113+
/// Our local Ip if known.
114+
pub my_ip: Option<IpAddr>,
115+
}
116+
117+
impl HomeRouter {
118+
/// Returns `None` — no gateway discovery available on this platform.
119+
pub fn new() -> Option<Self> {
120+
None
121+
}
122+
}
123+
124+
/// Intended to store the state of the machine's network interfaces.
125+
#[derive(Debug, PartialEq, Eq, Clone)]
126+
pub struct State {
127+
/// Maps from an interface name to interface.
128+
pub interfaces: HashMap<String, Interface>,
129+
/// List of machine's local IP addresses.
130+
pub local_addresses: LocalAddresses,
131+
/// Whether this machine has IPv6 connectivity.
132+
pub have_v6: bool,
133+
/// Whether the machine has IPv4 connectivity.
134+
pub have_v4: bool,
135+
/// Whether the current network interface is considered "expensive".
136+
pub(crate) is_expensive: bool,
137+
/// The interface name for the machine's default route.
138+
pub default_route_interface: Option<String>,
139+
/// Monotonic timestamp, when an unsuspend was detected.
140+
pub last_unsuspend: Option<Instant>,
141+
}
142+
143+
impl fmt::Display for State {
144+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145+
write!(f, "fallback(no interfaces)")
146+
}
147+
}
148+
149+
impl State {
150+
/// Returns a default empty state (no interface enumeration on this platform).
151+
pub async fn new() -> Self {
152+
State {
153+
interfaces: HashMap::new(),
154+
local_addresses: LocalAddresses::default(),
155+
have_v6: false,
156+
have_v4: true,
157+
is_expensive: false,
158+
default_route_interface: None,
159+
last_unsuspend: None,
160+
}
161+
}
162+
163+
/// Is this a major change compared to the `old` one?
164+
pub fn is_major_change(&self, old: &State) -> bool {
165+
self != old
166+
}
167+
}

netwatch/src/ip_posix_minimal.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
//! IP address related utilities — fallback for platforms without `netdev`.
2+
3+
/// List of machine's IP addresses (stub).
4+
#[derive(Debug, Clone, PartialEq, Eq, Default)]
5+
pub struct LocalAddresses {
6+
/// Loopback addresses.
7+
pub loopback: Vec<std::net::IpAddr>,
8+
/// Regular addresses.
9+
pub regular: Vec<std::net::IpAddr>,
10+
}

netwatch/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
//! Networking related utilities
22
33
#[cfg_attr(wasm_browser, path = "interfaces/wasm_browser.rs")]
4+
#[cfg_attr(posix_minimal, path = "interfaces/posix_minimal.rs")]
45
pub mod interfaces;
6+
#[cfg_attr(posix_minimal, path = "ip_posix_minimal.rs")]
57
pub mod ip;
68
mod ip_family;
79
pub mod netmon;

netwatch/src/netmon.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@ mod android;
1818
mod bsd;
1919
#[cfg(target_os = "linux")]
2020
mod linux;
21+
#[cfg(posix_minimal)]
22+
mod posix_minimal;
2123
#[cfg(wasm_browser)]
2224
mod wasm_browser;
2325
#[cfg(target_os = "windows")]
2426
mod windows;
2527

26-
#[cfg(not(wasm_browser))]
28+
#[cfg(not(any(posix_minimal, wasm_browser)))]
2729
pub(crate) use self::actor::is_interesting_interface;
2830
use self::actor::{Actor, ActorMessage};
2931
pub use crate::interfaces::State;

netwatch/src/netmon/actor.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use n0_future::time::{self, Duration, Instant};
22
use n0_watcher::Watchable;
33
pub(super) use os::Error;
44
use os::RouteMonitor;
5-
#[cfg(not(wasm_browser))]
5+
#[cfg(not(any(posix_minimal, wasm_browser)))]
66
pub(crate) use os::is_interesting_interface;
77
use tokio::sync::mpsc;
88
use tracing::{debug, trace};
@@ -19,6 +19,8 @@ use super::android as os;
1919
use super::bsd as os;
2020
#[cfg(target_os = "linux")]
2121
use super::linux as os;
22+
#[cfg(posix_minimal)]
23+
use super::posix_minimal as os;
2224
#[cfg(wasm_browser)]
2325
use super::wasm_browser as os;
2426
#[cfg(target_os = "windows")]

0 commit comments

Comments
 (0)