@achingbrain/nat-port-mapper
Version:
Port mapping with UPnP and NAT-PMP
122 lines • 4.83 kB
JavaScript
import { isIPv6, isIPv4 } from '@chainsafe/is-ip';
import { logger } from '@libp2p/logger';
import { isPrivateIp } from '../utils.js';
import { DEFAULT_REFRESH_THRESHOLD, DEFAULT_REFRESH_TIMEOUT, DEVICE_WAN_IP_CONNECTION_2 } from './constants.js';
import { Device } from './device.js';
import { discoverGateways } from './discovery.js';
import { findLocalAddresses, findNamespacedKey, stripHostBrackets } from './utils.js';
export class InternetGatewayService {
id;
host;
port;
family;
gateway;
log;
mappings;
options;
constructor(gateway, options = {}) {
this.gateway = gateway;
this.mappings = new Map();
this.id = gateway.service.uniqueServiceName;
this.options = options;
this.host = '';
this.port = 0;
this.setGateway(gateway);
this.family = isIPv6(this.host) ? 'IPv6' : 'IPv4';
this.log = logger(`nat-port-mapper:upnp:internetgatewaydevice2:${this.family.toLowerCase()}`);
}
async getGateway(options) {
if (this.gateway.service.expires > Date.now()) {
return this.gateway;
}
for await (const service of discoverGateways({
...this.options,
...options
})) {
if (service.uniqueServiceName !== this.id) {
continue;
}
const host = stripHostBrackets(service.location.hostname);
if (isIPv4(host) && this.family === 'IPv6') {
continue;
}
if (isIPv6(host) && this.family === 'IPv4') {
continue;
}
this.setGateway(new Device(service));
return this.gateway;
}
throw new Error(`Could not resolve gateway with USN ${this.id}`);
}
setGateway(gateway) {
this.gateway = gateway;
this.host = stripHostBrackets(this.gateway.service.location.hostname);
this.port = Number(this.gateway.service.location.port ?? (this.gateway.service.location.protocol === 'http:' ? 80 : 443));
}
async externalIp(options) {
this.log.trace('discover external IP address');
const gateway = await this.getGateway(options);
const response = await gateway.run(DEVICE_WAN_IP_CONNECTION_2, 'GetExternalIPAddress', [], options);
const key = findNamespacedKey('GetExternalIPAddressResponse', response);
this.log.trace('discovered external IP address %s', response[key].NewExternalIPAddress);
return response[key].NewExternalIPAddress;
}
async *mapAll(localPort, options = {}) {
let mapped = false;
for (const host of findLocalAddresses(this.family)) {
try {
const mapping = await this.map(localPort, host, options);
mapped = true;
yield mapping;
}
catch (err) {
this.log.error('error mapping %s:%d - %e', host, localPort, err);
}
}
if (!mapped) {
throw new Error(`All attempts to map port ${localPort} failed`);
}
}
async map(localPort, localHost, options) {
const port = await this.mapPort(localPort, localHost, options);
return {
externalHost: isPrivateIp(localHost) === true ? await this.externalIp(options) : localHost,
externalPort: port,
internalHost: localHost,
internalPort: localPort,
protocol: options?.protocol?.toUpperCase() === 'UDP' ? 'UDP' : 'TCP'
};
}
addMapping(localPort, mapping) {
const mappings = this.mappings.get(localPort) ?? [];
mappings.push(mapping);
this.mappings.set(localPort, mappings);
}
configureRefresh(localPort, mapping, options = {}) {
if (options.autoRefresh === false || this.options.autoRefresh === false) {
return;
}
const refresh = ((localPort, options = {}) => {
this.refreshPort(localPort, {
...options,
signal: AbortSignal.timeout(options.refreshTimeout ?? this.options.refreshTimeout ?? DEFAULT_REFRESH_TIMEOUT)
})
.catch(err => {
this.log.error('could not refresh port mapping - %e', err);
});
}).bind(this, localPort, {
...options,
signal: undefined
});
const ms = (mapping.ttl * 1000) - (options.refreshThreshold ?? this.options.refreshThreshold ?? DEFAULT_REFRESH_THRESHOLD);
mapping.refreshTimeout = setTimeout(refresh, ms);
}
async stop(options) {
for (const port of this.mappings.keys()) {
await this.unmap(port, options);
}
this.mappings.clear();
this.gateway.close();
}
}
//# sourceMappingURL=internet-gateway-service.js.map