mirror of https://github.com/lukechilds/node.git
Browse Source
Add wrapper for uv's uv_udp_set_multicast_interface which provides the sender side mechanism to explicitly select an interface. The equivalent receiver side mechanism is the optional 2nd argument of addMembership(). PR-URL: https://github.com/nodejs/node/pull/7855 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>canary-base
Will Young
9 years ago
committed by
Matteo Collina
7 changed files with 530 additions and 1 deletions
@ -0,0 +1,293 @@ |
|||
'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); |
|||
} |
@ -0,0 +1,119 @@ |
|||
'use strict'; |
|||
const common = require('../common'); |
|||
const assert = require('assert'); |
|||
const dgram = require('dgram'); |
|||
|
|||
{ |
|||
const socket = dgram.createSocket('udp4'); |
|||
|
|||
socket.bind(0); |
|||
socket.on('listening', common.mustCall(() => { |
|||
// Explicitly request default system selection
|
|||
socket.setMulticastInterface('0.0.0.0'); |
|||
|
|||
socket.close(); |
|||
})); |
|||
} |
|||
|
|||
{ |
|||
const socket = dgram.createSocket('udp4'); |
|||
|
|||
socket.bind(0); |
|||
socket.on('listening', common.mustCall(() => { |
|||
socket.close(common.mustCall(() => { |
|||
assert.throws(() => { socket.setMulticastInterface('0.0.0.0'); }, |
|||
/Not running/); |
|||
})); |
|||
})); |
|||
} |
|||
|
|||
{ |
|||
const socket = dgram.createSocket('udp4'); |
|||
|
|||
socket.bind(0); |
|||
socket.on('listening', common.mustCall(() => { |
|||
// Try to set with an invalid interfaceAddress (wrong address class)
|
|||
try { |
|||
socket.setMulticastInterface('::'); |
|||
throw new Error('Not detected.'); |
|||
} catch (e) { |
|||
console.error(`setMulticastInterface: wrong family error is: ${e}`); |
|||
} |
|||
|
|||
socket.close(); |
|||
})); |
|||
} |
|||
|
|||
{ |
|||
const socket = dgram.createSocket('udp4'); |
|||
|
|||
socket.bind(0); |
|||
socket.on('listening', common.mustCall(() => { |
|||
// Try to set with an invalid interfaceAddress (wrong Type)
|
|||
assert.throws(() => { |
|||
socket.setMulticastInterface(1); |
|||
}, /TypeError/); |
|||
|
|||
socket.close(); |
|||
})); |
|||
} |
|||
|
|||
{ |
|||
const socket = dgram.createSocket('udp4'); |
|||
|
|||
socket.bind(0); |
|||
socket.on('listening', common.mustCall(() => { |
|||
// Try to set with an invalid interfaceAddress (non-unicast)
|
|||
assert.throws(() => { |
|||
socket.setMulticastInterface('224.0.0.2'); |
|||
}, /Error/); |
|||
|
|||
socket.close(); |
|||
})); |
|||
} |
|||
|
|||
if (!common.hasIPv6) { |
|||
common.skip('Skipping udp6 tests, no IPv6 support.'); |
|||
return; |
|||
} |
|||
|
|||
{ |
|||
const socket = dgram.createSocket('udp6'); |
|||
|
|||
socket.bind(0); |
|||
socket.on('listening', common.mustCall(() => { |
|||
// Try to set with an invalid interfaceAddress ('undefined')
|
|||
assert.throws(() => { |
|||
socket.setMulticastInterface(String(undefined)); |
|||
}, /EINVAL/); |
|||
|
|||
socket.close(); |
|||
})); |
|||
} |
|||
|
|||
{ |
|||
const socket = dgram.createSocket('udp6'); |
|||
|
|||
socket.bind(0); |
|||
socket.on('listening', common.mustCall(() => { |
|||
// Try to set with an invalid interfaceAddress ('')
|
|||
assert.throws(() => { |
|||
socket.setMulticastInterface(''); |
|||
}, /EINVAL/); |
|||
|
|||
socket.close(); |
|||
})); |
|||
} |
|||
|
|||
{ |
|||
const socket = dgram.createSocket('udp6'); |
|||
|
|||
socket.bind(0); |
|||
socket.on('listening', common.mustCall(() => { |
|||
// Using lo0 for OsX, on all other OSes, an invalid Scope gets
|
|||
// turned into #0 (default selection) which is also acceptable.
|
|||
socket.setMulticastInterface('::%lo0'); |
|||
|
|||
socket.close(); |
|||
})); |
|||
} |
Loading…
Reference in new issue