Skip to content

Commit 6ddc1b8

Browse files
authored
feat: allow adding external ip/port mapping (#2836)
Adds functions to the address manager to add external ip/port/protocol tuples which will be added as extra addresses to the list of multiaddrs the node is listening on.
1 parent 0862522 commit 6ddc1b8

File tree

3 files changed

+174
-5
lines changed

3 files changed

+174
-5
lines changed

packages/interface-internal/src/address-manager/index.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,21 @@ export interface AddressManager {
5353
* Remove a mapping previously added with `addDNSMapping`.
5454
*/
5555
removeDNSMapping(domain: string): void
56+
57+
/**
58+
* Add a publicly routable address/port/protocol tuple that this node is
59+
* reachable on. Where this node listens on a link-local (e.g. LAN) address
60+
* with the same protocol for any transport, an additional listen address will
61+
* be added with the IP and port replaced with this IP and port.
62+
*
63+
* It's possible to add a IPv6 address here and have it added to the address
64+
* list, this is for the case when a router has an external IPv6 address with
65+
* port forwarding configured, but it does IPv6 -> IPv4 NAT.
66+
*/
67+
addPublicAddressMapping (internalIp: string, internalPort: number, externalIp: string, externalPort?: number, protocol?: 'tcp' | 'udp'): void
68+
69+
/**
70+
* Remove a publicly routable address that this node is no longer reachable on
71+
*/
72+
removePublicAddressMapping (internalIp: string, internalPort: number, externalIp: string, externalPort?: number, protocol?: 'tcp' | 'udp'): void
5673
}

packages/libp2p/src/address-manager.ts

Lines changed: 79 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,13 @@ const CODEC_IP4 = 0x04
7878
const CODEC_IP6 = 0x29
7979
const CODEC_DNS4 = 0x36
8080
const CODEC_DNS6 = 0x37
81+
const CODEC_TCP = 0x06
82+
const CODEC_UDP = 0x0111
83+
84+
interface PublicAddressMapping {
85+
externalIp: string
86+
externalPort: number
87+
}
8188

8289
export class AddressManager implements AddressManagerInterface {
8390
private readonly log: Logger
@@ -89,6 +96,7 @@ export class AddressManager implements AddressManagerInterface {
8996
private readonly observed: Map<string, ObservedAddressMetadata>
9097
private readonly announceFilter: AddressFilter
9198
private readonly ipDomainMappings: Map<string, string>
99+
private readonly publicAddressMappings: Map<string, PublicAddressMapping[]>
92100

93101
/**
94102
* Responsible for managing the peer addresses.
@@ -106,6 +114,7 @@ export class AddressManager implements AddressManagerInterface {
106114
this.appendAnnounce = new Set(appendAnnounce.map(ma => ma.toString()))
107115
this.observed = new Map()
108116
this.ipDomainMappings = new Map()
117+
this.publicAddressMappings = new Map()
109118
this.announceFilter = init.announceFilter ?? defaultAddressFilter
110119

111120
// this method gets called repeatedly on startup when transports start listening so
@@ -239,11 +248,51 @@ export class AddressManager implements AddressManagerInterface {
239248
.map(([ma]) => multiaddr(ma))
240249
)
241250

242-
const mappedMultiaddrs: Multiaddr[] = []
251+
// add public addresses
252+
const ipMappedMultiaddrs: Multiaddr[] = []
253+
multiaddrs.forEach(ma => {
254+
const tuples = ma.stringTuples()
255+
let tuple: string | undefined
256+
257+
// see if the internal host/port/protocol tuple has been mapped externally
258+
if ((tuples[0][0] === CODEC_IP4 || tuples[0][0] === CODEC_IP6) && tuples[1][0] === CODEC_TCP) {
259+
tuple = `${tuples[0][1]}-${tuples[1][1]}-tcp`
260+
} else if ((tuples[0][0] === CODEC_IP4 || tuples[0][0] === CODEC_IP6) && tuples[1][0] === CODEC_UDP) {
261+
tuple = `${tuples[0][1]}-${tuples[1][1]}-udp`
262+
}
263+
264+
if (tuple == null) {
265+
return
266+
}
267+
268+
const mappings = this.publicAddressMappings.get(tuple)
269+
270+
if (mappings == null) {
271+
return
272+
}
273+
274+
mappings.forEach(mapping => {
275+
tuples[0][1] = mapping.externalIp
276+
tuples[1][1] = `${mapping.externalPort}`
277+
278+
ipMappedMultiaddrs.push(
279+
multiaddr(`/${
280+
tuples.map(tuple => {
281+
return [
282+
protocols(tuple[0]).name,
283+
tuple[1]
284+
].join('/')
285+
}).join('/')
286+
}`)
287+
)
288+
})
289+
})
290+
multiaddrs = multiaddrs.concat(ipMappedMultiaddrs)
243291

244292
// add ip->domain mappings
293+
const dnsMappedMultiaddrs: Multiaddr[] = []
245294
for (const ma of multiaddrs) {
246-
const tuples = [...ma.stringTuples()]
295+
const tuples = ma.stringTuples()
247296
let mappedIp = false
248297

249298
for (const [ip, domain] of this.ipDomainMappings.entries()) {
@@ -267,7 +316,7 @@ export class AddressManager implements AddressManagerInterface {
267316
}
268317

269318
if (mappedIp) {
270-
mappedMultiaddrs.push(
319+
dnsMappedMultiaddrs.push(
271320
multiaddr(`/${
272321
tuples.map(tuple => {
273322
return [
@@ -279,8 +328,7 @@ export class AddressManager implements AddressManagerInterface {
279328
)
280329
}
281330
}
282-
283-
multiaddrs = multiaddrs.concat(mappedMultiaddrs)
331+
multiaddrs = multiaddrs.concat(dnsMappedMultiaddrs)
284332

285333
// dedupe multiaddrs
286334
const addrSet = new Set<string>()
@@ -318,15 +366,41 @@ export class AddressManager implements AddressManagerInterface {
318366

319367
addDNSMapping (domain: string, addresses: string[]): void {
320368
addresses.forEach(ip => {
369+
this.log('add DNS mapping %s to %s', ip, domain)
321370
this.ipDomainMappings.set(ip, domain)
322371
})
323372
}
324373

325374
removeDNSMapping (domain: string): void {
326375
for (const [key, value] of this.ipDomainMappings.entries()) {
327376
if (value === domain) {
377+
this.log('remove DNS mapping for %s', domain)
328378
this.ipDomainMappings.delete(key)
329379
}
330380
}
331381
}
382+
383+
addPublicAddressMapping (internalIp: string, internalPort: number, externalIp: string, externalPort: number = internalPort, protocol: 'tcp' | 'udp' = 'tcp'): void {
384+
const key = `${internalIp}-${internalPort}-${protocol}`
385+
const mappings = this.publicAddressMappings.get(key) ?? []
386+
mappings.push({
387+
externalIp,
388+
externalPort
389+
})
390+
391+
this.publicAddressMappings.set(key, mappings)
392+
}
393+
394+
removePublicAddressMapping (internalIp: string, internalPort: number, externalIp: string, externalPort: number = internalPort, protocol: 'tcp' | 'udp' = 'tcp'): void {
395+
const key = `${internalIp}-${internalPort}-${protocol}`
396+
const mappings = (this.publicAddressMappings.get(key) ?? []).filter(mapping => {
397+
return mapping.externalIp !== externalIp && mapping.externalPort !== externalPort
398+
})
399+
400+
if (mappings.length === 0) {
401+
this.publicAddressMappings.delete(key)
402+
} else {
403+
this.publicAddressMappings.set(key, mappings)
404+
}
405+
}
332406
}

packages/libp2p/test/addresses/address-manager.spec.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,4 +349,82 @@ describe('Address Manager', () => {
349349

350350
expect(am.getAddresses()).to.deep.equal([externalAddress.encapsulate(`/p2p/${peerId.toString()}`)])
351351
})
352+
353+
it('should add a public IPv4 address mapping', () => {
354+
const transportManager = stubInterface<TransportManager>()
355+
const am = new AddressManager({
356+
peerId,
357+
transportManager,
358+
peerStore,
359+
events,
360+
logger: defaultLogger()
361+
})
362+
363+
const internalIp = '192.168.1.123'
364+
const internalPort = 4567
365+
const externalIp = '81.12.12.1'
366+
const externalPort = 8910
367+
const protocol = 'tcp'
368+
369+
am.addPublicAddressMapping(internalIp, internalPort, externalIp, externalPort, protocol)
370+
371+
// one loopback, one LAN address
372+
transportManager.getAddrs.returns([
373+
multiaddr('/ip4/127.0.0.1/tcp/1234'),
374+
multiaddr(`/ip4/${internalIp}/${protocol}/${internalPort}`)
375+
])
376+
377+
// should have mapped the LAN address to the external IP
378+
expect(am.getAddresses()).to.deep.equal([
379+
multiaddr(`/ip4/127.0.0.1/tcp/1234/p2p/${peerId.toString()}`),
380+
multiaddr(`/ip4/${internalIp}/${protocol}/${internalPort}/p2p/${peerId.toString()}`),
381+
multiaddr(`/ip4/${externalIp}/${protocol}/${externalPort}/p2p/${peerId.toString()}`)
382+
])
383+
384+
am.removePublicAddressMapping(internalIp, internalPort, externalIp, externalPort, protocol)
385+
386+
expect(am.getAddresses()).to.deep.equal([
387+
multiaddr(`/ip4/127.0.0.1/tcp/1234/p2p/${peerId.toString()}`),
388+
multiaddr(`/ip4/${internalIp}/${protocol}/${internalPort}/p2p/${peerId.toString()}`)
389+
])
390+
})
391+
392+
it('should add a public IPv6 address mapping', () => {
393+
const transportManager = stubInterface<TransportManager>()
394+
const am = new AddressManager({
395+
peerId,
396+
transportManager,
397+
peerStore,
398+
events,
399+
logger: defaultLogger()
400+
})
401+
402+
const internalIp = 'fd9b:ec6c:a487:efd2:14bc:d40:b478:9555'
403+
const internalPort = 4567
404+
const externalIp = '2a00:23c6:14b1:7e00:28b8:30d:944e:27f3'
405+
const externalPort = 8910
406+
const protocol = 'tcp'
407+
408+
am.addPublicAddressMapping(internalIp, internalPort, externalIp, externalPort, protocol)
409+
410+
// one loopback, one LAN address
411+
transportManager.getAddrs.returns([
412+
multiaddr('/ip6/::1/tcp/1234'),
413+
multiaddr(`/ip6/${internalIp}/${protocol}/${internalPort}`)
414+
])
415+
416+
// should have mapped the LAN address to the external IP
417+
expect(am.getAddresses()).to.deep.equal([
418+
multiaddr(`/ip6/::1/tcp/1234/p2p/${peerId.toString()}`),
419+
multiaddr(`/ip6/${internalIp}/${protocol}/${internalPort}/p2p/${peerId.toString()}`),
420+
multiaddr(`/ip6/${externalIp}/${protocol}/${externalPort}/p2p/${peerId.toString()}`)
421+
])
422+
423+
am.removePublicAddressMapping(internalIp, internalPort, externalIp, externalPort, protocol)
424+
425+
expect(am.getAddresses()).to.deep.equal([
426+
multiaddr(`/ip6/::1/tcp/1234/p2p/${peerId.toString()}`),
427+
multiaddr(`/ip6/${internalIp}/${protocol}/${internalPort}/p2p/${peerId.toString()}`)
428+
])
429+
})
352430
})

0 commit comments

Comments
 (0)