fix: support rootless ICMP and verify SELinux deployment [CI]
ci/woodpecker/push/main-amd64-build Pipeline was successful
ci/woodpecker/push/main-amd64-build Pipeline was successful
This commit is contained in:
8 files changed
+134
-24
No files matched your search
@@ -1,6 +1,7 @@
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Net.Sockets;
|
||||
using System.Security.Authentication;
|
||||
using Fleet.Core;
|
||||
using MoShiro.Protocol;
|
||||
|
||||
namespace MoShiro.Measurement;
|
||||
@@ -306,7 +307,8 @@ public sealed class IcmpEchoSolo : BlockBase
|
||||
TimeSpan interval = TimeSpan.FromSeconds(1.0 / Spec.Int("rate_hz"));
|
||||
int size = (int)Spec.Int("size");
|
||||
byte[] payload = new byte[Math.Max(0, size - 28)];
|
||||
using var ping = new Ping();
|
||||
using LinuxIcmpEcho? linuxPing = OperatingSystem.IsLinux() ? LinuxIcmpEcho.TryCreate(Env.PeerAddress!, payload.Length) : null;
|
||||
using Ping? ping = linuxPing is null ? new Ping() : null;
|
||||
try
|
||||
{
|
||||
while (!ct.IsCancellationRequested)
|
||||
@@ -315,8 +317,10 @@ public sealed class IcmpEchoSolo : BlockBase
|
||||
long? t4 = null;
|
||||
try
|
||||
{
|
||||
PingReply reply = await ping.SendPingAsync(Env.PeerAddress!, TimeSpan.FromSeconds(2), payload, null, ct);
|
||||
if (reply.Status == IPStatus.Success)
|
||||
bool receivedReply = linuxPing is not null
|
||||
? await linuxPing.SendAsync(TimeSpan.FromSeconds(2), ct)
|
||||
: (await ping!.SendPingAsync(Env.PeerAddress!, TimeSpan.FromSeconds(2), payload, null, ct)).Status == IPStatus.Success;
|
||||
if (receivedReply)
|
||||
{
|
||||
t4 = Env.Clock.NowUs();
|
||||
}
|
||||
@@ -325,6 +329,11 @@ public sealed class IcmpEchoSolo : BlockBase
|
||||
{
|
||||
// 发送失败(例如本机没有路由):记为丢失
|
||||
}
|
||||
catch (PlatformNotSupportedException) when (OperatingSystem.IsLinux())
|
||||
{
|
||||
Env.Log.Warn("ICMP permission unavailable", ("hint", "allow the mapped process GID in net.ipv4.ping_group_range, or grant effective NET_RAW"));
|
||||
throw;
|
||||
}
|
||||
|
||||
long key = _log.Store.Append();
|
||||
_log.Store.Set(key, _seq, key);
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace MoShiro.Measurement;
|
||||
|
||||
/// <summary>Linux ping sockets permit ICMP echo without CAP_NET_RAW when ping_group_range allows the process group.</summary>
|
||||
internal sealed class LinuxIcmpEcho : IDisposable
|
||||
{
|
||||
private readonly Socket _socket;
|
||||
private readonly byte[] _request;
|
||||
private readonly byte[] _response;
|
||||
private readonly byte _replyType;
|
||||
private ushort _sequence;
|
||||
|
||||
private LinuxIcmpEcho(IPAddress address, int payloadSize)
|
||||
{
|
||||
bool ipv6 = address.AddressFamily == AddressFamily.InterNetworkV6;
|
||||
_socket = new Socket(address.AddressFamily, SocketType.Dgram, ipv6 ? ProtocolType.IcmpV6 : ProtocolType.Icmp);
|
||||
try
|
||||
{
|
||||
_socket.Connect(new IPEndPoint(address, 0));
|
||||
}
|
||||
catch
|
||||
{
|
||||
_socket.Dispose();
|
||||
throw;
|
||||
}
|
||||
|
||||
_request = new byte[8 + payloadSize];
|
||||
_request[0] = ipv6 ? (byte)128 : (byte)8;
|
||||
_replyType = ipv6 ? (byte)129 : (byte)0;
|
||||
_response = new byte[_request.Length + 64];
|
||||
// The Linux ping socket supplies the identifier and checksum.
|
||||
Random.Shared.NextBytes(_request.AsSpan(8));
|
||||
}
|
||||
|
||||
public static LinuxIcmpEcho? TryCreate(IPAddress address, int payloadSize)
|
||||
{
|
||||
try
|
||||
{
|
||||
return new LinuxIcmpEcho(address, payloadSize);
|
||||
}
|
||||
catch (SocketException)
|
||||
{
|
||||
// Preserve System.Net.Ping's raw-socket path for hosts that allow NET_RAW instead.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<bool> SendAsync(TimeSpan timeout, CancellationToken ct)
|
||||
{
|
||||
ushort sequence = unchecked(++_sequence);
|
||||
BinaryPrimitives.WriteUInt16BigEndian(_request.AsSpan(6), sequence);
|
||||
using var deadline = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
deadline.CancelAfter(timeout);
|
||||
try
|
||||
{
|
||||
await _socket.SendAsync(_request, SocketFlags.None, deadline.Token);
|
||||
while (true)
|
||||
{
|
||||
int count = await _socket.ReceiveAsync(_response, SocketFlags.None, deadline.Token);
|
||||
if (IsReply(_response.AsSpan(0, count), _request, _replyType, sequence))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (!ct.IsCancellationRequested)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (SocketException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool IsReply(ReadOnlySpan<byte> reply, ReadOnlySpan<byte> request, byte replyType, ushort sequence) =>
|
||||
reply.Length == request.Length && reply.Length >= 8 && reply[0] == replyType && reply[1] == 0 &&
|
||||
BinaryPrimitives.ReadUInt16BigEndian(reply[6..]) == sequence && reply[8..].SequenceEqual(request[8..]);
|
||||
|
||||
public void Dispose() => _socket.Dispose();
|
||||
}
|
||||
Reference in new issue
Block a user