Skip to content

Commit fc4661f

Browse files
authored
Apply the unconnected socket requirements to added paths (#63)
Description QUIC_PARAM_CONN_UNCONNECTED_UDP_SOCKET was only honoured on the connection's first path. QuicConnOpenNewPath, which opens the binding for every path added afterwards with QUIC_PARAM_CONN_ADD_PATH, still passed the path's remote address to QuicLibraryGetBinding: if (QuicAddrIsWildCard(&Path->Route.RemoteAddress) && QuicAddrGetPort(&Path->Route.RemoteAddress) == 0) { UdpConfig.RemoteAddress = &Connection->Paths[0].Route.RemoteAddress; } else { UdpConfig.RemoteAddress = &Path->Route.RemoteAddress; } So the added path got a connected socket and a binding of its own, even though the connection had asked for the opposite. A connection using the parameter to keep several destinations on one local port would silently get a second local port as soon as it added a path. Changes UdpConfig.RemoteAddress is left NULL when the parameter is set, which is what marks the binding unconnected and lets the lookup match on local port alone. The two requirements QuicConnStart already enforces are enforced here too, before the binding is built: A shared binding. An unconnected socket receives datagrams from any remote address, so packets are matched to a connection by connection ID alone. Returns QUIC_STATUS_INVALID_STATE. A specific local address. An unconnected socket has no source address of its own, and the path's first packet goes out before anything has been learned from the peer. Returns QUIC_STATUS_INVALID_PARAMETER. Both log through ConnError with the same wording QuicConnStart uses. The early goto Error is safe: the label only releases PathID, which is NULL at that point. Status code change The local address requirement now reports QUIC_STATUS_INVALID_PARAMETER rather than QUIC_STATUS_INVALID_STATE, in QuicConnStart as well. It describes an address the caller passed in, not a state the connection is in. docs/Settings.md and the existing test are updated to match. Testing Two tests, because ADD_PATH reaches this code by two different routes. QuicTestUnconnectedSocketAddPathBeforeStart — before the connection is started, QuicConnAddPath configures Paths[0] and returns without opening a binding (if (!Connection->State.Connected) goto Done;), so the address it sets has to satisfy QuicConnStart's requirement instead. Covers a wildcard local address being rejected, and a specific one connecting on the address that was named. QuicTestUnconnectedSocketAddPathAfterStart — after the handshake, QuicConnAddPath runs QuicConnOpenNewPath and the binding is opened there. Two listeners are used so the added path has a remote address a connected socket could not have reached. Covers: Requested Expected Enforced by same local + same remote QUIC_STATUS_ADDRESS_IN_USE QuicConnAddPath's duplicate check wildcard local + other remote QUIC_STATUS_INVALID_PARAMETER the new check in QuicConnOpenNewPath same local + other remote success, local port unchanged UdpConfig.RemoteAddress = NULL Both new checks were verified to be the ones failing the tests by disabling each in turn and watching the corresponding case flip to a failure. Results: *UnconnectedSocket*:*Path*:*Migration*:*ConnectionParam*:*Datagram* passes in full: 112 tests. *Basic* passes in full: 497 tests. No compiler warnings. Not covered: the !ShareBinding branch in QuicConnOpenNewPath. Setting QUIC_PARAM_CONN_UNCONNECTED_UDP_SOCKET requires a shared binding, and QUIC_PARAM_CONN_SHARE_UDP_BINDING cannot be changed once the connection is started, so the combination is unreachable through the API. It stays as a guard against a binding that was un-shared afterwards. Documentation docs/Settings.md records the corrected status code and notes that each additional path opened with QUIC_PARAM_CONN_ADD_PATH needs a specific local address for the same reason.
1 parent 0c7ea12 commit fc4661f

6 files changed

Lines changed: 255 additions & 4 deletions

File tree

docs/Settings.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ By default a client connection's UDP socket is connected to the server's address
229229

230230
The parameter requires `QUIC_PARAM_CONN_SHARE_UDP_BINDING` to also be set: an unconnected socket receives datagrams from any remote address, so incoming packets are matched to a connection by connection ID alone, and only a shared binding gives the connection a non-zero length source connection ID. Setting it without one fails with `QUIC_STATUS_INVALID_STATE`.
231231

232-
It also requires a specific local address, set with `QUIC_PARAM_CONN_LOCAL_ADDRESS`. A connected socket takes its source address from the kernel when it is connected; an unconnected one does not, and the connection's first packet goes out before anything has been learned from the peer, so the address to send from has to be named. The port may be left as 0 to let the stack choose one. Starting a connection with an unconnected socket and no local address, or a wildcard one, fails the connection with `QUIC_STATUS_INVALID_STATE`.
232+
It also requires a specific local address, set with `QUIC_PARAM_CONN_LOCAL_ADDRESS`. A connected socket takes its source address from the kernel when it is connected; an unconnected one does not, and the connection's first packet goes out before anything has been learned from the peer, so the address to send from has to be named. The port may be left as 0 to let the stack choose one. Starting a connection with an unconnected socket and no local address, or a wildcard one, fails the connection with `QUIC_STATUS_INVALID_PARAMETER`. The same applies to each additional path opened with `QUIC_PARAM_CONN_ADD_PATH`, whose local address must likewise be a specific one.
233233

234234
To place several connections on one local port, start the first connection, read its local address back with `QUIC_PARAM_CONN_LOCAL_ADDRESS`, and set that address on the subsequent connections along with the same two parameters.
235235

src/core/connection.c

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1666,7 +1666,7 @@ QuicConnStart(
16661666
// goes out before anything has been learned from the peer, so the
16671667
// application has to name the address to send from.
16681668
//
1669-
Status = QUIC_STATUS_INVALID_STATE;
1669+
Status = QUIC_STATUS_INVALID_PARAMETER;
16701670
QuicTraceEvent(
16711671
ConnError,
16721672
"[conn][%p] ERROR, %s.",
@@ -6844,12 +6844,51 @@ QuicConnOpenNewPath(
68446844

68456845
QUIC_BINDING* NewBinding = NULL;
68466846
CXPLAT_UDP_CONFIG UdpConfig = {0};
6847+
6848+
if (Connection->State.UnconnectedSocket) {
6849+
if (!Connection->State.ShareBinding) {
6850+
//
6851+
// Setting the parameter requires a shared binding, so this only
6852+
// catches a binding that was un-shared afterwards.
6853+
//
6854+
Status = QUIC_STATUS_INVALID_STATE;
6855+
QuicTraceEvent(
6856+
ConnError,
6857+
"[conn][%p] ERROR, %s.",
6858+
Connection,
6859+
"Unconnected socket requires a shared binding");
6860+
goto Error;
6861+
}
6862+
6863+
if (QuicAddrIsWildCard(&Path->Route.LocalAddress)) {
6864+
//
6865+
// A connected socket takes its source address from the kernel when
6866+
// it is connected. An unconnected one does not, and the path's first
6867+
// packet goes out before anything has been learned from the peer, so
6868+
// the caller has to name the address to send from.
6869+
//
6870+
Status = QUIC_STATUS_INVALID_PARAMETER;
6871+
QuicTraceEvent(
6872+
ConnError,
6873+
"[conn][%p] ERROR, %s.",
6874+
Connection,
6875+
"Unconnected socket requires a specific local address");
6876+
goto Error;
6877+
}
6878+
}
6879+
68476880
if (QuicAddrIsWildCard(&Path->Route.LocalAddress) && QuicAddrGetPort(&Path->Route.LocalAddress) == 0) {
68486881
UdpConfig.LocalAddress = NULL;
68496882
} else {
68506883
UdpConfig.LocalAddress = &Path->Route.LocalAddress;
68516884
}
6852-
if (QuicAddrIsWildCard(&Path->Route.RemoteAddress) && QuicAddrGetPort(&Path->Route.RemoteAddress) == 0) {
6885+
if (Connection->State.UnconnectedSocket) {
6886+
//
6887+
// Passing no remote address leaves the socket unconnected, which is what
6888+
// lets a single binding carry connections to different remote addresses.
6889+
//
6890+
UdpConfig.RemoteAddress = NULL;
6891+
} else if (QuicAddrIsWildCard(&Path->Route.RemoteAddress) && QuicAddrGetPort(&Path->Route.RemoteAddress) == 0) {
68536892
UdpConfig.RemoteAddress = &Connection->Paths[0].Route.RemoteAddress;
68546893
} else {
68556894
UdpConfig.RemoteAddress = &Path->Route.RemoteAddress;

src/test/MsQuicTests.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,8 @@ void QuicTestAddrFunctions(const FamilyArgs& Params);
158158
#ifdef QUIC_API_ENABLE_PREVIEW_FEATURES
159159
void QuicTestConnectUnconnectedSocket(const FamilyArgs& Params);
160160
void QuicTestUnconnectedSocketRequirements();
161+
void QuicTestUnconnectedSocketAddPathBeforeStart(const FamilyArgs& Params);
162+
void QuicTestUnconnectedSocketAddPathAfterStart(const FamilyArgs& Params);
161163
#endif
162164

163165
//

src/test/bin/quic_gtest.cpp

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1062,6 +1062,24 @@ TEST_P(WithFamilyArgs, ConnectUnconnectedSocket) {
10621062
}
10631063
}
10641064

1065+
TEST_P(WithFamilyArgs, UnconnectedSocketAddPathBeforeStart) {
1066+
TestLoggerT<ParamType> Logger("QuicTestUnconnectedSocketAddPathBeforeStart", GetParam());
1067+
if (TestingKernelMode) {
1068+
ASSERT_TRUE(InvokeKernelTest(FUNC(QuicTestUnconnectedSocketAddPathBeforeStart), GetParam()));
1069+
} else {
1070+
QuicTestUnconnectedSocketAddPathBeforeStart(GetParam());
1071+
}
1072+
}
1073+
1074+
TEST_P(WithFamilyArgs, UnconnectedSocketAddPathAfterStart) {
1075+
TestLoggerT<ParamType> Logger("QuicTestUnconnectedSocketAddPathAfterStart", GetParam());
1076+
if (TestingKernelMode) {
1077+
ASSERT_TRUE(InvokeKernelTest(FUNC(QuicTestUnconnectedSocketAddPathAfterStart), GetParam()));
1078+
} else {
1079+
QuicTestUnconnectedSocketAddPathAfterStart(GetParam());
1080+
}
1081+
}
1082+
10651083
TEST(Basic, UnconnectedSocketRequirements) {
10661084
TestLogger Logger("QuicTestUnconnectedSocketRequirements");
10671085
if (TestingKernelMode) {

src/test/bin/winkernel/control.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,8 @@ ExecuteTestRequest(
522522
#ifdef QUIC_API_ENABLE_PREVIEW_FEATURES
523523
RegisterTestFunction(QuicTestConnectUnconnectedSocket);
524524
RegisterTestFunction(QuicTestUnconnectedSocketRequirements);
525+
RegisterTestFunction(QuicTestUnconnectedSocketAddPathBeforeStart);
526+
RegisterTestFunction(QuicTestUnconnectedSocketAddPathAfterStart);
525527
#endif
526528
RegisterTestFunction(QuicTestConnect_Connect);
527529
#ifndef QUIC_DISABLE_RESUMPTION

src/test/lib/BasicTest.cpp

Lines changed: 191 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -492,7 +492,197 @@ void QuicTestUnconnectedSocketRequirements()
492492

493493
TEST_TRUE(Connection.HandshakeCompleteEvent.WaitTimeout(TestWaitTimeout));
494494
TEST_FALSE(Connection.HandshakeComplete);
495-
TEST_EQUAL(QUIC_STATUS_INVALID_STATE, Connection.TransportShutdownStatus);
495+
TEST_EQUAL(QUIC_STATUS_INVALID_PARAMETER, Connection.TransportShutdownStatus);
496+
}
497+
}
498+
499+
//
500+
// Sets the connection's local and remote address with QUIC_PARAM_CONN_ADD_PATH
501+
// instead of QUIC_PARAM_CONN_LOCAL_ADDRESS/REMOTE_ADDRESS. Before the
502+
// connection is started, ADD_PATH configures Paths[0] rather than opening an
503+
// additional path, so it has to satisfy the unconnected socket's requirement
504+
// for a specific local address the same way the individual parameters do.
505+
//
506+
void QuicTestUnconnectedSocketAddPathBeforeStart(const FamilyArgs& Params)
507+
{
508+
const int Family = Params.Family;
509+
const QUIC_ADDRESS_FAMILY QuicAddrFamily =
510+
(Family == 4) ? QUIC_ADDRESS_FAMILY_INET : QUIC_ADDRESS_FAMILY_INET6;
511+
512+
MsQuicRegistration Registration(true);
513+
TEST_QUIC_SUCCEEDED(Registration.GetInitStatus());
514+
515+
MsQuicConfiguration ServerConfiguration(Registration, "MsQuicTest", ServerSelfSignedCredConfig);
516+
TEST_QUIC_SUCCEEDED(ServerConfiguration.GetInitStatus());
517+
518+
MsQuicConfiguration ClientConfiguration(Registration, "MsQuicTest", MsQuicCredentialConfig());
519+
TEST_QUIC_SUCCEEDED(ClientConfiguration.GetInitStatus());
520+
521+
MsQuicAutoAcceptListener Listener(Registration, ServerConfiguration, MsQuicConnection::NoOpCallback);
522+
TEST_QUIC_SUCCEEDED(Listener.GetInitStatus());
523+
TEST_QUIC_SUCCEEDED(Listener.Start("MsQuicTest"));
524+
QuicAddr ServerAddr;
525+
TEST_QUIC_SUCCEEDED(Listener.GetLocalAddr(ServerAddr));
526+
527+
QuicAddr LocalAddr(QuicAddrFamily, true);
528+
QuicAddr RemoteAddr(QuicAddrFamily, true);
529+
if (UseDuoNic) {
530+
QuicAddrSetToDuoNic(&LocalAddr.SockAddr);
531+
QuicAddrSetToDuoNic(&RemoteAddr.SockAddr);
532+
}
533+
RemoteAddr.SetPort(ServerAddr.GetPort());
534+
535+
QUIC_PATH_PARAM PathParam = { &LocalAddr.SockAddr, &RemoteAddr.SockAddr };
536+
537+
//
538+
// A wildcard local address is rejected, since an unconnected socket has no
539+
// source address of its own to send the connection's first packet from.
540+
//
541+
{
542+
QuicAddr WildcardLocalAddr(QuicAddrFamily);
543+
QUIC_PATH_PARAM WildcardParam = { &WildcardLocalAddr.SockAddr, &RemoteAddr.SockAddr };
544+
545+
MsQuicConnection Connection(Registration);
546+
TEST_QUIC_SUCCEEDED(Connection.GetInitStatus());
547+
TEST_QUIC_SUCCEEDED(Connection.SetShareUdpBinding());
548+
TEST_QUIC_SUCCEEDED(Connection.SetUnconnectedUdpSocket());
549+
TEST_QUIC_SUCCEEDED(
550+
Connection.SetParam(QUIC_PARAM_CONN_ADD_PATH, sizeof(WildcardParam), &WildcardParam));
551+
TEST_QUIC_SUCCEEDED(
552+
Connection.Start(ClientConfiguration, QuicAddrFamily, nullptr, ServerAddr.GetPort()));
553+
554+
TEST_TRUE(Connection.HandshakeCompleteEvent.WaitTimeout(TestWaitTimeout));
555+
TEST_FALSE(Connection.HandshakeComplete);
556+
TEST_EQUAL(QUIC_STATUS_INVALID_PARAMETER, Connection.TransportShutdownStatus);
557+
}
558+
559+
//
560+
// With a specific one the connection is established, on the address that
561+
// was named.
562+
//
563+
{
564+
MsQuicConnection Connection(Registration);
565+
TEST_QUIC_SUCCEEDED(Connection.GetInitStatus());
566+
TEST_QUIC_SUCCEEDED(Connection.SetShareUdpBinding());
567+
TEST_QUIC_SUCCEEDED(Connection.SetUnconnectedUdpSocket());
568+
TEST_QUIC_SUCCEEDED(
569+
Connection.SetParam(QUIC_PARAM_CONN_ADD_PATH, sizeof(PathParam), &PathParam));
570+
TEST_QUIC_SUCCEEDED(
571+
Connection.Start(ClientConfiguration, QuicAddrFamily, nullptr, ServerAddr.GetPort()));
572+
573+
TEST_TRUE(Connection.HandshakeCompleteEvent.WaitTimeout(TestWaitTimeout));
574+
TEST_TRUE(Connection.HandshakeComplete);
575+
576+
QuicAddr ActualLocalAddr;
577+
TEST_QUIC_SUCCEEDED(Connection.GetLocalAddr(ActualLocalAddr));
578+
TEST_FALSE(QuicAddrIsWildCard(&ActualLocalAddr.SockAddr));
579+
TEST_NOT_EQUAL(0, ActualLocalAddr.GetPort());
580+
581+
QuicAddr ActualRemoteAddr;
582+
TEST_QUIC_SUCCEEDED(Connection.GetRemoteAddr(ActualRemoteAddr));
583+
TEST_EQUAL(ServerAddr.GetPort(), ActualRemoteAddr.GetPort());
584+
}
585+
}
586+
587+
//
588+
// Adds a path with QUIC_PARAM_CONN_ADD_PATH once the connection is established.
589+
// Unlike the pre-start case, this opens a binding of its own, so it is the path
590+
// that exercises the unconnected socket handling in QuicConnOpenNewPath.
591+
//
592+
void QuicTestUnconnectedSocketAddPathAfterStart(const FamilyArgs& Params)
593+
{
594+
const int Family = Params.Family;
595+
const QUIC_ADDRESS_FAMILY QuicAddrFamily =
596+
(Family == 4) ? QUIC_ADDRESS_FAMILY_INET : QUIC_ADDRESS_FAMILY_INET6;
597+
598+
MsQuicRegistration Registration(true);
599+
TEST_QUIC_SUCCEEDED(Registration.GetInitStatus());
600+
601+
MsQuicConfiguration ServerConfiguration(Registration, "MsQuicTest", ServerSelfSignedCredConfig);
602+
TEST_QUIC_SUCCEEDED(ServerConfiguration.GetInitStatus());
603+
604+
MsQuicConfiguration ClientConfiguration(Registration, "MsQuicTest", MsQuicCredentialConfig());
605+
TEST_QUIC_SUCCEEDED(ClientConfiguration.GetInitStatus());
606+
607+
//
608+
// A second server, so the added path has a remote address the connected
609+
// socket of the first path could not have reached.
610+
//
611+
MsQuicAutoAcceptListener Listener1(Registration, ServerConfiguration, MsQuicConnection::NoOpCallback);
612+
TEST_QUIC_SUCCEEDED(Listener1.GetInitStatus());
613+
TEST_QUIC_SUCCEEDED(Listener1.Start("MsQuicTest"));
614+
QuicAddr Server1Addr;
615+
TEST_QUIC_SUCCEEDED(Listener1.GetLocalAddr(Server1Addr));
616+
617+
MsQuicAutoAcceptListener Listener2(Registration, ServerConfiguration, MsQuicConnection::NoOpCallback);
618+
TEST_QUIC_SUCCEEDED(Listener2.GetInitStatus());
619+
TEST_QUIC_SUCCEEDED(Listener2.Start("MsQuicTest"));
620+
QuicAddr Server2Addr;
621+
TEST_QUIC_SUCCEEDED(Listener2.GetLocalAddr(Server2Addr));
622+
623+
TEST_NOT_EQUAL(Server1Addr.GetPort(), Server2Addr.GetPort());
624+
625+
QuicAddr LocalAddr(QuicAddrFamily, true);
626+
if (UseDuoNic) {
627+
QuicAddrSetToDuoNic(&LocalAddr.SockAddr);
628+
}
629+
630+
MsQuicConnection Connection(Registration);
631+
TEST_QUIC_SUCCEEDED(Connection.GetInitStatus());
632+
TEST_QUIC_SUCCEEDED(Connection.SetShareUdpBinding());
633+
TEST_QUIC_SUCCEEDED(Connection.SetUnconnectedUdpSocket());
634+
TEST_QUIC_SUCCEEDED(Connection.SetLocalAddr(LocalAddr));
635+
TEST_QUIC_SUCCEEDED(
636+
Connection.Start(
637+
ClientConfiguration,
638+
QuicAddrFamily,
639+
QUIC_TEST_LOOPBACK_FOR_AF(QuicAddrFamily),
640+
Server1Addr.GetPort()));
641+
TEST_TRUE(Connection.HandshakeCompleteEvent.WaitTimeout(TestWaitTimeout));
642+
TEST_TRUE(Connection.HandshakeComplete);
643+
644+
QuicAddr ClientAddr;
645+
TEST_QUIC_SUCCEEDED(Connection.GetLocalAddr(ClientAddr));
646+
QuicAddr Peer1Addr;
647+
TEST_QUIC_SUCCEEDED(Connection.GetRemoteAddr(Peer1Addr));
648+
649+
QuicAddr Peer2Addr = Peer1Addr;
650+
Peer2Addr.SetPort(Server2Addr.GetPort());
651+
652+
//
653+
// The address pair the connection already runs on is not a new path.
654+
//
655+
{
656+
QUIC_PATH_PARAM PathParam = { &ClientAddr.SockAddr, &Peer1Addr.SockAddr };
657+
TEST_QUIC_STATUS(
658+
QUIC_STATUS_ADDRESS_IN_USE,
659+
Connection.SetParam(QUIC_PARAM_CONN_ADD_PATH, sizeof(PathParam), &PathParam));
660+
}
661+
662+
//
663+
// A wildcard local address is rejected: the added path's socket is left
664+
// unconnected, so it has no source address of its own to send from.
665+
//
666+
{
667+
QuicAddr WildcardLocalAddr(QuicAddrFamily);
668+
QUIC_PATH_PARAM PathParam = { &WildcardLocalAddr.SockAddr, &Peer2Addr.SockAddr };
669+
TEST_QUIC_STATUS(
670+
QUIC_STATUS_INVALID_PARAMETER,
671+
Connection.SetParam(QUIC_PARAM_CONN_ADD_PATH, sizeof(PathParam), &PathParam));
672+
}
673+
674+
//
675+
// The same local address towards the second server is a new path, and it
676+
// shares the binding the connection is already using.
677+
//
678+
{
679+
QUIC_PATH_PARAM PathParam = { &ClientAddr.SockAddr, &Peer2Addr.SockAddr };
680+
TEST_QUIC_SUCCEEDED(
681+
Connection.SetParam(QUIC_PARAM_CONN_ADD_PATH, sizeof(PathParam), &PathParam));
682+
683+
QuicAddr StillClientAddr;
684+
TEST_QUIC_SUCCEEDED(Connection.GetLocalAddr(StillClientAddr));
685+
TEST_EQUAL(ClientAddr.GetPort(), StillClientAddr.GetPort());
496686
}
497687
}
498688

0 commit comments

Comments
 (0)