Skip to content

Commit afc7138

Browse files
committed
Add native SCTP listener
1 parent a5f2c65 commit afc7138

5 files changed

Lines changed: 176 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, endpoint connection planner, native socket adapter, and client connector 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, native socket adapter, client connector, and server listener added; full production readiness still requires native transport completion and lab verification |
4949

5050
## Requirements
5151

docs/PHASE8_NATIVE_SCTP.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,16 @@ NativeSctpSocketAdapter socket = await connector.ConnectAsync(options);
5858
```
5959

6060
The connector is Linux-native only through `NativeSctpSocketFactory`; unsupported platforms fail before attempting network I/O.
61+
62+
## Listener
63+
64+
`NativeSctpListener` provides the server-side bind/listen/accept path for native SCTP.
65+
66+
```csharp
67+
NativeSctpListenerOptions options = new(new SctpEndpoint("0.0.0.0", 2905));
68+
using NativeSctpListener listener = new();
69+
await listener.StartAsync(options);
70+
NativeSctpSocketAdapter association = await listener.AcceptAsync(options);
71+
```
72+
73+
The listener shares the same socket factory and unsupported-platform behavior as the connector. Real accept/send/receive verification belongs in Linux SCTP lab runs.

docs/SCTP_TRANSPORT.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,3 +132,5 @@ The probe does not mark the transport production-ready by itself. It is the firs
132132
`NativeSctpSocketAdapter` wraps an SCTP socket as `ISctpSocket` and reports `SctpTransportHealth` snapshots for native associations.
133133

134134
`NativeSctpConnector` performs the client-side bind/connect path and returns an established native adapter.
135+
136+
`NativeSctpListener` provides the server-side bind/listen/accept path for Linux native SCTP lab scenarios.

src/sigtran.net.Tests/Program.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
Run("Native SCTP connection planner resolves endpoints", NativeSctpConnectionPlannerResolvesEndpoints);
3636
Run("Native SCTP socket adapter reports lifecycle health", NativeSctpSocketAdapterReportsLifecycleHealth);
3737
Run("Native SCTP connector reports unsupported platform safely", NativeSctpConnectorReportsUnsupportedPlatformSafely);
38+
Run("Native SCTP listener validates options and unsupported platform", NativeSctpListenerValidatesOptionsAndUnsupportedPlatform);
3839
Run("TCAP BER element encodes short and long lengths", TcapBerElementEncodesShortAndLongLengths);
3940
Run("TCAP transaction identifiers use BER context tags", TcapTransactionIdentifiersUseBerContextTags);
4041
Run("TCAP BER Invoke component round-trips", TcapBerInvokeComponentRoundTrips);
@@ -492,6 +493,24 @@ static void NativeSctpConnectorReportsUnsupportedPlatformSafely()
492493
}
493494
}
494495

496+
static void NativeSctpListenerValidatesOptionsAndUnsupportedPlatform()
497+
{
498+
AssertThrows<ArgumentOutOfRangeException>(() => new NativeSctpListenerOptions(new SctpEndpoint("127.0.0.1", 2905), backlog: 0));
499+
AssertThrows<ArgumentOutOfRangeException>(() => new NativeSctpListenerOptions(new SctpEndpoint("127.0.0.1", 2905), outboundStreams: 0));
500+
501+
NativeSctpListenerOptions options = new(new SctpEndpoint("127.0.0.1", 2905), backlog: 1);
502+
AssertEqual((uint)SctpPayloadProtocolIdentifiers.M3ua, options.DefaultPayloadProtocolIdentifier, "native listener default PPID");
503+
504+
NativeSctpPlatformCapability capability = NativeSctpPlatform.Probe();
505+
if (!capability.CanCreateSocket)
506+
{
507+
using NativeSctpListener listener = new();
508+
NativeSctpUnavailableException exception = AssertThrows<NativeSctpUnavailableException>(() =>
509+
listener.StartAsync(options).GetAwaiter().GetResult());
510+
AssertEqual(capability.Status, exception.Capability.Status, "native listener unsupported status");
511+
}
512+
}
513+
495514
static void TcapBerElementEncodesShortAndLongLengths()
496515
{
497516
Span<byte> shortBuffer = stackalloc byte[8];
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
using System.Net;
2+
using System.Net.Sockets;
3+
4+
namespace sigtran.net.Layers.SCTP;
5+
6+
/// <summary>
7+
/// Options for a native SCTP listener.
8+
/// </summary>
9+
public sealed class NativeSctpListenerOptions
10+
{
11+
/// <summary>Creates native SCTP listener options.</summary>
12+
/// <param name="localEndpoint">The local endpoint to bind.</param>
13+
/// <param name="backlog">The listen backlog.</param>
14+
/// <param name="outboundStreams">The outbound stream count to report for accepted associations.</param>
15+
/// <param name="inboundStreams">The inbound stream count to report for accepted associations.</param>
16+
/// <param name="defaultPayloadProtocolIdentifier">The default Payload Protocol Identifier.</param>
17+
public NativeSctpListenerOptions(
18+
SctpEndpoint localEndpoint,
19+
int backlog = 100,
20+
ushort outboundStreams = 1,
21+
ushort inboundStreams = 1,
22+
uint defaultPayloadProtocolIdentifier = SctpPayloadProtocolIdentifiers.M3ua)
23+
{
24+
if (backlog <= 0)
25+
{
26+
throw new ArgumentOutOfRangeException(nameof(backlog), "Listen backlog must be positive.");
27+
}
28+
29+
if (outboundStreams == 0)
30+
{
31+
throw new ArgumentOutOfRangeException(nameof(outboundStreams), "Outbound stream count must be positive.");
32+
}
33+
34+
if (inboundStreams == 0)
35+
{
36+
throw new ArgumentOutOfRangeException(nameof(inboundStreams), "Inbound stream count must be positive.");
37+
}
38+
39+
LocalEndpoint = localEndpoint ?? throw new ArgumentNullException(nameof(localEndpoint));
40+
Backlog = backlog;
41+
OutboundStreams = outboundStreams;
42+
InboundStreams = inboundStreams;
43+
DefaultPayloadProtocolIdentifier = defaultPayloadProtocolIdentifier;
44+
}
45+
46+
/// <summary>The local endpoint to bind.</summary>
47+
public SctpEndpoint LocalEndpoint { get; }
48+
49+
/// <summary>The listen backlog.</summary>
50+
public int Backlog { get; }
51+
52+
/// <summary>The outbound stream count to report for accepted associations.</summary>
53+
public ushort OutboundStreams { get; }
54+
55+
/// <summary>The inbound stream count to report for accepted associations.</summary>
56+
public ushort InboundStreams { get; }
57+
58+
/// <summary>The default Payload Protocol Identifier.</summary>
59+
public uint DefaultPayloadProtocolIdentifier { get; }
60+
}
61+
62+
/// <summary>
63+
/// Native SCTP server-side listener.
64+
/// </summary>
65+
public sealed class NativeSctpListener : IDisposable
66+
{
67+
private readonly INativeSctpSocketFactory _socketFactory;
68+
private readonly NativeSctpEndpointResolver _resolver = new();
69+
private Socket? _listenSocket;
70+
71+
/// <summary>Creates a native SCTP listener.</summary>
72+
/// <param name="socketFactory">The socket factory.</param>
73+
public NativeSctpListener(INativeSctpSocketFactory? socketFactory = null)
74+
{
75+
_socketFactory = socketFactory ?? new NativeSctpSocketFactory();
76+
}
77+
78+
/// <summary>Starts listening on the configured local endpoint.</summary>
79+
/// <param name="options">The listener options.</param>
80+
/// <param name="ct">A cancellation token.</param>
81+
public async Task StartAsync(NativeSctpListenerOptions options, CancellationToken ct = default)
82+
{
83+
ArgumentNullException.ThrowIfNull(options);
84+
if (_listenSocket is not null)
85+
{
86+
throw new InvalidOperationException("Native SCTP listener has already started.");
87+
}
88+
89+
IPEndPoint local = await _resolver.ResolveAsync(options.LocalEndpoint, ct).ConfigureAwait(false);
90+
Socket socket = _socketFactory.CreateSocket();
91+
try
92+
{
93+
socket.Bind(local);
94+
socket.Listen(options.Backlog);
95+
_listenSocket = socket;
96+
}
97+
catch
98+
{
99+
socket.Dispose();
100+
throw;
101+
}
102+
}
103+
104+
/// <summary>Accepts one native SCTP association.</summary>
105+
/// <param name="options">The listener options used to describe accepted association defaults.</param>
106+
/// <param name="ct">A cancellation token.</param>
107+
/// <returns>The accepted native SCTP socket adapter.</returns>
108+
public async Task<NativeSctpSocketAdapter> AcceptAsync(NativeSctpListenerOptions options, CancellationToken ct = default)
109+
{
110+
ArgumentNullException.ThrowIfNull(options);
111+
Socket listenSocket = _listenSocket ?? throw new InvalidOperationException("Native SCTP listener has not started.");
112+
Socket accepted = await listenSocket.AcceptAsync(ct).ConfigureAwait(false);
113+
114+
SctpEndpoint remote = ToSctpEndpoint(accepted.RemoteEndPoint, "remote");
115+
SctpConnectionOptions connectionOptions = new(
116+
remote,
117+
options.LocalEndpoint,
118+
options.OutboundStreams,
119+
options.InboundStreams,
120+
options.DefaultPayloadProtocolIdentifier);
121+
122+
return new NativeSctpSocketAdapter(accepted, connectionOptions, SctpAssociationState.Established);
123+
}
124+
125+
/// <inheritdoc />
126+
public void Dispose()
127+
{
128+
_listenSocket?.Dispose();
129+
_listenSocket = null;
130+
}
131+
132+
private static SctpEndpoint ToSctpEndpoint(EndPoint? endpoint, string label)
133+
{
134+
if (endpoint is IPEndPoint ip)
135+
{
136+
return new SctpEndpoint(ip.Address.ToString(), ip.Port);
137+
}
138+
139+
throw new InvalidOperationException($"Accepted SCTP socket did not expose a valid {label} endpoint.");
140+
}
141+
}

0 commit comments

Comments
 (0)