From ce4e72849386b5771959de7d6e024cf775090279 Mon Sep 17 00:00:00 2001 From: Michael Morrison Date: Tue, 18 May 2021 23:13:18 -0500 Subject: [PATCH] Add support for udp bind port override (3.0.5) Fixes #149 --- CHANGELOG.md | 4 ++++ README.md | 24 ++++++++++++++++++++---- bin/gamedig.js | 5 +++-- lib/GlobalUdpSocket.js | 34 ++++++++++++++++++++++++---------- lib/QueryRunner.js | 6 ++++-- lib/index.js | 4 ++-- package.json | 2 +- protocols/core.js | 6 +----- 8 files changed, 59 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca0e18e..84abc29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +### 3.0.5 +* Add support for `listenUdpPort` to specify a fixed bind port. +* Improved udp bind failure detection. + ### 3.0.4 * Add support for Discord widget diff --git a/README.md b/README.md index ed25f0c..f5dadb2 100644 --- a/README.md +++ b/README.md @@ -467,25 +467,41 @@ Valheim servers will only respond to queries if they are started in public mode For many valve games, additional 'rules' may be fetched into the unstable `raw` field by passing the additional option: `requestRules: true`. Beware that this may increase query time. -Important note about Firewalls (replit / docker / some VPS providers) +Common Issues --- + +### Firewalls block incoming UDP +*(replit / docker / some VPS providers)* + Most game query protocols require a UDP request and response. This means that in some environments, gamedig may not be able to receive the reponse required due to environmental restrictions. Some examples include: * Docker containers - * You may need to run the container in `--network host` mode so that gamedig can bind a UDP listen port + * You may need to run the container in `--network host` mode so that gamedig can bind a UDP listen port. + * Alternatively, you can forward a single UDP port to your container, and force gamedig to listen on that port using the + instructions in the section down below. * replit * Most online IDEs run in an isolated container, which will never receive UDP responses from outside networks. * Various VPS / server providers * Even if your server provider doesn't explicitly block incoming UDP packets, some server hosts block other server hosts from connecting to them for DDOS-mitigation and anti-botting purposes. -Important note about gamedig in the browser ---- +### Gamedig doesn't work in the browser Gamedig cannot operate within a browser. This means you cannot package it as part of your webpack / browserify / rollup / parcel package. Even if you were able to get it packaged into a bundle, unfortunately no browsers support the UDP protocols required to query server status from most game servers. As an alternative, we'd recommend using gamedig on your server-side, then expose your own API to your webapp's frontend displaying the status information. If your application is thin (with no constant server component), you may wish to investigate a server-less lambda provider. +### Specifying a listen UDP port override +In some very rare scenarios, you may need to bind / listen on a fixed local UDP port. The is usually not needed except behind +some extremely strict firewalls, or within a docker container (where you only wish to forward a single UDP port). +To use a fixed listen udp port, construct a new Gamedig object like this: +``` +const gamedig = new Gamedig({ + listenUdpPort: 13337 +}); +gamedig.query(...) +``` + Usage from Command Line --- diff --git a/bin/gamedig.js b/bin/gamedig.js index e4b53e9..8fd9a91 100755 --- a/bin/gamedig.js +++ b/bin/gamedig.js @@ -5,7 +5,7 @@ const Minimist = require('minimist'), const argv = Minimist(process.argv.slice(2), { boolean: ['pretty','debug','givenPortOnly'], - string: ['guildId'] + string: ['guildId','listenUdpPort'] }); const debug = argv.debug; @@ -41,7 +41,8 @@ if (givenPortOnly) { options.givenPortOnly = true; } -Gamedig.query(options) +const gamedig = new Gamedig(options); +gamedig.query(options) .then((state) => { if(pretty) { console.log(JSON.stringify(state,null,' ')); diff --git a/lib/GlobalUdpSocket.js b/lib/GlobalUdpSocket.js index 590ee19..99a6c13 100644 --- a/lib/GlobalUdpSocket.js +++ b/lib/GlobalUdpSocket.js @@ -1,25 +1,29 @@ -const dgram = require('dgram'), - HexUtil = require('./HexUtil'), - Logger = require('./Logger'); +const dgram = require('dgram'); +const HexUtil = require('./HexUtil'); +const Logger = require('./Logger'); +const util = require('util'); class GlobalUdpSocket { - constructor() { + constructor({port}) { this.socket = null; this.callbacks = new Set(); this.debuggingCallbacks = new Set(); this.logger = new Logger(); + this.port = port; } - _getSocket() { + async _getSocket() { if (!this.socket) { - const udpSocket = this.socket = dgram.createSocket('udp4'); + const udpSocket = dgram.createSocket({ + type: 'udp4', + reuseAddr: true + }); udpSocket.unref(); - udpSocket.bind(); udpSocket.on('message', (buffer, rinfo) => { const fromAddress = rinfo.address; const fromPort = rinfo.port; this.logger.debug(log => { - log(fromAddress + ':' + fromPort + " <--UDP"); + log(fromAddress + ':' + fromPort + " <--UDP(" + this.port + ")"); log(HexUtil.debugDump(buffer)); }); for (const cb of this.callbacks) { @@ -29,12 +33,22 @@ class GlobalUdpSocket { udpSocket.on('error', e => { this.logger.debug("UDP ERROR:", e); }); + await util.promisify(udpSocket.bind).bind(udpSocket)(this.port); + this.port = udpSocket.address().port; + this.socket = udpSocket; } return this.socket; } - send(buffer, address, port) { - this._getSocket().send(buffer,0,buffer.length,port,address); + async send(buffer, address, port, debug) { + const socket = await this._getSocket(); + if (debug) { + this.logger._print(log => { + log(address + ':' + port + " UDP(" + this.port + ")-->"); + log(HexUtil.debugDump(buffer)); + }); + } + await util.promisify(socket.send).bind(socket)(buffer,0,buffer.length,port,address); } addCallback(callback, debug) { diff --git a/lib/QueryRunner.js b/lib/QueryRunner.js index 31eaac0..47244b7 100644 --- a/lib/QueryRunner.js +++ b/lib/QueryRunner.js @@ -9,8 +9,10 @@ const defaultOptions = { }; class QueryRunner { - constructor() { - this.udpSocket = new GlobalUdpSocket(); + constructor(runnerOpts = {}) { + this.udpSocket = new GlobalUdpSocket({ + port: runnerOpts.listenUdpPort + }); this.gameResolver = new GameResolver(); this.protocolResolver = new ProtocolResolver(); } diff --git a/lib/index.js b/lib/index.js index 4ded359..b1a1f3f 100644 --- a/lib/index.js +++ b/lib/index.js @@ -3,8 +3,8 @@ const QueryRunner = require('./QueryRunner'); let singleton = null; class Gamedig { - constructor() { - this.queryRunner = new QueryRunner(); + constructor(runnerOpts) { + this.queryRunner = new QueryRunner(runnerOpts); } async query(userOptions) { diff --git a/package.json b/package.json index d965d6b..0372cef 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ ], "main": "lib/index.js", "author": "GameDig Contributors", - "version": "3.0.4", + "version": "3.0.5", "repository": { "type": "git", "url": "https://github.com/gamedig/node-gamedig.git" diff --git a/protocols/core.js b/protocols/core.js index 3e7d1e1..a2c7465 100644 --- a/protocols/core.js +++ b/protocols/core.js @@ -252,13 +252,9 @@ class Core extends EventEmitter { this.assertValidPort(port); if(typeof buffer === 'string') buffer = Buffer.from(buffer,'binary'); - this.debugLog(log => { - log(address+':'+port+" UDP-->"); - log(HexUtil.debugDump(buffer)); - }); const socket = this.udpSocket; - socket.send(buffer, address, port); + await socket.send(buffer, address, port, this.options.debug); if (!onPacket && !onTimeout) { return null;