Skip to content

Commit

Permalink
fix(@libp2p/webtransport): handle dialing circuit addresses (#2054)
Browse files Browse the repository at this point in the history
We should be able to dial WebRTC addresses over Circuit Relay over
WebTransport but the `parseMultiaddr` function was rejecting them.

Simplifies the parsing function to handle this and future address
variants and adds tests.
  • Loading branch information
achingbrain authored Sep 15, 2023
1 parent 0ce318e commit 20d5f22
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 68 deletions.
1 change: 1 addition & 0 deletions packages/transport-webtransport/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"@libp2p/logger": "^3.0.2",
"@libp2p/peer-id": "^3.0.2",
"@multiformats/multiaddr": "^12.1.5",
"@multiformats/multiaddr-matcher": "^1.0.1",
"it-stream-types": "^2.0.1",
"multiformats": "^12.0.1",
"uint8arraylist": "^2.4.3",
Expand Down
107 changes: 41 additions & 66 deletions packages/transport-webtransport/src/utils/parse-multiaddr.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { CodeError } from '@libp2p/interface/errors'
import { peerIdFromString } from '@libp2p/peer-id'
import { type Multiaddr, protocols } from '@multiformats/multiaddr'
import { WebTransport } from '@multiformats/multiaddr-matcher'
import { bases, digest } from 'multiformats/basics'
import type { PeerId } from '@libp2p/interface/peer-id'
import type { MultihashDigest } from 'multiformats/hashes/interface'
Expand All @@ -11,73 +13,46 @@ function decodeCerthashStr (s: string): MultihashDigest {
return digest.decode(multibaseDecoder.decode(s))
}

export function parseMultiaddr (ma: Multiaddr): { url: string, certhashes: MultihashDigest[], remotePeer?: PeerId } {
export interface ParsedMultiaddr {
url: string
certhashes: MultihashDigest[]
remotePeer?: PeerId
}

export function parseMultiaddr (ma: Multiaddr): ParsedMultiaddr {
if (!WebTransport.matches(ma)) {
throw new CodeError('Invalid multiaddr, was not a WebTransport address', 'ERR_INVALID_MULTIADDR')
}

const parts = ma.stringTuples()
const certhashes = parts
.filter(([name, _]) => name === protocols('certhash').code)
.map(([_, value]) => decodeCerthashStr(value ?? ''))

// only take the first peer id in the multiaddr as it may be a relay
const remotePeer = parts
.filter(([name, _]) => name === protocols('p2p').code)
.map(([_, value]) => peerIdFromString(value ?? ''))[0]

const opts = ma.toOptions()
let host = opts.host

// This is simpler to have inline than extract into a separate function
// eslint-disable-next-line complexity
const { url, certhashes, remotePeer } = parts.reduce((state: { url: string, certhashes: MultihashDigest[], seenHost: boolean, seenPort: boolean, remotePeer?: PeerId }, [proto, value]) => {
switch (proto) {
case protocols('ip6').code:
// @ts-expect-error - ts error on switch fallthrough
case protocols('dns6').code:
if (value?.includes(':') === true) {
/**
* This resolves cases where `new globalThis.WebTransport` fails to construct because of an invalid URL being passed.
*
* `new URL('https://::1:4001/blah')` will throw a `TypeError: Failed to construct 'URL': Invalid URL`
* `new URL('https://[::1]:4001/blah')` is valid and will not.
*
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2
*/
value = `[${value}]`
}
// eslint-disable-next-line no-fallthrough
case protocols('ip4').code:
case protocols('dns4').code:
if (state.seenHost || state.seenPort) {
throw new Error('Invalid multiaddr, saw host and already saw the host or port')
}
return {
...state,
url: `${state.url}${value ?? ''}`,
seenHost: true
}
case protocols('quic').code:
case protocols('quic-v1').code:
case protocols('webtransport').code:
if (!state.seenHost || !state.seenPort) {
throw new Error("Invalid multiaddr, Didn't see host and port, but saw quic/webtransport")
}
return state
case protocols('udp').code:
if (state.seenPort) {
throw new Error('Invalid multiaddr, saw port but already saw the port')
}
return {
...state,
url: `${state.url}:${value ?? ''}`,
seenPort: true
}
case protocols('certhash').code:
if (!state.seenHost || !state.seenPort) {
throw new Error('Invalid multiaddr, saw the certhash before seeing the host and port')
}
return {
...state,
certhashes: state.certhashes.concat([decodeCerthashStr(value ?? '')])
}
case protocols('p2p').code:
return {
...state,
remotePeer: peerIdFromString(value ?? '')
}
default:
throw new Error(`unexpected component in multiaddr: ${proto} ${protocols(proto).name} ${value ?? ''} `)
}
},
// All webtransport urls are https
{ url: 'https://', seenHost: false, seenPort: false, certhashes: [] })
if (opts.family === 6 && host?.includes(':')) {
/**
* This resolves cases where `new WebTransport()` fails to construct because of an invalid URL being passed.
*
* `new URL('https://::1:4001/blah')` will throw a `TypeError: Failed to construct 'URL': Invalid URL`
* `new URL('https://[::1]:4001/blah')` is valid and will not.
*
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2
*/
host = `[${host}]`
}

return { url, certhashes, remotePeer }
return {
// All webtransport urls are https
url: `https://${host}:${opts.port}`,
certhashes,
remotePeer
}
}
4 changes: 2 additions & 2 deletions packages/transport-webtransport/test/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@ describe('libp2p-webtransport', () => {
const maStrP2p = maStr.split('/p2p/')[1]
const ma = multiaddr(maStrNoCerthash + '/p2p/' + maStrP2p)

const err = await expect(node.dial(ma)).to.eventually.be.rejected()
expect(err.toString()).to.contain('Expected multiaddr to contain certhashes')
await expect(node.dial(ma)).to.eventually.be.rejected()
.with.property('code', 'ERR_INVALID_MULTIADDR')
})

it('fails to connect due to an aborted signal', async () => {
Expand Down
73 changes: 73 additions & 0 deletions packages/transport-webtransport/test/utils/parse-multiaddr.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { multiaddr } from '@multiformats/multiaddr'
import { expect } from 'aegir/chai'
import { base64url } from 'multiformats/bases/base64'
import { parseMultiaddr } from '../../src/utils/parse-multiaddr.js'

describe('parse multiaddr', () => {
describe('valid addresses', () => {
it('parses relay address', () => {
const relayPeer = '12D3KooWKtv8rpaXJkLCoH4C299wFCVBg1eMzZrPfaV37QVVJrhF'
const targetPeer = '12D3KooWCDt87xcGVJWmQpaXGTSaevbRpAoMJqvsVuETDrQJvSC5'
const certHashes = [
'uEiCvU3clCu16U6Xjh9dzH7yKE2bkGftZw404nYMR6ZXIyg',
'uEiB8ZfHAe_lEBtxio0KQwmE8mFEesh3p_7-Ac5oOU7HhOw'
]

const ma = multiaddr(`/ip4/154.38.162.255/udp/4001/quic-v1/webtransport/${certHashes.map(c => `certhash/${c}`).join('/')}/p2p/${relayPeer}/p2p-circuit/p2p/${targetPeer}`)
const { url, certhashes, remotePeer } = parseMultiaddr(ma)

expect(url).to.equal('https://154.38.162.255:4001')
expect(certhashes.map(hash => base64url.encode(hash.bytes))).to.deep.equal(certHashes)
expect(remotePeer?.toString()).to.equal(relayPeer.toString())
})

it('parses WebRTC relay address', () => {
const relayPeer = '12D3KooWKtv8rpaXJkLCoH4C299wFCVBg1eMzZrPfaV37QVVJrhF'
const targetPeer = '12D3KooWCDt87xcGVJWmQpaXGTSaevbRpAoMJqvsVuETDrQJvSC5'
const certHashes = [
'uEiCvU3clCu16U6Xjh9dzH7yKE2bkGftZw404nYMR6ZXIyg',
'uEiB8ZfHAe_lEBtxio0KQwmE8mFEesh3p_7-Ac5oOU7HhOw'
]

const ma = multiaddr(`/ip4/154.38.162.255/udp/4001/quic-v1/webtransport/${certHashes.map(c => `certhash/${c}`).join('/')}/p2p/${relayPeer}/p2p-circuit/webrtc/p2p/${targetPeer}`)
const { url, certhashes, remotePeer } = parseMultiaddr(ma)

expect(url).to.equal('https://154.38.162.255:4001')
expect(certhashes.map(hash => base64url.encode(hash.bytes))).to.deep.equal(certHashes)
expect(remotePeer?.toString()).to.equal(relayPeer)
})

it('parses ip6 loopback address', () => {
const targetPeer = '12D3KooWCDt87xcGVJWmQpaXGTSaevbRpAoMJqvsVuETDrQJvSC5'
const certHashes = [
'uEiCvU3clCu16U6Xjh9dzH7yKE2bkGftZw404nYMR6ZXIyg',
'uEiB8ZfHAe_lEBtxio0KQwmE8mFEesh3p_7-Ac5oOU7HhOw'
]

const ma = multiaddr(`/ip6/::1/udp/4001/quic-v1/webtransport/${certHashes.map(c => `certhash/${c}`).join('/')}/p2p/${targetPeer}`)
const { url, certhashes, remotePeer } = parseMultiaddr(ma)

expect(url).to.equal('https://[::1]:4001')
expect(certhashes.map(hash => base64url.encode(hash.bytes))).to.deep.equal(certHashes)
expect(remotePeer?.toString()).to.equal(targetPeer)
})
})

describe('invalid addresses', () => {
it('fails to parse a non-webtransport address', () => {
const targetPeer = '12D3KooWCDt87xcGVJWmQpaXGTSaevbRpAoMJqvsVuETDrQJvSC5'
const ma = multiaddr(`/ip4/123.123.123.123/udp/4001/p2p/${targetPeer}`)

expect(() => parseMultiaddr(ma)).to.throw()
.with.property('code', 'ERR_INVALID_MULTIADDR')
})

it('fails to parse a webtransport address without certhashes', () => {
const targetPeer = '12D3KooWCDt87xcGVJWmQpaXGTSaevbRpAoMJqvsVuETDrQJvSC5'
const ma = multiaddr(`/ip4/123.123.123.123/udp/4001/webtransport/p2p/${targetPeer}`)

expect(() => parseMultiaddr(ma)).to.throw()
.with.property('code', 'ERR_INVALID_MULTIADDR')
})
})
})

0 comments on commit 20d5f22

Please sign in to comment.