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