@libp2p/autonat-v2 0.0.0-2d6079bc1

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/src/server.ts ADDED
@@ -0,0 +1,237 @@
1
+ import { ProtocolError } from '@libp2p/interface'
2
+ import { isPrivateIp } from '@libp2p/utils/private-ip'
3
+ import { CODE_IP4, CODE_IP6, multiaddr } from '@multiformats/multiaddr'
4
+ import { pbStream } from 'it-protobuf-stream'
5
+ import { setMaxListeners } from 'main-event'
6
+ import { MAX_INBOUND_STREAMS, MAX_MESSAGE_SIZE, MAX_OUTBOUND_STREAMS, TIMEOUT } from './constants.ts'
7
+ import { DialBack, DialBackResponse, DialResponse, DialStatus, Message } from './pb/index.ts'
8
+ import { randomNumber } from './utils.ts'
9
+ import type { AutoNATv2Components, AutoNATv2ServiceInit } from './index.ts'
10
+ import type { Logger, Connection, Startable, AbortOptions, IncomingStreamData, Stream } from '@libp2p/interface'
11
+ import type { Multiaddr } from '@multiformats/multiaddr'
12
+ import type { MessageStream } from 'it-protobuf-stream'
13
+
14
+ export interface AutoNATv2ServerInit extends AutoNATv2ServiceInit {
15
+ dialRequestProtocol: string
16
+ dialBackProtocol: string
17
+ }
18
+
19
+ export class AutoNATv2Server implements Startable {
20
+ private readonly components: AutoNATv2Components
21
+ private readonly dialRequestProtocol: string
22
+ private readonly dialBackProtocol: string
23
+ private readonly timeout: number
24
+ private readonly maxInboundStreams: number
25
+ private readonly maxOutboundStreams: number
26
+ private readonly maxMessageSize: number
27
+ private started: boolean
28
+ private readonly log: Logger
29
+
30
+ constructor (components: AutoNATv2Components, init: AutoNATv2ServerInit) {
31
+ this.components = components
32
+ this.log = components.logger.forComponent('libp2p:auto-nat-v2:server')
33
+ this.started = false
34
+ this.dialRequestProtocol = init.dialRequestProtocol
35
+ this.dialBackProtocol = init.dialBackProtocol
36
+ this.timeout = init.timeout ?? TIMEOUT
37
+ this.maxInboundStreams = init.maxInboundStreams ?? MAX_INBOUND_STREAMS
38
+ this.maxOutboundStreams = init.maxOutboundStreams ?? MAX_OUTBOUND_STREAMS
39
+ this.maxMessageSize = init.maxMessageSize ?? MAX_MESSAGE_SIZE
40
+ }
41
+
42
+ async start (): Promise<void> {
43
+ if (this.started) {
44
+ return
45
+ }
46
+
47
+ // AutoNat server
48
+ await this.components.registrar.handle(this.dialRequestProtocol, (data) => {
49
+ void this.handleDialRequestStream(data)
50
+ .catch(err => {
51
+ this.log.error('error handling incoming autonat stream - %e', err)
52
+ })
53
+ }, {
54
+ maxInboundStreams: this.maxInboundStreams,
55
+ maxOutboundStreams: this.maxOutboundStreams
56
+ })
57
+
58
+ this.started = true
59
+ }
60
+
61
+ async stop (): Promise<void> {
62
+ await this.components.registrar.unhandle(this.dialRequestProtocol)
63
+
64
+ this.started = false
65
+ }
66
+
67
+ /**
68
+ * Handle an incoming AutoNAT request
69
+ */
70
+ async handleDialRequestStream (data: IncomingStreamData): Promise<void> {
71
+ const signal = AbortSignal.timeout(this.timeout)
72
+ setMaxListeners(Infinity, signal)
73
+
74
+ const messages = pbStream(data.stream, {
75
+ maxDataLength: this.maxMessageSize
76
+ }).pb(Message)
77
+
78
+ try {
79
+ const connectionIp = getIpAddress(data.connection.remoteAddr)
80
+
81
+ if (connectionIp == null) {
82
+ throw new ProtocolError(`Could not find IP address in connection address "${data.connection.remoteAddr}"`)
83
+ }
84
+
85
+ const { dialRequest } = await messages.read({
86
+ signal
87
+ })
88
+
89
+ if (dialRequest == null) {
90
+ throw new ProtocolError('Did not receive DialRequest message on incoming dial request stream')
91
+ }
92
+
93
+ if (dialRequest.addrs.length === 0) {
94
+ throw new ProtocolError('Did not receive any addresses to dial')
95
+ }
96
+
97
+ for (let i = 0; i < dialRequest.addrs.length; i++) {
98
+ try {
99
+ const ma = multiaddr(dialRequest.addrs[i])
100
+ const isDialable = await this.components.connectionManager.isDialable(ma, {
101
+ signal
102
+ })
103
+
104
+ if (!isDialable) {
105
+ await messages.write({
106
+ dialResponse: {
107
+ addrIdx: i,
108
+ status: DialResponse.ResponseStatus.E_DIAL_REFUSED,
109
+ dialStatus: DialStatus.UNUSED
110
+ }
111
+ }, {
112
+ signal
113
+ })
114
+
115
+ continue
116
+ }
117
+
118
+ const ip = getIpAddress(ma)
119
+
120
+ if (ip == null) {
121
+ throw new ProtocolError(`Could not find IP address in requested address "${ma}"`)
122
+ }
123
+
124
+ if (isPrivateIp(ip)) {
125
+ throw new ProtocolError(`Requested address had private IP "${ma}"`)
126
+ }
127
+
128
+ if (ip !== connectionIp) {
129
+ // amplification attack protection - request the client sends us a
130
+ // random number of bytes before we'll dial the address
131
+ await this.preventAmplificationAttack(messages, i, {
132
+ signal
133
+ })
134
+ }
135
+
136
+ const dialStatus = await this.dialClientBack(ma, dialRequest.nonce, {
137
+ signal
138
+ })
139
+
140
+ await messages.write({
141
+ dialResponse: {
142
+ addrIdx: i,
143
+ status: DialResponse.ResponseStatus.OK,
144
+ dialStatus
145
+ }
146
+ }, {
147
+ signal
148
+ })
149
+ } catch (err) {
150
+ this.log.error('could not parse multiaddr - %e', err)
151
+ }
152
+ }
153
+
154
+ await data.stream.close({
155
+ signal
156
+ })
157
+ } catch (err: any) {
158
+ this.log.error('error handling incoming autonat stream - %e', err)
159
+ data.stream.abort(err)
160
+ }
161
+ }
162
+
163
+ private async preventAmplificationAttack (messages: MessageStream<Message, Stream>, index: number, options: AbortOptions): Promise<void> {
164
+ const numBytes = randomNumber(30_000, 100_000)
165
+
166
+ await messages.write({
167
+ dialDataRequest: {
168
+ addrIdx: index,
169
+ numBytes: BigInt(numBytes)
170
+ }
171
+ }, options)
172
+
173
+ let received = 0
174
+
175
+ while (received < numBytes) {
176
+ const { dialDataResponse } = await messages.read(options)
177
+
178
+ if (dialDataResponse == null) {
179
+ throw new ProtocolError('Did not receive DialDataResponse message on incoming dial request stream')
180
+ }
181
+
182
+ received += dialDataResponse.data.byteLength
183
+ }
184
+ }
185
+
186
+ private async dialClientBack (ma: Multiaddr, nonce: bigint, options: AbortOptions): Promise<DialStatus> {
187
+ let connection: Connection
188
+
189
+ try {
190
+ connection = await this.components.connectionManager.openConnection(ma, {
191
+ force: true,
192
+ ...options
193
+ })
194
+ } catch (err: any) {
195
+ this.log.error('failed to open connection to %a - %e', err, ma)
196
+
197
+ return DialStatus.E_DIAL_ERROR
198
+ }
199
+
200
+ try {
201
+ const stream = await connection.newStream(this.dialBackProtocol, options)
202
+ const dialBackMessages = pbStream(stream, {
203
+ maxDataLength: this.maxMessageSize
204
+ })
205
+
206
+ await dialBackMessages.write({
207
+ nonce
208
+ }, DialBack, options)
209
+
210
+ const response = await dialBackMessages.read(DialBackResponse)
211
+
212
+ if (response.status !== DialBackResponse.DialBackStatus.OK) {
213
+ throw new ProtocolError('DialBackResponse status was not OK')
214
+ }
215
+
216
+ await connection.close(options)
217
+ } catch (err: any) {
218
+ this.log.error('could not perform dial back - %e', err)
219
+
220
+ connection.abort(err)
221
+
222
+ return DialStatus.E_DIAL_BACK_ERROR
223
+ }
224
+
225
+ // dial back was successful
226
+ return DialStatus.OK
227
+ }
228
+ }
229
+
230
+ function getIpAddress (ma: Multiaddr): string | undefined {
231
+ return ma.getComponents()
232
+ .filter(component => {
233
+ return component.code === CODE_IP4 || component.code === CODE_IP6
234
+ })
235
+ .map(component => component.value)
236
+ .pop()
237
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,3 @@
1
+ export function randomNumber (min: number, max: number): number {
2
+ return Math.round(Math.random() * (max - min) + min)
3
+ }