v0.6: DHCP tunnel transport with NOP heartbeat protocol
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
using System.Net;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using Robovoice.Core;
|
||||
|
||||
namespace Robovoice.Stt.Dhcp;
|
||||
|
||||
public sealed class DhcpSttSource : ISttSource
|
||||
{
|
||||
private const string Magic = "HKMSTR";
|
||||
private const int DhcpClientPort = 68;
|
||||
private const int DhcpServerPort = 67;
|
||||
private static readonly TimeSpan NopInterval = TimeSpan.FromMilliseconds(50);
|
||||
|
||||
private Socket? _sock;
|
||||
private CancellationTokenSource? _cts;
|
||||
private Task? _receiveTask;
|
||||
private Task? _nopTask;
|
||||
private EndPoint _broadcastEp = new IPEndPoint(IPAddress.Broadcast, DhcpServerPort);
|
||||
private uint _nonce;
|
||||
private bool _disposed;
|
||||
|
||||
public string InterfaceIp { get; set; } = string.Empty;
|
||||
|
||||
public Action<string>? Log { get; set; }
|
||||
|
||||
public event TranscriptEventHandler? TranscriptReceived;
|
||||
|
||||
public Task StartAsync(CancellationToken ct = default)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (_cts is not null)
|
||||
return Task.CompletedTask;
|
||||
|
||||
string ip = string.IsNullOrEmpty(InterfaceIp) ? AutoDetectInterfaceIp() ?? "0.0.0.0" : InterfaceIp;
|
||||
|
||||
_sock = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
|
||||
_sock.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
|
||||
_sock.EnableBroadcast = true;
|
||||
_sock.Bind(new IPEndPoint(IPAddress.Parse(ip), DhcpClientPort));
|
||||
|
||||
Log?.Invoke($"STT: bound to {ip}:{DhcpClientPort}, broadcasting to :{DhcpServerPort}");
|
||||
|
||||
_cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
_receiveTask = ReceiveLoopAsync(_cts.Token);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken ct = default)
|
||||
{
|
||||
StopNop();
|
||||
|
||||
if (_cts is not null)
|
||||
_cts.Cancel();
|
||||
|
||||
_sock?.Dispose();
|
||||
_sock = null;
|
||||
|
||||
if (_receiveTask is not null)
|
||||
{
|
||||
try { await _receiveTask.WaitAsync(ct); }
|
||||
catch { }
|
||||
_receiveTask = null;
|
||||
}
|
||||
|
||||
_cts?.Dispose();
|
||||
_cts = null;
|
||||
}
|
||||
|
||||
public void SendOn()
|
||||
{
|
||||
if (_cts is null)
|
||||
return;
|
||||
|
||||
_nonce = 0;
|
||||
SendNop();
|
||||
_nopTask = NopLoopAsync(_cts.Token);
|
||||
}
|
||||
|
||||
public void SendOff()
|
||||
{
|
||||
StopNop();
|
||||
SendControl("HKMSTR:OFF");
|
||||
}
|
||||
|
||||
private void StopNop()
|
||||
{
|
||||
if (_nopTask is not null)
|
||||
{
|
||||
try { _nopTask.Wait(2000); } catch { }
|
||||
_nopTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task NopLoopAsync(CancellationToken ct)
|
||||
{
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
try { await Task.Delay(NopInterval, ct); }
|
||||
catch (OperationCanceledException) { break; }
|
||||
|
||||
SendNop();
|
||||
}
|
||||
}
|
||||
|
||||
private void SendNop()
|
||||
{
|
||||
_nonce++;
|
||||
SendControl($"HKMSTR {_nonce}");
|
||||
}
|
||||
|
||||
private void SendControl(string message)
|
||||
{
|
||||
if (_sock is null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
byte[] payload = Encoding.UTF8.GetBytes(message + "\n");
|
||||
_sock.SendTo(payload, _broadcastEp);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log?.Invoke($"STT: send failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ReceiveLoopAsync(CancellationToken ct)
|
||||
{
|
||||
byte[] buffer = new byte[4096];
|
||||
EndPoint fromEp = new IPEndPoint(IPAddress.Any, 0);
|
||||
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
int received;
|
||||
try
|
||||
{
|
||||
if (!_sock!.Poll(500_000, SelectMode.SelectRead))
|
||||
continue;
|
||||
received = _sock.ReceiveFrom(buffer, ref fromEp);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log?.Invoke($"STT: receive error: {ex.Message}");
|
||||
continue;
|
||||
}
|
||||
|
||||
string text = Encoding.UTF8.GetString(buffer, 0, received).TrimEnd('\n', '\r');
|
||||
if (!text.StartsWith(Magic))
|
||||
continue;
|
||||
|
||||
if (text.StartsWith("HKMSTR:P "))
|
||||
{
|
||||
string transcript = text["HKMSTR:P ".Length..];
|
||||
TranscriptReceived?.Invoke(this, new TranscriptEventArgs
|
||||
{
|
||||
Message = new TranscriptMessage(TranscriptType.Partial, transcript),
|
||||
});
|
||||
}
|
||||
else if (text.StartsWith("HKMSTR:F "))
|
||||
{
|
||||
string transcript = text["HKMSTR:F ".Length..];
|
||||
TranscriptReceived?.Invoke(this, new TranscriptEventArgs
|
||||
{
|
||||
Message = new TranscriptMessage(TranscriptType.Final, transcript),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static List<(string Ip, string Name)> GetAvailableInterfaces()
|
||||
{
|
||||
var result = new List<(string, string)>();
|
||||
|
||||
foreach (var nic in NetworkInterface.GetAllNetworkInterfaces())
|
||||
{
|
||||
if (nic.OperationalStatus != OperationalStatus.Up)
|
||||
continue;
|
||||
if (nic.NetworkInterfaceType == NetworkInterfaceType.Loopback)
|
||||
continue;
|
||||
|
||||
string desc = nic.Description;
|
||||
if (desc.Contains("WireGuard", StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
foreach (var addr in nic.GetIPProperties().UnicastAddresses)
|
||||
{
|
||||
if (addr.Address.AddressFamily != AddressFamily.InterNetwork)
|
||||
continue;
|
||||
|
||||
string ip = addr.Address.ToString();
|
||||
if (ip.StartsWith("192.168.") || ip.StartsWith("10.") ||
|
||||
ip.StartsWith("172.16.") || ip.StartsWith("172.17.") ||
|
||||
ip.StartsWith("172.18.") || ip.StartsWith("172.19.") ||
|
||||
ip.StartsWith("172.20.") || ip.StartsWith("172.21.") ||
|
||||
ip.StartsWith("172.22.") || ip.StartsWith("172.23.") ||
|
||||
ip.StartsWith("172.24.") || ip.StartsWith("172.25.") ||
|
||||
ip.StartsWith("172.26.") || ip.StartsWith("172.27.") ||
|
||||
ip.StartsWith("172.28.") || ip.StartsWith("172.29.") ||
|
||||
ip.StartsWith("172.30.") || ip.StartsWith("172.31."))
|
||||
{
|
||||
result.Add((ip, $"{nic.Name} ({ip})"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string? AutoDetectInterfaceIp()
|
||||
{
|
||||
foreach (var (ip, _) in GetAvailableInterfaces())
|
||||
return ip;
|
||||
return null;
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_disposed) return;
|
||||
await StopAsync();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user