diff --git a/logtail/logtail.go b/logtail/logtail.go index f17fd11d5..fcaf80e41 100644 --- a/logtail/logtail.go +++ b/logtail/logtail.go @@ -15,7 +15,9 @@ "log" mrand "math/rand" "net/http" + "net/netip" "os" + "regexp" "strconv" "sync" "sync/atomic" @@ -24,6 +26,7 @@ "tailscale.com/envknob" "tailscale.com/net/netmon" "tailscale.com/net/sockstats" + "tailscale.com/net/tsaddr" "tailscale.com/tstime" tslogger "tailscale.com/types/logger" "tailscale.com/types/logid" @@ -725,6 +728,8 @@ func (l *Logger) Logf(format string, args ...any) { fmt.Fprintf(l, format, args...) } +var obscureIPs = envknob.RegisterBool("TS_OBSCURE_LOGGED_IPS") + // Write logs an encoded JSON blob. // // If the []byte passed to Write is not an encoded JSON blob, @@ -749,6 +754,10 @@ func (l *Logger) Write(buf []byte) (int, error) { } } + if obscureIPs() { + buf = redactIPs(buf) + } + l.writeLock.Lock() defer l.writeLock.Unlock() @@ -757,6 +766,40 @@ func (l *Logger) Write(buf []byte) (int, error) { return inLen, err } +var ( + regexMatchesIPv6 = regexp.MustCompile(`([0-9a-fA-F]{1,4}):([0-9a-fA-F]{1,4}):([0-9a-fA-F:]{1,4})*`) + regexMatchesIPv4 = regexp.MustCompile(`(\d{1,3})\.(\d{1,3})\.\d{1,3}\.\d{1,3}`) +) + +// redactIPs is a helper function used in Write() to redact IPs (other than tailscale IPs). +// This function takes a log line as a byte slice and +// uses regex matching to parse and find IP addresses. Based on if the IP address is IPv4 or +// IPv6, it parses and replaces the end of the addresses with an "x". This function returns the +// log line with the IPs redacted. +func redactIPs(buf []byte) []byte { + out := regexMatchesIPv6.ReplaceAllFunc(buf, func(b []byte) []byte { + ip, err := netip.ParseAddr(string(b)) + if err != nil || tsaddr.IsTailscaleIP(ip) { + return b // don't change this one + } + + prefix := bytes.Split(b, []byte(":")) + return bytes.Join(append(prefix[:2], []byte("x")), []byte(":")) + }) + + out = regexMatchesIPv4.ReplaceAllFunc(out, func(b []byte) []byte { + ip, err := netip.ParseAddr(string(b)) + if err != nil || tsaddr.IsTailscaleIP(ip) { + return b // don't change this one + } + + prefix := bytes.Split(b, []byte(".")) + return bytes.Join(append(prefix[:2], []byte("x.x")), []byte(".")) + }) + + return []byte(out) +} + var ( openBracketV = []byte("[v") v1 = []byte("[v1] ") diff --git a/logtail/logtail_test.go b/logtail/logtail_test.go index f0d3f36b2..594214ede 100644 --- a/logtail/logtail_test.go +++ b/logtail/logtail_test.go @@ -14,6 +14,7 @@ "testing" "time" + "tailscale.com/envknob" "tailscale.com/tstest" "tailscale.com/tstime" ) @@ -406,3 +407,82 @@ func TestLoggerWriteResult(t *testing.T) { t.Errorf("mismatch.\n got: %#q\nwant: %#q", back, want) } } +func TestRedact(t *testing.T) { + envknob.Setenv("TS_OBSCURE_LOGGED_IPS", "true") + tests := []struct { + in string + want string + }{ + // tests for ipv4 addresses + { + "120.100.30.47", + "120.100.x.x", + }, + { + "192.167.0.1/65", + "192.167.x.x/65", + }, + { + "node [5Btdd] d:e89a3384f526d251 now using 10.0.0.222:41641 mtu=1360 tx=d81a8a35a0ce", + "node [5Btdd] d:e89a3384f526d251 now using 10.0.x.x:41641 mtu=1360 tx=d81a8a35a0ce", + }, + //tests for ipv6 addresses + { + "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + "2001:0db8:x", + }, + { + "2345:0425:2CA1:0000:0000:0567:5673:23b5", + "2345:0425:x", + }, + { + "2601:645:8200:edf0::c9de/64", + "2601:645:x/64", + }, + { + "node [5Btdd] d:e89a3384f526d251 now using 2051:0000:140F::875B:131C mtu=1360 tx=d81a8a35a0ce", + "node [5Btdd] d:e89a3384f526d251 now using 2051:0000:x mtu=1360 tx=d81a8a35a0ce", + }, + { + "2601:645:8200:edf0::c9de/64 2601:645:8200:edf0:1ce9:b17d:71f5:f6a3/64", + "2601:645:x/64 2601:645:x/64", + }, + //tests for tailscale ip addresses + { + "100.64.5.6", + "100.64.5.6", + }, + { + "fd7a:115c:a1e0::/96", + "fd7a:115c:a1e0::/96", + }, + //tests for ipv6 and ipv4 together + { + "192.167.0.1 2001:0db8:85a3:0000:0000:8a2e:0370:7334", + "192.167.x.x 2001:0db8:x", + }, + { + "node [5Btdd] d:e89a3384f526d251 now using 10.0.0.222:41641 mtu=1360 tx=d81a8a35a0ce 2345:0425:2CA1::0567:5673:23b5", + "node [5Btdd] d:e89a3384f526d251 now using 10.0.x.x:41641 mtu=1360 tx=d81a8a35a0ce 2345:0425:x", + }, + { + "100.64.5.6 2091:0db8:85a3:0000:0000:8a2e:0370:7334", + "100.64.5.6 2091:0db8:x", + }, + { + "192.167.0.1 120.100.30.47 2041:0000:140F::875B:131B", + "192.167.x.x 120.100.x.x 2041:0000:x", + }, + { + "fd7a:115c:a1e0::/96 192.167.0.1 2001:0db8:85a3:0000:0000:8a2e:0370:7334", + "fd7a:115c:a1e0::/96 192.167.x.x 2001:0db8:x", + }, + } + + for _, tt := range tests { + gotBuf := redactIPs([]byte(tt.in)) + if string(gotBuf) != tt.want { + t.Errorf("for %q,\n got: %#q\nwant: %#q\n", tt.in, gotBuf, tt.want) + } + } +}