From 57624203c94c686ee538f7e8f6d076967d6e8a6b Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Thu, 1 Aug 2024 18:38:19 +0200 Subject: [PATCH 01/13] Allow route updates even if some domains failed resolution (#2368) --- client/internal/routemanager/dynamic/route.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/client/internal/routemanager/dynamic/route.go b/client/internal/routemanager/dynamic/route.go index 8429b4534..e95710798 100644 --- a/client/internal/routemanager/dynamic/route.go +++ b/client/internal/routemanager/dynamic/route.go @@ -189,9 +189,14 @@ func (r *Route) startResolver(ctx context.Context) { } func (r *Route) update(ctx context.Context) error { - if resolved, err := r.resolveDomains(); err != nil { - return fmt.Errorf("resolve domains: %w", err) - } else if err := r.updateDynamicRoutes(ctx, resolved); err != nil { + resolved, err := r.resolveDomains() + if err != nil { + if len(resolved) == 0 { + return fmt.Errorf("resolve domains: %w", err) + } + log.Warnf("Failed to resolve domains: %v", err) + } + if err := r.updateDynamicRoutes(ctx, resolved); err != nil { return fmt.Errorf("update dynamic routes: %w", err) } From 216d9f2ee8bd0c7cbcd6e0bf3c4318aa8785c10e Mon Sep 17 00:00:00 2001 From: keacwu Date: Fri, 2 Aug 2024 00:52:38 +0800 Subject: [PATCH 02/13] Adding geolocation download log message. (#2085) * Adding geolocation download prompt message. * import log file and remove unnecessary else --------- Co-authored-by: Maycon Santos --- management/server/geolocation/database.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/management/server/geolocation/database.go b/management/server/geolocation/database.go index 1bada6075..c9b2eafff 100644 --- a/management/server/geolocation/database.go +++ b/management/server/geolocation/database.go @@ -9,6 +9,7 @@ import ( "path" "strconv" + log "github.com/sirupsen/logrus" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" @@ -30,6 +31,8 @@ func loadGeolocationDatabases(dataDir string) error { continue } + log.Infof("geo location file %s not found , file will be downloaded", file) + switch file { case MMDBFileName: extractFunc := func(src string, dst string) error { From f84b606506f6082581e4bb1983dd31bb1f9ffbd9 Mon Sep 17 00:00:00 2001 From: David Fry Date: Thu, 1 Aug 2024 18:52:50 +0200 Subject: [PATCH 03/13] add extra auth audience (#2350) --- management/server/config.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/management/server/config.go b/management/server/config.go index 3e5ff1eaf..4efe4fe74 100644 --- a/management/server/config.go +++ b/management/server/config.go @@ -56,6 +56,10 @@ type Config struct { func (c Config) GetAuthAudiences() []string { audiences := []string{c.HttpConfig.AuthAudience} + if c.HttpConfig.ExtraAuthAudience != "" { + audiences = append(audiences, c.HttpConfig.ExtraAuthAudience) + } + if c.DeviceAuthorizationFlow != nil && c.DeviceAuthorizationFlow.ProviderConfig.Audience != "" { audiences = append(audiences, c.DeviceAuthorizationFlow.ProviderConfig.Audience) } @@ -90,6 +94,8 @@ type HttpServerConfig struct { OIDCConfigEndpoint string // IdpSignKeyRefreshEnabled identifies the signing key is currently being rotated or not IdpSignKeyRefreshEnabled bool + // Extra audience + ExtraAuthAudience string } // Host represents a Wiretrustee host (e.g. STUN, TURN, Signal) From 5ad4ae769a9e9629b0ed52e2f8eeec0f03812639 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Fri, 2 Aug 2024 11:47:12 +0200 Subject: [PATCH 04/13] Extend client debug bundle (#2341) Adds readme (with --anonymize) Fixes archive file timestamps Adds routes info Adds interfaces Adds client config --- client/anonymize/anonymize.go | 15 + client/cmd/debug.go | 31 +- client/cmd/root.go | 4 + client/cmd/status.go | 17 +- .../routemanager/systemops/systemops_bsd.go | 2 +- .../systemops/systemops_generic.go | 4 +- .../routemanager/systemops/systemops_linux.go | 4 +- .../systemops/systemops_nonlinux.go | 2 +- .../systemops/systemops_windows.go | 2 +- client/proto/daemon.pb.go | 18 +- client/proto/daemon.proto | 1 + client/server/debug.go | 431 +++++++++++++++--- 12 files changed, 435 insertions(+), 96 deletions(-) diff --git a/client/anonymize/anonymize.go b/client/anonymize/anonymize.go index acbd0441e..208e74d53 100644 --- a/client/anonymize/anonymize.go +++ b/client/anonymize/anonymize.go @@ -178,6 +178,21 @@ func (a *Anonymizer) AnonymizeDNSLogLine(logEntry string) string { }) } +// AnonymizeRoute anonymizes a route string by replacing IP addresses with anonymized versions and +// domain names with random strings. +func (a *Anonymizer) AnonymizeRoute(route string) string { + prefix, err := netip.ParsePrefix(route) + if err == nil { + ip := a.AnonymizeIPString(prefix.Addr().String()) + return fmt.Sprintf("%s/%d", ip, prefix.Bits()) + } + domains := strings.Split(route, ", ") + for i, domain := range domains { + domains[i] = a.AnonymizeDomain(domain) + } + return strings.Join(domains, ", ") +} + func isWellKnown(addr netip.Addr) bool { wellKnown := []string{ "8.8.8.8", "8.8.4.4", // Google DNS IPv4 diff --git a/client/cmd/debug.go b/client/cmd/debug.go index da5e0945a..dbdce91ab 100644 --- a/client/cmd/debug.go +++ b/client/cmd/debug.go @@ -5,6 +5,7 @@ import ( "fmt" "time" + log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "google.golang.org/grpc/status" @@ -13,6 +14,8 @@ import ( "github.com/netbirdio/netbird/client/server" ) +const errCloseConnection = "Failed to close connection: %v" + var debugCmd = &cobra.Command{ Use: "debug", Short: "Debugging commands", @@ -63,12 +66,17 @@ func debugBundle(cmd *cobra.Command, _ []string) error { if err != nil { return err } - defer conn.Close() + defer func() { + if err := conn.Close(); err != nil { + log.Errorf(errCloseConnection, err) + } + }() client := proto.NewDaemonServiceClient(conn) resp, err := client.DebugBundle(cmd.Context(), &proto.DebugBundleRequest{ - Anonymize: anonymizeFlag, - Status: getStatusOutput(cmd), + Anonymize: anonymizeFlag, + Status: getStatusOutput(cmd), + SystemInfo: debugSystemInfoFlag, }) if err != nil { return fmt.Errorf("failed to bundle debug: %v", status.Convert(err).Message()) @@ -84,7 +92,11 @@ func setLogLevel(cmd *cobra.Command, args []string) error { if err != nil { return err } - defer conn.Close() + defer func() { + if err := conn.Close(); err != nil { + log.Errorf(errCloseConnection, err) + } + }() client := proto.NewDaemonServiceClient(conn) level := server.ParseLogLevel(args[0]) @@ -113,7 +125,11 @@ func runForDuration(cmd *cobra.Command, args []string) error { if err != nil { return err } - defer conn.Close() + defer func() { + if err := conn.Close(); err != nil { + log.Errorf(errCloseConnection, err) + } + }() client := proto.NewDaemonServiceClient(conn) @@ -189,8 +205,9 @@ func runForDuration(cmd *cobra.Command, args []string) error { cmd.Println("Creating debug bundle...") resp, err := client.DebugBundle(cmd.Context(), &proto.DebugBundleRequest{ - Anonymize: anonymizeFlag, - Status: statusOutput, + Anonymize: anonymizeFlag, + Status: statusOutput, + SystemInfo: debugSystemInfoFlag, }) if err != nil { return fmt.Errorf("failed to bundle debug: %v", status.Convert(err).Message()) diff --git a/client/cmd/root.go b/client/cmd/root.go index 1e5c56366..010ffb25a 100644 --- a/client/cmd/root.go +++ b/client/cmd/root.go @@ -37,6 +37,7 @@ const ( serverSSHAllowedFlag = "allow-server-ssh" extraIFaceBlackListFlag = "extra-iface-blacklist" dnsRouteIntervalFlag = "dns-router-interval" + systemInfoFlag = "system-info" ) var ( @@ -69,6 +70,7 @@ var ( autoConnectDisabled bool extraIFaceBlackList []string anonymizeFlag bool + debugSystemInfoFlag bool dnsRouteInterval time.Duration rootCmd = &cobra.Command{ @@ -165,6 +167,8 @@ func init() { upCmd.PersistentFlags().BoolVar(&rosenpassPermissive, rosenpassPermissiveFlag, false, "[Experimental] Enable Rosenpass in permissive mode to allow this peer to accept WireGuard connections without requiring Rosenpass functionality from peers that do not have Rosenpass enabled.") upCmd.PersistentFlags().BoolVar(&serverSSHAllowed, serverSSHAllowedFlag, false, "Allow SSH server on peer. If enabled, the SSH server will be permitted") upCmd.PersistentFlags().BoolVar(&autoConnectDisabled, disableAutoConnectFlag, false, "Disables auto-connect feature. If enabled, then the client won't connect automatically when the service starts.") + + debugCmd.PersistentFlags().BoolVarP(&debugSystemInfoFlag, systemInfoFlag, "S", false, "Adds system information to the debug bundle") } // SetupCloseHandler handles SIGTERM signal and exits with success diff --git a/client/cmd/status.go b/client/cmd/status.go index e6c7b8be8..d9b7a9c91 100644 --- a/client/cmd/status.go +++ b/client/cmd/status.go @@ -807,7 +807,7 @@ func anonymizePeerDetail(a *anonymize.Anonymizer, peer *peerStateDetailOutput) { } for i, route := range peer.Routes { - peer.Routes[i] = anonymizeRoute(a, route) + peer.Routes[i] = a.AnonymizeRoute(route) } } @@ -843,21 +843,8 @@ func anonymizeOverview(a *anonymize.Anonymizer, overview *statusOutputOverview) } for i, route := range overview.Routes { - overview.Routes[i] = anonymizeRoute(a, route) + overview.Routes[i] = a.AnonymizeRoute(route) } overview.FQDN = a.AnonymizeDomain(overview.FQDN) } - -func anonymizeRoute(a *anonymize.Anonymizer, route string) string { - prefix, err := netip.ParsePrefix(route) - if err == nil { - ip := a.AnonymizeIPString(prefix.Addr().String()) - return fmt.Sprintf("%s/%d", ip, prefix.Bits()) - } - domains := strings.Split(route, ", ") - for i, domain := range domains { - domains[i] = a.AnonymizeDomain(domain) - } - return strings.Join(domains, ", ") -} diff --git a/client/internal/routemanager/systemops/systemops_bsd.go b/client/internal/routemanager/systemops/systemops_bsd.go index b7fb554db..5e3b20a86 100644 --- a/client/internal/routemanager/systemops/systemops_bsd.go +++ b/client/internal/routemanager/systemops/systemops_bsd.go @@ -22,7 +22,7 @@ type Route struct { Interface *net.Interface } -func getRoutesFromTable() ([]netip.Prefix, error) { +func GetRoutesFromTable() ([]netip.Prefix, error) { tab, err := retryFetchRIB() if err != nil { return nil, fmt.Errorf("fetch RIB: %v", err) diff --git a/client/internal/routemanager/systemops/systemops_generic.go b/client/internal/routemanager/systemops/systemops_generic.go index 615e1b528..671545b86 100644 --- a/client/internal/routemanager/systemops/systemops_generic.go +++ b/client/internal/routemanager/systemops/systemops_generic.go @@ -427,7 +427,7 @@ func ipToAddr(ip net.IP, intf *net.Interface) (netip.Addr, error) { } func existsInRouteTable(prefix netip.Prefix) (bool, error) { - routes, err := getRoutesFromTable() + routes, err := GetRoutesFromTable() if err != nil { return false, fmt.Errorf("get routes from table: %w", err) } @@ -440,7 +440,7 @@ func existsInRouteTable(prefix netip.Prefix) (bool, error) { } func isSubRange(prefix netip.Prefix) (bool, error) { - routes, err := getRoutesFromTable() + routes, err := GetRoutesFromTable() if err != nil { return false, fmt.Errorf("get routes from table: %w", err) } diff --git a/client/internal/routemanager/systemops/systemops_linux.go b/client/internal/routemanager/systemops/systemops_linux.go index c4f69fba5..2d0c57826 100644 --- a/client/internal/routemanager/systemops/systemops_linux.go +++ b/client/internal/routemanager/systemops/systemops_linux.go @@ -206,7 +206,7 @@ func (r *SysOps) RemoveVPNRoute(prefix netip.Prefix, intf *net.Interface) error return nil } -func getRoutesFromTable() ([]netip.Prefix, error) { +func GetRoutesFromTable() ([]netip.Prefix, error) { v4Routes, err := getRoutes(syscall.RT_TABLE_MAIN, netlink.FAMILY_V4) if err != nil { return nil, fmt.Errorf("get v4 routes: %w", err) @@ -504,7 +504,7 @@ func getAddressFamily(prefix netip.Prefix) int { func hasSeparateRouting() ([]netip.Prefix, error) { if isLegacy() { - return getRoutesFromTable() + return GetRoutesFromTable() } return nil, ErrRoutingIsSeparate } diff --git a/client/internal/routemanager/systemops/systemops_nonlinux.go b/client/internal/routemanager/systemops/systemops_nonlinux.go index 0adeb0992..3b52fc7af 100644 --- a/client/internal/routemanager/systemops/systemops_nonlinux.go +++ b/client/internal/routemanager/systemops/systemops_nonlinux.go @@ -24,5 +24,5 @@ func EnableIPForwarding() error { } func hasSeparateRouting() ([]netip.Prefix, error) { - return getRoutesFromTable() + return GetRoutesFromTable() } diff --git a/client/internal/routemanager/systemops/systemops_windows.go b/client/internal/routemanager/systemops/systemops_windows.go index 88bdce7c9..0d3630cb8 100644 --- a/client/internal/routemanager/systemops/systemops_windows.go +++ b/client/internal/routemanager/systemops/systemops_windows.go @@ -94,7 +94,7 @@ func (r *SysOps) removeFromRouteTable(prefix netip.Prefix, nexthop Nexthop) erro return nil } -func getRoutesFromTable() ([]netip.Prefix, error) { +func GetRoutesFromTable() ([]netip.Prefix, error) { mux.Lock() defer mux.Unlock() diff --git a/client/proto/daemon.pb.go b/client/proto/daemon.pb.go index 813540246..fb10a38d3 100644 --- a/client/proto/daemon.pb.go +++ b/client/proto/daemon.pb.go @@ -1828,8 +1828,9 @@ type DebugBundleRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Anonymize bool `protobuf:"varint,1,opt,name=anonymize,proto3" json:"anonymize,omitempty"` - Status string `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"` + Anonymize bool `protobuf:"varint,1,opt,name=anonymize,proto3" json:"anonymize,omitempty"` + Status string `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"` + SystemInfo bool `protobuf:"varint,3,opt,name=systemInfo,proto3" json:"systemInfo,omitempty"` } func (x *DebugBundleRequest) Reset() { @@ -1878,6 +1879,13 @@ func (x *DebugBundleRequest) GetStatus() string { return "" } +func (x *DebugBundleRequest) GetSystemInfo() bool { + if x != nil { + return x.SystemInfo + } + return false +} + type DebugBundleResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -2370,11 +2378,13 @@ var file_daemon_proto_rawDesc = []byte{ 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x24, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x49, 0x50, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, - 0x02, 0x38, 0x01, 0x22, 0x4a, 0x0a, 0x12, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, + 0x02, 0x38, 0x01, 0x22, 0x6a, 0x0a, 0x12, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x6e, 0x6f, 0x6e, 0x79, 0x6d, 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x6e, 0x6f, 0x6e, 0x79, 0x6d, 0x69, 0x7a, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, + 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, + 0x1e, 0x0a, 0x0a, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0x29, 0x0a, 0x13, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x22, 0x14, 0x0a, 0x12, 0x47, 0x65, diff --git a/client/proto/daemon.proto b/client/proto/daemon.proto index 267eec279..43c379fb5 100644 --- a/client/proto/daemon.proto +++ b/client/proto/daemon.proto @@ -263,6 +263,7 @@ message Route { message DebugBundleRequest { bool anonymize = 1; string status = 2; + bool systemInfo = 3; } message DebugBundleResponse { diff --git a/client/server/debug.go b/client/server/debug.go index 9b6a52659..1187f3187 100644 --- a/client/server/debug.go +++ b/client/server/debug.go @@ -1,3 +1,5 @@ +//go:build !android && !ios + package server import ( @@ -6,16 +8,70 @@ import ( "context" "fmt" "io" + "net" + "net/netip" "os" + "sort" "strings" + "time" log "github.com/sirupsen/logrus" "github.com/netbirdio/netbird/client/anonymize" "github.com/netbirdio/netbird/client/internal/peer" + "github.com/netbirdio/netbird/client/internal/routemanager/systemops" "github.com/netbirdio/netbird/client/proto" ) +const readmeContent = `Netbird debug bundle +This debug bundle contains the following files: + +status.txt: Anonymized status information of the NetBird client. +client.log: Most recent, anonymized log file of the NetBird client. +routes.txt: Anonymized system routes, if --system-info flag was provided. +interfaces.txt: Anonymized network interface information, if --system-info flag was provided. +config.txt: Anonymized configuration information of the NetBird client. + + +Anonymization Process +The files in this bundle have been anonymized to protect sensitive information. Here's how the anonymization was applied: + +IP Addresses + +IPv4 addresses are replaced with addresses starting from 192.51.100.0 +IPv6 addresses are replaced with addresses starting from 100:: + +IP addresses from non public ranges and well known addresses are not anonymized (e.g. 8.8.8.8, 100.64.0.0/10, addresses starting with 192.168., 172.16., 10., etc.). +Reoccuring IP addresses are replaced with the same anonymized address. + +Note: The anonymized IP addresses in the status file do not match those in the log and routes files. However, the anonymized IP addresses are consistent within the status file and across the routes and log files. + +Domains +All domain names (except for the netbird domains) are replaced with randomly generated strings ending in ".domain". Anonymized domains are consistent across all files in the bundle. +Reoccuring domain names are replaced with the same anonymized domain. + +Routes +For anonymized routes, the IP addresses are replaced as described above. The prefix length remains unchanged. Note that for prefixes, the anonymized IP might not be a network address, but the prefix length is still correct. +Network Interfaces +The interfaces.txt file contains information about network interfaces, including: +- Interface name +- Interface index +- MTU (Maximum Transmission Unit) +- Flags +- IP addresses associated with each interface + +The IP addresses in the interfaces file are anonymized using the same process as described above. Interface names, indexes, MTUs, and flags are not anonymized. + +Configuration +The config.txt file contains anonymized configuration information of the NetBird client. Sensitive information such as private keys and SSH keys are excluded. The following fields are anonymized: +- ManagementURL +- AdminURL +- NATExternalIPs +- CustomDNSAddress + +Other non-sensitive configuration options are included without anonymization. +` + // DebugBundle creates a debug bundle and returns the location. func (s *Server) DebugBundle(_ context.Context, req *proto.DebugBundleRequest) (resp *proto.DebugBundleResponse, err error) { s.mutex.Lock() @@ -30,93 +86,211 @@ func (s *Server) DebugBundle(_ context.Context, req *proto.DebugBundleRequest) ( return nil, fmt.Errorf("create zip file: %w", err) } defer func() { - if err := bundlePath.Close(); err != nil { - log.Errorf("failed to close zip file: %v", err) + if closeErr := bundlePath.Close(); closeErr != nil && err == nil { + err = fmt.Errorf("close zip file: %w", closeErr) } if err != nil { - if err2 := os.Remove(bundlePath.Name()); err2 != nil { - log.Errorf("Failed to remove zip file: %v", err2) + if removeErr := os.Remove(bundlePath.Name()); removeErr != nil { + log.Errorf("Failed to remove zip file: %v", removeErr) } } }() - archive := zip.NewWriter(bundlePath) - defer func() { - if err := archive.Close(); err != nil { - log.Errorf("failed to close archive writer: %v", err) - } - }() - - if status := req.GetStatus(); status != "" { - filename := "status.txt" - if req.GetAnonymize() { - filename = "status.anon.txt" - } - statusReader := strings.NewReader(status) - if err := addFileToZip(archive, statusReader, filename); err != nil { - return nil, fmt.Errorf("add status file to zip: %w", err) - } - } - - logFile, err := os.Open(s.logFile) - if err != nil { - return nil, fmt.Errorf("open log file: %w", err) - } - defer func() { - if err := logFile.Close(); err != nil { - log.Errorf("failed to close original log file: %v", err) - } - }() - - filename := "client.log.txt" - var logReader io.Reader - errChan := make(chan error, 1) - if req.GetAnonymize() { - filename = "client.anon.log.txt" - var writer io.WriteCloser - logReader, writer = io.Pipe() - - go s.anonymize(logFile, writer, errChan) - } else { - logReader = logFile - } - if err := addFileToZip(archive, logReader, filename); err != nil { - return nil, fmt.Errorf("add log file to zip: %w", err) - } - - select { - case err := <-errChan: - if err != nil { - return nil, err - } - default: + if err := s.createArchive(bundlePath, req); err != nil { + return nil, err } return &proto.DebugBundleResponse{Path: bundlePath.Name()}, nil } -func (s *Server) anonymize(reader io.Reader, writer io.WriteCloser, errChan chan<- error) { - scanner := bufio.NewScanner(reader) - anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses()) +func (s *Server) createArchive(bundlePath *os.File, req *proto.DebugBundleRequest) error { + archive := zip.NewWriter(bundlePath) + if err := s.addReadme(req, archive); err != nil { + return fmt.Errorf("add readme: %w", err) + } + if err := s.addStatus(req, archive); err != nil { + return fmt.Errorf("add status: %w", err) + } + + anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses()) status := s.statusRecorder.GetFullStatus() seedFromStatus(anonymizer, &status) + if err := s.addConfig(req, anonymizer, archive); err != nil { + return fmt.Errorf("add config: %w", err) + } + + if req.GetSystemInfo() { + if err := s.addRoutes(req, anonymizer, archive); err != nil { + return fmt.Errorf("add routes: %w", err) + } + + if err := s.addInterfaces(req, anonymizer, archive); err != nil { + return fmt.Errorf("add interfaces: %w", err) + } + } + + if err := s.addLogfile(req, anonymizer, archive); err != nil { + return fmt.Errorf("add log file: %w", err) + } + + if err := archive.Close(); err != nil { + return fmt.Errorf("close archive writer: %w", err) + } + return nil +} + +func (s *Server) addReadme(req *proto.DebugBundleRequest, archive *zip.Writer) error { + if req.GetAnonymize() { + readmeReader := strings.NewReader(readmeContent) + if err := addFileToZip(archive, readmeReader, "README.txt"); err != nil { + return fmt.Errorf("add README file to zip: %w", err) + } + } + return nil +} + +func (s *Server) addStatus(req *proto.DebugBundleRequest, archive *zip.Writer) error { + if status := req.GetStatus(); status != "" { + statusReader := strings.NewReader(status) + if err := addFileToZip(archive, statusReader, "status.txt"); err != nil { + return fmt.Errorf("add status file to zip: %w", err) + } + } + return nil +} + +func (s *Server) addConfig(req *proto.DebugBundleRequest, anonymizer *anonymize.Anonymizer, archive *zip.Writer) error { + var configContent strings.Builder + s.addCommonConfigFields(&configContent) + + if req.GetAnonymize() { + if s.config.ManagementURL != nil { + configContent.WriteString(fmt.Sprintf("ManagementURL: %s\n", anonymizer.AnonymizeURI(s.config.ManagementURL.String()))) + } + if s.config.AdminURL != nil { + configContent.WriteString(fmt.Sprintf("AdminURL: %s\n", anonymizer.AnonymizeURI(s.config.AdminURL.String()))) + } + configContent.WriteString(fmt.Sprintf("NATExternalIPs: %v\n", anonymizeNATExternalIPs(s.config.NATExternalIPs, anonymizer))) + if s.config.CustomDNSAddress != "" { + configContent.WriteString(fmt.Sprintf("CustomDNSAddress: %s\n", anonymizer.AnonymizeString(s.config.CustomDNSAddress))) + } + } else { + if s.config.ManagementURL != nil { + configContent.WriteString(fmt.Sprintf("ManagementURL: %s\n", s.config.ManagementURL.String())) + } + if s.config.AdminURL != nil { + configContent.WriteString(fmt.Sprintf("AdminURL: %s\n", s.config.AdminURL.String())) + } + configContent.WriteString(fmt.Sprintf("NATExternalIPs: %v\n", s.config.NATExternalIPs)) + if s.config.CustomDNSAddress != "" { + configContent.WriteString(fmt.Sprintf("CustomDNSAddress: %s\n", s.config.CustomDNSAddress)) + } + } + + // Add config content to zip file + configReader := strings.NewReader(configContent.String()) + if err := addFileToZip(archive, configReader, "config.txt"); err != nil { + return fmt.Errorf("add config file to zip: %w", err) + } + + return nil +} + +func (s *Server) addCommonConfigFields(configContent *strings.Builder) { + configContent.WriteString("NetBird Client Configuration:\n\n") + + // Add non-sensitive fields + configContent.WriteString(fmt.Sprintf("WgIface: %s\n", s.config.WgIface)) + configContent.WriteString(fmt.Sprintf("WgPort: %d\n", s.config.WgPort)) + if s.config.NetworkMonitor != nil { + configContent.WriteString(fmt.Sprintf("NetworkMonitor: %v\n", *s.config.NetworkMonitor)) + } + configContent.WriteString(fmt.Sprintf("IFaceBlackList: %v\n", s.config.IFaceBlackList)) + configContent.WriteString(fmt.Sprintf("DisableIPv6Discovery: %v\n", s.config.DisableIPv6Discovery)) + configContent.WriteString(fmt.Sprintf("RosenpassEnabled: %v\n", s.config.RosenpassEnabled)) + configContent.WriteString(fmt.Sprintf("RosenpassPermissive: %v\n", s.config.RosenpassPermissive)) + if s.config.ServerSSHAllowed != nil { + configContent.WriteString(fmt.Sprintf("ServerSSHAllowed: %v\n", *s.config.ServerSSHAllowed)) + } + configContent.WriteString(fmt.Sprintf("DisableAutoConnect: %v\n", s.config.DisableAutoConnect)) + configContent.WriteString(fmt.Sprintf("DNSRouteInterval: %s\n", s.config.DNSRouteInterval)) +} + +func (s *Server) addRoutes(req *proto.DebugBundleRequest, anonymizer *anonymize.Anonymizer, archive *zip.Writer) error { + if routes, err := systemops.GetRoutesFromTable(); err != nil { + log.Errorf("Failed to get routes: %v", err) + } else { + // TODO: get routes including nexthop + routesContent := formatRoutes(routes, req.GetAnonymize(), anonymizer) + routesReader := strings.NewReader(routesContent) + if err := addFileToZip(archive, routesReader, "routes.txt"); err != nil { + return fmt.Errorf("add routes file to zip: %w", err) + } + } + return nil +} + +func (s *Server) addInterfaces(req *proto.DebugBundleRequest, anonymizer *anonymize.Anonymizer, archive *zip.Writer) error { + interfaces, err := net.Interfaces() + if err != nil { + return fmt.Errorf("get interfaces: %w", err) + } + + interfacesContent := formatInterfaces(interfaces, req.GetAnonymize(), anonymizer) + interfacesReader := strings.NewReader(interfacesContent) + if err := addFileToZip(archive, interfacesReader, "interfaces.txt"); err != nil { + return fmt.Errorf("add interfaces file to zip: %w", err) + } + + return nil +} + +func (s *Server) addLogfile(req *proto.DebugBundleRequest, anonymizer *anonymize.Anonymizer, archive *zip.Writer) (err error) { + logFile, err := os.Open(s.logFile) + if err != nil { + return fmt.Errorf("open log file: %w", err) + } defer func() { - if err := writer.Close(); err != nil { - log.Errorf("Failed to close writer: %v", err) + if err := logFile.Close(); err != nil { + log.Errorf("Failed to close original log file: %v", err) } }() + + var logReader io.Reader + if req.GetAnonymize() { + var writer *io.PipeWriter + logReader, writer = io.Pipe() + + go s.anonymize(logFile, writer, anonymizer) + } else { + logReader = logFile + } + if err := addFileToZip(archive, logReader, "client.log"); err != nil { + return fmt.Errorf("add log file to zip: %w", err) + } + + return nil +} + +func (s *Server) anonymize(reader io.Reader, writer *io.PipeWriter, anonymizer *anonymize.Anonymizer) { + defer func() { + // always nil + _ = writer.Close() + }() + + scanner := bufio.NewScanner(reader) for scanner.Scan() { line := anonymizer.AnonymizeString(scanner.Text()) if _, err := writer.Write([]byte(line + "\n")); err != nil { - errChan <- fmt.Errorf("write line to writer: %w", err) + writer.CloseWithError(fmt.Errorf("anonymize write: %w", err)) return } } if err := scanner.Err(); err != nil { - errChan <- fmt.Errorf("read line from scanner: %w", err) + writer.CloseWithError(fmt.Errorf("anonymize scan: %w", err)) return } } @@ -141,8 +315,22 @@ func (s *Server) SetLogLevel(_ context.Context, req *proto.SetLogLevelRequest) ( func addFileToZip(archive *zip.Writer, reader io.Reader, filename string) error { header := &zip.FileHeader{ - Name: filename, - Method: zip.Deflate, + Name: filename, + Method: zip.Deflate, + Modified: time.Now(), + + CreatorVersion: 20, // Version 2.0 + ReaderVersion: 20, // Version 2.0 + Flags: 0x800, // UTF-8 filename + } + + // If the reader is a file, we can get more accurate information + if f, ok := reader.(*os.File); ok { + if stat, err := f.Stat(); err != nil { + log.Tracef("Failed to get file stat for %s: %v", filename, err) + } else { + header.Modified = stat.ModTime() + } } writer, err := archive.CreateHeader(header) @@ -165,6 +353,13 @@ func seedFromStatus(a *anonymize.Anonymizer, status *peer.FullStatus) { for _, peer := range status.Peers { a.AnonymizeDomain(peer.FQDN) + for route := range peer.GetRoutes() { + a.AnonymizeRoute(route) + } + } + + for route := range status.LocalPeerState.Routes { + a.AnonymizeRoute(route) } for _, nsGroup := range status.NSGroupStates { @@ -179,3 +374,113 @@ func seedFromStatus(a *anonymize.Anonymizer, status *peer.FullStatus) { } } } + +func formatRoutes(routes []netip.Prefix, anonymize bool, anonymizer *anonymize.Anonymizer) string { + var ipv4Routes, ipv6Routes []netip.Prefix + + // Separate IPv4 and IPv6 routes + for _, route := range routes { + if route.Addr().Is4() { + ipv4Routes = append(ipv4Routes, route) + } else { + ipv6Routes = append(ipv6Routes, route) + } + } + + // Sort IPv4 and IPv6 routes separately + sort.Slice(ipv4Routes, func(i, j int) bool { + return ipv4Routes[i].Bits() > ipv4Routes[j].Bits() + }) + sort.Slice(ipv6Routes, func(i, j int) bool { + return ipv6Routes[i].Bits() > ipv6Routes[j].Bits() + }) + + var builder strings.Builder + + // Format IPv4 routes + builder.WriteString("IPv4 Routes:\n") + for _, route := range ipv4Routes { + formatRoute(&builder, route, anonymize, anonymizer) + } + + // Format IPv6 routes + builder.WriteString("\nIPv6 Routes:\n") + for _, route := range ipv6Routes { + formatRoute(&builder, route, anonymize, anonymizer) + } + + return builder.String() +} + +func formatRoute(builder *strings.Builder, route netip.Prefix, anonymize bool, anonymizer *anonymize.Anonymizer) { + if anonymize { + anonymizedIP := anonymizer.AnonymizeIP(route.Addr()) + builder.WriteString(fmt.Sprintf("%s/%d\n", anonymizedIP, route.Bits())) + } else { + builder.WriteString(fmt.Sprintf("%s\n", route)) + } +} + +func formatInterfaces(interfaces []net.Interface, anonymize bool, anonymizer *anonymize.Anonymizer) string { + sort.Slice(interfaces, func(i, j int) bool { + return interfaces[i].Name < interfaces[j].Name + }) + + var builder strings.Builder + builder.WriteString("Network Interfaces:\n") + + for _, iface := range interfaces { + builder.WriteString(fmt.Sprintf("\nInterface: %s\n", iface.Name)) + builder.WriteString(fmt.Sprintf(" Index: %d\n", iface.Index)) + builder.WriteString(fmt.Sprintf(" MTU: %d\n", iface.MTU)) + builder.WriteString(fmt.Sprintf(" Flags: %v\n", iface.Flags)) + + addrs, err := iface.Addrs() + if err != nil { + builder.WriteString(fmt.Sprintf(" Addresses: Error retrieving addresses: %v\n", err)) + } else { + builder.WriteString(" Addresses:\n") + for _, addr := range addrs { + prefix, err := netip.ParsePrefix(addr.String()) + if err != nil { + builder.WriteString(fmt.Sprintf(" Error parsing address: %v\n", err)) + continue + } + ip := prefix.Addr() + if anonymize { + ip = anonymizer.AnonymizeIP(ip) + } + builder.WriteString(fmt.Sprintf(" %s/%d\n", ip, prefix.Bits())) + } + } + } + + return builder.String() +} + +func anonymizeNATExternalIPs(ips []string, anonymizer *anonymize.Anonymizer) []string { + anonymizedIPs := make([]string, len(ips)) + for i, ip := range ips { + parts := strings.SplitN(ip, "/", 2) + + ip1, err := netip.ParseAddr(parts[0]) + if err != nil { + anonymizedIPs[i] = ip + continue + } + ip1anon := anonymizer.AnonymizeIP(ip1) + + if len(parts) == 2 { + ip2, err := netip.ParseAddr(parts[1]) + if err != nil { + anonymizedIPs[i] = fmt.Sprintf("%s/%s", ip1anon, parts[1]) + } else { + ip2anon := anonymizer.AnonymizeIP(ip2) + anonymizedIPs[i] = fmt.Sprintf("%s/%s", ip1anon, ip2anon) + } + } else { + anonymizedIPs[i] = ip1anon.String() + } + } + return anonymizedIPs +} From bfc33a3f6f49d1d6b43c276d243a48f33c77bc09 Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Fri, 2 Aug 2024 14:54:37 +0200 Subject: [PATCH 05/13] Move Bundle to before netbird down (#2377) This allows to get interface and route information added by the agent --- client/cmd/debug.go | 55 +++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/client/cmd/debug.go b/client/cmd/debug.go index dbdce91ab..9abd2039d 100644 --- a/client/cmd/debug.go +++ b/client/cmd/debug.go @@ -138,17 +138,20 @@ func runForDuration(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to get status: %v", status.Convert(err).Message()) } - restoreUp := stat.Status == string(internal.StatusConnected) || stat.Status == string(internal.StatusConnecting) + stateWasDown := stat.Status != string(internal.StatusConnected) && stat.Status != string(internal.StatusConnecting) initialLogLevel, err := client.GetLogLevel(cmd.Context(), &proto.GetLogLevelRequest{}) if err != nil { return fmt.Errorf("failed to get log level: %v", status.Convert(err).Message()) } - if _, err := client.Down(cmd.Context(), &proto.DownRequest{}); err != nil { - return fmt.Errorf("failed to down: %v", status.Convert(err).Message()) + if stateWasDown { + if _, err := client.Up(cmd.Context(), &proto.UpRequest{}); err != nil { + return fmt.Errorf("failed to up: %v", status.Convert(err).Message()) + } + cmd.Println("Netbird up") + time.Sleep(time.Second * 10) } - cmd.Println("Netbird down") initialLevelTrace := initialLogLevel.GetLevel() >= proto.LogLevel_TRACE if !initialLevelTrace { @@ -161,6 +164,11 @@ func runForDuration(cmd *cobra.Command, args []string) error { cmd.Println("Log level set to trace.") } + if _, err := client.Down(cmd.Context(), &proto.DownRequest{}); err != nil { + return fmt.Errorf("failed to down: %v", status.Convert(err).Message()) + } + cmd.Println("Netbird down") + time.Sleep(1 * time.Second) if _, err := client.Up(cmd.Context(), &proto.UpRequest{}); err != nil { @@ -178,32 +186,11 @@ func runForDuration(cmd *cobra.Command, args []string) error { } cmd.Println("\nDuration completed") + cmd.Println("Creating debug bundle...") + headerPreDown := fmt.Sprintf("----- Netbird pre-down - Timestamp: %s - Duration: %s", time.Now().Format(time.RFC3339), duration) statusOutput = fmt.Sprintf("%s\n%s\n%s", statusOutput, headerPreDown, getStatusOutput(cmd)) - if _, err := client.Down(cmd.Context(), &proto.DownRequest{}); err != nil { - return fmt.Errorf("failed to down: %v", status.Convert(err).Message()) - } - cmd.Println("Netbird down") - - time.Sleep(1 * time.Second) - - if restoreUp { - if _, err := client.Up(cmd.Context(), &proto.UpRequest{}); err != nil { - return fmt.Errorf("failed to up: %v", status.Convert(err).Message()) - } - cmd.Println("Netbird up") - } - - if !initialLevelTrace { - if _, err := client.SetLogLevel(cmd.Context(), &proto.SetLogLevelRequest{Level: initialLogLevel.GetLevel()}); err != nil { - return fmt.Errorf("failed to restore log level: %v", status.Convert(err).Message()) - } - cmd.Println("Log level restored to", initialLogLevel.GetLevel()) - } - - cmd.Println("Creating debug bundle...") - resp, err := client.DebugBundle(cmd.Context(), &proto.DebugBundleRequest{ Anonymize: anonymizeFlag, Status: statusOutput, @@ -213,6 +200,20 @@ func runForDuration(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to bundle debug: %v", status.Convert(err).Message()) } + if stateWasDown { + if _, err := client.Down(cmd.Context(), &proto.DownRequest{}); err != nil { + return fmt.Errorf("failed to down: %v", status.Convert(err).Message()) + } + cmd.Println("Netbird down") + } + + if !initialLevelTrace { + if _, err := client.SetLogLevel(cmd.Context(), &proto.SetLogLevelRequest{Level: initialLogLevel.GetLevel()}); err != nil { + return fmt.Errorf("failed to restore log level: %v", status.Convert(err).Message()) + } + cmd.Println("Log level restored to", initialLogLevel.GetLevel()) + } + cmd.Println(resp.GetPath()) return nil From e6f7222034bb715d4983d02d2009192ca36d973e Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Fri, 2 Aug 2024 18:07:57 +0200 Subject: [PATCH 06/13] Fix Windows file version (#2380) Systems that validates the binary version didn't like the build number as we set This fixes the versioning and will use a static build number --- .github/workflows/release.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1889b58e7..89eb2ded9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -80,13 +80,13 @@ jobs: - name: Install goversioninfo run: go install github.com/josephspurrier/goversioninfo/cmd/goversioninfo@233067e - name: Generate windows syso 386 - run: goversioninfo -icon client/ui/netbird.ico -manifest client/manifest.xml -product-name ${{ env.PRODUCT_NAME }} -copyright "${{ env.COPYRIGHT }}" -ver-build ${{ github.run_id }} -ver-major ${{ steps.semver_parser.outputs.major }} -ver-minor ${{ steps.semver_parser.outputs.minor }} -ver-patch ${{ steps.semver_parser.outputs.patch }} -product-version ${{ steps.semver_parser.outputs.fullversion }}.${{ github.run_id }} -o client/resources_windows_386.syso + run: goversioninfo -icon client/ui/netbird.ico -manifest client/manifest.xml -product-name ${{ env.PRODUCT_NAME }} -copyright "${{ env.COPYRIGHT }}" -file-version ${{ steps.semver_parser.outputs.fullversion }}.0 -product-version ${{ steps.semver_parser.outputs.fullversion }}.0 -o client/resources_windows_386.syso - name: Generate windows syso arm - run: goversioninfo -icon client/ui/netbird.ico -manifest client/manifest.xml -product-name ${{ env.PRODUCT_NAME }} -copyright "${{ env.COPYRIGHT }}" -ver-build ${{ github.run_id }} -ver-major ${{ steps.semver_parser.outputs.major }} -ver-minor ${{ steps.semver_parser.outputs.minor }} -ver-patch ${{ steps.semver_parser.outputs.patch }} -product-version ${{ steps.semver_parser.outputs.fullversion }}.${{ github.run_id }} -o client/resources_windows_arm.syso + run: goversioninfo -icon client/ui/netbird.ico -manifest client/manifest.xml -product-name ${{ env.PRODUCT_NAME }} -copyright "${{ env.COPYRIGHT }}" -file-version ${{ steps.semver_parser.outputs.fullversion }}.0 -product-version ${{ steps.semver_parser.outputs.fullversion }}.0 -o client/resources_windows_arm.syso - name: Generate windows syso arm64 - run: goversioninfo -icon client/ui/netbird.ico -manifest client/manifest.xml -product-name ${{ env.PRODUCT_NAME }} -copyright "${{ env.COPYRIGHT }}" -ver-build ${{ github.run_id }} -ver-major ${{ steps.semver_parser.outputs.major }} -ver-minor ${{ steps.semver_parser.outputs.minor }} -ver-patch ${{ steps.semver_parser.outputs.patch }} -product-version ${{ steps.semver_parser.outputs.fullversion }}.${{ github.run_id }} -o client/resources_windows_arm64.syso + run: goversioninfo -icon client/ui/netbird.ico -manifest client/manifest.xml -product-name ${{ env.PRODUCT_NAME }} -copyright "${{ env.COPYRIGHT }}" -file-version ${{ steps.semver_parser.outputs.fullversion }}.0 -product-version ${{ steps.semver_parser.outputs.fullversion }}.0 -o client/resources_windows_arm64.syso - name: Generate windows syso amd64 - run: goversioninfo -icon client/ui/netbird.ico -manifest client/manifest.xml -product-name ${{ env.PRODUCT_NAME }} -copyright "${{ env.COPYRIGHT }}" -ver-build ${{ github.run_id }} -ver-major ${{ steps.semver_parser.outputs.major }} -ver-minor ${{ steps.semver_parser.outputs.minor }} -ver-patch ${{ steps.semver_parser.outputs.patch }} -product-version ${{ steps.semver_parser.outputs.fullversion }}.${{ github.run_id }} -o client/resources_windows_amd64.syso + run: goversioninfo -icon client/ui/netbird.ico -manifest client/manifest.xml -product-name ${{ env.PRODUCT_NAME }} -copyright "${{ env.COPYRIGHT }}" -file-version ${{ steps.semver_parser.outputs.fullversion }}.0 -product-version ${{ steps.semver_parser.outputs.fullversion }}.0 -o client/resources_windows_amd64.syso - name: Run GoReleaser uses: goreleaser/goreleaser-action@v4 @@ -170,7 +170,7 @@ jobs: - name: Install goversioninfo run: go install github.com/josephspurrier/goversioninfo/cmd/goversioninfo@233067e - name: Generate windows syso amd64 - run: goversioninfo -64 -icon client/ui/netbird.ico -manifest client/ui/manifest.xml -product-name ${{ env.PRODUCT_NAME }}-"UI" -copyright "${{ env.COPYRIGHT }}" -ver-build ${{ github.run_id }} -ver-major ${{ steps.semver_parser.outputs.major }} -ver-minor ${{ steps.semver_parser.outputs.minor }} -ver-patch ${{ steps.semver_parser.outputs.patch }} -product-version ${{ steps.semver_parser.outputs.fullversion }}.${{ github.run_id }} -o client/ui/resources_windows_amd64.syso + run: goversioninfo -64 -icon client/ui/netbird.ico -manifest client/ui/manifest.xml -product-name ${{ env.PRODUCT_NAME }}-"UI" -copyright "${{ env.COPYRIGHT }}" -file-version ${{ steps.semver_parser.outputs.fullversion }}.0 -product-version ${{ steps.semver_parser.outputs.fullversion }}.0 -o client/ui/resources_windows_amd64.syso - name: Run GoReleaser uses: goreleaser/goreleaser-action@v4 From 727a4f075311279e2f9004f51a6759bfa4f94fd7 Mon Sep 17 00:00:00 2001 From: Misha Bragin Date: Fri, 2 Aug 2024 18:20:13 +0200 Subject: [PATCH 07/13] Remove Codacy badge as it is broken (#2379) --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 5be1826b4..98dd18d37 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,6 @@ -
From 501fd93e479cc58d4a447e84437aa52023145f80 Mon Sep 17 00:00:00 2001 From: pascal-fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Fri, 2 Aug 2024 18:43:00 +0200 Subject: [PATCH 08/13] Fix DNS resolution for routes on iOS (#2378) --- client/internal/dns/server.go | 6 +- client/internal/dns/server_test.go | 2 +- client/internal/dns/service_memory.go | 20 +++---- client/internal/dns/upstream_ios.go | 38 +++++++------ client/internal/routemanager/client.go | 8 ++- client/internal/routemanager/dynamic/route.go | 19 ++++++- .../routemanager/dynamic/route_generic.go | 13 +++++ .../routemanager/dynamic/route_ios.go | 55 +++++++++++++++++++ 8 files changed, 124 insertions(+), 37 deletions(-) create mode 100644 client/internal/routemanager/dynamic/route_generic.go create mode 100644 client/internal/routemanager/dynamic/route_ios.go diff --git a/client/internal/dns/server.go b/client/internal/dns/server.go index 267c1ed80..a4651ebb5 100644 --- a/client/internal/dns/server.go +++ b/client/internal/dns/server.go @@ -94,7 +94,7 @@ func NewDefaultServer( var dnsService service if wgInterface.IsUserspaceBind() { - dnsService = newServiceViaMemory(wgInterface) + dnsService = NewServiceViaMemory(wgInterface) } else { dnsService = newServiceViaListener(wgInterface, addrPort) } @@ -112,7 +112,7 @@ func NewDefaultServerPermanentUpstream( statusRecorder *peer.Status, ) *DefaultServer { log.Debugf("host dns address list is: %v", hostsDnsList) - ds := newDefaultServer(ctx, wgInterface, newServiceViaMemory(wgInterface), statusRecorder) + ds := newDefaultServer(ctx, wgInterface, NewServiceViaMemory(wgInterface), statusRecorder) ds.hostsDNSHolder.set(hostsDnsList) ds.permanent = true ds.addHostRootZone() @@ -130,7 +130,7 @@ func NewDefaultServerIos( iosDnsManager IosDnsManager, statusRecorder *peer.Status, ) *DefaultServer { - ds := newDefaultServer(ctx, wgInterface, newServiceViaMemory(wgInterface), statusRecorder) + ds := newDefaultServer(ctx, wgInterface, NewServiceViaMemory(wgInterface), statusRecorder) ds.iosDnsManager = iosDnsManager return ds } diff --git a/client/internal/dns/server_test.go b/client/internal/dns/server_test.go index 6cbd9ea15..b9552bc17 100644 --- a/client/internal/dns/server_test.go +++ b/client/internal/dns/server_test.go @@ -534,7 +534,7 @@ func TestDNSServerStartStop(t *testing.T) { func TestDNSServerUpstreamDeactivateCallback(t *testing.T) { hostManager := &mockHostConfigurator{} server := DefaultServer{ - service: newServiceViaMemory(&mocWGIface{}), + service: NewServiceViaMemory(&mocWGIface{}), localResolver: &localResolver{ registeredMap: make(registrationMap), }, diff --git a/client/internal/dns/service_memory.go b/client/internal/dns/service_memory.go index 757cd962a..729b90cc0 100644 --- a/client/internal/dns/service_memory.go +++ b/client/internal/dns/service_memory.go @@ -12,7 +12,7 @@ import ( log "github.com/sirupsen/logrus" ) -type serviceViaMemory struct { +type ServiceViaMemory struct { wgInterface WGIface dnsMux *dns.ServeMux runtimeIP string @@ -22,8 +22,8 @@ type serviceViaMemory struct { listenerFlagLock sync.Mutex } -func newServiceViaMemory(wgIface WGIface) *serviceViaMemory { - s := &serviceViaMemory{ +func NewServiceViaMemory(wgIface WGIface) *ServiceViaMemory { + s := &ServiceViaMemory{ wgInterface: wgIface, dnsMux: dns.NewServeMux(), @@ -33,7 +33,7 @@ func newServiceViaMemory(wgIface WGIface) *serviceViaMemory { return s } -func (s *serviceViaMemory) Listen() error { +func (s *ServiceViaMemory) Listen() error { s.listenerFlagLock.Lock() defer s.listenerFlagLock.Unlock() @@ -52,7 +52,7 @@ func (s *serviceViaMemory) Listen() error { return nil } -func (s *serviceViaMemory) Stop() { +func (s *ServiceViaMemory) Stop() { s.listenerFlagLock.Lock() defer s.listenerFlagLock.Unlock() @@ -67,23 +67,23 @@ func (s *serviceViaMemory) Stop() { s.listenerIsRunning = false } -func (s *serviceViaMemory) RegisterMux(pattern string, handler dns.Handler) { +func (s *ServiceViaMemory) RegisterMux(pattern string, handler dns.Handler) { s.dnsMux.Handle(pattern, handler) } -func (s *serviceViaMemory) DeregisterMux(pattern string) { +func (s *ServiceViaMemory) DeregisterMux(pattern string) { s.dnsMux.HandleRemove(pattern) } -func (s *serviceViaMemory) RuntimePort() int { +func (s *ServiceViaMemory) RuntimePort() int { return s.runtimePort } -func (s *serviceViaMemory) RuntimeIP() string { +func (s *ServiceViaMemory) RuntimeIP() string { return s.runtimeIP } -func (s *serviceViaMemory) filterDNSTraffic() (string, error) { +func (s *ServiceViaMemory) filterDNSTraffic() (string, error) { filter := s.wgInterface.GetFilter() if filter == nil { return "", fmt.Errorf("can't set DNS filter, filter not initialized") diff --git a/client/internal/dns/upstream_ios.go b/client/internal/dns/upstream_ios.go index 0c01a013e..60ed79d87 100644 --- a/client/internal/dns/upstream_ios.go +++ b/client/internal/dns/upstream_ios.go @@ -4,6 +4,7 @@ package dns import ( "context" + "fmt" "net" "syscall" "time" @@ -17,9 +18,9 @@ import ( type upstreamResolverIOS struct { *upstreamResolverBase - lIP net.IP - lNet *net.IPNet - iIndex int + lIP net.IP + lNet *net.IPNet + interfaceName string } func newUpstreamResolver( @@ -32,17 +33,11 @@ func newUpstreamResolver( ) (*upstreamResolverIOS, error) { upstreamResolverBase := newUpstreamResolverBase(ctx, statusRecorder) - index, err := getInterfaceIndex(interfaceName) - if err != nil { - log.Debugf("unable to get interface index for %s: %s", interfaceName, err) - return nil, err - } - ios := &upstreamResolverIOS{ upstreamResolverBase: upstreamResolverBase, lIP: ip, lNet: net, - iIndex: index, + interfaceName: interfaceName, } ios.upstreamClient = ios @@ -53,7 +48,7 @@ func (u *upstreamResolverIOS) exchange(ctx context.Context, upstream string, r * client := &dns.Client{} upstreamHost, _, err := net.SplitHostPort(upstream) if err != nil { - log.Errorf("error while parsing upstream host: %s", err) + return nil, 0, fmt.Errorf("error while parsing upstream host: %s", err) } timeout := upstreamTimeout @@ -65,26 +60,35 @@ func (u *upstreamResolverIOS) exchange(ctx context.Context, upstream string, r * upstreamIP := net.ParseIP(upstreamHost) if u.lNet.Contains(upstreamIP) || net.IP.IsPrivate(upstreamIP) { log.Debugf("using private client to query upstream: %s", upstream) - client = u.getClientPrivate(timeout) + client, err = GetClientPrivate(u.lIP, u.interfaceName, timeout) + if err != nil { + return nil, 0, fmt.Errorf("error while creating private client: %s", err) + } } // Cannot use client.ExchangeContext because it overwrites our Dialer return client.Exchange(r, upstream) } -// getClientPrivate returns a new DNS client bound to the local IP address of the Netbird interface +// GetClientPrivate returns a new DNS client bound to the local IP address of the Netbird interface // This method is needed for iOS -func (u *upstreamResolverIOS) getClientPrivate(dialTimeout time.Duration) *dns.Client { +func GetClientPrivate(ip net.IP, interfaceName string, dialTimeout time.Duration) (*dns.Client, error) { + index, err := getInterfaceIndex(interfaceName) + if err != nil { + log.Debugf("unable to get interface index for %s: %s", interfaceName, err) + return nil, err + } + dialer := &net.Dialer{ LocalAddr: &net.UDPAddr{ - IP: u.lIP, + IP: ip, Port: 0, // Let the OS pick a free port }, Timeout: dialTimeout, Control: func(network, address string, c syscall.RawConn) error { var operr error fn := func(s uintptr) { - operr = unix.SetsockoptInt(int(s), unix.IPPROTO_IP, unix.IP_BOUND_IF, u.iIndex) + operr = unix.SetsockoptInt(int(s), unix.IPPROTO_IP, unix.IP_BOUND_IF, index) } if err := c.Control(fn); err != nil { @@ -101,7 +105,7 @@ func (u *upstreamResolverIOS) getClientPrivate(dialTimeout time.Duration) *dns.C client := &dns.Client{ Dialer: dialer, } - return client + return client, nil } func getInterfaceIndex(interfaceName string) (int, error) { diff --git a/client/internal/routemanager/client.go b/client/internal/routemanager/client.go index 92c71b1e0..1566d10dd 100644 --- a/client/internal/routemanager/client.go +++ b/client/internal/routemanager/client.go @@ -10,6 +10,7 @@ import ( log "github.com/sirupsen/logrus" nberrors "github.com/netbirdio/netbird/client/errors" + nbdns "github.com/netbirdio/netbird/client/internal/dns" "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/routemanager/dynamic" "github.com/netbirdio/netbird/client/internal/routemanager/refcounter" @@ -65,7 +66,7 @@ func newClientNetworkWatcher(ctx context.Context, dnsRouteInterval time.Duration routePeersNotifiers: make(map[string]chan struct{}), routeUpdate: make(chan routesUpdate), peerStateUpdate: make(chan struct{}), - handler: handlerFromRoute(rt, routeRefCounter, allowedIPsRefCounter, dnsRouteInterval, statusRecorder), + handler: handlerFromRoute(rt, routeRefCounter, allowedIPsRefCounter, dnsRouteInterval, statusRecorder, wgInterface), } return client } @@ -383,9 +384,10 @@ func (c *clientNetwork) peersStateAndUpdateWatcher() { } } -func handlerFromRoute(rt *route.Route, routeRefCounter *refcounter.RouteRefCounter, allowedIPsRefCounter *refcounter.AllowedIPsRefCounter, dnsRouterInteval time.Duration, statusRecorder *peer.Status) RouteHandler { +func handlerFromRoute(rt *route.Route, routeRefCounter *refcounter.RouteRefCounter, allowedIPsRefCounter *refcounter.AllowedIPsRefCounter, dnsRouterInteval time.Duration, statusRecorder *peer.Status, wgInterface *iface.WGIface) RouteHandler { if rt.IsDynamic() { - return dynamic.NewRoute(rt, routeRefCounter, allowedIPsRefCounter, dnsRouterInteval, statusRecorder) + dns := nbdns.NewServiceViaMemory(wgInterface) + return dynamic.NewRoute(rt, routeRefCounter, allowedIPsRefCounter, dnsRouterInteval, statusRecorder, wgInterface, fmt.Sprintf("%s:%d", dns.RuntimeIP(), dns.RuntimePort())) } return static.NewRoute(rt, routeRefCounter, allowedIPsRefCounter) } diff --git a/client/internal/routemanager/dynamic/route.go b/client/internal/routemanager/dynamic/route.go index e95710798..3296f3ddf 100644 --- a/client/internal/routemanager/dynamic/route.go +++ b/client/internal/routemanager/dynamic/route.go @@ -16,6 +16,7 @@ import ( "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/routemanager/refcounter" "github.com/netbirdio/netbird/client/internal/routemanager/util" + "github.com/netbirdio/netbird/iface" "github.com/netbirdio/netbird/management/domain" "github.com/netbirdio/netbird/route" ) @@ -47,6 +48,8 @@ type Route struct { currentPeerKey string cancel context.CancelFunc statusRecorder *peer.Status + wgInterface *iface.WGIface + resolverAddr string } func NewRoute( @@ -55,6 +58,8 @@ func NewRoute( allowedIPsRefCounter *refcounter.AllowedIPsRefCounter, interval time.Duration, statusRecorder *peer.Status, + wgInterface *iface.WGIface, + resolverAddr string, ) *Route { return &Route{ route: rt, @@ -63,6 +68,8 @@ func NewRoute( interval: interval, dynamicDomains: domainMap{}, statusRecorder: statusRecorder, + wgInterface: wgInterface, + resolverAddr: resolverAddr, } } @@ -228,11 +235,17 @@ func (r *Route) resolve(results chan resolveResult) { wg.Add(1) go func(domain domain.Domain) { defer wg.Done() - ips, err := net.LookupIP(string(domain)) + + ips, err := r.getIPsFromResolver(domain) if err != nil { - results <- resolveResult{domain: domain, err: fmt.Errorf("resolve d %s: %w", domain.SafeString(), err)} - return + log.Tracef("Failed to resolve domain %s with private resolver: %v", domain.SafeString(), err) + ips, err = net.LookupIP(string(domain)) + if err != nil { + results <- resolveResult{domain: domain, err: fmt.Errorf("resolve d %s: %w", domain.SafeString(), err)} + return + } } + for _, ip := range ips { prefix, err := util.GetPrefixFromIP(ip) if err != nil { diff --git a/client/internal/routemanager/dynamic/route_generic.go b/client/internal/routemanager/dynamic/route_generic.go new file mode 100644 index 000000000..cf3d913a4 --- /dev/null +++ b/client/internal/routemanager/dynamic/route_generic.go @@ -0,0 +1,13 @@ +//go:build !ios + +package dynamic + +import ( + "net" + + "github.com/netbirdio/netbird/management/domain" +) + +func (r *Route) getIPsFromResolver(domain domain.Domain) ([]net.IP, error) { + return net.LookupIP(string(domain)) +} diff --git a/client/internal/routemanager/dynamic/route_ios.go b/client/internal/routemanager/dynamic/route_ios.go new file mode 100644 index 000000000..67138222f --- /dev/null +++ b/client/internal/routemanager/dynamic/route_ios.go @@ -0,0 +1,55 @@ +//go:build ios + +package dynamic + +import ( + "fmt" + "net" + "time" + + "github.com/miekg/dns" + + nbdns "github.com/netbirdio/netbird/client/internal/dns" + + "github.com/netbirdio/netbird/management/domain" +) + +const dialTimeout = 10 * time.Second + +func (r *Route) getIPsFromResolver(domain domain.Domain) ([]net.IP, error) { + privateClient, err := nbdns.GetClientPrivate(r.wgInterface.Address().IP, r.wgInterface.Name(), dialTimeout) + if err != nil { + return nil, fmt.Errorf("error while creating private client: %s", err) + } + + msg := new(dns.Msg) + msg.SetQuestion(dns.Fqdn(string(domain)), dns.TypeA) + + startTime := time.Now() + + response, _, err := privateClient.Exchange(msg, r.resolverAddr) + if err != nil { + return nil, fmt.Errorf("DNS query for %s failed after %s: %s ", domain.SafeString(), time.Since(startTime), err) + } + + if response.Rcode != dns.RcodeSuccess { + return nil, fmt.Errorf("dns response code: %s", dns.RcodeToString[response.Rcode]) + } + + ips := make([]net.IP, 0) + + for _, answ := range response.Answer { + if aRecord, ok := answ.(*dns.A); ok { + ips = append(ips, aRecord.A) + } + if aaaaRecord, ok := answ.(*dns.AAAA); ok { + ips = append(ips, aaaaRecord.AAAA) + } + } + + if len(ips) == 0 { + return nil, fmt.Errorf("no A or AAAA records found for %s", domain.SafeString()) + } + + return ips, nil +} From 0371f529cae15c2429c8d18fb92a5cabaf77109f Mon Sep 17 00:00:00 2001 From: pascal-fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Fri, 2 Aug 2024 18:48:12 +0200 Subject: [PATCH 09/13] Add sonar badge (#2381) --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 98dd18d37..370445412 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,9 @@

+ + + From 059fc7c3a2182756ac8830353841b2e3747e22f8 Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Sat, 3 Aug 2024 20:15:19 +0200 Subject: [PATCH 10/13] Use docker compose command (#2382) replace calls to docker-compose with docker compose --- .github/workflows/test-infrastructure-files.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test-infrastructure-files.yml b/.github/workflows/test-infrastructure-files.yml index abdd18ceb..52b8ee3e2 100644 --- a/.github/workflows/test-infrastructure-files.yml +++ b/.github/workflows/test-infrastructure-files.yml @@ -151,10 +151,10 @@ jobs: - name: run docker compose up working-directory: infrastructure_files/artifacts run: | - docker-compose up -d + docker compose up -d sleep 5 - docker-compose ps - docker-compose logs --tail=20 + docker compose ps + docker compose logs --tail=20 - name: test running containers run: | @@ -207,7 +207,7 @@ jobs: - name: Postgres run cleanup run: | - docker-compose down --volumes --rmi all + docker compose down --volumes --rmi all rm -rf docker-compose.yml Caddyfile zitadel.env dashboard.env machinekey/zitadel-admin-sa.token turnserver.conf management.json zdb.env - name: run script with Zitadel CockroachDB From 6b930271fd87b1dd27f2074736194928a3b22161 Mon Sep 17 00:00:00 2001 From: Evgenii Date: Sun, 4 Aug 2024 21:13:08 +0100 Subject: [PATCH 11/13] change default config location on freebsd (#2388) --- client/cmd/root.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/cmd/root.go b/client/cmd/root.go index 010ffb25a..db02ff5ea 100644 --- a/client/cmd/root.go +++ b/client/cmd/root.go @@ -93,12 +93,15 @@ func init() { oldDefaultConfigPathDir = "/etc/wiretrustee/" oldDefaultLogFileDir = "/var/log/wiretrustee/" - if runtime.GOOS == "windows" { + switch runtime.GOOS { + case "windows": defaultConfigPathDir = os.Getenv("PROGRAMDATA") + "\\Netbird\\" defaultLogFileDir = os.Getenv("PROGRAMDATA") + "\\Netbird\\" oldDefaultConfigPathDir = os.Getenv("PROGRAMDATA") + "\\Wiretrustee\\" oldDefaultLogFileDir = os.Getenv("PROGRAMDATA") + "\\Wiretrustee\\" + case "freebsd": + defaultConfigPathDir = "/var/db/netbird/" } defaultConfigPath = defaultConfigPathDir + "config.json" From d56dfae9b883c57f150201bfaf913525f785a3fe Mon Sep 17 00:00:00 2001 From: Evgenii Date: Sun, 4 Aug 2024 21:31:43 +0100 Subject: [PATCH 12/13] Offer only Device Code Flow on FreeBSD (#2389) --- client/internal/auth/oauth.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/internal/auth/oauth.go b/client/internal/auth/oauth.go index 7467584a3..c9f10ca86 100644 --- a/client/internal/auth/oauth.go +++ b/client/internal/auth/oauth.go @@ -69,6 +69,11 @@ func NewOAuthFlow(ctx context.Context, config *internal.Config, isLinuxDesktopCl return authenticateWithDeviceCodeFlow(ctx, config) } + // On FreeBSD we currently do not support desktop environments and offer only Device Code Flow (#2384) + if runtime.GOOS == "freebsd" { + return authenticateWithDeviceCodeFlow(ctx, config) + } + pkceFlow, err := authenticateWithPKCEFlow(ctx, config) if err != nil { // fallback to device code flow From 1802e51213df139f942527522e5cea828a99217e Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Mon, 5 Aug 2024 11:03:14 +0200 Subject: [PATCH 13/13] Fix windows binary version (#2390) --- .github/workflows/release.yml | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 89eb2ded9..30f24e92e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -79,15 +79,8 @@ jobs: - name: Install goversioninfo run: go install github.com/josephspurrier/goversioninfo/cmd/goversioninfo@233067e - - name: Generate windows syso 386 - run: goversioninfo -icon client/ui/netbird.ico -manifest client/manifest.xml -product-name ${{ env.PRODUCT_NAME }} -copyright "${{ env.COPYRIGHT }}" -file-version ${{ steps.semver_parser.outputs.fullversion }}.0 -product-version ${{ steps.semver_parser.outputs.fullversion }}.0 -o client/resources_windows_386.syso - - name: Generate windows syso arm - run: goversioninfo -icon client/ui/netbird.ico -manifest client/manifest.xml -product-name ${{ env.PRODUCT_NAME }} -copyright "${{ env.COPYRIGHT }}" -file-version ${{ steps.semver_parser.outputs.fullversion }}.0 -product-version ${{ steps.semver_parser.outputs.fullversion }}.0 -o client/resources_windows_arm.syso - - name: Generate windows syso arm64 - run: goversioninfo -icon client/ui/netbird.ico -manifest client/manifest.xml -product-name ${{ env.PRODUCT_NAME }} -copyright "${{ env.COPYRIGHT }}" -file-version ${{ steps.semver_parser.outputs.fullversion }}.0 -product-version ${{ steps.semver_parser.outputs.fullversion }}.0 -o client/resources_windows_arm64.syso - name: Generate windows syso amd64 - run: goversioninfo -icon client/ui/netbird.ico -manifest client/manifest.xml -product-name ${{ env.PRODUCT_NAME }} -copyright "${{ env.COPYRIGHT }}" -file-version ${{ steps.semver_parser.outputs.fullversion }}.0 -product-version ${{ steps.semver_parser.outputs.fullversion }}.0 -o client/resources_windows_amd64.syso - + run: goversioninfo -icon client/ui/netbird.ico -manifest client/manifest.xml -product-name ${{ env.PRODUCT_NAME }} -copyright "${{ env.COPYRIGHT }}" -ver-major ${{ steps.semver_parser.outputs.major }} -ver-minor ${{ steps.semver_parser.outputs.minor }} -ver-patch ${{ steps.semver_parser.outputs.patch }} -ver-build 0 -file-version ${{ steps.semver_parser.outputs.fullversion }}.0 -product-version ${{ steps.semver_parser.outputs.fullversion }}.0 -o client/resources_windows_amd64.syso - name: Run GoReleaser uses: goreleaser/goreleaser-action@v4 with: @@ -170,7 +163,7 @@ jobs: - name: Install goversioninfo run: go install github.com/josephspurrier/goversioninfo/cmd/goversioninfo@233067e - name: Generate windows syso amd64 - run: goversioninfo -64 -icon client/ui/netbird.ico -manifest client/ui/manifest.xml -product-name ${{ env.PRODUCT_NAME }}-"UI" -copyright "${{ env.COPYRIGHT }}" -file-version ${{ steps.semver_parser.outputs.fullversion }}.0 -product-version ${{ steps.semver_parser.outputs.fullversion }}.0 -o client/ui/resources_windows_amd64.syso + run: goversioninfo -64 -icon client/ui/netbird.ico -manifest client/ui/manifest.xml -product-name ${{ env.PRODUCT_NAME }}-"UI" -copyright "${{ env.COPYRIGHT }}" -ver-major ${{ steps.semver_parser.outputs.major }} -ver-minor ${{ steps.semver_parser.outputs.minor }} -ver-patch ${{ steps.semver_parser.outputs.patch }} -ver-build 0 -file-version ${{ steps.semver_parser.outputs.fullversion }}.0 -product-version ${{ steps.semver_parser.outputs.fullversion }}.0 -o client/ui/resources_windows_amd64.syso - name: Run GoReleaser uses: goreleaser/goreleaser-action@v4