-
-
Notifications
You must be signed in to change notification settings - Fork 566
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add status anonymization * Add OS/arch to the status command * Use human-friendly last-update status messages * Add debug bundle command to collect (anonymized) logs * Add debug log level command * And debug for a certain time span command
- Loading branch information
Showing
12 changed files
with
1,588 additions
and
104 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,212 @@ | ||
package anonymize | ||
|
||
import ( | ||
"crypto/rand" | ||
"fmt" | ||
"math/big" | ||
"net" | ||
"net/netip" | ||
"net/url" | ||
"regexp" | ||
"slices" | ||
"strings" | ||
) | ||
|
||
type Anonymizer struct { | ||
ipAnonymizer map[netip.Addr]netip.Addr | ||
domainAnonymizer map[string]string | ||
currentAnonIPv4 netip.Addr | ||
currentAnonIPv6 netip.Addr | ||
startAnonIPv4 netip.Addr | ||
startAnonIPv6 netip.Addr | ||
} | ||
|
||
func DefaultAddresses() (netip.Addr, netip.Addr) { | ||
// 192.51.100.0, 100:: | ||
return netip.AddrFrom4([4]byte{198, 51, 100, 0}), netip.AddrFrom16([16]byte{0x01}) | ||
} | ||
|
||
func NewAnonymizer(startIPv4, startIPv6 netip.Addr) *Anonymizer { | ||
return &Anonymizer{ | ||
ipAnonymizer: map[netip.Addr]netip.Addr{}, | ||
domainAnonymizer: map[string]string{}, | ||
currentAnonIPv4: startIPv4, | ||
currentAnonIPv6: startIPv6, | ||
startAnonIPv4: startIPv4, | ||
startAnonIPv6: startIPv6, | ||
} | ||
} | ||
|
||
func (a *Anonymizer) AnonymizeIP(ip netip.Addr) netip.Addr { | ||
if ip.IsLoopback() || | ||
ip.IsLinkLocalUnicast() || | ||
ip.IsLinkLocalMulticast() || | ||
ip.IsInterfaceLocalMulticast() || | ||
ip.IsPrivate() || | ||
ip.IsUnspecified() || | ||
ip.IsMulticast() || | ||
isWellKnown(ip) || | ||
a.isInAnonymizedRange(ip) { | ||
|
||
return ip | ||
} | ||
|
||
if _, ok := a.ipAnonymizer[ip]; !ok { | ||
if ip.Is4() { | ||
a.ipAnonymizer[ip] = a.currentAnonIPv4 | ||
a.currentAnonIPv4 = a.currentAnonIPv4.Next() | ||
} else { | ||
a.ipAnonymizer[ip] = a.currentAnonIPv6 | ||
a.currentAnonIPv6 = a.currentAnonIPv6.Next() | ||
} | ||
} | ||
return a.ipAnonymizer[ip] | ||
} | ||
|
||
// isInAnonymizedRange checks if an IP is within the range of already assigned anonymized IPs | ||
func (a *Anonymizer) isInAnonymizedRange(ip netip.Addr) bool { | ||
if ip.Is4() && ip.Compare(a.startAnonIPv4) >= 0 && ip.Compare(a.currentAnonIPv4) <= 0 { | ||
return true | ||
} else if !ip.Is4() && ip.Compare(a.startAnonIPv6) >= 0 && ip.Compare(a.currentAnonIPv6) <= 0 { | ||
return true | ||
} | ||
return false | ||
} | ||
|
||
func (a *Anonymizer) AnonymizeIPString(ip string) string { | ||
addr, err := netip.ParseAddr(ip) | ||
if err != nil { | ||
return ip | ||
} | ||
|
||
return a.AnonymizeIP(addr).String() | ||
} | ||
|
||
func (a *Anonymizer) AnonymizeDomain(domain string) string { | ||
if strings.HasSuffix(domain, "netbird.io") || | ||
strings.HasSuffix(domain, "netbird.selfhosted") || | ||
strings.HasSuffix(domain, "netbird.cloud") || | ||
strings.HasSuffix(domain, "netbird.stage") || | ||
strings.HasSuffix(domain, ".domain") { | ||
return domain | ||
} | ||
|
||
parts := strings.Split(domain, ".") | ||
if len(parts) < 2 { | ||
return domain | ||
} | ||
|
||
baseDomain := parts[len(parts)-2] + "." + parts[len(parts)-1] | ||
|
||
anonymized, ok := a.domainAnonymizer[baseDomain] | ||
if !ok { | ||
anonymizedBase := "anon-" + generateRandomString(5) + ".domain" | ||
a.domainAnonymizer[baseDomain] = anonymizedBase | ||
anonymized = anonymizedBase | ||
} | ||
|
||
return strings.Replace(domain, baseDomain, anonymized, 1) | ||
} | ||
|
||
func (a *Anonymizer) AnonymizeURI(uri string) string { | ||
u, err := url.Parse(uri) | ||
if err != nil { | ||
return uri | ||
} | ||
|
||
var anonymizedHost string | ||
if u.Opaque != "" { | ||
host, port, err := net.SplitHostPort(u.Opaque) | ||
if err == nil { | ||
anonymizedHost = fmt.Sprintf("%s:%s", a.AnonymizeDomain(host), port) | ||
} else { | ||
anonymizedHost = a.AnonymizeDomain(u.Opaque) | ||
} | ||
u.Opaque = anonymizedHost | ||
} else if u.Host != "" { | ||
host, port, err := net.SplitHostPort(u.Host) | ||
if err == nil { | ||
anonymizedHost = fmt.Sprintf("%s:%s", a.AnonymizeDomain(host), port) | ||
} else { | ||
anonymizedHost = a.AnonymizeDomain(u.Host) | ||
} | ||
u.Host = anonymizedHost | ||
} | ||
return u.String() | ||
} | ||
|
||
func (a *Anonymizer) AnonymizeString(str string) string { | ||
ipv4Regex := regexp.MustCompile(`\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b`) | ||
ipv6Regex := regexp.MustCompile(`\b([0-9a-fA-F:]+:+[0-9a-fA-F]{0,4})(?:%[0-9a-zA-Z]+)?(?:\/[0-9]{1,3})?(?::[0-9]{1,5})?\b`) | ||
|
||
str = ipv4Regex.ReplaceAllStringFunc(str, a.AnonymizeIPString) | ||
str = ipv6Regex.ReplaceAllStringFunc(str, a.AnonymizeIPString) | ||
|
||
for domain, anonDomain := range a.domainAnonymizer { | ||
str = strings.ReplaceAll(str, domain, anonDomain) | ||
} | ||
|
||
str = a.AnonymizeSchemeURI(str) | ||
str = a.AnonymizeDNSLogLine(str) | ||
|
||
return str | ||
} | ||
|
||
// AnonymizeSchemeURI finds and anonymizes URIs with stun, stuns, turn, and turns schemes. | ||
func (a *Anonymizer) AnonymizeSchemeURI(text string) string { | ||
re := regexp.MustCompile(`(?i)\b(stuns?:|turns?:|https?://)\S+\b`) | ||
|
||
return re.ReplaceAllStringFunc(text, a.AnonymizeURI) | ||
} | ||
|
||
// AnonymizeDNSLogLine anonymizes domain names in DNS log entries by replacing them with a random string. | ||
func (a *Anonymizer) AnonymizeDNSLogLine(logEntry string) string { | ||
domainPattern := `dns\.Question{Name:"([^"]+)",` | ||
domainRegex := regexp.MustCompile(domainPattern) | ||
|
||
return domainRegex.ReplaceAllStringFunc(logEntry, func(match string) string { | ||
parts := strings.Split(match, `"`) | ||
if len(parts) >= 2 { | ||
domain := parts[1] | ||
if strings.HasSuffix(domain, ".domain") { | ||
return match | ||
} | ||
randomDomain := generateRandomString(10) + ".domain" | ||
return strings.Replace(match, domain, randomDomain, 1) | ||
} | ||
return match | ||
}) | ||
} | ||
|
||
func isWellKnown(addr netip.Addr) bool { | ||
wellKnown := []string{ | ||
"8.8.8.8", "8.8.4.4", // Google DNS IPv4 | ||
"2001:4860:4860::8888", "2001:4860:4860::8844", // Google DNS IPv6 | ||
"1.1.1.1", "1.0.0.1", // Cloudflare DNS IPv4 | ||
"2606:4700:4700::1111", "2606:4700:4700::1001", // Cloudflare DNS IPv6 | ||
"9.9.9.9", "149.112.112.112", // Quad9 DNS IPv4 | ||
"2620:fe::fe", "2620:fe::9", // Quad9 DNS IPv6 | ||
} | ||
|
||
if slices.Contains(wellKnown, addr.String()) { | ||
return true | ||
} | ||
|
||
cgnatRangeStart := netip.AddrFrom4([4]byte{100, 64, 0, 0}) | ||
cgnatRange := netip.PrefixFrom(cgnatRangeStart, 10) | ||
|
||
return cgnatRange.Contains(addr) | ||
} | ||
|
||
func generateRandomString(length int) string { | ||
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" | ||
result := make([]byte, length) | ||
for i := range result { | ||
num, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters)))) | ||
if err != nil { | ||
continue | ||
} | ||
result[i] = letters[num.Int64()] | ||
} | ||
return string(result) | ||
} |
Oops, something went wrong.