Multi-network foundation: netmgr, derived identities, additive IPC protocol

YAW/2 peer wire protocol is unchanged. Changes are local only.

internal/crypto:
- DeriveForNetwork(master, networkHash) — HKDF-SHA256 from master seed + network hash;
  same master + same network always produces the same Ed25519 keypair (stable peer ID)

internal/proto:
- NetworkInfo type (network_id, network_name, local_peer)
- NetworkID field on IpcMessage (optional; commands default to first network when absent)
- Networks []NetworkInfo on state_snapshot (additive alongside existing local_peer)
- EvtNetworkJoined / EvtNetworkLeft events

internal/netmgr (new):
- Manager holds N independent Network contexts (derived identity, mesh, store, anchor)
- Join(name) creates context, derives identity, opens per-network DB, starts anchor client
- Leave(id) / LeaveAll() cancel contexts and close stores
- Resolve(netID) returns named network, or Default() when netID is empty (backward compat)
- Fan-out: Manager.Subscribe() receives tagged events from all networks
- Network IDs are the first 8 hex chars of SHA-256("yaw2-net:"+name) — stable and short

internal/ipc:
- Run(mgr, port) replaces Run(m *mesh.Mesh, port, anchorURL, joinFn)
- Commands without network_id route to mgr.Default() (backward compat)
- state_snapshot includes Networks array; local_peer/connected_peers still populated from first network
- generate_invite, get_file_list, send_message all respect network_id routing

cmd/daemon:
- Creates netmgr.Manager instead of mesh.Mesh directly
- --join and --share-dir pass through Config
- Auto-join via mgr.Join() before IPC starts

test-network.sh:
- Fix peer_name bash bug: $() with && inside triggers set -e; use if/elif/else instead

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Fredrik Johansson
2026-06-21 19:13:54 +02:00
parent 8d3ca9d331
commit 13fb7ba1fe
6 changed files with 437 additions and 141 deletions

View File

@@ -3,19 +3,14 @@
package main
import (
"context"
"flag"
"log"
"os"
"path/filepath"
"github.com/waste-go/internal/anchor"
"github.com/waste-go/internal/crypto"
"github.com/waste-go/internal/invite"
"github.com/waste-go/internal/ipc"
"github.com/waste-go/internal/mesh"
"github.com/waste-go/internal/proto"
"github.com/waste-go/internal/store"
"github.com/waste-go/internal/invite"
"github.com/waste-go/internal/netmgr"
)
func main() {
@@ -27,6 +22,14 @@ func main() {
joinInvite := flag.String("join", "", "waste: invite string — sets anchor URL and auto-joins the network on startup")
flag.Parse()
dir := expandHome(*dataDir)
id, err := crypto.LoadOrCreate(dir, *alias)
if err != nil {
log.Fatalf("identity: %v", err)
}
log.Printf("daemon: local peer id: %s alias: %s", id.PeerID().Short(), id.Alias)
// --join overrides/sets the anchor URL and triggers an auto-join.
var autoJoinNetwork string
if *joinInvite != "" {
@@ -39,45 +42,20 @@ func main() {
log.Printf("daemon: invite decoded — anchor=%s network=%s", inv.Anchor, inv.Network)
}
dir := expandHome(*dataDir)
id, err := crypto.LoadOrCreate(dir, *alias)
if err != nil {
log.Fatalf("identity: %v", err)
}
log.Printf("daemon: local peer id: %s alias: %s", id.PeerID().Short(), id.Alias)
mgr := netmgr.New(netmgr.Config{
MasterIdentity: id,
StoreDir: dir,
AnchorURL: *anchorURL,
ShareDir: expandHome(*shareDir),
})
st, err := store.Open(filepath.Join(dir, "messages.db"))
if err != nil {
log.Fatalf("store: %v", err)
}
defer st.Close()
m := mesh.New(id, st)
if *shareDir != "" {
m.ShareDir = expandHome(*shareDir)
log.Printf("daemon: sharing %s", m.ShareDir)
}
// joinFn is passed to the IPC layer; it's called when the UI sends join_network.
joinFn := func(ctx context.Context, networkName string) {
if *anchorURL == "" {
log.Printf("daemon: join_network: no -anchor flag set")
m.Emit(proto.IpcMessage{Type: proto.EvtError, ErrorMessage: "no anchor configured — start daemon with -anchor <url>"})
return
}
log.Printf("daemon: joining network %q via %s", networkName, *anchorURL)
anchor.Run(ctx, *anchorURL, networkName, id, m)
log.Printf("daemon: left network %q", networkName)
}
// Auto-join from --join flag before starting IPC (non-blocking).
if autoJoinNetwork != "" {
ctx, cancel := context.WithCancel(context.Background())
_ = cancel // lifecycle managed by the joinFn / ipc.Run leave
go joinFn(ctx, autoJoinNetwork)
if _, err := mgr.Join(autoJoinNetwork); err != nil {
log.Fatalf("auto-join: %v", err)
}
}
if err := ipc.Run(m, *ipcPort, *anchorURL, joinFn); err != nil {
if err := ipc.Run(mgr, *ipcPort); err != nil {
log.Fatalf("ipc: %v", err)
}
}
@@ -90,4 +68,3 @@ func expandHome(path string) string {
}
return path
}