Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] feat: NAT port mapping manager #1

Merged
merged 11 commits into from
Jan 3, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
test/repo-tests*
**/bundle.js
**/.nyc_output
**/.vscode/

# Logs
logs
Expand Down
29 changes: 29 additions & 0 deletions appveyor.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Warning: This file is automatically synced from https://github.com/ipfs/ci-sync so if you want to change it, please change it there and ask someone to sync all repositories.
version: "{build}"

environment:
matrix:
- nodejs_version: "6"
- nodejs_version: "8"

matrix:
fast_finish: true

install:
# Install Node.js
- ps: Install-Product node $env:nodejs_version

# Upgrade npm
- npm install -g npm

# Output our current versions for debugging
- node --version
- npm --version

# Install our package dependencies
- npm install

test_script:
- npm run test:node

build: off
3 changes: 3 additions & 0 deletions ci/Jenkinsfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

// Warning: This file is automatically synced from https://github.com/ipfs/ci-sync so if you want to change it, please change it there and ask someone to sync all repositories.
javascript()
22 changes: 22 additions & 0 deletions circle.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Warning: This file is automatically synced from https://github.com/ipfs/ci-sync so if you want to change it, please change it there and ask someone to sync all repositories.
machine:
node:
version: stable

test:
pre:
- npm run lint
post:
- make test
- npm run coverage -- --upload --providers coveralls

dependencies:
pre:
- google-chrome --version
- curl -L -o google-chrome.deb https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
- sudo dpkg -i google-chrome.deb || true
- sudo apt-get update
- sudo apt-get install -f
- sudo apt-get install --only-upgrade lsb-base
- sudo dpkg -i google-chrome.deb
- google-chrome --version
34 changes: 34 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "libp2p-nat-mngr",
"version": "1.0.0",
"description": "Create and remove NAT port mappings.",
"main": "src/index.js",
"scripts": {
"lint": "aegir lint",
"docs": "aegir docs",
"build": "aegir build",
"test": "aegir test -t node",
"test:node": "aegir test -t node",
"release": "aegir release",
"release-minor": "aegir release --type minor",
"release-major": "aegir release --type major",
"coverage": "COVERAGE=true aegir coverage --timeout 50000",
"coverage-publish": "aegir coverage -u"
},
"author": "",
"license": "MIT",
"dependencies": {
"chai-checkmark": "^1.0.1",
"dgram": "^1.0.1",
"ipaddr.js": "^1.7.0",
"nat-pmp": "^1.0.0",
"nat-upnp": "^1.1.1",
"network": "^0.4.1",
"sinon": "^5.0.10"
},
"devDependencies": {
"aegir": "^13.1.0",
"chai": "^4.1.2",
"dirty-chai": "^2.0.1"
}
}
8 changes: 8 additions & 0 deletions requirements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Requirements

- mapper should be able to hold multiple mappings for the same external port but different external ips
- for example, if the client moves around (laptops, mobile devices) and connects
from different access points, the mapper should be able to detect if we're using
a different external ip/getaway for which we don't have a prior mapping and add one
- mapper should be able to auto-renew after a timeout
- mapper should be plugable - different nat techniques should be easy to adapt
117 changes: 117 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
'use strict'

const NatPmp = require('./mappers/pmp')
const UPnP = require('./mappers/upnp')
const EE = require('events')
const tryEach = require('async/tryEach')
const eachSeries = require('async/eachSeries')
const parallel = require('async/parallel')
const waterfall = require('async/waterfall')
const network = require('network')

const log = require('debug')('libp2p-nat-mngr')

class NatManager extends EE {
constructor (mappers, options) {
super()

options = options || {
autorenew: true,
every: 60 * 10 * 1000
}

this.mappers = mappers || [
new NatPmp(),
new UPnP()
]

this.activeMappings = {}

if (options.autorenew) {
setInterval(() => {
this.renewMappings()
}, options.every)
}
}

renewMappings (callback) {
callback = callback || (() => {})
this.getPublicIp((err, ip) => {
if (err) {
return log(err)
}

eachSeries(Object.keys(this.activeMappings), (key, cb) => {
const mapping = this.activeMappings[key].mappings[key]
if (mapping.externalIp !== ip) {
delete this.activeMappings[key]
this.addMapping(mapping.internalPort,
mapping.externalPort,
mapping.ttl,
(err) => {
if (err) {
return log(err)
}
return cb()
})
}
}, callback)
})
}

addMapping (intPort, extPort, ttl, callback) {
tryEach(this.mappers.map((mapper) => {
return (cb) => {
return mapper.addMapping(intPort,
extPort,
ttl,
(err, mapping) => {
if (err) {
return cb(err)
}

const mapKey = `${mapping.externalIp}:${mapping.externalPort}`
this.activeMappings[mapKey] = mapper
this.emit('mapping', mapping)
cb(null, mapping)
})
}
}), callback)
}

deleteMapping (extPort, extIp, callback) {
if (typeof extIp === 'function') {
callback = extIp
extIp = undefined
}

waterfall([
(cb) => extIp
? cb(null, extIp)
: network.get_public_ip(cb),
(ip, cb) => {
const mapper = this.activeMappings[`${ip}:${extPort}`]
if (mapper) {
mapper.deleteMapping(extPort, cb)
}
}
], callback)
}

getPublicIp (callback) {
network.get_public_ip(callback)
}

getGwIp (callback) {
network.get_gateway_ip(callback)
}

close (callback) {
parallel(Object.keys(this.activeMappings).map((key) => {
const [ip, port] = key.split(':')
return (cb) => this.activeMappings[key].deleteMapping(port, ip, cb)
}), callback)
}
}

module.exports = NatManager
67 changes: 67 additions & 0 deletions src/mappers/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
'use strict'
const debug = require('debug')

class BaseMapper {
constructor (name) {
this.name = name
this.mappings = {}

this.log = debug(`nat-puncher:${name}`)
this.log.err = debug(`nat-puncher:${name}:error`)
}

newMapping (port) {
return {
routerIp: null,
internalIp: null,
internalPort: port,
externalIp: null, // Only provided by PCP, undefined for other protocols
externalPort: null, // The actual external port of the mapping, -1 on failure
ttl: null, // The actual (response) lifetime of the mapping
protocol: this.name, // The protocol used to make the mapping ('natPmp', 'pcp', 'upnp')
nonce: null, // Only for PCP; the nonce field for deletion
errInfo: null // Error message if failure; currently used only for UPnP
}
}

addMapping (intPort, extPort, ttl, callback) {
// If lifetime is zero, we want to refresh every 24 hours
ttl = !ttl ? 24 * 60 * 60 : ttl

this._addPortMapping(intPort,
extPort,
ttl,
(err, mapping) => {
if (err) {
this.log.err(err)
return callback(err)
}
this.mappings[`${mapping.externalIp}:${mapping.externalPort}`] = mapping
callback(null, mapping)
})
}

_addPortMapping (intPort, extPort, lifetime, cb) {
cb(new Error('Not implemented!'))
}

deleteMapping (mapping, callback) {
this._removePortMapping(mapping.internalPort,
mapping.externalPort,
(err) => {
if (err) {
return callback(err)
}

// delete the mappings
delete this.mappings[`${mapping.externalIp}:${mapping.externalPort}`]
callback()
})
}

_removePortMapping (intPort, extPort, callback) {
callback(new Error('Not implemented!'))
}
}

module.exports = BaseMapper
89 changes: 89 additions & 0 deletions src/mappers/pmp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
'use strict'

const natPmp = require('nat-pmp')
const waterfall = require('async/waterfall')
const network = require('network')

const Mapper = require('./')

class NatPMP extends Mapper {
constructor () {
super('nat-pmp')
}

/**
* Create port mapping
*
* @param {number} intPort
* @param {number} extPort
* @param {number} ttl
* @param {Function} callback
* @returns {undefined}
*/
_addPortMapping (intPort, extPort, ttl, callback) {
network.get_active_interface((err, activeIf) => {
if (err) {
return callback(err)
}

const client = natPmp.connect(activeIf.gateway_ip)
const mapping = this.newMapping(intPort)
mapping.routerIp = activeIf.gateway_ip
waterfall([
(cb) => client.externalIp((err, info) => {
if (err) {
return callback(err)
}
mapping.externalIp = info.ip.join('.')
cb(null, mapping)
}),
(mapping, cb) => {
client.portMapping({
private: intPort,
public: extPort,
ttl
}, (err, info) => {
if (err) {
this.log.err(err)
return cb(err)
}

mapping.externalPort = info.public
mapping.internalPort = info.private
mapping.internalIp = activeIf.ip_address
mapping.ttl = info.ttl
cb(null, mapping)
})
}
], (err, mapping) => {
client.close() // should be closed immediately
if (err) {
return callback(err)
}
callback(null, mapping)
})
})
}

_removePortMapping (intPort, extPort, callback) {
network.get_gateway_ip((err, routerIp) => {
if (err) {
return callback(err)
}

const client = natPmp.connect(routerIp)
client.portUnmapping({
private: intPort,
public: extPort
}, (err, info) => {
client.close() // should be closed immediately
if (err) {
return callback(err)
}
return callback(null, err)
})
})
}
}

module.exports = NatPMP
Loading