Fix #18: accept audio from all of a multi-homed sender's source IPs
A sender reachable at more than one IP at once (e.g. LAN + Tailscale/VPN) picks its own egress interface per packet, so its audio can arrive from a different IP than the single address we discovered/dialled and allow-listed. The receiver then silently dropped every Format/Audio packet (packetsRejectedNotAllowed climbing) while heartbeats — which skip the allow-list — kept the peer showing connected: connected but silent. (Reported by Jonathans859 building the RemSoundApple client; receiver-side, affects any multi-homed sender incl. Windows<->Windows over a VPN.) Discovery now remembers ALL source IPs per peer InstanceId (PeerDiscoveryService .addressesById, expired on the same 8 s window; GetKnownAddresses). PushAllowedReceiveSenders unions each selected peer's endpoint address with every address that peer has announced from, so audio from any of the peer's interfaces is accepted. The SEND targets are unchanged (still single-address) — only the accept-list widens, and only to other addresses the SAME peer (by InstanceId) announced from, so it can't accept an unrelated machine. Held for next release. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
1c1bf5a5cb
commit
703e6a022d
@@ -5682,7 +5682,21 @@ public sealed class MainForm : Form
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private void PushAllowedReceiveSenders()
|
private void PushAllowedReceiveSenders()
|
||||||
{
|
{
|
||||||
receiver.SetAllowedSenders(SelectedSendEndpoints());
|
// Accept audio from ANY source IP a selected peer is known to use, not only the single address
|
||||||
|
// we currently target. A multi-homed sender (on a LAN and a VPN at once) can egress audio from a
|
||||||
|
// different interface than the one we discovered or dialled; allow-listing just the one made the
|
||||||
|
// receiver silently drop that audio while heartbeats (which skip this check) kept the peer
|
||||||
|
// looking connected — connected but silent (#18). The SEND targets stay single-address; only the
|
||||||
|
// accept-list widens, and only to other addresses the SAME peer (by InstanceId) announced from.
|
||||||
|
var allowed = new List<IPEndPoint>();
|
||||||
|
var seen = new HashSet<IPAddress>();
|
||||||
|
foreach (var (id, ep) in selectedPeerEndpoints)
|
||||||
|
{
|
||||||
|
if (seen.Add(ep.Address)) allowed.Add(new IPEndPoint(ep.Address, 0));
|
||||||
|
foreach (var addr in discovery.GetKnownAddresses(id))
|
||||||
|
if (seen.Add(addr)) allowed.Add(new IPEndPoint(addr, 0));
|
||||||
|
}
|
||||||
|
receiver.SetAllowedSenders(allowed);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>True if the heartbeat currently considers <paramref name="endpoint"/> healthy —
|
/// <summary>True if the heartbeat currently considers <paramref name="endpoint"/> healthy —
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ public sealed class PeerDiscoveryService : IDisposable
|
|||||||
private readonly Guid instanceId = Guid.NewGuid();
|
private readonly Guid instanceId = Guid.NewGuid();
|
||||||
private readonly object gate = new();
|
private readonly object gate = new();
|
||||||
private readonly Dictionary<Guid, PeerAnnouncement> peers = [];
|
private readonly Dictionary<Guid, PeerAnnouncement> peers = [];
|
||||||
|
// All source IPs each peer (by InstanceId) has recently announced from, with last-seen for expiry.
|
||||||
|
// A multi-homed peer (LAN + VPN at once) announces from several interfaces; we keep them all so the
|
||||||
|
// receiver can accept audio from whichever one a given packet egresses from (#18), not only the
|
||||||
|
// last-seen address. Guarded by gate; pruned on the same 8 s window as `peers`.
|
||||||
|
private readonly Dictionary<Guid, Dictionary<IPAddress, DateTime>> addressesById = [];
|
||||||
private CancellationTokenSource? cts;
|
private CancellationTokenSource? cts;
|
||||||
private UdpClient? listener;
|
private UdpClient? listener;
|
||||||
private UdpClient? announcer;
|
private UdpClient? announcer;
|
||||||
@@ -178,6 +183,13 @@ public sealed class PeerDiscoveryService : IDisposable
|
|||||||
|| existing.CanReceive != peer.CanReceive
|
|| existing.CanReceive != peer.CanReceive
|
||||||
|| !Equals(existing.Address, peer.Address);
|
|| !Equals(existing.Address, peer.Address);
|
||||||
peers[peer.InstanceId] = peer;
|
peers[peer.InstanceId] = peer;
|
||||||
|
// Remember this source IP for the peer (multi-homed senders announce from several).
|
||||||
|
if (!addressesById.TryGetValue(peer.InstanceId, out var addrs))
|
||||||
|
{
|
||||||
|
addrs = [];
|
||||||
|
addressesById[peer.InstanceId] = addrs;
|
||||||
|
}
|
||||||
|
addrs[peer.Address] = peer.LastSeenUtc;
|
||||||
PruneExpiredPeers();
|
PruneExpiredPeers();
|
||||||
}
|
}
|
||||||
if (changed) PeersChanged?.Invoke();
|
if (changed) PeersChanged?.Invoke();
|
||||||
@@ -300,6 +312,27 @@ public sealed class PeerDiscoveryService : IDisposable
|
|||||||
{
|
{
|
||||||
peers.Remove(peer.InstanceId);
|
peers.Remove(peer.InstanceId);
|
||||||
}
|
}
|
||||||
|
// Expire per-interface source addresses on the same window, and drop any peer left with none.
|
||||||
|
foreach (var (id, addrs) in addressesById.ToList())
|
||||||
|
{
|
||||||
|
foreach (var addr in addrs.Where(kv => kv.Value < cutoff).Select(kv => kv.Key).ToList())
|
||||||
|
{
|
||||||
|
addrs.Remove(addr);
|
||||||
|
}
|
||||||
|
if (addrs.Count == 0) addressesById.Remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>All source IPs a peer (by InstanceId) has announced from within the expiry window. A
|
||||||
|
/// multi-homed peer announces from several; the receiver allow-lists all of them so audio egressing
|
||||||
|
/// from any of the peer's interfaces is accepted rather than silently dropped (#18).</summary>
|
||||||
|
public IReadOnlyCollection<IPAddress> GetKnownAddresses(Guid instanceId)
|
||||||
|
{
|
||||||
|
lock (gate)
|
||||||
|
{
|
||||||
|
PruneExpiredPeers();
|
||||||
|
return addressesById.TryGetValue(instanceId, out var addrs) ? addrs.Keys.ToArray() : [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed record DiscoveryMessage(Guid InstanceId, string Name, int AudioPort, bool CanSend, bool CanReceive);
|
private sealed record DiscoveryMessage(Guid InstanceId, string Name, int AudioPort, bool CanSend, bool CanReceive);
|
||||||
|
|||||||
Reference in New Issue
Block a user