tests/test_node_otp26: portal-match TLS config + long node names + container stdout capture
Iterates on the docker-based repro infrastructure while diagnosing the portal@unsandbox.com 'peer closed after 0/4 bytes' failure: - _write_ssl_config_portal_match() emits an ssl_dist.config with the exact options portal runs: permissive verify_fun (accepts bad_cert), versions [tlsv1.3, tlsv1.2], secure_renegotiate, server_name_indication disabled. Same shape as /opt/unsandbox/certs/inet_tls.conf. - Elixir docker peer now boots with --name (long FQDN, like prod) instead of --sname (short). Production portal is portal@unsandbox.com so the dist driver's routing path is different from short-name peers. - Elixir GenTarget now logs init + handle_call + handle_info. Fixture redirects container stdout to a file; test prints it on pass-or-fail so we can see whether the peer received our gen_call. - elixir image bumped to 1.16-otp-25 (portal runs OTP 25.3.2.5 / erts 13.2.2.5, NOT OTP 26 — discovered via the portal release's bundled erts version). Despite matching every dimension I can find (OTP version, TLS dist config, Elixir GenServer wrapping, long node names), the test still passes locally — so the production failure is something specific to the live portal beam state, not a general protocol or version issue.
This commit is contained in:
parent
ca022a8f0d
commit
b716f1487e
1 changed files with 89 additions and 21 deletions
|
|
@ -279,6 +279,42 @@ def _write_ssl_config(workdir: Path, certs_in_container: dict[str, str]) -> str:
|
|||
return str(cfg)
|
||||
|
||||
|
||||
def _write_ssl_config_portal_match(workdir: Path, certs_in_container: dict[str, str]) -> str:
|
||||
"""Match production portal's /opt/unsandbox/certs/inet_tls.conf exactly:
|
||||
permissive verify_fun (accepts bad_cert), TLS 1.2/1.3 only, secure
|
||||
renegotiate. This is the shape the production failure runs under."""
|
||||
cfg = workdir / "ssl_dist_portal.config"
|
||||
permissive_verify_fun = (
|
||||
"{verify_fun, {fun(_,{bad_cert, _}, UserState) -> {valid, UserState}; "
|
||||
" (_,{extension, _}, UserState) -> {unknown, UserState}; "
|
||||
" (_, valid, UserState) -> {valid, UserState}; "
|
||||
" (_, valid_peer, UserState) -> {valid, UserState} "
|
||||
" end, []}}"
|
||||
)
|
||||
body = (
|
||||
"[{server, "
|
||||
f'[{{certfile, "{certs_in_container["server_cert"]}"}}, '
|
||||
f'{{keyfile, "{certs_in_container["server_key"]}"}}, '
|
||||
f'{{cacertfile, "{certs_in_container["ca"]}"}}, '
|
||||
"{verify, verify_peer}, "
|
||||
"{fail_if_no_peer_cert, true}, "
|
||||
f"{permissive_verify_fun}, "
|
||||
"{secure_renegotiate, true}, "
|
||||
"{versions, ['tlsv1.3', 'tlsv1.2']}]}, "
|
||||
"{client, "
|
||||
f'[{{certfile, "{certs_in_container["server_cert"]}"}}, '
|
||||
f'{{keyfile, "{certs_in_container["server_key"]}"}}, '
|
||||
f'{{cacertfile, "{certs_in_container["ca"]}"}}, '
|
||||
"{verify, verify_peer}, "
|
||||
"{server_name_indication, disable}, "
|
||||
f"{permissive_verify_fun}, "
|
||||
"{secure_renegotiate, true}, "
|
||||
"{versions, ['tlsv1.3', 'tlsv1.2']}]}]."
|
||||
)
|
||||
cfg.write_text(body)
|
||||
return str(cfg)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def otp26_tls_peer(tmp_path_factory):
|
||||
"""OTP 26 + TLS dist peer in Docker — the production combo.
|
||||
|
|
@ -401,14 +437,29 @@ defmodule GenTarget do
|
|||
|
||||
def start_link, do: GenServer.start_link(__MODULE__, %{}, name: :gen_target)
|
||||
|
||||
def init(state), do: {:ok, state}
|
||||
def init(state) do
|
||||
IO.puts("[gen_target] init")
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
def handle_call({:ping, x}, _from, state), do: {:reply, {:pong, x}, state}
|
||||
def handle_call({:add, a, b}, _from, state), do: {:reply, {:ok, a + b}, state}
|
||||
def handle_call(other, _from, state), do: {:reply, {:error, {:bad_request, other}}, state}
|
||||
def handle_call(msg, from, state) do
|
||||
IO.puts("[gen_target] handle_call msg=#{inspect(msg)} from=#{inspect(from)}")
|
||||
reply = case msg do
|
||||
{:ping, x} -> {:pong, x}
|
||||
{:add, a, b} -> {:ok, a + b}
|
||||
other -> {:error, {:bad_request, other}}
|
||||
end
|
||||
{:reply, reply, state}
|
||||
end
|
||||
|
||||
def handle_info(msg, state) do
|
||||
IO.puts("[gen_target] handle_info msg=#{inspect(msg)}")
|
||||
{:noreply, state}
|
||||
end
|
||||
end
|
||||
|
||||
{:ok, _} = GenTarget.start_link()
|
||||
IO.puts("[gen_target] registered: #{inspect(Process.whereis(:gen_target))}")
|
||||
Process.sleep(:infinity)
|
||||
"""
|
||||
|
||||
|
|
@ -441,13 +492,22 @@ def otp26_elixir_tls_peer(tmp_path_factory):
|
|||
container_certs = {
|
||||
k: v.replace(str(workdir), "/certs") for k, v in host_certs.items()
|
||||
}
|
||||
_write_ssl_config(workdir, container_certs)
|
||||
ssl_config_container = "/certs/ssl_dist.config"
|
||||
# Use portal's exact inet_tls.conf shape (permissive verify_fun,
|
||||
# TLS 1.2/1.3 only, secure_renegotiate) so the test reproduces the
|
||||
# exact wire-level conditions of the production failure.
|
||||
_write_ssl_config_portal_match(workdir, container_certs)
|
||||
ssl_config_container = "/certs/ssl_dist_portal.config"
|
||||
|
||||
boot_script = workdir / "boot.exs"
|
||||
boot_script.write_text(ELIXIR_GENSERVER_BOOT)
|
||||
|
||||
container_name = f"erldistpy-elixir-tls-{int(time.time())}"
|
||||
container_log = workdir / "container.log"
|
||||
|
||||
# Long node names (--name) match production. Short names (--sname)
|
||||
# would route differently through the dist driver and may not
|
||||
# reproduce the same failure mode.
|
||||
long_node_name = f"{ELIXIR_TLS_SNAME}@127.0.0.1"
|
||||
|
||||
proc = subprocess.Popen(
|
||||
[
|
||||
|
|
@ -455,15 +515,15 @@ def otp26_elixir_tls_peer(tmp_path_factory):
|
|||
"--name", container_name,
|
||||
"--network", "host",
|
||||
"-v", f"{workdir}:/certs:ro",
|
||||
"elixir:1.16-otp-26",
|
||||
"elixir:1.16-otp-25",
|
||||
"elixir",
|
||||
"--sname", ELIXIR_TLS_SNAME,
|
||||
"--name", long_node_name,
|
||||
"--cookie", ELIXIR_TLS_COOKIE,
|
||||
"--erl", f"-proto_dist inet_tls -ssl_dist_optfile {ssl_config_container}",
|
||||
"/certs/boot.exs",
|
||||
],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
stdout=open(container_log, "w"),
|
||||
stderr=subprocess.STDOUT,
|
||||
)
|
||||
|
||||
deadline = time.monotonic() + 25.0
|
||||
|
|
@ -480,7 +540,7 @@ def otp26_elixir_tls_peer(tmp_path_factory):
|
|||
proc.wait(timeout=3)
|
||||
pytest.skip(f"Elixir TLS docker node {ELIXIR_TLS_SNAME} did not register")
|
||||
|
||||
yield ELIXIR_TLS_SNAME, ELIXIR_TLS_COOKIE, host_certs
|
||||
yield ELIXIR_TLS_SNAME, ELIXIR_TLS_COOKIE, host_certs, container_log
|
||||
|
||||
subprocess.run(["docker", "rm", "-f", container_name], capture_output=True)
|
||||
proc.terminate()
|
||||
|
|
@ -498,18 +558,26 @@ def test_call_elixir_genserver_over_tls_otp26(otp26_elixir_tls_peer):
|
|||
:gen_server.reply/2 which sends via :erlang.send/2 — same path as
|
||||
Wallet.Bridge in production.
|
||||
"""
|
||||
sname, cookie, certs = otp26_elixir_tls_peer
|
||||
sname, cookie, certs, container_log = otp26_elixir_tls_peer
|
||||
ctx = make_dist_tls_context(
|
||||
cert=certs["client_cert"],
|
||||
key=certs["client_key"],
|
||||
ca=certs["ca"],
|
||||
)
|
||||
with Node(
|
||||
our_name="erldistpy_test_elixir_tls@localhost",
|
||||
peer_name=sname,
|
||||
cookie=cookie,
|
||||
tls_context=ctx,
|
||||
) as n:
|
||||
assert n.tls is True
|
||||
reply = n.call("gen_target", (Atom("ping"), 7), timeout=5.0)
|
||||
assert reply == (Atom("pong"), 7)
|
||||
try:
|
||||
with Node(
|
||||
our_name="erldistpy_test_elixir_tls@localhost",
|
||||
peer_name=sname,
|
||||
cookie=cookie,
|
||||
tls_context=ctx,
|
||||
) as n:
|
||||
assert n.tls is True
|
||||
reply = n.call("gen_target", (Atom("ping"), 7), timeout=5.0)
|
||||
assert reply == (Atom("pong"), 7)
|
||||
finally:
|
||||
# Whether pass or fail, dump container output for diagnosis.
|
||||
try:
|
||||
text = container_log.read_text()
|
||||
print(f"=== container stdout ===\n{text}\n=== end ===")
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue