@libp2p/upnp-nat 2.0.12 → 3.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.
@@ -0,0 +1,242 @@
1
+ import { isIPv4 } from '@chainsafe/is-ip'
2
+ import { InvalidParametersError, start, stop } from '@libp2p/interface'
3
+ import { isLinkLocal } from '@libp2p/utils/multiaddr/is-link-local'
4
+ import { isLoopback } from '@libp2p/utils/multiaddr/is-loopback'
5
+ import { isPrivate } from '@libp2p/utils/multiaddr/is-private'
6
+ import { isPrivateIp } from '@libp2p/utils/private-ip'
7
+ import { multiaddr } from '@multiformats/multiaddr'
8
+ import { QUICV1, TCP, WebSockets, WebSocketsSecure, WebTransport } from '@multiformats/multiaddr-matcher'
9
+ import { dynamicExternalAddress } from './check-external-address.js'
10
+ import { DoubleNATError } from './errors.js'
11
+ import type { ExternalAddress } from './check-external-address.js'
12
+ import type { Gateway } from '@achingbrain/nat-port-mapper'
13
+ import type { ComponentLogger, Logger } from '@libp2p/interface'
14
+ import type { AddressManager, NodeAddress } from '@libp2p/interface-internal'
15
+ import type { Multiaddr } from '@multiformats/multiaddr'
16
+
17
+ const MAX_DATE = 8_640_000_000_000_000
18
+
19
+ export interface UPnPPortMapperInit {
20
+ gateway: Gateway
21
+ externalAddressCheckInterval?: number
22
+ externalAddressCheckTimeout?: number
23
+ }
24
+
25
+ export interface UPnPPortMapperComponents {
26
+ logger: ComponentLogger
27
+ addressManager: AddressManager
28
+ }
29
+
30
+ interface PortMapping {
31
+ externalHost: string
32
+ externalPort: number
33
+ }
34
+
35
+ export interface MapPortsOptions {
36
+ autoConfirmAddress?: boolean
37
+ }
38
+
39
+ export class UPnPPortMapper {
40
+ private readonly gateway: Gateway
41
+ private readonly externalAddress: ExternalAddress
42
+ private readonly addressManager: AddressManager
43
+ private readonly log: Logger
44
+ private readonly mappedPorts: Map<string, PortMapping>
45
+ private started: boolean
46
+
47
+ constructor (components: UPnPPortMapperComponents, init: UPnPPortMapperInit) {
48
+ this.log = components.logger.forComponent(`libp2p:upnp-nat:gateway:${init.gateway.id}`)
49
+ this.addressManager = components.addressManager
50
+ this.gateway = init.gateway
51
+ this.externalAddress = dynamicExternalAddress({
52
+ gateway: this.gateway,
53
+ addressManager: this.addressManager,
54
+ logger: components.logger
55
+ }, {
56
+ interval: init.externalAddressCheckInterval,
57
+ timeout: init.externalAddressCheckTimeout,
58
+ onExternalAddressChange: this.remapPorts.bind(this)
59
+ })
60
+ this.gateway = init.gateway
61
+ this.mappedPorts = new Map()
62
+ this.started = false
63
+ }
64
+
65
+ async start (): Promise<void> {
66
+ if (this.started) {
67
+ return
68
+ }
69
+
70
+ await start(this.externalAddress)
71
+ this.started = true
72
+ }
73
+
74
+ async stop (): Promise<void> {
75
+ try {
76
+ const shutdownTimeout = AbortSignal.timeout(1000)
77
+
78
+ await this.gateway.stop({
79
+ signal: shutdownTimeout
80
+ })
81
+ } catch (err: any) {
82
+ this.log.error('error closing gateway - %e', err)
83
+ }
84
+
85
+ await stop(this.externalAddress)
86
+ this.started = false
87
+ }
88
+
89
+ /**
90
+ * Update the local address mappings when the gateway's external interface
91
+ * address changes
92
+ */
93
+ private remapPorts (newExternalHost: string): void {
94
+ for (const [key, { externalHost, externalPort }] of this.mappedPorts.entries()) {
95
+ const [
96
+ host,
97
+ port,
98
+ transport
99
+ ] = key.split('-')
100
+
101
+ this.addressManager.removePublicAddressMapping(host, parseInt(port), externalHost, externalPort, transport === 'tcp' ? 'tcp' : 'udp')
102
+ this.addressManager.addPublicAddressMapping(host, parseInt(port), newExternalHost, externalPort, transport === 'tcp' ? 'tcp' : 'udp')
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Return any eligible multiaddrs that are not mapped on the detected gateway
108
+ */
109
+ private getUnmappedAddresses (multiaddrs: NodeAddress[], publicAddresses: string[]): Multiaddr[] {
110
+ const output: Multiaddr[] = []
111
+
112
+ for (const { multiaddr: ma, type } of multiaddrs) {
113
+ // only consider transport addresses, ignore mapped/observed addrs
114
+ if (type !== 'transport') {
115
+ continue
116
+ }
117
+
118
+ const stringTuples = ma.stringTuples()
119
+ const address = `${stringTuples[0][1]}`
120
+
121
+ // ignore public IPv4 addresses
122
+ if (isIPv4(address) && !isPrivate(ma)) {
123
+ continue
124
+ }
125
+
126
+ // ignore any addresses that match the interface on the network gateway
127
+ if (publicAddresses.includes(address)) {
128
+ continue
129
+ }
130
+
131
+ // ignore loopback
132
+ if (isLoopback(ma)) {
133
+ continue
134
+ }
135
+
136
+ // ignore link-local addresses
137
+ if (isLinkLocal(ma)) {
138
+ continue
139
+ }
140
+
141
+ // only IP based addresses
142
+ if (!this.isIPAddress(ma)) {
143
+ continue
144
+ }
145
+
146
+ const { port, transport } = ma.toOptions()
147
+
148
+ if (this.mappedPorts.has(`${port}-${transport}`)) {
149
+ continue
150
+ }
151
+
152
+ output.push(ma)
153
+ }
154
+
155
+ return output
156
+ }
157
+
158
+ async mapIpAddresses (options?: MapPortsOptions): Promise<void> {
159
+ try {
160
+ const externalHost = await this.externalAddress.getPublicIp()
161
+
162
+ // filter addresses to get private, non-relay, IP based addresses that we
163
+ // haven't mapped yet
164
+ const addresses = this.getUnmappedAddresses(this.addressManager.getAddressesWithMetadata(), [externalHost])
165
+
166
+ if (addresses.length === 0) {
167
+ this.log('no private, non-relay, unmapped, IP based addresses found')
168
+ return
169
+ }
170
+
171
+ this.log('discovered public IP %s', externalHost)
172
+
173
+ this.assertNotBehindDoubleNAT(externalHost)
174
+
175
+ for (const addr of addresses) {
176
+ // try to open uPnP ports for each thin waist address
177
+ const { port, host, transport, family } = addr.toOptions()
178
+
179
+ // don't try to open port on IPv6 host via IPv4 gateway
180
+ if (family === 4 && this.gateway.family !== 'IPv4') {
181
+ continue
182
+ }
183
+
184
+ // don't try to open port on IPv4 host via IPv6 gateway
185
+ if (family === 6 && this.gateway.family !== 'IPv6') {
186
+ continue
187
+ }
188
+
189
+ const key = `${host}-${port}-${transport}`
190
+
191
+ if (this.mappedPorts.has(key)) {
192
+ // already mapped this port
193
+ continue
194
+ }
195
+
196
+ try {
197
+ const mapping = await this.gateway.map(port, host, {
198
+ protocol: transport === 'tcp' ? 'TCP' : 'UDP'
199
+ })
200
+ this.mappedPorts.set(key, mapping)
201
+ this.addressManager.addPublicAddressMapping(mapping.internalHost, mapping.internalPort, mapping.externalHost, mapping.externalPort, transport === 'tcp' ? 'tcp' : 'udp')
202
+ this.log('created mapping of %s:%s to %s:%s for protocol %s', mapping.internalHost, mapping.internalPort, mapping.externalHost, mapping.externalPort, transport)
203
+
204
+ if (options?.autoConfirmAddress === true) {
205
+ const ma = multiaddr(`/ip${family}/${host}/${transport}/${port}`)
206
+ this.log('auto-confirming IP address %a', ma)
207
+ this.addressManager.confirmObservedAddr(ma, {
208
+ ttl: MAX_DATE - Date.now()
209
+ })
210
+ }
211
+ } catch (err) {
212
+ this.log.error('failed to create mapping for %s:%d for protocol - %e', host, port, transport, err)
213
+ }
214
+ }
215
+ } catch (err: any) {
216
+ this.log.error('error finding gateways - %e', err)
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Some ISPs have double-NATs, there's not much we can do with them
222
+ */
223
+ private assertNotBehindDoubleNAT (publicIp: string): void {
224
+ const isPrivate = isPrivateIp(publicIp)
225
+
226
+ if (isPrivate === true) {
227
+ throw new DoubleNATError(`${publicIp} is private - please init uPnPNAT with 'externalAddress' set to an externally routable IP or ensure you are not behind a double NAT`)
228
+ }
229
+
230
+ if (isPrivate == null) {
231
+ throw new InvalidParametersError(`${publicIp} is not an IP address`)
232
+ }
233
+ }
234
+
235
+ private isIPAddress (ma: Multiaddr): boolean {
236
+ return TCP.exactMatch(ma) ||
237
+ WebSockets.exactMatch(ma) ||
238
+ WebSocketsSecure.exactMatch(ma) ||
239
+ QUICV1.exactMatch(ma) ||
240
+ WebTransport.exactMatch(ma)
241
+ }
242
+ }
package/LICENSE DELETED
@@ -1,4 +0,0 @@
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