diff --git a/packages/interface-internal/src/address-manager/index.ts b/packages/interface-internal/src/address-manager/index.ts index 9d055d5f65..34cb48825e 100644 --- a/packages/interface-internal/src/address-manager/index.ts +++ b/packages/interface-internal/src/address-manager/index.ts @@ -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 } diff --git a/packages/libp2p/src/address-manager.ts b/packages/libp2p/src/address-manager.ts index 37ae0c0aaa..15457709b7 100644 --- a/packages/libp2p/src/address-manager.ts +++ b/packages/libp2p/src/address-manager.ts @@ -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 @@ -89,6 +96,7 @@ export class AddressManager implements AddressManagerInterface { private readonly observed: Map private readonly announceFilter: AddressFilter private readonly ipDomainMappings: Map + private readonly publicAddressMappings: Map /** * Responsible for managing the peer addresses. @@ -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 @@ -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` + } + + 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()) { @@ -267,7 +316,7 @@ export class AddressManager implements AddressManagerInterface { } if (mappedIp) { - mappedMultiaddrs.push( + dnsMappedMultiaddrs.push( multiaddr(`/${ tuples.map(tuple => { return [ @@ -279,8 +328,7 @@ export class AddressManager implements AddressManagerInterface { ) } } - - multiaddrs = multiaddrs.concat(mappedMultiaddrs) + multiaddrs = multiaddrs.concat(dnsMappedMultiaddrs) // dedupe multiaddrs const addrSet = new Set() @@ -318,6 +366,7 @@ 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) }) } @@ -325,8 +374,33 @@ export class AddressManager implements AddressManagerInterface { 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) + } + } } diff --git a/packages/libp2p/test/addresses/address-manager.spec.ts b/packages/libp2p/test/addresses/address-manager.spec.ts index f8a425a1c6..a7e1e61216 100644 --- a/packages/libp2p/test/addresses/address-manager.spec.ts +++ b/packages/libp2p/test/addresses/address-manager.spec.ts @@ -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() + 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() + 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()}`) + ]) + }) })