validateOffer bounded the file count and inline text but only checked the
sign of an announced size, and nothing limited the aggregate or how many
offers a sender could keep open. Every announced byte is staged in the spool
before delivery, so a peer decided how much of the receiver's disk to take:
512 files of 8 GiB were accepted unchallenged, and a flood of offers each
raised its own consent prompt.
Sizes are now capped per file and per offer, and a sender is held to a fixed
number of open offers. The aggregate accumulates against the remaining
headroom instead of summing first, because 512 files of 2^60 wrap an int64
back through zero and a plain sum would report such an offer as nil bytes.
The count is of open offers, not lifetime ones, so settling one frees a slot.
The receiving policy was read once, in submitOffer, and never again. The
upload path and the reader guarding it both watched only the offer's own
Decision, so blocking a sender refused its next offer while the transfer it
had already started kept streaming into the spool and was delivered.
SetSenderRule tried to settle the sender's offers but went through Decide,
which only moves an offer out of Pending, and skipped everything else -
precisely the accepted ones that were still running. It now revokes any
non-terminal offer instead, and the staged copy re-reads the policy on the
same cadence it already checks the decision, so a block stops the bytes.
State.terminal carries the state list that Transfer.terminal duplicated.
The file drop server listened on 41421, which falls inside the ephemeral
port range on Linux (32768-60999) and Windows (49152-65535), so any
outbound connection could take it after boot. The receiver then bound a
dynamic port and advertised it over signaling, and the sender waited for
that advertisement before retrying.
That coupled a data plane feature to signaling traffic: once a peer
connection is established there is no reason for another offer or answer
to go out, so a sender could wait out the grace period for an
advertisement that never came.
Move the port to 22042, next to the SSH (22022) and DNS forwarder (22054)
ports and clear of both ephemeral ranges, and keep the tunnel side fixed
the way SSH does. A receiver that cannot bind it falls back to a dynamic
port and redirects 22042 to it with an inbound DNAT rule, so senders
always dial the well known port and never negotiate. NB_FILEDROP_PORT
overrides the local bind only.
The DNAT runs ahead of the filter on every backend (nftables prerouting
at NAT dest priority, iptables nat/PREROUTING, and the userspace filter's
translate-then-redecode path), so the netstack service registry keeps
taking the bound port.
This drops the port registry, the retry that waited on it, and the
signaling plumbing that fed it.
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.