Support IPv6 addresses for direct SRTP calls
Extend shared endpoint validation, interface discovery and native media sockets to IPv6. Add real bidirectional IPv6 SRTP, wrong-key rejection and port lifecycle tests, and document connectivity limitations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
73411f2f7e
commit
a39c3912ea
9 changed files with 108 additions and 52 deletions
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
### Added
|
||||
|
||||
* [PostIt/Calls] Support direct IPv6 SRTP calls using global, ULA and loopback addresses, shared endpoint validation, IPv6 UDP sockets and bracketed reception URLs. Exclude scoped/link-local, multicast, unspecified and IPv4-mapped addresses; verify real bidirectional IPv6 audio/video, wrong-key rejection and port lifecycle with LibVLC.
|
||||
* [PostIt/Calls] Allow directly assigned public IPv4 addresses for direct SRTP calls, alongside LAN/VPN and loopback addresses. Share unicast validation between interface discovery, media and server signaling; continue rejecting unspecified, link-local, multicast, reserved and IPv6 addresses. Keep explicit address-disclosure consent and no automatic NAT traversal.
|
||||
* [PostIt] Update bundled settings with the default podcast title, camera/microphone preferences and French language selection.
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
Ce document décrit l'architecture cible et l'implémentation commencée.
|
||||
La préférence de confidentialité est persistée et éditable dans le profil.
|
||||
La signalisation est intégrée au chat, désactivée par défaut. Un premier
|
||||
transport natif **Android et Desktop Linux en IPv4 directe (LAN/VPN ou IP publique)** est raccordé :
|
||||
transport natif **Android et Desktop Linux en IPv4/IPv6 directe (LAN/VPN ou IP publique)** est raccordé :
|
||||
captures Podcast existantes et SRTP direct, sans libwebrtc ni nouvelle
|
||||
couche JNI WebRTC. Il ne réalise pas encore la cible Internet, TURN ou groupe.
|
||||
Il complète la [topologie de PostIt](postit.md) et les
|
||||
|
|
@ -18,17 +18,28 @@ La cible couvre :
|
|||
- les appels de groupe ;
|
||||
- les clients Browser, Android et Desktop.
|
||||
|
||||
## Première implémentation Android et Desktop Linux : SRTP direct IPv4
|
||||
## Première implémentation Android et Desktop Linux : SRTP direct IPv4/IPv6
|
||||
|
||||
Les adresses IPv4 publiques directement assignees a une interface locale active
|
||||
sont egalement disponibles, avec la meme validation unicast cote client,
|
||||
media et signalisation serveur. Les adresses non specifiees, link-local,
|
||||
multicast, reservees (240/4), IPv6 et les formes non canoniques sont refusees.
|
||||
media et signalisation serveur. Les IPv6 globales (2000::/3), privees ULA
|
||||
(fc00::/7) et de boucle locale (::1) sont aussi acceptees. Les sockets UDP
|
||||
utilisent la famille de l'adresse locale et les URL de reception encadrent
|
||||
les IPv6 de crochets. Les adresses non specifiees, link-local, multicast,
|
||||
reservees IPv4 (240/4), IPv4-mapped IPv6 et les formes non canoniques ou
|
||||
avec identifiant de zone sont refusees. Une zone link-local du premier
|
||||
pair n'est pas reutilisable telle quelle par le second.
|
||||
Il faut autoriser en entree les deux ports UDP consecutifs choisis et assurer
|
||||
la joignabilite entre les pairs. Ce support n'ajoute ni ICE/TURN ni traversee
|
||||
NAT automatique : l'adresse publique d'un routeur ne remplace pas une adresse
|
||||
assignee a la machine. Le consentement explicite au partage d'IP et le refus
|
||||
du mode direct pour les comptes masquant leur IP restent inchanges.
|
||||
Les pairs doivent disposer de routes vers leurs adresses respectives ;
|
||||
aucune traduction automatique IPv4/IPv6 n'est fournie. Les tests natifs
|
||||
Linux verifient sur ::1 la reception audio/video bidirectionnelle chiffree,
|
||||
le rejet d'une mauvaise cle et le cycle de vie des ports RTP/RTCP.
|
||||
Un appel IPv6 entre deux machines et le transport IPv6 Android restent
|
||||
a valider en conditions reelles.
|
||||
|
||||
Cette tranche réutilise la capture caméra/microphone et les encodeurs des
|
||||
Podcasts, mais **pas leur segmentation HLS ni leurs envois de fichiers** :
|
||||
|
|
@ -77,7 +88,7 @@ de fichier média temporaire ou de moteur WebRTC Android ajouté.
|
|||
H.264/AAC. La version Debian 13 a été utilisée pour les essais Linux.
|
||||
Les deux comptes doivent être présents dans
|
||||
la même salle existante et ne pas se bloquer mutuellement.
|
||||
3. Dans « Appel direct IPv4 (LAN/VPN ou IP publique) », actualiser les périphériques,
|
||||
3. Dans « Appel direct IPv4/IPv6 (LAN/VPN ou IP publique) », actualiser les périphériques,
|
||||
sélectionner la caméra, le microphone et une IPv4 privée locale
|
||||
accessible depuis le pair. Les adresses RFC1918 et de bouclage sont
|
||||
acceptées ; pas de DNS, IPv6 ou adresse publique. Le bouclage sert aux
|
||||
|
|
@ -89,7 +100,7 @@ de fichier média temporaire ou de moteur WebRTC Android ajouté.
|
|||
adresse doivent utiliser des paires distinctes.
|
||||
Aucun pare-feu n'est modifié automatiquement.
|
||||
5. Saisir le pseudo exact du destinataire, confirmer la divulgation de
|
||||
l'adresse IPv4 locale (LAN/VPN ou publique), puis appeler. Le destinataire choisit ses propres
|
||||
l'adresse IP locale (LAN/VPN ou publique), puis appeler. Le destinataire choisit ses propres
|
||||
périphériques et son adresse, confirme et accepte explicitement.
|
||||
Aucun accès à la caméra/microphone ne commence avant l'acceptation ;
|
||||
l'émission attend également la destination validée du pair.
|
||||
|
|
@ -150,7 +161,7 @@ l'ajout d'un moteur WebRTC ou d'une cryptographie propre au projet.
|
|||
Il n'y a aucun repli direct contournant cette préférence.
|
||||
- Pas d'ICE, de STUN, de TURN, de traversée NAT automatique, de groupe,
|
||||
d'interopérabilité Browser, de négociation de codecs ni
|
||||
d'adaptation de débit. Ce mode exige une connectivite IPv4 directe entre les pairs
|
||||
d'adaptation de débit. Ce mode exige une connectivite IP directe entre les pairs
|
||||
(LAN/VPN ou adresse publique assignee), avec les ports UDP ouverts.
|
||||
- H.264 à 30 images/s, cible 1 200 kbit/s ; AAC mono 48 kHz, 96 kbit/s.
|
||||
Ces réglages ne constituent pas une garantie de latence ni de débit.
|
||||
|
|
|
|||
|
|
@ -114,13 +114,16 @@ public sealed class LanCallViewModelTests
|
|||
return vm;
|
||||
}
|
||||
|
||||
[AvaloniaFact]
|
||||
public async Task Public_ipv4_can_be_selected_for_an_explicitly_consented_direct_call()
|
||||
[AvaloniaTheory]
|
||||
[InlineData("8.8.8.8")]
|
||||
[InlineData("2001:db8::1")]
|
||||
[InlineData("fd00::1")]
|
||||
public async Task Public_or_private_ip_can_be_selected_for_an_explicitly_consented_direct_call(string address)
|
||||
{
|
||||
var signaling = new Signaling();
|
||||
await using var vm = Configure(signaling, (_, _) => throw new Exception("No media before acceptance"));
|
||||
vm.Addresses.Add("8.8.8.8");
|
||||
vm.Address = "8.8.8.8";
|
||||
vm.Addresses.Add(address);
|
||||
vm.Address = address;
|
||||
await vm.InviteCommand.ExecuteAsync(null);
|
||||
Assert.Equal(1, signaling.Invites);
|
||||
Assert.True(vm.Busy);
|
||||
|
|
|
|||
|
|
@ -15,38 +15,45 @@ public sealed class LinuxLanCallMediaTests(ITestOutputHelper output)
|
|||
public static bool HasTsFixture => OperatingSystem.IsLinux() &&
|
||||
File.Exists(Environment.GetEnvironmentVariable("POSTIT_TEST_TS"));
|
||||
|
||||
[Fact(Skip = "Requires Linux LibVLC.", SkipUnless = nameof(HasFixture))]
|
||||
public async Task Ports_are_released_and_credentials_regenerated_after_disposal()
|
||||
[Theory(Skip = "Requires Linux LibVLC.", SkipUnless = nameof(HasFixture))]
|
||||
[InlineData("127.0.0.1")]
|
||||
[InlineData("::1")]
|
||||
public async Task Ports_are_released_and_credentials_regenerated_after_disposal(string address)
|
||||
{
|
||||
var port = FreePort();
|
||||
var first = new LibVlcLanCallMedia("127.0.0.1", port, headless: true);
|
||||
var port = FreePort(address: address);
|
||||
var first = new LibVlcLanCallMedia(address, port, headless: true);
|
||||
var endpoint = first.Endpoint;
|
||||
await first.StartReceivingAsync(TestContext.Current.CancellationToken);
|
||||
await first.DisposeAsync();
|
||||
await first.DisposeAsync();
|
||||
await using var next = new LibVlcLanCallMedia("127.0.0.1", port, headless: true);
|
||||
await using var next = new LibVlcLanCallMedia(address, port, headless: true);
|
||||
Assert.NotEqual(endpoint.Key, next.Endpoint.Key);
|
||||
Assert.NotEqual(endpoint.Salt, next.Endpoint.Salt);
|
||||
await next.StartReceivingAsync(TestContext.Current.CancellationToken);
|
||||
}
|
||||
|
||||
[Fact(Skip = "Requires Linux LibVLC.", SkipUnless = nameof(HasFixture))]
|
||||
public void Occupied_rtcp_port_is_rejected_before_capture_starts()
|
||||
[Theory(Skip = "Requires Linux LibVLC.", SkipUnless = nameof(HasFixture))]
|
||||
[InlineData("127.0.0.1")]
|
||||
[InlineData("::1")]
|
||||
public void Occupied_rtcp_port_is_rejected_before_capture_starts(string address)
|
||||
{
|
||||
var port = FreePort();
|
||||
using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
|
||||
socket.Bind(new IPEndPoint(IPAddress.Loopback, port + 1));
|
||||
var error = Assert.Throws<SocketException>(() => new LibVlcLanCallMedia("127.0.0.1", port, headless: true));
|
||||
var port = FreePort(address: address);
|
||||
var ip = IPAddress.Parse(address);
|
||||
using var socket = new Socket(ip.AddressFamily, SocketType.Dgram, ProtocolType.Udp);
|
||||
socket.Bind(new IPEndPoint(ip, port + 1));
|
||||
var error = Assert.Throws<SocketException>(() => new LibVlcLanCallMedia(address, port, headless: true));
|
||||
Assert.Equal(SocketError.AddressAlreadyInUse, error.SocketErrorCode);
|
||||
}
|
||||
|
||||
[Fact(Skip = "Set POSTIT_TEST_TS to an H.264/AAC MPEG-TS file.", SkipUnless = nameof(HasTsFixture))]
|
||||
public async Task Already_encoded_capture_is_sent_over_srtp_without_transcoding()
|
||||
[Theory(Skip = "Set POSTIT_TEST_TS to an H.264/AAC MPEG-TS file.", SkipUnless = nameof(HasTsFixture))]
|
||||
[InlineData("127.0.0.1")]
|
||||
[InlineData("::1")]
|
||||
public async Task Already_encoded_capture_is_sent_over_srtp_without_transcoding(string address)
|
||||
{
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var firstPort = FreePort();
|
||||
await using var sender = new LibVlcLanCallMedia("127.0.0.1", firstPort, headless: true);
|
||||
await using var receiver = new LibVlcLanCallMedia("127.0.0.1", FreePort(firstPort), headless: true);
|
||||
var firstPort = FreePort(address: address);
|
||||
await using var sender = new LibVlcLanCallMedia(address, firstPort, headless: true);
|
||||
await using var receiver = new LibVlcLanCallMedia(address, FreePort(firstPort, address), headless: true);
|
||||
using var decoded = new DecodedVideo(receiver.Receiver);
|
||||
using var pipe = new AnonymousPipeServerStream(PipeDirection.Out);
|
||||
await using var source = File.OpenRead(Environment.GetEnvironmentVariable("POSTIT_TEST_TS")!);
|
||||
|
|
@ -88,13 +95,16 @@ public sealed class LinuxLanCallMediaTests(ITestOutputHelper output)
|
|||
[InlineData(false, false)]
|
||||
[InlineData(true, false)]
|
||||
[InlineData(false, true)]
|
||||
public async Task Two_peers_decode_bidirectional_encrypted_audio_video_and_reject_wrong_keys(bool wrongKey, bool preview)
|
||||
[InlineData(false, false, "::1")]
|
||||
[InlineData(true, false, "::1")]
|
||||
public async Task Two_peers_decode_bidirectional_encrypted_audio_video_and_reject_wrong_keys(
|
||||
bool wrongKey, bool preview, string address = "127.0.0.1")
|
||||
{
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var firstPort = FreePort();
|
||||
var secondPort = FreePort(firstPort);
|
||||
await using var first = new LibVlcLanCallMedia("127.0.0.1", firstPort, headless: true);
|
||||
await using var second = new LibVlcLanCallMedia("127.0.0.1", secondPort, headless: true);
|
||||
var firstPort = FreePort(address: address);
|
||||
var secondPort = FreePort(firstPort, address);
|
||||
await using var first = new LibVlcLanCallMedia(address, firstPort, headless: true);
|
||||
await using var second = new LibVlcLanCallMedia(address, secondPort, headless: true);
|
||||
using var firstVideo = new DecodedVideo(first.Receiver);
|
||||
using var secondVideo = new DecodedVideo(second.Receiver);
|
||||
await first.StartReceivingAsync(ct);
|
||||
|
|
@ -142,17 +152,18 @@ public sealed class LinuxLanCallMediaTests(ITestOutputHelper output)
|
|||
private static bool HasMedia(LibVlcLanCallMedia peer, DecodedVideo video) =>
|
||||
peer.Statistics.DecodedAudio > 0 && video.AudioSamples > 4800 && video.Frames >= 10;
|
||||
|
||||
private static int FreePort(int except = 0)
|
||||
private static int FreePort(int except = 0, string address = "127.0.0.1")
|
||||
{
|
||||
var ip = IPAddress.Parse(address);
|
||||
for (var port = 32000; port < 60000; port += 2)
|
||||
{
|
||||
if (port == except) continue;
|
||||
using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
|
||||
using var rtcp = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
|
||||
using var socket = new Socket(ip.AddressFamily, SocketType.Dgram, ProtocolType.Udp);
|
||||
using var rtcp = new Socket(ip.AddressFamily, SocketType.Dgram, ProtocolType.Udp);
|
||||
try
|
||||
{
|
||||
socket.Bind(new IPEndPoint(IPAddress.Loopback, port));
|
||||
rtcp.Bind(new IPEndPoint(IPAddress.Loopback, port + 1));
|
||||
socket.Bind(new IPEndPoint(ip, port));
|
||||
rtcp.Bind(new IPEndPoint(ip, port + 1));
|
||||
return port;
|
||||
}
|
||||
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.AddressAlreadyInUse) { }
|
||||
|
|
|
|||
|
|
@ -35,9 +35,10 @@ public sealed class LibVlcLanCallMedia : IAsyncDisposable
|
|||
Endpoint = new(localAddress, port, Convert.ToHexString(RandomNumberGenerator.GetBytes(16)),
|
||||
Convert.ToHexString(RandomNumberGenerator.GetBytes(14)));
|
||||
if (!Endpoint.IsValid())
|
||||
throw new ArgumentException("Adresse IPv4 unicast et port UDP pair (1024-65534) requis.");
|
||||
using (var reservation = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp))
|
||||
using (var rtcp = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp))
|
||||
throw new ArgumentException("Adresse IPv4/IPv6 unicast et port UDP pair (1024-65534) requis.");
|
||||
var localIp = IPAddress.Parse(localAddress);
|
||||
using (var reservation = new Socket(localIp.AddressFamily, SocketType.Dgram, ProtocolType.Udp))
|
||||
using (var rtcp = new Socket(localIp.AddressFamily, SocketType.Dgram, ProtocolType.Udp))
|
||||
{
|
||||
reservation.Bind(new IPEndPoint(IPAddress.Parse(localAddress), port));
|
||||
rtcp.Bind(new IPEndPoint(IPAddress.Parse(localAddress), port + 1));
|
||||
|
|
@ -51,7 +52,8 @@ public sealed class LibVlcLanCallMedia : IAsyncDisposable
|
|||
: new LibVLC("--avcodec-hw=none", "--intf=dummy");
|
||||
try
|
||||
{
|
||||
_receiverMedia = new Media(_receiverVlc, $"rtp://@{localAddress}:{port}", FromType.FromLocation);
|
||||
var host = localIp.AddressFamily == AddressFamily.InterNetworkV6 ? $"[{localAddress}]" : localAddress;
|
||||
_receiverMedia = new Media(_receiverVlc, $"rtp://@{host}:{port}", FromType.FromLocation);
|
||||
_receiverMedia.AddOption(":network-caching=150");
|
||||
_receiverMedia.AddOption(":rtcp-port=" + (port + 1));
|
||||
_receiverMedia.AddOption(":srtp-key=" + Endpoint.Key);
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ public partial class LanCallViewModel : ObservableObject, IAsyncDisposable
|
|||
foreach (var address in NetworkInterface.GetAllNetworkInterfaces()
|
||||
.Where(n => n.OperationalStatus == OperationalStatus.Up)
|
||||
.SelectMany(n => n.GetIPProperties().UnicastAddresses)
|
||||
.Select(a => a.Address.ToString()).Where(LanCallNetwork.IsUnicastIPv4)
|
||||
.Select(a => a.Address.ToString()).Where(LanCallNetwork.IsUnicastAddress)
|
||||
.Distinct().OrderBy(a => IPAddress.IsLoopback(IPAddress.Parse(a))))
|
||||
Addresses.Add(address);
|
||||
Address = preferred == null ? Addresses.FirstOrDefault() : Addresses.FirstOrDefault(a => a == preferred);
|
||||
|
|
@ -181,7 +181,7 @@ public partial class LanCallViewModel : ObservableObject, IAsyncDisposable
|
|||
Status = Cameras.Count == 0 ? "Aucune camera detectee. Verifiez les peripheriques puis actualisez."
|
||||
: Incoming && _call != null
|
||||
? $"{_call.Caller} propose un appel dans {_call.RoomName}. Selectionnez vos peripheriques puis acceptez ou refusez."
|
||||
: "Selectionnez le pair, l'adresse IPv4 locale ou publique et les peripheriques. Utilisez un casque.";
|
||||
: "Selectionnez le pair, l'adresse IPv4/IPv6 locale ou publique et les peripheriques. Utilisez un casque.";
|
||||
if (_settings != null)
|
||||
{
|
||||
_settings.PodcastCameras = Cameras.ToArray();
|
||||
|
|
@ -200,10 +200,10 @@ public partial class LanCallViewModel : ObservableObject, IAsyncDisposable
|
|||
private void ValidateSelection()
|
||||
{
|
||||
if (!AllowAddressDisclosure)
|
||||
throw new InvalidOperationException("Confirmez le partage de votre adresse IPv4 avec le pair.");
|
||||
throw new InvalidOperationException("Confirmez le partage de votre adresse IP avec le pair.");
|
||||
if (Camera == null || Microphone == null)
|
||||
throw new InvalidOperationException("Une camera et un microphone sont requis.");
|
||||
if (Address == null || !Addresses.Contains(Address) || !LanCallNetwork.IsUnicastIPv4(Address) ||
|
||||
if (Address == null || !Addresses.Contains(Address) || !LanCallNetwork.IsUnicastAddress(Address) ||
|
||||
!int.TryParse(Port, out var port) || port is < 1024 or > 65534 || port % 2 != 0)
|
||||
throw new InvalidOperationException("Selectionnez une adresse locale et un port UDP pair (1024-65534).");
|
||||
_activeCamera = Camera.Value;
|
||||
|
|
|
|||
|
|
@ -103,16 +103,16 @@
|
|||
Content="Envoyer" />
|
||||
</Grid>
|
||||
|
||||
<Expander Grid.Row="3" Header="Appel direct IPv4 (LAN/VPN ou IP publique)" IsVisible="{Binding SupportsLanCalls}" IsExpanded="True"
|
||||
<Expander Grid.Row="3" Header="Appel direct IPv4/IPv6 (LAN/VPN ou IP publique)" IsVisible="{Binding SupportsLanCalls}" IsExpanded="True"
|
||||
IsEnabled="{Binding IsConnected}" Margin="0,0,0,8">
|
||||
<ScrollViewer MaxHeight="520">
|
||||
<StackPanel DataContext="{Binding LanCall}" x:DataType="vm:LanCallViewModel" Spacing="8">
|
||||
<TextBlock Text="Android et Desktop Linux : IPv4 locale LAN/VPN ou publique directement assignee au PC. SRTP chiffre les medias ; le pair connait votre IP. Ouvrir le port UDP choisi et le suivant. Sans TURN ni traversee NAT. Casque recommande (pas d'annulation d'echo)."
|
||||
<TextBlock Text="Android et Desktop Linux : IPv4 ou IPv6 globale/privee directement assignee a la machine (hors IPv6 link-local). Les pairs doivent pouvoir joindre leurs adresses. SRTP chiffre les medias ; le pair connait votre IP. Ouvrir le port UDP choisi et le suivant. Sans TURN ni traversee NAT. Casque recommande (pas d'annulation d'echo)."
|
||||
TextWrapping="Wrap" />
|
||||
<WrapPanel IsEnabled="{Binding CanConfigure}" IsVisible="{Binding !RemoteVideo}">
|
||||
<Button Content="Actualiser les peripheriques" Command="{Binding RefreshDevicesCommand}" Margin="0,0,8,4" />
|
||||
<ComboBox Width="160" ItemsSource="{Binding Addresses}" SelectedItem="{Binding Address}"
|
||||
PlaceholderText="Adresse IPv4 de cette machine" Margin="0,0,8,4" />
|
||||
PlaceholderText="Adresse IPv4/IPv6 de cette machine" Margin="0,0,8,4" />
|
||||
<TextBox Width="80" Text="{Binding Port}" PlaceholderText="Port pair" Margin="0,0,8,4" />
|
||||
<ComboBox Width="190" ItemsSource="{Binding Cameras}" SelectedItem="{Binding Camera}"
|
||||
PlaceholderText="Camera" Margin="0,0,8,4">
|
||||
|
|
@ -132,7 +132,7 @@
|
|||
</ComboBox>
|
||||
<TextBox Width="170" Text="{Binding Target}" PlaceholderText="Pseudo du pair" Margin="0,0,8,4" />
|
||||
</WrapPanel>
|
||||
<CheckBox Content="Je consens a partager mon adresse IPv4 avec le pair."
|
||||
<CheckBox Content="Je consens a partager mon adresse IP avec le pair."
|
||||
IsChecked="{Binding AllowAddressDisclosure}" IsEnabled="{Binding CanConfigure}"
|
||||
IsVisible="{Binding !RemoteVideo}" />
|
||||
<WrapPanel>
|
||||
|
|
|
|||
|
|
@ -40,13 +40,26 @@ public sealed record LanCallEndpoint(string Address, int Port, string Key, strin
|
|||
public override string ToString() => "SRTP endpoint (credentials redacted)";
|
||||
|
||||
public bool IsValid() =>
|
||||
LanCallNetwork.IsUnicastIPv4(Address) && Port is >= 1024 and <= 65534 && Port % 2 == 0 &&
|
||||
LanCallNetwork.IsUnicastAddress(Address) && Port is >= 1024 and <= 65534 && Port % 2 == 0 &&
|
||||
Key is { Length: 32 } && Key.All(Uri.IsHexDigit) &&
|
||||
Salt is { Length: 28 } && Salt.All(Uri.IsHexDigit);
|
||||
}
|
||||
|
||||
public static class LanCallNetwork
|
||||
{
|
||||
public static bool IsUnicastAddress(string address)
|
||||
{
|
||||
if (IsUnicastIPv4(address)) return true;
|
||||
if (!System.Net.IPAddress.TryParse(address, out var ip) ||
|
||||
ip.AddressFamily != System.Net.Sockets.AddressFamily.InterNetworkV6 ||
|
||||
ip.ToString() != address || ip.ScopeId != 0 || ip.IsIPv4MappedToIPv6)
|
||||
return false;
|
||||
var bytes = ip.GetAddressBytes();
|
||||
return ip.Equals(System.Net.IPAddress.IPv6Loopback)
|
||||
|| (bytes[0] & 0xe0) == 0x20
|
||||
|| (bytes[0] & 0xfe) == 0xfc;
|
||||
}
|
||||
|
||||
public static bool IsUnicastIPv4(string address)
|
||||
{
|
||||
if (!System.Net.IPAddress.TryParse(address, out var ip) ||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,8 @@ public sealed class CallLanSignalingTests
|
|||
[Theory]
|
||||
[InlineData("192.168.1.2")]
|
||||
[InlineData("8.8.8.8")]
|
||||
[InlineData("2001:db8::1")]
|
||||
[InlineData("fd00::1")]
|
||||
public async Task Endpoints_are_exchanged_only_once_between_consented_devices_without_ice_credentials(string address)
|
||||
{
|
||||
var hub = new RecordingCallHub();
|
||||
|
|
@ -106,11 +108,24 @@ public sealed class CallLanSignalingTests
|
|||
[InlineData("255.255.255.255", false)]
|
||||
[InlineData("169.254.1.1", false)]
|
||||
[InlineData("::ffff:8.8.8.8", false)]
|
||||
[InlineData("::1", false)]
|
||||
[InlineData("::1", true)]
|
||||
[InlineData("2001:db8::1", true)]
|
||||
[InlineData("2606:4700:4700::1111", true)]
|
||||
[InlineData("fd00::123", true)]
|
||||
[InlineData("fc00::1", true)]
|
||||
[InlineData("::", false)]
|
||||
[InlineData("ff02::1", false)]
|
||||
[InlineData("fe80::1", false)]
|
||||
[InlineData("fe80::1%2", false)]
|
||||
[InlineData("2001:db8::1%2", false)]
|
||||
[InlineData("fec0::1", false)]
|
||||
[InlineData("2001:DB8::1", false)]
|
||||
[InlineData("[2001:db8::1]", false)]
|
||||
[InlineData("2001:db8::1,port=80", false)]
|
||||
[InlineData("localhost", false)]
|
||||
[InlineData("127.1", false)]
|
||||
[InlineData("192.168.1.2,port=80", false)]
|
||||
public void Only_canonical_unicast_ipv4_endpoints_are_allowed(string address, bool expected) =>
|
||||
public void Only_canonical_supported_unicast_endpoints_are_allowed(string address, bool expected) =>
|
||||
Assert.Equal(expected, Endpoint(address).IsValid());
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
Loading…
Reference in a new issue