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);