You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

293 lines
8.7 KiB

'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 Buffer = require('buffer').Buffer;
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');
killChildren(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');
killChildren(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');
killChildren(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 killChildren(children) {
for (const i in children)
children[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);
}