milestone 13 discrepancies: redact credentials from urls in logs, metrics and cli output
This commit is contained in:
+60
-1
@@ -40,10 +40,32 @@
|
||||
const std = @import("std");
|
||||
|
||||
const health = @import("health.zig");
|
||||
const safe_url = @import("../safe_url.zig");
|
||||
const transport = @import("transport.zig");
|
||||
|
||||
const log = std.log.scoped(.upstream);
|
||||
|
||||
/// The one line `exchange` writes about a failed attempt, as a value.
|
||||
///
|
||||
/// It is a value rather than a format string at the call site for the same
|
||||
/// reason `dot_client.Diagnostic` is: a `std.log` line is not readable from a
|
||||
/// unit test under the default test runner, so the test below reads this
|
||||
/// instead of stderr. An upstream url is operator-supplied and a DoH one carries
|
||||
/// its credential in the path — `https://dns.nextdns.io/abcd12` is a whole
|
||||
/// NextDNS account identifier — so it reaches the line through
|
||||
/// `safe_url.redactQuoted`. Quoted rather than bare because the error name
|
||||
/// follows it: a redacted authority may still hold a space and a `:`, so
|
||||
/// unquoted, a url ending `ok failed: Timeout` would report a failure that did
|
||||
/// not happen.
|
||||
const AttemptFailure = struct {
|
||||
endpoint: transport.Endpoint,
|
||||
err: transport.ExchangeError,
|
||||
|
||||
pub fn format(self: AttemptFailure, w: *std.Io.Writer) std.Io.Writer.Error!void {
|
||||
try w.print("upstream {f} failed: {t}", .{ safe_url.redactQuoted(self.endpoint.url), self.err });
|
||||
}
|
||||
};
|
||||
|
||||
pub const Entry = struct {
|
||||
endpoint: transport.Endpoint,
|
||||
client: transport.Client,
|
||||
@@ -61,6 +83,11 @@ pub const Entry = struct {
|
||||
/// A copy of one entry's health, taken under the mutex. Feeds
|
||||
/// `GET /api/upstream/health` in Phase 8.
|
||||
pub const Snapshot = struct {
|
||||
/// Whole, not redacted. `GET /api/upstream/health` returns this to a session
|
||||
/// that `GET /api/upstreams` already serves the same url to in full, so
|
||||
/// redacting here would hide nothing from that reader and would make two
|
||||
/// responses of one API disagree. A consumer reachable without a session has
|
||||
/// to redact it itself.
|
||||
url: []const u8,
|
||||
enabled: bool,
|
||||
available: bool,
|
||||
@@ -165,7 +192,7 @@ pub const Pool = struct {
|
||||
|
||||
const response = result catch |err| switch (transport.group(err)) {
|
||||
.peer_fault => {
|
||||
log.debug("upstream {s} failed: {t}", .{ entry.endpoint.url, err });
|
||||
log.debug("{f}", .{AttemptFailure{ .endpoint = entry.endpoint, .err = err }});
|
||||
self.recordFailure(io, entry, completed_at, err);
|
||||
last_fault = err;
|
||||
continue;
|
||||
@@ -385,6 +412,38 @@ const test_cfg: health.Config = .{
|
||||
|
||||
const test_timeout: std.Io.Clock.Duration = .{ .raw = .fromSeconds(10), .clock = .awake };
|
||||
|
||||
fn expectFailureLine(expected: []const u8, url: []const u8, err: transport.ExchangeError) !void {
|
||||
var buf: [8 * safe_url.max_len]u8 = undefined;
|
||||
const line = try std.fmt.bufPrint(&buf, "{f}", .{AttemptFailure{
|
||||
.endpoint = try .parse(url),
|
||||
.err = err,
|
||||
}});
|
||||
try testing.expectEqualStrings(expected, line);
|
||||
}
|
||||
|
||||
test "the failed-attempt line names an upstream by a url carrying no credential" {
|
||||
// A NextDNS DoH upstream puts the whole account identifier in the path, and
|
||||
// this line ran at `debug` on every peer fault, so a debug-level operator
|
||||
// persisted it to the journal once per failure.
|
||||
try expectFailureLine(
|
||||
"upstream 'https://dns.nextdns.io' failed: Timeout",
|
||||
"https://dns.nextdns.io/abcd12",
|
||||
error.Timeout,
|
||||
);
|
||||
try expectFailureLine(
|
||||
"upstream 'https://cdn.example:8443' failed: TlsFailed",
|
||||
"https://cdn.example:8443/d/hunter2/dns-query",
|
||||
error.TlsFailed,
|
||||
);
|
||||
// What it still says, because an operator reading a failover has to know
|
||||
// which upstream failed: the scheme, the host and the port.
|
||||
try expectFailureLine(
|
||||
"upstream 'tls://9.9.9.9:853' failed: ConnectFailed",
|
||||
"tls://9.9.9.9:853",
|
||||
error.ConnectFailed,
|
||||
);
|
||||
}
|
||||
|
||||
test "Pool satisfies the Client interface" {
|
||||
var threaded: std.Io.Threaded = .init(testing.allocator, .{});
|
||||
defer threaded.deinit();
|
||||
|
||||
Reference in New Issue
Block a user