113 lines
5.0 KiB
C#
113 lines
5.0 KiB
C#
using System.Net;
|
|
using Fleet.Identity;
|
|
|
|
namespace Fleet.Tests;
|
|
|
|
public sealed class KnockTests
|
|
{
|
|
private sealed class Clock : TimeProvider
|
|
{
|
|
public DateTimeOffset Now { get; set; } = new(2026, 10, 7, 12, 0, 0, TimeSpan.Zero);
|
|
|
|
public override DateTimeOffset GetUtcNow() => Now;
|
|
}
|
|
|
|
[Fact]
|
|
public void PairKey_IsTheSameOnBothSides_AndDiffersPerPair()
|
|
{
|
|
using NodeIdentity probe = NodeIdentity.Generate();
|
|
using NodeIdentity agent = NodeIdentity.Generate();
|
|
using NodeIdentity other = NodeIdentity.Generate();
|
|
byte[] fromProbe = Knock.DeriveKey(probe, agent.PublicKeySpki);
|
|
byte[] fromAgent = Knock.DeriveKey(agent, probe.PublicKeySpki);
|
|
Assert.Equal(fromProbe, fromAgent);
|
|
Assert.Equal(32, fromProbe.Length);
|
|
Assert.NotEqual(fromProbe, Knock.DeriveKey(other, agent.PublicKeySpki));
|
|
}
|
|
|
|
[Fact]
|
|
public void Packet_IsAcceptedWithinTheWindowAndOneWindowOfSkew_ButNotBeyond()
|
|
{
|
|
byte[] key = new byte[32];
|
|
long now = new DateTimeOffset(2026, 10, 7, 12, 0, 15, TimeSpan.Zero).ToUnixTimeMilliseconds();
|
|
byte[] packet = Knock.Build(key, now);
|
|
Assert.Equal(Knock.PacketBytes, packet.Length);
|
|
Assert.True(Knock.Check(key, packet, now));
|
|
Assert.True(Knock.Check(key, packet, now + (Knock.WindowSeconds * 1000)));
|
|
Assert.True(Knock.Check(key, packet, now - (Knock.WindowSeconds * 1000)));
|
|
Assert.False(Knock.Check(key, packet, now + (3 * Knock.WindowSeconds * 1000)));
|
|
Assert.False(Knock.Check(key, packet, now - (3 * Knock.WindowSeconds * 1000)));
|
|
}
|
|
|
|
[Fact]
|
|
public void Packet_WithAWrongKeyATamperedByteOrTheWrongLength_IsRejected()
|
|
{
|
|
byte[] key = new byte[32];
|
|
long now = 1_700_000_000_000;
|
|
byte[] packet = Knock.Build(key, now);
|
|
Assert.False(Knock.Check(new byte[32] { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, packet, now));
|
|
foreach (int index in new[] { 0, 15, 16, 31 })
|
|
{
|
|
byte[] tampered = (byte[])packet.Clone();
|
|
tampered[index] ^= 0x01;
|
|
Assert.False(Knock.Check(key, tampered, now), $"byte {index}");
|
|
}
|
|
|
|
Assert.False(Knock.Check(key, packet.AsSpan(0, 31), now));
|
|
Assert.False(Knock.Check(key, [], now));
|
|
}
|
|
|
|
[Fact]
|
|
public void Gate_AdmitsRosterMembersOnce_AndRejectsReplaysStrangersAndExpiredPackets()
|
|
{
|
|
using NodeIdentity agent = NodeIdentity.Generate();
|
|
using NodeIdentity probe = NodeIdentity.Generate();
|
|
using NodeIdentity stranger = NodeIdentity.Generate();
|
|
var roster = new List<byte[]> { probe.PublicKeySpki };
|
|
var gate = new KnockGate(agent, () => roster, new Clock());
|
|
long now = 1_700_000_000_000;
|
|
|
|
byte[] packet = Knock.Build(Knock.DeriveKey(probe, agent.PublicKeySpki), now);
|
|
Assert.True(gate.Accept(packet, now));
|
|
Assert.False(gate.Accept(packet, now), "the same nonce cannot be used twice");
|
|
Assert.False(gate.Accept(Knock.Build(Knock.DeriveKey(stranger, agent.PublicKeySpki), now), now), "not on the roster");
|
|
Assert.False(gate.Accept(Knock.Build(Knock.DeriveKey(probe, agent.PublicKeySpki), now - 10 * 60_000), now), "expired");
|
|
|
|
// 名册变了:新成员马上能进,被移除的马上进不来
|
|
using NodeIdentity second = NodeIdentity.Generate();
|
|
roster.Add(second.PublicKeySpki);
|
|
Assert.True(gate.Accept(Knock.Build(Knock.DeriveKey(second, agent.PublicKeySpki), now), now));
|
|
roster.RemoveAt(0);
|
|
Assert.False(gate.Accept(Knock.Build(Knock.DeriveKey(probe, agent.PublicKeySpki), now), now));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Gate_RateLimitsEachSource_AndReadsAtMost32Bytes()
|
|
{
|
|
using NodeIdentity agent = NodeIdentity.Generate();
|
|
using NodeIdentity probe = NodeIdentity.Generate();
|
|
var clock = new Clock();
|
|
var gate = new KnockGate(agent, () => [probe.PublicKeySpki], clock, attemptsPerPeriod: 3);
|
|
IPAddress noisy = IPAddress.Parse("203.0.113.9");
|
|
IPAddress quiet = IPAddress.Parse("203.0.113.10");
|
|
byte[] key = Knock.DeriveKey(probe, agent.PublicKeySpki);
|
|
|
|
for (int i = 0; i < 3; i++)
|
|
{
|
|
Assert.False(await gate.AdmitAsync(new MemoryStream(new byte[Knock.PacketBytes]), noisy, default));
|
|
}
|
|
|
|
// 超过限额:连有效的敲门包也不读、不算
|
|
byte[] valid = Knock.Build(key, clock.Now.ToUnixTimeMilliseconds());
|
|
Assert.False(await gate.AdmitAsync(new MemoryStream(valid), noisy, default));
|
|
Assert.True(await gate.AdmitAsync(new MemoryStream(valid), quiet, default));
|
|
|
|
// 过了统计周期,恢复
|
|
clock.Now += TimeSpan.FromSeconds(11);
|
|
Assert.True(await gate.AdmitAsync(new MemoryStream(Knock.Build(key, clock.Now.ToUnixTimeMilliseconds())), noisy, default));
|
|
|
|
// 流在 32 字节之前就结束:拒绝,不抛异常
|
|
Assert.False(await gate.AdmitAsync(new MemoryStream(new byte[10]), quiet, default));
|
|
}
|
|
}
|