@@ -1218,3 +1218,165 @@ async def test_reservation_fails_with_invalid_record_transfer():
12181218 logger .info ("Invalid SPR was correctly rejected" )
12191219
12201220 logger .info ("Invalid SPR correctly rejected, peerstore protected" )
1221+
1222+
1223+ @pytest .mark .trio
1224+ async def test_circuit_v2_connect_fails_without_reservation ():
1225+ """Test that relay rejects CONNECT requests for unreserved destinations."""
1226+ async with HostFactory .create_batch_and_listen (3 ) as hosts :
1227+ relay_host , source_host , dest_host = hosts
1228+ logger .info (
1229+ "Created hosts for test_circuit_v2_connect_fails_without_reservation"
1230+ )
1231+
1232+ # Setup relay
1233+ limits = RelayLimits (
1234+ duration = DEFAULT_RELAY_LIMITS .duration ,
1235+ data = DEFAULT_RELAY_LIMITS .data ,
1236+ max_circuit_conns = DEFAULT_RELAY_LIMITS .max_circuit_conns ,
1237+ max_reservations = DEFAULT_RELAY_LIMITS .max_reservations ,
1238+ )
1239+ relay_protocol = CircuitV2Protocol (relay_host , limits , allow_hop = True )
1240+
1241+ async with background_trio_service (relay_protocol ):
1242+ await relay_protocol .event_started .wait ()
1243+
1244+ # Connect source to relay
1245+ await connect (source_host , relay_host )
1246+ await trio .sleep (SLEEP_TIME )
1247+
1248+ # Source sends CONNECT request for dest (who has NO reservation)
1249+ stream = None
1250+ try :
1251+ with trio .fail_after (STREAM_TIMEOUT ):
1252+ stream = await source_host .new_stream (
1253+ relay_host .get_id (), [PROTOCOL_ID ]
1254+ )
1255+
1256+ connect_msg = proto .HopMessage (
1257+ type = proto .HopMessage .CONNECT ,
1258+ peer = dest_host .get_id ().to_bytes (),
1259+ )
1260+
1261+ await stream .write (connect_msg .SerializeToString ())
1262+ logger .info (
1263+ "Sent CONNECT request for destination without reservation"
1264+ )
1265+
1266+ # Read response
1267+ response_bytes = await stream .read (MAX_READ_LEN )
1268+ assert response_bytes , "No response received"
1269+
1270+ response = proto .HopMessage ()
1271+ response .ParseFromString (response_bytes )
1272+
1273+ # Verify response status is NO_RESERVATION (204)
1274+ assert response .type == proto .HopMessage .STATUS
1275+ assert response .status .code == proto .Status .NO_RESERVATION , (
1276+ "Expected status NO_RESERVATION(204), "
1277+ f"got { response .status .code } "
1278+ )
1279+
1280+ # Verify stream is reset (or EOF)
1281+ try :
1282+ next_data = await stream .read (MAX_READ_LEN )
1283+ assert not next_data , (
1284+ "Stream should be closed/reset after error status"
1285+ )
1286+ except (StreamEOF , StreamReset , StreamError ):
1287+ pass
1288+
1289+ finally :
1290+ if stream :
1291+ await close_stream (stream )
1292+
1293+
1294+ @pytest .mark .trio
1295+ async def test_circuit_v2_connect_fails_when_source_limit_exceeded ():
1296+ """Test that relay rejects CONNECT when source exceeds connection limit."""
1297+ async with HostFactory .create_batch_and_listen (3 ) as hosts :
1298+ relay_host , source_host , dest_host = hosts
1299+ logger .info (
1300+ "Created hosts for test_circuit_v2_connect_fails_when_source_limit_exceeded"
1301+ )
1302+
1303+ limits = RelayLimits (
1304+ duration = DEFAULT_RELAY_LIMITS .duration ,
1305+ data = DEFAULT_RELAY_LIMITS .data ,
1306+ max_circuit_conns = 1 ,
1307+ max_reservations = DEFAULT_RELAY_LIMITS .max_reservations ,
1308+ )
1309+ relay_protocol = CircuitV2Protocol (relay_host , limits , allow_hop = True )
1310+
1311+ async with background_trio_service (relay_protocol ):
1312+ await relay_protocol .event_started .wait ()
1313+
1314+ async def send_reserve (host ) -> None :
1315+ envelope_bytes , _ = env_to_send_in_RPC (host )
1316+ stream = await host .new_stream (relay_host .get_id (), [PROTOCOL_ID ])
1317+ try :
1318+ reserve_msg = proto .HopMessage (
1319+ type = proto .HopMessage .RESERVE ,
1320+ peer = host .get_id ().to_bytes (),
1321+ senderRecord = envelope_bytes ,
1322+ )
1323+ await stream .write (reserve_msg .SerializeToString ())
1324+ await stream .read (MAX_READ_LEN )
1325+ finally :
1326+ await close_stream (stream )
1327+
1328+ await connect (dest_host , relay_host )
1329+ await connect (source_host , relay_host )
1330+ await trio .sleep (SLEEP_TIME )
1331+
1332+ await send_reserve (dest_host )
1333+ await send_reserve (source_host )
1334+ await trio .sleep (SLEEP_TIME )
1335+
1336+ source_reservation = relay_protocol .resource_manager .get_reservation (
1337+ source_host .get_id ()
1338+ )
1339+ assert source_reservation is not None
1340+ source_reservation .active_connections = 1
1341+
1342+ stream = None
1343+ try :
1344+ with trio .fail_after (STREAM_TIMEOUT ):
1345+ stream = await source_host .new_stream (
1346+ relay_host .get_id (), [PROTOCOL_ID ]
1347+ )
1348+
1349+ connect_msg = proto .HopMessage (
1350+ type = proto .HopMessage .CONNECT ,
1351+ peer = dest_host .get_id ().to_bytes (),
1352+ )
1353+ await stream .write (connect_msg .SerializeToString ())
1354+ logger .info (
1355+ "Sent CONNECT request with source connection limit exceeded"
1356+ )
1357+
1358+ response_bytes = await stream .read (MAX_READ_LEN )
1359+ assert response_bytes , "No response received"
1360+
1361+ response = proto .HopMessage ()
1362+ response .ParseFromString (response_bytes )
1363+
1364+ assert response .type == proto .HopMessage .STATUS
1365+ assert (
1366+ response .status .code == proto .Status .RESOURCE_LIMIT_EXCEEDED
1367+ ), (
1368+ "Expected status RESOURCE_LIMIT_EXCEEDED(101), "
1369+ f"got { response .status .code } "
1370+ )
1371+
1372+ try :
1373+ next_data = await stream .read (MAX_READ_LEN )
1374+ assert not next_data , (
1375+ "Stream should be closed/reset after error status"
1376+ )
1377+ except (StreamEOF , StreamReset , StreamError ):
1378+ pass
1379+
1380+ finally :
1381+ if stream :
1382+ await close_stream (stream )
0 commit comments