diff --git a/Dockerfile b/Dockerfile index c7c89bcb..73c8acff 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,7 @@ RUN wget https://github.com/seriousm4x/UpSnap/releases/download/${VERSION}/UpSna chmod +x upsnap &&\ apk update &&\ apk add --no-cache libcap &&\ - setcap 'cap_net_raw=+ep' ./upsnap + setcap 'cap_net_raw=+p' ./upsnap FROM alpine:3 ARG UPSNAP_HTTP_LISTEN=0.0.0.0:8090 @@ -23,4 +23,5 @@ WORKDIR /app COPY --from=downloader /app/upsnap upsnap HEALTHCHECK --interval=10s \ CMD curl -fs "http://${UPSNAP_HTTP_LISTEN}/api/health" || exit 1 -ENTRYPOINT ["sh", "-c", "./upsnap serve --http ${UPSNAP_HTTP_LISTEN}"] +CMD ["serve","--http","${UPSNAP_HTTP_LISTEN}"] +ENTRYPOINT ["/app/upsnap"] \ No newline at end of file diff --git a/README.md b/README.md index 81708661..a27b19c5 100644 --- a/README.md +++ b/README.md @@ -61,10 +61,10 @@ Just download the latest binary from the [release page](https://github.com/serio sudo ./upsnap serve --http=0.0.0.0:8090 ``` -### Non-root: +### Non-root on linux only: ```bash -sudo setcap cap_net_raw=+ep ./upsnap # only once after downloading +sudo setcap cap_net_raw=+p ./upsnap # only once after downloading to allow for pinging devices ./upsnap serve --http=0.0.0.0:8090 ``` diff --git a/backend/networking/ping.go b/backend/networking/ping.go index 1584ad66..c0b743d3 100644 --- a/backend/networking/ping.go +++ b/backend/networking/ping.go @@ -4,14 +4,8 @@ import ( "errors" "net" "os" - "os/exec" - "runtime" - "strconv" "syscall" "time" - - "github.com/pocketbase/pocketbase/core" - probing "github.com/prometheus-community/pro-bing" ) func isNoRouteOrDownError(err error) bool { @@ -26,53 +20,6 @@ func isNoRouteOrDownError(err error) bool { return syscallErr.Err == syscall.EHOSTUNREACH || syscallErr.Err == syscall.EHOSTDOWN } -func PingDevice(device *core.Record) (bool, error) { - ping_cmd := device.GetString("ping_cmd") - if ping_cmd == "" { - pinger, err := probing.NewPinger(device.GetString("ip")) - if err != nil { - return false, err - } - pinger.Count = 1 - pinger.Timeout = 500 * time.Millisecond - - privileged := isRoot() - privilegedEnv := os.Getenv("UPSNAP_PING_PRIVILEGED") - if privilegedEnv != "" { - privileged, err = strconv.ParseBool(privilegedEnv) - if err != nil { - privileged = false - } - } - pinger.SetPrivileged(privileged) - - err = pinger.Run() - if err != nil { - if isNoRouteOrDownError(err) { - return false, nil - } - return false, err - } - stats := pinger.Statistics() - return stats.PacketLoss == 0, nil - } else { - var shell string - var shell_arg string - if runtime.GOOS == "windows" { - shell = "cmd" - shell_arg = "/C" - } else { - shell = "/bin/sh" - shell_arg = "-c" - } - - cmd := exec.Command(shell, shell_arg, ping_cmd) - err := cmd.Run() - - return err == nil, err - } -} - func CheckPort(host string, port string) (bool, error) { timeout := 500 * time.Millisecond conn, err := net.DialTimeout("tcp", net.JoinHostPort(host, port), timeout) diff --git a/backend/networking/pingdevice_linux.go b/backend/networking/pingdevice_linux.go new file mode 100644 index 00000000..d870d204 --- /dev/null +++ b/backend/networking/pingdevice_linux.go @@ -0,0 +1,79 @@ +//go:build linux + +package networking + +import ( + "fmt" + "os" + "os/exec" + "strconv" + "time" + + "github.com/pocketbase/pocketbase/core" + probing "github.com/prometheus-community/pro-bing" + "kernel.org/pub/linux/libs/security/libcap/cap" +) + +func PingDevice(device *core.Record) (bool, error) { + ping_cmd := device.GetString("ping_cmd") + if ping_cmd == "" { + pinger, err := probing.NewPinger(device.GetString("ip")) + if err != nil { + return false, err + } + pinger.Count = 1 + pinger.Timeout = 500 * time.Millisecond + + privileged := true + privilegedEnv := os.Getenv("UPSNAP_PING_PRIVILEGED") + if privilegedEnv != "" { + privileged, err = strconv.ParseBool(privilegedEnv) + if err != nil { + privileged = false + } + } + if privileged { + orig := cap.GetProc() + defer orig.SetProc() // restore original caps on exit. + + c, err := orig.Dup() + if err != nil { + return false, fmt.Errorf("Failed to dup existing capabilities: %v", err) + } + + if on, _ := c.GetFlag(cap.Permitted, cap.NET_RAW); !on { + return false, fmt.Errorf("Privileged ping selected but NET_RAW capability not permitted") + } + + if err := c.SetFlag(cap.Effective, true, cap.NET_RAW); err != nil { + return false, fmt.Errorf("unable to set NET_RAW capability") + } + + if err := c.SetProc(); err != nil { + return false, fmt.Errorf("unable to raise NET_RAW capability") + } + } + pinger.SetPrivileged(privileged) + + err = pinger.Run() + if err != nil { + if isNoRouteOrDownError(err) { + return false, nil + } + return false, err + } + stats := pinger.Statistics() + return stats.PacketLoss == 0, nil + } else { + var shell string + var shell_arg string + + shell = "/bin/sh" + shell_arg = "-c" + + cmd := exec.Command(shell, shell_arg, ping_cmd) + err := cmd.Run() + + return err == nil, err + } +} diff --git a/backend/networking/pingdevice_other.go b/backend/networking/pingdevice_other.go new file mode 100644 index 00000000..f3a24351 --- /dev/null +++ b/backend/networking/pingdevice_other.go @@ -0,0 +1,65 @@ +//go:build !linux + +package networking + +import ( + "os" + "os/exec" + "runtime" + "strconv" + "time" + + "github.com/pocketbase/pocketbase/core" + probing "github.com/prometheus-community/pro-bing" +) + +func PingDevice(device *core.Record) (bool, error) { + ping_cmd := device.GetString("ping_cmd") + if ping_cmd == "" { + pinger, err := probing.NewPinger(device.GetString("ip")) + if err != nil { + return false, err + } + pinger.Count = 1 + pinger.Timeout = 500 * time.Millisecond + + privileged := true // default to privileged ping, required by Windows + if runtime.GOOS == "darwin" { + // macOS pings will fail when using privileged ping as non root, but works with non-privileged. + privileged = false + } + privilegedEnv := os.Getenv("UPSNAP_PING_PRIVILEGED") + if privilegedEnv != "" { + privileged, err = strconv.ParseBool(privilegedEnv) + if err != nil { + privileged = false + } + } + pinger.SetPrivileged(privileged) + + err = pinger.Run() + if err != nil { + if isNoRouteOrDownError(err) { + return false, nil + } + return false, err + } + stats := pinger.Statistics() + return stats.PacketLoss == 0, nil + } else { + var shell string + var shell_arg string + if runtime.GOOS == "windows" { + shell = "cmd" + shell_arg = "/C" + } else { + shell = "/bin/sh" + shell_arg = "-c" + } + + cmd := exec.Command(shell, shell_arg, ping_cmd) + err := cmd.Run() + + return err == nil, err + } +} diff --git a/backend/networking/root.go b/backend/networking/root.go deleted file mode 100644 index 0cf0691f..00000000 --- a/backend/networking/root.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build !windows - -package networking - -import "os" - -func isRoot() bool { - return os.Geteuid() == 0 -} diff --git a/backend/networking/root_windows.go b/backend/networking/root_windows.go deleted file mode 100644 index 2f62d0b0..00000000 --- a/backend/networking/root_windows.go +++ /dev/null @@ -1,16 +0,0 @@ -//go:build windows - -package networking - -import "golang.org/x/sys/windows" - -func isRoot() bool { - var sid *windows.SID - sid, err := windows.CreateWellKnownSid(windows.WinBuiltinAdministratorsSid) - if err != nil { - return false - } - token := windows.GetCurrentProcessToken() - isAdmin, err := token.IsMember(sid) - return err == nil && isAdmin -} diff --git a/docker-compose.yml b/docker-compose.yml index b351c56e..4b770b45 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,8 +1,12 @@ services: upsnap: + cap_add: + - NET_RAW # NET_RAW is required for privileged ping + cap_drop: + - ALL container_name: upsnap image: ghcr.io/seriousm4x/upsnap:5 # images are also available on docker hub: seriousm4x/upsnap:5 - network_mode: host + network_mode: host # host is required for WOL magic packets restart: unless-stopped volumes: - ./data:/app/pb_data @@ -14,8 +18,12 @@ services: # - UPSNAP_INTERVAL=*/10 * * * * * # Sets the interval in which the devices are pinged # - UPSNAP_SCAN_RANGE=192.168.1.0/24 # Scan range is used for device discovery on local network # - UPSNAP_SCAN_TIMEOUT=500ms # Scan timeout is nmap's --host-timeout value to wait for devices (https://nmap.org/book/man-performance.html) - # - UPSNAP_PING_PRIVILEGED=true # Set to false if non-root user and no NET_RAW capability *requires host setting net.ipv4.ping_group_range="0 2147483647" + # - UPSNAP_PING_PRIVILEGED=true # Default is true. Set to false for unprivileged ping + # For Linux hosts, unprivileged pings REQUIRES the host to set 'sysctl net.ipv4.ping_group_range="0 2147483647"' + # or another suitable value to grant the group access to send ping and WOL magic packets. # - UPSNAP_WEBSITE_TITLE=Custom name # Custom website title + # security_opt: + # - no-new-privileges=true # # dns is used for name resolution during network scan # dns: # - 192.18.0.1