diff --git a/tests/raft_tls_e2e_test.nim b/tests/raft_tls_e2e_test.nim index 10c93e3..a1b8178 100644 --- a/tests/raft_tls_e2e_test.nim +++ b/tests/raft_tls_e2e_test.nim @@ -31,6 +31,7 @@ type NodeProc = object id: string clientPort: int + raftPort: int p: Process dataDir: string output: string @@ -67,6 +68,28 @@ proc portOpen(port: int): bool = if s != nil: s.close() result = false +proc tlsHandshake(port: int): bool = + ## True when a real TLS client handshake completes against `port`. + ## Wire-level discriminator: against a plaintext peer it fails — either + ## immediately (SSL error on the garbage reply) or, when the peer swallows + ## our ClientHello and waits for more bytes, via the 3s recv timeout. + var ctx: SslContext + var s: Socket + try: + ctx = newContext(verifyMode = CVerifyNone) + s = newSocket() + var tv = Timeval(tvSec: posix.Time(3), tvUsec: Suseconds(0)) + discard setsockopt(s.getFd(), SOL_SOCKET, SO_RCVTIMEO, + addr tv, SockLen(sizeof(tv))) + s.connect("127.0.0.1", Port(port), timeout = 3000) + ctx.wrapConnectedSocket(s, handshakeAsClient) + result = true + except CatchableError: + result = false + finally: + if s != nil: s.close() + if ctx != nil: ctx.destroyContext() + proc killNode(n: var NodeProc) = if n.p != nil and n.alive: try: @@ -159,7 +182,8 @@ proc startNode(id: string, clientPort, raftPort: int, peers, clientPeers: string options = {poStdErrToStdOut, poDaemon}) discard fcntl(p.outputHandle.cint, F_SETFL, fcntl(p.outputHandle.cint, F_GETFL) or O_NONBLOCK) - NodeProc(id: id, clientPort: clientPort, p: p, dataDir: dataDir, alive: true) + NodeProc(id: id, clientPort: clientPort, raftPort: raftPort, p: p, + dataDir: dataDir, alive: true) proc runTlsScenario() = ## Fatal phase failures dump all captured node output, record a test @@ -180,16 +204,18 @@ proc runTlsScenario() = ",n3@127.0.0.1:" & $(cbase + 30) var nodes: seq[NodeProc] - for i in 1 .. 3: - nodes.add startNode("n" & $i, cbase + i * 10, rbase + i, - peers, clientPeers, tlsEnabled = true) - # Negative-case node: same peers, raft TLS DISABLED, own dir and ports # (4th port in each base range). Started later, after the TLS cluster is # up, so the positive assertions are not polluted by its noise. var rogue: NodeProc try: + # Node starts live inside the try so a raise from start #2/#3 still + # reaches the cleanup in the finally below. + for i in 1 .. 3: + nodes.add startNode("n" & $i, cbase + i * 10, rbase + i, + peers, clientPeers, tlsEnabled = true) + # Readiness: all three client ports accept TCP connections (10s each). for i in 0 ..< nodes.len: let readyStart = getTime() @@ -235,6 +261,21 @@ proc runTlsScenario() = return echo "TLS leader elected: ", nodes[leaderIdx].id, " (term ", leaderTerm, ")" + # Wire-level TLS assertion: a real client handshake must complete + # against every cluster node's raft port. Without this, the negative + # "plaintext node never becomes leader" check alone cannot distinguish + # TLS rejection from "can't win an election anyway" — if TLS wrapping + # were silently dropped from the transport, these handshakes raise and + # the suite fails. + for n in nodes.items: + if not tlsHandshake(n.raftPort): + echo "TLS handshake failed against raft port of ", n.id, + " (port ", n.raftPort, ") — raft transport not encrypted?" + dumpAll(nodes) + fail() + return + echo "TLS handshake verified against all 3 raft ports" + let followerIdx = (if leaderIdx == 0: 1 else: 0) # Schema: CREATE TABLE goes through the raft "ddl" log (C3c) — this @@ -319,9 +360,26 @@ proc runTlsScenario() = return echo "plaintext node n4 started against the TLS peers" + # Complementary wire-level negative: the rogue node's raft port speaks + # plaintext, so a TLS handshake against it must FAIL. + if tlsHandshake(rogue.raftPort): + echo "TLS handshake unexpectedly succeeded against plaintext n4 ", + "raft port ", rogue.raftPort + dumpAll(nodes) + fail() + return + echo "TLS handshake correctly fails against plaintext n4 raft port" + # Give n4 many election cycles (timeouts 150-300ms) to try its luck. # The TLS cluster must keep operating among its 3 members meanwhile. - nodes.drainFor(6000) + # Drain n4's pipe alongside the others — its connection-refused spam + # must not fill the pipe and block the child. + block: + let negStart = getTime() + while getTime() - negStart < initDuration(seconds = 6): + nodes.drainAll() + rogue.drainOutput() + sleep(50) # Cluster still elects/operates: leader among the 3 TLS nodes accepts a # write and it replicates to a follower, with n4 running. @@ -364,8 +422,6 @@ proc runTlsScenario() = fail() return echo "plaintext node never became leader (TLS rejection confirmed)" - - check leaderIdx2 >= 0 finally: for n in nodes.mitems: n.killNode()