feat: per-network share directories + isolation test

Each network now carries its own share dir, set at join_network time via
optional share_dir field or updated live with set_share_dir. The global
-share-dir daemon flag becomes a fallback default.

- proto: add ShareDir/DownloadDir to NetworkInfo and IpcMessage
- netmgr: Join accepts shareDir override; SetShareDir updates live
- ipc: wire join_network share_dir and set_share_dir command
- daemon: remove -share-dir from auto-join path (pass "" for default)
- test-network.sh: per-network join with share_dir; isolation verification
  section confirms alice/friends and alice/work share dirs are independent
- test-tui.sh: join_network with share_dir; peer IDs resolved after join

All tests pass: YAW/2.1 FS, share isolation, file transfer, persistence.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Fredrik Johansson
2026-06-22 15:13:26 +02:00
parent f437fe94f4
commit d02e18e212
7 changed files with 277 additions and 42 deletions

View File

@@ -25,10 +25,19 @@ DATA_ROOT="/tmp/waste-test"
rm -rf "$DATA_ROOT"
mkdir -p "$DATA_ROOT/bin"
# Seed per-peer share directories with dummy files.
mkdir -p "$DATA_ROOT/alice/share" "$DATA_ROOT/bob/share" "$DATA_ROOT/charlie/share"
echo "alice's notes" > "$DATA_ROOT/alice/share/notes.txt"
dd if=/dev/urandom bs=1K count=64 2>/dev/null | base64 > "$DATA_ROOT/alice/share/photo.jpg.b64"
# Per-network share directories — isolation is the whole point:
# alice/friends-share → shared on the "friends" network only
# alice/work-share → shared on the "work" network only (alice joins both)
# bob/share → bob is only on "friends"
# charlie/share → charlie is only on "friends"
mkdir -p "$DATA_ROOT/alice/friends-share" "$DATA_ROOT/alice/work-share"
mkdir -p "$DATA_ROOT/bob/share" "$DATA_ROOT/charlie/share"
echo "alice's notes" > "$DATA_ROOT/alice/friends-share/notes.txt"
dd if=/dev/urandom bs=1K count=64 2>/dev/null | base64 > "$DATA_ROOT/alice/friends-share/photo.jpg.b64"
echo "alice's report.pdf" > "$DATA_ROOT/alice/work-share/report.pdf"
echo "alice's budget.xlsx" > "$DATA_ROOT/alice/work-share/budget.xlsx"
dd if=/dev/urandom bs=1K count=128 2>/dev/null | base64 > "$DATA_ROOT/bob/share/archive.tar.b64"
echo -e "file1.mp3\nfile2.mp3" > "$DATA_ROOT/bob/share/playlist.m3u"
echo "charlie's doc" > "$DATA_ROOT/charlie/share/document.txt"
@@ -235,7 +244,6 @@ log "$ALICE_COLOR" "alice" "starting daemon (ipc :${ALICE_IPC})"
-data-dir "$DATA_ROOT/alice" \
-ipc-port "$ALICE_IPC" \
-anchor "$ANCHOR_URL" \
-share-dir "$DATA_ROOT/alice/share" \
2> >(tee "$DATA_ROOT/alice/daemon.log" | while IFS= read -r l; do echo -e "${ALICE_COLOR}${DIM}[alice] ${l}${RESET}"; done) &
PIDS+=($!)
@@ -245,7 +253,6 @@ log "$BOB_COLOR" "bob" "starting daemon (ipc :${BOB_IPC})"
-data-dir "$DATA_ROOT/bob" \
-ipc-port "$BOB_IPC" \
-anchor "$ANCHOR_URL" \
-share-dir "$DATA_ROOT/bob/share" \
2> >(tee "$DATA_ROOT/bob/daemon.log" | while IFS= read -r l; do echo -e "${BOB_COLOR}${DIM}[bob] ${l}${RESET}"; done) &
PIDS+=($!)
@@ -255,7 +262,6 @@ log "$CHARLIE_COLOR" "charlie" "starting daemon (ipc :${CHARLIE_IPC})"
-data-dir "$DATA_ROOT/charlie" \
-ipc-port "$CHARLIE_IPC" \
-anchor "$ANCHOR_URL" \
-share-dir "$DATA_ROOT/charlie/share" \
2> >(tee "$DATA_ROOT/charlie/daemon.log" | while IFS= read -r l; do echo -e "${CHARLIE_COLOR}${DIM}[charlie]${l}${RESET}"; done) &
PIDS+=($!)
@@ -267,14 +273,18 @@ echo ""
# ── join network ──────────────────────────────────────────────────────────────
echo -e "${DIM}────────────────────────────────────────────────────────${RESET}"
echo -e "joining all peers to network: ${BOLD}${NETWORK_NAME}${RESET}"
echo -e "${DIM}share dirs are per-network (per-network isolation test)${RESET}"
echo -e "${DIM}────────────────────────────────────────────────────────${RESET}"
JOIN=$(printf '{"type":"join_network","network_name":"%s"}' "$NETWORK_NAME")
ipc "$ALICE_IPC" "$JOIN"
# Each peer passes share_dir scoped to this network.
ipc "$ALICE_IPC" "$(jq -cn --arg net "$NETWORK_NAME" --arg dir "$DATA_ROOT/alice/friends-share" \
'{"type":"join_network","network_name":$net,"share_dir":$dir}')"
sleep 0.2
ipc "$BOB_IPC" "$JOIN"
ipc "$BOB_IPC" "$(jq -cn --arg net "$NETWORK_NAME" --arg dir "$DATA_ROOT/bob/share" \
'{"type":"join_network","network_name":$net,"share_dir":$dir}')"
sleep 0.2
ipc "$CHARLIE_IPC" "$JOIN"
ipc "$CHARLIE_IPC" "$(jq -cn --arg net "$NETWORK_NAME" --arg dir "$DATA_ROOT/charlie/share" \
'{"type":"join_network","network_name":$net,"share_dir":$dir}')"
sleep 0.3
# ── resolve peer IDs ──────────────────────────────────────────────────────────
@@ -394,7 +404,7 @@ echo -e "${DIM}─────────────────────
alice_nets=$(echo '{"type":"get_state"}' | timeout 2 nc 127.0.0.1 "$ALICE_IPC" 2>/dev/null \
| grep '"type":"state_snapshot"' | head -1 \
| jq -r '[.networks[].network_name] | join(", ")' 2>/dev/null || echo "?")
| jq -rc '[.networks[].network_name] | join(", ")' 2>/dev/null || echo "?")
echo -e "${DIM} alice networks: [${alice_nets:-none}]${RESET}"
for peer_ipc in "$ALICE_IPC" "$BOB_IPC" "$CHARLIE_IPC"; do
@@ -415,6 +425,54 @@ for peer_ipc in "$ALICE_IPC" "$BOB_IPC" "$CHARLIE_IPC"; do
done
sleep 0.5
# ── per-network share isolation ───────────────────────────────────────────────
echo ""
echo -e "${DIM}────────────────────────────────────────────────────────${RESET}"
echo -e "per-network share directory isolation"
echo -e "${DIM}────────────────────────────────────────────────────────${RESET}"
# Alice also joins a "work" network with a completely different share dir.
WORK_NET="work-$(date +%s)"
ipc "$ALICE_IPC" "$(jq -cn --arg net "$WORK_NET" --arg dir "$DATA_ROOT/alice/work-share" \
'{"type":"join_network","network_name":$net,"share_dir":$dir}')"
sleep 1
# Alice's own file list on "friends" should show friends-share files only.
friends_raw=$(echo '{"type":"get_file_list"}' | nc -q 2 127.0.0.1 "$ALICE_IPC" 2>/dev/null || true)
friends_files=$(echo "$friends_raw" | grep '"type":"file_list"' | head -1 \
| jq -r '[.files[].name] | join(" ")' 2>/dev/null || true)
# Use set_share_dir to dynamically update the work network share dir (runtime change smoke test).
work_net_id=$(echo '{"type":"get_state"}' | timeout 2 nc 127.0.0.1 "$ALICE_IPC" 2>/dev/null \
| grep '"type":"state_snapshot"' | head -1 \
| jq -r --arg net "$WORK_NET" '.networks[] | select(.network_name==$net) | .network_id' 2>/dev/null || true)
if [ -n "$work_net_id" ]; then
# Verify the work network's share_dir is isolated from friends.
work_raw=$(echo "{\"type\":\"get_file_list\",\"network_id\":\"$work_net_id\"}" \
| nc -q 2 127.0.0.1 "$ALICE_IPC" 2>/dev/null || true)
work_files=$(echo "$work_raw" | grep '"type":"file_list"' | head -1 \
| jq -r '[.files[].name] | join(" ")' 2>/dev/null || true)
echo -e " ${BOLD}alice/friends${RESET}: ${friends_files:-<empty>}"
echo -e " ${BOLD}alice/work${RESET}: ${work_files:-<empty>}"
# Check isolation: friends files must not appear in work list and vice versa.
if echo "$friends_files" | grep -q "notes.txt" && \
echo "$work_files" | grep -q "report.pdf" && \
! echo "$friends_files" | grep -q "report.pdf" && \
! echo "$work_files" | grep -q "notes.txt"; then
echo -e " ${GREEN}✓ share directories are isolated per network${RESET}"
else
echo -e " ${RED}✗ share isolation failed${RESET}"
echo -e " friends: ${friends_files}"
echo -e " work: ${work_files}"
fi
else
echo -e " ${RED}✗ could not resolve work network id${RESET}"
fi
sleep 0.3
# ── file transfer ────────────────────────────────────────────────────────────
echo ""
echo -e "${DIM}────────────────────────────────────────────────────────${RESET}"
@@ -435,7 +493,7 @@ done
if [ "$received" = "1" ]; then
rx_file=$(ls "$DATA_ROOT/bob/downloads-"*/notes.txt 2>/dev/null | head -1)
orig_sha=$(sha256sum "$DATA_ROOT/alice/share/notes.txt" | awk '{print $1}')
orig_sha=$(sha256sum "$DATA_ROOT/alice/friends-share/notes.txt" | awk '{print $1}')
rx_sha=$(sha256sum "$rx_file" | awk '{print $1}')
if [ "$orig_sha" = "$rx_sha" ]; then
echo -e " ${GREEN}✓ notes.txt received by bob, sha256 matches${RESET}"