fix: harden localhost resolution and reload transport resilience on Windows#688
fix: harden localhost resolution and reload transport resilience on Windows#688dsarno wants to merge 3 commits intoCoplayDev:betafrom
Conversation
…on Windows On Windows machines where localhost resolves to ::1 (IPv6), the server and Unity plugin could end up on different address families, causing WinError 64 disconnects. Hardcode 127.0.0.1 in all default bind/connect paths to eliminate the ambiguity. Users can still explicitly set localhost if desired. Addresses CoplayDev#672 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reviewer's guide (collapsed on small PRs)Reviewer's GuideThis PR standardizes default loopback usage on the MCP server and Unity plugin by replacing localhost-based defaults with 127.0.0.1 and tightening up HTTP/WebSocket host resolution to avoid IPv4/IPv6 mismatches on Windows, while keeping explicit localhost configurations working as-is. Sequence diagram for default Unity WebSocket connection to MCP serversequenceDiagram
actor UnityDeveloper
participant UnityEditor
participant HttpEndpointUtility
participant WebSocketTransportClient
participant MCPServer_main
UnityDeveloper->>UnityEditor: Start play mode with default settings
UnityEditor->>HttpEndpointUtility: GetLocalBaseUrl()
HttpEndpointUtility-->>UnityEditor: DefaultLocalBaseUrl = http://127.0.0.1:8080
UnityEditor->>WebSocketTransportClient: Connect(baseUrl = http://127.0.0.1:8080)
par Server_startup
UnityDeveloper->>MCPServer_main: python -m src.server --transport http
MCPServer_main->>MCPServer_main: Parse args
MCPServer_main->>MCPServer_main: http_url default = http://127.0.0.1:8080
MCPServer_main->>MCPServer_main: http_host = UNITY_MCP_HTTP_HOST or parsed_url.hostname or 127.0.0.1
MCPServer_main->>MCPServer_main: Bind HTTP server on 127.0.0.1:8080
and Client_connect
WebSocketTransportClient->>WebSocketTransportClient: BuildWebSocketUri(baseUrl)
WebSocketTransportClient->>WebSocketTransportClient: host = httpUri.Host
alt Bind_only_host
WebSocketTransportClient->>WebSocketTransportClient: if host == 0.0.0.0 or ::
WebSocketTransportClient->>WebSocketTransportClient: host = 127.0.0.1
end
WebSocketTransportClient->>MCPServer_main: Open WebSocket to ws://127.0.0.1:8080
end
MCPServer_main-->>WebSocketTransportClient: WebSocket connection established
WebSocketTransportClient-->>UnityEditor: Ready
Class diagram for updated server and Unity networking defaultsclassDiagram
class ServerConfig {
+str unity_host = 127.0.0.1
+int unity_port
+int mcp_port
}
class HttpEndpointUtility {
-string LocalPrefKey
-string RemotePrefKey
-string DefaultLocalBaseUrl = http://127.0.0.1:8080
-string DefaultRemoteBaseUrl
+string GetLocalBaseUrl()
+void SetLocalBaseUrl(string url)
+string GetRemoteBaseUrl()
+void SetRemoteBaseUrl(string url)
}
class WebSocketTransportClient {
+Uri BuildWebSocketUri(string baseUrl)
-Uri NormalizeBaseUrl(string baseUrl)
-void LogConnection(string url)
}
HttpEndpointUtility ..> WebSocketTransportClient : provides_baseUrl
ServerConfig ..> WebSocketTransportClient : server_bind_host
ServerConfig ..> HttpEndpointUtility : aligns_default_host
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
📝 WalkthroughWalkthroughReplaced many implicit "localhost" references with explicit loopback IPs (127.0.0.1 and ::1) across client and server code; added granular handling for bind-only addresses in WebSocket URI construction; widened stdio bridge retry/cleanup and added an assembly-reload stop hook; added/updated tests covering HTTP host defaults. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Hey - I've found 2 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location> `MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs:727-728` </location>
<code_context>
{
- McpLog.Warn($"[WebSocket] Base URL host '{host}' is bind-only; using 'localhost' for client connection.");
- host = "localhost";
+ McpLog.Warn($"[WebSocket] Base URL host '{host}' is bind-only; using '127.0.0.1' for client connection.");
+ host = "127.0.0.1";
}
</code_context>
<issue_to_address>
**issue:** Mapping bind-only host '::' to 127.0.0.1 may break IPv6-only setups
This change rewrites both `0.0.0.0` and `"::"` to `127.0.0.1`. On IPv6-only systems where the server binds to `"::"`, this IPv4-only loopback may fail. Consider mapping `"::"` to `"::1"` and `0.0.0.0` to `127.0.0.1` to avoid bind-only hosts while keeping IPv6-only setups working.
</issue_to_address>
### Comment 2
<location> `Server/tests/test_core_infrastructure_characterization.py:661` </location>
<code_context>
config = ServerConfig()
- assert config.unity_host == "localhost"
+ assert config.unity_host == "127.0.0.1"
assert config.unity_port == 6400
assert config.mcp_port == 6500
</code_context>
<issue_to_address>
**suggestion (testing):** Add tests to cover the new HTTP default host/URL behavior in `main.py`.
This now covers the updated `unity_host` default, but there are still no tests for the new `http_url`/`http_host` defaults and fallbacks in `main.py` (default `--http-url` of `http://127.0.0.1:8080`, `UNITY_MCP_HTTP_URL`/`UNITY_MCP_HTTP_HOST`, and the `127.0.0.1` fallback). Please add tests that:
- Run the argument parser (or `main`) with no HTTP flags and assert host `127.0.0.1` and port `8080`.
- Set `UNITY_MCP_HTTP_URL` and/or `UNITY_MCP_HTTP_HOST` to `localhost` and verify they are honored without being rewritten.
- Set only `UNITY_MCP_HTTP_PORT` and verify the host still falls back to `127.0.0.1`.
This will lock in the behavior described in the PR and help detect regressions if defaults change again.
Suggested implementation:
```python
assert config.unity_host == "127.0.0.1"
assert config.unity_port == 6400
assert config.mcp_port == 6500
assert config.connection_timeout == 30.0
def _parse_http_args(monkeypatch, env_vars, argv=None):
"""Helper to exercise the HTTP argument / env var handling in main.py.
This uses the same argument parser as `main()` so that defaults and
fallbacks are exercised exactly as in production.
"""
from Server import main as server_main
# Clear any pre-existing env that could affect the parser.
for key in ("UNITY_MCP_HTTP_URL", "UNITY_MCP_HTTP_HOST", "UNITY_MCP_HTTP_PORT"):
monkeypatch.delenv(key, raising=False)
# Apply the env vars for this scenario.
for key, value in env_vars.items():
monkeypatch.setenv(key, value)
# The parser is expected to match what `main()` uses.
if hasattr(server_main, "build_argument_parser"):
parser = server_main.build_argument_parser()
else:
# Fall back to a module-level parser if present.
parser = server_main.PARSER # type: ignore[attr-defined]
args = parser.parse_args([] if argv is None else argv)
# Some implementations derive http_host/port from http_url only.
# Normalize them here so the tests are robust to that detail.
http_host = getattr(args, "http_host", None)
http_port = getattr(args, "http_port", None)
http_url = getattr(args, "http_url", None)
if http_url and (http_host is None or http_port is None):
from urllib.parse import urlparse
parsed = urlparse(http_url)
if http_host is None:
http_host = parsed.hostname
if http_port is None:
http_port = parsed.port
return http_host, http_port
def test_http_defaults_no_flags_uses_127_0_0_1_8080(monkeypatch):
"""With no HTTP flags or env, default URL should be http://127.0.0.1:8080."""
http_host, http_port = _parse_http_args(monkeypatch, env_vars={}, argv=None)
assert http_host == "127.0.0.1"
assert http_port == 8080
def test_http_env_url_and_host_localhost_are_honored(monkeypatch):
"""UNITY_MCP_HTTP_URL / UNITY_MCP_HTTP_HOST set to localhost should be honored."""
http_host, http_port = _parse_http_args(
monkeypatch,
env_vars={
"UNITY_MCP_HTTP_URL": "http://localhost:8080",
"UNITY_MCP_HTTP_HOST": "localhost",
},
argv=None,
)
# The explicit localhost values must not be rewritten to 127.0.0.1
assert http_host == "localhost"
assert http_port == 8080
def test_http_env_only_port_falls_back_to_127_0_0_1(monkeypatch):
"""UNITY_MCP_HTTP_PORT alone should keep the host fallback of 127.0.0.1."""
http_host, http_port = _parse_http_args(
monkeypatch,
env_vars={"UNITY_MCP_HTTP_PORT": "9090"},
argv=None,
)
assert http_host == "127.0.0.1"
assert http_port == 9090
```
To make these tests pass, ensure the following in `Server/main.py` (or adjust the test helper accordingly):
1. There is an argument parser used by `main()` that supports the HTTP options and env defaults, exposed either as:
- `build_argument_parser()` returning an `argparse.ArgumentParser`, or
- a module-level `PARSER` object.
2. The parser must:
- Default to `http_url="http://127.0.0.1:8080"` (or equivalent `http_host="127.0.0.1", http_port=8080`) when no HTTP args/env are provided.
- Respect `UNITY_MCP_HTTP_URL` and `UNITY_MCP_HTTP_HOST` when they are set to `localhost` without rewriting them to `127.0.0.1`.
- When only `UNITY_MCP_HTTP_PORT` is set, keep/fall back to a host of `127.0.0.1`.
3. If your parser uses different attribute names than `http_url`, `http_host`, and `http_port`, update `_parse_http_args` to map to the actual attribute names.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs
Show resolved
Hide resolved
| config = ServerConfig() | ||
|
|
||
| assert config.unity_host == "localhost" | ||
| assert config.unity_host == "127.0.0.1" |
There was a problem hiding this comment.
suggestion (testing): Add tests to cover the new HTTP default host/URL behavior in main.py.
This now covers the updated unity_host default, but there are still no tests for the new http_url/http_host defaults and fallbacks in main.py (default --http-url of http://127.0.0.1:8080, UNITY_MCP_HTTP_URL/UNITY_MCP_HTTP_HOST, and the 127.0.0.1 fallback). Please add tests that:
- Run the argument parser (or
main) with no HTTP flags and assert host127.0.0.1and port8080. - Set
UNITY_MCP_HTTP_URLand/orUNITY_MCP_HTTP_HOSTtolocalhostand verify they are honored without being rewritten. - Set only
UNITY_MCP_HTTP_PORTand verify the host still falls back to127.0.0.1.
This will lock in the behavior described in the PR and help detect regressions if defaults change again.
Suggested implementation:
assert config.unity_host == "127.0.0.1"
assert config.unity_port == 6400
assert config.mcp_port == 6500
assert config.connection_timeout == 30.0
def _parse_http_args(monkeypatch, env_vars, argv=None):
"""Helper to exercise the HTTP argument / env var handling in main.py.
This uses the same argument parser as `main()` so that defaults and
fallbacks are exercised exactly as in production.
"""
from Server import main as server_main
# Clear any pre-existing env that could affect the parser.
for key in ("UNITY_MCP_HTTP_URL", "UNITY_MCP_HTTP_HOST", "UNITY_MCP_HTTP_PORT"):
monkeypatch.delenv(key, raising=False)
# Apply the env vars for this scenario.
for key, value in env_vars.items():
monkeypatch.setenv(key, value)
# The parser is expected to match what `main()` uses.
if hasattr(server_main, "build_argument_parser"):
parser = server_main.build_argument_parser()
else:
# Fall back to a module-level parser if present.
parser = server_main.PARSER # type: ignore[attr-defined]
args = parser.parse_args([] if argv is None else argv)
# Some implementations derive http_host/port from http_url only.
# Normalize them here so the tests are robust to that detail.
http_host = getattr(args, "http_host", None)
http_port = getattr(args, "http_port", None)
http_url = getattr(args, "http_url", None)
if http_url and (http_host is None or http_port is None):
from urllib.parse import urlparse
parsed = urlparse(http_url)
if http_host is None:
http_host = parsed.hostname
if http_port is None:
http_port = parsed.port
return http_host, http_port
def test_http_defaults_no_flags_uses_127_0_0_1_8080(monkeypatch):
"""With no HTTP flags or env, default URL should be http://127.0.0.1:8080."""
http_host, http_port = _parse_http_args(monkeypatch, env_vars={}, argv=None)
assert http_host == "127.0.0.1"
assert http_port == 8080
def test_http_env_url_and_host_localhost_are_honored(monkeypatch):
"""UNITY_MCP_HTTP_URL / UNITY_MCP_HTTP_HOST set to localhost should be honored."""
http_host, http_port = _parse_http_args(
monkeypatch,
env_vars={
"UNITY_MCP_HTTP_URL": "http://localhost:8080",
"UNITY_MCP_HTTP_HOST": "localhost",
},
argv=None,
)
# The explicit localhost values must not be rewritten to 127.0.0.1
assert http_host == "localhost"
assert http_port == 8080
def test_http_env_only_port_falls_back_to_127_0_0_1(monkeypatch):
"""UNITY_MCP_HTTP_PORT alone should keep the host fallback of 127.0.0.1."""
http_host, http_port = _parse_http_args(
monkeypatch,
env_vars={"UNITY_MCP_HTTP_PORT": "9090"},
argv=None,
)
assert http_host == "127.0.0.1"
assert http_port == 9090To make these tests pass, ensure the following in Server/main.py (or adjust the test helper accordingly):
- There is an argument parser used by
main()that supports the HTTP options and env defaults, exposed either as:build_argument_parser()returning anargparse.ArgumentParser, or- a module-level
PARSERobject.
- The parser must:
- Default to
http_url="http://127.0.0.1:8080"(or equivalenthttp_host="127.0.0.1", http_port=8080) when no HTTP args/env are provided. - Respect
UNITY_MCP_HTTP_URLandUNITY_MCP_HTTP_HOSTwhen they are set tolocalhostwithout rewriting them to127.0.0.1. - When only
UNITY_MCP_HTTP_PORTis set, keep/fall back to a host of127.0.0.1.
- Default to
- If your parser uses different attribute names than
http_url,http_host, andhttp_port, update_parse_http_argsto map to the actual attribute names.
Address review feedback: - Split bind-only fallback so 0.0.0.0 maps to 127.0.0.1 and :: maps to ::1, preserving address family for IPv6-only setups - Add tests for HTTP host/URL defaults and env var overrides in main.py - Fix remaining localhost reference in epilog help text Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
I'd like to suggest #687 as an alternative approach. Main difference: instead of changing defaults everywhere, I added a HostAddress helper that centralizes the logic Why #687:
Re: main.py HTTP defaults — Unity explicitly passes --http-url when launching the server (via platform-aware Ultimately, you know the project best — I trust your judgment on which approach fits better here. |
On Windows, domain reload orphans the OS socket (managed C# reference is lost) but ExclusiveAddressUse=false + ReuseAddress=true allowed new listeners to bind alongside the orphaned one. Incoming connections then round-robin between live and dead listeners, causing WELCOME hangs. Changes: - Register AssemblyReloadEvents.beforeAssemblyReload to call Stop() before managed state is destroyed (root cause fix) - Remove ExclusiveAddressUse=false on Windows - Restrict ReuseAddress=true to macOS only (where it is needed) - Add listener.Server.Dispose() in Stop() for immediate socket release - Increase listener task wait from 100ms to 2000ms - Widen retry window (10 attempts, 200ms) for post-reload rebind Addresses CoplayDev#692 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs`:
- Line 131: StdioBridgeHost and StdioBridgeReloadHandler both subscribe to
AssemblyReloadEvents.beforeAssemblyReload causing a race where
StdioBridgeHost.OnBeforeAssemblyReload calls Stop() (clearing isRunning) before
StdioBridgeReloadHandler.OnBeforeAssemblyReload checks StdioBridgeHost.IsRunning
and thus prevents setting EditorPrefs to resume; fix by removing the
beforeAssemblyReload subscription from StdioBridgeHost (do not add
AssemblyReloadEvents.beforeAssemblyReload += OnBeforeAssemblyReload in
StdioBridgeHost), and instead move any socket disposal logic into
StdioBridgeReloadHandler.OnBeforeAssemblyReload after it invokes its stop logic,
or expose a dedicated socket disposal method on StdioBridgeHost (e.g.,
DisposeSockets) and call that from StdioBridgeReloadHandler so only the reload
handler controls assembly-reload sequencing and restart preference updates.
🧹 Nitpick comments (2)
MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs (2)
270-271: Increased retry budget blocks the main thread for up to 2 seconds.
maxImmediateRetries=10×retrySleepMs=200= 2 s ofThread.Sleepon the main thread before falling back to port discovery. The previous values (3 × 75 ms ≈ 225 ms) were much lighter. This is acceptable if the goal is to survive longer socket linger times on Windows, but worth noting as a UX trade-off during editor startup.
420-420: Wait timeout increased from 100 ms to 2000 ms — verify this won't stall the editor on quit.
Stop()is called fromEditorApplication.quittingand now fromOnBeforeAssemblyReload. A 2-second synchronousTask.Waiton the main thread during these callbacks could cause a noticeable pause. This is likely fine for quitting, but during assembly reload it compounds with theStdioBridgeReloadHandler's own 500 ms wait.
| } | ||
| } | ||
| EditorApplication.quitting += Stop; | ||
| AssemblyReloadEvents.beforeAssemblyReload += OnBeforeAssemblyReload; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Search for StdioBridgeReloadHandler and its beforeAssemblyReload subscription
fd -t f "StdioBridgeReloadHandler" --extension csRepository: CoplayDev/unity-mcp
Length of output: 118
🏁 Script executed:
# Search for all beforeAssemblyReload subscriptions in the codebase
rg "beforeAssemblyReload" --type cs -B2 -A2Repository: CoplayDev/unity-mcp
Length of output: 2088
🏁 Script executed:
# Check the Stop() method and IsRunning property in StdioBridgeHost
rg -n "public.*Stop\(\)|IsRunning|void Stop" MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs -A5Repository: CoplayDev/unity-mcp
Length of output: 503
🏁 Script executed:
# Look for the status file deletion and understanding the flow
rg -n "statusFile|Delete\|Remove" MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs -B2 -A2Repository: CoplayDev/unity-mcp
Length of output: 593
🏁 Script executed:
# Read StdioBridgeReloadHandler.cs to see the OnBeforeAssemblyReload implementation
cat MCPForUnity/Editor/Services/StdioBridgeReloadHandler.csRepository: CoplayDev/unity-mcp
Length of output: 4817
🏁 Script executed:
# Read the new OnBeforeAssemblyReload method and the Stop() method in full from StdioBridgeHost.cs
sed -n '376,395p' MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs
sed -n '448,454p' MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.csRepository: CoplayDev/unity-mcp
Length of output: 958
Duplicate beforeAssemblyReload handler creates a race condition with StdioBridgeReloadHandler.
Both StdioBridgeReloadHandler (in StdioBridgeReloadHandler.cs) and the new handler in StdioBridgeHost subscribe to the same event via [InitializeOnLoad] static constructors. Unity does not guarantee invocation order across different static initializers. If StdioBridgeHost.OnBeforeAssemblyReload() runs first, it calls Stop() which sets isRunning = false and deletes the status file. When StdioBridgeReloadHandler.OnBeforeAssemblyReload() then runs, it checks StdioBridgeHost.IsRunning (line 34 in StdioBridgeReloadHandler.cs), finds it false, and skips the EditorPrefs.SetBool(EditorPrefKeys.ResumeStdioAfterReload, true) call — so the bridge will not auto-restart after the domain reload.
The socket disposal concern (preventing orphaned OS listeners) is valid, but it should not be implemented as a separate handler on the same event. Instead, move socket disposal into StdioBridgeReloadHandler.OnBeforeAssemblyReload() after its own stop logic completes, or call a dedicated socket disposal method from the reload handler.
🤖 Prompt for AI Agents
In `@MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs` at line
131, StdioBridgeHost and StdioBridgeReloadHandler both subscribe to
AssemblyReloadEvents.beforeAssemblyReload causing a race where
StdioBridgeHost.OnBeforeAssemblyReload calls Stop() (clearing isRunning) before
StdioBridgeReloadHandler.OnBeforeAssemblyReload checks StdioBridgeHost.IsRunning
and thus prevents setting EditorPrefs to resume; fix by removing the
beforeAssemblyReload subscription from StdioBridgeHost (do not add
AssemblyReloadEvents.beforeAssemblyReload += OnBeforeAssemblyReload in
StdioBridgeHost), and instead move any socket disposal logic into
StdioBridgeReloadHandler.OnBeforeAssemblyReload after it invokes its stop logic,
or expose a dedicated socket disposal method on StdioBridgeHost (e.g.,
DisposeSockets) and call that from StdioBridgeReloadHandler so only the reload
handler controls assembly-reload sequencing and restart preference updates.
Summary
Fix 1: IPv4/IPv6 localhost ambiguity (#672)
localhostreferences with127.0.0.1in bind/connect defaults across Python server and Unity plugin.localhostcan resolve to::1(IPv6) first, causing server/client address-family mismatch and intermittent disconnects.localhostvalues are still honored.Fix 2: Duplicate TCP listeners after domain reload (#692)
StdioBridgeReloadHandlerto avoid resume-flag races.beforeAssemblyReloadstop hook fromStdioBridgeHost.Fix 3: HTTP/WebSocket reload resilience hardening
localhost, then127.0.0.1, then::1) to handle resolver/address-family mismatches during reconnect/resume.Files changed
Server/src/core/config.py—unity_hostdefaultServer/src/main.py—--http-urlarg default, host fallbacks, help textMCPForUnity/Editor/Helpers/HttpEndpointUtility.cs—DefaultLocalBaseUrlMCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs— bind-only host mapping + localhost candidate fallbacks + reconnect handlingMCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs— socket options, stop disposal, retry params, remove duplicate reload stop hookMCPForUnity/Editor/Services/StdioBridgeReloadHandler.cs— centralized stdio pre-reload stop sequencing and resume flag handlingMCPForUnity/Editor/Services/Transport/TransportManager.cs— synchronousForceStop(TransportMode)bridge helperMCPForUnity/Editor/Services/HttpBridgeReloadHandler.cs— synchronous pre-reload stop + bounded post-reload resume retriesServer/tests/test_core_infrastructure_characterization.py— updated assertion + HTTP default/fallback coverageTestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/WebSocketTransportClientTests.cs— new tests for localhost fallback candidate generationRelationship to #687
PR #687 by @whatevertogo independently identified the same IPv4/IPv6 root cause. This PR keeps direct default-path changes and also includes reload hardening behavior in the Unity editor-side transport lifecycle.
Test plan
localhostconfigs still work0.0.0.0 -> 127.0.0.1,:: -> ::1Repro for duplicate listener bug (Windows)
Addresses #672, #692