Bring wire protocol into full YAW/2 spec compliance

Five interop issues fixed against PROTOCOL.md:

- Join signature now covers nonce_raw || net_ascii (64-char UTF-8 hex
  string) as specified in §5.1, not net_raw_bytes. Both anchor server
  and client updated to match.

- Chat wire fields renamed to spec names: text/ts (Unix ms int64)
  replacing body/sent_at (ISO timestamp). Flat layout on PeerMessage
  matches §8 exactly; store and TUI updated accordingly.

- Direct messages now use the spec "pm" type (flat {type,mid,text,ts})
  instead of chat+to. Receiver reconstructs a ChatMessage with
  dm:<short-id> room for IPC/storage. §8 compliant.

- File transfer message types changed to spec hyphenated names:
  file-offer, file-accept, file-cancel, file-done with spec field
  names (name/size not filename/size_bytes). §9 compliant.

- DataChannel open-race (§14 gotcha #3) fixed with sync.Once: doOpen
  fires on OnOpen callback or immediately if the channel is already
  open when WireDataChannel is called (answerer race).

Also fixes two bugs found during testing:

- mid was missing from outgoing wire messages, causing all received
  messages to arrive with mid="" and collide on the UNIQUE DB
  constraint. mid is now included on all sent chat/pm messages; a
  random mid is generated for any received message that omits it.

- Test scripts hardened: kill -9 + active lsof polling replaces blind
  sleep for port cleanup; join_network sent before peer_field queries
  (local_peer is now network-scoped and nil until joined).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Fredrik Johansson
2026-06-22 11:38:01 +02:00
parent 13fb7ba1fe
commit b87f14a361
11 changed files with 623 additions and 126 deletions

View File

@@ -16,7 +16,6 @@ import (
"net"
"time"
"github.com/google/uuid"
"github.com/waste-go/internal/invite"
"github.com/waste-go/internal/netmgr"
"github.com/waste-go/internal/proto"
@@ -49,9 +48,11 @@ func handleClient(conn net.Conn, mgr *netmgr.Manager) {
writeCh := make(chan []byte, 128)
done := make(chan struct{})
writerDone := make(chan struct{})
// Writer goroutine.
go func() {
defer close(writerDone)
w := bufio.NewWriter(conn)
for line := range writeCh {
line = append(line, '\n')
@@ -140,30 +141,63 @@ func handleClient(conn net.Conn, mgr *netmgr.Manager) {
send(errMsg("send_message: not joined to any network"))
continue
}
msg := &proto.ChatMessage{
Mid: randomHex(16),
ID: uuid.NewString(),
From: n.Identity.PeerID(),
To: cmd.To,
Room: cmd.Room,
Body: cmd.Body,
SentAt: time.Now(),
}
payload, err := json.Marshal(proto.PeerMessage{Type: proto.MsgChat, Chat: msg})
if err != nil {
continue
}
ts := time.Now().UnixMilli()
if cmd.To != nil {
n.Mesh.SendTo(*cmd.To, payload)
// DM → spec "pm" type: flat {type, mid, text, ts} on the wire
mid := randomHex(16)
wire, err := json.Marshal(proto.PeerMessage{
Type: proto.MsgPm,
Mid: mid,
Text: cmd.Body,
Ts: ts,
})
if err != nil {
continue
}
n.Mesh.SendTo(*cmd.To, wire)
// Store locally with dm:<short-id> room convention
local := &proto.ChatMessage{
Mid: mid,
From: n.Identity.PeerID(),
To: cmd.To,
Room: "dm:" + (*cmd.To).Short(),
Text: cmd.Body,
Ts: ts,
}
n.Mesh.SaveMessage(local)
n.Mesh.Emit(proto.IpcMessage{
Type: proto.EvtMessageReceived,
NetworkID: n.ID,
Message: local,
})
} else {
n.Mesh.Broadcast(payload)
// Group chat → spec "chat" type: flat {type, mid, room, text, ts}
mid := randomHex(16)
wire, err := json.Marshal(proto.PeerMessage{
Type: proto.MsgChat,
Mid: mid,
Room: cmd.Room,
Text: cmd.Body,
Ts: ts,
})
if err != nil {
continue
}
n.Mesh.Broadcast(wire)
local := &proto.ChatMessage{
Mid: mid,
From: n.Identity.PeerID(),
Room: cmd.Room,
Text: cmd.Body,
Ts: ts,
}
n.Mesh.SaveMessage(local)
n.Mesh.Emit(proto.IpcMessage{
Type: proto.EvtMessageReceived,
NetworkID: n.ID,
Message: local,
})
}
n.Mesh.SaveMessage(msg)
n.Mesh.Emit(proto.IpcMessage{
Type: proto.EvtMessageReceived,
NetworkID: n.ID,
Message: msg,
})
case proto.CmdGetState:
send(stateSnapshot(mgr))
@@ -222,6 +256,7 @@ func handleClient(conn net.Conn, mgr *netmgr.Manager) {
close(done)
close(writeCh)
<-writerDone // wait for writer to flush before conn.Close() fires
log.Printf("ipc: UI client disconnected")
}