Fix file transfer, signaling reliability, and DC callback races

- wireFileRecv: replace OnMessage/OnClose channel approach with pion
  Detach API so SCTP buffers data regardless of callback timing — fixes
  0-byte files caused by pion dispatching data goroutines before OnMessage
  was registered
- yaw DC: same Detach API treatment for the control channel
- connectTo: use DetachDataChannels SettingEngine, replace stale PCs
  (Connecting/Failed/Disconnected state) instead of returning early;
  isCurrent guard in OnConnectionStateChange prevents stale-PC close
  from firing OnDisconnected
- offerByOrder: gate maybeOffer on peer ID order so only one side
  creates the yaw DC and SDP offer — fixes signaling glare causing
  triple connected (verified) logs
- pendingRecvs: promote single pendingRecv slot to map[string]*recvState
  so concurrent file offers don't overwrite each other
- peer-left: stop calling resetPeer() so active file DCs aren't closed
  before data arrives
- signaling ping: wrap conn.Ping with 10s timeout context so a dead TCP
  connection is detected within 30s rather than hanging forever
- signaling read: 3-minute read deadline forces reconnect if anchor
  silently evicts the peer from the room (idle timeout)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Fredrik Johansson
2026-07-02 17:38:48 +02:00
parent 488431eaec
commit 0fe01e146c
6 changed files with 307 additions and 74 deletions

View File

@@ -115,6 +115,7 @@ func runDaemon(ctx context.Context) {
// reconnecting with exponential backoff whenever the connection drops.
func runPeerLoop(ctx context.Context, tc transport.Config, myID string, peer peerConfig, downloadDir string) {
room := transport.PairRoomName(myID, peer.ID)
log.Printf("[%s] room: %s", peer.Label, room)
backoff := 2 * time.Second
for {

View File

@@ -1,5 +1,6 @@
// flit — headless CLI for ephemeral, E2E-encrypted file transfer.
//
// flit id print this device's peer ID (hex Ed25519 pubkey)
// flit send <path> generate a one-shot room, print QR + invite, wait for a peer, send
// flit recv <invite|code> join a room from an invite string, accept the offered file
// flit daemon run as a persistent receiver for trusted peers (reads ~/.flit/daemon.toml)
@@ -100,6 +101,8 @@ func main() {
defer stop()
switch os.Args[1] {
case "id":
printID()
case "send":
if len(os.Args) < 3 {
fmt.Println("usage: flit send <path>")
@@ -115,11 +118,20 @@ func main() {
case "daemon":
runDaemon(ctx)
default:
fmt.Println("usage: flit send <path> | flit recv <invite> | flit daemon")
fmt.Println("usage: flit id | flit send <path> | flit recv <invite> | flit daemon")
os.Exit(1)
}
}
func printID() {
sess, err := transport.NewSession(dataDir(), transport.Config{})
if err != nil {
fmt.Fprintln(os.Stderr, "flit:", err)
os.Exit(1)
}
fmt.Println(sess.PeerID())
}
func send(ctx context.Context, path string) {
if _, err := os.Stat(path); err != nil {
fmt.Fprintln(os.Stderr, "flit:", err)