Files
Zoltán Papp 73cffdb702 Add peer-to-peer file drop
Files move directly between peers over the overlay, with no server in the
path. The receiver listens on the WireGuard address only, so the port is
unreachable from outside the tunnel, and every offer is matched to a known
peer before anything is read.

Consent is the default: an offer carries metadata alone, and no payload
moves until the receiver accepts. Policy is per profile and device-local —
off, ask, or auto-accept, with per-sender exceptions on top.

Policy and history live in the profile's preferences, so removing a profile
takes its file drop state with it. Transfers interrupted by a restart are
settled on load; nothing survives to finish them, and left alone they would
sit in the log as permanently pending.

The Android bindings pull payload bytes through a chunk-returning stream:
gomobile copies a []byte argument into a fresh Java array and never copies
it back, so a fill-my-buffer method would hand back the right length with
no data.
2026-08-16 22:04:38 +02:00

82 lines
1.9 KiB
Go

package filedrop
import (
"context"
"sync"
)
// PortRegistry tracks the file drop listen port each peer advertised over
// signaling; 0 means the well-known default. Senders can wait on it to learn a
// better port after a failed attempt.
type PortRegistry struct {
mu sync.Mutex
ports map[PeerKey]uint16
waits map[PeerKey][]chan uint16
}
// NewPortRegistry returns an empty registry.
func NewPortRegistry() *PortRegistry {
return &PortRegistry{
ports: make(map[PeerKey]uint16),
waits: make(map[PeerKey][]chan uint16),
}
}
// Set records the port a peer advertised and releases every waiter for it.
func (r *PortRegistry) Set(key PeerKey, port uint16) {
r.mu.Lock()
r.ports[key] = port
waiters := r.waits[key]
delete(r.waits, key)
r.mu.Unlock()
for _, ch := range waiters {
ch <- port
}
}
// Port returns the last advertised port for a peer; 0 means default or unknown.
func (r *PortRegistry) Port(key PeerKey) uint16 {
r.mu.Lock()
defer r.mu.Unlock()
return r.ports[key]
}
// Await returns the peer's port as soon as it differs from used, or after the next
// advertisement even when it does not, reporting whether it differs. It returns
// immediately when the currently known port already differs.
func (r *PortRegistry) Await(ctx context.Context, key PeerKey, used uint16) (uint16, bool) {
r.mu.Lock()
if port, ok := r.ports[key]; ok && port != used {
r.mu.Unlock()
return port, true
}
ch := make(chan uint16, 1)
r.waits[key] = append(r.waits[key], ch)
r.mu.Unlock()
select {
case port := <-ch:
return port, port != used
case <-ctx.Done():
r.drop(key, ch)
return 0, false
}
}
func (r *PortRegistry) drop(key PeerKey, ch chan uint16) {
r.mu.Lock()
defer r.mu.Unlock()
waiters := r.waits[key]
for i, w := range waiters {
if w == ch {
r.waits[key] = append(waiters[:i], waiters[i+1:]...)
break
}
}
if len(r.waits[key]) == 0 {
delete(r.waits, key)
}
}