Skip to content

Commit 16bdcc8

Browse files
committed
Add native SCTP socket adapter
1 parent 1e2e034 commit 16bdcc8

5 files changed

Lines changed: 161 additions & 1 deletion

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ The first production milestone is M3UA over a transport abstraction. SCCP, TCAP,
4545
| Observability | Phase 7 profile added for commercial metrics, trace categories, and health signals |
4646
| Deployment profiles | Phase 7 profiles added for commercial Linux and local development use |
4747
| Phase 7 status | Commercialization foundation complete; commercial production remains blocked on native SCTP verification, external lab evidence, signing, and SBOM |
48-
| Native SCTP implementation | Phase 8 started: Linux SCTP socket probe, socket factory, and endpoint connection planner added; full production readiness still requires native transport completion and lab verification |
48+
| Native SCTP implementation | Phase 8 started: Linux SCTP socket probe, socket factory, endpoint connection planner, and native socket adapter added; full production readiness still requires native transport completion and lab verification |
4949

5050
## Requirements
5151

docs/PHASE8_NATIVE_SCTP.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,14 @@ NativeSctpConnectionPlan plan = await new NativeSctpConnectionPlanner()
3636
```
3737

3838
The planner resolves the remote endpoint and optional local endpoint before a socket attempts bind/connect. Phase 8 currently resolves IPv4 endpoints because the Linux native SCTP path starts with IPv4 verification.
39+
40+
## Socket Adapter
41+
42+
`NativeSctpSocketAdapter` wraps a native SCTP socket behind the SDK `ISctpSocket` contract.
43+
44+
```csharp
45+
using Socket socket = factory.CreateSocket();
46+
using NativeSctpSocketAdapter adapter = new(socket, options);
47+
```
48+
49+
The adapter exposes lifecycle state and `SctpTransportHealth` snapshots. It currently sends and receives complete socket messages through the native socket API; SCTP ancillary metadata handling is tracked separately before production readiness can be claimed.

docs/SCTP_TRANSPORT.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,5 @@ The probe does not mark the transport production-ready by itself. It is the firs
128128
`NativeSctpSocketFactory` centralizes socket creation and throws `NativeSctpUnavailableException` when the current platform cannot create native SCTP sockets.
129129

130130
`NativeSctpConnectionPlanner` resolves configured SCTP endpoints to `IPEndPoint` values before native bind/connect attempts.
131+
132+
`NativeSctpSocketAdapter` wraps an SCTP socket as `ISctpSocket` and reports `SctpTransportHealth` snapshots for native associations.

src/sigtran.net.Tests/Program.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
Run("Native SCTP platform probe reports socket creation capability", NativeSctpPlatformProbeReportsSocketCreationCapability);
3434
Run("Native SCTP socket factory creates or reports unsupported platform", NativeSctpSocketFactoryCreatesOrReportsUnsupportedPlatform);
3535
Run("Native SCTP connection planner resolves endpoints", NativeSctpConnectionPlannerResolvesEndpoints);
36+
Run("Native SCTP socket adapter reports lifecycle health", NativeSctpSocketAdapterReportsLifecycleHealth);
3637
Run("TCAP BER element encodes short and long lengths", TcapBerElementEncodesShortAndLongLengths);
3738
Run("TCAP transaction identifiers use BER context tags", TcapTransactionIdentifiersUseBerContextTags);
3839
Run("TCAP BER Invoke component round-trips", TcapBerInvokeComponentRoundTrips);
@@ -447,6 +448,29 @@ static void NativeSctpConnectionPlannerResolvesEndpoints()
447448
Assert(plan.Describe().Contains("remote=127.0.0.1:2905", StringComparison.Ordinal), plan.Describe());
448449
}
449450

451+
static void NativeSctpSocketAdapterReportsLifecycleHealth()
452+
{
453+
NativeSctpPlatformCapability capability = NativeSctpPlatform.Probe();
454+
if (!capability.CanCreateSocket)
455+
{
456+
Assert(!capability.CanCreateSocket, capability.Describe());
457+
return;
458+
}
459+
460+
using Socket socket = new NativeSctpSocketFactory().CreateSocket();
461+
SctpConnectionOptions options = new(new SctpEndpoint("127.0.0.1", 2905), outboundStreams: 2, inboundStreams: 3);
462+
using NativeSctpSocketAdapter adapter = new(socket, options);
463+
464+
AssertEqual(SctpAssociationState.Closed, adapter.AssociationState, "native adapter initial state");
465+
adapter.MarkEstablished();
466+
SctpTransportHealth health = adapter.GetHealthSnapshot();
467+
468+
Assert(health.IsEstablished, "native adapter health should be established");
469+
AssertEqual((ushort)2, health.OutboundStreams, "native adapter outbound streams");
470+
AssertEqual((ushort)3, health.InboundStreams, "native adapter inbound streams");
471+
AssertEqual((uint)SctpPayloadProtocolIdentifiers.M3ua, health.DefaultPayloadProtocolIdentifier, "native adapter PPID");
472+
}
473+
450474
static void TcapBerElementEncodesShortAndLongLengths()
451475
{
452476
Span<byte> shortBuffer = stackalloc byte[8];
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
using System.Net.Sockets;
2+
3+
using sigtran.net.Core.Interfaces;
4+
5+
namespace sigtran.net.Layers.SCTP;
6+
7+
/// <summary>
8+
/// Wraps a native SCTP socket as the SDK packet transport contract.
9+
/// </summary>
10+
public sealed class NativeSctpSocketAdapter : ISctpSocket
11+
{
12+
private readonly Socket _socket;
13+
private readonly SctpConnectionOptions _options;
14+
private long _sentMessages;
15+
private long _receivedMessages;
16+
private bool _disposed;
17+
private SctpAssociationState _associationState;
18+
19+
/// <summary>Creates a native SCTP socket adapter.</summary>
20+
/// <param name="socket">The native SCTP socket.</param>
21+
/// <param name="options">The SCTP connection options.</param>
22+
/// <param name="associationState">The initial association state.</param>
23+
public NativeSctpSocketAdapter(
24+
Socket socket,
25+
SctpConnectionOptions options,
26+
SctpAssociationState associationState = SctpAssociationState.Closed)
27+
{
28+
_socket = socket ?? throw new ArgumentNullException(nameof(socket));
29+
_options = options ?? throw new ArgumentNullException(nameof(options));
30+
_associationState = associationState;
31+
}
32+
33+
/// <summary>The current association state.</summary>
34+
public SctpAssociationState AssociationState => _disposed ? SctpAssociationState.Closed : _associationState;
35+
36+
/// <summary>Marks the association as established.</summary>
37+
public void MarkEstablished()
38+
{
39+
ThrowIfDisposed();
40+
_associationState = SctpAssociationState.Established;
41+
}
42+
43+
/// <summary>Marks the association as failed.</summary>
44+
public void MarkFailed()
45+
{
46+
if (!_disposed)
47+
{
48+
_associationState = SctpAssociationState.Failed;
49+
}
50+
}
51+
52+
/// <inheritdoc />
53+
public async Task SendAsync(ReadOnlyMemory<byte> data, CancellationToken ct = default)
54+
{
55+
ThrowIfDisposed();
56+
if (data.IsEmpty)
57+
{
58+
throw new ArgumentException("SCTP payload must not be empty.", nameof(data));
59+
}
60+
61+
int sent = await _socket.SendAsync(data, SocketFlags.None, ct).ConfigureAwait(false);
62+
if (sent != data.Length)
63+
{
64+
throw new InvalidDataException($"Native SCTP send wrote {sent} bytes for a {data.Length} byte message.");
65+
}
66+
67+
Interlocked.Increment(ref _sentMessages);
68+
}
69+
70+
/// <inheritdoc />
71+
public async Task<int> ReceiveAsync(Memory<byte> buffer, CancellationToken ct = default)
72+
{
73+
ThrowIfDisposed();
74+
if (buffer.IsEmpty)
75+
{
76+
throw new ArgumentException("Receive buffer must not be empty.", nameof(buffer));
77+
}
78+
79+
int received = await _socket.ReceiveAsync(buffer, SocketFlags.None, ct).ConfigureAwait(false);
80+
if (received > 0)
81+
{
82+
Interlocked.Increment(ref _receivedMessages);
83+
}
84+
85+
return received;
86+
}
87+
88+
/// <summary>Captures a native SCTP transport health snapshot.</summary>
89+
/// <returns>The transport health snapshot.</returns>
90+
public SctpTransportHealth GetHealthSnapshot()
91+
{
92+
return new(
93+
AssociationState,
94+
_options.RemoteEndpoint,
95+
_options.LocalEndpoint,
96+
_options.OutboundStreams,
97+
_options.InboundStreams,
98+
_options.DefaultPayloadProtocolIdentifier,
99+
Interlocked.Read(ref _sentMessages),
100+
Interlocked.Read(ref _receivedMessages));
101+
}
102+
103+
/// <inheritdoc />
104+
public void Dispose()
105+
{
106+
if (_disposed)
107+
{
108+
return;
109+
}
110+
111+
_disposed = true;
112+
_associationState = SctpAssociationState.Closed;
113+
_socket.Dispose();
114+
}
115+
116+
private void ThrowIfDisposed()
117+
{
118+
if (_disposed)
119+
{
120+
throw new ObjectDisposedException(nameof(NativeSctpSocketAdapter));
121+
}
122+
}
123+
}

0 commit comments

Comments
 (0)