Skip to content

Commit 9a22a8f

Browse files
authored
fix: listen on multiple wildcards (#32)
* fix: listen on multiple wildcards Switches UDP port creation to use the socket2 crate - this lets us configure ipv6 ports to only listen on ipv6 addresses and not be dual stack (default on linux). Fixes #28 * chore: remove unused var
1 parent b730d02 commit 9a22a8f

File tree

7 files changed

+668
-554
lines changed

7 files changed

+668
-554
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ npm-debug.log*
99
yarn-debug.log*
1010
yarn-error.log*
1111
lerna-debug.log*
12+
package-lock.json
1213

1314
# Diagnostic reports (https://nodejs.org/api/report.html)
1415
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
"uint8arraylist": "^2.4.8"
7070
},
7171
"devDependencies": {
72+
"@chainsafe/is-ip": "^2.1.0",
7273
"@libp2p/interface-compliance-tests": "^6.3.2",
7374
"@libp2p/logger": "^5.0.1",
7475
"@napi-rs/cli": "^3.0.0-alpha.70",

rust/lib.rs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use std::{
77
use napi::bindgen_prelude::*;
88
use napi_derive::napi;
99
use tokio::sync::Mutex;
10+
use socket2::{Socket, Domain, Type};
1011

1112
mod config;
1213
mod socket;
@@ -30,10 +31,20 @@ impl Server {
3031
pub fn new(config: &config::QuinnConfig, ip: String, port: u16) -> Result<Self> {
3132
let ip_addr = ip.parse::<IpAddr>().map_err(to_err)?;
3233
let socket_addr = SocketAddr::new(ip_addr, port);
33-
let socket = socket::create_socket(config.socket_config, socket_addr)?;
34+
let socket;
35+
36+
// Create a TCP listener bound to two addresses.
37+
if ip_addr.is_ipv6() {
38+
socket = Socket::new(Domain::IPV6, Type::DGRAM, None)?;
39+
socket.set_only_v6(true)?;
40+
} else {
41+
socket = Socket::new(Domain::IPV4, Type::DGRAM, None)?;
42+
}
43+
44+
socket.bind(&socket_addr.into())?;
3445

35-
let socket = block_on(async move{
36-
socket::UdpSocket::wrap_udp_socket(socket)
46+
let socket: Arc<socket::UdpSocket> = block_on(async move{
47+
socket::UdpSocket::wrap_udp_socket(socket.into())
3748
})?;
3849
let endpoint = block_on(async {
3950
quinn::Endpoint::new_with_abstract_socket(

rust/socket.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,22 @@ use std::{
2323
use quinn::udp;
2424
use quinn::{AsyncUdpSocket, UdpPoller};
2525
use tokio::{io::Interest, sync::RwLock};
26-
use socket2::Socket;
26+
use socket2::{Socket, Domain, Type};
2727

2828
use crate::config;
2929

3030
pub fn create_socket(config: config::SocketConfig, socket_addr: std::net::SocketAddr) -> napi::Result<std::net::UdpSocket> {
31-
let socket = std::net::UdpSocket::bind(socket_addr)?;
31+
let socket;
32+
33+
// Create a TCP listener bound to two addresses.
34+
if socket_addr.is_ipv6() {
35+
socket = Socket::new(Domain::IPV6, Type::DGRAM, None)?;
36+
socket.set_only_v6(true)?;
37+
} else {
38+
socket = Socket::new(Domain::IPV4, Type::DGRAM, None)?;
39+
}
40+
41+
socket.bind(&socket_addr.into())?;
3242

3343
let socket = Socket::from(socket);
3444
socket.set_send_buffer_size(config.send_buffer_size as usize).unwrap();

src/listener.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { setMaxListeners, TypedEventEmitter } from '@libp2p/interface'
2-
import { fromStringTuples } from '@multiformats/multiaddr'
2+
import { multiaddr } from '@multiformats/multiaddr'
33
import { QuicConnection } from './connection.js'
44
import * as napi from './napi.js'
55
import { QuicStreamMuxerFactory } from './stream-muxer.js'
@@ -86,28 +86,28 @@ export class QuicListener extends TypedEventEmitter<ListenerEvents> implements L
8686
return []
8787
}
8888

89-
async listen (multiaddr: Multiaddr): Promise<void> {
90-
const addr = multiaddr.nodeAddress()
89+
async listen (ma: Multiaddr): Promise<void> {
90+
const addr = ma.nodeAddress()
9191
const controller = new AbortController()
9292
const listener = new napi.Server(this.#config, addr.address, addr.port)
9393

9494
// replace wildcard port with actual listening port
9595
if (addr.port === 0) {
96-
const stringTuples = multiaddr.stringTuples()
96+
const components = ma.getComponents()
9797

98-
for (const stringTuple of stringTuples) {
99-
if (stringTuple[0] === 0x0111) {
100-
stringTuple[1] = `${listener.port()}`
98+
for (const component of components) {
99+
if (component.name === 'udp') {
100+
component.value = `${listener.port()}`
101101
}
102102
}
103103

104-
multiaddr = fromStringTuples(stringTuples)
104+
ma = multiaddr(components)
105105
}
106106

107107
this.state = {
108108
status: 'listening',
109109
listener,
110-
listenAddr: multiaddr,
110+
listenAddr: ma,
111111
controller,
112112
connections: new Set()
113113
}

test/transport.spec.ts

Lines changed: 94 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/* eslint-env mocha */
22

3+
import { isIPv4, isIPv6 } from '@chainsafe/is-ip'
34
import { generateKeyPair } from '@libp2p/crypto/keys'
45
import { defaultLogger } from '@libp2p/logger'
56
import { multiaddr } from '@multiformats/multiaddr'
@@ -14,14 +15,17 @@ import type { Multiaddr } from '@multiformats/multiaddr'
1415

1516
describe('Quic Transport', () => {
1617
let components: QuicComponents
17-
let listener: Listener
18+
let listeners: Listener[]
1819

1920
beforeEach(async () => {
21+
listeners = []
2022
components = await createComponents()
2123
})
2224

2325
afterEach(async () => {
24-
await listener?.close()
26+
await Promise.all(
27+
listeners.map(l => l.close())
28+
)
2529
})
2630

2731
it('transport filter filters out invalid dial multiaddrs', async () => {
@@ -48,9 +52,10 @@ describe('Quic Transport', () => {
4852
}
4953

5054
const transport = quic()(components)
51-
listener = transport.createListener({
55+
const listener = transport.createListener({
5256
upgrader: stubInterface<Upgrader>()
5357
})
58+
listeners.push(listener)
5459

5560
await Promise.all([
5661
pEvent(listener, 'listening'),
@@ -89,4 +94,90 @@ describe('Quic Transport', () => {
8994
it('supports listening on specific ipv6 addresses', async () => {
9095
await testListenAddresses(multiaddr('/ip6/::1/udp/0/quic-v1'), false)
9196
})
97+
98+
it('supports listening on multiple wildcards', async () => {
99+
const components = {
100+
logger: defaultLogger(),
101+
privateKey: await generateKeyPair('Ed25519')
102+
}
103+
104+
const transport = quic()(components)
105+
const ip4Listener = transport.createListener({
106+
upgrader: stubInterface<Upgrader>()
107+
})
108+
listeners.push(ip4Listener)
109+
const ip6Listener = transport.createListener({
110+
upgrader: stubInterface<Upgrader>()
111+
})
112+
listeners.push(ip6Listener)
113+
114+
await Promise.all([
115+
pEvent(ip4Listener, 'listening'),
116+
ip4Listener.listen(multiaddr('/ip4/0.0.0.0/udp/0/quic-v1')),
117+
pEvent(ip6Listener, 'listening'),
118+
ip6Listener.listen(multiaddr('/ip6/::/udp/0/quic-v1'))
119+
])
120+
121+
const addrs = [
122+
...ip4Listener.getAddrs(),
123+
...ip6Listener.getAddrs()
124+
]
125+
126+
let hadIp4 = false
127+
let hadIp6 = false
128+
129+
for (const addr of addrs) {
130+
const { host, port } = addr.toOptions()
131+
expect(port).to.be.greaterThan(0, 'did not translate wildcard port')
132+
133+
if (isIPv4(host)) {
134+
hadIp4 = true
135+
expect(host).to.not.equal('0.0.0.0', 'did not translate wildcard host')
136+
} else if (isIPv6(host)) {
137+
hadIp6 = true
138+
expect(host).to.not.equal('::', 'did not translate wildcard host')
139+
} else {
140+
throw new Error(`Host "${host}" was neither IPv4 nor IPv6`)
141+
}
142+
}
143+
144+
expect(hadIp4).to.be.true('did not listen on IPv4 addresses')
145+
expect(hadIp6).to.be.true('did not listen on IPv6 addresses')
146+
})
147+
148+
it('supports listening the same port for different families', async () => {
149+
const components = {
150+
logger: defaultLogger(),
151+
privateKey: await generateKeyPair('Ed25519')
152+
}
153+
154+
const transport = quic()(components)
155+
const ip4Listener = transport.createListener({
156+
upgrader: stubInterface<Upgrader>()
157+
})
158+
listeners.push(ip4Listener)
159+
const ip6Listener = transport.createListener({
160+
upgrader: stubInterface<Upgrader>()
161+
})
162+
listeners.push(ip6Listener)
163+
164+
await Promise.all([
165+
pEvent(ip4Listener, 'listening'),
166+
ip4Listener.listen(multiaddr('/ip4/127.0.0.1/udp/14000/quic-v1')),
167+
pEvent(ip6Listener, 'listening'),
168+
ip6Listener.listen(multiaddr('/ip6/::1/udp/14000/quic-v1'))
169+
])
170+
171+
const addrs = [
172+
...ip4Listener.getAddrs(),
173+
...ip6Listener.getAddrs()
174+
]
175+
176+
expect(addrs).to.have.lengthOf(2, 'did not listen on correct amount of addresses')
177+
178+
for (const addr of addrs) {
179+
const { port } = addr.toOptions()
180+
expect(port).to.equal(14000, 'did not listen on port')
181+
}
182+
})
92183
})

0 commit comments

Comments
 (0)