Skip to content

Resolve target hostnames to IPv4 before connecting the OSC sender - #48

Open
i2pi wants to merge 1 commit into
drlight-code:mainfrom
i2pi:fix/resolve-hostname-to-ipv4
Open

Resolve target hostnames to IPv4 before connecting the OSC sender#48
i2pi wants to merge 1 commit into
drlight-code:mainfrom
i2pi:fix/resolve-hostname-to-ipv4

Conversation

@i2pi

@i2pi i2pi commented Jul 24, 2026

Copy link
Copy Markdown

Problem

Sending to a hostname that resolves to an IPv6 address first — most commonly an mDNS .local name — fails silently. The plugin loads, controls move, host automation runs, and no packets leave the machine.

I hit this driving a device at foo.local from a preset-built plugin. Everything looked correct; nothing was being sent.

Cause

JUCE creates its datagram socket as IPv4-only (juce_Socket.cpp):

handle = (int) socket (AF_INET, SOCK_DGRAM, 0);

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:

return (int) ::sendto ((SocketHandle) handle.load(), ...,
                       info->ai_addr, (socklen_t) info->ai_addrlen);

When that first entry is IPv6 it is handed to an AF_INET socket and every write fails. mDNS names hit this routinely, since they commonly resolve to an IPv6 link-local address ahead of the IPv4 one.

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.

Fix

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.

Applied at both connect sites — initializeHeadless() for preset-built plugins and PresetPage::connectOsc() for the GUI build.

The helper fails soft: if the lookup fails or returns no IPv4 address, the input is passed through unchanged, so behaviour is never worse than before. Addresses that are already literals are unaffected.

Verification

Reproduced and confirmed with the same preset text, changing only the host: field:

host: result
foo.local (before fix) every send fails
raw IPv4 literal (before fix) every send succeeds
foo.local (after fix) every send succeeds

Verified on macOS with both the GUI build and a preset-built headless plugin, hosted in a DAW. Builds clean.

Notes

  • Header-only, no new dependencies. getaddrinfo is available on all three platforms; includes are guarded for Windows via ws2tcpip.h.
  • The IP is resolved once, at connect time. A host whose address changes mid-session keeps sending to the old one until reconnected — same as the previous behaviour, which cached lastServerAddress in JUCE.
  • Not addressed here, but worth flagging separately: both connect() and send() return values are discarded, which is what made this silent. Surfacing them would turn this class of failure into something visible.

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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TfUqNJxtEHdgeKitDnNktr
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant