Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions ZTSharp.Tests/ZeroTierExtFramePacketBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@ public void BuildIpv4Packet_CanBeDecrypted_AndParsed()
to,
from,
ipv4Packet,
sharedKey);
sharedKey,
remoteProtocolVersion: 11);

Assert.Equal(1, (packet[18] & 0x38) >> 3);
Assert.True(ZeroTierPacketCrypto.Dearmor(packet, sharedKey));
Assert.Equal(ZeroTierVerb.ExtFrame, (ZeroTierVerb)(packet[27] & 0x1F));

Expand All @@ -62,4 +64,3 @@ public void BuildIpv4Packet_CanBeDecrypted_AndParsed()
Assert.Equal(ipv4Packet, frame.ToArray());
}
}

4 changes: 3 additions & 1 deletion ZTSharp.Tests/ZeroTierHelloClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public async Task HelloAsync_SendsHello_AndCompletesOnOk()

var serverTask = RunHelloServerOnceAsync(remoteUdp, remoteIdentity, localIdentity);

await ZeroTierHelloClient.HelloAsync(
var remoteProtocolVersion = await ZeroTierHelloClient.HelloAsync(
udp,
localIdentity,
planet,
Expand All @@ -95,6 +95,8 @@ await ZeroTierHelloClient.HelloAsync(
timeout: TimeSpan.FromSeconds(2),
cancellationToken: CancellationToken.None);

Assert.Equal(11, remoteProtocolVersion);

await serverTask;
}

Expand Down
2 changes: 2 additions & 0 deletions ZTSharp.Tests/ZeroTierMulticastGatherClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public async Task GatherAsync_IncludesInlineCom_AndReturnsMembers()
rootIdentity.NodeId,
rootEndpoint,
rootKey,
rootProtocolVersion: 12,
localIdentity.NodeId,
networkId,
group,
Expand Down Expand Up @@ -106,6 +107,7 @@ public async Task GatherAsync_ThrowsOnErrorResponse()
rootIdentity.NodeId,
rootEndpoint,
rootKey,
rootProtocolVersion: 12,
localIdentity.NodeId,
networkId,
group,
Expand Down
88 changes: 71 additions & 17 deletions ZTSharp.Tests/ZeroTierPacketCryptoTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ public sealed class ZeroTierPacketCryptoTests
0x1a, 0xe1, 0x3d, 0x70, 0x49, 0x00, 0xa7, 0xe3, 0x5b, 0x1e, 0xa1, 0x9b, 0x68, 0x1e, 0xa1, 0x73
];

private static readonly byte[] AesGmacSivKatArmoredPacket =
[
0x6a, 0x9c, 0x24, 0x6a, 0x15, 0x94, 0xed, 0xc1, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88,
0x99, 0xaa, 0x18, 0xb9, 0x39, 0x13, 0xb3, 0x2d, 0x34, 0x9e, 0xa0, 0x84, 0x65, 0xd3, 0x97, 0x43,
0x8d, 0xf5, 0xd0, 0xad, 0xd3, 0x2b, 0x70, 0x47
];

[Fact]
public void Salsa20_12_Matches_Upstream_TestVector()
{
Expand Down Expand Up @@ -104,27 +111,75 @@ public void PacketCrypto_Roundtrips_With_And_Without_Encryption()
var payload = "test-payload"u8.ToArray();
var plain = ZeroTierPacketCodec.Encode(header, payload);

var key = new byte[32];
RandomNumberGenerator.Fill(key);
// Encrypt + decrypt (Salsa20/12-Poly1305, 32-byte key)
var key32 = new byte[32];
RandomNumberGenerator.Fill(key32);

// Encrypt + decrypt
var encrypted = (byte[])plain.Clone();
ZeroTierPacketCrypto.Armor(encrypted, key, encryptPayload: true);
Assert.True(ZeroTierPacketCrypto.Dearmor(encrypted, key));
Assert.True(encrypted.AsSpan(27).SequenceEqual(plain.AsSpan(27)));
var encrypted32 = (byte[])plain.Clone();
ZeroTierPacketCrypto.Armor(encrypted32, key32, encryptPayload: true);
Assert.Equal(1, (encrypted32[18] & 0x38) >> 3);
Assert.True(ZeroTierPacketCrypto.Dearmor(encrypted32, key32));
Assert.True(encrypted32.AsSpan(27).SequenceEqual(plain.AsSpan(27)));

// MAC-only + verify
// Encrypt + decrypt (AES-GMAC-SIV, 48-byte key)
var key48 = new byte[48];
RandomNumberGenerator.Fill(key48);

var encrypted48 = (byte[])plain.Clone();
ZeroTierPacketCrypto.Armor(encrypted48, key48, encryptPayload: true);
Assert.Equal(3, (encrypted48[18] & 0x38) >> 3);
Assert.True(ZeroTierPacketCrypto.Dearmor(encrypted48, key48));
Assert.True(encrypted48.AsSpan(27).SequenceEqual(plain.AsSpan(27)));

// MAC-only + verify (cipher suite 0, key length doesn't matter beyond 32 bytes)
var macOnly = (byte[])plain.Clone();
ZeroTierPacketCrypto.Armor(macOnly, key, encryptPayload: false);
Assert.True(ZeroTierPacketCrypto.Dearmor(macOnly, key));
ZeroTierPacketCrypto.Armor(macOnly, key48, encryptPayload: false);
Assert.Equal(0, (macOnly[18] & 0x38) >> 3);
Assert.True(ZeroTierPacketCrypto.Dearmor(macOnly, key48));
Assert.True(macOnly.AsSpan(27).SequenceEqual(plain.AsSpan(27)));

// Wrong key fails
var wrongKey = new byte[32];
RandomNumberGenerator.Fill(wrongKey);
var shouldFail = (byte[])plain.Clone();
ZeroTierPacketCrypto.Armor(shouldFail, key, encryptPayload: true);
Assert.False(ZeroTierPacketCrypto.Dearmor(shouldFail, wrongKey));
// Wrong key fails (Salsa)
var wrongKey32 = new byte[32];
RandomNumberGenerator.Fill(wrongKey32);
var shouldFail32 = (byte[])plain.Clone();
ZeroTierPacketCrypto.Armor(shouldFail32, key32, encryptPayload: true);
Assert.False(ZeroTierPacketCrypto.Dearmor(shouldFail32, wrongKey32));

// Wrong key fails (AES-GMAC-SIV)
var wrongKey48 = new byte[48];
RandomNumberGenerator.Fill(wrongKey48);
var shouldFail48 = (byte[])plain.Clone();
ZeroTierPacketCrypto.Armor(shouldFail48, key48, encryptPayload: true);
Assert.False(ZeroTierPacketCrypto.Dearmor(shouldFail48, wrongKey48));
}

[Fact]
public void AesGmacSiv_Armor_Matches_KnownAnswer()
{
var header = new ZeroTierPacketHeader(
PacketId: 0x0102030405060708UL,
Destination: new NodeId(0x1122334455UL),
Source: new NodeId(0x66778899AAUL),
Flags: 0,
Mac: 0,
VerbRaw: (byte)ZeroTierVerb.Echo);

var payload = "test-payload"u8.ToArray();
var plain = ZeroTierPacketCodec.Encode(header, payload);

var key48 = new byte[48];
for (var i = 0; i < key48.Length; i++)
{
key48[i] = (byte)i;
}

var armored = (byte[])plain.Clone();
ZeroTierPacketCrypto.Armor(armored, key48, encryptPayload: true);

Assert.True(armored.SequenceEqual(AesGmacSivKatArmoredPacket));

Assert.True(ZeroTierPacketCrypto.Dearmor(armored, key48));
Assert.True(armored.AsSpan(27).SequenceEqual(plain.AsSpan(27)));
}

private static byte[] ComputePoly1305(ReadOnlySpan<byte> key, ReadOnlySpan<byte> data)
Expand All @@ -137,4 +192,3 @@ private static byte[] ComputePoly1305(ReadOnlySpan<byte> key, ReadOnlySpan<byte>
return tag;
}
}

22 changes: 17 additions & 5 deletions ZTSharp/ZeroTier/Internal/ZeroTierDataplaneRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ internal sealed class ZeroTierDataplaneRuntime : IAsyncDisposable
private readonly NodeId _rootNodeId;
private readonly IPEndPoint _rootEndpoint;
private readonly byte[] _rootKey;
private readonly byte _rootProtocolVersion;
private readonly ZeroTierIdentity _localIdentity;
private readonly ulong _networkId;
private readonly byte[] _inlineCom;
Expand All @@ -42,6 +43,7 @@ internal sealed class ZeroTierDataplaneRuntime : IAsyncDisposable
private readonly ConcurrentDictionary<ulong, TaskCompletionSource<(uint TotalKnown, NodeId[] Members)>> _pendingGather = new();

private readonly ConcurrentDictionary<NodeId, byte[]> _peerKeys = new();
private readonly ConcurrentDictionary<NodeId, byte> _peerProtocolVersions = new();
private readonly SemaphoreSlim _peerKeyLock = new(1, 1);

private readonly ConcurrentDictionary<IPAddress, NodeId> _managedIpToNodeId = new();
Expand All @@ -62,6 +64,7 @@ public ZeroTierDataplaneRuntime(
NodeId rootNodeId,
IPEndPoint rootEndpoint,
byte[] rootKey,
byte rootProtocolVersion,
ZeroTierIdentity localIdentity,
ulong networkId,
IPAddress? localManagedIpV4,
Expand Down Expand Up @@ -102,6 +105,7 @@ public ZeroTierDataplaneRuntime(
_rootNodeId = rootNodeId;
_rootEndpoint = rootEndpoint;
_rootKey = rootKey;
_rootProtocolVersion = rootProtocolVersion;
_localIdentity = localIdentity;
_networkId = networkId;
_inlineCom = inlineCom;
Expand Down Expand Up @@ -256,6 +260,7 @@ public async ValueTask SendIpv4Async(NodeId peerNodeId, ReadOnlyMemory<byte> ipv
ObjectDisposedException.ThrowIf(_disposed, this);

var key = await GetPeerKeyAsync(peerNodeId, cancellationToken).ConfigureAwait(false);
var peerProtocolVersion = _peerProtocolVersions.TryGetValue(peerNodeId, out var protocolVersion) ? protocolVersion : (byte)0;
var remoteMac = ZeroTierMac.FromAddress(peerNodeId, _networkId);
var packetId = GeneratePacketId();
var packet = ZeroTierExtFramePacketBuilder.BuildPacket(
Expand All @@ -268,7 +273,8 @@ public async ValueTask SendIpv4Async(NodeId peerNodeId, ReadOnlyMemory<byte> ipv
from: _localMac,
etherType: ZeroTierFrameCodec.EtherTypeIpv4,
frame: ipv4Packet.Span,
sharedKey: key);
sharedKey: key,
remoteProtocolVersion: peerProtocolVersion);

await _udp.SendAsync(_rootEndpoint, packet, cancellationToken).ConfigureAwait(false);
}
Expand All @@ -283,6 +289,7 @@ public async ValueTask SendEthernetFrameAsync(
ObjectDisposedException.ThrowIf(_disposed, this);

var key = await GetPeerKeyAsync(peerNodeId, cancellationToken).ConfigureAwait(false);
var peerProtocolVersion = _peerProtocolVersions.TryGetValue(peerNodeId, out var protocolVersion) ? protocolVersion : (byte)0;
var remoteMac = ZeroTierMac.FromAddress(peerNodeId, _networkId);
var packetId = GeneratePacketId();
var packet = ZeroTierExtFramePacketBuilder.BuildPacket(
Expand All @@ -295,7 +302,8 @@ public async ValueTask SendEthernetFrameAsync(
from: _localMac,
etherType: etherType,
frame: frame.Span,
sharedKey: key);
sharedKey: key,
remoteProtocolVersion: peerProtocolVersion);

await _udp.SendAsync(_rootEndpoint, packet, cancellationToken).ConfigureAwait(false);
}
Expand Down Expand Up @@ -638,6 +646,8 @@ private async ValueTask HandleHelloAsync(
}

_peerKeys[peerNodeId] = sharedKey;
var peerProtocolVersion = payload[0];
_peerProtocolVersions[peerNodeId] = peerProtocolVersion;

var okPacket = ZeroTierHelloOkPacketBuilder.BuildPacket(
packetId: GeneratePacketId(),
Expand All @@ -646,7 +656,7 @@ private async ValueTask HandleHelloAsync(
inRePacketId: helloPacketId,
helloTimestampEcho: helloTimestamp,
externalSurfaceAddress: remoteEndPoint,
sharedKey: sharedKey);
sharedKey: ZeroTierPacketCrypto.SelectOutboundKey(sharedKey, peerProtocolVersion));

try
{
Expand Down Expand Up @@ -1290,7 +1300,8 @@ private async Task<ZeroTierIdentity> WhoisAsync(NodeId targetNodeId, TimeSpan ti
VerbRaw: (byte)ZeroTierVerb.Whois);

var packet = ZeroTierPacketCodec.Encode(header, payload);
ZeroTierPacketCrypto.Armor(packet, _rootKey, encryptPayload: true);
ZeroTierPacketCrypto.Armor(packet, ZeroTierPacketCrypto.SelectOutboundKey(_rootKey, _rootProtocolVersion), encryptPayload: true);
packetId = BinaryPrimitives.ReadUInt64BigEndian(packet.AsSpan(0, 8));

var tcs = new TaskCompletionSource<ZeroTierIdentity>(TaskCreationOptions.RunContinuationsAsynchronously);
if (!_pendingWhois.TryAdd(packetId, tcs))
Expand Down Expand Up @@ -1337,7 +1348,8 @@ private async Task<ZeroTierIdentity> WhoisAsync(NodeId targetNodeId, TimeSpan ti
VerbRaw: (byte)ZeroTierVerb.MulticastGather);

var packet = ZeroTierPacketCodec.Encode(header, payload);
ZeroTierPacketCrypto.Armor(packet, _rootKey, encryptPayload: true);
ZeroTierPacketCrypto.Armor(packet, ZeroTierPacketCrypto.SelectOutboundKey(_rootKey, _rootProtocolVersion), encryptPayload: true);
packetId = BinaryPrimitives.ReadUInt64BigEndian(packet.AsSpan(0, 8));

var tcs = new TaskCompletionSource<(uint TotalKnown, NodeId[] Members)>(TaskCreationOptions.RunContinuationsAsynchronously);
if (!_pendingGather.TryAdd(packetId, tcs))
Expand Down
13 changes: 8 additions & 5 deletions ZTSharp/ZeroTier/Internal/ZeroTierExtFramePacketBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ public static byte[] BuildPacket(
ZeroTierMac from,
ushort etherType,
ReadOnlySpan<byte> frame,
ReadOnlySpan<byte> sharedKey)
ReadOnlySpan<byte> sharedKey,
byte remoteProtocolVersion)
{
var extFrameFlags = (byte)(0x01 | (ZeroTierTrace.Enabled ? 0x10 : 0x00));
var payload = ZeroTierFrameCodec.EncodeExtFramePayload(
Expand All @@ -32,10 +33,10 @@ public static byte[] BuildPacket(
Source: source,
Flags: 0,
Mac: 0,
VerbRaw: (byte)ZeroTierVerb.ExtFrame);
VerbRaw: (byte)ZeroTierVerb.ExtFrame);

var packet = ZeroTierPacketCodec.Encode(header, payload);
ZeroTierPacketCrypto.Armor(packet, sharedKey, encryptPayload: true);
ZeroTierPacketCrypto.Armor(packet, ZeroTierPacketCrypto.SelectOutboundKey(sharedKey, remoteProtocolVersion), encryptPayload: true);
return packet;
}

Expand All @@ -48,7 +49,8 @@ public static byte[] BuildIpv4Packet(
ZeroTierMac to,
ZeroTierMac from,
ReadOnlySpan<byte> ipv4Packet,
ReadOnlySpan<byte> sharedKey)
ReadOnlySpan<byte> sharedKey,
byte remoteProtocolVersion)
=> BuildPacket(
packetId,
destination,
Expand All @@ -59,5 +61,6 @@ public static byte[] BuildIpv4Packet(
from,
ZeroTierFrameCodec.EtherTypeIpv4,
ipv4Packet,
sharedKey);
sharedKey,
remoteProtocolVersion);
}
7 changes: 4 additions & 3 deletions ZTSharp/ZeroTier/Internal/ZeroTierHelloClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ internal readonly record struct ZeroTierHelloOk(

internal static class ZeroTierHelloClient
{
internal const byte AdvertisedProtocolVersion = 11; // <12 avoids AES-GMAC-SIV (v12+), which is not implemented.
internal const byte AdvertisedProtocolVersion = 12;
internal const byte AdvertisedMajorVersion = 1;
internal const byte AdvertisedMinorVersion = 12;
internal const ushort AdvertisedRevision = 0;
Expand Down Expand Up @@ -195,7 +195,7 @@ public static async Task<ZeroTierHelloOk> HelloRootsAsync(
}
}

public static async Task HelloAsync(
public static async Task<byte> HelloAsync(
ZeroTierUdpTransport udp,
ZeroTierIdentity localIdentity,
ZeroTierWorld planet,
Expand Down Expand Up @@ -297,7 +297,8 @@ public static async Task HelloAsync(
continue;
}

return;
var remoteProto = packetBytes[HelloOkIndexProtocolVersion];
return remoteProto;
}
}

Expand Down
10 changes: 7 additions & 3 deletions ZTSharp/ZeroTier/Internal/ZeroTierIpv4Link.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ internal sealed class ZeroTierIpv4Link : IUserSpaceIpLink
private readonly ZeroTierMac _from;
private readonly byte[] _rootKey;
private readonly byte[] _sharedKey;
private readonly byte _remoteProtocolVersion;
private IPEndPoint[] _directEndpoints = Array.Empty<IPEndPoint>();
private int _traceRxRemaining = 50;
private int _traceRxVerbRemaining = 50;
Expand All @@ -42,7 +43,8 @@ public ZeroTierIpv4Link(
ulong networkId,
IPAddress localManagedIp,
byte[] inlineCom,
byte[] sharedKey)
byte[] sharedKey,
byte remoteProtocolVersion)
{
ArgumentNullException.ThrowIfNull(udp);
ArgumentNullException.ThrowIfNull(relayEndpoint);
Expand All @@ -68,6 +70,7 @@ public ZeroTierIpv4Link(
_from = ZeroTierMac.FromAddress(localNodeId, networkId);
_rootKey = rootKey;
_sharedKey = sharedKey;
_remoteProtocolVersion = remoteProtocolVersion;
}

public async ValueTask SendAsync(ReadOnlyMemory<byte> ipPacket, CancellationToken cancellationToken = default)
Expand All @@ -86,7 +89,8 @@ public async ValueTask SendAsync(ReadOnlyMemory<byte> ipPacket, CancellationToke
to: _to,
from: _from,
ipv4Packet: ipv4Packet.Span,
sharedKey: _sharedKey);
sharedKey: _sharedKey,
remoteProtocolVersion: _remoteProtocolVersion);

var directEndpoints = _directEndpoints;
if (ZeroTierTrace.Enabled && _traceTxRemaining > 0)
Expand Down Expand Up @@ -565,7 +569,7 @@ private byte[] BuildExtFramePacket(ulong packetId, ushort etherType, ReadOnlySpa
VerbRaw: (byte)ZeroTierVerb.ExtFrame);

var packet = ZeroTierPacketCodec.Encode(header, payload);
ZeroTierPacketCrypto.Armor(packet, _sharedKey, encryptPayload: true);
ZeroTierPacketCrypto.Armor(packet, ZeroTierPacketCrypto.SelectOutboundKey(_sharedKey, _remoteProtocolVersion), encryptPayload: true);
return packet;
}

Expand Down
Loading