Every log line the daemon produced carried aurora escapes, and under procd
stderr is not a screen, it is syslog:
daemon.err shaterd[27540]: ...Z ESC[31mERRORESC[0m[0026]
[ESC[38;5;193m1728741629ESC[0m 70ms] dns: exchange failed ...
`logread | grep ERROR` misses that line — the level word has invisible
bytes inside it — external collectors store the escapes forever, and a
captured log reads as mojibake.
Both producers defaulted to colour, and both are fixed at the producer,
because colour is a property of the DESTINATION and should never be
generated for a destination that cannot render it:
* control plane (cmd/shaterd): log.Formatter{BaseTime: ...} left
DisableColors at its false zero value. It now comes from
controlLogFormatter(), gated on logsink.IsTTY(os.Stderr). The helper
lives in an untagged file (same split as profilewatch.go) so it is
unit-testable off the linux target.
* engine (shater/generate): the generated option.LogOptions never set
DisableColor, so box.New built a colouring formatter over the shared
sink. logOptions() now sets it from the same TTY gate (seam:
logColorAllowed).
logsink.IsTTY is the single source of the decision: a character-device
check, so no cgo, no termios and no new dependency on a CGO_ENABLED=0
musl-static binary. Under procd stderr is a pipe => no colour; an
interactive `shaterd run` from a shell keeps it.
The file half already stripped ANSI on the way out (emitLocked ->
stripANSI); that stays as the belt to this new braces, and the leak it
never covered — the syslog half — is now closed at the source.
Tests: the syslog half of the sink carries no 0x1b for any level with a
context ID set (the connection id is coloured by a separate branch of
log/format.go, so a level-only fix would still leak); the same for the
control-plane formatter and for a factory built from the REAL generated
log block. Each has a teeth check that a colouring formatter does emit
0x1b, so the guards cannot rot into passing for the wrong reason.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
38 lines
1.3 KiB
Go
38 lines
1.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/sagernet/sing-box/log"
|
|
)
|
|
|
|
// TestControlLogFormatterNoANSIOffTTY pins the control-plane half of the syslog
|
|
// colour leak: when the daemon's stderr is not a terminal — which is ALWAYS the
|
|
// case under procd, where stderr is the pipe procd relays to syslog — no line
|
|
// the daemon formats may contain an ESC (0x1b).
|
|
func TestControlLogFormatterNoANSIOffTTY(t *testing.T) {
|
|
r, w, err := os.Pipe()
|
|
if err != nil {
|
|
t.Fatalf("pipe: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = r.Close(); _ = w.Close() })
|
|
|
|
f := controlLogFormatter(time.Now(), w)
|
|
if !f.DisableColors {
|
|
t.Fatalf("colours enabled for a non-terminal stderr")
|
|
}
|
|
// A context ID exercises the second colouring branch of log/format.go (the
|
|
// 256-colour connection id), which is what produced ESC[38;5;193m on the router.
|
|
ctx := log.ContextWithNewID(context.Background())
|
|
for _, level := range []log.Level{log.LevelError, log.LevelWarn, log.LevelInfo, log.LevelDebug, log.LevelTrace} {
|
|
line := f.Format(ctx, level, "dns", "exchange failed for example.com. IN AAAA: unexpected EOF", time.Now())
|
|
if i := strings.IndexByte(line, 0x1b); i >= 0 {
|
|
t.Errorf("level %v: formatted line carries an ANSI escape at byte %d: %q", level, i, line)
|
|
}
|
|
}
|
|
}
|