-
-
Notifications
You must be signed in to change notification settings - Fork 504
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Bug]: MappedPort potentially does not return correct value for IPv6 enabled systems #551
Comments
@mdonkers thanks for such detailed report. We appreciate your time Would you be willing to contribute the enhancement? |
Hi, I can give that a shot. But not sure if you have any proposals on your end how the API should look like? The What actually might be easiest, is allow specifying Would it be allowed to only take the |
Regarding the SQL host missing, we merged #524 very recently, although not released yet. Would it resolve that part?
On this, the MappedPort accepts a
I'm sorry but I'm not following you on this, my bad. Could you please add links to the places where Thanks in advance |
I don't think so, as this appears to be the hostname as given by Docker. I think that would still be ambiguous and does not map the interface for which the port is retrieved. See Line 153 in 4c79569
Yes, correct. But currently Line 153 in 4c79569
That would be my intend for the PR and guess the simplest solution.
When you create a container, code looks something like; req := testcontainers.ContainerRequest{
Image: "clickhouse/clickhouse-server:head-alpine",
ExposedPorts: []string{"9000/tcp"},
WaitingFor: wait.ForSQL(nat.Port("9000/tcp"), "clickhouse", dbURL).Timeout(time.Second * 10),
} If you follow where those |
We observed this behavior in other language implementations as well (see dotnet/Docker.DotNet#565) and it seems the root cause is this upstream bug in Docker/Moby: |
As I'm researching in a bit more I'm finding some more Docker dual-stack networking issues. But also not yet 100% clear to me, if either really a Docker bug or expected behaviour. So I'll dive in a bit more and see whether the above proposed solution still makes sense, or whether it should be resolved in some other place. |
Thanks for your time here @mdonkers, please let us know about your findings 🙏 |
Docker creates two port bindings while creating a container. One for IPv4 and another one for IPv6. This is probably the case when the port binding definition does not contain a host IP. The public assigned ports for IPv4 and IPv6 are not always the same. Probably they are not using the IPV6_V6ONLY socket option. We do not know which IP address the system resolves for For .NET (for now) I changed the host resolution to // Contains two items:
// IsIPv6 ::1
// IsIPv4 127.0.0.1
var ips = Dns.GetHostAddresses("localhost");
var httpPort = 80;
var httpPortKey = httpPort + "/tcp";
var ipv4Binding = new PortBinding { HostIP = "0.0.0.0", HostPort = string.Format(CultureInfo.CurrentCulture, "{0}", ++httpPort) };
var ipv6Binding = new PortBinding { HostIP = "::1", HostPort = string.Format(CultureInfo.CurrentCulture, "{0}", ++httpPort) };
IDictionary<string, IList<PortBinding>> mappedPortBindings = new Dictionary<string, IList<PortBinding>>();
mappedPortBindings.Add(new KeyValuePair<string, IList<PortBinding>>(httpPortKey, new List<PortBinding> { ipv4Binding, ipv6Binding }));
mappedPortBindings.TryGetValue(httpPortKey, out var portBindings);
// Lets imagine Dns.GetHostAddresses(string) returns the preferred order.
var addressFamilies = ips
.Select(ip => ip.AddressFamily)
.ToList();
// We can order the port bindings due to their address family, and use the first item.
var portBinding = portBindings
.Select(portBinding => new IPEndPoint(IPAddress.Parse(portBinding.HostIP), ushort.Parse(portBinding.HostPort, NumberStyles.None, CultureInfo.InvariantCulture)))
.OrderBy(portBinding => addressFamilies.IndexOf(portBinding.AddressFamily))
.First(); |
I'm going to close this as unfortunately I did not find the time to look into this. And it seems to be a kind of 'special' issue where resolution is not that straightforward. Feel free to re-open in case someone does really plan to pick this up. |
Minor update (as I did happen to run into this again): An easy fix / work-around is to specify exposed ports as ExposedPorts: []string{"0.0.0.0::9090/tcp"}, Then the container will only bind to IPv4, so no issues of two networks / overlapping ports. |
@mdonkers Thanks for that! It works correctly! |
Testcontainers version
0.14.0
Using the latest Testcontainers version?
Yes
Host OS
Linux
Host Arch
x86
Go Version
1.19
Docker version
$ docker version Client: Docker Engine - Community Version: 22.06.0-beta.0 API version: 1.42 Go version: go1.18.3 Git commit: 3e9117b Built: Fri Jun 3 17:56:12 2022 OS/Arch: linux/amd64 Context: default Server: Docker Engine - Community Engine: Version: 22.06.0-beta.0 API version: 1.42 (minimum version 1.12) Go version: go1.18.3 Git commit: 38633e7 Built: Fri Jun 3 17:56:12 2022 OS/Arch: linux/amd64 Experimental: false containerd: Version: 1.6.8 GitCommit: 9cd3357b7fd7218e4aec3eae239db1f68a5a6ec6 runc: Version: 1.1.4 GitCommit: v1.1.4-0-g5fd4c4d docker-init: Version: 0.19.0 GitCommit: de40ad0
Docker info
What happened?
Trying to run integration tests for the project https://github.com/ClickHouse/clickhouse-go, it times out as container fails to start up.
Analysing, the startup checks fail because the port and IP address (picked by Golang net.Dialer) does not match.
localhost
is used but the IPv4 port is returned and the[::1]
address.Local hosts file:
Docker container running:
Port
33789
is mapped to the IPv4 interface (0.0.0.0
), port33788
is mapped to IPv6 ([::1]
).When using
MappedPort()
, just the first port is always returned, which happens to be the IPv4 one (testcontainers-go/docker.go
Line 153 in 4c79569
But Golang, given the
net.DialContext
(https://cs.opensource.google/go/go/+/master:src/net/dial.go;l=428;drc=432158b69a50e292b625d08dcfacd0604acbabd3) happens to pick IPv6 as primary and try to connect (which I don't fully understand why it succeeds, but at least generates exceptions on both server and client side).The solution could (hopefully) be to add methods explicitly returning the interface for which the port is returned. Also the
wait.For...
methods such aswait.ForSQL
need to get an additional parameter with the IP though, so it can connect to the explicit address.Relevant log output
No response
Additional Information
No response
The text was updated successfully, but these errors were encountered: