Skip to content

Commit cbb67f0

Browse files
committed
Merge PR libp2p#763: Add QUIC v1 transport support
2 parents 8bf261c + 9cb67cf commit cbb67f0

26 files changed

+8253
-23
lines changed

docs/libp2p.transport.quic.rst

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
libp2p.transport.quic package
2+
=============================
3+
4+
Submodules
5+
----------
6+
7+
libp2p.transport.quic.config module
8+
-----------------------------------
9+
10+
.. automodule:: libp2p.transport.quic.config
11+
:members:
12+
:undoc-members:
13+
:show-inheritance:
14+
15+
libp2p.transport.quic.connection module
16+
---------------------------------------
17+
18+
.. automodule:: libp2p.transport.quic.connection
19+
:members:
20+
:undoc-members:
21+
:show-inheritance:
22+
23+
libp2p.transport.quic.exceptions module
24+
---------------------------------------
25+
26+
.. automodule:: libp2p.transport.quic.exceptions
27+
:members:
28+
:undoc-members:
29+
:show-inheritance:
30+
31+
libp2p.transport.quic.listener module
32+
-------------------------------------
33+
34+
.. automodule:: libp2p.transport.quic.listener
35+
:members:
36+
:undoc-members:
37+
:show-inheritance:
38+
39+
libp2p.transport.quic.security module
40+
-------------------------------------
41+
42+
.. automodule:: libp2p.transport.quic.security
43+
:members:
44+
:undoc-members:
45+
:show-inheritance:
46+
47+
libp2p.transport.quic.stream module
48+
-----------------------------------
49+
50+
.. automodule:: libp2p.transport.quic.stream
51+
:members:
52+
:undoc-members:
53+
:show-inheritance:
54+
55+
libp2p.transport.quic.transport module
56+
--------------------------------------
57+
58+
.. automodule:: libp2p.transport.quic.transport
59+
:members:
60+
:undoc-members:
61+
:show-inheritance:
62+
63+
libp2p.transport.quic.utils module
64+
----------------------------------
65+
66+
.. automodule:: libp2p.transport.quic.utils
67+
:members:
68+
:undoc-members:
69+
:show-inheritance:
70+
71+
Module contents
72+
---------------
73+
74+
.. automodule:: libp2p.transport.quic
75+
:members:
76+
:undoc-members:
77+
:show-inheritance:

docs/libp2p.transport.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ Subpackages
99

1010
libp2p.transport.tcp
1111

12+
.. toctree::
13+
:maxdepth: 4
14+
15+
libp2p.transport.quic
16+
1217
Submodules
1318
----------
1419

examples/echo/echo_quic.py

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
#!/usr/bin/env python3
2+
"""
3+
QUIC Echo Example - Fixed version with proper client/server separation
4+
5+
This program demonstrates a simple echo protocol using QUIC transport where a peer
6+
listens for connections and copies back any input received on a stream.
7+
8+
Fixed to properly separate client and server modes - clients don't start listeners.
9+
"""
10+
11+
import argparse
12+
import logging
13+
14+
from multiaddr import Multiaddr
15+
import trio
16+
17+
from libp2p import new_host
18+
from libp2p.crypto.secp256k1 import create_new_key_pair
19+
from libp2p.custom_types import TProtocol
20+
from libp2p.network.stream.net_stream import INetStream
21+
from libp2p.peer.peerinfo import info_from_p2p_addr
22+
from libp2p.transport.quic.config import QUICTransportConfig
23+
24+
PROTOCOL_ID = TProtocol("/echo/1.0.0")
25+
26+
27+
async def _echo_stream_handler(stream: INetStream) -> None:
28+
try:
29+
msg = await stream.read()
30+
await stream.write(msg)
31+
await stream.close()
32+
except Exception as e:
33+
print(f"Echo handler error: {e}")
34+
try:
35+
await stream.close()
36+
except: # noqa: E722
37+
pass
38+
39+
40+
async def run_server(port: int, seed: int | None = None) -> None:
41+
"""Run echo server with QUIC transport."""
42+
listen_addr = Multiaddr(f"/ip4/0.0.0.0/udp/{port}/quic")
43+
44+
if seed:
45+
import random
46+
47+
random.seed(seed)
48+
secret_number = random.getrandbits(32 * 8)
49+
secret = secret_number.to_bytes(length=32, byteorder="big")
50+
else:
51+
import secrets
52+
53+
secret = secrets.token_bytes(32)
54+
55+
# QUIC transport configuration
56+
quic_config = QUICTransportConfig(
57+
idle_timeout=30.0,
58+
max_concurrent_streams=100,
59+
connection_timeout=10.0,
60+
enable_draft29=False,
61+
)
62+
63+
# Create host with QUIC transport
64+
host = new_host(
65+
key_pair=create_new_key_pair(secret),
66+
transport_opt={"quic_config": quic_config},
67+
)
68+
69+
# Server mode: start listener
70+
async with host.run(listen_addrs=[listen_addr]):
71+
try:
72+
print(f"I am {host.get_id().to_string()}")
73+
host.set_stream_handler(PROTOCOL_ID, _echo_stream_handler)
74+
75+
print(
76+
"Run this from the same folder in another console:\n\n"
77+
f"python3 ./examples/echo/echo_quic.py "
78+
f"-d {host.get_addrs()[0]}\n"
79+
)
80+
print("Waiting for incoming QUIC connections...")
81+
await trio.sleep_forever()
82+
except KeyboardInterrupt:
83+
print("Closing server gracefully...")
84+
await host.close()
85+
return
86+
87+
88+
async def run_client(destination: str, seed: int | None = None) -> None:
89+
"""Run echo client with QUIC transport."""
90+
if seed:
91+
import random
92+
93+
random.seed(seed)
94+
secret_number = random.getrandbits(32 * 8)
95+
secret = secret_number.to_bytes(length=32, byteorder="big")
96+
else:
97+
import secrets
98+
99+
secret = secrets.token_bytes(32)
100+
101+
# QUIC transport configuration
102+
quic_config = QUICTransportConfig(
103+
idle_timeout=30.0,
104+
max_concurrent_streams=100,
105+
connection_timeout=10.0,
106+
enable_draft29=False,
107+
)
108+
109+
# Create host with QUIC transport
110+
host = new_host(
111+
key_pair=create_new_key_pair(secret),
112+
transport_opt={"quic_config": quic_config},
113+
)
114+
115+
# Client mode: NO listener, just connect
116+
async with host.run(listen_addrs=[]): # Empty listen_addrs for client
117+
print(f"I am {host.get_id().to_string()}")
118+
119+
maddr = Multiaddr(destination)
120+
info = info_from_p2p_addr(maddr)
121+
122+
# Connect to server
123+
print("STARTING CLIENT CONNECTION PROCESS")
124+
await host.connect(info)
125+
print("CLIENT CONNECTED TO SERVER")
126+
127+
# Start a stream with the destination
128+
stream = await host.new_stream(info.peer_id, [PROTOCOL_ID])
129+
130+
msg = b"hi, there!\n"
131+
132+
await stream.write(msg)
133+
response = await stream.read()
134+
135+
print(f"Sent: {msg.decode('utf-8')}")
136+
print(f"Got: {response.decode('utf-8')}")
137+
await stream.close()
138+
await host.disconnect(info.peer_id)
139+
140+
141+
async def run(port: int, destination: str, seed: int | None = None) -> None:
142+
"""
143+
Run echo server or client with QUIC transport.
144+
145+
Fixed version that properly separates client and server modes.
146+
"""
147+
if not destination: # Server mode
148+
await run_server(port, seed)
149+
else: # Client mode
150+
await run_client(destination, seed)
151+
152+
153+
def main() -> None:
154+
"""Main function - help text updated for QUIC."""
155+
description = """
156+
This program demonstrates a simple echo protocol using QUIC
157+
transport where a peer listens for connections and copies back
158+
any input received on a stream.
159+
160+
QUIC provides built-in TLS security and stream multiplexing over UDP.
161+
162+
To use it, first run 'python ./echo_quic_fixed.py -p <PORT>', where <PORT> is
163+
the UDP port number. Then, run another host with ,
164+
'python ./echo_quic_fixed.py -d <DESTINATION>'
165+
where <DESTINATION> is the QUIC multiaddress of the previous listener host.
166+
"""
167+
168+
example_maddr = "/ip4/127.0.0.1/udp/8000/quic/p2p/QmQn4SwGkDZKkUEpBRBv"
169+
170+
parser = argparse.ArgumentParser(description=description)
171+
parser.add_argument("-p", "--port", default=0, type=int, help="UDP port number")
172+
parser.add_argument(
173+
"-d",
174+
"--destination",
175+
type=str,
176+
help=f"destination multiaddr string, e.g. {example_maddr}",
177+
)
178+
parser.add_argument(
179+
"-s",
180+
"--seed",
181+
type=int,
182+
help="provide a seed to the random number generator",
183+
)
184+
args = parser.parse_args()
185+
186+
try:
187+
trio.run(run, args.port, args.destination, args.seed)
188+
except KeyboardInterrupt:
189+
pass
190+
191+
192+
if __name__ == "__main__":
193+
logging.basicConfig(level=logging.DEBUG)
194+
logging.getLogger("aioquic").setLevel(logging.DEBUG)
195+
main()

libp2p/__init__.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
1+
from libp2p.transport.quic.utils import is_quic_multiaddr
2+
from typing import Any
3+
from libp2p.transport.quic.transport import QUICTransport
4+
from libp2p.transport.quic.config import QUICTransportConfig
15
from collections.abc import (
26
Mapping,
37
Sequence,
48
)
59
from importlib.metadata import version as __version
610
from typing import (
711
Literal,
8-
Optional,
9-
Type,
10-
cast,
1112
)
1213

1314
import multiaddr
1415

1516
from libp2p.abc import (
1617
IHost,
17-
IMuxedConn,
1818
INetworkService,
1919
IPeerRouting,
2020
IPeerStore,
@@ -163,6 +163,7 @@ def new_swarm(
163163
peerstore_opt: IPeerStore | None = None,
164164
muxer_preference: Literal["YAMUX", "MPLEX"] | None = None,
165165
listen_addrs: Sequence[multiaddr.Multiaddr] | None = None,
166+
transport_opt: dict[Any, Any] | None = None,
166167
) -> INetworkService:
167168
"""
168169
Create a swarm instance based on the parameters.
@@ -173,6 +174,7 @@ def new_swarm(
173174
:param peerstore_opt: optional peerstore
174175
:param muxer_preference: optional explicit muxer preference
175176
:param listen_addrs: optional list of multiaddrs to listen on
177+
:param transport_opt: options for transport
176178
:return: return a default swarm instance
177179
178180
Note: Yamux (/yamux/1.0.0) is the preferred stream multiplexer
@@ -185,14 +187,24 @@ def new_swarm(
185187

186188
id_opt = generate_peer_id_from(key_pair)
187189

190+
transport: TCP | QUICTransport
191+
188192
if listen_addrs is None:
189-
transport = TCP()
193+
transport_opt = transport_opt or {}
194+
quic_config: QUICTransportConfig | None = transport_opt.get('quic_config')
195+
196+
if quic_config:
197+
transport = QUICTransport(key_pair.private_key, quic_config)
198+
else:
199+
transport = TCP()
190200
else:
191201
addr = listen_addrs[0]
192202
if addr.__contains__("tcp"):
193203
transport = TCP()
194204
elif addr.__contains__("quic"):
195-
raise ValueError("QUIC not yet supported")
205+
transport_opt = transport_opt or {}
206+
quic_config = transport_opt.get('quic_config', QUICTransportConfig())
207+
transport = QUICTransport(key_pair.private_key, quic_config)
196208
else:
197209
raise ValueError(f"Unknown transport in listen_addrs: {listen_addrs}")
198210

@@ -252,6 +264,7 @@ def new_host(
252264
listen_addrs: Sequence[multiaddr.Multiaddr] | None = None,
253265
enable_mDNS: bool = False,
254266
negotiate_timeout: int = DEFAULT_NEGOTIATE_TIMEOUT,
267+
transport_opt: dict[Any, Any] | None = None,
255268
) -> IHost:
256269
"""
257270
Create a new libp2p host based on the given parameters.
@@ -264,6 +277,7 @@ def new_host(
264277
:param muxer_preference: optional explicit muxer preference
265278
:param listen_addrs: optional list of multiaddrs to listen on
266279
:param enable_mDNS: whether to enable mDNS discovery
280+
:param transport_opt: optional dictionary of properties of transport
267281
:return: return a host instance
268282
"""
269283
swarm = new_swarm(
@@ -273,6 +287,7 @@ def new_host(
273287
peerstore_opt=peerstore_opt,
274288
muxer_preference=muxer_preference,
275289
listen_addrs=listen_addrs,
290+
transport_opt=transport_opt
276291
)
277292

278293
if disc_opt is not None:

0 commit comments

Comments
 (0)