From 9dac3e982ebd903c4e04722913751ad08d3a2636 Mon Sep 17 00:00:00 2001 From: invario <67800603+invario@users.noreply.github.com> Date: Thu, 19 Mar 2026 07:26:26 -0400 Subject: [PATCH] Add ability for device/network scan when rootless (#1700) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add ability for device/network scan when rootless Signed-off-by: invario <67800603+invario@users.noreply.github.com> * adjust err msg --------- Signed-off-by: invario <67800603+invario@users.noreply.github.com> Co-authored-by: Maxi Quoß --- backend/go.sum | 4 + backend/pb/handlers.go | 96 ---------------------- backend/pb/handlerscan_linux.go | 137 ++++++++++++++++++++++++++++++++ backend/pb/handlerscan_other.go | 107 +++++++++++++++++++++++++ 4 files changed, 248 insertions(+), 96 deletions(-) create mode 100644 backend/pb/handlerscan_linux.go create mode 100644 backend/pb/handlerscan_other.go diff --git a/backend/go.sum b/backend/go.sum index b4d98663..4bd45c3d 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -132,6 +132,10 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +kernel.org/pub/linux/libs/security/libcap/cap v1.2.77 h1:iQtQTjFUOcTT19fI8sTCzYXsjeVs56et3D8AbKS2Uks= +kernel.org/pub/linux/libs/security/libcap/cap v1.2.77/go.mod h1:oV+IO8kGh0B7TxErbydDe2+BRmi9g/W0CkpVV+QBTJU= +kernel.org/pub/linux/libs/security/libcap/psx v1.2.77 h1:Z06sMOzc0GNCwp6efaVrIrz4ywGJ1v+DP0pjVkOfDuA= +kernel.org/pub/linux/libs/security/libcap/psx v1.2.77/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24= modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw= diff --git a/backend/pb/handlers.go b/backend/pb/handlers.go index 12c362c3..cfdc659b 100644 --- a/backend/pb/handlers.go +++ b/backend/pb/handlers.go @@ -2,14 +2,9 @@ package pb import ( "encoding/json" - "encoding/xml" "fmt" - "net" "net/http" - "os" - "os/exec" "strconv" - "strings" "time" "github.com/pocketbase/dbx" @@ -216,97 +211,6 @@ type Nmaprun struct { } `xml:"host"` } -func HandlerScan(e *core.RequestEvent) error { - // check if nmap installed - nmap, err := exec.LookPath("nmap") - if err != nil { - return apis.NewBadRequestError(err.Error(), nil) - } - - // check if scan range is valid - allPrivateSettings, err := e.App.FindAllRecords("settings_private") - if err != nil { - return err - } - settingsPrivate := allPrivateSettings[0] - scanRange := settingsPrivate.GetString("scan_range") - _, ipNet, err := net.ParseCIDR(scanRange) - if err != nil { - return apis.NewBadRequestError(err.Error(), nil) - } - - // run nmap - timeout := os.Getenv("UPSNAP_SCAN_TIMEOUT") - if timeout == "" { - timeout = "500ms" - } - cmd := exec.Command(nmap, "-sn", "-oX", "-", scanRange, "--host-timeout", timeout) - cmdOutput, err := cmd.Output() - if err != nil { - return err - } - - // unmarshal xml - nmapOutput := Nmaprun{} - if err := xml.Unmarshal(cmdOutput, &nmapOutput); err != nil { - return err - } - - type Device struct { - Name string `json:"name"` - IP string `json:"ip"` - MAC string `json:"mac"` - MACVendor string `json:"mac_vendor"` - } - - // extract info from struct into data - type Response struct { - Netmask string `json:"netmask"` - Devices []Device `json:"devices"` - } - res := Response{} - var nm []string - for _, octet := range ipNet.Mask { - nm = append(nm, strconv.Itoa(int(octet))) - } - res.Netmask = strings.Join(nm, ".") - - for _, host := range nmapOutput.Host { - dev := Device{} - for _, addr := range host.Address { - if addr.Addrtype == "ipv4" { - dev.IP = addr.Addr - } else if addr.Addrtype == "mac" { - dev.MAC = addr.Addr - } - if addr.Vendor != "" { - dev.MACVendor = addr.Vendor - } else { - dev.MACVendor = "Unknown" - } - } - - if dev.IP == "" || dev.MAC == "" { - continue - } - - names, err := net.LookupAddr(dev.IP) - if err != nil || len(names) == 0 { - dev.Name = dev.MACVendor - } else { - dev.Name = strings.TrimSuffix(names[0], ".") - } - - if dev.Name == "" && dev.MACVendor == "" { - continue - } - - res.Devices = append(res.Devices, dev) - } - - return e.JSON(http.StatusOK, res) -} - func HandlerInitSuperuser(e *core.RequestEvent) error { superusersCollection, err := e.App.FindCollectionByNameOrId(core.CollectionNameSuperusers) if err != nil { diff --git a/backend/pb/handlerscan_linux.go b/backend/pb/handlerscan_linux.go new file mode 100644 index 00000000..1afdecc6 --- /dev/null +++ b/backend/pb/handlerscan_linux.go @@ -0,0 +1,137 @@ +//go:build linux + +package pb + +import ( + "encoding/xml" + "fmt" + "net" + "net/http" + "os" + "os/exec" + "strconv" + "strings" + + "github.com/pocketbase/pocketbase/apis" + "github.com/pocketbase/pocketbase/core" + "kernel.org/pub/linux/libs/security/libcap/cap" +) + +func HandlerScan(e *core.RequestEvent) error { + // check if nmap installed + nmap, err := exec.LookPath("nmap") + if err != nil { + return apis.NewBadRequestError(err.Error(), nil) + } + + // check if scan range is valid + allPrivateSettings, err := e.App.FindAllRecords("settings_private") + if err != nil { + return err + } + settingsPrivate := allPrivateSettings[0] + scanRange := settingsPrivate.GetString("scan_range") + _, ipNet, err := net.ParseCIDR(scanRange) + if err != nil { + return apis.NewBadRequestError(err.Error(), nil) + } + + // run nmap + timeout := os.Getenv("UPSNAP_SCAN_TIMEOUT") + if timeout == "" { + timeout = "500ms" + } + orig := cap.GetProc() + defer orig.SetProc() // restore original caps on exit. + + c, err := orig.Dup() + if err != nil { + return fmt.Errorf("Failed to dup existing capabilities: %v", err) + } + + if on, _ := c.GetFlag(cap.Permitted, cap.NET_RAW); !on { + return fmt.Errorf("unable to get NET_RAW permissions") + } + + if err := c.SetFlag(cap.Effective, true, cap.NET_RAW); err != nil { + return fmt.Errorf("unable to set NET_RAW capability effective") + } + + if err := c.SetFlag(cap.Inheritable, true, cap.NET_RAW); err != nil { + return fmt.Errorf("unable to set NET_RAW capability inheritable") + } + + if err := c.SetProc(); err != nil { + return fmt.Errorf("unable to raise NET_RAW capability") + } + + if err := cap.SetAmbient(true, cap.NET_RAW); err != nil { + return fmt.Errorf("unable to set NET_RAW capability ambient") + } + + cmd := exec.Command(nmap, "-sn", "-oX", "-", scanRange, "--host-timeout", timeout, "--privileged") + cmdOutput, err := cmd.Output() + if err != nil { + return err + } + + // unmarshal xml + nmapOutput := Nmaprun{} + if err := xml.Unmarshal(cmdOutput, &nmapOutput); err != nil { + return err + } + + type Device struct { + Name string `json:"name"` + IP string `json:"ip"` + MAC string `json:"mac"` + MACVendor string `json:"mac_vendor"` + } + + // extract info from struct into data + type Response struct { + Netmask string `json:"netmask"` + Devices []Device `json:"devices"` + } + res := Response{} + var nm []string + for _, octet := range ipNet.Mask { + nm = append(nm, strconv.Itoa(int(octet))) + } + res.Netmask = strings.Join(nm, ".") + + for _, host := range nmapOutput.Host { + dev := Device{} + for _, addr := range host.Address { + if addr.Addrtype == "ipv4" { + dev.IP = addr.Addr + } else if addr.Addrtype == "mac" { + dev.MAC = addr.Addr + } + if addr.Vendor != "" { + dev.MACVendor = addr.Vendor + } else { + dev.MACVendor = "Unknown" + } + } + + if dev.IP == "" || dev.MAC == "" { + continue + } + + names, err := net.LookupAddr(dev.IP) + if err != nil || len(names) == 0 { + dev.Name = dev.MACVendor + } else { + dev.Name = strings.TrimSuffix(names[0], ".") + } + + if dev.Name == "" && dev.MACVendor == "" { + continue + } + + res.Devices = append(res.Devices, dev) + } + + return e.JSON(http.StatusOK, res) +} diff --git a/backend/pb/handlerscan_other.go b/backend/pb/handlerscan_other.go new file mode 100644 index 00000000..35616c46 --- /dev/null +++ b/backend/pb/handlerscan_other.go @@ -0,0 +1,107 @@ +//go:build !linux + +package pb + +import ( + "encoding/xml" + "net" + "net/http" + "os" + "os/exec" + "strconv" + "strings" + + "github.com/pocketbase/pocketbase/apis" + "github.com/pocketbase/pocketbase/core" +) + +func HandlerScan(e *core.RequestEvent) error { + // check if nmap installed + nmap, err := exec.LookPath("nmap") + if err != nil { + return apis.NewBadRequestError(err.Error(), nil) + } + + // check if scan range is valid + allPrivateSettings, err := e.App.FindAllRecords("settings_private") + if err != nil { + return err + } + settingsPrivate := allPrivateSettings[0] + scanRange := settingsPrivate.GetString("scan_range") + _, ipNet, err := net.ParseCIDR(scanRange) + if err != nil { + return apis.NewBadRequestError(err.Error(), nil) + } + + // run nmap + timeout := os.Getenv("UPSNAP_SCAN_TIMEOUT") + if timeout == "" { + timeout = "500ms" + } + cmd := exec.Command(nmap, "-sn", "-oX", "-", scanRange, "--host-timeout", timeout, "--privileged") + cmdOutput, err := cmd.Output() + if err != nil { + return err + } + + // unmarshal xml + nmapOutput := Nmaprun{} + if err := xml.Unmarshal(cmdOutput, &nmapOutput); err != nil { + return err + } + + type Device struct { + Name string `json:"name"` + IP string `json:"ip"` + MAC string `json:"mac"` + MACVendor string `json:"mac_vendor"` + } + + // extract info from struct into data + type Response struct { + Netmask string `json:"netmask"` + Devices []Device `json:"devices"` + } + res := Response{} + var nm []string + for _, octet := range ipNet.Mask { + nm = append(nm, strconv.Itoa(int(octet))) + } + res.Netmask = strings.Join(nm, ".") + + for _, host := range nmapOutput.Host { + dev := Device{} + for _, addr := range host.Address { + if addr.Addrtype == "ipv4" { + dev.IP = addr.Addr + } else if addr.Addrtype == "mac" { + dev.MAC = addr.Addr + } + if addr.Vendor != "" { + dev.MACVendor = addr.Vendor + } else { + dev.MACVendor = "Unknown" + } + } + + if dev.IP == "" || dev.MAC == "" { + continue + } + + names, err := net.LookupAddr(dev.IP) + if err != nil || len(names) == 0 { + dev.Name = dev.MACVendor + } else { + dev.Name = strings.TrimSuffix(names[0], ".") + } + + if dev.Name == "" && dev.MACVendor == "" { + continue + } + + res.Devices = append(res.Devices, dev) + } + + return e.JSON(http.StatusOK, res) +}