I think it's a clearer indicator of what the default network is: it's the node's home network/clan.
365 lines
12 KiB
Nix
365 lines
12 KiB
Nix
{
|
|
lib,
|
|
dmModule,
|
|
testers,
|
|
}:
|
|
testers.nixosTest {
|
|
name = "data-mesher-multi-network";
|
|
|
|
nodes =
|
|
let
|
|
|
|
peerID = {
|
|
a1 = builtins.readFile ./keys/network_a/a1.peerID;
|
|
a2 = builtins.readFile ./keys/network_a/a2.peerID;
|
|
a3 = builtins.readFile ./keys/network_a/a3.peerID;
|
|
b1 = builtins.readFile ./keys/network_b/b1.peerID;
|
|
b2 = builtins.readFile ./keys/network_b/b2.peerID;
|
|
b3 = builtins.readFile ./keys/network_b/b3.peerID;
|
|
c1 = builtins.readFile ./keys/network_c/c1.peerID;
|
|
c2 = builtins.readFile ./keys/network_c/c2.peerID;
|
|
c3 = builtins.readFile ./keys/network_c/c3.peerID;
|
|
};
|
|
|
|
mkPeerAddr = name: "/dns/${name}/tcp/7946/p2p/${peerID.${name}}";
|
|
|
|
mkNode =
|
|
{
|
|
name,
|
|
networkCA,
|
|
bootstrapPeers,
|
|
homeFiles,
|
|
extraNetworks ? { },
|
|
}:
|
|
{
|
|
imports = [ dmModule ];
|
|
|
|
config = {
|
|
system.activationScripts.identity_key = {
|
|
text =
|
|
let
|
|
# Collect all network pub keys that need to be copied
|
|
extraNetworkPubs = lib.concatMapStrings (netName: ''
|
|
cp ${./keys/${netName}/network.pub} /run/${netName}.pub
|
|
chown data-mesher:data-mesher /run/${netName}.pub
|
|
chmod 600 /run/${netName}.pub
|
|
'') (builtins.attrNames extraNetworks);
|
|
in
|
|
''
|
|
# Copy home network public key
|
|
cp ${./keys/${networkCA}/network.pub} /run/${networkCA}.pub
|
|
chown data-mesher:data-mesher /run/${networkCA}.pub
|
|
chmod 600 /run/${networkCA}.pub
|
|
|
|
# Copy extra network public keys
|
|
${extraNetworkPubs}
|
|
|
|
# Copy peer identity key
|
|
cp ${./keys/${networkCA}/${name}.key} /run/identity.key
|
|
chown data-mesher:data-mesher /run/identity.key
|
|
chmod 600 /run/identity.key
|
|
|
|
# Copy peer identity certificate signed by the network
|
|
cp ${./keys/${networkCA}/${name}.cert} /run/identity.cert
|
|
chown data-mesher:data-mesher /run/identity.cert
|
|
chmod 600 /run/identity.cert
|
|
'';
|
|
};
|
|
|
|
services.data-mesher = {
|
|
enable = true;
|
|
openFirewall = true;
|
|
|
|
settings = {
|
|
log_level = "debug";
|
|
|
|
cluster = {
|
|
port = 7946;
|
|
bootstrap_peers = bootstrapPeers;
|
|
|
|
# speed up for testing
|
|
push_pull_interval = "5s";
|
|
|
|
identity_key = "/run/identity.key";
|
|
identity_cert = "/run/identity.cert";
|
|
};
|
|
|
|
network = {
|
|
id = "/run/${networkCA}.pub";
|
|
files = homeFiles;
|
|
};
|
|
|
|
extra_networks = builtins.mapAttrs (netName: netCfg: {
|
|
id = "/run/${netName}.pub";
|
|
inherit (netCfg) files;
|
|
}) extraNetworks;
|
|
};
|
|
};
|
|
|
|
# override just for testing
|
|
# when booting the interface we bind to is not ready, causing an error
|
|
systemd.services.data-mesher.serviceConfig.RestartSec = lib.mkForce "1s";
|
|
};
|
|
};
|
|
|
|
# Files configuration per network
|
|
# Network A: file_a signed by alice
|
|
filesA = {
|
|
"file_a" = [ "${./keys/signers/alice.pub}" ];
|
|
};
|
|
|
|
# Network B: file_b signed by bob
|
|
filesB = {
|
|
"file_b" = [ "${./keys/signers/bob.pub}" ];
|
|
};
|
|
|
|
# Network C: file_c signed by charlie
|
|
filesC = {
|
|
"file_c" = [ "${./keys/signers/charlie.pub}" ];
|
|
};
|
|
|
|
# Bootstrap peers per network group
|
|
# A nodes bootstrap to a1, a2, and c1 (cross-network)
|
|
bootstrapA = [
|
|
(mkPeerAddr "a1")
|
|
(mkPeerAddr "a2")
|
|
(mkPeerAddr "c1")
|
|
];
|
|
|
|
# B nodes bootstrap to b1, b2, and c1 (cross-network)
|
|
bootstrapB = [
|
|
(mkPeerAddr "b1")
|
|
(mkPeerAddr "b2")
|
|
(mkPeerAddr "c1")
|
|
];
|
|
|
|
# C nodes bootstrap to c1, c2, a1, and b1 (cross-network to both)
|
|
bootstrapC = [
|
|
(mkPeerAddr "c1")
|
|
(mkPeerAddr "c2")
|
|
(mkPeerAddr "a1")
|
|
(mkPeerAddr "b1")
|
|
];
|
|
|
|
in
|
|
{
|
|
# Network A nodes: home=netA, extra=netC
|
|
a1 = mkNode {
|
|
name = "a1";
|
|
networkCA = "network_a";
|
|
bootstrapPeers = bootstrapA;
|
|
homeFiles = filesA;
|
|
extraNetworks.network_c = {
|
|
files = filesC;
|
|
};
|
|
};
|
|
a2 = mkNode {
|
|
name = "a2";
|
|
networkCA = "network_a";
|
|
bootstrapPeers = bootstrapA;
|
|
homeFiles = filesA;
|
|
extraNetworks.network_c = {
|
|
files = filesC;
|
|
};
|
|
};
|
|
a3 = mkNode {
|
|
name = "a3";
|
|
networkCA = "network_a";
|
|
bootstrapPeers = bootstrapA;
|
|
homeFiles = filesA;
|
|
extraNetworks.network_c = {
|
|
files = filesC;
|
|
};
|
|
};
|
|
|
|
# Network B nodes: home=netB, extra=netC
|
|
b1 = mkNode {
|
|
name = "b1";
|
|
networkCA = "network_b";
|
|
bootstrapPeers = bootstrapB;
|
|
homeFiles = filesB;
|
|
extraNetworks.network_c = {
|
|
files = filesC;
|
|
};
|
|
};
|
|
b2 = mkNode {
|
|
name = "b2";
|
|
networkCA = "network_b";
|
|
bootstrapPeers = bootstrapB;
|
|
homeFiles = filesB;
|
|
extraNetworks.network_c = {
|
|
files = filesC;
|
|
};
|
|
};
|
|
b3 = mkNode {
|
|
name = "b3";
|
|
networkCA = "network_b";
|
|
bootstrapPeers = bootstrapB;
|
|
homeFiles = filesB;
|
|
extraNetworks.network_c = {
|
|
files = filesC;
|
|
};
|
|
};
|
|
|
|
# Network C nodes: home=netC, extra=netA and netB
|
|
c1 = mkNode {
|
|
name = "c1";
|
|
networkCA = "network_c";
|
|
bootstrapPeers = bootstrapC;
|
|
homeFiles = filesC;
|
|
extraNetworks = {
|
|
network_a = {
|
|
files = filesA;
|
|
};
|
|
network_b = {
|
|
files = filesB;
|
|
};
|
|
};
|
|
};
|
|
c2 = mkNode {
|
|
name = "c2";
|
|
networkCA = "network_c";
|
|
bootstrapPeers = bootstrapC;
|
|
homeFiles = filesC;
|
|
extraNetworks = {
|
|
network_a = {
|
|
files = filesA;
|
|
};
|
|
network_b = {
|
|
files = filesB;
|
|
};
|
|
};
|
|
};
|
|
c3 = mkNode {
|
|
name = "c3";
|
|
networkCA = "network_c";
|
|
bootstrapPeers = bootstrapC;
|
|
homeFiles = filesC;
|
|
extraNetworks = {
|
|
network_a = {
|
|
files = filesA;
|
|
};
|
|
network_b = {
|
|
files = filesB;
|
|
};
|
|
};
|
|
};
|
|
};
|
|
|
|
testScript = ''
|
|
# Network names: home network is always "home", extra networks use their configured names
|
|
# For network A nodes: home="home" (file_a), extra="network_c" (file_c)
|
|
# For network B nodes: home="home" (file_b), extra="network_c" (file_c)
|
|
# For network C nodes: home="home" (file_c), extra="network_a" (file_a), extra="network_b" (file_b)
|
|
|
|
def file_path(net_name, filename):
|
|
"""Return the on-disk path for a file under a given network."""
|
|
return f"/var/lib/data-mesher/files/{net_name}/{filename}"
|
|
|
|
def upload_file(node, filename, content, key_path, network_pub):
|
|
"""Upload a file to a node's home network via the CLI."""
|
|
node.succeed(f"echo -n '{content}' > /tmp/{filename}")
|
|
node.succeed(f"data-mesher file update /tmp/{filename} --url http://[::1]:7331 --key {key_path} --network-id {network_pub}")
|
|
|
|
def wait_for_file(node, net_name, filename, expected_content, timeout=60):
|
|
"""Wait until a file exists with the expected content."""
|
|
fp = file_path(net_name, filename)
|
|
node.wait_until_succeeds(
|
|
f"test -f {fp} && grep -q '{expected_content}' {fp}",
|
|
timeout
|
|
)
|
|
|
|
def file_absent(node, net_name, filename):
|
|
"""Assert that a file does NOT exist under the given network."""
|
|
fp = file_path(net_name, filename)
|
|
status = node.succeed(f"test -f {fp} && echo yes || echo no").strip()
|
|
assert status == "no", f"Expected {fp} to be absent on {node.name}, but it exists"
|
|
|
|
# Wait for all nodes to start
|
|
all_nodes = [a1, a2, a3, b1, b2, b3, c1, c2, c3]
|
|
for node in all_nodes:
|
|
node.wait_for_unit("data-mesher.service")
|
|
|
|
# ========================================
|
|
# Test 1: Intra-network propagation (A)
|
|
# ========================================
|
|
|
|
upload_file(a1, "file_a", "hello_from_a", "${./keys/signers/alice.key}", "${./keys/network_a/network.pub}")
|
|
# On A nodes, file_a is stored in "home" (their home network)
|
|
wait_for_file(a1, "home", "file_a", "hello_from_a")
|
|
wait_for_file(a2, "home", "file_a", "hello_from_a")
|
|
wait_for_file(a3, "home", "file_a", "hello_from_a")
|
|
print("Test 1 PASSED: Intra-network propagation (A)")
|
|
|
|
# ========================================
|
|
# Test 2: Cross-network A -> C
|
|
# ========================================
|
|
|
|
# On C nodes, file_a is stored in "network_a" (their extra network)
|
|
wait_for_file(c1, "network_a", "file_a", "hello_from_a")
|
|
wait_for_file(c2, "network_a", "file_a", "hello_from_a")
|
|
wait_for_file(c3, "network_a", "file_a", "hello_from_a")
|
|
print("Test 2 PASSED: Cross-network A -> C")
|
|
|
|
# ========================================
|
|
# Test 3: Intra-network propagation (B)
|
|
# ========================================
|
|
|
|
upload_file(b1, "file_b", "hello_from_b", "${./keys/signers/bob.key}", "${./keys/network_b/network.pub}")
|
|
# On B nodes, file_b is stored in "home" (their home network)
|
|
wait_for_file(b1, "home", "file_b", "hello_from_b")
|
|
wait_for_file(b2, "home", "file_b", "hello_from_b")
|
|
wait_for_file(b3, "home", "file_b", "hello_from_b")
|
|
print("Test 3 PASSED: Intra-network propagation (B)")
|
|
|
|
# ========================================
|
|
# Test 4: Cross-network B -> C
|
|
# ========================================
|
|
|
|
# On C nodes, file_b is stored in "network_b" (their extra network)
|
|
wait_for_file(c1, "network_b", "file_b", "hello_from_b")
|
|
wait_for_file(c2, "network_b", "file_b", "hello_from_b")
|
|
wait_for_file(c3, "network_b", "file_b", "hello_from_b")
|
|
print("Test 4 PASSED: Cross-network B -> C")
|
|
|
|
# ========================================
|
|
# Test 5: Cross-network C -> A and C -> B
|
|
# ========================================
|
|
|
|
upload_file(c1, "file_c", "hello_from_c", "${./keys/signers/charlie.key}", "${./keys/network_c/network.pub}")
|
|
# On C nodes, file_c is stored in "home" (their home network)
|
|
wait_for_file(c1, "home", "file_c", "hello_from_c")
|
|
wait_for_file(c2, "home", "file_c", "hello_from_c")
|
|
wait_for_file(c3, "home", "file_c", "hello_from_c")
|
|
|
|
# C -> A: On A nodes, file_c is stored in "network_c" (their extra network)
|
|
wait_for_file(a1, "network_c", "file_c", "hello_from_c")
|
|
wait_for_file(a2, "network_c", "file_c", "hello_from_c")
|
|
wait_for_file(a3, "network_c", "file_c", "hello_from_c")
|
|
|
|
# C -> B: On B nodes, file_c is stored in "network_c" (their extra network)
|
|
wait_for_file(b1, "network_c", "file_c", "hello_from_c")
|
|
wait_for_file(b2, "network_c", "file_c", "hello_from_c")
|
|
wait_for_file(b3, "network_c", "file_c", "hello_from_c")
|
|
print("Test 5 PASSED: Cross-network C -> A and C -> B")
|
|
|
|
# ========================================
|
|
# Test 6: Network isolation (A <-> B)
|
|
# ========================================
|
|
|
|
# file_a should NOT be on any B nodes (B nodes don't have network_a configured)
|
|
# Check both default (where file_b is) and network_a (which doesn't exist for B nodes)
|
|
for node in [b1, b2, b3]:
|
|
file_absent(node, "network_a", "file_a")
|
|
|
|
# file_b should NOT be on any A nodes (A nodes don't have network_b configured)
|
|
# Check both default (where file_a is) and network_b (which doesn't exist for A nodes)
|
|
for node in [a1, a2, a3]:
|
|
file_absent(node, "network_b", "file_b")
|
|
|
|
print("Test 6 PASSED: Network isolation (A <-> B)")
|
|
|
|
print("All tests PASSED!")
|
|
'';
|
|
}
|