@libp2p/upnp-nat 0.0.0-97ab31c0c

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,4 @@
1
+ This project is dual licensed under MIT and Apache-2.0.
2
+
3
+ MIT: https://www.opensource.org/licenses/mit
4
+ Apache-2.0: https://www.apache.org/licenses/license-2.0
package/README.md ADDED
@@ -0,0 +1,62 @@
1
+ [![libp2p.io](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/)
2
+ [![Discuss](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg?style=flat-square)](https://discuss.libp2p.io)
3
+ [![codecov](https://img.shields.io/codecov/c/github/libp2p/js-libp2p.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p)
4
+ [![CI](https://img.shields.io/github/actions/workflow/status/libp2p/js-libp2p/main.yml?branch=master\&style=flat-square)](https://github.com/libp2p/js-libp2p/actions/workflows/main.yml?query=branch%3Amaster)
5
+
6
+ > UPnP NAT hole punching
7
+
8
+ # About
9
+
10
+ The service exported by this module attempts to configure NAT hole punching
11
+ via UPnP.
12
+
13
+ This will make your node publicly accessible from the internet.
14
+
15
+ For this to work there are some prerequisites:
16
+
17
+ 1. Your router must have UPnP support enabled
18
+ 2. Your libp2p node must be listening on a non-loopback IPv4 address
19
+ 3. You must not be [double-NATed](https://kb.netgear.com/30186/What-is-double-NAT-and-why-is-it-bad) by your ISP
20
+
21
+ ## Example
22
+
23
+ ```typescript
24
+ import { createLibp2p } from 'libp2p'
25
+ import { tcp } from '@libp2p/tcp'
26
+ import { uPnPNAT } from '@libp2p/upnp-nat'
27
+
28
+ const node = await createLibp2p({
29
+ addresses: [
30
+ listen: [
31
+ '/ip4/0.0.0.0/tcp/0'
32
+ ]
33
+ ],
34
+ transports: [
35
+ tcp()
36
+ ],
37
+ services: {
38
+ upnpNAT: uPnPNAT()
39
+ }
40
+ })
41
+ ```
42
+
43
+ # Install
44
+
45
+ ```console
46
+ $ npm i @libp2p/upnp-nat
47
+ ```
48
+
49
+ # API Docs
50
+
51
+ - <https://libp2p.github.io/js-libp2p/modules/_libp2p_upnp_nat.html>
52
+
53
+ # License
54
+
55
+ Licensed under either of
56
+
57
+ - Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / <http://www.apache.org/licenses/LICENSE-2.0>)
58
+ - MIT ([LICENSE-MIT](LICENSE-MIT) / <http://opensource.org/licenses/MIT>)
59
+
60
+ # Contribution
61
+
62
+ Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
@@ -0,0 +1,81 @@
1
+ /**
2
+ * @packageDocumentation
3
+ *
4
+ * The service exported by this module attempts to configure NAT hole punching
5
+ * via UPnP.
6
+ *
7
+ * This will make your node publicly accessible from the internet.
8
+ *
9
+ * For this to work there are some prerequisites:
10
+ *
11
+ * 1. Your router must have UPnP support enabled
12
+ * 2. Your libp2p node must be listening on a non-loopback IPv4 address
13
+ * 3. You must not be [double-NATed](https://kb.netgear.com/30186/What-is-double-NAT-and-why-is-it-bad) by your ISP
14
+ *
15
+ * @example
16
+ *
17
+ * ```typescript
18
+ * import { createLibp2p } from 'libp2p'
19
+ * import { tcp } from '@libp2p/tcp'
20
+ * import { uPnPNAT } from '@libp2p/upnp-nat'
21
+ *
22
+ * const node = await createLibp2p({
23
+ * addresses: [
24
+ * listen: [
25
+ * '/ip4/0.0.0.0/tcp/0'
26
+ * ]
27
+ * ],
28
+ * transports: [
29
+ * tcp()
30
+ * ],
31
+ * services: {
32
+ * upnpNAT: uPnPNAT()
33
+ * }
34
+ * })
35
+ * ```
36
+ */
37
+ import type { ComponentLogger, NodeInfo } from '@libp2p/interface';
38
+ import type { PeerId } from '@libp2p/interface/peer-id';
39
+ import type { AddressManager } from '@libp2p/interface-internal/address-manager';
40
+ import type { TransportManager } from '@libp2p/interface-internal/transport-manager';
41
+ export interface PMPOptions {
42
+ /**
43
+ * Whether to enable PMP as well as UPnP
44
+ */
45
+ enabled?: boolean;
46
+ }
47
+ export interface UPnPNATInit {
48
+ /**
49
+ * Pass a value to use instead of auto-detection
50
+ */
51
+ externalAddress?: string;
52
+ /**
53
+ * Pass a value to use instead of auto-detection
54
+ */
55
+ localAddress?: string;
56
+ /**
57
+ * A string value to use for the port mapping description on the gateway
58
+ */
59
+ description?: string;
60
+ /**
61
+ * How long UPnP port mappings should last for in seconds (minimum 1200)
62
+ */
63
+ ttl?: number;
64
+ /**
65
+ * Whether to automatically refresh UPnP port mappings when their TTL is reached
66
+ */
67
+ keepAlive?: boolean;
68
+ /**
69
+ * Pass a value to use instead of auto-detection
70
+ */
71
+ gateway?: string;
72
+ }
73
+ export interface UPnPNATComponents {
74
+ peerId: PeerId;
75
+ nodeInfo: NodeInfo;
76
+ logger: ComponentLogger;
77
+ transportManager: TransportManager;
78
+ addressManager: AddressManager;
79
+ }
80
+ export declare function uPnPNAT(init?: UPnPNATInit): (components: UPnPNATComponents) => unknown;
81
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAClE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,2BAA2B,CAAA;AACvD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4CAA4C,CAAA;AAChF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8CAA8C,CAAA;AAEpF,MAAM,WAAW,UAAU;IACzB;;OAEG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B;;OAEG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;IAExB;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IAErB;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IAEpB;;OAEG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IAEZ;;OAEG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IAEnB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,QAAQ,CAAA;IAClB,MAAM,EAAE,eAAe,CAAA;IACvB,gBAAgB,EAAE,gBAAgB,CAAA;IAClC,cAAc,EAAE,cAAc,CAAA;CAC/B;AAED,wBAAgB,OAAO,CAAE,IAAI,GAAE,WAAgB,GAAG,CAAC,UAAU,EAAE,iBAAiB,KAAK,OAAO,CAI3F"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * @packageDocumentation
3
+ *
4
+ * The service exported by this module attempts to configure NAT hole punching
5
+ * via UPnP.
6
+ *
7
+ * This will make your node publicly accessible from the internet.
8
+ *
9
+ * For this to work there are some prerequisites:
10
+ *
11
+ * 1. Your router must have UPnP support enabled
12
+ * 2. Your libp2p node must be listening on a non-loopback IPv4 address
13
+ * 3. You must not be [double-NATed](https://kb.netgear.com/30186/What-is-double-NAT-and-why-is-it-bad) by your ISP
14
+ *
15
+ * @example
16
+ *
17
+ * ```typescript
18
+ * import { createLibp2p } from 'libp2p'
19
+ * import { tcp } from '@libp2p/tcp'
20
+ * import { uPnPNAT } from '@libp2p/upnp-nat'
21
+ *
22
+ * const node = await createLibp2p({
23
+ * addresses: [
24
+ * listen: [
25
+ * '/ip4/0.0.0.0/tcp/0'
26
+ * ]
27
+ * ],
28
+ * transports: [
29
+ * tcp()
30
+ * ],
31
+ * services: {
32
+ * upnpNAT: uPnPNAT()
33
+ * }
34
+ * })
35
+ * ```
36
+ */
37
+ import { UPnPNAT } from './upnp-nat.js';
38
+ export function uPnPNAT(init = {}) {
39
+ return (components) => {
40
+ return new UPnPNAT(components, init);
41
+ };
42
+ }
43
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AAqDvC,MAAM,UAAU,OAAO,CAAE,OAAoB,EAAE;IAC7C,OAAO,CAAC,UAA6B,EAAE,EAAE;QACvC,OAAO,IAAI,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,CAAA;IACtC,CAAC,CAAA;AACH,CAAC"}
@@ -0,0 +1,31 @@
1
+ import { type NatAPI } from '@achingbrain/nat-port-mapper';
2
+ import type { UPnPNATComponents, UPnPNATInit } from './index.js';
3
+ import type { Startable } from '@libp2p/interface/startable';
4
+ export declare class UPnPNAT implements Startable {
5
+ private readonly components;
6
+ private readonly externalAddress?;
7
+ private readonly localAddress?;
8
+ private readonly description;
9
+ private readonly ttl;
10
+ private readonly keepAlive;
11
+ private readonly gateway?;
12
+ private started;
13
+ private client?;
14
+ private readonly log;
15
+ constructor(components: UPnPNATComponents, init: UPnPNATInit);
16
+ isStarted(): boolean;
17
+ start(): void;
18
+ /**
19
+ * Attempt to use uPnP to configure port mapping using the current gateway.
20
+ *
21
+ * Run after start to ensure the transport manager has all addresses configured.
22
+ */
23
+ afterStart(): void;
24
+ mapIpAddresses(): Promise<void>;
25
+ _getClient(): NatAPI;
26
+ /**
27
+ * Stops the NAT manager
28
+ */
29
+ stop(): Promise<void>;
30
+ }
31
+ //# sourceMappingURL=upnp-nat.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upnp-nat.d.ts","sourceRoot":"","sources":["../../src/upnp-nat.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,8BAA8B,CAAA;AAMnE,OAAO,KAAK,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAEhE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAA;AAQ5D,qBAAa,OAAQ,YAAW,SAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAmB;IAC9C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAQ;IACzC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAQ;IACtC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAQ;IACpC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAQ;IAC5B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAQ;IACjC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,MAAM,CAAC,CAAQ;IACvB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAQ;gBAEf,UAAU,EAAE,iBAAiB,EAAE,IAAI,EAAE,WAAW;IAiB7D,SAAS,IAAK,OAAO;IAIrB,KAAK,IAAK,IAAI;IAId;;;;OAIG;IACH,UAAU,IAAK,IAAI;IAcb,cAAc,IAAK,OAAO,CAAC,IAAI,CAAC;IAuDtC,UAAU,IAAK,MAAM;IAerB;;OAEG;IACG,IAAI,IAAK,OAAO,CAAC,IAAI,CAAC;CAY7B"}
@@ -0,0 +1,129 @@
1
+ import { upnpNat } from '@achingbrain/nat-port-mapper';
2
+ import { CodeError, ERR_INVALID_PARAMETERS } from '@libp2p/interface/errors';
3
+ import { isLoopback } from '@libp2p/utils/multiaddr/is-loopback';
4
+ import { fromNodeAddress } from '@multiformats/multiaddr';
5
+ import isPrivateIp from 'private-ip';
6
+ import { isBrowser } from 'wherearewe';
7
+ const DEFAULT_TTL = 7200;
8
+ function highPort(min = 1024, max = 65535) {
9
+ return Math.floor(Math.random() * (max - min + 1) + min);
10
+ }
11
+ export class UPnPNAT {
12
+ components;
13
+ externalAddress;
14
+ localAddress;
15
+ description;
16
+ ttl;
17
+ keepAlive;
18
+ gateway;
19
+ started;
20
+ client;
21
+ log;
22
+ constructor(components, init) {
23
+ this.components = components;
24
+ this.log = components.logger.forComponent('libp2p:upnp-nat');
25
+ this.started = false;
26
+ this.externalAddress = init.externalAddress;
27
+ this.localAddress = init.localAddress;
28
+ this.description = init.description ?? `${components.nodeInfo.name}@${components.nodeInfo.version} ${this.components.peerId.toString()}`;
29
+ this.ttl = init.ttl ?? DEFAULT_TTL;
30
+ this.keepAlive = init.keepAlive ?? true;
31
+ this.gateway = init.gateway;
32
+ if (this.ttl < DEFAULT_TTL) {
33
+ throw new CodeError(`NatManager ttl should be at least ${DEFAULT_TTL} seconds`, ERR_INVALID_PARAMETERS);
34
+ }
35
+ }
36
+ isStarted() {
37
+ return this.started;
38
+ }
39
+ start() {
40
+ // #TODO: is there a way to remove this? Seems like a hack
41
+ }
42
+ /**
43
+ * Attempt to use uPnP to configure port mapping using the current gateway.
44
+ *
45
+ * Run after start to ensure the transport manager has all addresses configured.
46
+ */
47
+ afterStart() {
48
+ if (isBrowser || this.started) {
49
+ return;
50
+ }
51
+ this.started = true;
52
+ // done async to not slow down startup
53
+ void this.mapIpAddresses().catch((err) => {
54
+ // hole punching errors are non-fatal
55
+ this.log.error(err);
56
+ });
57
+ }
58
+ async mapIpAddresses() {
59
+ const addrs = this.components.transportManager.getAddrs();
60
+ for (const addr of addrs) {
61
+ // try to open uPnP ports for each thin waist address
62
+ const { family, host, port, transport } = addr.toOptions();
63
+ if (!addr.isThinWaistAddress() || transport !== 'tcp') {
64
+ // only bare tcp addresses
65
+ // eslint-disable-next-line no-continue
66
+ continue;
67
+ }
68
+ if (isLoopback(addr)) {
69
+ // eslint-disable-next-line no-continue
70
+ continue;
71
+ }
72
+ if (family !== 4) {
73
+ // ignore ipv6
74
+ // eslint-disable-next-line no-continue
75
+ continue;
76
+ }
77
+ const client = this._getClient();
78
+ const publicIp = this.externalAddress ?? await client.externalIp();
79
+ const isPrivate = isPrivateIp(publicIp);
80
+ if (isPrivate === true) {
81
+ throw new CodeError(`${publicIp} is private - please set config.nat.externalIp to an externally routable IP or ensure you are not behind a double NAT`, 'ERR_DOUBLE_NAT');
82
+ }
83
+ if (isPrivate == null) {
84
+ throw new CodeError(`${publicIp} is not an IP address`, ERR_INVALID_PARAMETERS);
85
+ }
86
+ const publicPort = highPort();
87
+ this.log(`opening uPnP connection from ${publicIp}:${publicPort} to ${host}:${port}`);
88
+ await client.map({
89
+ publicPort,
90
+ localPort: port,
91
+ localAddress: this.localAddress,
92
+ protocol: transport.toUpperCase() === 'TCP' ? 'TCP' : 'UDP'
93
+ });
94
+ this.components.addressManager.addObservedAddr(fromNodeAddress({
95
+ family: 4,
96
+ address: publicIp,
97
+ port: publicPort
98
+ }, transport));
99
+ }
100
+ }
101
+ _getClient() {
102
+ if (this.client != null) {
103
+ return this.client;
104
+ }
105
+ this.client = upnpNat({
106
+ description: this.description,
107
+ ttl: this.ttl,
108
+ keepAlive: this.keepAlive,
109
+ gateway: this.gateway
110
+ });
111
+ return this.client;
112
+ }
113
+ /**
114
+ * Stops the NAT manager
115
+ */
116
+ async stop() {
117
+ if (isBrowser || this.client == null) {
118
+ return;
119
+ }
120
+ try {
121
+ await this.client.close();
122
+ this.client = undefined;
123
+ }
124
+ catch (err) {
125
+ this.log.error(err);
126
+ }
127
+ }
128
+ }
129
+ //# sourceMappingURL=upnp-nat.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upnp-nat.js","sourceRoot":"","sources":["../../src/upnp-nat.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@libp2p/upnp-nat",
3
+ "version": "0.0.0-97ab31c0c",
4
+ "description": "UPnP NAT hole punching",
5
+ "license": "Apache-2.0 OR MIT",
6
+ "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/upnp-nat#readme",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/libp2p/js-libp2p.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/libp2p/js-libp2p/issues"
13
+ },
14
+ "keywords": [
15
+ "IPFS"
16
+ ],
17
+ "type": "module",
18
+ "types": "./dist/src/index.d.ts",
19
+ "files": [
20
+ "src",
21
+ "dist",
22
+ "!dist/test",
23
+ "!**/*.tsbuildinfo"
24
+ ],
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/src/index.d.ts",
28
+ "import": "./dist/src/index.js"
29
+ }
30
+ },
31
+ "eslintConfig": {
32
+ "extends": "ipfs",
33
+ "parserOptions": {
34
+ "project": true,
35
+ "sourceType": "module"
36
+ }
37
+ },
38
+ "scripts": {
39
+ "clean": "aegir clean",
40
+ "lint": "aegir lint",
41
+ "dep-check": "aegir dep-check",
42
+ "build": "aegir build --no-bundle",
43
+ "test": "aegir test -t node -t electron-main",
44
+ "test:node": "aegir test -t node --cov",
45
+ "test:electron-main": "aegir test -t electron-main"
46
+ },
47
+ "dependencies": {
48
+ "@achingbrain/nat-port-mapper": "^1.0.12",
49
+ "@libp2p/interface": "0.1.6-97ab31c0c",
50
+ "@libp2p/interface-internal": "0.1.9-97ab31c0c",
51
+ "@libp2p/utils": "4.0.7-97ab31c0c",
52
+ "@multiformats/multiaddr": "^12.1.10",
53
+ "private-ip": "^3.0.1",
54
+ "wherearewe": "^2.0.1"
55
+ },
56
+ "devDependencies": {
57
+ "@libp2p/logger": "3.1.0-97ab31c0c",
58
+ "@libp2p/peer-id-factory": "3.0.8-97ab31c0c",
59
+ "aegir": "^41.0.2",
60
+ "sinon-ts": "^2.0.0"
61
+ }
62
+ }
package/src/index.ts ADDED
@@ -0,0 +1,95 @@
1
+ /**
2
+ * @packageDocumentation
3
+ *
4
+ * The service exported by this module attempts to configure NAT hole punching
5
+ * via UPnP.
6
+ *
7
+ * This will make your node publicly accessible from the internet.
8
+ *
9
+ * For this to work there are some prerequisites:
10
+ *
11
+ * 1. Your router must have UPnP support enabled
12
+ * 2. Your libp2p node must be listening on a non-loopback IPv4 address
13
+ * 3. You must not be [double-NATed](https://kb.netgear.com/30186/What-is-double-NAT-and-why-is-it-bad) by your ISP
14
+ *
15
+ * @example
16
+ *
17
+ * ```typescript
18
+ * import { createLibp2p } from 'libp2p'
19
+ * import { tcp } from '@libp2p/tcp'
20
+ * import { uPnPNAT } from '@libp2p/upnp-nat'
21
+ *
22
+ * const node = await createLibp2p({
23
+ * addresses: [
24
+ * listen: [
25
+ * '/ip4/0.0.0.0/tcp/0'
26
+ * ]
27
+ * ],
28
+ * transports: [
29
+ * tcp()
30
+ * ],
31
+ * services: {
32
+ * upnpNAT: uPnPNAT()
33
+ * }
34
+ * })
35
+ * ```
36
+ */
37
+
38
+ import { UPnPNAT } from './upnp-nat.js'
39
+ import type { ComponentLogger, NodeInfo } from '@libp2p/interface'
40
+ import type { PeerId } from '@libp2p/interface/peer-id'
41
+ import type { AddressManager } from '@libp2p/interface-internal/address-manager'
42
+ import type { TransportManager } from '@libp2p/interface-internal/transport-manager'
43
+
44
+ export interface PMPOptions {
45
+ /**
46
+ * Whether to enable PMP as well as UPnP
47
+ */
48
+ enabled?: boolean
49
+ }
50
+
51
+ export interface UPnPNATInit {
52
+ /**
53
+ * Pass a value to use instead of auto-detection
54
+ */
55
+ externalAddress?: string
56
+
57
+ /**
58
+ * Pass a value to use instead of auto-detection
59
+ */
60
+ localAddress?: string
61
+
62
+ /**
63
+ * A string value to use for the port mapping description on the gateway
64
+ */
65
+ description?: string
66
+
67
+ /**
68
+ * How long UPnP port mappings should last for in seconds (minimum 1200)
69
+ */
70
+ ttl?: number
71
+
72
+ /**
73
+ * Whether to automatically refresh UPnP port mappings when their TTL is reached
74
+ */
75
+ keepAlive?: boolean
76
+
77
+ /**
78
+ * Pass a value to use instead of auto-detection
79
+ */
80
+ gateway?: string
81
+ }
82
+
83
+ export interface UPnPNATComponents {
84
+ peerId: PeerId
85
+ nodeInfo: NodeInfo
86
+ logger: ComponentLogger
87
+ transportManager: TransportManager
88
+ addressManager: AddressManager
89
+ }
90
+
91
+ export function uPnPNAT (init: UPnPNATInit = {}): (components: UPnPNATComponents) => unknown {
92
+ return (components: UPnPNATComponents) => {
93
+ return new UPnPNAT(components, init)
94
+ }
95
+ }
@@ -0,0 +1,158 @@
1
+ import { upnpNat, type NatAPI } from '@achingbrain/nat-port-mapper'
2
+ import { CodeError, ERR_INVALID_PARAMETERS } from '@libp2p/interface/errors'
3
+ import { isLoopback } from '@libp2p/utils/multiaddr/is-loopback'
4
+ import { fromNodeAddress } from '@multiformats/multiaddr'
5
+ import isPrivateIp from 'private-ip'
6
+ import { isBrowser } from 'wherearewe'
7
+ import type { UPnPNATComponents, UPnPNATInit } from './index.js'
8
+ import type { Logger } from '@libp2p/interface'
9
+ import type { Startable } from '@libp2p/interface/startable'
10
+
11
+ const DEFAULT_TTL = 7200
12
+
13
+ function highPort (min = 1024, max = 65535): number {
14
+ return Math.floor(Math.random() * (max - min + 1) + min)
15
+ }
16
+
17
+ export class UPnPNAT implements Startable {
18
+ private readonly components: UPnPNATComponents
19
+ private readonly externalAddress?: string
20
+ private readonly localAddress?: string
21
+ private readonly description: string
22
+ private readonly ttl: number
23
+ private readonly keepAlive: boolean
24
+ private readonly gateway?: string
25
+ private started: boolean
26
+ private client?: NatAPI
27
+ private readonly log: Logger
28
+
29
+ constructor (components: UPnPNATComponents, init: UPnPNATInit) {
30
+ this.components = components
31
+
32
+ this.log = components.logger.forComponent('libp2p:upnp-nat')
33
+ this.started = false
34
+ this.externalAddress = init.externalAddress
35
+ this.localAddress = init.localAddress
36
+ this.description = init.description ?? `${components.nodeInfo.name}@${components.nodeInfo.version} ${this.components.peerId.toString()}`
37
+ this.ttl = init.ttl ?? DEFAULT_TTL
38
+ this.keepAlive = init.keepAlive ?? true
39
+ this.gateway = init.gateway
40
+
41
+ if (this.ttl < DEFAULT_TTL) {
42
+ throw new CodeError(`NatManager ttl should be at least ${DEFAULT_TTL} seconds`, ERR_INVALID_PARAMETERS)
43
+ }
44
+ }
45
+
46
+ isStarted (): boolean {
47
+ return this.started
48
+ }
49
+
50
+ start (): void {
51
+ // #TODO: is there a way to remove this? Seems like a hack
52
+ }
53
+
54
+ /**
55
+ * Attempt to use uPnP to configure port mapping using the current gateway.
56
+ *
57
+ * Run after start to ensure the transport manager has all addresses configured.
58
+ */
59
+ afterStart (): void {
60
+ if (isBrowser || this.started) {
61
+ return
62
+ }
63
+
64
+ this.started = true
65
+
66
+ // done async to not slow down startup
67
+ void this.mapIpAddresses().catch((err) => {
68
+ // hole punching errors are non-fatal
69
+ this.log.error(err)
70
+ })
71
+ }
72
+
73
+ async mapIpAddresses (): Promise<void> {
74
+ const addrs = this.components.transportManager.getAddrs()
75
+
76
+ for (const addr of addrs) {
77
+ // try to open uPnP ports for each thin waist address
78
+ const { family, host, port, transport } = addr.toOptions()
79
+
80
+ if (!addr.isThinWaistAddress() || transport !== 'tcp') {
81
+ // only bare tcp addresses
82
+ // eslint-disable-next-line no-continue
83
+ continue
84
+ }
85
+
86
+ if (isLoopback(addr)) {
87
+ // eslint-disable-next-line no-continue
88
+ continue
89
+ }
90
+
91
+ if (family !== 4) {
92
+ // ignore ipv6
93
+ // eslint-disable-next-line no-continue
94
+ continue
95
+ }
96
+
97
+ const client = this._getClient()
98
+ const publicIp = this.externalAddress ?? await client.externalIp()
99
+ const isPrivate = isPrivateIp(publicIp)
100
+
101
+ if (isPrivate === true) {
102
+ throw new CodeError(`${publicIp} is private - please set config.nat.externalIp to an externally routable IP or ensure you are not behind a double NAT`, 'ERR_DOUBLE_NAT')
103
+ }
104
+
105
+ if (isPrivate == null) {
106
+ throw new CodeError(`${publicIp} is not an IP address`, ERR_INVALID_PARAMETERS)
107
+ }
108
+
109
+ const publicPort = highPort()
110
+
111
+ this.log(`opening uPnP connection from ${publicIp}:${publicPort} to ${host}:${port}`)
112
+
113
+ await client.map({
114
+ publicPort,
115
+ localPort: port,
116
+ localAddress: this.localAddress,
117
+ protocol: transport.toUpperCase() === 'TCP' ? 'TCP' : 'UDP'
118
+ })
119
+
120
+ this.components.addressManager.addObservedAddr(fromNodeAddress({
121
+ family: 4,
122
+ address: publicIp,
123
+ port: publicPort
124
+ }, transport))
125
+ }
126
+ }
127
+
128
+ _getClient (): NatAPI {
129
+ if (this.client != null) {
130
+ return this.client
131
+ }
132
+
133
+ this.client = upnpNat({
134
+ description: this.description,
135
+ ttl: this.ttl,
136
+ keepAlive: this.keepAlive,
137
+ gateway: this.gateway
138
+ })
139
+
140
+ return this.client
141
+ }
142
+
143
+ /**
144
+ * Stops the NAT manager
145
+ */
146
+ async stop (): Promise<void> {
147
+ if (isBrowser || this.client == null) {
148
+ return
149
+ }
150
+
151
+ try {
152
+ await this.client.close()
153
+ this.client = undefined
154
+ } catch (err: any) {
155
+ this.log.error(err)
156
+ }
157
+ }
158
+ }