diff --git a/client/netbird b/client/netbird new file mode 100755 index 00000000000..85ed176ac89 Binary files /dev/null and b/client/netbird differ diff --git a/client/netbird.exe b/client/netbird.exe new file mode 100755 index 00000000000..b3f347fdea5 Binary files /dev/null and b/client/netbird.exe differ diff --git a/client/system/detect_cloud/alibabacloud.go b/client/system/detect_cloud/alibabacloud.go new file mode 100644 index 00000000000..86cfc3af614 --- /dev/null +++ b/client/system/detect_cloud/alibabacloud.go @@ -0,0 +1,24 @@ +package detect_cloud + +import ( + "context" + "net/http" +) + +func detectAlibabaCloud(ctx context.Context) string { + req, err := http.NewRequestWithContext(ctx, "GET", "http://100.100.100.200/latest/", nil) + if err != nil { + return "" + } + + resp, err := hc.Do(req) + if err != nil { + return "" + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusOK { + return "Alibaba Cloud" + } + return "" +} diff --git a/client/system/detect_cloud/aws.go b/client/system/detect_cloud/aws.go new file mode 100644 index 00000000000..62200652870 --- /dev/null +++ b/client/system/detect_cloud/aws.go @@ -0,0 +1,57 @@ +package detect_cloud + +import ( + "context" + "net/http" +) + +func detectAWS(ctx context.Context) string { + v1ResultChan := make(chan bool, 1) + v2ResultChan := make(chan bool, 1) + + go func() { + v1ResultChan <- detectAWSIDMSv1(ctx) + }() + + go func() { + v2ResultChan <- detectAWSIDMSv2(ctx) + }() + + v1Result, v2Result := <-v1ResultChan, <-v2ResultChan + + if v1Result || v2Result { + return "Amazon Web Services" + } + return "" +} + +func detectAWSIDMSv1(ctx context.Context) bool { + req, err := http.NewRequestWithContext(ctx, "GET", "http://169.254.169.254/latest/", nil) + if err != nil { + return false + } + + resp, err := hc.Do(req) + if err != nil { + return false + } + defer resp.Body.Close() + + return resp.StatusCode == http.StatusOK +} + +func detectAWSIDMSv2(ctx context.Context) bool { + req, err := http.NewRequestWithContext(ctx, "PUT", "http://169.254.169.254/latest/api/token", nil) + if err != nil { + return false + } + req.Header.Set("X-aws-ec2-metadata-token-ttl-seconds", "21600") + + resp, err := hc.Do(req) + if err != nil { + return false + } + defer resp.Body.Close() + + return resp.StatusCode == http.StatusOK +} diff --git a/client/system/detect_cloud/azure.go b/client/system/detect_cloud/azure.go new file mode 100644 index 00000000000..b354d659be5 --- /dev/null +++ b/client/system/detect_cloud/azure.go @@ -0,0 +1,25 @@ +package detect_cloud + +import ( + "context" + "net/http" +) + +func detectAzure(ctx context.Context) string { + req, err := http.NewRequestWithContext(ctx, "GET", "http://169.254.169.254/metadata/instance?api-version=2021-02-01", nil) + if err != nil { + return "" + } + req.Header.Set("Metadata", "true") + + resp, err := hc.Do(req) + if err != nil { + return "" + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusOK { + return "Microsoft Azure" + } + return "" +} diff --git a/client/system/detect_cloud/detect.go b/client/system/detect_cloud/detect.go new file mode 100644 index 00000000000..3bbff434580 --- /dev/null +++ b/client/system/detect_cloud/detect.go @@ -0,0 +1,65 @@ +package detect_cloud + +import ( + "context" + "net/http" + "sync" + "time" +) + +/* + This packages is inspired by the work of the original author (https://github.com/perlogix), but it has been modified to fit the needs of the project. + Original project: https://github.com/perlogix/libdetectcloud +*/ + +var hc = &http.Client{Timeout: 300 * time.Millisecond} + +func Detect(ctx context.Context) string { + subCtx, cancel := context.WithCancel(context.Background()) + defer cancel() + + funcs := []func(context.Context) string{ + detectAlibabaCloud, + detectAWS, + detectAzure, + detectDigitalOcean, + detectGCP, + detectOracle, + detectIBMCloud, + detectSoftlayer, + detectVultr, + } + + results := make(chan string, len(funcs)) + + var wg sync.WaitGroup + + for _, fn := range funcs { + wg.Add(1) + go func(f func(context.Context) string) { + defer wg.Done() + select { + case <-subCtx.Done(): + return + default: + if result := f(ctx); result != "" { + results <- result + cancel() + } + } + }(fn) + } + + go func() { + wg.Wait() + close(results) + }() + + for result := range results { + if result != "" { + return result + } + } + + return "" +} diff --git a/client/system/detect_cloud/digitalocean.go b/client/system/detect_cloud/digitalocean.go new file mode 100644 index 00000000000..d64f56c87a3 --- /dev/null +++ b/client/system/detect_cloud/digitalocean.go @@ -0,0 +1,24 @@ +package detect_cloud + +import ( + "context" + "net/http" +) + +func detectDigitalOcean(ctx context.Context) string { + req, err := http.NewRequestWithContext(ctx, "GET", "http://169.254.169.254/metadata/v1/", nil) + if err != nil { + return "" + } + + resp, err := hc.Do(req) + if err != nil { + return "" + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusOK { + return "Digital Ocean" + } + return "" +} diff --git a/client/system/detect_cloud/gcp.go b/client/system/detect_cloud/gcp.go new file mode 100644 index 00000000000..c673f893739 --- /dev/null +++ b/client/system/detect_cloud/gcp.go @@ -0,0 +1,25 @@ +package detect_cloud + +import ( + "context" + "net/http" +) + +func detectGCP(ctx context.Context) string { + req, err := http.NewRequestWithContext(ctx, "GET", "http://metadata.google.internal", nil) + if err != nil { + return "" + } + req.Header.Add("Metadata-Flavor", "Google") + + resp, err := hc.Do(req) + if err != nil { + return "" + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusOK { + return "Google Cloud Platform" + } + return "" +} diff --git a/client/system/detect_cloud/ibmcloud.go b/client/system/detect_cloud/ibmcloud.go new file mode 100644 index 00000000000..07de6a2ee11 --- /dev/null +++ b/client/system/detect_cloud/ibmcloud.go @@ -0,0 +1,54 @@ +package detect_cloud + +import ( + "context" + "net/http" +) + +func detectIBMCloud(ctx context.Context) string { + v1ResultChan := make(chan bool, 1) + v2ResultChan := make(chan bool, 1) + + go func() { + v1ResultChan <- detectIBMSecure(ctx) + }() + + go func() { + v2ResultChan <- detectIBM(ctx) + }() + + v1Result, v2Result := <-v1ResultChan, <-v2ResultChan + + if v1Result || v2Result { + return "IBM Cloud" + } + return "" +} + +func detectIBMSecure(ctx context.Context) bool { + req, err := http.NewRequestWithContext(ctx, "PUT", "https://api.metadata.cloud.ibm.com/instance_identity/v1/token", nil) + if err != nil { + return false + } + + resp, err := hc.Do(req) + if err != nil { + return false + } + defer resp.Body.Close() + return resp.StatusCode == http.StatusOK +} + +func detectIBM(ctx context.Context) bool { + req, err := http.NewRequestWithContext(ctx, "PUT", "http://api.metadata.cloud.ibm.com/instance_identity/v1/token", nil) + if err != nil { + return false + } + + resp, err := hc.Do(req) + if err != nil { + return false + } + defer resp.Body.Close() + return resp.StatusCode == http.StatusOK +} diff --git a/client/system/detect_cloud/oracle.go b/client/system/detect_cloud/oracle.go new file mode 100644 index 00000000000..d0117c2a016 --- /dev/null +++ b/client/system/detect_cloud/oracle.go @@ -0,0 +1,56 @@ +package detect_cloud + +import ( + "context" + "net/http" +) + +func detectOracle(ctx context.Context) string { + v1ResultChan := make(chan bool, 1) + v2ResultChan := make(chan bool, 1) + + go func() { + v1ResultChan <- detectOracleIDMSv1(ctx) + }() + + go func() { + v2ResultChan <- detectOracleIDMSv2(ctx) + }() + + v1Result, v2Result := <-v1ResultChan, <-v2ResultChan + + if v1Result || v2Result { + return "Oracle" + } + return "" +} + +func detectOracleIDMSv1(ctx context.Context) bool { + req, err := http.NewRequestWithContext(ctx, "GET", "http://169.254.169.254/opc/v1/instance/", nil) + if err != nil { + return false + } + req.Header.Add("Authorization", "Bearer Oracle") + + resp, err := hc.Do(req) + if err != nil { + return false + } + defer resp.Body.Close() + return resp.StatusCode == http.StatusOK +} + +func detectOracleIDMSv2(ctx context.Context) bool { + req, err := http.NewRequestWithContext(ctx, "GET", "http://169.254.169.254/opc/v2/instance/", nil) + if err != nil { + return false + } + req.Header.Add("Authorization", "Bearer Oracle") + + resp, err := hc.Do(req) + if err != nil { + return false + } + defer resp.Body.Close() + return resp.StatusCode == http.StatusOK +} diff --git a/client/system/detect_cloud/softlayer.go b/client/system/detect_cloud/softlayer.go new file mode 100644 index 00000000000..a09b522c454 --- /dev/null +++ b/client/system/detect_cloud/softlayer.go @@ -0,0 +1,25 @@ +package detect_cloud + +import ( + "context" + "net/http" +) + +func detectSoftlayer(ctx context.Context) string { + req, err := http.NewRequestWithContext(ctx, "GET", "https://api.service.softlayer.com/rest/v3/SoftLayer_Resource_Metadata/UserMetadata.txt", nil) + if err != nil { + return "" + } + + resp, err := hc.Do(req) + if err != nil { + return "" + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusOK { + // Since SoftLayer was acquired by IBM, we should return "IBM Cloud" + return "IBM Cloud" + } + return "" +} diff --git a/client/system/detect_cloud/vultr.go b/client/system/detect_cloud/vultr.go new file mode 100644 index 00000000000..8f12f5cce56 --- /dev/null +++ b/client/system/detect_cloud/vultr.go @@ -0,0 +1,24 @@ +package detect_cloud + +import ( + "context" + "net/http" +) + +func detectVultr(ctx context.Context) string { + req, err := http.NewRequestWithContext(ctx, "GET", "http://169.254.169.254/v1.json", nil) + if err != nil { + return "" + } + + resp, err := hc.Do(req) + if err != nil { + return "" + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusOK { + return "Vultr" + } + return "" +} diff --git a/client/system/detect_platform/detect.go b/client/system/detect_platform/detect.go new file mode 100644 index 00000000000..2eb72f3f4b2 --- /dev/null +++ b/client/system/detect_platform/detect.go @@ -0,0 +1,53 @@ +package detect_platform + +import ( + "context" + "net/http" + "sync" + "time" +) + +var hc = &http.Client{Timeout: 300 * time.Millisecond} + +func Detect(ctx context.Context) string { + subCtx, cancel := context.WithCancel(context.Background()) + defer cancel() + + funcs := []func(context.Context) string{ + detectOpenStack, + detectContainer, + } + + results := make(chan string, len(funcs)) + + var wg sync.WaitGroup + + for _, fn := range funcs { + wg.Add(1) + go func(f func(context.Context) string) { + defer wg.Done() + select { + case <-subCtx.Done(): + return + default: + if result := f(ctx); result != "" { + results <- result + cancel() + } + } + }(fn) + } + + go func() { + wg.Wait() + close(results) + }() + + for result := range results { + if result != "" { + return result + } + } + + return "" +} diff --git a/client/system/detect_platform/kube.go b/client/system/detect_platform/kube.go new file mode 100644 index 00000000000..1c0b89843e3 --- /dev/null +++ b/client/system/detect_platform/kube.go @@ -0,0 +1,17 @@ +package detect_platform + +import ( + "context" + "os" +) + +func detectContainer(ctx context.Context) string { + if _, exists := os.LookupEnv("KUBERNETES_SERVICE_HOST"); exists { + return "Kubernetes" + } + + if _, err := os.Stat("/.dockerenv"); err == nil { + return "Docker" + } + return "" +} diff --git a/client/system/detect_platform/openstack.go b/client/system/detect_platform/openstack.go new file mode 100644 index 00000000000..82650149913 --- /dev/null +++ b/client/system/detect_platform/openstack.go @@ -0,0 +1,24 @@ +package detect_platform + +import ( + "context" + "net/http" +) + +func detectOpenStack(ctx context.Context) string { + req, err := http.NewRequestWithContext(ctx, "GET", "http://169.254.169.254/openstack", nil) + if err != nil { + return "" + } + + resp, err := hc.Do(req) + if err != nil { + return "" + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusOK { + return "OpenStack" + } + return "" +} diff --git a/client/system/info.go b/client/system/info.go index 9dba2205fff..097c6706e43 100644 --- a/client/system/info.go +++ b/client/system/info.go @@ -25,6 +25,11 @@ type NetworkAddress struct { Mac string } +type Environment struct { + Cloud string + Platform string +} + // Info is an object that contains machine information // Most of the code is taken from https://github.com/matishsiao/goInfo type Info struct { @@ -42,6 +47,7 @@ type Info struct { SystemSerialNumber string SystemProductName string SystemManufacturer string + Environment Environment } // extractUserAgent extracts Netbird's agent (client) name and version from the outgoing context diff --git a/client/system/info_darwin.go b/client/system/info_darwin.go index 6c0f803e7bf..586476df6c8 100644 --- a/client/system/info_darwin.go +++ b/client/system/info_darwin.go @@ -15,6 +15,8 @@ import ( log "github.com/sirupsen/logrus" + "github.com/netbirdio/netbird/client/system/detect_cloud" + "github.com/netbirdio/netbird/client/system/detect_platform" "github.com/netbirdio/netbird/version" ) @@ -41,6 +43,11 @@ func GetInfo(ctx context.Context) *Info { serialNum, prodName, manufacturer := sysInfo() + env := Environment{ + Cloud: detect_cloud.Detect(ctx), + Platform: detect_platform.Detect(ctx), + } + gio := &Info{ Kernel: sysName, OSVersion: strings.TrimSpace(string(swVersion)), @@ -53,6 +60,7 @@ func GetInfo(ctx context.Context) *Info { SystemSerialNumber: serialNum, SystemProductName: prodName, SystemManufacturer: manufacturer, + Environment: env, } systemHostname, _ := os.Hostname() gio.Hostname = extractDeviceName(ctx, systemHostname) diff --git a/client/system/info_freebsd.go b/client/system/info_freebsd.go index 74b132f4a98..e2199544bec 100644 --- a/client/system/info_freebsd.go +++ b/client/system/info_freebsd.go @@ -10,6 +10,8 @@ import ( "strings" "time" + "github.com/netbirdio/netbird/client/system/detect_cloud" + "github.com/netbirdio/netbird/client/system/detect_platform" "github.com/netbirdio/netbird/version" ) @@ -23,7 +25,13 @@ func GetInfo(ctx context.Context) *Info { osStr := strings.Replace(out, "\n", "", -1) osStr = strings.Replace(osStr, "\r\n", "", -1) osInfo := strings.Split(osStr, " ") - gio := &Info{Kernel: osInfo[0], Platform: runtime.GOARCH, OS: osInfo[2], GoOS: runtime.GOOS, CPUs: runtime.NumCPU(), KernelVersion: osInfo[1]} + + env := Environment{ + Cloud: detect_cloud.Detect(ctx), + Platform: detect_platform.Detect(ctx), + } + + gio := &Info{Kernel: osInfo[0], Platform: runtime.GOARCH, OS: osInfo[2], GoOS: runtime.GOOS, CPUs: runtime.NumCPU(), KernelVersion: osInfo[1], Environment: env} systemHostname, _ := os.Hostname() gio.Hostname = extractDeviceName(ctx, systemHostname) gio.WiretrusteeVersion = version.NetbirdVersion() diff --git a/client/system/info_linux.go b/client/system/info_linux.go index 22ee780d0e1..ca3be9d1c5b 100644 --- a/client/system/info_linux.go +++ b/client/system/info_linux.go @@ -15,6 +15,8 @@ import ( log "github.com/sirupsen/logrus" "github.com/zcalusic/sysinfo" + "github.com/netbirdio/netbird/client/system/detect_cloud" + "github.com/netbirdio/netbird/client/system/detect_platform" "github.com/netbirdio/netbird/version" ) @@ -61,6 +63,11 @@ func GetInfo(ctx context.Context) *Info { serialNum, prodName, manufacturer := sysInfo() + env := Environment{ + Cloud: detect_cloud.Detect(ctx), + Platform: detect_platform.Detect(ctx), + } + gio := &Info{ Kernel: osInfo[0], Platform: osInfo[2], @@ -76,6 +83,7 @@ func GetInfo(ctx context.Context) *Info { SystemSerialNumber: serialNum, SystemProductName: prodName, SystemManufacturer: manufacturer, + Environment: env, } return gio diff --git a/client/system/info_windows.go b/client/system/info_windows.go index c185010465b..bcdfb7b2455 100644 --- a/client/system/info_windows.go +++ b/client/system/info_windows.go @@ -11,6 +11,8 @@ import ( "github.com/yusufpapurcu/wmi" "golang.org/x/sys/windows/registry" + "github.com/netbirdio/netbird/client/system/detect_cloud" + "github.com/netbirdio/netbird/client/system/detect_platform" "github.com/netbirdio/netbird/version" ) @@ -55,6 +57,11 @@ func GetInfo(ctx context.Context) *Info { log.Warnf("failed to get system manufacturer: %s", err) } + env := Environment{ + Cloud: detect_cloud.Detect(ctx), + Platform: detect_platform.Detect(ctx), + } + gio := &Info{ Kernel: "windows", OSVersion: osVersion, @@ -67,6 +74,7 @@ func GetInfo(ctx context.Context) *Info { SystemSerialNumber: serialNum, SystemProductName: prodName, SystemManufacturer: manufacturer, + Environment: env, } systemHostname, _ := os.Hostname() gio.Hostname = extractDeviceName(ctx, systemHostname)