Skip to content

Commit c4b6a37

Browse files
authored
feat: select muxer early (#3026)
Uses ALPN protocols to select an early muxer and skip the multistream-select dance. Restores compatibility with jvm-libp2p.
1 parent 2fbcdb6 commit c4b6a37

File tree

10 files changed

+135
-23
lines changed

10 files changed

+135
-23
lines changed

interop/node-version.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,6 @@
1111
"webrtc",
1212
"webrtc-direct"
1313
],
14-
"secureChannels": ["noise"],
14+
"secureChannels": ["noise", "tls"],
1515
"muxers": ["yamux", "mplex"]
1616
}

interop/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,12 @@
2121
"@chainsafe/libp2p-noise": "^16.0.0",
2222
"@chainsafe/libp2p-yamux": "^7.0.1",
2323
"@libp2p/circuit-relay-v2": "^3.1.3",
24-
"@libp2p/interface": "^2.2.1",
2524
"@libp2p/identify": "^3.0.12",
25+
"@libp2p/interface": "^2.2.1",
2626
"@libp2p/mplex": "^11.0.13",
2727
"@libp2p/ping": "^2.0.12",
2828
"@libp2p/tcp": "^10.0.13",
29+
"@libp2p/tls": "^2.0.17",
2930
"@libp2p/webrtc": "^5.0.19",
3031
"@libp2p/websockets": "^9.0.13",
3132
"@libp2p/webtransport": "^5.0.18",

interop/test/fixtures/get-libp2p.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,19 @@
33
import { noise } from '@chainsafe/libp2p-noise'
44
import { yamux } from '@chainsafe/libp2p-yamux'
55
import { circuitRelayTransport } from '@libp2p/circuit-relay-v2'
6-
import { type Identify, identify } from '@libp2p/identify'
6+
import { identify } from '@libp2p/identify'
77
import { mplex } from '@libp2p/mplex'
8-
import { type PingService, ping } from '@libp2p/ping'
8+
import { ping } from '@libp2p/ping'
99
import { tcp } from '@libp2p/tcp'
10+
import { tls } from '@libp2p/tls'
1011
import { webRTC, webRTCDirect } from '@libp2p/webrtc'
1112
import { webSockets } from '@libp2p/websockets'
1213
import { webTransport } from '@libp2p/webtransport'
13-
import { type Libp2pOptions, createLibp2p } from 'libp2p'
14+
import { createLibp2p } from 'libp2p'
15+
import type { Identify } from '@libp2p/identify'
1416
import type { Libp2p } from '@libp2p/interface'
17+
import type { PingService } from '@libp2p/ping'
18+
import type { Libp2pOptions } from 'libp2p'
1519

1620
const isDialer: boolean = process.env.is_dialer === 'true'
1721

@@ -106,6 +110,9 @@ export async function getLibp2p (): Promise<Libp2p<{ ping: PingService }>> {
106110
case 'noise':
107111
options.connectionEncrypters = [noise()]
108112
break
113+
case 'tls':
114+
options.connectionEncrypters = [tls()]
115+
break
109116
default:
110117
throw new Error(`Unknown secure channel: ${SECURE_CHANNEL ?? ''}`)
111118
}

packages/connection-encrypter-tls/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@
6767
"aegir": "^45.1.1",
6868
"it-pair": "^2.0.6",
6969
"protons": "^7.6.0",
70-
"sinon": "^19.0.2"
70+
"sinon": "^19.0.2",
71+
"sinon-ts": "^2.0.0"
7172
},
7273
"browser": {
7374
"./dist/src/tls.js": "./dist/src/tls.browser.js"

packages/connection-encrypter-tls/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,14 @@
1919
*/
2020

2121
import { TLS } from './tls.js'
22-
import type { ComponentLogger, ConnectionEncrypter, Metrics, PrivateKey } from '@libp2p/interface'
22+
import type { ComponentLogger, ConnectionEncrypter, Metrics, PrivateKey, Upgrader } from '@libp2p/interface'
2323

2424
export const PROTOCOL = '/tls/1.0.0'
2525

2626
export interface TLSComponents {
2727
privateKey: PrivateKey
2828
logger: ComponentLogger
29+
upgrader: Upgrader
2930
metrics?: Metrics
3031
}
3132

packages/connection-encrypter-tls/src/tls.ts

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@
1919
*/
2020

2121
import { TLSSocket, type TLSSocketOptions, connect } from 'node:tls'
22-
import { serviceCapabilities } from '@libp2p/interface'
22+
import { InvalidCryptoExchangeError, serviceCapabilities } from '@libp2p/interface'
2323
import { HandshakeTimeoutError } from './errors.js'
2424
import { generateCertificate, verifyPeerCertificate, itToStream, streamToIt } from './utils.js'
2525
import { PROTOCOL } from './index.js'
2626
import type { TLSComponents } from './index.js'
27-
import type { MultiaddrConnection, ConnectionEncrypter, SecuredConnection, Logger, SecureConnectionOptions, CounterGroup } from '@libp2p/interface'
27+
import type { MultiaddrConnection, ConnectionEncrypter, SecuredConnection, Logger, SecureConnectionOptions, CounterGroup, StreamMuxerFactory } from '@libp2p/interface'
2828
import type { Duplex } from 'it-stream-types'
2929
import type { Uint8ArrayList } from 'uint8arraylist'
3030

@@ -88,14 +88,41 @@ export class TLS implements ConnectionEncrypter {
8888
* Encrypt connection
8989
*/
9090
async _encrypt <Stream extends Duplex<AsyncGenerator<Uint8Array | Uint8ArrayList>> = MultiaddrConnection> (conn: Stream, isServer: boolean, options?: SecureConnectionOptions): Promise<SecuredConnection<Stream>> {
91+
let streamMuxer: StreamMuxerFactory | undefined
92+
9193
const opts: TLSSocketOptions = {
9294
...await generateCertificate(this.components.privateKey),
9395
isServer,
9496
// require TLS 1.3 or later
9597
minVersion: 'TLSv1.3',
9698
maxVersion: 'TLSv1.3',
9799
// accept self-signed certificates
98-
rejectUnauthorized: false
100+
rejectUnauthorized: false,
101+
102+
// early negotiation of muxer via ALPN protocols
103+
ALPNProtocols: [
104+
...this.components.upgrader.getStreamMuxers().keys(),
105+
'libp2p'
106+
],
107+
ALPNCallback: ({ protocols }) => {
108+
this.log.trace('received protocols %s', protocols)
109+
let chosenProtocol: string | undefined
110+
111+
for (const protocol of protocols) {
112+
if (protocol === 'libp2p') {
113+
chosenProtocol = 'libp2p'
114+
}
115+
116+
streamMuxer = this.components.upgrader.getStreamMuxers().get(protocol)
117+
118+
if (streamMuxer != null) {
119+
chosenProtocol = protocol
120+
break
121+
}
122+
}
123+
124+
return chosenProtocol
125+
}
99126
}
100127

101128
let socket: TLSSocket
@@ -131,12 +158,38 @@ export class TLS implements ConnectionEncrypter {
131158
.then(remotePeer => {
132159
this.log('remote certificate ok, remote peer %p', remotePeer)
133160

161+
if (!isServer && typeof socket.alpnProtocol === 'string') {
162+
streamMuxer = this.components.upgrader.getStreamMuxers().get(socket.alpnProtocol)
163+
164+
if (streamMuxer == null) {
165+
this.log.error('selected muxer that did not exist')
166+
}
167+
}
168+
169+
// 'libp2p' is a special protocol - if it's sent the remote does not
170+
// support early muxer negotiation
171+
if (!isServer && typeof socket.alpnProtocol === 'string' && socket.alpnProtocol !== 'libp2p') {
172+
this.log.trace('got early muxer', socket.alpnProtocol)
173+
streamMuxer = this.components.upgrader.getStreamMuxers().get(socket.alpnProtocol)
174+
175+
if (streamMuxer == null) {
176+
const err = new InvalidCryptoExchangeError(`Selected muxer ${socket.alpnProtocol} did not exist`)
177+
this.log.error(`Selected muxer ${socket.alpnProtocol} did not exist - %e`, err)
178+
179+
if (isAbortable(conn)) {
180+
conn.abort(err)
181+
reject(err)
182+
}
183+
}
184+
}
185+
134186
resolve({
135187
remotePeer,
136188
conn: {
137189
...conn,
138190
...streamToIt(socket)
139-
}
191+
},
192+
streamMuxer
140193
})
141194
})
142195
.catch((err: Error) => {

packages/connection-encrypter-tls/test/index.spec.ts

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import { peerIdFromMultihash, peerIdFromPrivateKey } from '@libp2p/peer-id'
66
import { expect } from 'aegir/chai'
77
import { duplexPair } from 'it-pair/duplex'
88
import sinon from 'sinon'
9+
import { stubInterface } from 'sinon-ts'
910
import { tls } from '../src/index.js'
10-
import type { ConnectionEncrypter, PeerId } from '@libp2p/interface'
11+
import type { StreamMuxerFactory, ConnectionEncrypter, PeerId, Upgrader, MultiaddrConnection } from '@libp2p/interface'
1112

1213
describe('tls', () => {
1314
let localPeer: PeerId
@@ -26,7 +27,14 @@ describe('tls', () => {
2627

2728
encrypter = tls()({
2829
privateKey: localKeyPair,
29-
logger: defaultLogger()
30+
logger: defaultLogger(),
31+
upgrader: stubInterface<Upgrader>({
32+
getStreamMuxers () {
33+
return new Map([['/test/muxer', stubInterface<StreamMuxerFactory>({
34+
protocol: '/test/muxer'
35+
})]])
36+
}
37+
})
3038
})
3139
})
3240

@@ -38,10 +46,14 @@ describe('tls', () => {
3846
const [inbound, outbound] = duplexPair<any>()
3947

4048
await Promise.all([
41-
encrypter.secureInbound(inbound, {
49+
encrypter.secureInbound(stubInterface<MultiaddrConnection>({
50+
...inbound
51+
}), {
4252
remotePeer
4353
}),
44-
encrypter.secureOutbound(outbound, {
54+
encrypter.secureOutbound(stubInterface<MultiaddrConnection>({
55+
...outbound
56+
}), {
4557
remotePeer: wrongPeer
4658
})
4759
]).then(() => expect.fail('should have failed'), (err) => {
@@ -57,19 +69,48 @@ describe('tls', () => {
5769

5870
encrypter = tls()({
5971
privateKey: keyPair,
60-
logger: defaultLogger()
72+
logger: defaultLogger(),
73+
upgrader: stubInterface<Upgrader>({
74+
getStreamMuxers () {
75+
return new Map([['/test/muxer', stubInterface<StreamMuxerFactory>()]])
76+
}
77+
})
6178
})
6279

6380
const [inbound, outbound] = duplexPair<any>()
6481

6582
await expect(Promise.all([
66-
encrypter.secureInbound(inbound, {
83+
encrypter.secureInbound(stubInterface<MultiaddrConnection>({
84+
...inbound
85+
}), {
6786
remotePeer
6887
}),
69-
encrypter.secureOutbound(outbound, {
88+
encrypter.secureOutbound(stubInterface<MultiaddrConnection>({
89+
...outbound
90+
}), {
7091
remotePeer: localPeer
7192
})
7293
]))
7394
.to.eventually.be.rejected.with.property('name', 'UnexpectedPeerError')
7495
})
96+
97+
it('should select an early muxer', async () => {
98+
const [inbound, outbound] = duplexPair<any>()
99+
100+
const result = await Promise.all([
101+
encrypter.secureInbound(stubInterface<MultiaddrConnection>({
102+
...inbound
103+
}), {
104+
remotePeer: localPeer
105+
}),
106+
encrypter.secureOutbound(stubInterface<MultiaddrConnection>({
107+
...outbound
108+
}), {
109+
remotePeer: localPeer
110+
})
111+
])
112+
113+
expect(result).to.have.nested.property('[0].streamMuxer.protocol', '/test/muxer')
114+
expect(result).to.have.nested.property('[1].streamMuxer.protocol', '/test/muxer')
115+
})
75116
})

packages/integration-tests/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
"p-retry": "^6.2.1",
8484
"p-wait-for": "^5.0.2",
8585
"sinon": "^19.0.2",
86+
"sinon-ts": "^2.0.0",
8687
"uint8arraylist": "^2.4.8",
8788
"uint8arrays": "^5.1.0",
8889
"wherearewe": "^2.0.1"

packages/integration-tests/test/compliance/connection-encryption/tls.spec.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import { generateKeyPair } from '@libp2p/crypto/keys'
44
import suite from '@libp2p/interface-compliance-tests/connection-encryption'
55
import { defaultLogger } from '@libp2p/logger'
66
import { tls } from '@libp2p/tls'
7+
import { stubInterface } from 'sinon-ts'
78
import { isBrowser, isWebWorker } from 'wherearewe'
9+
import type { StreamMuxerFactory, Upgrader } from '@libp2p/interface'
810

911
describe('tls connection encrypter interface compliance', () => {
1012
if (isBrowser || isWebWorker) {
@@ -15,7 +17,12 @@ describe('tls connection encrypter interface compliance', () => {
1517
async setup (opts) {
1618
return tls()({
1719
privateKey: opts?.privateKey ?? await generateKeyPair('Ed25519'),
18-
logger: defaultLogger()
20+
logger: defaultLogger(),
21+
upgrader: stubInterface<Upgrader>({
22+
getStreamMuxers () {
23+
return new Map([['/test/muxer', stubInterface<StreamMuxerFactory>()]])
24+
}
25+
})
1926
})
2027
},
2128
async teardown () {

packages/integration-tests/test/interop.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ async function createJsPeer (options: SpawnOptions): Promise<Daemon> {
138138
webRTCDirect()
139139
],
140140
streamMuxers: [],
141-
connectionEncrypters: [noise()]
141+
connectionEncrypters: []
142142
}
143143

144144
if (options.noListen !== true) {
@@ -159,12 +159,12 @@ async function createJsPeer (options: SpawnOptions): Promise<Daemon> {
159159
identify: identify()
160160
}
161161

162-
if (options.encryption === 'noise') {
163-
opts.connectionEncrypters?.push(noise())
164-
} else if (options.encryption === 'tls') {
162+
if (options.encryption === 'tls') {
165163
opts.connectionEncrypters?.push(tls())
166164
} else if (options.encryption === 'plaintext') {
167165
opts.connectionEncrypters?.push(plaintext())
166+
} else {
167+
opts.connectionEncrypters?.push(noise())
168168
}
169169

170170
if (options.muxer === 'mplex') {

0 commit comments

Comments
 (0)