'use strict'; const common = require('../common'); const assert = require('assert'); const dgram = require('dgram'); const util = require('util'); if (common.inFreeBSDJail) { common.skip('in a FreeBSD jail'); return; } // All SunOS systems must be able to pass this manual test before the // following barrier can be removed: // $ socat UDP-RECVFROM:12356,ip-add-membership=224.0.0.115:127.0.0.1,fork \ // EXEC:hostname & // $ echo hi |socat STDIO \ // UDP4-DATAGRAM:224.0.0.115:12356,ip-multicast-if=127.0.0.1 if (common.isSunOS) { common.skip('SunOs is not correctly delivering to loopback multicast.'); return; } const networkInterfaces = require('os').networkInterfaces(); const fork = require('child_process').fork; const MULTICASTS = { IPv4: ['224.0.0.115', '224.0.0.116', '224.0.0.117'], IPv6: ['ff02::1:115', 'ff02::1:116', 'ff02::1:117'] }; const LOOPBACK = { IPv4: '127.0.0.1', IPv6: '::1' }; const ANY = { IPv4: '0.0.0.0', IPv6: '::' }; const FAM = 'IPv4'; // Windows wont bind on multicasts so its filtering is by port. const PORTS = {}; for (let i = 0; i < MULTICASTS[FAM].length; i++) { PORTS[MULTICASTS[FAM][i]] = common.PORT + (common.isWindows ? i : 0); } const UDP = { IPv4: 'udp4', IPv6: 'udp6' }; const TIMEOUT = common.platformTimeout(5000); const NOW = Date.now(); const TMPL = (tail) => `${NOW} - ${tail}`; // Take the first non-internal interface as the other interface to isolate // from loopback. Ideally, this should check for whether or not this interface // and the loopback have the MULTICAST flag. const interfaceAddress = ((networkInterfaces) => { for (const name in networkInterfaces) { for (const localInterface of networkInterfaces[name]) { if (!localInterface.internal && localInterface.family === FAM) { let interfaceAddress = localInterface.address; // On Windows, IPv6 would need: `%${localInterface.scopeid}` if (FAM === 'IPv6') interfaceAddress += `${interfaceAddress}%${name}`; return interfaceAddress; } } } })(networkInterfaces); assert.ok(interfaceAddress); const messages = [ { tail: 'First message to send', mcast: MULTICASTS[FAM][0], rcv: true }, { tail: 'Second message to send', mcast: MULTICASTS[FAM][0], rcv: true }, { tail: 'Third message to send', mcast: MULTICASTS[FAM][1], rcv: true, newAddr: interfaceAddress }, { tail: 'Fourth message to send', mcast: MULTICASTS[FAM][2] }, { tail: 'Fifth message to send', mcast: MULTICASTS[FAM][1], rcv: true }, { tail: 'Sixth message to send', mcast: MULTICASTS[FAM][2], rcv: true, newAddr: LOOPBACK[FAM] } ]; if (process.argv[2] !== 'child') { const IFACES = [ANY[FAM], interfaceAddress, LOOPBACK[FAM]]; const workers = {}; const listeners = MULTICASTS[FAM].length * 2; let listening = 0; let dead = 0; let i = 0; let done = 0; let timer = null; // Exit the test if it doesn't succeed within the TIMEOUT. timer = setTimeout(function() { console.error('[PARENT] Responses were not received within %d ms.', TIMEOUT); console.error('[PARENT] Skip'); killSubprocesses(workers); common.skip('Check filter policy'); process.exit(1); }, TIMEOUT); // Launch the child processes. for (let i = 0; i < listeners; i++) { const IFACE = IFACES[i % IFACES.length]; const MULTICAST = MULTICASTS[FAM][i % MULTICASTS[FAM].length]; const messagesNeeded = messages.filter((m) => m.rcv && m.mcast === MULTICAST) .map((m) => TMPL(m.tail)); const worker = fork(process.argv[1], ['child', IFACE, MULTICAST, messagesNeeded.length, NOW]); workers[worker.pid] = worker; worker.messagesReceived = []; worker.messagesNeeded = messagesNeeded; // Handle the death of workers. worker.on('exit', function(code, signal) { // Don't consider this a true death if the worker has finished // successfully or if the exit code is 0. if (worker.isDone || code === 0) { return; } dead += 1; console.error('[PARENT] Worker %d died. %d dead of %d', worker.pid, dead, listeners); if (dead === listeners) { console.error('[PARENT] All workers have died.'); console.error('[PARENT] Fail'); killSubprocesses(workers); process.exit(1); } }); worker.on('message', function(msg) { if (msg.listening) { listening += 1; if (listening === listeners) { // All child process are listening, so start sending. sendSocket.sendNext(); } } else if (msg.message) { worker.messagesReceived.push(msg.message); if (worker.messagesReceived.length === worker.messagesNeeded.length) { done += 1; worker.isDone = true; console.error('[PARENT] %d received %d messages total.', worker.pid, worker.messagesReceived.length); } if (done === listeners) { console.error('[PARENT] All workers have received the ' + 'required number of ' + 'messages. Will now compare.'); Object.keys(workers).forEach(function(pid) { const worker = workers[pid]; let count = 0; worker.messagesReceived.forEach(function(buf) { for (let i = 0; i < worker.messagesNeeded.length; ++i) { if (buf.toString() === worker.messagesNeeded[i]) { count++; break; } } }); console.error('[PARENT] %d received %d matching messages.', worker.pid, count); assert.strictEqual(count, worker.messagesNeeded.length, 'A worker received ' + 'an invalid multicast message'); }); clearTimeout(timer); console.error('[PARENT] Success'); killSubprocesses(workers); } } }); } const sendSocket = dgram.createSocket({ type: UDP[FAM], reuseAddr: true }); // Don't bind the address explicitly when sending and start with // the OSes default multicast interface selection. sendSocket.bind(common.PORT, ANY[FAM]); sendSocket.on('listening', function() { console.error(`outgoing iface ${interfaceAddress}`); }); sendSocket.on('close', function() { console.error('[PARENT] sendSocket closed'); }); sendSocket.sendNext = function() { const msg = messages[i++]; if (!msg) { sendSocket.close(); return; } console.error(TMPL(NOW, msg.tail)); const buf = Buffer.from(TMPL(msg.tail)); if (msg.newAddr) { console.error(`changing outgoing multicast ${msg.newAddr}`); sendSocket.setMulticastInterface(msg.newAddr); } sendSocket.send( buf, 0, buf.length, PORTS[msg.mcast], msg.mcast, function(err) { assert.ifError(err); console.error('[PARENT] sent %s to %s:%s', util.inspect(buf.toString()), msg.mcast, PORTS[msg.mcast]); process.nextTick(sendSocket.sendNext); } ); }; function killSubprocesses(subprocesses) { for (const i in subprocesses) subprocesses[i].kill(); } } if (process.argv[2] === 'child') { const IFACE = process.argv[3]; const MULTICAST = process.argv[4]; const NEEDEDMSGS = Number(process.argv[5]); const SESSION = Number(process.argv[6]); const receivedMessages = []; console.error(`pid ${process.pid} iface ${IFACE} MULTICAST ${MULTICAST}`); const listenSocket = dgram.createSocket({ type: UDP[FAM], reuseAddr: true }); listenSocket.on('message', function(buf, rinfo) { // Examine udp messages only when they were sent by the parent. if (!buf.toString().startsWith(SESSION)) return; console.error('[CHILD] %s received %s from %j', process.pid, util.inspect(buf.toString()), rinfo); receivedMessages.push(buf); let closecb; if (receivedMessages.length === NEEDEDMSGS) { listenSocket.close(); closecb = () => process.exit(); } process.send({ message: buf.toString() }, closecb); }); listenSocket.on('listening', function() { listenSocket.addMembership(MULTICAST, IFACE); process.send({ listening: true }); }); if (common.isWindows) listenSocket.bind(PORTS[MULTICAST], ANY[FAM]); else listenSocket.bind(common.PORT, MULTICAST); }