feat(calls): implement Linux LAN peer calls over SRTP

Reuse existing ChatHub consent and Podcast LibVLC capture for direct bidirectional H.264/AAC media, with embedded call controls and cleanup. Require HTTPS key exchange and reject IP-masking policies for this opt-in LAN/VPN transport.

Add native SRTP, signaling and lifecycle tests; document setup and remaining physical-device validation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Paul Schneider 2026-10-26 03:05:23 +00:00
commit cc28ee8b29
Signed by: notazof
GPG key ID: AD4B9B545FBD7D12
20 changed files with 1394 additions and 29 deletions

View file

@ -9,22 +9,30 @@ namespace Yavsc.Server.Hubs;
public partial class ChatHub
{
public async Task<CallSession> InviteCall(string roomName, string callee)
public Task<CallSession> InviteCall(string roomName, string callee) =>
InviteCoreAsync(roomName, callee, CallTransports.WebRtc);
public Task<CallSession> InviteLanCall(string roomName, string callee) =>
InviteCoreAsync(roomName, callee, CallTransports.LanSrtp);
private async Task<CallSession> InviteCoreAsync(string roomName, string callee, string transport)
{
var caller = CallUser();
RequireSecureLanControl(transport);
if (string.IsNullOrWhiteSpace(roomName) || !InputValidator.ValidateRoomName(roomName) ||
string.IsNullOrWhiteSpace(callee) || !InputValidator.ValidateUserName(callee))
throw new HubException("Invalid call participant or room.");
RequireCallPresence(roomName, caller, callee);
var relayOnly = await RequiresRelayAsync(caller, callee);
return await _calls.InviteAsync(roomName, caller, callee, Context.ConnectionId,
_cxManager.GetConnexionIds(callee)?.Distinct().ToArray() ?? [], relayOnly);
_cxManager.GetConnexionIds(callee)?.Distinct().ToArray() ?? [], relayOnly, transport);
}
public async Task AcceptCall(Guid callId)
{
var user = CallUser();
var session = await _calls.GetSessionAsync(callId, user, Context.ConnectionId);
RequireSecureLanControl(session.Transport);
RequireCallPresence(session.RoomName, session.Caller, session.Callee);
await _calls.AcceptAsync(callId, user, Context.ConnectionId,
await RequiresRelayAsync(session.Caller, session.Callee));
@ -36,6 +44,7 @@ public partial class ChatHub
{
var user = CallUser();
var session = await _calls.GetSessionAsync(callId, user, Context.ConnectionId);
RequireSecureLanControl(session.Transport);
RequireCallPresence(session.RoomName, session.Caller, session.Callee);
if (!session.RelayOnly && await RequiresRelayAsync(session.Caller, session.Callee))
{
@ -53,6 +62,12 @@ public partial class ChatHub
return user;
}
private void RequireSecureLanControl(string transport)
{
if (transport == CallTransports.LanSrtp && Context.GetHttpContext()?.Request.IsHttps != true)
throw new HubException("LAN calls require HTTPS signaling to protect SRTP keys.");
}
private void RequireCallPresence(string room, string caller, string callee)
{
if (!_cxManager.IsConnected(caller) || !_cxManager.IsConnected(callee) ||