Skip to content

Commit

Permalink
feat: allow adding external ip/port mapping
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
achingbrain committed Nov 21, 2024
1 parent 98f3c77 commit 0d68b06
Show file tree
Hide file tree
Showing 3 changed files with 174 additions and 5 deletions.
17 changes: 17 additions & 0 deletions packages/interface-internal/src/address-manager/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,21 @@ export interface AddressManager {
* Remove a mapping previously added with `addDNSMapping`.
*/
removeDNSMapping(domain: string): void

/**
* Add a publicly routable address/port/protocol tuple that this node is
* reachable on. Where this node listens on a link-local (e.g. LAN) address
* with the same protocol for any transport, an additional listen address will
* be added with the IP and port replaced with this IP and port.
*
* It's possible to add a IPv6 address here and have it added to the address
* list, this is for the case when a router has an external IPv6 address with
* port forwarding configured, but it does IPv6 -> IPv4 NAT.
*/
addPublicAddressMapping (internalIp: string, internalPort: number, externalIp: string, externalPort?: number, protocol?: 'tcp' | 'udp'): void

/**
* Remove a publicly routable address that this node is no longer reachable on
*/
removePublicAddressMapping (internalIp: string, internalPort: number, externalIp: string, externalPort?: number, protocol?: 'tcp' | 'udp'): void
}
84 changes: 79 additions & 5 deletions packages/libp2p/src/address-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@ const CODEC_IP4 = 0x04
const CODEC_IP6 = 0x29
const CODEC_DNS4 = 0x36
const CODEC_DNS6 = 0x37
const CODEC_TCP = 0x06
const CODEC_UDP = 0x0111

interface PublicAddressMapping {
externalIp: string
externalPort: number
}

export class AddressManager implements AddressManagerInterface {
private readonly log: Logger
Expand All @@ -89,6 +96,7 @@ export class AddressManager implements AddressManagerInterface {
private readonly observed: Map<string, ObservedAddressMetadata>
private readonly announceFilter: AddressFilter
private readonly ipDomainMappings: Map<string, string>
private readonly publicAddressMappings: Map<string, PublicAddressMapping[]>

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

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

const mappedMultiaddrs: Multiaddr[] = []
// add public addresses
const ipMappedMultiaddrs: Multiaddr[] = []
multiaddrs.forEach(ma => {
const tuples = ma.stringTuples()
let tuple: string | undefined

// see if the internal host/port/protocol tuple has been mapped externally
if ((tuples[0][0] === CODEC_IP4 || tuples[0][0] === CODEC_IP6) && tuples[1][0] === CODEC_TCP) {
tuple = `${tuples[0][1]}-${tuples[1][1]}-tcp`
} else if ((tuples[0][0] === CODEC_IP4 || tuples[0][0] === CODEC_IP6) && tuples[1][0] === CODEC_UDP) {
tuple = `${tuples[0][1]}-${tuples[1][1]}-udp`
}

Check warning on line 262 in packages/libp2p/src/address-manager.ts

View check run for this annotation

Codecov / codecov/patch

packages/libp2p/src/address-manager.ts#L261-L262

Added lines #L261 - L262 were not covered by tests

if (tuple == null) {
return
}

const mappings = this.publicAddressMappings.get(tuple)

if (mappings == null) {
return
}

mappings.forEach(mapping => {
tuples[0][1] = mapping.externalIp
tuples[1][1] = `${mapping.externalPort}`

ipMappedMultiaddrs.push(
multiaddr(`/${
tuples.map(tuple => {
return [
protocols(tuple[0]).name,
tuple[1]
].join('/')
}).join('/')
}`)
)
})
})
multiaddrs = multiaddrs.concat(ipMappedMultiaddrs)

// add ip->domain mappings
const dnsMappedMultiaddrs: Multiaddr[] = []
for (const ma of multiaddrs) {
const tuples = [...ma.stringTuples()]
const tuples = ma.stringTuples()
let mappedIp = false

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

if (mappedIp) {
mappedMultiaddrs.push(
dnsMappedMultiaddrs.push(
multiaddr(`/${
tuples.map(tuple => {
return [
Expand All @@ -279,8 +328,7 @@ export class AddressManager implements AddressManagerInterface {
)
}
}

multiaddrs = multiaddrs.concat(mappedMultiaddrs)
multiaddrs = multiaddrs.concat(dnsMappedMultiaddrs)

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

addDNSMapping (domain: string, addresses: string[]): void {
addresses.forEach(ip => {
this.log('add DNS mapping %s to %s', ip, domain)
this.ipDomainMappings.set(ip, domain)
})
}

removeDNSMapping (domain: string): void {
for (const [key, value] of this.ipDomainMappings.entries()) {
if (value === domain) {
this.log('remove DNS mapping for %s', domain)
this.ipDomainMappings.delete(key)
}
}
}

addPublicAddressMapping (internalIp: string, internalPort: number, externalIp: string, externalPort: number = internalPort, protocol: 'tcp' | 'udp' = 'tcp'): void {
const key = `${internalIp}-${internalPort}-${protocol}`
const mappings = this.publicAddressMappings.get(key) ?? []
mappings.push({
externalIp,
externalPort
})

this.publicAddressMappings.set(key, mappings)
}

removePublicAddressMapping (internalIp: string, internalPort: number, externalIp: string, externalPort: number = internalPort, protocol: 'tcp' | 'udp' = 'tcp'): void {
const key = `${internalIp}-${internalPort}-${protocol}`
const mappings = (this.publicAddressMappings.get(key) ?? []).filter(mapping => {
return mapping.externalIp !== externalIp && mapping.externalPort !== externalPort
})

if (mappings.length === 0) {
this.publicAddressMappings.delete(key)
} else {
this.publicAddressMappings.set(key, mappings)
}

Check warning on line 404 in packages/libp2p/src/address-manager.ts

View check run for this annotation

Codecov / codecov/patch

packages/libp2p/src/address-manager.ts#L403-L404

Added lines #L403 - L404 were not covered by tests
}
}
78 changes: 78 additions & 0 deletions packages/libp2p/test/addresses/address-manager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,4 +349,82 @@ describe('Address Manager', () => {

expect(am.getAddresses()).to.deep.equal([externalAddress.encapsulate(`/p2p/${peerId.toString()}`)])
})

it('should add a public IPv4 address mapping', () => {
const transportManager = stubInterface<TransportManager>()
const am = new AddressManager({
peerId,
transportManager,
peerStore,
events,
logger: defaultLogger()
})

const internalIp = '192.168.1.123'
const internalPort = 4567
const externalIp = '81.12.12.1'
const externalPort = 8910
const protocol = 'tcp'

am.addPublicAddressMapping(internalIp, internalPort, externalIp, externalPort, protocol)

// one loopback, one LAN address
transportManager.getAddrs.returns([
multiaddr('/ip4/127.0.0.1/tcp/1234'),
multiaddr(`/ip4/${internalIp}/${protocol}/${internalPort}`)
])

// should have mapped the LAN address to the external IP
expect(am.getAddresses()).to.deep.equal([
multiaddr(`/ip4/127.0.0.1/tcp/1234/p2p/${peerId.toString()}`),
multiaddr(`/ip4/${internalIp}/${protocol}/${internalPort}/p2p/${peerId.toString()}`),
multiaddr(`/ip4/${externalIp}/${protocol}/${externalPort}/p2p/${peerId.toString()}`)
])

am.removePublicAddressMapping(internalIp, internalPort, externalIp, externalPort, protocol)

expect(am.getAddresses()).to.deep.equal([
multiaddr(`/ip4/127.0.0.1/tcp/1234/p2p/${peerId.toString()}`),
multiaddr(`/ip4/${internalIp}/${protocol}/${internalPort}/p2p/${peerId.toString()}`)
])
})

it('should add a public IPv6 address mapping', () => {
const transportManager = stubInterface<TransportManager>()
const am = new AddressManager({
peerId,
transportManager,
peerStore,
events,
logger: defaultLogger()
})

const internalIp = 'fd9b:ec6c:a487:efd2:14bc:d40:b478:9555'
const internalPort = 4567
const externalIp = '2a00:23c6:14b1:7e00:28b8:30d:944e:27f3'
const externalPort = 8910
const protocol = 'tcp'

am.addPublicAddressMapping(internalIp, internalPort, externalIp, externalPort, protocol)

// one loopback, one LAN address
transportManager.getAddrs.returns([
multiaddr('/ip6/::1/tcp/1234'),
multiaddr(`/ip6/${internalIp}/${protocol}/${internalPort}`)
])

// should have mapped the LAN address to the external IP
expect(am.getAddresses()).to.deep.equal([
multiaddr(`/ip6/::1/tcp/1234/p2p/${peerId.toString()}`),
multiaddr(`/ip6/${internalIp}/${protocol}/${internalPort}/p2p/${peerId.toString()}`),
multiaddr(`/ip6/${externalIp}/${protocol}/${externalPort}/p2p/${peerId.toString()}`)
])

am.removePublicAddressMapping(internalIp, internalPort, externalIp, externalPort, protocol)

expect(am.getAddresses()).to.deep.equal([
multiaddr(`/ip6/::1/tcp/1234/p2p/${peerId.toString()}`),
multiaddr(`/ip6/${internalIp}/${protocol}/${internalPort}/p2p/${peerId.toString()}`)
])
})
})

0 comments on commit 0d68b06

Please sign in to comment.