Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions endpoint-demo/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -671,11 +671,11 @@ async fn stream_negative_client(
return Err("s6: the oversized write completed against an unread stream".into());
}
match send.write(b"x".to_vec()).await {
Err(Error::Other(msg)) if msg.contains("in flight") => {}
Err(Error::InUse(_)) => {}
other => return Err(format!("s6: concurrent write not refused: {other:?}")),
}
match send.finish() {
Err(Error::Other(msg)) if msg.contains("in flight") => {}
Err(Error::InUse(_)) => {}
other => return Err(format!("s6: finish under a write not refused: {other:?}")),
}
// Dropping the parked future cancels the write: a prefix is
Expand All @@ -696,7 +696,7 @@ async fn stream_negative_client(
return Err("s7: the read did not park on a silent stream".into());
}
match recv.read(READ_MAX).await {
Err(Error::Other(msg)) if msg.contains("in flight") => {}
Err(Error::InUse(_)) => {}
other => return Err(format!("s7: concurrent read not refused: {other:?}")),
}
// Dropping the parked future cancels the read and releases
Expand Down Expand Up @@ -724,9 +724,7 @@ async fn stream_negative_client(
// The cancelled S7 read released its guard: a later read runs and
// reports the connection's failure, not a refusal.
match recv.read(READ_MAX).await {
Err(Error::Other(msg)) if msg.contains("in flight") => {
return Err("s7: the cancelled read left its guard claimed".into())
}
Err(Error::InUse(_)) => return Err("s7: the cancelled read left its guard claimed".into()),
Err(_) => {}
other => return Err(format!("s7: post-close read got {other:?}")),
}
Expand Down
37 changes: 21 additions & 16 deletions endpoint/src/endpoint_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -709,6 +709,7 @@
Error::Closed
}
ConnectionError::LocallyClosed => Error::Closed,
ConnectionError::TimedOut => Error::TimedOut("the connection timed out".into()),
other => Error::Other(format!("connection lost: {other}")),
});
}
Expand Down Expand Up @@ -1048,6 +1049,10 @@
Error::Other(detail.to_string())
}

fn in_use(detail: impl std::fmt::Display) -> Error {
Error::InUse(detail.to_string())
}

/// A synthetic socket address under the IPv6 documentation prefix
/// (RFC 3849, `2001:db8::/32`): `2001:db8:<space>::<hi>:<lo>`, port
/// 4433. Documentation addresses are never routable, so standins
Expand Down Expand Up @@ -1341,7 +1346,7 @@
return Some(Err(Error::Closed));
}
if started.elapsed() > Duration::from_secs(30) {
return Some(Err(Error::ConnectFailed("relay open timed out".into())));
return Some(Err(Error::TimedOut("relay open timed out".into())));
}
None
})
Expand Down Expand Up @@ -1447,9 +1452,7 @@
);

let udp = match &udp_bind_addr {
Some(bind_addr) => Some(Rc::new(
UdpWire::bind(bind_addr).map_err(Error::InvalidArgument)?,
)),
Some(bind_addr) => Some(Rc::new(UdpWire::bind(bind_addr)?)),
None => None,
};

Expand Down Expand Up @@ -1578,11 +1581,13 @@
let entry = st.conns.get_mut(&handle).expect("connection entry");
if let Some(err) = &entry.error {
// A dial that dies before connecting failed to
// connect, whatever the mechanism — the peer's refusal
// and a handshake timeout both arrive as transport
// closes. Local endpoint closure stays `closed`.
// connect: the peer's refusal arrives as a transport
// close and folds into `connect-failed`. Timeouts keep
// their own case; local endpoint closure stays
// `closed`.
return Some(Err(match err.clone() {
Error::Closed => Error::Closed,
Error::TimedOut(msg) => Error::TimedOut(msg),
Error::Other(msg) => Error::ConnectFailed(msg),
other => other,
}));
Expand Down Expand Up @@ -1964,7 +1969,7 @@
// ever will be, and the stream did not reach its FIN — a
// connection close must not read as one (issue #13,
// finding A2).
Err(ReadError::Blocked) => match conn_failure {

Check warning on line 1972 in endpoint/src/endpoint_impl.rs

View workflow job for this annotation

GitHub Actions / ci

manual implementation of `Option::map`
Some(err) => Some(Err(err)),
None => None,
},
Expand Down Expand Up @@ -1999,22 +2004,22 @@
impl GuestSendStream for SendStreamRes {
async fn write(&self, bytes: Vec<u8>) -> Result<(), Error> {
if self.streaming.get() {
return Err(other("the stream's writes were taken by write-via-stream"));
return Err(in_use("the stream's writes were taken by write-via-stream"));
}
if self.writing.replace(true) {
return Err(other("a write is already in flight on this stream"));
return Err(in_use("a write is already in flight on this stream"));
}
let _claim = Unclaim(&self.writing);
write_all(&self.shared, self.handle, self.id, bytes).await
}

fn finish(&self) -> Result<(), Error> {
if self.streaming.get() {
return Err(other("the stream's writes were taken by write-via-stream"));
return Err(in_use("the stream's writes were taken by write-via-stream"));
}
if self.writing.get() {
// A FIN under an in-flight write would truncate it.
return Err(other("a write is in flight on this stream"));
return Err(in_use("a write is in flight on this stream"));
}
self.do_finish()
}
Expand All @@ -2030,11 +2035,11 @@

async fn write_via_stream(&self, mut data: StreamReader<u8>) -> Result<(), Error> {
if self.streaming.get() {
return Err(other("write-via-stream may be called once"));
return Err(in_use("write-via-stream may be called once"));
}
if self.writing.replace(true) {
// Refused without claiming: the call had no effect.
return Err(other("a write is already in flight on this stream"));
return Err(in_use("a write is already in flight on this stream"));
}
let _claim = Unclaim(&self.writing);
self.streaming.set(true);
Expand All @@ -2055,7 +2060,7 @@
impl GuestRecvStream for RecvStreamRes {
async fn read(&self, max: u32) -> Result<Option<Vec<u8>>, Error> {
if self.streaming.get() {
return Err(other("the stream's bytes were taken by read-via-stream"));
return Err(in_use("the stream's bytes were taken by read-via-stream"));
}
if let Some(terminal) = self.terminal.borrow().clone() {
return terminal.map(|()| None);
Expand All @@ -2064,7 +2069,7 @@
return Ok(Some(Vec::new()));
}
if self.reading.replace(true) {
return Err(other("a read is already in flight on this stream"));
return Err(in_use("a read is already in flight on this stream"));
}
let _claim = Unclaim(&self.reading);
match read_some(&self.shared, self.handle, self.id, max).await {
Expand Down Expand Up @@ -2092,7 +2097,7 @@
&self,
) -> Result<(StreamReader<u8>, FutureReader<Result<(), Error>>), Error> {
if self.streaming.replace(true) {
return Err(other("read-via-stream may be called once"));
return Err(in_use("read-via-stream may be called once"));
}
let (mut writer, reader) = wit_stream::new();
// The default never surfaces: every pump path below writes an
Expand Down
23 changes: 18 additions & 5 deletions endpoint/src/udp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};

use crate::bindings::polymorph::iroh::types::Error;
use crate::bindings::wasi::sockets::types::{
ErrorCode, IpAddressFamily, IpSocketAddress, Ipv4SocketAddress, Ipv6SocketAddress, UdpSocket,
};
Expand All @@ -13,25 +14,37 @@ pub struct UdpWire {
local: SocketAddr,
}

/// A socket failure at bind time: `error-code.not-supported` is the
/// host's honest no-UDP answer (the browser profile) and surfaces as
/// `error.not-supported`; anything else rejects the arguments.
fn bind_error(what: &str, code: ErrorCode) -> Error {
match code {
ErrorCode::NotSupported => {
Error::NotSupported("this deployment provides no UDP socket".into())
}
other => Error::InvalidArgument(format!("{what}: {other:?}")),
}
}

impl UdpWire {
/// Create and bind a socket at `bind_addr` (`ip:port`; port 0 picks a
/// free one).
pub fn bind(bind_addr: &str) -> Result<Self, String> {
pub fn bind(bind_addr: &str) -> Result<Self, Error> {
let addr: SocketAddr = bind_addr
.parse()
.map_err(|e| format!("udp bind address {bind_addr:?}: {e}"))?;
.map_err(|e| Error::InvalidArgument(format!("udp bind address {bind_addr:?}: {e}")))?;
let family = match addr {
SocketAddr::V4(_) => IpAddressFamily::Ipv4,
SocketAddr::V6(_) => IpAddressFamily::Ipv6,
};
let socket = UdpSocket::create(family).map_err(|e| format!("udp create: {e:?}"))?;
let socket = UdpSocket::create(family).map_err(|e| bind_error("udp create", e))?;
socket
.bind(to_wasi(addr))
.map_err(|e| format!("udp bind: {e:?}"))?;
.map_err(|e| bind_error("udp bind", e))?;
let local = from_wasi(
socket
.get_local_address()
.map_err(|e| format!("udp local address: {e:?}"))?,
.map_err(|e| bind_error("udp local address", e))?,
);
Ok(Self { socket, local })
}
Expand Down
33 changes: 33 additions & 0 deletions host-deltic/src/run-endpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,39 @@ async function main(): Promise<number> {
`zero wasi:sockets calls (browser profile) — log: [${udpCallLog().join(", ")}]`,
);
await deadline(ep.close(), 10_000, "close() after bind");

// The deployment-profile latitude (wit/iroh.wit, udp-bind-addr):
// this host has no UDP, so a bind that asks for the direct path
// must fail not-supported — the stub's honest error-code carried
// through. Probed after the zero-calls check above: this call is
// MEANT to reach the stub.
let unsupported = "no error";
try {
await deadline(
bindEndpoint(inst, {
alpns: [ALPN],
relayUrl: relay.url,
udpBindAddr: "127.0.0.1:0",
webrtc: false,
}),
30_000,
"bind with udp-bind-addr",
);
unsupported = "bind succeeded";
} catch (err) {
if (err instanceof ComponentException) {
const p = err.payload as { kind?: string } | undefined;
unsupported = p?.kind ?? "unknown";
} else {
unsupported = describeError(err);
}
}
check(
v,
unsupported === "not-supported",
`udp-bind-addr on the browser profile fails not-supported: ${unsupported}`,
);

await settle();
check(v, takeGuestPanics().length === 0, "no guest trap during bind/identity");
v.detail = `bind ${bindMs.toFixed(0)} ms, id ${shortId(id)}, 3 post-pump export calls`;
Expand Down
3 changes: 3 additions & 0 deletions host-deltic/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ export type IrohError =
| { kind: "closed" }
| { kind: "reset"; value: bigint }
| { kind: "connect-failed"; value: string }
| { kind: "timed-out"; value: string }
| { kind: "not-supported"; value: string }
| { kind: "in-use"; value: string }
| { kind: "invalid-argument"; value: string }
| { kind: "other"; value: string };

Expand Down
16 changes: 8 additions & 8 deletions scripts/matrix.sh
Original file line number Diff line number Diff line change
Expand Up @@ -214,11 +214,11 @@ run_pair "interop-udp-theirs-client" \
# exist (the connect must time out, not hang). The wrong-key TLS pin
# rejection itself is asserted by component-tls's rpk handshake tests.

# Start a server, run a client expected to FAIL, assert it fails with a
# connect-shaped error; the server never completes and is killed.
# run_client_failure <name> <with-server 0|1> <client-cmd...>
# Start a server, run a client expected to FAIL, assert it fails with
# the named error case; the server never completes and is killed.
# run_client_failure <name> <with-server 0|1> <error-pattern> <client-cmd...>
run_client_failure() {
local name=$1 with_server=$2; shift 2
local name=$1 with_server=$2 pattern=$3; shift 3
local server_pid="" server_id="0000000000000000000000000000000000000000000000000000000000000000"
if [ "$with_server" = 1 ]; then
timeout 120 "$EHOST" "$COMPOSED_WASM" --role server --relay "$RELAY_URL" \
Expand All @@ -236,20 +236,20 @@ run_client_failure() {
[ -n "$server_pid" ] && kill "$server_pid" 2>/dev/null

if [ "$client_status" != 0 ] && [ "$client_status" != 124 ] \
&& grep -qi "connect" "$LOGDIR/$name-client.log" \
&& grep -q "$pattern" "$LOGDIR/$name-client.log" \
&& ! grep -q "^OK:" "$LOGDIR/$name-client.log"; then
echo "PASS $name"
else
echo "FAIL $name (client=$client_status, expected a bounded connect failure; logs in $LOGDIR)"
echo "FAIL $name (client=$client_status, expected a bounded $pattern failure; logs in $LOGDIR)"
FAILURES=$((FAILURES + 1))
fi
}

run_client_failure "endpoint-negative-wrong-alpn" 1 \
run_client_failure "endpoint-negative-wrong-alpn" 1 "ConnectFailed" \
timeout 60 "$EHOST" "$COMPOSED_WASM" --role client --relay "$RELAY_URL" \
--alpn "iroh-demo-negative/0" --peer

run_client_failure "endpoint-negative-absent-peer" 0 \
run_client_failure "endpoint-negative-absent-peer" 0 "TimedOut" \
timeout 60 "$EHOST" "$COMPOSED_WASM" --role client --relay "$RELAY_URL" \
--peer

Expand Down
Loading