Browse Source

tailscale/logtail: redact public ipv6 and ipv4 ip addresses within tailscaled. (#10531)

Updates #15664

Signed-off-by: Anishka Singh <[email protected]>
as2643 2 years ago
parent
commit
3fb6ee7fdb
2 changed files with 123 additions and 0 deletions
  1. 43 0
      logtail/logtail.go
  2. 80 0
      logtail/logtail_test.go

+ 43 - 0
logtail/logtail.go

@@ -15,7 +15,9 @@ import (
 	"log"
 	mrand "math/rand"
 	"net/http"
+	"net/netip"
 	"os"
+	"regexp"
 	"strconv"
 	"sync"
 	"sync/atomic"
@@ -24,6 +26,7 @@ import (
 	"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] ")

+ 80 - 0
logtail/logtail_test.go

@@ -14,6 +14,7 @@ import (
 	"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)
+		}
+	}
+}