Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(pp): log redaction #785

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/ddns/ddns.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func main() {

func realMain() int { //nolint:funlen
ppfmt := pp.New(os.Stdout)
if !config.ReadEmoji("EMOJI", &ppfmt) || !config.ReadQuiet("QUIET", &ppfmt) {
if !config.InitializePP(&ppfmt) {
ppfmt.Infof(pp.EmojiUserError, "Bye!")
return 1
}
Expand Down
39 changes: 31 additions & 8 deletions internal/api/cloudflare.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,15 +196,24 @@ func (h *CloudflareHandle) ListRecords(ctx context.Context, ppfmt pp.PP,
Type: ipNet.RecordType(),
})
if err != nil {
ppfmt.Warningf(pp.EmojiError, "Failed to retrieve records of %q: %v", domain.Describe(), err)
ppfmt.Warningf(pp.EmojiError, "Failed to retrieve records of %q: %s",
pp.Redact(ppfmt, pp.Domains, domain.Describe(), "(redacted)"),
// Redact error messages out of fear
pp.Redact(ppfmt, pp.Domains|pp.IPs|pp.DNSResourceIDs, err.Error(), "(error message redacted)"),
)
return nil, false, false
}

rmap := map[string]netip.Addr{}
for i := range rs {
rmap[rs[i].ID], err = netip.ParseAddr(rs[i].Content)
if err != nil {
ppfmt.Warningf(pp.EmojiImpossible, "Failed to parse the IP address in records of %q: %v", domain.Describe(), err)
ppfmt.Warningf(pp.EmojiImpossible,
"Failed to parse the IP address in records of %q: %s",
pp.Redact(ppfmt, pp.Domains, domain.Describe(), "(redacted)"),
// Redact error messages out of fear
pp.Redact(ppfmt, pp.Domains|pp.IPs|pp.DNSResourceIDs, err.Error(), "(error message redacted)"),
)
return nil, false, false
}
}
Expand All @@ -224,8 +233,13 @@ func (h *CloudflareHandle) DeleteRecord(ctx context.Context, ppfmt pp.PP,
}

if err := h.cf.DeleteDNSRecord(ctx, cloudflare.ZoneIdentifier(zone), id); err != nil {
ppfmt.Warningf(pp.EmojiError, "Failed to delete a stale %s record of %q (ID: %s): %v",
ipNet.RecordType(), domain.Describe(), id, err)
ppfmt.Warningf(pp.EmojiError, "Failed to delete a stale %s record of %q (ID: %s): %s",
ipNet.RecordType(),
pp.Redact(ppfmt, pp.Domains, domain.Describe(), "(redacted)"),
pp.Redact(ppfmt, pp.DNSResourceIDs, id, "redacted"),
// Redact error messages out of fear
pp.Redact(ppfmt, pp.Domains|pp.IPs|pp.DNSResourceIDs, err.Error(), "(error message redacted)"),
)

h.cache.listRecords[ipNet].Delete(domain.DNSNameASCII())

Expand Down Expand Up @@ -255,8 +269,13 @@ func (h *CloudflareHandle) UpdateRecord(ctx context.Context, ppfmt pp.PP,
}

if _, err := h.cf.UpdateDNSRecord(ctx, cloudflare.ZoneIdentifier(zone), params); err != nil {
ppfmt.Warningf(pp.EmojiError, "Failed to update a stale %s record of %q (ID: %s): %v",
ipNet.RecordType(), domain.Describe(), id, err)
ppfmt.Warningf(pp.EmojiError, "Failed to update a stale %s record of %q (ID: %s): %s",
ipNet.RecordType(),
pp.Redact(ppfmt, pp.Domains, domain.Describe(), "(redacted)"),
pp.Redact(ppfmt, pp.DNSResourceIDs, id, "redacted"),
// Redact error messages out of fear
pp.Redact(ppfmt, pp.Domains|pp.IPs|pp.DNSResourceIDs, err.Error(), "(error message redacted)"),
)

h.cache.listRecords[ipNet].Delete(domain.DNSNameASCII())

Expand Down Expand Up @@ -291,8 +310,12 @@ func (h *CloudflareHandle) CreateRecord(ctx context.Context, ppfmt pp.PP,

res, err := h.cf.CreateDNSRecord(ctx, cloudflare.ZoneIdentifier(zone), params)
if err != nil {
ppfmt.Warningf(pp.EmojiError, "Failed to add a new %s record of %q: %v",
ipNet.RecordType(), domain.Describe(), err)
ppfmt.Warningf(pp.EmojiError, "Failed to add a new %s record of %q: %s",
ipNet.RecordType(),
pp.Redact(ppfmt, pp.Domains, domain.Describe(), "(redacted)"),
// Redact error messages out of fear
pp.Redact(ppfmt, pp.Domains|pp.IPs|pp.DNSResourceIDs, err.Error(), "(error message redacted)"),
)

h.cache.listRecords[ipNet].Delete(domain.DNSNameASCII())

Expand Down
12 changes: 8 additions & 4 deletions internal/config/config_print.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,13 @@ func (c *Config) Print(ppfmt pp.PP) {

section("Domains and IP providers:")
if c.Provider[ipnet.IP4] != nil {
item("IPv4 domains:", "%s", describeDomains(c.Domains[ipnet.IP4]))
item("IPv4 domains:", "%s",
pp.Redact(ppfmt, pp.Domains, describeDomains(c.Domains[ipnet.IP4]), "(redacted)"))
item("IPv4 provider:", "%s", provider.Name(c.Provider[ipnet.IP4]))
}
if c.Provider[ipnet.IP6] != nil {
item("IPv6 domains:", "%s", describeDomains(c.Domains[ipnet.IP6]))
item("IPv6 domains:", "%s",
pp.Redact(ppfmt, pp.Domains, describeDomains(c.Domains[ipnet.IP6]), "(redacted)"))
item("IPv6 provider:", "%s", provider.Name(c.Provider[ipnet.IP6]))
}

Expand All @@ -89,8 +91,10 @@ func (c *Config) Print(ppfmt pp.PP) {
item("TTL:", "%s", c.TTL.Describe())
{
_, inverseMap := getInverseMap(c.Proxied)
item("Proxied domains:", "%s", describeDomains(inverseMap[true]))
item("Unproxied domains:", "%s", describeDomains(inverseMap[false]))
item("Proxied domains:", "%s",
pp.Redact(ppfmt, pp.Domains, describeDomains(inverseMap[true]), "(redacted)"))
item("Unproxied domains:", "%s",
pp.Redact(ppfmt, pp.Domains, describeDomains(inverseMap[false]), "(redacted)"))
}
item("Record comment:", "%s", describeComment(c.RecordComment))

Expand Down
7 changes: 7 additions & 0 deletions internal/config/config_print_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@
mockPP.EXPECT().IncIndent().Return(mockPP),
mockPP.EXPECT().IncIndent().Return(innerMockPP),
mockPP.EXPECT().Infof(pp.EmojiConfig, "Domains and IP providers:"),
mockPP.EXPECT().ShouldRedact(pp.PrivateDataTypeDomains).Return(false),

Check failure on line 35 in internal/config/config_print_test.go

View workflow job for this annotation

GitHub Actions / Lint

undefined: pp.PrivateDataTypeDomains

Check failure on line 35 in internal/config/config_print_test.go

View workflow job for this annotation

GitHub Actions / Test

undefined: pp.PrivateDataTypeDomains
printItem(innerMockPP, "IPv4 domains:", "(none)"),
printItem(innerMockPP, "IPv4 provider:", "cloudflare.trace"),
mockPP.EXPECT().ShouldRedact(pp.PrivateDataTypeDomains).Return(false),

Check failure on line 38 in internal/config/config_print_test.go

View workflow job for this annotation

GitHub Actions / Lint

undefined: pp.PrivateDataTypeDomains

Check failure on line 38 in internal/config/config_print_test.go

View workflow job for this annotation

GitHub Actions / Test

undefined: pp.PrivateDataTypeDomains
printItem(innerMockPP, "IPv6 domains:", "(none)"),
printItem(innerMockPP, "IPv6 provider:", "cloudflare.trace"),
mockPP.EXPECT().Infof(pp.EmojiConfig, "Scheduling:"),
Expand All @@ -44,6 +46,7 @@
printItem(innerMockPP, "Cache expiration:", "6h0m0s"),
mockPP.EXPECT().Infof(pp.EmojiConfig, "Parameters of new DNS records:"),
printItem(innerMockPP, "TTL:", "1 (auto)"),
mockPP.EXPECT().ShouldRedact(pp.PrivateDataTypeDomains).Return(false),

Check failure on line 49 in internal/config/config_print_test.go

View workflow job for this annotation

GitHub Actions / Lint

undefined: pp.PrivateDataTypeDomains

Check failure on line 49 in internal/config/config_print_test.go

View workflow job for this annotation

GitHub Actions / Test

undefined: pp.PrivateDataTypeDomains
printItem(innerMockPP, "Proxied domains:", "(none)"),
printItem(innerMockPP, "Unproxied domains:", "(none)"),
printItem(innerMockPP, "Record comment:", "(empty)"),
Expand All @@ -68,8 +71,10 @@
mockPP.EXPECT().IncIndent().Return(mockPP),
mockPP.EXPECT().IncIndent().Return(innerMockPP),
mockPP.EXPECT().Infof(pp.EmojiConfig, "Domains and IP providers:"),
mockPP.EXPECT().ShouldRedact(pp.PrivateDataTypeDomains).Return(false),

Check failure on line 74 in internal/config/config_print_test.go

View workflow job for this annotation

GitHub Actions / Lint

undefined: pp.PrivateDataTypeDomains

Check failure on line 74 in internal/config/config_print_test.go

View workflow job for this annotation

GitHub Actions / Test

undefined: pp.PrivateDataTypeDomains
printItem(innerMockPP, "IPv4 domains:", "test4.org, *.test4.org"),
printItem(innerMockPP, "IPv4 provider:", "cloudflare.trace"),
mockPP.EXPECT().ShouldRedact(pp.PrivateDataTypeDomains).Return(false),

Check failure on line 77 in internal/config/config_print_test.go

View workflow job for this annotation

GitHub Actions / Lint

undefined: pp.PrivateDataTypeDomains

Check failure on line 77 in internal/config/config_print_test.go

View workflow job for this annotation

GitHub Actions / Test

undefined: pp.PrivateDataTypeDomains
printItem(innerMockPP, "IPv6 domains:", "test6.org, *.test6.org"),
printItem(innerMockPP, "IPv6 provider:", "cloudflare.trace"),
mockPP.EXPECT().Infof(pp.EmojiConfig, "Scheduling:"),
Expand All @@ -80,6 +85,7 @@
printItem(innerMockPP, "Cache expiration:", "6h0m0s"),
mockPP.EXPECT().Infof(pp.EmojiConfig, "Parameters of new DNS records:"),
printItem(innerMockPP, "TTL:", "30000"),
mockPP.EXPECT().ShouldRedact(pp.PrivateDataTypeDomains).Return(false),

Check failure on line 88 in internal/config/config_print_test.go

View workflow job for this annotation

GitHub Actions / Lint

undefined: pp.PrivateDataTypeDomains

Check failure on line 88 in internal/config/config_print_test.go

View workflow job for this annotation

GitHub Actions / Test

undefined: pp.PrivateDataTypeDomains
printItem(innerMockPP, "Proxied domains:", "a, b"),
printItem(innerMockPP, "Unproxied domains:", "c, d"),
printItem(innerMockPP, "Record comment:", "\"Created by Cloudflare DDNS\""),
Expand Down Expand Up @@ -146,6 +152,7 @@
printItem(innerMockPP, "Cache expiration:", "0s"),
mockPP.EXPECT().Infof(pp.EmojiConfig, "Parameters of new DNS records:"),
printItem(innerMockPP, "TTL:", "0"),
mockPP.EXPECT().ShouldRedact(pp.PrivateDataTypeDomains).Return(false),

Check failure on line 155 in internal/config/config_print_test.go

View workflow job for this annotation

GitHub Actions / Lint

undefined: pp.PrivateDataTypeDomains (typecheck)

Check failure on line 155 in internal/config/config_print_test.go

View workflow job for this annotation

GitHub Actions / Test

undefined: pp.PrivateDataTypeDomains
printItem(innerMockPP, "Proxied domains:", "(none)"),
printItem(innerMockPP, "Unproxied domains:", "(none)"),
printItem(innerMockPP, "Record comment:", "(empty)"),
Expand Down
22 changes: 18 additions & 4 deletions internal/config/config_read.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import (
"github.com/favonia/cloudflare-ddns/internal/provider"
)

func InitializePP(ppfmt *pp.PP) bool {
return ReadEmoji("EMOJI", ppfmt) && ReadQuiet("QUIET", ppfmt) && ReadRedaction("LOG_REDACTION", ppfmt)
}

// ReadEnv calls the relevant readers to read all relevant environment variables except TZ
// and update relevant fields. One should subsequently call [Config.NormalizeConfig]
// to maintain invariants across different fields.
Expand Down Expand Up @@ -112,9 +116,11 @@ func (c *Config) NormalizeConfig(ppfmt pp.PP) bool {
continue
}

ppfmt.Warningf(pp.EmojiUserWarning,
"Domain %q is ignored because it is only for %s but %s is disabled",
domain.Describe(), ipNet.Describe(), ipNet.Describe())
if !ppfmt.ShouldRedact(pp.Domains) {
ppfmt.Warningf(pp.EmojiUserWarning,
"Domain %q is ignored because it is only for %s but %s is disabled",
domain.Describe(), ipNet.Describe(), ipNet.Describe())
}
}
}

Expand All @@ -123,8 +129,16 @@ func (c *Config) NormalizeConfig(ppfmt pp.PP) bool {
if !ok {
return false
}
allProxied := true
for dom := range activeDomainSet {
proxiedMap[dom] = proxiedPred(dom)
proxied := proxiedPred(dom)
allProxied = allProxied && proxied
proxiedMap[dom] = proxied
}
// Warn about LOG_REDACTION=ip and PROXIED=false
if ppfmt.ShouldRedact(pp.IPs) && !allProxied {
ppfmt.Warningf(pp.EmojiUserWarning,
"Some domains are not proxied by Cloudflare; their DNS records can leak IP addresses")
}

// Part 3: override the old values
Expand Down
23 changes: 23 additions & 0 deletions internal/config/env_base.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,29 @@ func ReadQuiet(key string, ppfmt *pp.PP) bool {
return true
}

// ReadRedaction reads an environment variable as the redaction mask.
func ReadRedaction(key string, ppfmt *pp.PP) bool {
valRedaction := Getenv(key)
if valRedaction == "" {
return true
}

switch valRedaction {
case "min":
*ppfmt = (*ppfmt).SetRedactMask(pp.RedactNone)
return true
case "token":
*ppfmt = (*ppfmt).SetRedactMask(pp.RedactTokens)
return true
case "max":
*ppfmt = (*ppfmt).SetRedactMask(pp.RedactMaximum)
return true
default:
(*ppfmt).Errorf(pp.EmojiUserError, "%s (%q) is not a supported redaction mode", key, valRedaction)
return false
}
}

// ReadBool reads an environment variable as a boolean value.
func ReadBool(ppfmt pp.PP, key string, field *bool) bool {
val := Getenv(key)
Expand Down
76 changes: 76 additions & 0 deletions internal/mocks/mock_pp.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions internal/pp/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ type PP interface {
// IsEnabledFor checks whether a message of a certain level will be displayed.
IsEnabledFor(v Verbosity) bool

// SetRedactMask sets the mask to determine the redaction.
SetRedactMask(m RedactMask) PP

// ShouldRedact(t) returns whether data of type t should be redacted.
ShouldRedact(t PrivateDataType) bool

// IncIndent returns a new pretty-printer with more indentation.
IncIndent() PP

Expand All @@ -29,3 +35,11 @@ type PP interface {
// Errorf formats and prints a message at the error level.
Errorf(emoji Emoji, format string, args ...any)
}

func Redact(pp PP, t PrivateDataType, orig string, redacted string) string {
if pp.ShouldRedact(t) {
return redacted
} else {
return orig
}
}
27 changes: 19 additions & 8 deletions internal/pp/fmt.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,21 @@ import (
)

type formatter struct {
writer io.Writer
emoji bool
indent int
verbosity Verbosity
writer io.Writer
emoji bool
indent int
verbosity Verbosity
redactionMask RedactMask
}

// New creates a new pretty printer.
func New(writer io.Writer) PP {
return formatter{
writer: writer,
emoji: true,
indent: 0,
verbosity: DefaultVerbosity,
writer: writer,
emoji: true,
indent: 0,
verbosity: DefaultVerbosity,
redactionMask: DefaultRedactMask,
}
}

Expand All @@ -37,6 +39,15 @@ func (f formatter) IsEnabledFor(v Verbosity) bool {
return v >= f.verbosity
}

func (f formatter) SetRedactMask(m RedactMask) PP {
f.redactionMask = m
return f
}

func (f formatter) ShouldRedact(t PrivateDataType) bool {
return f.redactionMask&RedactMask(t) > 0
}

func (f formatter) IncIndent() PP {
f.indent++
return f
Expand Down
Loading
Loading