diff --git a/.ci/bindcov.sh b/.ci/bindcov.sh index 4b76f2bd..37508d78 100755 --- a/.ci/bindcov.sh +++ b/.ci/bindcov.sh @@ -26,7 +26,7 @@ skipped+=( uv_loop_size uv_loop_fork uv_loop_get_data uv_loop_set_data uv_strerror uv_strerror_r uv_err_name uv_err_name_r uv_handle_size uv_handle_get_type uv_handle_type_name uv_handle_get_data uv_handle_get_loop uv_handle_set_data uv_req_size uv_req_get_data uv_req_set_data uv_req_get_type - uv_req_type_name uv_udp_set_source_membership uv_pipe_chmod uv_process_get_pid uv_get_osfhandle + uv_req_type_name uv_pipe_chmod uv_process_get_pid uv_get_osfhandle uv_open_osfhandle uv_fs_get_type uv_fs_get_result uv_fs_get_ptr uv_fs_get_path uv_fs_get_statbuf uv_ip4_addr uv_ip6_addr uv_ip4_name uv_ip6_name uv_inet_ntop uv_inet_pton uv_dlopen uv_dlclose uv_dlsym uv_dlerror diff --git a/.ci/libuv2840.supp b/.ci/libuv2840.supp new file mode 100644 index 00000000..5b98af80 --- /dev/null +++ b/.ci/libuv2840.supp @@ -0,0 +1,21 @@ +{ + + Memcheck:Param + socketcall.setsockopt(optval) + fun:setsockopt + fun:uv__udp_set_source_membership6 + fun:uv_udp_set_source_membership + fun:luv_udp_set_source_membership + fun:luaD_precall + fun:luaV_execute + fun:luaD_call + fun:luaD_callnoyield + fun:f_call + fun:luaD_rawrunprotected + fun:luaD_pcall + fun:lua_pcallk + fun:luv_cfpcall + fun:uv__udp_recvmsg + fun:uv__udp_io +} + diff --git a/.travis.yml b/.travis.yml index 84b97482..cf294ce8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -73,7 +73,10 @@ jobs: - WITH_LUA_ENGINE=Lua script: - make - - valgrind --error-exitcode=1 --leak-check=full --child-silent-after-fork=yes ./build/lua tests/run.lua + # --suppressions should be removed once + # https://github.com/libuv/libuv/issues/2840 + # is closed and we update to the fixed libuv + - valgrind --suppressions=.ci/libuv2840.supp --error-exitcode=1 --leak-check=full --child-silent-after-fork=yes ./build/lua tests/run.lua - name: process cleanup test os: linux env: diff --git a/docs.md b/docs.md index 379949d7..c1df5985 100644 --- a/docs.md +++ b/docs.md @@ -1884,7 +1884,7 @@ Get the remote IP and port of the UDP handle on connected UDP handles. **Parameters:** - `udp`: `uv_udp_t userdata` - `multicast_addr`: `string` -- `interface_addr`: `string` +- `interface_addr`: `string` or `nil` - `membership`: `string` Set membership for a multicast address. `multicast_addr` is multicast address to @@ -1893,6 +1893,23 @@ the string `"leave"` or `"join"`. **Returns:** `0` or `fail` +### `uv.udp_set_source_membership(udp, multicast_addr, interface_addr, source_addr, membership)` + +> method form `udp:set_source_membership(multicast_addr, interface_addr, source_addr, membership)` + +**Parameters:** +- `udp`: `uv_udp_t userdata` +- `multicast_addr`: `string` +- `interface_addr`: `string` or `nil` +- `source_addr`: `string` +- `membership`: `string` + +Set membership for a source-specific multicast group. `multicast_addr` is multicast +address to set membership for. `interface_addr` is interface address. `source_addr` +is source address. `membership` can be the string `"leave"` or `"join"`. + +**Returns:** `0` or `fail` + ### `uv.udp_set_multicast_loop(udp, on)` > method form `udp:set_multicast_loop(on)` diff --git a/src/luv.c b/src/luv.c index eea50d66..4e480e6c 100644 --- a/src/luv.c +++ b/src/luv.c @@ -184,6 +184,9 @@ static const luaL_Reg luv_functions[] = { {"udp_bind", luv_udp_bind}, {"udp_getsockname", luv_udp_getsockname}, {"udp_set_membership", luv_udp_set_membership}, +#if LUV_UV_VERSION_GEQ(1, 32, 0) + {"udp_set_source_membership", luv_udp_set_source_membership}, +#endif {"udp_set_multicast_loop", luv_udp_set_multicast_loop}, {"udp_set_multicast_ttl", luv_udp_set_multicast_ttl}, {"udp_set_multicast_interface", luv_udp_set_multicast_interface}, @@ -488,6 +491,9 @@ static const luaL_Reg luv_udp_methods[] = { {"bind", luv_udp_bind}, {"getsockname", luv_udp_getsockname}, {"set_membership", luv_udp_set_membership}, +#if LUV_UV_VERSION_GEQ(1, 32, 0) + {"set_source_membership", luv_udp_set_source_membership}, +#endif {"set_multicast_loop", luv_udp_set_multicast_loop}, {"set_multicast_ttl", luv_udp_set_multicast_ttl}, {"set_multicast_interface", luv_udp_set_multicast_interface}, diff --git a/src/udp.c b/src/udp.c index 4129d9df..c8982003 100644 --- a/src/udp.c +++ b/src/udp.c @@ -120,12 +120,26 @@ static const char *const luv_membership_opts[] = { static int luv_udp_set_membership(lua_State* L) { uv_udp_t* handle = luv_check_udp(L, 1); const char* multicast_addr = luaL_checkstring(L, 2); - const char* interface_addr = luaL_checkstring(L, 3); + const char* interface_addr = lua_isstring(L, 3) ? lua_tostring(L, 3) : NULL; + luaL_argcheck(L, lua_isstring(L, 3) || lua_isnil(L, 3), 3, "expected string or nil"); uv_membership membership = (uv_membership)luaL_checkoption(L, 4, NULL, luv_membership_opts); int ret = uv_udp_set_membership(handle, multicast_addr, interface_addr, membership); return luv_result(L, ret); } +#if LUV_UV_VERSION_GEQ(1, 32, 0) +static int luv_udp_set_source_membership(lua_State* L) { + uv_udp_t* handle = luv_check_udp(L, 1); + const char* multicast_addr = luaL_checkstring(L, 2); + const char* interface_addr = lua_isstring(L, 3) ? lua_tostring(L, 3) : NULL; + luaL_argcheck(L, lua_isstring(L, 3) || lua_isnil(L, 3), 3, "expected string or nil"); + const char* source_addr = luaL_checkstring(L, 4); + uv_membership membership = (uv_membership)luaL_checkoption(L, 5, NULL, luv_membership_opts); + int ret = uv_udp_set_source_membership(handle, multicast_addr, interface_addr, source_addr, membership); + return luv_result(L, ret); +} +#endif + static int luv_udp_set_multicast_loop(lua_State* L) { uv_udp_t* handle = luv_check_udp(L, 1); int on, ret; diff --git a/tests/test-udp.lua b/tests/test-udp.lua index 39fe946b..d1bb6ad3 100644 --- a/tests/test-udp.lua +++ b/tests/test-udp.lua @@ -169,4 +169,98 @@ return require('lib/tap')(function (test) end)) end))) end, "1.27.0") + + -- return a test function reusable for ipv4 and ipv6 + local function multicast_join_test(bind_addr, multicast_addr, interface_addr) + return function(print, p, expect, uv) + local uvVersionGEQ = require('lib/utils').uvVersionGEQ + + local server = assert(uv.new_udp()) + assert(uv.udp_bind(server, bind_addr, TEST_PORT)) + local _, err, errname = uv.udp_set_membership(server, multicast_addr, interface_addr, "join") + if errname == "ENODEV" then + print("no ipv6 multicast route, skipping") + server:close() + return + elseif errname == "EADDRNOTAVAIL" and multicast_addr == "ff02::1" then + -- OSX, BSDs, and some other platforms need %lo in their multicast/interface addr + -- so try that instead + multicast_addr = "ff02::1%lo0" + interface_addr = "::1%lo0" + assert(uv.udp_set_membership(server, multicast_addr, interface_addr, "join")) + else + assert(not err, err) + end + + local client = assert(uv.new_udp()) + + local recv_cb_called = 0 + local function recv_cb(err, data, addr, flags) + assert(not err, err) + p(data, addr) + + -- empty callback can happen, just return early + if data == nil and addr == nil then + return + end + + assert(addr) + assert(data == "PING") + + recv_cb_called = recv_cb_called + 1 + if recv_cb_called == 2 then + -- note: because of this conditional close, the test will fail with an unclosed handle if recv_cb_called + -- doesn't hit 2, so we don't need to expect(recv_cb) or assert recv_cb_called == 2 + server:close() + else + -- udp_set_source_membership added in 1.32.0 + if uvVersionGEQ("1.32.0") then + local source_addr = addr.ip + assert(server:set_membership(multicast_addr, interface_addr, "leave")) + _, err, errname = server:set_source_membership(multicast_addr, interface_addr, source_addr, "join") + if errname == "ENOSYS" then + -- not all systems support set_source_membership, so rejoin the previous group and continue on + assert(server:set_membership(multicast_addr, interface_addr, "join")) + else + assert(not err, err) + end + end + assert(client:send("PING", multicast_addr, TEST_PORT, expect(function(err) + assert(not err, err) + client:close() + end))) + end + end + + server:recv_start(recv_cb) + + assert(client:send("PING", multicast_addr, TEST_PORT, expect(function(err) + assert(not err, err) + end))) + end + end + + test("udp multicast join ipv4", multicast_join_test("0.0.0.0", "239.255.0.1", nil)) + + test("udp multicast join ipv6", function(print, p, expect, uv) + local function can_ipv6_external() + local addresses = assert(uv.interface_addresses()) + for _, vals in pairs(addresses) do + for _, info in ipairs(vals) do + if info.family == "inet6" and not info.internal then + return true + end + end + end + return false + end + + if not can_ipv6_external() then + print("no ipv6 support, skipping") + return + end + + local testfn = multicast_join_test("::", "ff02::1", nil) + return testfn(print, p, expect, uv) + end) end)