node-rtc-connection 1.0.12 → 1.0.14

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,643 @@
1
+ /**
2
+ * @file stun-client.js
3
+ * @description STUN (Session Traversal Utilities for NAT) client implementation
4
+ * @module stun/stun-client
5
+ *
6
+ * STUN Protocol: RFC 5389
7
+ * TURN Protocol: RFC 5766
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ const dgram = require('dgram');
13
+ const crypto = require('crypto');
14
+
15
+ /**
16
+ * STUN message types
17
+ */
18
+ const STUN_MESSAGE_TYPES = {
19
+ BINDING_REQUEST: 0x0001,
20
+ BINDING_RESPONSE: 0x0101,
21
+ BINDING_ERROR_RESPONSE: 0x0111,
22
+
23
+ // TURN
24
+ ALLOCATE_REQUEST: 0x0003,
25
+ ALLOCATE_RESPONSE: 0x0103,
26
+ ALLOCATE_ERROR_RESPONSE: 0x0113,
27
+
28
+ REFRESH_REQUEST: 0x0004,
29
+ REFRESH_RESPONSE: 0x0104,
30
+
31
+ SEND_INDICATION: 0x0016,
32
+ DATA_INDICATION: 0x0017,
33
+
34
+ CREATE_PERMISSION_REQUEST: 0x0008,
35
+ CREATE_PERMISSION_RESPONSE: 0x0108,
36
+
37
+ CHANNEL_BIND_REQUEST: 0x0009,
38
+ CHANNEL_BIND_RESPONSE: 0x0109
39
+ };
40
+
41
+ /**
42
+ * STUN attribute types
43
+ */
44
+ const STUN_ATTRIBUTES = {
45
+ MAPPED_ADDRESS: 0x0001,
46
+ USERNAME: 0x0006,
47
+ MESSAGE_INTEGRITY: 0x0008,
48
+ ERROR_CODE: 0x0009,
49
+ UNKNOWN_ATTRIBUTES: 0x000A,
50
+ REALM: 0x0014,
51
+ NONCE: 0x0015,
52
+ XOR_MAPPED_ADDRESS: 0x0020,
53
+
54
+ // TURN
55
+ CHANNEL_NUMBER: 0x000C,
56
+ LIFETIME: 0x000D,
57
+ XOR_PEER_ADDRESS: 0x0012,
58
+ DATA: 0x0013,
59
+ XOR_RELAYED_ADDRESS: 0x0016,
60
+ REQUESTED_TRANSPORT: 0x0019,
61
+
62
+ SOFTWARE: 0x8022,
63
+ FINGERPRINT: 0x8028
64
+ };
65
+
66
+ const MAGIC_COOKIE = 0x2112A442;
67
+
68
+ /**
69
+ * @class STUNClient
70
+ * @description STUN/TURN client for NAT traversal
71
+ */
72
+ class STUNClient {
73
+ /**
74
+ * Create a STUN client
75
+ * @param {Object} options - Client options
76
+ * @param {string} options.server - STUN/TURN server address
77
+ * @param {number} options.port - Server port
78
+ * @param {string} [options.username] - TURN username
79
+ * @param {string} [options.credential] - TURN password
80
+ * @param {string} [options.transport='udp'] - Transport protocol (udp/tcp)
81
+ * @param {Object} [options.params={}] - Additional query parameters from URL
82
+ */
83
+ constructor(options) {
84
+ this.server = options.server;
85
+ this.port = options.port;
86
+ this.username = options.username;
87
+ this.credential = options.credential;
88
+ this.transport = options.transport || 'udp';
89
+ this.params = options.params || {};
90
+
91
+ this.socket = null;
92
+ this.transactions = new Map();
93
+ this.realm = null;
94
+ this.nonce = null;
95
+ }
96
+
97
+ /**
98
+ * Connect to the STUN/TURN server
99
+ * @returns {Promise<void>}
100
+ */
101
+ async connect() {
102
+ if (this.socket) {
103
+ return;
104
+ }
105
+
106
+ return new Promise((resolve, reject) => {
107
+ this.socket = dgram.createSocket('udp4');
108
+
109
+ this.socket.on('message', (msg, rinfo) => {
110
+ this._handleMessage(msg, rinfo);
111
+ });
112
+
113
+ this.socket.on('error', (err) => {
114
+ console.error('STUN socket error:', err);
115
+ reject(err);
116
+ });
117
+
118
+ this.socket.bind(() => {
119
+ resolve();
120
+ });
121
+ });
122
+ }
123
+
124
+ /**
125
+ * Send a STUN Binding Request to get reflexive address
126
+ * @returns {Promise<Object>} Reflexive address info
127
+ */
128
+ async getReflexiveAddress() {
129
+ await this.connect();
130
+
131
+ const transactionId = crypto.randomBytes(12);
132
+ const request = this._createBindingRequest(transactionId);
133
+
134
+ return new Promise((resolve, reject) => {
135
+ const timeout = setTimeout(() => {
136
+ this.transactions.delete(transactionId.toString('hex'));
137
+ reject(new Error('STUN request timeout'));
138
+ }, 5000);
139
+
140
+ this.transactions.set(transactionId.toString('hex'), {
141
+ resolve: (result) => {
142
+ clearTimeout(timeout);
143
+ resolve(result);
144
+ },
145
+ reject: (error) => {
146
+ clearTimeout(timeout);
147
+ reject(error);
148
+ }
149
+ });
150
+
151
+ this.socket.send(request, this.port, this.server, (err) => {
152
+ if (err) {
153
+ clearTimeout(timeout);
154
+ this.transactions.delete(transactionId.toString('hex'));
155
+ reject(err);
156
+ }
157
+ });
158
+ });
159
+ }
160
+
161
+ /**
162
+ * Send a TURN Allocate Request to get relay address
163
+ * @param {number} [lifetime=600] - Allocation lifetime in seconds
164
+ * @returns {Promise<Object>} Relay address info
165
+ */
166
+ async allocateRelay(lifetime = 600) {
167
+ if (!this.username || !this.credential) {
168
+ throw new Error('TURN requires username and credential');
169
+ }
170
+
171
+ await this.connect();
172
+
173
+ let transactionId = crypto.randomBytes(12);
174
+ let request = this._createAllocateRequest(transactionId, lifetime);
175
+
176
+ // First attempt without credentials to get realm and nonce
177
+ try {
178
+ return await this._sendRequest(request, transactionId, 'allocate');
179
+ } catch (error) {
180
+ // If we get 401 Unauthorized, retry with credentials
181
+ if (error.message.includes('401') && this.realm && this.nonce) {
182
+ // Create new transaction ID for retry
183
+ transactionId = crypto.randomBytes(12);
184
+ request = this._createAllocateRequest(transactionId, lifetime, true);
185
+ return await this._sendRequest(request, transactionId, 'allocate');
186
+ }
187
+ throw error;
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Send a TURN Refresh Request to keep allocation alive
193
+ * @param {number} [lifetime=600] - Allocation lifetime in seconds
194
+ * @returns {Promise<Object>} Updated allocation info
195
+ */
196
+ async refreshAllocation(lifetime = 600) {
197
+ if (!this.username || !this.credential) {
198
+ throw new Error('TURN requires username and credential');
199
+ }
200
+
201
+ const transactionId = crypto.randomBytes(12);
202
+ const request = this._createRefreshRequest(transactionId, lifetime);
203
+
204
+ return this._sendRequest(request, transactionId, 'refresh');
205
+ }
206
+
207
+ /**
208
+ * Send a TURN request
209
+ * @param {Buffer} request - Request message
210
+ * @param {Buffer} transactionId - Transaction ID
211
+ * @param {string} requestType - Type of request
212
+ * @returns {Promise<Object>}
213
+ * @private
214
+ */
215
+ _sendRequest(request, transactionId, requestType) {
216
+ return new Promise((resolve, reject) => {
217
+ const timeout = setTimeout(() => {
218
+ this.transactions.delete(transactionId.toString('hex'));
219
+ reject(new Error(`${requestType} request timeout`));
220
+ }, 5000);
221
+
222
+ this.transactions.set(transactionId.toString('hex'), {
223
+ type: requestType,
224
+ resolve: (result) => {
225
+ clearTimeout(timeout);
226
+ resolve(result);
227
+ },
228
+ reject: (error) => {
229
+ clearTimeout(timeout);
230
+ reject(error);
231
+ }
232
+ });
233
+
234
+ this.socket.send(request, this.port, this.server, (err) => {
235
+ if (err) {
236
+ clearTimeout(timeout);
237
+ this.transactions.delete(transactionId.toString('hex'));
238
+ reject(err);
239
+ }
240
+ });
241
+ });
242
+ }
243
+
244
+ /**
245
+ * Create a STUN Binding Request
246
+ * @param {Buffer} transactionId - Transaction ID
247
+ * @returns {Buffer} STUN message
248
+ * @private
249
+ */
250
+ _createBindingRequest(transactionId) {
251
+ const header = Buffer.alloc(20);
252
+
253
+ // Message Type (2 bytes)
254
+ header.writeUInt16BE(STUN_MESSAGE_TYPES.BINDING_REQUEST, 0);
255
+
256
+ // Message Length (2 bytes) - 0 for now, no attributes
257
+ header.writeUInt16BE(0, 2);
258
+
259
+ // Magic Cookie (4 bytes)
260
+ header.writeUInt32BE(MAGIC_COOKIE, 4);
261
+
262
+ // Transaction ID (12 bytes)
263
+ transactionId.copy(header, 8);
264
+
265
+ return header;
266
+ }
267
+
268
+ /**
269
+ * Create a TURN Allocate Request
270
+ * @param {Buffer} transactionId - Transaction ID
271
+ * @param {number} lifetime - Allocation lifetime in seconds
272
+ * @param {boolean} withAuth - Include authentication
273
+ * @returns {Buffer} STUN message
274
+ * @private
275
+ */
276
+ _createAllocateRequest(transactionId, lifetime, withAuth = false) {
277
+ const attributes = [];
278
+
279
+ // REQUESTED-TRANSPORT (UDP = 17)
280
+ const transport = Buffer.alloc(8);
281
+ transport.writeUInt16BE(STUN_ATTRIBUTES.REQUESTED_TRANSPORT, 0);
282
+ transport.writeUInt16BE(4, 2);
283
+ transport.writeUInt8(17, 4); // UDP
284
+ attributes.push(transport);
285
+
286
+ // LIFETIME
287
+ const lifetimeAttr = Buffer.alloc(8);
288
+ lifetimeAttr.writeUInt16BE(STUN_ATTRIBUTES.LIFETIME, 0);
289
+ lifetimeAttr.writeUInt16BE(4, 2);
290
+ lifetimeAttr.writeUInt32BE(lifetime, 4);
291
+ attributes.push(lifetimeAttr);
292
+
293
+ if (withAuth && this.realm && this.nonce) {
294
+ // USERNAME
295
+ const usernameAttr = this._createStringAttribute(STUN_ATTRIBUTES.USERNAME, this.username);
296
+ attributes.push(usernameAttr);
297
+
298
+ // REALM
299
+ const realmAttr = this._createStringAttribute(STUN_ATTRIBUTES.REALM, this.realm);
300
+ attributes.push(realmAttr);
301
+
302
+ // NONCE
303
+ const nonceAttr = this._createStringAttribute(STUN_ATTRIBUTES.NONCE, this.nonce);
304
+ attributes.push(nonceAttr);
305
+ }
306
+
307
+ return this._createMessage(STUN_MESSAGE_TYPES.ALLOCATE_REQUEST, transactionId, attributes, withAuth);
308
+ }
309
+
310
+ /**
311
+ * Create a TURN Refresh Request
312
+ * @param {Buffer} transactionId - Transaction ID
313
+ * @param {number} lifetime - Allocation lifetime in seconds
314
+ * @returns {Buffer} STUN message
315
+ * @private
316
+ */
317
+ _createRefreshRequest(transactionId, lifetime) {
318
+ const attributes = [];
319
+
320
+ // LIFETIME
321
+ const lifetimeAttr = Buffer.alloc(8);
322
+ lifetimeAttr.writeUInt16BE(STUN_ATTRIBUTES.LIFETIME, 0);
323
+ lifetimeAttr.writeUInt16BE(4, 2);
324
+ lifetimeAttr.writeUInt32BE(lifetime, 4);
325
+ attributes.push(lifetimeAttr);
326
+
327
+ // USERNAME
328
+ const usernameAttr = this._createStringAttribute(STUN_ATTRIBUTES.USERNAME, this.username);
329
+ attributes.push(usernameAttr);
330
+
331
+ // REALM
332
+ if (this.realm) {
333
+ const realmAttr = this._createStringAttribute(STUN_ATTRIBUTES.REALM, this.realm);
334
+ attributes.push(realmAttr);
335
+ }
336
+
337
+ // NONCE
338
+ if (this.nonce) {
339
+ const nonceAttr = this._createStringAttribute(STUN_ATTRIBUTES.NONCE, this.nonce);
340
+ attributes.push(nonceAttr);
341
+ }
342
+
343
+ return this._createMessage(STUN_MESSAGE_TYPES.REFRESH_REQUEST, transactionId, attributes, true);
344
+ }
345
+
346
+ /**
347
+ * Create a STUN message with attributes
348
+ * @param {number} messageType - Message type
349
+ * @param {Buffer} transactionId - Transaction ID
350
+ * @param {Array<Buffer>} attributes - Attribute buffers
351
+ * @param {boolean} withIntegrity - Add MESSAGE-INTEGRITY
352
+ * @returns {Buffer} Complete STUN message
353
+ * @private
354
+ */
355
+ _createMessage(messageType, transactionId, attributes, withIntegrity = false) {
356
+ let attributesBuffer = Buffer.concat(attributes);
357
+
358
+ // Add MESSAGE-INTEGRITY if needed
359
+ if (withIntegrity && this.credential) {
360
+ const tempHeader = Buffer.alloc(20);
361
+ tempHeader.writeUInt16BE(messageType, 0);
362
+ tempHeader.writeUInt16BE(attributesBuffer.length + 24, 2); // +24 for MESSAGE-INTEGRITY
363
+ tempHeader.writeUInt32BE(MAGIC_COOKIE, 4);
364
+ transactionId.copy(tempHeader, 8);
365
+
366
+ const tempMessage = Buffer.concat([tempHeader, attributesBuffer]);
367
+
368
+ // For TURN, compute key as MD5(username:realm:password) per RFC 5766
369
+ let key = this.credential;
370
+ if (this.username && this.realm) {
371
+ const keyString = `${this.username}:${this.realm}:${this.credential}`;
372
+ key = crypto.createHash('md5').update(keyString).digest();
373
+ }
374
+
375
+ const hmac = crypto.createHmac('sha1', key);
376
+ hmac.update(tempMessage);
377
+ const integrity = hmac.digest();
378
+
379
+ const integrityAttr = Buffer.alloc(4 + integrity.length);
380
+ integrityAttr.writeUInt16BE(STUN_ATTRIBUTES.MESSAGE_INTEGRITY, 0);
381
+ integrityAttr.writeUInt16BE(integrity.length, 2);
382
+ integrity.copy(integrityAttr, 4);
383
+
384
+ attributesBuffer = Buffer.concat([attributesBuffer, integrityAttr]);
385
+ }
386
+
387
+ // Create final message
388
+ const header = Buffer.alloc(20);
389
+ header.writeUInt16BE(messageType, 0);
390
+ header.writeUInt16BE(attributesBuffer.length, 2);
391
+ header.writeUInt32BE(MAGIC_COOKIE, 4);
392
+ transactionId.copy(header, 8);
393
+
394
+ return Buffer.concat([header, attributesBuffer]);
395
+ }
396
+
397
+ /**
398
+ * Create a string attribute
399
+ * @param {number} type - Attribute type
400
+ * @param {string} value - String value
401
+ * @returns {Buffer} Attribute buffer
402
+ * @private
403
+ */
404
+ _createStringAttribute(type, value) {
405
+ const valueBuffer = Buffer.from(value, 'utf8');
406
+ const length = valueBuffer.length;
407
+ const padding = (4 - (length % 4)) % 4;
408
+ const buffer = Buffer.alloc(4 + length + padding);
409
+
410
+ buffer.writeUInt16BE(type, 0);
411
+ buffer.writeUInt16BE(length, 2);
412
+ valueBuffer.copy(buffer, 4);
413
+
414
+ return buffer;
415
+ }
416
+
417
+ /**
418
+ * Handle incoming STUN message
419
+ * @param {Buffer} msg - Message buffer
420
+ * @param {Object} rinfo - Remote info
421
+ * @private
422
+ */
423
+ _handleMessage(msg, rinfo) {
424
+ if (msg.length < 20) {
425
+ return; // Invalid STUN message
426
+ }
427
+
428
+ const messageType = msg.readUInt16BE(0);
429
+ const messageLength = msg.readUInt16BE(2);
430
+ const magicCookie = msg.readUInt32BE(4);
431
+ const transactionId = msg.slice(8, 20);
432
+
433
+ if (magicCookie !== MAGIC_COOKIE) {
434
+ return; // Not a STUN message
435
+ }
436
+
437
+ const transactionKey = transactionId.toString('hex');
438
+ const transaction = this.transactions.get(transactionKey);
439
+
440
+ if (!transaction) {
441
+ return; // Unknown transaction
442
+ }
443
+
444
+ const attributes = this._parseAttributes(msg.slice(20, 20 + messageLength), transactionId);
445
+
446
+ // Handle STUN Binding responses
447
+ if (messageType === STUN_MESSAGE_TYPES.BINDING_RESPONSE) {
448
+ if (attributes.xorMappedAddress) {
449
+ transaction.resolve({
450
+ address: attributes.xorMappedAddress.address,
451
+ port: attributes.xorMappedAddress.port,
452
+ family: attributes.xorMappedAddress.family
453
+ });
454
+ } else if (attributes.mappedAddress) {
455
+ transaction.resolve({
456
+ address: attributes.mappedAddress.address,
457
+ port: attributes.mappedAddress.port,
458
+ family: attributes.mappedAddress.family
459
+ });
460
+ } else {
461
+ transaction.reject(new Error('No mapped address in STUN response'));
462
+ }
463
+ this.transactions.delete(transactionKey);
464
+ }
465
+ // Handle TURN Allocate responses
466
+ else if (messageType === STUN_MESSAGE_TYPES.ALLOCATE_RESPONSE) {
467
+ if (attributes.xorRelayedAddress) {
468
+ transaction.resolve({
469
+ relayedAddress: attributes.xorRelayedAddress.address,
470
+ relayedPort: attributes.xorRelayedAddress.port,
471
+ lifetime: attributes.lifetime || 600,
472
+ type: 'relay'
473
+ });
474
+ } else {
475
+ transaction.reject(new Error('No relayed address in ALLOCATE response'));
476
+ }
477
+ this.transactions.delete(transactionKey);
478
+ }
479
+ // Handle TURN Refresh responses
480
+ else if (messageType === STUN_MESSAGE_TYPES.REFRESH_RESPONSE) {
481
+ transaction.resolve({
482
+ lifetime: attributes.lifetime || 600
483
+ });
484
+ this.transactions.delete(transactionKey);
485
+ }
486
+ // Handle error responses
487
+ else if (messageType === STUN_MESSAGE_TYPES.BINDING_ERROR_RESPONSE ||
488
+ messageType === STUN_MESSAGE_TYPES.ALLOCATE_ERROR_RESPONSE) {
489
+
490
+ // Store realm and nonce for subsequent requests
491
+ if (attributes.realm) {
492
+ this.realm = attributes.realm;
493
+ }
494
+ if (attributes.nonce) {
495
+ this.nonce = attributes.nonce;
496
+ }
497
+
498
+ const errorMsg = attributes.errorCode || 'Unknown error';
499
+ transaction.reject(new Error(`STUN error: ${errorMsg}`));
500
+ this.transactions.delete(transactionKey);
501
+ }
502
+ }
503
+
504
+ /**
505
+ * Parse STUN attributes
506
+ * @param {Buffer} data - Attributes data
507
+ * @param {Buffer} transactionId - Transaction ID
508
+ * @returns {Object} Parsed attributes
509
+ * @private
510
+ */
511
+ _parseAttributes(data, transactionId) {
512
+ const attributes = {};
513
+ let offset = 0;
514
+
515
+ while (offset < data.length) {
516
+ if (offset + 4 > data.length) break;
517
+
518
+ const type = data.readUInt16BE(offset);
519
+ const length = data.readUInt16BE(offset + 2);
520
+ offset += 4;
521
+
522
+ if (offset + length > data.length) break;
523
+
524
+ const value = data.slice(offset, offset + length);
525
+
526
+ switch (type) {
527
+ case STUN_ATTRIBUTES.XOR_MAPPED_ADDRESS:
528
+ attributes.xorMappedAddress = this._parseXorAddress(value, transactionId);
529
+ break;
530
+ case STUN_ATTRIBUTES.XOR_RELAYED_ADDRESS:
531
+ attributes.xorRelayedAddress = this._parseXorAddress(value, transactionId);
532
+ break;
533
+ case STUN_ATTRIBUTES.MAPPED_ADDRESS:
534
+ attributes.mappedAddress = this._parseAddress(value);
535
+ break;
536
+ case STUN_ATTRIBUTES.LIFETIME:
537
+ attributes.lifetime = value.readUInt32BE(0);
538
+ break;
539
+ case STUN_ATTRIBUTES.ERROR_CODE:
540
+ attributes.errorCode = this._parseErrorCode(value);
541
+ break;
542
+ case STUN_ATTRIBUTES.REALM:
543
+ attributes.realm = value.toString('utf8');
544
+ this.realm = attributes.realm;
545
+ break;
546
+ case STUN_ATTRIBUTES.NONCE:
547
+ attributes.nonce = value.toString('utf8');
548
+ this.nonce = attributes.nonce;
549
+ break;
550
+ }
551
+
552
+ // Pad to 4-byte boundary
553
+ offset += length;
554
+ const padding = (4 - (length % 4)) % 4;
555
+ offset += padding;
556
+ }
557
+
558
+ return attributes;
559
+ }
560
+
561
+ /**
562
+ * Parse XOR-MAPPED-ADDRESS attribute
563
+ * @param {Buffer} data - Attribute data
564
+ * @param {Buffer} transactionId - Transaction ID
565
+ * @returns {Object} Address info
566
+ * @private
567
+ */
568
+ _parseXorAddress(data, transactionId) {
569
+ const family = data.readUInt8(1);
570
+ const xorPort = data.readUInt16BE(2);
571
+
572
+ // XOR port with magic cookie high 16 bits
573
+ const port = xorPort ^ (MAGIC_COOKIE >> 16);
574
+
575
+ if (family === 0x01) { // IPv4
576
+ const xorAddress = data.readUInt32BE(4);
577
+ const address = xorAddress ^ MAGIC_COOKIE;
578
+
579
+ return {
580
+ family: 'IPv4',
581
+ port,
582
+ address: [
583
+ (address >> 24) & 0xFF,
584
+ (address >> 16) & 0xFF,
585
+ (address >> 8) & 0xFF,
586
+ address & 0xFF
587
+ ].join('.')
588
+ };
589
+ }
590
+
591
+ return null;
592
+ }
593
+
594
+ /**
595
+ * Parse MAPPED-ADDRESS attribute
596
+ * @param {Buffer} data - Attribute data
597
+ * @returns {Object} Address info
598
+ * @private
599
+ */
600
+ _parseAddress(data) {
601
+ const family = data.readUInt8(1);
602
+ const port = data.readUInt16BE(2);
603
+
604
+ if (family === 0x01) { // IPv4
605
+ const address = data.slice(4, 8);
606
+ return {
607
+ family: 'IPv4',
608
+ port,
609
+ address: Array.from(address).join('.')
610
+ };
611
+ }
612
+
613
+ return null;
614
+ }
615
+
616
+ /**
617
+ * Parse ERROR-CODE attribute
618
+ * @param {Buffer} data - Attribute data
619
+ * @returns {string} Error message
620
+ * @private
621
+ */
622
+ _parseErrorCode(data) {
623
+ const errorClass = data.readUInt8(2) & 0x07;
624
+ const errorNumber = data.readUInt8(3);
625
+ const errorCode = errorClass * 100 + errorNumber;
626
+ const reason = data.slice(4).toString('utf8');
627
+
628
+ return `${errorCode} ${reason}`;
629
+ }
630
+
631
+ /**
632
+ * Close the client
633
+ */
634
+ close() {
635
+ if (this.socket) {
636
+ this.socket.close();
637
+ this.socket = null;
638
+ }
639
+ this.transactions.clear();
640
+ }
641
+ }
642
+
643
+ module.exports = STUNClient;