Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
node: New relay configuration for nodes
cloudhead committed 2 years ago
commit a9b94b0ad671e4d8e3129b1e2da86325a114896f
parent 1708ddf77709ef8e9dc8cbe2ac8a09c0f9a69ee1
8 files changed +165 -112
modified radicle-cli/examples/rad-config.md
@@ -29,7 +29,7 @@ $ rad config
    "tor": null,
    "network": "main",
    "log": "INFO",
-
    "relay": true,
+
    "relay": "auto",
    "limits": {
      "routingMaxSize": 1000,
      "routingMaxAge": 604800,
modified radicle-cli/examples/rad-sync.md
@@ -11,13 +11,13 @@ If we check the sync status, we see that our peers are out of sync.

```
$ rad sync status --sort-by alias
-
╭──────────────────────────────────────────────────────────────────────────────────────────╮
-
│ ●   Node                      Address                  Status        Tip       Timestamp │
-
├──────────────────────────────────────────────────────────────────────────────────────────┤
-
│ ●   alice   (you)             alice.radicle.xyz:8776                 f209c9f   [  ...  ] │
-
│ ●   bob     z6Mkt67…v4N1tRk   bob.radicle.xyz:8776     out-of-sync   f209c9f   [  ...  ] │
-
│ ●   eve     z6Mkux1…nVhib7Z   eve.radicle.xyz:8776     out-of-sync   f209c9f   [  ...  ] │
-
╰──────────────────────────────────────────────────────────────────────────────────────────╯
+
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
+
│ ●   Node                      Address                      Status        Tip       Timestamp │
+
├──────────────────────────────────────────────────────────────────────────────────────────────┤
+
│ ●   alice   (you)             alice.radicle.example:8776                 f209c9f   [  ...  ] │
+
│ ●   bob     z6Mkt67…v4N1tRk   bob.radicle.example:8776     out-of-sync   f209c9f   [  ...  ] │
+
│ ●   eve     z6Mkux1…nVhib7Z   eve.radicle.example:8776     out-of-sync   f209c9f   [  ...  ] │
+
╰──────────────────────────────────────────────────────────────────────────────────────────────╯
```

Now let's run `rad sync`. This will announce the issue refs to the network and
@@ -33,13 +33,13 @@ Now, when we run `rad sync status` again, we can see that `bob` and

```
$ rad sync status --sort-by alias
-
╭─────────────────────────────────────────────────────────────────────────────────────╮
-
│ ●   Node                      Address                  Status   Tip       Timestamp │
-
├─────────────────────────────────────────────────────────────────────────────────────┤
-
│ ●   alice   (you)             alice.radicle.xyz:8776            a9ce0d1   [  ...  ] │
-
│ ●   bob     z6Mkt67…v4N1tRk   bob.radicle.xyz:8776     synced   a9ce0d1   [  ...  ] │
-
│ ●   eve     z6Mkux1…nVhib7Z   eve.radicle.xyz:8776     synced   a9ce0d1   [  ...  ] │
-
╰─────────────────────────────────────────────────────────────────────────────────────╯
+
╭─────────────────────────────────────────────────────────────────────────────────────────╮
+
│ ●   Node                      Address                      Status   Tip       Timestamp │
+
├─────────────────────────────────────────────────────────────────────────────────────────┤
+
│ ●   alice   (you)             alice.radicle.example:8776            a9ce0d1   [  ...  ] │
+
│ ●   bob     z6Mkt67…v4N1tRk   bob.radicle.example:8776     synced   a9ce0d1   [  ...  ] │
+
│ ●   eve     z6Mkux1…nVhib7Z   eve.radicle.example:8776     synced   a9ce0d1   [  ...  ] │
+
╰─────────────────────────────────────────────────────────────────────────────────────────╯
```

If we try to sync again after the nodes have synced, we will already
modified radicle-cli/tests/commands.rs
@@ -39,6 +39,7 @@ mod config {
    pub fn seed(alias: &'static str) -> Config {
        Config {
            network: Network::Test,
+
            relay: node::config::Relay::Always,
            limits: Limits {
                rate: RateLimits {
                    inbound: RateLimit {
@@ -52,20 +53,27 @@ mod config {
                },
                ..Limits::default()
            },
+
            external_addresses: vec![node::Address::from_str(&format!(
+
                "{alias}.radicle.example:8776"
+
            ))
+
            .unwrap()],
            ..node(alias)
        }
    }

-
    /// Test node config.
-
    pub fn node(alias: &'static str) -> Config {
+
    /// Relay node config.
+
    pub fn relay(alias: &'static str) -> Config {
        Config {
-
            external_addresses: vec![
-
                node::Address::from_str(&format!("{alias}.radicle.xyz:8776")).unwrap(),
-
            ],
-
            ..Config::test(Alias::new(alias))
+
            relay: node::config::Relay::Always,
+
            ..node(alias)
        }
    }

+
    /// Test node config.
+
    pub fn node(alias: &'static str) -> Config {
+
        Config::test(Alias::new(alias))
+
    }
+

    /// Test profile config.
    pub fn profile(alias: &'static str) -> profile::Config {
        Environment::config(Alias::new(alias))
@@ -965,9 +973,9 @@ fn rad_review_by_hunk() {
#[test]
fn rad_patch_delete() {
    let mut environment = Environment::new();
-
    let alice = environment.node(Config::test(Alias::new("alice")));
-
    let bob = environment.node(Config::test(Alias::new("bob")));
-
    let seed = environment.node(Config::test(Alias::new("seed")));
+
    let alice = environment.node(config::relay("alice"));
+
    let bob = environment.node(config::relay("bob"));
+
    let seed = environment.node(config::relay("seed"));
    let working = environment.tmp().join("working");
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();

@@ -1332,9 +1340,9 @@ fn rad_clone_connect() {
#[test]
fn rad_sync_without_node() {
    let mut environment = Environment::new();
-
    let alice = environment.node(Config::test(Alias::new("alice")));
-
    let bob = environment.node(Config::test(Alias::new("bob")));
-
    let mut eve = environment.node(Config::test(Alias::new("eve")));
+
    let alice = environment.node(config::seed("alice"));
+
    let bob = environment.node(config::seed("bob"));
+
    let mut eve = environment.node(config::seed("eve"));

    let rid = RepoId::from_urn("rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5").unwrap();
    eve.policies.seed(&rid, Scope::All).unwrap();
@@ -1738,9 +1746,9 @@ fn test_cob_deletion() {
fn rad_sync() {
    let mut environment = Environment::new();
    let working = environment.tmp().join("working");
-
    let alice = environment.node(config::node("alice"));
-
    let bob = environment.node(config::node("bob"));
-
    let eve = environment.node(config::node("eve"));
+
    let alice = environment.node(config::seed("alice"));
+
    let bob = environment.node(config::seed("bob"));
+
    let eve = environment.node(config::seed("eve"));
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();

    fixtures::repository(working.join("acme"));
@@ -1784,12 +1792,12 @@ fn rad_sync() {
//
fn test_replication_via_seed() {
    let mut environment = Environment::new();
-
    let alice = environment.node(Config::test(Alias::new("alice")));
-
    let bob = environment.node(Config::test(Alias::new("bob")));
+
    let alice = environment.node(config::relay("alice"));
+
    let bob = environment.node(config::relay("bob"));
    let seed = environment.node(Config {
        policy: Policy::Allow,
        scope: Scope::All,
-
        ..Config::test(Alias::new("seed"))
+
        ..config::relay("seed")
    });
    let working = environment.tmp().join("working");
    let rid = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
@@ -1874,9 +1882,9 @@ fn test_replication_via_seed() {
#[test]
fn rad_remote() {
    let mut environment = Environment::new();
-
    let alice = environment.node(Config::test(Alias::new("alice")));
-
    let bob = environment.node(Config::test(Alias::new("bob")));
-
    let eve = environment.node(Config::test(Alias::new("eve")));
+
    let alice = environment.node(config::relay("alice"));
+
    let bob = environment.node(config::relay("bob"));
+
    let eve = environment.node(config::relay("eve"));
    let working = environment.tmp().join("working");
    let home = alice.home.clone();
    let rid = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
modified radicle-httpd/src/api/v1/profile.rs
@@ -92,7 +92,7 @@ mod routes {
                  "tor": null,
                  "network": "main",
                  "log": "INFO",
-
                  "relay": true,
+
                  "relay": "auto",
                  "limits": {
                    "routingMaxSize": 1000,
                    "routingMaxAge": 604800,
modified radicle-node/src/service.rs
@@ -25,7 +25,7 @@ use radicle::node;
use radicle::node::address;
use radicle::node::address::Store as _;
use radicle::node::address::{AddressBook, KnownAddress};
-
use radicle::node::config::PeerConfig;
+
use radicle::node::config::{PeerConfig, Relay};
use radicle::node::refs::Store as _;
use radicle::node::routing::Store as _;
use radicle::node::seed;
@@ -1334,7 +1334,7 @@ where
        &mut self,
        relayer_addr: &Address,
        announcement: &Announcement,
-
    ) -> Result<bool, session::Error> {
+
    ) -> Result<Relay, session::Error> {
        if !announcement.verify() {
            return Err(session::Error::Misbehavior);
        }
@@ -1346,7 +1346,7 @@ where

        // Ignore our own announcements, in case the relayer sent one by mistake.
        if announcer == self.nid() {
-
            return Ok(false);
+
            return Ok(Relay::Never);
        }
        let now = self.clock;
        let timestamp = message.timestamp();
@@ -1358,7 +1358,7 @@ where
        {
            self.config.relay
        } else {
-
            false
+
            Relay::Never
        };

        // Don't allow messages from too far in the future.
@@ -1379,12 +1379,12 @@ where
                Ok(node) => {
                    if node.is_none() {
                        debug!(target: "service", "Ignoring announcement from unknown node {announcer} (t={timestamp})");
-
                        return Ok(false);
+
                        return Ok(Relay::Never);
                    }
                }
                Err(e) => {
                    error!(target: "service", "Error looking up node in address book: {e}");
-
                    return Ok(false);
+
                    return Ok(Relay::Never);
                }
            }
        }
@@ -1394,12 +1394,12 @@ where
            Ok(fresh) => {
                if !fresh {
                    debug!(target: "service", "Ignoring stale announcement from {announcer} (t={timestamp})");
-
                    return Ok(false);
+
                    return Ok(Relay::Never);
                }
            }
            Err(e) => {
                error!(target: "service", "Error updating gossip entry from {announcer}: {e}");
-
                return Ok(false);
+
                return Ok(Relay::Never);
            }
        }

@@ -1419,12 +1419,12 @@ where
                    Ok(synced) => {
                        if synced.is_empty() {
                            trace!(target: "service", "No routes updated by inventory announcement from {announcer}");
-
                            return Ok(false);
+
                            return Ok(Relay::Never);
                        }
                    }
                    Err(e) => {
                        error!(target: "service", "Error processing inventory from {announcer}: {e}");
-
                        return Ok(false);
+
                        return Ok(Relay::Never);
                    }
                }

@@ -1473,7 +1473,7 @@ where
                // Empty announcements can be safely ignored.
                let Some(refs) = NonEmpty::from_vec(message.refs.to_vec()) else {
                    debug!(target: "service", "Skipping fetch, no refs in announcement for {} (t={timestamp})", message.rid);
-
                    return Ok(false);
+
                    return Ok(Relay::Never);
                };
                // We update inventories when receiving ref announcements, as these could come
                // from a new repository being initialized.
@@ -1526,7 +1526,7 @@ where
                        "Ignoring refs announcement from {announcer}: repository {} isn't seeded (t={timestamp})",
                        message.rid
                    );
-
                    return Ok(false);
+
                    return Ok(Relay::Never);
                }
                // Refs can be relayed by peers who don't have the data in storage,
                // therefore we only check whether we are connected to the *announcer*,
@@ -1600,7 +1600,7 @@ where
                }
            }
        }
-
        Ok(false)
+
        Ok(Relay::Never)
    }

    pub fn handle_info(&mut self, remote: NodeId, info: &Info) -> Result<(), session::Error> {
@@ -1652,8 +1652,15 @@ where
                let relayer_addr = peer.addr.clone();
                let announcer = ann.node;

-
                // Returning true here means that the message should be relayed.
-
                if self.handle_announcement(&relayer_addr, &ann)? {
+
                let relay = match self.handle_announcement(&relayer_addr, &ann)? {
+
                    // In "auto" mode, we only relay if we're a public seed node.
+
                    // This reduces traffic for private nodes, as well as message redundancy.
+
                    Relay::Auto => !self.config.external_addresses.is_empty(),
+
                    Relay::Never => false,
+
                    Relay::Always => true,
+
                };
+

+
                if relay {
                    // Choose peers we should relay this message to.
                    // 1. Don't relay to the peer who sent us this message.
                    // 2. Don't relay to the peer who signed this announcement.
modified radicle-node/src/tests/e2e.rs
@@ -18,6 +18,19 @@ use crate::storage::git::transport;
use crate::test::environment::{converge, Environment, Node};
use crate::test::logger;

+
mod config {
+
    use super::*;
+
    use radicle::node::config::{Config, Relay};
+

+
    /// Relay node config.
+
    pub fn relay(alias: &'static str) -> Config {
+
        Config {
+
            relay: Relay::Always,
+
            ..Config::test(Alias::new(alias))
+
        }
+
    }
+
}
+

#[test]
//
//     alice -- bob
@@ -27,8 +40,8 @@ fn test_inventory_sync_basic() {

    let tmp = tempfile::tempdir().unwrap();

-
    let mut alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
-
    let mut bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
+
    let mut alice = Node::init(tmp.path(), config::relay("alice"));
+
    let mut bob = Node::init(tmp.path(), config::relay("bob"));

    alice.project("alice", "");
    bob.project("bob", "");
@@ -51,9 +64,9 @@ fn test_inventory_sync_bridge() {

    let tmp = tempfile::tempdir().unwrap();

-
    let mut alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
-
    let mut bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
-
    let mut eve = Node::init(tmp.path(), Config::test(Alias::new("eve")));
+
    let mut alice = Node::init(tmp.path(), config::relay("alice"));
+
    let mut bob = Node::init(tmp.path(), config::relay("bob"));
+
    let mut eve = Node::init(tmp.path(), config::relay("eve"));

    alice.project("alice", "");
    bob.project("bob", "");
@@ -81,9 +94,9 @@ fn test_inventory_sync_ring() {

    let tmp = tempfile::tempdir().unwrap();

-
    let mut alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
-
    let mut bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
-
    let mut eve = Node::init(tmp.path(), Config::test(Alias::new("eve")));
+
    let mut alice = Node::init(tmp.path(), config::relay("alice"));
+
    let mut bob = Node::init(tmp.path(), config::relay("bob"));
+
    let mut eve = Node::init(tmp.path(), config::relay("eve"));
    let mut carol = Node::init(tmp.path(), Config::test(Alias::new("carol")));

    alice.project("alice", "");
@@ -118,9 +131,9 @@ fn test_inventory_sync_star() {

    let tmp = tempfile::tempdir().unwrap();

-
    let mut alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
-
    let mut bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
-
    let mut eve = Node::init(tmp.path(), Config::test(Alias::new("eve")));
+
    let mut alice = Node::init(tmp.path(), config::relay("alice"));
+
    let mut bob = Node::init(tmp.path(), config::relay("bob"));
+
    let mut eve = Node::init(tmp.path(), config::relay("eve"));
    let mut carol = Node::init(tmp.path(), Config::test(Alias::new("carol")));
    let mut dave = Node::init(tmp.path(), Config::test(Alias::new("dave")));

@@ -150,8 +163,8 @@ fn test_replication() {
    logger::init(log::Level::Debug);

    let tmp = tempfile::tempdir().unwrap();
-
    let alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
-
    let mut bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
+
    let alice = Node::init(tmp.path(), config::relay("alice"));
+
    let mut bob = Node::init(tmp.path(), config::relay("bob"));
    let acme = bob.project("acme", "");

    let mut alice = alice.spawn();
@@ -210,8 +223,8 @@ fn test_replication_ref_in_sigrefs() {
    logger::init(log::Level::Debug);

    let tmp = tempfile::tempdir().unwrap();
-
    let alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
-
    let mut bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
+
    let alice = Node::init(tmp.path(), config::relay("alice"));
+
    let mut bob = Node::init(tmp.path(), config::relay("bob"));

    let acme = bob.project("acme", "");
    // Delete one of the signed refs.
@@ -251,8 +264,8 @@ fn test_replication_ref_in_sigrefs() {
#[test]
fn test_replication_invalid() {
    let tmp = tempfile::tempdir().unwrap();
-
    let alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
-
    let mut bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
+
    let alice = Node::init(tmp.path(), config::relay("alice"));
+
    let mut bob = Node::init(tmp.path(), config::relay("bob"));
    let carol = MockSigner::default();
    let acme = bob.project("acme", "");
    let repo = bob.storage.repository_mut(acme).unwrap();
@@ -304,8 +317,8 @@ fn test_migrated_clone() {
    logger::init(log::Level::Debug);

    let tmp = tempfile::tempdir().unwrap();
-
    let mut alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
-
    let bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
+
    let mut alice = Node::init(tmp.path(), config::relay("alice"));
+
    let bob = Node::init(tmp.path(), config::relay("bob"));
    let acme = alice.project("acme", "");

    let mut alice = alice.spawn();
@@ -357,8 +370,8 @@ fn test_dont_fetch_owned_refs() {
    logger::init(log::Level::Debug);

    let tmp = tempfile::tempdir().unwrap();
-
    let mut alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
-
    let bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
+
    let mut alice = Node::init(tmp.path(), config::relay("alice"));
+
    let bob = Node::init(tmp.path(), config::relay("bob"));
    let acme = alice.project("acme", "");

    let mut alice = alice.spawn();
@@ -384,8 +397,8 @@ fn test_fetch_followed_remotes() {
    logger::init(log::Level::Debug);

    let tmp = tempfile::tempdir().unwrap();
-
    let mut alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
-
    let bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
+
    let mut alice = Node::init(tmp.path(), config::relay("alice"));
+
    let bob = Node::init(tmp.path(), config::relay("bob"));
    let acme = alice.project("acme", "");
    let mut signers = Vec::with_capacity(5);
    {
@@ -439,8 +452,8 @@ fn test_missing_remote() {
    logger::init(log::Level::Debug);

    let tmp = tempfile::tempdir().unwrap();
-
    let mut alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
-
    let bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
+
    let mut alice = Node::init(tmp.path(), config::relay("alice"));
+
    let bob = Node::init(tmp.path(), config::relay("bob"));
    let acme = alice.project("acme", "");

    let mut alice = alice.spawn();
@@ -468,8 +481,8 @@ fn test_fetch_preserve_owned_refs() {
    logger::init(log::Level::Debug);

    let tmp = tempfile::tempdir().unwrap();
-
    let mut alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
-
    let bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
+
    let mut alice = Node::init(tmp.path(), config::relay("alice"));
+
    let bob = Node::init(tmp.path(), config::relay("bob"));
    let acme = alice.project("acme", "");
    let mut alice = alice.spawn();
    let mut bob = bob.spawn();
@@ -514,8 +527,8 @@ fn test_clone() {
    logger::init(log::Level::Debug);

    let tmp = tempfile::tempdir().unwrap();
-
    let alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
-
    let mut bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
+
    let alice = Node::init(tmp.path(), config::relay("alice"));
+
    let mut bob = Node::init(tmp.path(), config::relay("bob"));
    let acme = bob.project("acme", "");

    let mut alice = alice.spawn();
@@ -572,8 +585,8 @@ fn test_fetch_up_to_date() {
    logger::init(log::Level::Debug);

    let tmp = tempfile::tempdir().unwrap();
-
    let alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
-
    let mut bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
+
    let alice = Node::init(tmp.path(), config::relay("alice"));
+
    let mut bob = Node::init(tmp.path(), config::relay("bob"));
    let acme = bob.project("acme", "");

    let mut alice = alice.spawn();
@@ -601,8 +614,8 @@ fn test_fetch_unseeded() {
    logger::init(log::Level::Debug);

    let tmp = tempfile::tempdir().unwrap();
-
    let alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
-
    let mut bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
+
    let alice = Node::init(tmp.path(), config::relay("alice"));
+
    let mut bob = Node::init(tmp.path(), config::relay("bob"));
    let acme = bob.project("acme", "");

    let mut alice = alice.spawn();
@@ -631,8 +644,8 @@ fn test_large_fetch() {

    let env = Environment::new();
    let scale = env.scale();
-
    let mut alice = Node::init(&env.tmp(), Config::test(Alias::new("alice")));
-
    let bob = Node::init(&env.tmp(), Config::test(Alias::new("bob")));
+
    let mut alice = Node::init(&env.tmp(), config::relay("alice"));
+
    let bob = Node::init(&env.tmp(), config::relay("bob"));

    let tmp = tempfile::tempdir().unwrap();
    let (repo, _) = fixtures::repository(tmp.path());
@@ -681,14 +694,16 @@ fn test_concurrent_fetches() {
        &env.tmp(),
        service::Config {
            limits: limits.clone(),
-
            ..service::Config::test(Alias::new("alice"))
+
            relay: radicle::node::config::Relay::Always,
+
            ..config::relay("alice")
        },
    );
    let mut bob = Node::init(
        &env.tmp(),
        service::Config {
            limits,
-
            ..service::Config::test(Alias::new("bob"))
+
            relay: radicle::node::config::Relay::Always,
+
            ..config::relay("bob")
        },
    );

@@ -773,8 +788,8 @@ fn test_connection_crossing() {
    logger::init(log::Level::Debug);

    let tmp = tempfile::tempdir().unwrap();
-
    let alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
-
    let bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
+
    let alice = Node::init(tmp.path(), config::relay("alice"));
+
    let bob = Node::init(tmp.path(), config::relay("bob"));

    let alice = alice.spawn();
    let bob = bob.spawn();
@@ -841,9 +856,9 @@ fn test_non_fastforward_sigrefs() {

    let tmp = tempfile::tempdir().unwrap();

-
    let alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
-
    let mut bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
-
    let eve = Node::init(tmp.path(), Config::test(Alias::new("eve")));
+
    let alice = Node::init(tmp.path(), config::relay("alice"));
+
    let mut bob = Node::init(tmp.path(), config::relay("bob"));
+
    let eve = Node::init(tmp.path(), config::relay("eve"));

    let rid = bob.project("acme", "");

@@ -949,9 +964,9 @@ fn test_outdated_sigrefs() {

    let tmp = tempfile::tempdir().unwrap();

-
    let mut alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
-
    let bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
-
    let eve = Node::init(tmp.path(), Config::test(Alias::new("eve")));
+
    let mut alice = Node::init(tmp.path(), config::relay("alice"));
+
    let bob = Node::init(tmp.path(), config::relay("bob"));
+
    let eve = Node::init(tmp.path(), config::relay("eve"));

    let rid = alice.project("acme", "");

@@ -1045,9 +1060,9 @@ fn test_outdated_delegate_sigrefs() {

    let tmp = tempfile::tempdir().unwrap();

-
    let mut alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
-
    let bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
-
    let eve = Node::init(tmp.path(), Config::test(Alias::new("eve")));
+
    let mut alice = Node::init(tmp.path(), config::relay("alice"));
+
    let bob = Node::init(tmp.path(), config::relay("bob"));
+
    let eve = Node::init(tmp.path(), config::relay("eve"));

    let rid = alice.project("acme", "");

@@ -1133,8 +1148,8 @@ fn missing_default_branch() {

    let tmp = tempfile::tempdir().unwrap();

-
    let mut alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
-
    let bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
+
    let mut alice = Node::init(tmp.path(), config::relay("alice"));
+
    let bob = Node::init(tmp.path(), config::relay("bob"));

    let rid = alice.project("acme", "");

@@ -1185,9 +1200,9 @@ fn test_background_foreground_fetch() {

    let tmp = tempfile::tempdir().unwrap();

-
    let mut alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
-
    let bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
-
    let eve = Node::init(tmp.path(), Config::test(Alias::new("eve")));
+
    let mut alice = Node::init(tmp.path(), config::relay("alice"));
+
    let bob = Node::init(tmp.path(), config::relay("bob"));
+
    let eve = Node::init(tmp.path(), config::relay("eve"));

    let rid = alice.project("acme", "");

@@ -1273,10 +1288,10 @@ fn test_catchup_on_refs_announcements() {
    logger::init(log::Level::Debug);

    let tmp = tempfile::tempdir().unwrap();
-
    let mut alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
-
    let bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
+
    let mut alice = Node::init(tmp.path(), config::relay("alice"));
+
    let bob = Node::init(tmp.path(), config::relay("bob"));
    let bob_id = bob.id;
-
    let seed = Node::init(tmp.path(), Config::test(Alias::new("seed")));
+
    let seed = Node::init(tmp.path(), config::relay("seed"));
    let acme = alice.project("acme", "");

    let mut alice = alice.spawn();
modified radicle/src/node/config.rs
@@ -234,6 +234,19 @@ impl Default for PeerConfig {
    }
}

+
/// Relay configuration.
+
#[derive(Debug, Copy, Clone, Default, serde::Serialize, serde::Deserialize)]
+
#[serde(rename_all = "camelCase")]
+
pub enum Relay {
+
    /// Always relay messages.
+
    Always,
+
    /// Never relay messages.
+
    Never,
+
    /// Relay messages when applicable.
+
    #[default]
+
    Auto,
+
}
+

/// Tor configuration.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase", tag = "mode")]
@@ -276,9 +289,9 @@ pub struct Config {
    #[serde(default = "defaults::log")]
    #[serde(with = "crate::serde_ext::string")]
    pub log: log::Level,
-
    /// Whether or not our node should relay inventories.
-
    #[serde(default = "crate::serde_ext::bool::yes")]
-
    pub relay: bool,
+
    /// Whether or not our node should relay messages.
+
    #[serde(default, deserialize_with = "crate::serde_ext::ok_or_default")]
+
    pub relay: Relay,
    /// Configured service limits.
    #[serde(default)]
    pub limits: Limits,
@@ -310,7 +323,7 @@ impl Config {
            external_addresses: vec![],
            network: Network::default(),
            tor: None,
-
            relay: true,
+
            relay: Relay::default(),
            limits: Limits::default(),
            workers: DEFAULT_WORKERS,
            log: defaults::log(),
modified radicle/src/serde_ext.rs
@@ -110,6 +110,16 @@ pub fn is_default<T: Default + PartialEq>(t: &T) -> bool {
    t == &T::default()
}

+
/// Deserialize a value, but if it fails, return the default value.
+
pub fn ok_or_default<'de, T, D>(deserializer: D) -> Result<T, D::Error>
+
where
+
    T: serde::Deserialize<'de> + Default,
+
    D: serde::Deserializer<'de>,
+
{
+
    let v: serde_json::Value = serde::Deserialize::deserialize(deserializer)?;
+
    Ok(T::deserialize(v).unwrap_or_default())
+
}
+

#[cfg(test)]
mod test {
    use super::*;