From 67593d4995db602c253d2389fb448f47b6473a87 Mon Sep 17 00:00:00 2001 From: Heiko Rothe Date: Thu, 20 Feb 2020 20:44:17 +0100 Subject: [PATCH] feat(cluster): add an option to resolve mdns hostnames with dig libnss-mdns is complicated and does not work on Alpine, which means the auto discovery could never resolve the hosts on such systems. This adds an option to resolve the hostname using dig instead, which is especially great for Docker images to use. --- bin/room-assistant.js | 10 ++++ src/cluster/cluster.service.ts | 9 ++-- src/cluster/resolvers.spec.ts | 91 ++++++++++++++++++++++++++++++++++ src/cluster/resolvers.ts | 22 ++++++++ 4 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 src/cluster/resolvers.spec.ts create mode 100644 src/cluster/resolvers.ts diff --git a/bin/room-assistant.js b/bin/room-assistant.js index 2183d44..588dc43 100644 --- a/bin/room-assistant.js +++ b/bin/room-assistant.js @@ -17,6 +17,12 @@ const optionDefinitions = [ type: String, defaultValue: './config' }, + { + name: 'digResolver', + description: + 'Use dig to resolve mdns hostnames instead of the native getaddrinfo.', + type: Boolean + }, { name: 'verbose', description: 'Turn on debugging output.', @@ -47,4 +53,8 @@ process.env.NODE_LOG_LEVEL = options.verbose : process.env.NODE_LOG_LEVEL || 'production'; process.env.NODE_CONFIG_DIR = options.config; +if (options.digResolver) { + process.env.NODE_DIG_RESOLVER = true; +} + require('../dist/main'); diff --git a/src/cluster/cluster.service.ts b/src/cluster/cluster.service.ts index 43a7b07..429676c 100644 --- a/src/cluster/cluster.service.ts +++ b/src/cluster/cluster.service.ts @@ -13,6 +13,7 @@ import { NetworkInterfaceInfo } from 'os'; import { ConfigService } from '../config/config.service'; import { ClusterConfig } from './cluster.config'; import { makeId } from '../util/id'; +import { getAddrInfoDig } from './resolvers'; let mdns; try { @@ -137,11 +138,13 @@ export class ClusterService extends Democracy networkInterface: this.config.networkInterface } ); - const sequence = [ - mdns.rst.DNSServiceResolve(), + const defaultGetAddr = 'DNSServiceGetAddrInfo' in mdns.dns_sd ? mdns.rst.DNSServiceGetAddrInfo() - : mdns.rst.getaddrinfo({ families: [0] }), + : mdns.rst.getaddrinfo({ families: [0] }); + const sequence = [ + mdns.rst.DNSServiceResolve(), + process.env.NODE_DIG_RESOLVER ? getAddrInfoDig : defaultGetAddr, mdns.rst.makeAddressesUnique() ]; this.browser = mdns.createBrowser(mdns.udp('room-assistant'), { diff --git a/src/cluster/resolvers.spec.ts b/src/cluster/resolvers.spec.ts new file mode 100644 index 0000000..ae4363f --- /dev/null +++ b/src/cluster/resolvers.spec.ts @@ -0,0 +1,91 @@ +const mockExec = jest.fn(); + +import { getAddrInfoDig } from './resolvers'; +import { Service } from 'mdns'; + +jest.mock('util', () => ({ + ...jest.requireActual('util'), + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + promisify: () => mockExec +})); + +describe('resolvers', () => { + beforeEach(async () => { + jest.clearAllMocks(); + }); + + it('should return a single address correctly', done => { + const service: Service = { + addresses: undefined, + flags: undefined, + fullname: undefined, + host: undefined, + interfaceIndex: undefined, + networkInterface: undefined, + port: undefined, + replyDomain: undefined, + type: undefined + }; + mockExec.mockResolvedValue({ stdout: '192.168.1.20\n' }); + + getAddrInfoDig(service, () => { + try { + expect(service.addresses).toStrictEqual(['192.168.1.20']); + done(); + } catch (e) { + done(e); + } + }); + }); + + it('should resolve multiple addresses correctly', done => { + const service: Service = { + addresses: undefined, + flags: undefined, + fullname: undefined, + host: undefined, + interfaceIndex: undefined, + networkInterface: undefined, + port: undefined, + replyDomain: undefined, + type: undefined + }; + mockExec.mockResolvedValue({ stdout: '192.168.1.20\n192.168.1.21\n' }); + + getAddrInfoDig(service, () => { + try { + expect(service.addresses).toStrictEqual([ + '192.168.1.20', + '192.168.1.21' + ]); + done(); + } catch (e) { + done(e); + } + }); + }); + + it('should pass errors to the callback', done => { + const service: Service = { + addresses: undefined, + flags: undefined, + fullname: undefined, + host: undefined, + interfaceIndex: undefined, + networkInterface: undefined, + port: undefined, + replyDomain: undefined, + type: undefined + }; + mockExec.mockRejectedValue({ stdout: 'dig not found' }); + + getAddrInfoDig(service, e => { + try { + expect(e).not.toBeUndefined(); + done(); + } catch (e) { + done(e); + } + }); + }); +}); diff --git a/src/cluster/resolvers.ts b/src/cluster/resolvers.ts new file mode 100644 index 0000000..8b439f7 --- /dev/null +++ b/src/cluster/resolvers.ts @@ -0,0 +1,22 @@ +import { Service } from 'mdns'; +import * as util from 'util'; +import { exec } from 'child_process'; +import * as os from 'os'; + +const execPromise = util.promisify(exec); + +export async function getAddrInfoDig( + service: Service, + next: (e?: Error) => void +): Promise { + try { + const digOutput = await execPromise( + `dig +short @224.0.0.251 -p 5353 -4 ${service.host}` + ); + const addresses = digOutput.stdout.trim().split(os.EOL); + service.addresses = (service.addresses || []).concat(addresses); + next(); + } catch (e) { + next(e); + } +}