@libp2p/upnp-nat 0.0.0
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 +4 -0
- package/README.md +33 -0
- package/dist/src/index.d.ts +45 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +7 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/upnp-nat.d.ts +31 -0
- package/dist/src/upnp-nat.d.ts.map +1 -0
- package/dist/src/upnp-nat.js +129 -0
- package/dist/src/upnp-nat.js.map +1 -0
- package/package.json +62 -0
- package/src/index.ts +95 -0
- package/src/upnp-nat.ts +158 -0
package/LICENSE
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[](http://libp2p.io/)
|
|
2
|
+
[](https://discuss.libp2p.io)
|
|
3
|
+
[](https://codecov.io/gh/libp2p/js-libp2p)
|
|
4
|
+
[](https://github.com/libp2p/js-libp2p/actions/workflows/main.yml?query=branch%3Amaster)
|
|
5
|
+
|
|
6
|
+
# Install
|
|
7
|
+
|
|
8
|
+
```console
|
|
9
|
+
$ npm i @libp2p/peer-id
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Browser `<script>` tag
|
|
13
|
+
|
|
14
|
+
Loading this module through a script tag will make it's exports available as `Libp2pPeerId` in the global namespace.
|
|
15
|
+
|
|
16
|
+
```html
|
|
17
|
+
<script src="https://unpkg.com/@libp2p/peer-id/dist/index.min.js"></script>
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
# API Docs
|
|
21
|
+
|
|
22
|
+
- <https://libp2p.github.io/js-libp2p/modules/_libp2p_peer_id.html>
|
|
23
|
+
|
|
24
|
+
# License
|
|
25
|
+
|
|
26
|
+
Licensed under either of
|
|
27
|
+
|
|
28
|
+
- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / <http://www.apache.org/licenses/LICENSE-2.0>)
|
|
29
|
+
- MIT ([LICENSE-MIT](LICENSE-MIT) / <http://opensource.org/licenses/MIT>)
|
|
30
|
+
|
|
31
|
+
# Contribution
|
|
32
|
+
|
|
33
|
+
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,45 @@
|
|
|
1
|
+
import type { ComponentLogger, NodeInfo } from '@libp2p/interface';
|
|
2
|
+
import type { PeerId } from '@libp2p/interface/peer-id';
|
|
3
|
+
import type { AddressManager } from '@libp2p/interface-internal/address-manager';
|
|
4
|
+
import type { TransportManager } from '@libp2p/interface-internal/transport-manager';
|
|
5
|
+
export interface PMPOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Whether to enable PMP as well as UPnP
|
|
8
|
+
*/
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface UPnPNATInit {
|
|
12
|
+
/**
|
|
13
|
+
* Pass a value to use instead of auto-detection
|
|
14
|
+
*/
|
|
15
|
+
externalAddress?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Pass a value to use instead of auto-detection
|
|
18
|
+
*/
|
|
19
|
+
localAddress?: string;
|
|
20
|
+
/**
|
|
21
|
+
* A string value to use for the port mapping description on the gateway
|
|
22
|
+
*/
|
|
23
|
+
description?: string;
|
|
24
|
+
/**
|
|
25
|
+
* How long UPnP port mappings should last for in seconds (minimum 1200)
|
|
26
|
+
*/
|
|
27
|
+
ttl?: number;
|
|
28
|
+
/**
|
|
29
|
+
* Whether to automatically refresh UPnP port mappings when their TTL is reached
|
|
30
|
+
*/
|
|
31
|
+
keepAlive?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Pass a value to use instead of auto-detection
|
|
34
|
+
*/
|
|
35
|
+
gateway?: string;
|
|
36
|
+
}
|
|
37
|
+
export interface UPnPNATComponents {
|
|
38
|
+
peerId: PeerId;
|
|
39
|
+
nodeInfo: NodeInfo;
|
|
40
|
+
logger: ComponentLogger;
|
|
41
|
+
transportManager: TransportManager;
|
|
42
|
+
addressManager: AddressManager;
|
|
43
|
+
}
|
|
44
|
+
export declare function uPnPNATService(init?: UPnPNATInit): (components: UPnPNATComponents) => unknown;
|
|
45
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AACA,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,cAAc,CAAE,IAAI,GAAE,WAAgB,GAAG,CAAC,UAAU,EAAE,iBAAiB,KAAK,OAAO,CAIlG"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AAqDvC,MAAM,UAAU,cAAc,CAAE,OAAoB,EAAE;IACpD,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;
|
|
6
|
+
private readonly components;
|
|
7
|
+
private readonly externalAddress?;
|
|
8
|
+
private readonly localAddress?;
|
|
9
|
+
private readonly description;
|
|
10
|
+
private readonly ttl;
|
|
11
|
+
private readonly keepAlive;
|
|
12
|
+
private readonly gateway?;
|
|
13
|
+
private started;
|
|
14
|
+
private client?;
|
|
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;gBAGV,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",
|
|
4
|
+
"description": "UPnP NAT hole punching",
|
|
5
|
+
"license": "Apache-2.0 OR MIT",
|
|
6
|
+
"homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/peer-id#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",
|
|
50
|
+
"@libp2p/interface-internal": "^0.1.9",
|
|
51
|
+
"@libp2p/utils": "^4.0.7",
|
|
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",
|
|
58
|
+
"@libp2p/peer-id-factory": "^3.0.8",
|
|
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
|
+
}
|
package/src/upnp-nat.ts
ADDED
|
@@ -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
|
+
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
|
+
}
|