Migrate to YAW/2 protocol: WebRTC transport, anchor signaling, nacl/box crypto
Replace the original raw-TCP peer connections and Blowfish/RSA-era design with the YAW/2 protocol stack: Transport - internal/mesh/peer.go: raw TCP + manual ECDH replaced with pion/webrtc DataChannels (ICE, DTLS, SCTP). Peers negotiate via sealed offer/answer/ candidate exchange rather than direct dialling. - internal/nat: deleted — ICE subsumes everything this package was going to do. Signaling - cmd/anchor: new WebSocket signaling server (replaces cmd/relay). Verifies Ed25519 challenge/join signatures, routes opaque nacl/box blobs, broadcasts peer-join/peer-leave events. Never sees plaintext signaling content. - internal/anchor: new anchor client. Manages PeerConnection lifecycle, decides offerer by peer-id comparison, seals/opens signaling payloads with nacl/box, implements mesh.Anchor. Crypto (internal/crypto) - PeerID encoding: base64url → lowercase hex (64 chars, YAW/2 §2). - Sign/Verify: hex signatures throughout. - Added CurvePublicKey/CurvePrivateKey: X25519 keys derived from Ed25519 identity via Montgomery conversion (filippo.io/edwards25519), matching libsodium's crypto_sign_ed25519_*_to_curve25519. - Added SignalingBox/SignalingOpen: nacl/box (XSalsa20-Poly1305) seal/open for signaling payloads. Wire types (internal/proto) - ChatMessage gains Mid (random 16-byte hex) for mesh deduplication (§8). - FileChunk removed from PeerMessage — chunks go on a separate binary DataChannel labelled "f:<xid>"; FileDone added. - New types: SignalingPayload, AnchorMessage, HelloMessage, HelloBindString. - PeerID.Short() now returns first 16 hex chars grouped in 4s. - IPC: CmdConnect removed; CmdJoinNetwork/CmdLeaveNetwork added; EvtSessionReady added (fires when DataChannel opens and hello is received). IPC (internal/ipc) - join_network/leave_network replace connect (direct TCP dial). - Network joins are context-scoped: leave_network or client disconnect cancels the anchor connection cleanly. README updated to reflect new project layout, getting-started commands, IPC protocol, and roadmap state. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,75 +1,50 @@
|
||||
// Package proto defines all wire types shared between the daemon and relay.
|
||||
// Package proto defines all wire types shared between the daemon and anchor.
|
||||
// Everything on the wire is newline-delimited JSON.
|
||||
// Binary data (keys, signatures, ciphertext) is base64url encoded.
|
||||
// Binary data (keys, signatures) is hex-encoded; signaling boxes are base64.
|
||||
package proto
|
||||
|
||||
import "time"
|
||||
|
||||
// ── Identity ──────────────────────────────────────────────────────────────────
|
||||
|
||||
// PeerID is a peer's stable identity: their Ed25519 public key, base64url encoded.
|
||||
// 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 8 characters, useful for display.
|
||||
// Short returns the first 16 hex chars grouped in 4s: "a1b2 c3d4 e5f6 0718".
|
||||
func (p PeerID) Short() string {
|
||||
if len(p) < 8 {
|
||||
return string(p)
|
||||
s := string(p)
|
||||
if len(s) < 16 {
|
||||
return s
|
||||
}
|
||||
return string(p)[:8]
|
||||
return s[0:4] + " " + s[4:8] + " " + s[8:12] + " " + s[12:16]
|
||||
}
|
||||
|
||||
// PeerInfo is a peer's self-description. Included in the Hello handshake.
|
||||
// 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, base64url
|
||||
PublicKey string `json:"public_key"` // Ed25519 pubkey, hex
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// ── Handshake ─────────────────────────────────────────────────────────────────
|
||||
// ── Peer-to-peer message types (over the "yaw" DataChannel) ──────────────────
|
||||
|
||||
// Hello is the first message sent on a new TCP connection.
|
||||
type Hello struct {
|
||||
Version int `json:"version"`
|
||||
Peer PeerInfo `json:"peer"`
|
||||
EphemeralKey string `json:"ephemeral_key"` // X25519 pubkey, base64url
|
||||
Signature string `json:"signature"` // Ed25519 sig over ephemeral_key
|
||||
}
|
||||
|
||||
// HelloAck is the response to Hello, completing the handshake.
|
||||
type HelloAck struct {
|
||||
Peer PeerInfo `json:"peer"`
|
||||
EphemeralKey string `json:"ephemeral_key"`
|
||||
Signature string `json:"signature"`
|
||||
RelayCapable bool `json:"relay_capable"`
|
||||
}
|
||||
|
||||
// ── Encrypted envelope ────────────────────────────────────────────────────────
|
||||
|
||||
// Envelope wraps all post-handshake messages.
|
||||
// The payload is ChaCha20-Poly1305 encrypted.
|
||||
type Envelope struct {
|
||||
Nonce string `json:"nonce"` // 12 bytes, base64url
|
||||
Payload string `json:"payload"` // ciphertext, base64url
|
||||
}
|
||||
|
||||
// ── Peer-to-peer message types ────────────────────────────────────────────────
|
||||
|
||||
// MsgType identifies the kind of peer message inside an Envelope.
|
||||
// MsgType identifies the kind of peer message.
|
||||
type MsgType string
|
||||
|
||||
const (
|
||||
MsgChat MsgType = "chat"
|
||||
MsgPeerGossip MsgType = "peer_gossip"
|
||||
MsgFileOffer MsgType = "file_offer"
|
||||
MsgFileResp MsgType = "file_response"
|
||||
MsgFileChunk MsgType = "file_chunk"
|
||||
MsgPing MsgType = "ping"
|
||||
MsgPong MsgType = "pong"
|
||||
MsgChat MsgType = "chat"
|
||||
MsgPeerGossip MsgType = "peer_gossip"
|
||||
MsgFileOffer MsgType = "file_offer"
|
||||
MsgFileResp MsgType = "file_response"
|
||||
MsgFileDone MsgType = "file_done"
|
||||
MsgPing MsgType = "ping"
|
||||
MsgPong MsgType = "pong"
|
||||
)
|
||||
|
||||
// PeerMessage is the top-level container decoded from inside an Envelope.
|
||||
// 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"`
|
||||
|
||||
@@ -78,13 +53,14 @@ type PeerMessage struct {
|
||||
Gossip *PeerGossip `json:"gossip,omitempty"`
|
||||
FileOffer *FileOffer `json:"file_offer,omitempty"`
|
||||
FileResp *FileResponse `json:"file_response,omitempty"`
|
||||
FileChunk *FileChunk `json:"file_chunk,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 {
|
||||
ID string `json:"id"`
|
||||
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"`
|
||||
@@ -100,64 +76,102 @@ type PeerGossip struct {
|
||||
// GossipEntry is one peer hint shared via gossip.
|
||||
type GossipEntry struct {
|
||||
Peer PeerInfo `json:"peer"`
|
||||
AddrHint string `json:"addr_hint"` // IP:port, may be behind NAT
|
||||
AddrHint string `json:"addr_hint"` // IP:port hint, may be behind NAT
|
||||
LastSeen time.Time `json:"last_seen"`
|
||||
}
|
||||
|
||||
// FileOffer initiates a file transfer.
|
||||
type FileOffer struct {
|
||||
TransferID string `json:"transfer_id"`
|
||||
Filename string `json:"filename"`
|
||||
SizeBytes int64 `json:"size_bytes"`
|
||||
SHA256 string `json:"sha256"`
|
||||
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 {
|
||||
TransferID string `json:"transfer_id"`
|
||||
Accepted bool `json:"accepted"`
|
||||
Mid string `json:"mid"`
|
||||
Xid string `json:"xid"`
|
||||
Accepted bool `json:"accepted"`
|
||||
}
|
||||
|
||||
// FileChunk is one piece of a file transfer.
|
||||
type FileChunk struct {
|
||||
TransferID string `json:"transfer_id"`
|
||||
Seq uint32 `json:"seq"`
|
||||
Data string `json:"data"` // base64url
|
||||
IsLast bool `json:"is_last"`
|
||||
// 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
|
||||
}
|
||||
|
||||
// ── Relay protocol ────────────────────────────────────────────────────────────
|
||||
// ── DataChannel hello (YAW/2 §6) ─────────────────────────────────────────────
|
||||
|
||||
// RelayMsgType identifies relay wire messages.
|
||||
type RelayMsgType string
|
||||
// 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 (
|
||||
RelayRegister RelayMsgType = "register"
|
||||
RelayForward RelayMsgType = "forward"
|
||||
RelayListPeers RelayMsgType = "list_peers"
|
||||
RelayForwarded RelayMsgType = "forwarded"
|
||||
RelayPeerList RelayMsgType = "peer_list"
|
||||
RelayError RelayMsgType = "error"
|
||||
SigOffer SignalingKind = "offer"
|
||||
SigAnswer SignalingKind = "answer"
|
||||
SigCandidate SignalingKind = "candidate"
|
||||
SigBye SignalingKind = "bye"
|
||||
)
|
||||
|
||||
// RelayMessage is used for both client→relay and relay→client.
|
||||
type RelayMessage struct {
|
||||
Type RelayMsgType `json:"type"`
|
||||
// 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
|
||||
}
|
||||
|
||||
// register
|
||||
Peer *PeerInfo `json:"peer,omitempty"`
|
||||
Signature string `json:"signature,omitempty"`
|
||||
// ── Anchor WebSocket wire types (YAW/2 §5) ────────────────────────────────────
|
||||
|
||||
// forward / forwarded
|
||||
To *PeerID `json:"to,omitempty"`
|
||||
From *PeerID `json:"from,omitempty"`
|
||||
Envelope *Envelope `json:"envelope,omitempty"`
|
||||
// AnchorMsgType identifies anchor WebSocket messages.
|
||||
type AnchorMsgType string
|
||||
|
||||
// peer_list
|
||||
Peers []GossipEntry `json:"peers,omitempty"`
|
||||
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"
|
||||
)
|
||||
|
||||
// error
|
||||
Message string `json:"message,omitempty"`
|
||||
// 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) ─────────────────────────────────────────
|
||||
@@ -167,15 +181,17 @@ type IpcMsgType string
|
||||
|
||||
const (
|
||||
// Commands (UI → daemon)
|
||||
CmdSendMessage IpcMsgType = "send_message"
|
||||
CmdConnect IpcMsgType = "connect"
|
||||
CmdGetState IpcMsgType = "get_state"
|
||||
CmdSendFile IpcMsgType = "send_file"
|
||||
CmdSendMessage IpcMsgType = "send_message"
|
||||
CmdJoinNetwork IpcMsgType = "join_network" // fields: network_name (plaintext)
|
||||
CmdLeaveNetwork IpcMsgType = "leave_network"
|
||||
CmdGetState IpcMsgType = "get_state"
|
||||
CmdSendFile IpcMsgType = "send_file"
|
||||
|
||||
// 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"
|
||||
@@ -191,8 +207,8 @@ type IpcMessage struct {
|
||||
To *PeerID `json:"to,omitempty"`
|
||||
Body string `json:"body,omitempty"`
|
||||
|
||||
// connect
|
||||
Addr string `json:"addr,omitempty"`
|
||||
// join_network / leave_network
|
||||
NetworkName string `json:"network_name,omitempty"`
|
||||
|
||||
// send_file
|
||||
Path string `json:"path,omitempty"`
|
||||
@@ -200,6 +216,7 @@ type IpcMessage struct {
|
||||
// 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"`
|
||||
|
||||
Reference in New Issue
Block a user