Skip to content

Dqlite: Fix TLS handshake errors - #470

Merged
roosterfish merged 6 commits into
canonical:v3from
roosterfish:dqlite_fixes
Sep 2, 2025
Merged

Dqlite: Fix TLS handshake errors#470
roosterfish merged 6 commits into
canonical:v3from
roosterfish:dqlite_fixes

Conversation

@roosterfish

@roosterfish roosterfish commented Aug 28, 2025

Copy link
Copy Markdown
Contributor

See canonical/go-dqlite#398 for reference.
Closes canonical/go-dqlite#398.

Microcluster's custom dial function passed into go-dqlite might cause TLS handshake errors on the target members in case the context passed into the dial function is cancelled early on. That might be the case when a leader was already found, so all the other parallel connection attempts to the other members are cancelled.
Whilst this is not a functional issue we should have extra handling to prevent seeing those error messages in the Microcluster daemon logs.

Furthermore the passed context's deadline is now used properly. Setting it whilst using the same context the deadline is coming from doesn't bring much value.
In addition a potential leak of the request's response body is fixed.

Setting both the timeout (from the outer context's deadline) and the context for the dial is redundant.
Instead only use the outer context for the TLS dial.

Signed-off-by: Julian Pelizäus <julian.pelizaeus@canonical.com>
We use lowercase dqlite in all other messages.

Signed-off-by: Julian Pelizäus <julian.pelizaeus@canonical.com>
Add the response body to the reverter.
Also closing the body should succeed. If not it can be considered an error.

Signed-off-by: Julian Pelizäus <julian.pelizaeus@canonical.com>
@tomponline

Copy link
Copy Markdown
Member

If deadline is zero, does WithDeadline never cancel or immediately cancel?

@roosterfish

Copy link
Copy Markdown
Contributor Author

If deadline is zero, does WithDeadline never cancel or immediately cancel?

I got rid of this part in the changes. But I guess it will never cancel as deadline is zero. So it's the same as setting no deadline.

@roosterfish

Copy link
Copy Markdown
Contributor Author

@markylaing let's talk this through during our 1:1 next week.

@roosterfish
roosterfish marked this pull request as ready for review August 28, 2025 15:22
Comment thread internal/daemon/logfilter.go Outdated
Comment thread internal/daemon/daemon.go Outdated
This allows filtering out the TLS handshake error messages which might appear in case go-dqlite
cancels the connection attempts prematuerely in case the leader was already found.

It doesn't hide an error but instead suppresses errors which are considered to be not desired.

Signed-off-by: Julian Pelizäus <julian.pelizaeus@canonical.com>
This allows injecting our filter logger.

Signed-off-by: Julian Pelizäus <julian.pelizaeus@canonical.com>
This allows calling the daemon's internal remote's Addresses func which is now required as part of the log filtering
to gather a list of cluster member addresses.

Signed-off-by: Julian Pelizäus <julian.pelizaeus@canonical.com>
Comment thread internal/daemon/daemon.go
@roosterfish

Copy link
Copy Markdown
Contributor Author

@markylaing when you are happy with the PR please leave it open and don't merge it yet.
Whilst this fixes the error message we are seeing, I found we could maybe use a different method on go-dqlite to get to the leader which maybe won't cause those error messages on the target side. Testing this right now.

@markylaing markylaing left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM thanks 🥳

@roosterfish

Copy link
Copy Markdown
Contributor Author

What I have mentioned in this comment isn't a full alternative unfortunately. However we could think about using go-dqlite's FindLeader instead of Leader to reuse an existing connection to the leader. This also reduces the likelihood of the error message we are trying to fix here, but doesn't remove it completely.

I'll add another PR for this and will merge this one now.

@roosterfish
roosterfish merged commit 3652475 into canonical:v3 Sep 2, 2025
5 checks passed
@roosterfish
roosterfish deleted the dqlite_fixes branch September 2, 2025 14:16
tomponline added a commit to canonical/lxd that referenced this pull request Sep 4, 2025
When using only the outer context's deadline as timeout for the actual
TLS dial, we ignore cases in which the outer context gets cancelled
which should also cancel the TLS dial.

In case the target addr is offline, the dial function will wait until
the timeout (outer context's deadline) instead of returing early in case
the context already got cancelled.
This is critical in cases where `go-dqlite` opens up connections in
parallel to all members to identify the current leader. If the leader
was found, all other attempts are cancelled.

A similar fix was put in place for Microcluster in
canonical/microcluster#470.
The additional log filtering added in Microcluster (see the PR) doesn't
seem to be that critical in LXD. Whilst Microcluster uses `go-dqlite`'s
`app` construct when opening up connections, LXD doesn't and instead
uses the `client` package directly. This results in the `http: TLS
handshake ...: EOF` errors not being triggered as permanent as in
Microcluster as the connections are handled differently by `go-dqlite`.

Those errors can potentially be seen, but their default log level is set
to
[`info`](https://github.com/canonical/lxd/blob/main/lxd/endpoints/network_util.go#L24)
so they don't appear in the regular LXD daemon log by default. The
already existing log filter [hooks
directly](https://github.com/canonical/lxd/blob/main/lxd/endpoints/network.go#L175)
into the endpoint's `http.Server`.
I tried to reproduce this many times and only ever saw those `http: TLS
handshake ...` errors appearing infrequently when forming the LXD
cluster or when restarting the cluster members.
So ultimately I don't think we need extra handling for those. In
Microcluster they sometimes appeared with every heartbeat (default 10s).
roosterfish added a commit that referenced this pull request Sep 8, 2025
Follow up on #470 to also
account for IPv6 addresses.

I initially thought this is working for IPv4 and IPv6 but more tests
have shown that in case of IPv6 we need to handle a newline and the
additional brackets.
markylaing added a commit that referenced this pull request Sep 10, 2025
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.

TLS handshake error regression

3 participants