From dd7f789b852a544858d807a0aa62eccede96edf8 Mon Sep 17 00:00:00 2001 From: Joshua Reich Date: Fri, 24 Jul 2026 10:48:31 +1000 Subject: [PATCH] Resolve target hostnames to IPv4 before connecting the OSC sender MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sending to a hostname that resolves to an IPv6 address first — most commonly an mDNS ".local" name — fails silently: no packets are sent, but nothing reports an error. JUCE creates its datagram socket as IPv4-only: handle = (int) socket (AF_INET, SOCK_DGRAM, 0); // juce_Socket.cpp yet SocketHelpers::getAddressInfo() resolves with hints.ai_family = AF_UNSPEC; and DatagramSocket::write() then calls sendto() on info->ai_addr, the first entry of the returned list, without walking it. When that first entry is IPv6 it is handed to an AF_INET socket and every write fails. Nothing surfaces the failure. OSCSender::connect() only binds a local port and never resolves the target, so it returns true regardless, and both call sites discard the bool that send() returns. The plugin loads, controls move, host automation runs, and no packets leave the machine. Resolve the host to an IPv4 literal before handing it to JUCE. A dotted-quad can only ever produce an AF_INET result, so the mismatch cannot arise. If the lookup fails or returns no IPv4 address the input is passed through unchanged, leaving behaviour no worse than before, and addresses that are already literals are unaffected. Applied at both connect sites: initializeHeadless() for preset-built plugins and PresetPage::connectOsc() for the GUI build. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01TfUqNJxtEHdgeKitDnNktr --- CMakeLists.txt | 1 + Source/HostResolver.h | 94 ++++++++++++++++++++++++++++++++++++++ Source/PluginProcessor.cpp | 5 +- Source/PresetPage.cpp | 4 +- 4 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 Source/HostResolver.h diff --git a/CMakeLists.txt b/CMakeLists.txt index f495fb8..e45845f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -42,6 +42,7 @@ function(configure_osccontrol_target TARGET) Source/ControlElementToggle.h Source/ControlElementUI.cpp Source/ControlElementUI.h + Source/HostResolver.h Source/LayoutHints.cpp Source/LayoutHints.h Source/PluginEditor.cpp diff --git a/Source/HostResolver.h b/Source/HostResolver.h new file mode 100644 index 0000000..a6fcb7a --- /dev/null +++ b/Source/HostResolver.h @@ -0,0 +1,94 @@ +/* + + osccontrol-light - An audio plugin that speaks OSC. + Copyright (C) 2020 Patric Schmitz + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +*/ + +#pragma once + +#include "JuceHeader.h" + +#if JUCE_WINDOWS + #include +#else + #include + #include + #include +#endif + +/** Resolve a hostname to its first IPv4 address, as a dotted-quad string. + + JUCE creates its datagram socket as IPv4-only: + + handle = (int) socket (AF_INET, SOCK_DGRAM, 0); // juce_Socket.cpp + + but resolves target names with hints.ai_family = AF_UNSPEC, and + DatagramSocket::write() then calls sendto() on info->ai_addr - the first + entry of the returned list, without walking it. When a name resolves to an + IPv6 address first, that address is handed to an AF_INET socket and every + send fails. + + This is easy to hit with mDNS ".local" names, which commonly resolve to an + IPv6 link-local address ahead of the IPv4 one. The failure is silent from + the user's point of view: OSCSender::connect() only binds a local port and + never resolves the target, so it still reports success, and the plugin + appears to work while no packets leave the machine. + + Resolving to an IPv4 literal before handing the host to JUCE avoids the + problem entirely, since a dotted-quad can only produce an AF_INET result. + + Returns the input unchanged if the lookup fails or yields no IPv4 address, + so behaviour is never worse than before this call existed. Addresses that + are already literals pass through untouched. +*/ +inline juce::String resolveHostToIPv4 (const juce::String & host) +{ + if (host.isEmpty()) + return host; + + struct addrinfo hints; + juce::zerostruct (hints); + + hints.ai_family = AF_INET; // IPv4 only, to match JUCE's socket + hints.ai_socktype = SOCK_DGRAM; + + struct addrinfo * info = nullptr; + + if (getaddrinfo (host.toRawUTF8(), nullptr, &hints, &info) != 0 + || info == nullptr) + return host; + + juce::String result (host); + + for (auto * i = info; i != nullptr; i = i->ai_next) + { + if (i->ai_family != AF_INET || i->ai_addr == nullptr) + continue; + + auto * addr = reinterpret_cast (i->ai_addr); + auto raw = (juce::uint32) ntohl (addr->sin_addr.s_addr); + + result = juce::String ((raw >> 24) & 0xff) + "." + + juce::String ((raw >> 16) & 0xff) + "." + + juce::String ((raw >> 8) & 0xff) + "." + + juce::String ( raw & 0xff); + break; + } + + freeaddrinfo (info); + return result; +} diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index 221fcce..6a00e25 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -26,6 +26,8 @@ #include "PresetParser.h" +#include "HostResolver.h" + #include "ControlElementHost.h" #include "ControlElementFactory.h" @@ -130,7 +132,8 @@ initializeHeadless PresetParser preset (inputStream); oscSender = std::make_unique (); - oscSender->connect (preset.getHost (), preset.getPort ()); + oscSender->connect (resolveHostToIPv4 (preset.getHost ()), + preset.getPort ()); ControlElementFactory factory (*oscSender); for(auto control : preset.getControlElements ()) { diff --git a/Source/PresetPage.cpp b/Source/PresetPage.cpp index 29fc9be..b7f7ab2 100644 --- a/Source/PresetPage.cpp +++ b/Source/PresetPage.cpp @@ -20,6 +20,8 @@ #include "PresetParser.h" +#include "HostResolver.h" + #include "LayoutHints.h" #include "ControlContainer.h" @@ -128,7 +130,7 @@ connectOsc () + host.toString () + ":" + port.toString(); Logger::writeToLog(message); - oscSender.connect(host.getValue (), port.getValue ()); + oscSender.connect(resolveHostToIPv4 (host.toString ()), port.getValue ()); for (auto & control : container->getControlElements ()) { control->setEnabled(true);