Files
waste-go/internal/proto/proto.go
Fredrik Johansson 13fb7ba1fe 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>
2026-06-21 19:13:54 +02:00

267 lines
10 KiB
Go

// Package proto defines all wire types shared between the daemon and anchor.
// Everything on the wire is newline-delimited JSON.
// Binary data (keys, signatures) is hex-encoded; signaling boxes are base64.
package proto
import "time"
// ── Identity ──────────────────────────────────────────────────────────────────
// PeerID is a peer's stable identity: lowercase hex of the 32-byte Ed25519 public key (64 chars).
// This IS the peer — display names are advisory only and unauthenticated.
type PeerID string
// Short returns the first 16 hex chars grouped in 4s: "a1b2 c3d4 e5f6 0718".
func (p PeerID) Short() string {
s := string(p)
if len(s) < 16 {
return s
}
return s[0:4] + " " + s[4:8] + " " + s[8:12] + " " + s[12:16]
}
// PeerInfo is a peer's self-description, included in the hello confirmation.
type PeerInfo struct {
ID PeerID `json:"id"`
Alias string `json:"alias"` // advisory, not authenticated
PublicKey string `json:"public_key"` // Ed25519 pubkey, hex
CreatedAt time.Time `json:"created_at"`
}
// ── Peer-to-peer message types (over the "yaw" DataChannel) ──────────────────
// MsgType identifies the kind of peer message.
type MsgType string
const (
MsgChat MsgType = "chat"
MsgPeerGossip MsgType = "peer_gossip"
MsgFileListReq MsgType = "file_list_req"
MsgFileListResp MsgType = "file_list_resp"
MsgFileOffer MsgType = "file_offer"
MsgFileResp MsgType = "file_response"
MsgFileDone MsgType = "file_done"
MsgPing MsgType = "ping"
MsgPong MsgType = "pong"
)
// PeerMessage is the top-level container sent over the "yaw" DataChannel.
// File chunks go over a separate binary DataChannel labeled "f:<xid>".
type PeerMessage struct {
Type MsgType `json:"type"`
// Only one of these will be set, depending on Type.
Chat *ChatMessage `json:"chat,omitempty"`
Gossip *PeerGossip `json:"gossip,omitempty"`
FileListResp *FileListResp `json:"file_list_resp,omitempty"`
FileOffer *FileOffer `json:"file_offer,omitempty"`
FileResp *FileResponse `json:"file_response,omitempty"`
FileDone *FileDone `json:"file_done,omitempty"`
Seq *uint64 `json:"seq,omitempty"` // for ping/pong
}
// ChatMessage is a message to a room or a DM.
type ChatMessage struct {
Mid string `json:"mid"` // random 16-byte hex, for deduplication (YAW/2 §8)
ID string `json:"id"` // internal uuid, kept for local use
From PeerID `json:"from"`
To *PeerID `json:"to,omitempty"` // nil = broadcast to room
Room string `json:"room"`
Body string `json:"body"`
SentAt time.Time `json:"sent_at"`
}
// PeerGossip shares known peer addresses.
type PeerGossip struct {
Peers []GossipEntry `json:"peers"`
}
// GossipEntry is one peer hint shared via gossip.
type GossipEntry struct {
Peer PeerInfo `json:"peer"`
AddrHint string `json:"addr_hint"` // IP:port hint, may be behind NAT
LastSeen time.Time `json:"last_seen"`
}
// FileEntry describes a single file in a peer's shared directory.
type FileEntry struct {
Name string `json:"name"`
SizeBytes int64 `json:"size_bytes"`
}
// FileListResp is the payload for MsgFileListResp.
// MsgFileListReq carries no payload — it is a zero-field request.
type FileListResp struct {
Files []FileEntry `json:"files"`
}
// FileOffer initiates a file transfer.
type FileOffer struct {
Mid string `json:"mid"` // dedup id
Xid string `json:"xid"` // transfer id, used as DataChannel label "f:<xid>"
Filename string `json:"filename"`
SizeBytes int64 `json:"size_bytes"`
SHA256 string `json:"sha256"` // hex
}
// FileResponse accepts or declines a FileOffer.
type FileResponse struct {
Mid string `json:"mid"`
Xid string `json:"xid"`
Accepted bool `json:"accepted"`
}
// FileDone signals that all chunks have been sent. Receiver verifies SHA256.
type FileDone struct {
Mid string `json:"mid"`
Xid string `json:"xid"`
SHA256 string `json:"sha256"` // hex
}
// ── DataChannel hello (YAW/2 §6) ─────────────────────────────────────────────
// HelloMessage is the first message sent on the "yaw" DataChannel.
// The signature binds this identity to the specific DTLS session.
type HelloMessage struct {
Type string `json:"type"` // always "hello"
ID string `json:"id"` // hex pubkey
Nick string `json:"nick"` // alias
Caps []string `json:"caps"` // capability list, e.g. ["chat","file"]
Sig string `json:"sig"` // hex ed25519 sig over HelloBindString
}
// HelloBindString returns the bytes the hello signature covers:
// "yaw/2 bind" || localDTLSFingerprint(32 bytes) || remoteDTLSFingerprint(32 bytes)
func HelloBindString(localFP, remoteFP []byte) []byte {
buf := []byte("yaw/2 bind")
buf = append(buf, localFP...)
buf = append(buf, remoteFP...)
return buf
}
// ── Signaling payload (sealed inside nacl/box, exchanged via anchor) ──────────
// SignalingKind identifies the kind of sealed signaling payload.
type SignalingKind string
const (
SigOffer SignalingKind = "offer"
SigAnswer SignalingKind = "answer"
SigCandidate SignalingKind = "candidate"
SigBye SignalingKind = "bye"
)
// SignalingPayload is the JSON plaintext sealed inside a crypto_box (YAW/2 §5).
type SignalingPayload struct {
Kind SignalingKind `json:"kind"`
SDP string `json:"sdp,omitempty"` // offer / answer
Cand string `json:"cand,omitempty"` // trickle ICE candidate line
Mid string `json:"mid,omitempty"` // media stream id for candidate
MLine int `json:"mline,omitempty"` // media line index
}
// ── Anchor WebSocket wire types (YAW/2 §5) ────────────────────────────────────
// AnchorMsgType identifies anchor WebSocket messages.
type AnchorMsgType string
const (
AnchorChallenge AnchorMsgType = "challenge"
AnchorJoin AnchorMsgType = "join"
AnchorJoined AnchorMsgType = "joined"
AnchorPeerJoin AnchorMsgType = "peer-join"
AnchorPeerLeave AnchorMsgType = "peer-leave"
AnchorTo AnchorMsgType = "to"
AnchorFrom AnchorMsgType = "from"
AnchorNoPeer AnchorMsgType = "no-peer"
)
// AnchorMessage covers all WebSocket frames to/from the anchor.
type AnchorMessage struct {
Type AnchorMsgType `json:"type"`
Nonce string `json:"nonce,omitempty"` // challenge nonce, hex
ID string `json:"id,omitempty"` // peer hex id
Net string `json:"net,omitempty"` // hashed network name
Sig string `json:"sig,omitempty"` // ed25519 sig over (nonce||net), hex
Peers []string `json:"peers,omitempty"` // joined: list of peer hex ids in network
To string `json:"to,omitempty"` // target peer hex id
From string `json:"from,omitempty"` // sender peer hex id
Box string `json:"box,omitempty"` // base64 nacl/box sealed payload
}
// ── IPC protocol (daemon ↔ local UI) ─────────────────────────────────────────
// IpcMsgType identifies IPC messages.
type IpcMsgType string
const (
// Commands (UI → daemon)
CmdSendMessage IpcMsgType = "send_message"
CmdJoinNetwork IpcMsgType = "join_network" // fields: network_name (plaintext)
CmdLeaveNetwork IpcMsgType = "leave_network"
CmdGetState IpcMsgType = "get_state"
CmdSendFile IpcMsgType = "send_file"
CmdGenerateInvite IpcMsgType = "generate_invite"
CmdGetFileList IpcMsgType = "get_file_list"
// Events (daemon → UI)
EvtMessageReceived IpcMsgType = "message_received"
EvtPeerConnected IpcMsgType = "peer_connected"
EvtPeerDisconnected IpcMsgType = "peer_disconnected"
EvtSessionReady IpcMsgType = "session_ready" // DataChannel open + hello verified
EvtIncomingFile IpcMsgType = "incoming_file"
EvtFileProgress IpcMsgType = "file_progress"
EvtStateSnapshot IpcMsgType = "state_snapshot"
EvtError IpcMsgType = "error"
EvtInviteGenerated IpcMsgType = "invite_generated"
EvtFileList IpcMsgType = "file_list"
EvtNetworkJoined IpcMsgType = "network_joined"
EvtNetworkLeft IpcMsgType = "network_left"
)
// NetworkInfo summarises one joined network for state_snapshot and network_joined events.
type NetworkInfo struct {
NetworkID string `json:"network_id"`
NetworkName string `json:"network_name"`
LocalPeer *PeerInfo `json:"local_peer,omitempty"`
}
// IpcMessage covers both commands and events.
type IpcMessage struct {
Type IpcMsgType `json:"type"`
// optional: scopes a command/event to a specific network.
// When absent, defaults to the first (or only) joined network.
NetworkID string `json:"network_id,omitempty"`
// send_message
Room string `json:"room,omitempty"`
To *PeerID `json:"to,omitempty"`
Body string `json:"body,omitempty"`
// join_network / leave_network
NetworkName string `json:"network_name,omitempty"`
// send_file
Path string `json:"path,omitempty"`
// events
Peer *PeerInfo `json:"peer,omitempty"`
PeerID *PeerID `json:"peer_id,omitempty"`
Nick string `json:"nick,omitempty"`
Message *ChatMessage `json:"message,omitempty"`
Offer *FileOffer `json:"offer,omitempty"`
TransferID string `json:"transfer_id,omitempty"`
BytesReceived int64 `json:"bytes_received,omitempty"`
TotalBytes int64 `json:"total_bytes,omitempty"`
// state_snapshot fields (existing shape preserved for backward compat)
LocalPeer *PeerInfo `json:"local_peer,omitempty"`
ConnectedPeers []PeerInfo `json:"connected_peers,omitempty"`
Rooms []string `json:"rooms,omitempty"`
// multi-network: all joined networks (additive)
Networks []NetworkInfo `json:"networks,omitempty"`
ErrorMessage string `json:"error_message,omitempty"`
InviteString string `json:"invite,omitempty"`
Files []FileEntry `json:"files,omitempty"`
}