diff --git a/protocols/schema-bundle.bin b/protocols/schema-bundle.bin
index 1be8eaa..834abcf 100644
--- a/protocols/schema-bundle.bin
+++ b/protocols/schema-bundle.bin
@@ -4,4 +4,4 @@ sourcePort
 ByteString„„„„„³	PacketOut´³rec´³lit³udp-out„´³tupleµ´³named³
 targetAddr´³atom³String„„´³named³
 targetPort´³atom³
SignedInteger„„´³named³body´³atom³
-ByteString„„„„„³MulticastLoopback´³rec´³lit³udp-multicast-loopback„´³tupleµ´³named³enabled´³atom³Boolean„„„„„³MulticastGroupMember´³rec´³lit³udp-multicast-group-member„´³tupleµ´³named³groupAddress´³atom³String„„´³named³groupInterface´³atom³String„„„„„„³embeddedType€„„„„
\ No newline at end of file
+ByteString„„„„„³MulticastTtl´³rec´³lit³udp-multicast-ttl„´³tupleµ´³named³hopCount´³atom³
SignedInteger„„„„„³MulticastLoopback´³rec´³lit³udp-multicast-loopback„´³tupleµ´³named³enabled´³atom³Boolean„„„„„³MulticastGroupMember´³rec´³lit³udp-multicast-group-member„´³tupleµ´³named³groupAddress´³atom³String„„´³named³groupInterface´³atom³String„„„„„„³embeddedType€„„„„
\ No newline at end of file
diff --git a/protocols/schemas/udp.prs b/protocols/schemas/udp.prs
index 8f69312..8672bdd 100644
--- a/protocols/schemas/udp.prs
+++ b/protocols/schemas/udp.prs
@@ -13,8 +13,9 @@ Error = <udp-error @detail any> .
 PacketIn = <udp-in @sourceAddr string @sourcePort int @body bytes> .
 PacketOut = <udp-out @targetAddr string @targetPort int @body bytes> .
 
-# Assertion from local application
+# Assertions from local application
 MulticastGroupMember = <udp-multicast-group-member @groupAddress string @groupInterface string> .
+MulticastTtl = <udp-multicast-ttl @hopCount int> .
 
 # Message from local application
 MulticastLoopback = <udp-multicast-loopback @enabled bool> .
diff --git a/src/main.rs b/src/main.rs
index 73ac772..4774903 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -2,7 +2,13 @@ use std::net::SocketAddr;
 use std::sync::Arc;
 
 use preserves_schema::Codec;
-use syndicate::{actor::{Account, Activation, ActorResult, AnyValue, Cap, FacetRef, LinkedTaskTermination}, dataspace::Dataspace, relay, schemas::trace, value::NestedValue};
+use syndicate::{
+    actor::{Account, Activation, ActorResult, AnyValue, Cap, FacetRef, LinkedTaskTermination},
+    dataspace::Dataspace,
+    relay,
+    schemas::trace,
+    value::NestedValue,
+};
 
 mod schemas {
     include!(concat!(env!("OUT_DIR"), "/src/schemas/mod.rs"));
@@ -41,41 +47,72 @@ async fn run_udp_socket(
             addr: local_addr.ip().to_string(),
             port: local_addr.port().into(),
         });
-        enclose!((sock) on_message!(t, space, language(), $p: udp::PacketOut, enclose!((sock) move |_t: &mut Activation| {
-            tracing::trace!(?p, "outbound packet");
-            let target: SocketAddr = str::parse(&format!("{}:{}", p.target_addr, p.target_port))?;
-            match sock.try_send_to(&p.body[..], target) {
-                Ok(_) => (),
-                Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => (),
-                Err(e) => Err(e)?,
-            }
-            Ok(())
-        })));
-        enclose!((sock, space) during!(t, space, language(), $g: udp::MulticastGroupMember, enclose!((sock, space) move |t: &mut Activation| {
-            tracing::debug!(?g, "adding multicast group");
-            let group_address = str::parse(&g.group_address)?;
-            let group_interface = str::parse(&g.group_interface)?;
-            let mut ok = true;
-            if let Err(err) = sock.join_multicast_v4(group_address, group_interface) {
-                ok = false;
-                space.assert(t, language(), &udp::Error { detail: AnyValue::new(format!("{}", err)) });
-            }
-            t.on_stop(enclose!((g) move |_t| {
-                if ok {
-                    tracing::debug!(?g, "removing multicast group");
-                    sock.leave_multicast_v4(group_address, group_interface)?;
-                }
-                Ok(())
-            }));
-            Ok(())
-        })));
-        enclose!((sock, space) on_message!(t, space, language(), $l: udp::MulticastLoopback, enclose!((sock, space) move |t: &mut Activation| {
-            tracing::debug!(?l, "setting multicast loopback");
-            if let Err(err) = sock.set_multicast_loop_v4(l.enabled) {
-                space.assert(t, language(), &udp::Error { detail: AnyValue::new(format!("{}", err)) });
-            }
-            Ok(())
-        })));
+        enclose!(
+            (sock) on_message!(t, space, language(), $p: udp::PacketOut, enclose!(
+                (sock) move |_t: &mut Activation| {
+                    tracing::trace!(?p, "outbound packet");
+                    let target: SocketAddr =
+                        str::parse(&format!("{}:{}", p.target_addr, p.target_port))?;
+                    match sock.try_send_to(&p.body[..], target) {
+                        Ok(_) => (),
+                        Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => (),
+                        Err(e) => Err(e)?,
+                    }
+                    Ok(())
+                })));
+        enclose!(
+            (sock, space) during!(t, space, language(), $g: udp::MulticastGroupMember, enclose!(
+                (sock, space) move |t: &mut Activation| {
+                    tracing::debug!(?g, "adding multicast group");
+                    let group_address = str::parse(&g.group_address)?;
+                    let group_interface = str::parse(&g.group_interface)?;
+                    let mut ok = true;
+                    if let Err(err) = sock.join_multicast_v4(group_address, group_interface) {
+                        ok = false;
+                        space.assert(t, language(), &udp::Error {
+                            detail: AnyValue::new(format!("join_multicast_v4: {}", err)),
+                        });
+                    }
+                    t.on_stop(enclose!((g) move |_t| {
+                        if ok {
+                            tracing::debug!(?g, "removing multicast group");
+                            sock.leave_multicast_v4(group_address, group_interface)?;
+                        }
+                        Ok(())
+                    }));
+                    Ok(())
+                })));
+        enclose!(
+            (sock, space) on_message!(t, space, language(), $l: udp::MulticastLoopback, enclose!(
+                (sock, space) move |t: &mut Activation| {
+                    tracing::debug!(?l, "setting multicast loopback");
+                    if let Err(err) = sock.set_multicast_loop_v4(l.enabled) {
+                        space.assert(t, language(), &udp::Error {
+                            detail: AnyValue::new(format!("set_multicast_loop_v4: {}", err)),
+                        });
+                    }
+                    Ok(())
+                })));
+        enclose!(
+            (sock, space) during!(t, space, language(), $n: udp::MulticastTtl, enclose!(
+                (sock, space) move |t: &mut Activation| {
+                    tracing::debug!(?n, "setting multicast ttl");
+                    let hop_count = match u32::try_from(&n.hop_count) {
+                        Ok(hop_count) => hop_count,
+                        Err(_) => {
+                            space.assert(t, language(), &udp::Error {
+                                detail: AnyValue::new("set_multicast_ttl_v4: bad hop count"),
+                            });
+                            return Ok(());
+                        }
+                    };
+                    if let Err(err) = sock.set_multicast_ttl_v4(hop_count) {
+                        space.assert(t, language(), &udp::Error {
+                            detail: AnyValue::new(format!("set_multicast_ttl_v4: {}", err)),
+                        });
+                    }
+                    Ok(())
+                })));
         Ok(())
     }));
     let mut buf = [0; 65536];
@@ -111,7 +148,7 @@ async fn main() -> ActorResult {
                     match run_udp_socket(&b, account.clone(), cause, facet.clone()).await {
                         Ok(()) => Ok(LinkedTaskTermination::Normal),
                         Err(err) => {
-                            let err_val = AnyValue::new(format!("{}", err));
+                            let err_val = AnyValue::new(format!("udp_io: {}", err));
                             facet.activate(&account, err_cause, move |t| {
                                 b.space.assert(t, language(), &udp::Error { detail: err_val });
                                 Ok(())
