Files
zero/internal/peermsg/service.go
Anandan f922cb378e Add cross-session messaging (#884)
* Add cross-session messaging

* Harden cross-session messaging

Authenticate sender metadata against live registry records, bound peer lifecycle state, preserve records across transient discovery failures, and make receipt delivery direct and bounded.

Also serialize TUI peer approvals, harden platform runtime paths and transport deadlines, widen session references, and add regression coverage for identity spoofing, queue limits, path boundaries, and tool failures.

* Support canonical macOS socket paths

Resolve Unix transport directories to their physical path before applying strict no-symlink traversal, allowing the standard /var to /private/var macOS alias without weakening runtime-directory validation.

* Fix macOS socket path regression test

Create the canonicalization fixture beneath a guaranteed short Unix path so it exercises the primary endpoint on macOS, and report temporary-directory cleanup failures.

* Harden peer messaging lifecycle

* Fix Windows peer directory traversal

* Handle Windows token-owned peer directories

* Accept equivalent Windows ACL masks

* Build Windows peer ACL explicitly

* Pin Windows ACL trustee SIDs

* Validate Windows peer ACL semantics

* Propagate Windows peer handle cleanup errors

* Use null handle for Windows close failure test

* Test held-message eviction callback
2026-08-09 18:12:09 +05:30

1235 lines
35 KiB
Go

package peermsg
import (
"bufio"
"context"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"time"
"unicode"
"github.com/Gitlawb/zero/internal/fsutil"
)
type Handler func(InboundMessage) bool
type StatusHandler func(StatusEvent)
type HeldEvictionHandler func(string)
type HeldReleaseHandler func(InboundMessage)
const (
peerBucketCapacity = 30.0
peerRefillPerSecond = 0.5
peerDedupWindow = 30 * time.Second
peerMaxSelfHops = 10
peerMaxChainLength = 28
peerMaxTrackedSenders = 256
peerMaxHeldMessages = 100
peerMaxOutstanding = 256
peerOutstandingTTL = 6 * time.Minute
peerProbeWorkers = 8
peerProbeTimeout = 300 * time.Millisecond
peerInboundWorkers = 32
peerReceiptWorkers = peerMaxHeldMessages
peerReceiptTimeout = 750 * time.Millisecond
peerRegistryCacheTTL = 250 * time.Millisecond
peerFrameTimeout = 5 * time.Second
peerRefHexLength = 12
peerErrorMaxRunes = 512
)
type inboundContextKey struct{}
type senderGuard struct {
tokens float64
lastRefill time.Time
lastBody string
lastBodyAt time.Time
lastActivity time.Time
}
type outstandingMessage struct {
peer Peer
createdAt time.Time
}
type Options struct {
RootDir string
Identity Identity
Now func() time.Time
Transport localTransport
PID int
InboundPolicy InboundPolicy
}
// Service owns one live peer endpoint plus the registry used to discover other
// local Zero sessions. The transport is platform-specific, while framing,
// identity resolution, limits, and delivery policy remain shared.
type Service struct {
mu sync.RWMutex
root string
identity Identity
now func() time.Time
transport localTransport
pid int
nonce string
self Peer
listener net.Listener
handler Handler
statusHandler StatusHandler
evictionHandler HeldEvictionHandler
releaseHandler HeldReleaseHandler
policy InboundPolicy
outstanding map[string]outstandingMessage
held map[string]InboundMessage
heldOrder []string
guards map[string]*senderGuard
closed bool
wg sync.WaitGroup
done chan struct{}
inboundSlots chan struct{}
registryMu sync.Mutex
registryCache []Peer
registryModTime time.Time
registryCached time.Time
registryValid bool
}
func New(options Options) (*Service, error) {
root := strings.TrimSpace(options.RootDir)
if root == "" {
var err error
root, err = DefaultRoot()
if err != nil {
return nil, err
}
}
abs, err := filepath.Abs(root)
if err != nil {
return nil, fmt.Errorf("peer messaging: resolve runtime directory: %w", err)
}
abs, err = canonicalRuntimePath(abs)
if err != nil {
return nil, fmt.Errorf("peer messaging: canonicalize runtime directory: %w", err)
}
nonce, err := randomHex(8)
if err != nil {
return nil, fmt.Errorf("peer messaging: generate endpoint id: %w", err)
}
now := options.Now
if now == nil {
now = time.Now
}
transport := options.Transport
if transport == nil {
transport = platformTransport()
}
pid := options.PID
if pid <= 0 {
pid = os.Getpid()
}
return &Service{
root: abs,
identity: normalizeIdentity(options.Identity),
now: now,
transport: transport,
pid: pid,
nonce: nonce,
policy: normalizeInboundPolicy(options.InboundPolicy),
outstanding: make(map[string]outstandingMessage),
held: make(map[string]InboundMessage),
guards: make(map[string]*senderGuard),
done: make(chan struct{}),
inboundSlots: make(chan struct{}, peerInboundWorkers),
}, nil
}
// canonicalRuntimePath normalizes aliases in the existing prefix. It does not
// establish a security boundary; ensurePrivateDir validates that boundary when
// the service starts.
func canonicalRuntimePath(path string) (string, error) {
missing := make([]string, 0, 4)
existing := filepath.Clean(path)
for {
_, err := os.Lstat(existing)
if err == nil {
break
}
if !errors.Is(err, os.ErrNotExist) {
return "", err
}
parent := filepath.Dir(existing)
if parent == existing {
return "", err
}
missing = append(missing, filepath.Base(existing))
existing = parent
}
resolved, err := filepath.EvalSymlinks(existing)
if err != nil {
return "", err
}
for index := len(missing) - 1; index >= 0; index-- {
resolved = filepath.Join(resolved, missing[index])
}
return resolved, nil
}
// WithInboundMessage carries a peer message's relay chain through the agent
// turn so a deliberate send_message reply or forward can extend it. Ordinary
// user turns have no chain and start a fresh one.
func WithInboundMessage(ctx context.Context, message InboundMessage) context.Context {
if ctx == nil {
ctx = context.Background()
}
chain := append([]string(nil), message.HopChain...)
return context.WithValue(ctx, inboundContextKey{}, chain)
}
func DefaultRoot() (string, error) {
if runtimeDir := strings.TrimSpace(os.Getenv("XDG_RUNTIME_DIR")); runtimeDir != "" {
return filepath.Join(runtimeDir, "zero", "peers"), nil
}
cacheDir, err := os.UserCacheDir()
if err != nil {
return "", fmt.Errorf("peer messaging: resolve user cache directory: %w", err)
}
return filepath.Join(cacheDir, "zero", "peers"), nil
}
func (service *Service) Start(handler Handler) error {
service.mu.Lock()
defer service.mu.Unlock()
if service.listener != nil {
return nil
}
if service.closed {
return errors.New("peer messaging: service is closed")
}
if err := ensurePrivateDir(service.root); err != nil {
return fmt.Errorf("peer messaging: create runtime directory: %w", err)
}
if err := ensurePrivateDir(service.registryDir()); err != nil {
return fmt.Errorf("peer messaging: create registry: %w", err)
}
endpoint, err := service.transport.Endpoint(service.root, service.nonce, service.pid)
if err != nil {
return err
}
listener, err := service.transport.Listen(endpoint)
if err != nil {
return fmt.Errorf("peer messaging: listen: %w", err)
}
now := service.now().UTC()
if service.identity.SessionID == "" {
service.identity.SessionID = "live-" + service.nonce
}
service.self = Peer{
Identity: service.identity,
Endpoint: endpoint,
PID: service.pid,
StartedAt: now,
UpdatedAt: now,
Ref: peerRef(endpoint),
}
service.listener = listener
service.handler = handler
if err := service.writeRecordLocked(); err != nil {
service.listener = nil
return errors.Join(err, listener.Close(), service.transport.Remove(endpoint))
}
service.wg.Add(1)
go service.acceptLoop(listener)
return nil
}
func (service *Service) SetStatusHandler(handler StatusHandler) {
service.mu.Lock()
defer service.mu.Unlock()
service.statusHandler = handler
}
func (service *Service) SetHeldEvictionHandler(handler HeldEvictionHandler) {
service.mu.Lock()
defer service.mu.Unlock()
service.evictionHandler = handler
}
func (service *Service) SetHeldReleaseHandler(handler HeldReleaseHandler) {
service.mu.Lock()
defer service.mu.Unlock()
service.releaseHandler = handler
}
func (service *Service) Close() error {
service.mu.Lock()
if service.closed {
service.mu.Unlock()
return nil
}
service.closed = true
close(service.done)
listener := service.listener
endpoint := service.self.Endpoint
held := make([]InboundMessage, 0, len(service.held))
for _, message := range service.held {
held = append(held, message)
}
service.held = make(map[string]InboundMessage)
service.heldOrder = nil
service.outstanding = make(map[string]outstandingMessage)
service.listener = nil
service.mu.Unlock()
service.sendStatuses(held, DeliveryExpired)
var closeErrs []error
if listener != nil {
if err := listener.Close(); err != nil && !errors.Is(err, net.ErrClosed) {
closeErrs = append(closeErrs, err)
}
}
service.wg.Wait()
if endpoint != "" {
if err := service.transport.Remove(endpoint); err != nil && !errors.Is(err, os.ErrNotExist) {
closeErrs = append(closeErrs, err)
}
}
if err := service.removeOwnRecord(endpoint); err != nil {
closeErrs = append(closeErrs, err)
}
return errors.Join(closeErrs...)
}
// ResolveHeld settles a message that was parked for local approval and sends a
// terminal receipt back to its sender. Repeated or stale decisions are ignored.
func (service *Service) ResolveHeld(ctx context.Context, messageID string, status DeliveryStatus) error {
if status != DeliveryDelivered && status != DeliveryDenied && status != DeliveryExpired {
return fmt.Errorf("peer messaging: invalid held-message status %q", status)
}
service.mu.Lock()
message, ok := service.held[messageID]
if ok {
delete(service.held, messageID)
service.removeHeldOrderLocked(messageID)
}
service.mu.Unlock()
if !ok {
return nil
}
return service.sendStatus(ctx, message, status)
}
func (service *Service) UpdateIdentity(identity Identity) error {
service.mu.Lock()
service.identity = normalizeIdentity(identity)
if service.listener == nil {
service.mu.Unlock()
return nil
}
service.self.Identity = service.identity
service.self.UpdatedAt = service.now().UTC()
if err := service.writeRecordLocked(); err != nil {
service.mu.Unlock()
return err
}
released := make([]InboundMessage, 0)
if service.policy == InboundPolicyParity && service.releaseHandler != nil {
for _, messageID := range append([]string(nil), service.heldOrder...) {
message, ok := service.held[messageID]
if !ok {
service.removeHeldOrderLocked(messageID)
continue
}
status, _ := service.inboundDecision(message.From.PermissionClass, service.self.PermissionClass)
if status != DeliveryAccepted {
continue
}
delete(service.held, messageID)
service.removeHeldOrderLocked(messageID)
message.RequiresApproval = false
message.HoldCause = ""
released = append(released, message)
}
}
releaseHandler := service.releaseHandler
service.mu.Unlock()
for _, message := range released {
if releaseHandler != nil {
releaseHandler(message)
}
}
if len(released) > 0 {
service.launchStatuses(released, DeliveryDelivered)
}
return nil
}
func (service *Service) Self() Peer {
service.mu.RLock()
defer service.mu.RUnlock()
return service.self
}
func (service *Service) List(ctx context.Context) ([]Peer, error) {
candidates, err := service.registryPeers()
if err != nil {
return nil, err
}
self := service.Self()
type probeResult struct {
peer Peer
reachable bool
}
jobs := make(chan Peer)
results := make(chan probeResult, len(candidates))
workers := min(peerProbeWorkers, len(candidates))
var wg sync.WaitGroup
for range workers {
wg.Add(1)
go func() {
defer wg.Done()
for peer := range jobs {
if peer.Endpoint == self.Endpoint || peer.SessionID == "" {
continue
}
probeCtx, cancel := context.WithTimeout(ctx, peerProbeTimeout)
conn, dialErr := service.transport.Dial(probeCtx, peer.Endpoint)
cancel()
if dialErr == nil {
_ = conn.Close()
}
results <- probeResult{peer: peer, reachable: dialErr == nil}
}
}()
}
go func() {
defer close(jobs)
for _, peer := range candidates {
select {
case jobs <- peer:
case <-ctx.Done():
return
}
}
}()
go func() { wg.Wait(); close(results) }()
peers := make([]Peer, 0, len(candidates))
for result := range results {
if result.reachable {
peers = append(peers, result.peer)
}
}
if err := ctx.Err(); err != nil {
return nil, fmt.Errorf("peer messaging: list sessions: %w", err)
}
sort.Slice(peers, func(i, j int) bool {
if strings.EqualFold(peers[i].Name, peers[j].Name) {
return peers[i].Ref < peers[j].Ref
}
return strings.ToLower(peers[i].Name) < strings.ToLower(peers[j].Name)
})
return peers, nil
}
func (service *Service) registryPeers() ([]Peer, error) {
service.registryMu.Lock()
defer service.registryMu.Unlock()
info, statErr := os.Stat(service.registryDir())
if errors.Is(statErr, os.ErrNotExist) {
service.registryCache = nil
service.registryModTime = time.Time{}
service.registryCached = service.now()
service.registryValid = true
return nil, nil
}
if statErr != nil {
return nil, fmt.Errorf("peer messaging: inspect registry: %w", statErr)
}
now := service.now()
if service.registryValid && info.ModTime().Equal(service.registryModTime) && now.Sub(service.registryCached) < peerRegistryCacheTTL {
return append([]Peer(nil), service.registryCache...), nil
}
entries, err := os.ReadDir(service.registryDir())
if errors.Is(err, os.ErrNotExist) {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("peer messaging: read registry: %w", err)
}
peers := make([]Peer, 0, len(entries))
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") {
continue
}
peer, readErr := readPeerRecord(filepath.Join(service.registryDir(), entry.Name()))
if readErr == nil && peer.Endpoint != "" && peer.SessionID != "" {
peers = append(peers, peer)
}
}
service.registryCache = append(service.registryCache[:0], peers...)
service.registryModTime = info.ModTime()
service.registryCached = now
service.registryValid = true
return append([]Peer(nil), peers...), nil
}
// registeredPeer replaces all sender-controlled identity metadata with the
// record published by the live service that owns the claimed endpoint.
func (service *Service) registeredPeer(claimed Peer) (Peer, error) {
peers, err := service.registryPeers()
if err != nil {
return Peer{}, err
}
for _, peer := range peers {
if peer.Endpoint == claimed.Endpoint && peer.SessionID == claimed.SessionID && peer.Ref == claimed.Ref {
return peer, nil
}
}
return Peer{}, errors.New("peer messaging: unregistered sender")
}
func (service *Service) Send(ctx context.Context, to, summary, body string) (SendResult, error) {
body = normalizeBody(body)
summary = normalizeSummary(summary)
if body == "" {
return SendResult{}, errors.New("peer messaging: message must not be empty")
}
if len(body) > maxMessageBytes {
return SendResult{}, fmt.Errorf("peer messaging: message exceeds %d bytes", maxMessageBytes)
}
if summary == "" {
return SendResult{}, errors.New("peer messaging: summary is required")
}
peers, err := service.List(ctx)
if err != nil {
return SendResult{}, err
}
peer, err := resolvePeer(peers, to)
if err != nil {
return SendResult{}, err
}
id, err := randomHex(16)
if err != nil {
return SendResult{}, fmt.Errorf("peer messaging: generate message id: %w", err)
}
self := service.Self()
hopChain := inboundHopChain(ctx)
if len(hopChain) == 0 || hopChain[len(hopChain)-1] != self.Ref {
hopChain = append(hopChain, self.Ref)
}
if err := validateHopChain(hopChain); err != nil {
return SendResult{}, err
}
frame := sendFrame{
Version: ProtocolVersion,
Type: "message",
ID: id,
From: self,
To: peer.SessionID,
Summary: summary,
Body: body,
HopChain: hopChain,
}
service.mu.Lock()
service.pruneOutstandingLocked(service.now())
service.outstanding[id] = outstandingMessage{peer: peer, createdAt: service.now()}
service.mu.Unlock()
keepOutstanding := false
defer func() {
if keepOutstanding {
return
}
service.mu.Lock()
delete(service.outstanding, id)
service.mu.Unlock()
}()
conn, err := service.transport.Dial(ctx, peer.Endpoint)
if err != nil {
return SendResult{}, fmt.Errorf("peer messaging: connect to %s: %w", displayPeer(peer), err)
}
defer conn.Close()
_ = conn.SetDeadline(deadlineFromContext(ctx, peerFrameTimeout))
if err := json.NewEncoder(conn).Encode(frame); err != nil {
return SendResult{}, fmt.Errorf("peer messaging: send to %s: %w", displayPeer(peer), err)
}
var response responseFrame
if err := decodeFrame(conn, &response); err != nil {
return SendResult{}, fmt.Errorf("peer messaging: receive delivery status from %s: %w", displayPeer(peer), err)
}
if response.Version != ProtocolVersion || response.Type != "delivery" || response.ID != id {
return SendResult{}, errors.New("peer messaging: invalid delivery response")
}
if response.Error != "" {
return SendResult{}, fmt.Errorf("peer messaging: receiver rejected message: %s", normalizeRemoteError(response.Error))
}
if response.Status != DeliveryAccepted && response.Status != DeliveryHeld && response.Status != DeliveryRefused {
return SendResult{}, errors.New("peer messaging: invalid delivery status")
}
keepOutstanding = response.Status == DeliveryHeld
return SendResult{MessageID: id, Peer: peer, Status: response.Status}, nil
}
func (service *Service) acceptLoop(listener net.Listener) {
defer service.wg.Done()
var retryDelay time.Duration
for {
conn, err := listener.Accept()
if err != nil {
if errors.Is(err, net.ErrClosed) {
return
}
service.mu.RLock()
closed := service.closed
service.mu.RUnlock()
if closed {
return
}
if retryDelay == 0 {
retryDelay = 5 * time.Millisecond
} else {
retryDelay *= 2
}
retryDelay = min(retryDelay, time.Second)
timer := time.NewTimer(retryDelay)
select {
case <-timer.C:
case <-service.done:
if !timer.Stop() {
<-timer.C
}
return
}
continue
}
retryDelay = 0
select {
case service.inboundSlots <- struct{}{}:
default:
_ = conn.Close()
continue
}
service.wg.Add(1)
go func() {
defer service.wg.Done()
defer func() { <-service.inboundSlots }()
defer conn.Close()
service.handleConn(conn)
}()
}
}
func (service *Service) handleConn(conn net.Conn) {
_ = conn.SetDeadline(time.Now().Add(peerFrameTimeout))
var frame sendFrame
if err := decodeFrame(conn, &frame); err != nil {
if !errors.Is(err, io.EOF) {
_ = json.NewEncoder(conn).Encode(responseFrame{Version: ProtocolVersion, Type: "delivery", Status: DeliveryRefused, Error: "invalid peer message"})
}
return
}
if frame.Type == "status" {
service.handleStatusFrame(frame)
return
}
response := responseFrame{Version: ProtocolVersion, Type: "delivery", ID: frame.ID, Status: DeliveryRefused}
if frame.Version != ProtocolVersion || frame.Type != "message" || frame.ID == "" ||
frame.From.SessionID == "" || frame.From.Endpoint == "" || !validPeerRef(frame.From.Endpoint, frame.From.Ref) {
response.Error = "peer messaging: invalid message envelope"
_ = json.NewEncoder(conn).Encode(response)
return
}
trustedSender, err := service.registeredPeer(frame.From)
if err != nil {
response.Error = "peer messaging: sender identity is not registered"
_ = json.NewEncoder(conn).Encode(response)
return
}
frame.From = trustedSender
if len(frame.Body) == 0 || len(frame.Body) > maxMessageBytes {
response.Error = "peer messaging: invalid message size"
_ = json.NewEncoder(conn).Encode(response)
return
}
body := normalizeBody(frame.Body)
if body == "" {
response.Error = "peer messaging: invalid message content"
_ = json.NewEncoder(conn).Encode(response)
return
}
service.mu.RLock()
self := service.self
handler := service.handler
service.mu.RUnlock()
if self.SessionID == "" || frame.To != self.SessionID {
response.Error = "peer messaging: target session is no longer active"
_ = json.NewEncoder(conn).Encode(response)
return
}
if len(frame.HopChain) == 0 {
frame.HopChain = []string{frame.From.Ref}
}
if err := validateHopChain(frame.HopChain); err != nil || frame.HopChain[len(frame.HopChain)-1] != frame.From.Ref {
response.Error = "peer messaging: invalid relay chain"
_ = json.NewEncoder(conn).Encode(response)
return
}
if reason := service.admitMessage(frame.From, body, frame.HopChain, self.Ref); reason != "" {
response.Error = "peer messaging: message dropped: " + reason
_ = json.NewEncoder(conn).Encode(response)
return
}
if handler == nil {
response.Status = DeliveryRefused
response.Error = "peer messaging: receiving session is unavailable"
_ = json.NewEncoder(conn).Encode(response)
return
}
status, holdCause := service.inboundDecision(frame.From.PermissionClass, self.PermissionClass)
response.Status = status
if status == DeliveryRefused {
_ = json.NewEncoder(conn).Encode(response)
return
}
message := InboundMessage{
ID: frame.ID,
From: frame.From,
Body: body,
Summary: normalizeSummary(frame.Summary),
ReceivedAt: service.now().UTC(),
RequiresApproval: status == DeliveryHeld,
HoldCause: holdCause,
HopChain: append([]string(nil), frame.HopChain...),
}
if status == DeliveryHeld {
var evicted InboundMessage
var evictionHandler HeldEvictionHandler
service.mu.Lock()
if len(service.held) >= peerMaxHeldMessages {
evicted = service.popOldestHeldLocked()
evictionHandler = service.evictionHandler
}
service.held[message.ID] = message
service.heldOrder = append(service.heldOrder, message.ID)
service.mu.Unlock()
if evicted.ID != "" {
if evictionHandler != nil {
evictionHandler(evicted.ID)
}
service.launchStatuses([]InboundMessage{evicted}, DeliveryExpired)
}
}
_ = conn.SetDeadline(time.Now().Add(peerFrameTimeout))
if !handler(message) {
if status == DeliveryHeld {
service.mu.Lock()
delete(service.held, message.ID)
service.removeHeldOrderLocked(message.ID)
service.mu.Unlock()
}
response.Status = DeliveryRefused
response.Error = "peer messaging: message dropped: receiver queue is full or unavailable"
_ = json.NewEncoder(conn).Encode(response)
return
}
_ = json.NewEncoder(conn).Encode(response)
}
func (service *Service) handleStatusFrame(frame sendFrame) {
if frame.Version != ProtocolVersion || frame.OrigID == "" || frame.From.SessionID == "" ||
frame.From.Endpoint == "" || !validPeerRef(frame.From.Endpoint, frame.From.Ref) || !terminalDeliveryStatus(frame.Status) {
return
}
trustedSender, err := service.registeredPeer(frame.From)
if err != nil {
return
}
frame.From = trustedSender
service.mu.Lock()
self := service.self
outstanding, ok := service.outstanding[frame.OrigID]
if !ok || frame.To != self.SessionID || outstanding.peer.Endpoint != frame.From.Endpoint || outstanding.peer.SessionID != frame.From.SessionID {
service.mu.Unlock()
return
}
delete(service.outstanding, frame.OrigID)
handler := service.statusHandler
service.mu.Unlock()
if handler != nil {
handler(StatusEvent{MessageID: frame.OrigID, Peer: outstanding.peer, Status: frame.Status})
}
}
func (service *Service) sendStatus(ctx context.Context, message InboundMessage, status DeliveryStatus) error {
if !terminalDeliveryStatus(status) {
return fmt.Errorf("peer messaging: invalid delivery status %q", status)
}
target := message.From
if target.Endpoint == "" || target.SessionID == "" || !validPeerRef(target.Endpoint, target.Ref) {
return errors.New("peer messaging: original sender identity is invalid")
}
self := service.Self()
frame := sendFrame{
Version: ProtocolVersion,
Type: "status",
From: self,
To: target.SessionID,
OrigID: message.ID,
Status: status,
}
conn, err := service.transport.Dial(ctx, target.Endpoint)
if err != nil {
return fmt.Errorf("peer messaging: send status to %s: %w", displayPeer(target), err)
}
defer conn.Close()
_ = conn.SetDeadline(deadlineFromContext(ctx, peerFrameTimeout))
if err := json.NewEncoder(conn).Encode(frame); err != nil {
return fmt.Errorf("peer messaging: send status to %s: %w", displayPeer(target), err)
}
return nil
}
func (service *Service) sendStatuses(messages []InboundMessage, status DeliveryStatus) {
jobs := make(chan InboundMessage)
workers := min(peerReceiptWorkers, len(messages))
var wg sync.WaitGroup
for range workers {
wg.Add(1)
go func() {
defer wg.Done()
for message := range jobs {
ctx, cancel := context.WithTimeout(context.Background(), peerReceiptTimeout)
_ = service.sendStatus(ctx, message, status)
cancel()
}
}()
}
for _, message := range messages {
jobs <- message
}
close(jobs)
wg.Wait()
}
func deadlineFromContext(ctx context.Context, maximum time.Duration) time.Time {
deadline := time.Now().Add(maximum)
if contextDeadline, ok := ctx.Deadline(); ok && contextDeadline.Before(deadline) {
return contextDeadline
}
return deadline
}
func (service *Service) launchStatuses(messages []InboundMessage, status DeliveryStatus) {
service.mu.Lock()
if service.closed {
service.mu.Unlock()
return
}
service.wg.Add(1)
service.mu.Unlock()
go func() {
defer service.wg.Done()
service.sendStatuses(messages, status)
}()
}
func (service *Service) pruneOutstandingLocked(now time.Time) {
for id, pending := range service.outstanding {
if now.Sub(pending.createdAt) >= peerOutstandingTTL {
delete(service.outstanding, id)
}
}
for len(service.outstanding) >= peerMaxOutstanding {
var oldestID string
var oldest time.Time
for id, pending := range service.outstanding {
if oldestID == "" || pending.createdAt.Before(oldest) {
oldestID, oldest = id, pending.createdAt
}
}
delete(service.outstanding, oldestID)
}
}
func (service *Service) inboundDecision(sender, receiver PermissionClass) (DeliveryStatus, HoldCause) {
switch service.policy {
case InboundPolicyAccept:
return DeliveryAccepted, ""
case InboundPolicyHold:
return DeliveryHeld, HoldCauseExplicit
case InboundPolicyRefuse:
return DeliveryRefused, ""
default:
if sender == "" {
if receiver == PermissionBypass {
return DeliveryHeld, HoldCauseModeUnknown
}
return DeliveryAccepted, ""
}
if sender == receiver {
return DeliveryAccepted, ""
}
return DeliveryHeld, HoldCauseModeMismatch
}
}
func (service *Service) admitMessage(sender Peer, body string, chain []string, selfRef string) string {
if len(chain) > peerMaxChainLength {
return "relay chain is too long"
}
selfHops := 0
for _, ref := range chain {
if ref == selfRef {
selfHops++
}
}
if selfHops >= peerMaxSelfHops {
return "peer messaging loop detected"
}
now := service.now()
key := sender.Endpoint
service.mu.Lock()
defer service.mu.Unlock()
guard := service.guards[key]
if guard == nil {
if len(service.guards) >= peerMaxTrackedSenders {
var oldestKey string
var oldest time.Time
for candidate, state := range service.guards {
if oldestKey == "" || state.lastActivity.Before(oldest) {
oldestKey, oldest = candidate, state.lastActivity
}
}
delete(service.guards, oldestKey)
}
guard = &senderGuard{tokens: peerBucketCapacity, lastRefill: now}
service.guards[key] = guard
}
elapsed := now.Sub(guard.lastRefill).Seconds()
if elapsed > 0 {
guard.tokens = min(peerBucketCapacity, guard.tokens+elapsed*peerRefillPerSecond)
guard.lastRefill = now
}
if guard.tokens < 1 {
return "sender exceeded the peer message rate limit"
}
guard.tokens--
if guard.lastBody == body && now.Sub(guard.lastBodyAt) < peerDedupWindow {
return "duplicate of a recent message from this sender"
}
guard.lastActivity = now
guard.lastBody = body
guard.lastBodyAt = now
return ""
}
func (service *Service) removeHeldOrderLocked(messageID string) {
for index, candidate := range service.heldOrder {
if candidate == messageID {
service.heldOrder = append(service.heldOrder[:index], service.heldOrder[index+1:]...)
return
}
}
}
func (service *Service) popOldestHeldLocked() InboundMessage {
for len(service.heldOrder) > 0 {
messageID := service.heldOrder[0]
service.heldOrder = service.heldOrder[1:]
if message, ok := service.held[messageID]; ok {
delete(service.held, messageID)
return message
}
}
var oldestID string
var oldest InboundMessage
for messageID, message := range service.held {
if oldestID == "" || message.ReceivedAt.Before(oldest.ReceivedAt) {
oldestID, oldest = messageID, message
}
}
if oldestID != "" {
delete(service.held, oldestID)
}
return oldest
}
func inboundHopChain(ctx context.Context) []string {
if ctx == nil {
return nil
}
chain, _ := ctx.Value(inboundContextKey{}).([]string)
return append([]string(nil), chain...)
}
func validateHopChain(chain []string) error {
if len(chain) == 0 || len(chain) > peerMaxChainLength {
return errors.New("peer messaging: invalid relay chain")
}
for _, ref := range chain {
if len(ref) < 8 || len(ref) > peerRefHexLength || len(ref)%2 != 0 {
return errors.New("peer messaging: invalid relay chain")
}
if _, err := hex.DecodeString(ref); err != nil {
return errors.New("peer messaging: invalid relay chain")
}
}
return nil
}
func terminalDeliveryStatus(status DeliveryStatus) bool {
return status == DeliveryDelivered || status == DeliveryDenied || status == DeliveryExpired
}
func normalizeInboundPolicy(policy InboundPolicy) InboundPolicy {
switch policy {
case InboundPolicyAccept, InboundPolicyHold, InboundPolicyRefuse:
return policy
default:
return InboundPolicyParity
}
}
func decodeFrame(reader io.Reader, target any) error {
buffered := bufio.NewReader(io.LimitReader(reader, maxFrameBytes+1))
line, err := buffered.ReadBytes('\n')
if err != nil {
return err
}
if len(line) > maxFrameBytes {
return errors.New("peer messaging: frame is too large")
}
if err := json.Unmarshal(line, target); err != nil {
return fmt.Errorf("peer messaging: decode frame: %w", err)
}
return nil
}
func (service *Service) registryDir() string { return filepath.Join(service.root, "registry") }
func (service *Service) recordPath() string {
return filepath.Join(service.registryDir(), fmt.Sprintf("%d-%s.json", service.pid, service.nonce))
}
func (service *Service) writeRecordLocked() error {
data, err := json.Marshal(service.self)
if err != nil {
return fmt.Errorf("peer messaging: encode registry record: %w", err)
}
path := service.recordPath()
tmp, err := os.CreateTemp(service.registryDir(), filepath.Base(path)+".*.tmp")
if err != nil {
return fmt.Errorf("peer messaging: create registry temporary file: %w", err)
}
tmpPath := tmp.Name()
defer os.Remove(tmpPath)
if err := tmp.Chmod(0o600); err != nil {
_ = tmp.Close()
return err
}
if _, err := tmp.Write(append(data, '\n')); err != nil {
_ = tmp.Close()
return err
}
if err := tmp.Sync(); err != nil {
_ = tmp.Close()
return err
}
if err := tmp.Close(); err != nil {
return err
}
if err := fsutil.RenameWithRetry(tmpPath, path, nil); err != nil {
return fmt.Errorf("peer messaging: publish registry record: %w", err)
}
return nil
}
func readPeerRecord(path string) (Peer, error) {
data, err := os.ReadFile(path)
if err != nil {
return Peer{}, err
}
var peer Peer
if err := json.Unmarshal(data, &peer); err != nil {
return Peer{}, err
}
if peer.PID <= 0 || peer.Endpoint == "" || !validPeerRef(peer.Endpoint, peer.Ref) {
return Peer{}, errors.New("peer messaging: invalid registry record")
}
peer.Identity = normalizeIdentity(peer.Identity)
return peer, nil
}
func (service *Service) removeOwnRecord(endpoint string) error {
path := service.recordPath()
peer, err := readPeerRecord(path)
if errors.Is(err, os.ErrNotExist) {
return nil
}
if err != nil {
return fmt.Errorf("peer messaging: verify registry record before removal: %w", err)
}
if peer.Endpoint != endpoint {
return nil
}
if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("peer messaging: remove registry record: %w", err)
}
return nil
}
func resolvePeer(peers []Peer, target string) (Peer, error) {
target = strings.TrimSpace(target)
if target == "" {
return Peer{}, errors.New("peer messaging: recipient must not be empty")
}
for _, peer := range peers {
if peer.SessionID == target {
return peer, nil
}
}
if name, ref, ok := splitPeerTarget(target); ok {
for _, peer := range peers {
peerName := strings.TrimSpace(peer.Name)
if peerName == "" {
peerName = "Zero session"
}
if strings.EqualFold(peer.Ref, ref) && strings.EqualFold(peerName, name) {
return peer, nil
}
}
return Peer{}, fmt.Errorf("peer messaging: no reachable session named %q", target)
}
matches := make([]Peer, 0, 2)
for _, peer := range peers {
if strings.EqualFold(peer.Name, target) {
matches = append(matches, peer)
}
}
if len(matches) == 1 {
return matches[0], nil
}
if len(matches) > 1 {
return Peer{}, fmt.Errorf("peer messaging: %q is ambiguous; use name [ref] from list_sessions", target)
}
return Peer{}, fmt.Errorf("peer messaging: no reachable session named %q", target)
}
func splitPeerTarget(target string) (name, ref string, ok bool) {
open := strings.LastIndex(target, " [")
if open <= 0 || !strings.HasSuffix(target, "]") {
return "", "", false
}
name = strings.TrimSpace(target[:open])
ref = target[open+2 : len(target)-1]
if name == "" || len(ref) < 8 || len(ref) > peerRefHexLength || len(ref)%2 != 0 {
return "", "", false
}
if _, err := hex.DecodeString(ref); err != nil {
return "", "", false
}
return name, ref, true
}
func displayPeer(peer Peer) string {
name := strings.TrimSpace(peer.Name)
if name == "" {
name = "Zero session"
}
return fmt.Sprintf("%s [%s]", name, peer.Ref)
}
func normalizeIdentity(identity Identity) Identity {
identity.SessionID = truncateRunes(strings.TrimSpace(sanitizePlainText(identity.SessionID, false)), 256)
identity.Name = truncateRunes(strings.TrimSpace(sanitizePlainText(identity.Name, false)), 80)
if strings.ContainsAny(identity.Name, "[]") {
identity.Name = ""
}
identity.Cwd = truncateRunes(strings.TrimSpace(sanitizePlainText(identity.Cwd, false)), 4096)
switch identity.PermissionClass {
case PermissionPrompting, PermissionBypass:
default:
identity.PermissionClass = ""
}
return identity
}
func truncateRunes(value string, limit int) string {
runes := []rune(value)
if len(runes) <= limit {
return value
}
return string(runes[:limit])
}
func normalizeSummary(summary string) string {
summary = strings.Split(summary, "\n")[0]
summary = strings.TrimSpace(sanitizePlainText(summary, false))
runes := []rune(summary)
if len(runes) > 200 {
summary = string(runes[:200])
}
return summary
}
func normalizeBody(body string) string {
return strings.TrimSpace(sanitizePlainText(body, true))
}
func normalizeRemoteError(message string) string {
message = truncateRunes(strings.TrimSpace(sanitizePlainText(message, false)), peerErrorMaxRunes)
if message == "" {
return "receiver rejected the message"
}
return message
}
func sanitizePlainText(value string, multiline bool) string {
return strings.Map(func(char rune) rune {
if multiline && (char == '\n' || char == '\t') {
return char
}
if unicode.IsControl(char) {
return -1
}
return char
}, value)
}
func peerRef(endpoint string) string {
sum := sha256.Sum256([]byte(endpoint))
return hex.EncodeToString(sum[:peerRefHexLength/2])
}
func validPeerRef(endpoint, ref string) bool {
if ref == peerRef(endpoint) {
return true
}
// Accept references from a process that was already running during an
// in-place upgrade. New sessions always publish the wider reference.
sum := sha256.Sum256([]byte(endpoint))
return ref == hex.EncodeToString(sum[:4])
}
func randomHex(bytes int) (string, error) {
value := make([]byte, bytes)
if _, err := rand.Read(value); err != nil {
return "", err
}
return hex.EncodeToString(value), nil
}