Skip to content

Commit 60bdd4c

Browse files
committed
Add ability for device/network scan when rootless
Signed-off-by: invario <67800603+invario@users.noreply.github.com>
1 parent 5c5b908 commit 60bdd4c

File tree

6 files changed

+262
-98
lines changed

6 files changed

+262
-98
lines changed

Dockerfile

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ RUN wget https://github.com/seriousm4x/UpSnap/releases/download/${VERSION}/UpSna
1111
chmod +x upsnap &&\
1212
apk update &&\
1313
apk add --no-cache libcap &&\
14-
setcap 'cap_net_raw=+ep' ./upsnap
14+
setcap 'cap_net_raw=+p' ./upsnap
1515

1616
FROM alpine:3
1717
ARG UPSNAP_HTTP_LISTEN=0.0.0.0:8090
@@ -23,4 +23,5 @@ WORKDIR /app
2323
COPY --from=downloader /app/upsnap upsnap
2424
HEALTHCHECK --interval=10s \
2525
CMD curl -fs "http://${UPSNAP_HTTP_LISTEN}/api/health" || exit 1
26-
ENTRYPOINT ["sh", "-c", "./upsnap serve --http ${UPSNAP_HTTP_LISTEN}"]
26+
CMD ["serve","--http","${UPSNAP_HTTP_LISTEN}"]
27+
ENTRYPOINT ["/app/upsnap"]

backend/go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ require (
4444
golang.org/x/oauth2 v0.35.0 // indirect
4545
golang.org/x/sync v0.19.0 // indirect
4646
golang.org/x/text v0.34.0 // indirect
47+
kernel.org/pub/linux/libs/security/libcap/cap v1.2.77 // indirect
48+
kernel.org/pub/linux/libs/security/libcap/psx v1.2.77 // indirect
4749
modernc.org/libc v1.67.6 // indirect
4850
modernc.org/mathutil v1.7.1 // indirect
4951
modernc.org/memory v1.11.0 // indirect

backend/go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,10 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
134134
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
135135
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
136136
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
137+
kernel.org/pub/linux/libs/security/libcap/cap v1.2.77 h1:iQtQTjFUOcTT19fI8sTCzYXsjeVs56et3D8AbKS2Uks=
138+
kernel.org/pub/linux/libs/security/libcap/cap v1.2.77/go.mod h1:oV+IO8kGh0B7TxErbydDe2+BRmi9g/W0CkpVV+QBTJU=
139+
kernel.org/pub/linux/libs/security/libcap/psx v1.2.77 h1:Z06sMOzc0GNCwp6efaVrIrz4ywGJ1v+DP0pjVkOfDuA=
140+
kernel.org/pub/linux/libs/security/libcap/psx v1.2.77/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24=
137141
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
138142
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
139143
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=

backend/pb/handlers.go

Lines changed: 0 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,9 @@ package pb
22

33
import (
44
"encoding/json"
5-
"encoding/xml"
65
"fmt"
7-
"net"
86
"net/http"
9-
"os"
10-
"os/exec"
117
"strconv"
12-
"strings"
138
"time"
149

1510
"github.com/pocketbase/dbx"
@@ -216,97 +211,6 @@ type Nmaprun struct {
216211
} `xml:"host"`
217212
}
218213

219-
func HandlerScan(e *core.RequestEvent) error {
220-
// check if nmap installed
221-
nmap, err := exec.LookPath("nmap")
222-
if err != nil {
223-
return apis.NewBadRequestError(err.Error(), nil)
224-
}
225-
226-
// check if scan range is valid
227-
allPrivateSettings, err := e.App.FindAllRecords("settings_private")
228-
if err != nil {
229-
return err
230-
}
231-
settingsPrivate := allPrivateSettings[0]
232-
scanRange := settingsPrivate.GetString("scan_range")
233-
_, ipNet, err := net.ParseCIDR(scanRange)
234-
if err != nil {
235-
return apis.NewBadRequestError(err.Error(), nil)
236-
}
237-
238-
// run nmap
239-
timeout := os.Getenv("UPSNAP_SCAN_TIMEOUT")
240-
if timeout == "" {
241-
timeout = "500ms"
242-
}
243-
cmd := exec.Command(nmap, "-sn", "-oX", "-", scanRange, "--host-timeout", timeout)
244-
cmdOutput, err := cmd.Output()
245-
if err != nil {
246-
return err
247-
}
248-
249-
// unmarshal xml
250-
nmapOutput := Nmaprun{}
251-
if err := xml.Unmarshal(cmdOutput, &nmapOutput); err != nil {
252-
return err
253-
}
254-
255-
type Device struct {
256-
Name string `json:"name"`
257-
IP string `json:"ip"`
258-
MAC string `json:"mac"`
259-
MACVendor string `json:"mac_vendor"`
260-
}
261-
262-
// extract info from struct into data
263-
type Response struct {
264-
Netmask string `json:"netmask"`
265-
Devices []Device `json:"devices"`
266-
}
267-
res := Response{}
268-
var nm []string
269-
for _, octet := range ipNet.Mask {
270-
nm = append(nm, strconv.Itoa(int(octet)))
271-
}
272-
res.Netmask = strings.Join(nm, ".")
273-
274-
for _, host := range nmapOutput.Host {
275-
dev := Device{}
276-
for _, addr := range host.Address {
277-
if addr.Addrtype == "ipv4" {
278-
dev.IP = addr.Addr
279-
} else if addr.Addrtype == "mac" {
280-
dev.MAC = addr.Addr
281-
}
282-
if addr.Vendor != "" {
283-
dev.MACVendor = addr.Vendor
284-
} else {
285-
dev.MACVendor = "Unknown"
286-
}
287-
}
288-
289-
if dev.IP == "" || dev.MAC == "" {
290-
continue
291-
}
292-
293-
names, err := net.LookupAddr(dev.IP)
294-
if err != nil || len(names) == 0 {
295-
dev.Name = dev.MACVendor
296-
} else {
297-
dev.Name = strings.TrimSuffix(names[0], ".")
298-
}
299-
300-
if dev.Name == "" && dev.MACVendor == "" {
301-
continue
302-
}
303-
304-
res.Devices = append(res.Devices, dev)
305-
}
306-
307-
return e.JSON(http.StatusOK, res)
308-
}
309-
310214
func HandlerInitSuperuser(e *core.RequestEvent) error {
311215
superusersCollection, err := e.App.FindCollectionByNameOrId(core.CollectionNameSuperusers)
312216
if err != nil {

backend/pb/handlerscan_linux.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
//go:build linux
2+
3+
package pb
4+
5+
import (
6+
"encoding/xml"
7+
"fmt"
8+
"net"
9+
"net/http"
10+
"os"
11+
"os/exec"
12+
"strconv"
13+
"strings"
14+
15+
"github.com/pocketbase/pocketbase/apis"
16+
"github.com/pocketbase/pocketbase/core"
17+
"kernel.org/pub/linux/libs/security/libcap/cap"
18+
)
19+
20+
func HandlerScan(e *core.RequestEvent) error {
21+
// check if nmap installed
22+
nmap, err := exec.LookPath("nmap")
23+
if err != nil {
24+
return apis.NewBadRequestError(err.Error(), nil)
25+
}
26+
27+
// check if scan range is valid
28+
allPrivateSettings, err := e.App.FindAllRecords("settings_private")
29+
if err != nil {
30+
return err
31+
}
32+
settingsPrivate := allPrivateSettings[0]
33+
scanRange := settingsPrivate.GetString("scan_range")
34+
_, ipNet, err := net.ParseCIDR(scanRange)
35+
if err != nil {
36+
return apis.NewBadRequestError(err.Error(), nil)
37+
}
38+
39+
// run nmap
40+
timeout := os.Getenv("UPSNAP_SCAN_TIMEOUT")
41+
if timeout == "" {
42+
timeout = "500ms"
43+
}
44+
orig := cap.GetProc()
45+
defer orig.SetProc() // restore original caps on exit.
46+
47+
c, err := orig.Dup()
48+
if err != nil {
49+
return fmt.Errorf("Failed to dup existing capabilities: %v", err)
50+
}
51+
52+
if on, _ := c.GetFlag(cap.Permitted, cap.NET_RAW); !on {
53+
return fmt.Errorf("Privileged ping selected but NET_RAW capability not permitted")
54+
}
55+
56+
if err := c.SetFlag(cap.Effective, true, cap.NET_RAW); err != nil {
57+
return fmt.Errorf("unable to set NET_RAW capability effective")
58+
}
59+
60+
if err := c.SetFlag(cap.Inheritable, true, cap.NET_RAW); err != nil {
61+
return fmt.Errorf("unable to set NET_RAW capability inheritable")
62+
}
63+
64+
if err := c.SetProc(); err != nil {
65+
return fmt.Errorf("unable to raise NET_RAW capability")
66+
}
67+
68+
if err := cap.SetAmbient(true, cap.NET_RAW); err != nil {
69+
return fmt.Errorf("unable to set NET_RAW capability ambient")
70+
}
71+
72+
cmd := exec.Command(nmap, "-sn", "-oX", "-", scanRange, "--host-timeout", timeout, "--privileged")
73+
cmdOutput, err := cmd.Output()
74+
if err != nil {
75+
return err
76+
}
77+
78+
// unmarshal xml
79+
nmapOutput := Nmaprun{}
80+
if err := xml.Unmarshal(cmdOutput, &nmapOutput); err != nil {
81+
return err
82+
}
83+
84+
type Device struct {
85+
Name string `json:"name"`
86+
IP string `json:"ip"`
87+
MAC string `json:"mac"`
88+
MACVendor string `json:"mac_vendor"`
89+
}
90+
91+
// extract info from struct into data
92+
type Response struct {
93+
Netmask string `json:"netmask"`
94+
Devices []Device `json:"devices"`
95+
}
96+
res := Response{}
97+
var nm []string
98+
for _, octet := range ipNet.Mask {
99+
nm = append(nm, strconv.Itoa(int(octet)))
100+
}
101+
res.Netmask = strings.Join(nm, ".")
102+
103+
for _, host := range nmapOutput.Host {
104+
dev := Device{}
105+
for _, addr := range host.Address {
106+
if addr.Addrtype == "ipv4" {
107+
dev.IP = addr.Addr
108+
} else if addr.Addrtype == "mac" {
109+
dev.MAC = addr.Addr
110+
}
111+
if addr.Vendor != "" {
112+
dev.MACVendor = addr.Vendor
113+
} else {
114+
dev.MACVendor = "Unknown"
115+
}
116+
}
117+
118+
if dev.IP == "" || dev.MAC == "" {
119+
continue
120+
}
121+
122+
names, err := net.LookupAddr(dev.IP)
123+
if err != nil || len(names) == 0 {
124+
dev.Name = dev.MACVendor
125+
} else {
126+
dev.Name = strings.TrimSuffix(names[0], ".")
127+
}
128+
129+
if dev.Name == "" && dev.MACVendor == "" {
130+
continue
131+
}
132+
133+
res.Devices = append(res.Devices, dev)
134+
}
135+
136+
return e.JSON(http.StatusOK, res)
137+
}

0 commit comments

Comments
 (0)