Trying goscan

This commit is contained in:
aceberg 2024-07-10 04:24:46 +07:00
parent 0c410d9458
commit 26f3759936
13 changed files with 254 additions and 107 deletions

View file

@ -1 +1 @@
internal/web/templates/version
internal/web/public/version

View file

@ -1,10 +1,3 @@
DUSER=aceberg
DNAME=watchyourlan
IFACE=enp4s0
DBPATH=/data/hosts.db
SHOUTRRR_URL=gotify://192.168.2.1:8083/AwQqpAae.rrl5Ob/?title=Unknown host detected&DisableTLS=yes
mod:
rm go.mod || true && \
rm go.sum || true && \
@ -14,7 +7,6 @@ mod:
run:
cd cmd/WatchYourLAN/ && \
sudo \
env IFACE=$(IFACE) DBPATH=$(DBPATH) THEME=$(THEME) \
go run . #-c /data/config #-n http://192.168.2.3:8850
fmt:
@ -24,27 +16,4 @@ lint:
golangci-lint run
golint ./...
check: fmt lint
go-build:
cd cmd/WatchYourLAN/ && \
CGO_ENABLED=0 go build -o ../../tmp/WatchYourLAN .
docker-build:
docker build -t $(DUSER)/$(DNAME) .
docker-run:
docker rm wyl || true
docker run --name wyl \
-e "IFACE=$(IFACE)" \
-e "SHOUTRRR_URL=$(SHOUTRRR_URL)" \
-e "TZ=Asia/Novosibirsk" \
--network="host" \
-v ~/.dockerdata/wyl:/data \
$(DUSER)/$(DNAME)
clean:
rm src/$(DNAME) || true
docker rmi -f $(DUSER)/$(DNAME)
dev: docker-build docker-run
check: fmt lint

1
go.mod
View file

@ -4,6 +4,7 @@ go 1.22.4
require (
github.com/gin-gonic/gin v1.10.0
github.com/google/gopacket v1.1.19
github.com/spf13/viper v1.19.0
)

14
go.sum
View file

@ -33,6 +33,8 @@ github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MG
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
@ -104,18 +106,30 @@ go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTV
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

57
internal/arp/arpscan.go Normal file
View file

@ -0,0 +1,57 @@
package arp
import (
"log"
"os/exec"
"strings"
"time"
"github.com/aceberg/WatchYourLAN/internal/models"
)
func scanIface(iface string) string {
cmd, err := exec.Command("arp-scan", "-glNx", "-I", iface).Output()
if err != nil {
return string("")
}
return string(cmd)
}
func parseOutput(text string) []models.Host {
var foundHosts = []models.Host{}
perString := strings.Split(text, "\n")
currentTime := time.Now()
for _, host := range perString {
if host != "" {
var oneHost models.Host
p := strings.Split(host, " ")
oneHost.IP = p[0]
oneHost.Mac = p[1]
oneHost.Hw = p[2]
oneHost.Date = currentTime.Format("2006-01-02 15:04:05")
oneHost.Now = 1
foundHosts = append(foundHosts, oneHost)
}
}
return foundHosts
}
// Scan all interfaces
func Scan(ifaces []string) []models.Host {
var text string
var foundHosts = []models.Host{}
for _, iface := range ifaces {
text = scanIface(iface)
log.Println("INFO: scanning interface", iface)
log.Println("INFO: found IPs:", text)
foundHosts = append(foundHosts, parseOutput(text)...)
}
return foundHosts
}

156
internal/arp/goscan.go Normal file
View file

@ -0,0 +1,156 @@
package arp
import (
"bytes"
"encoding/binary"
"errors"
"log"
"net"
"time"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/google/gopacket/pcap"
)
// GoScan - scan iface
func GoScan() {
iface, _ := net.InterfaceByName("enp4s0")
err := scan(iface)
log.Println(err)
}
func scan(iface *net.Interface) error {
// We just look for IPv4 addresses, so try to find if the interface has one.
var addr *net.IPNet
if addrs, err := iface.Addrs(); err != nil {
return err
} else {
for _, a := range addrs {
if ipnet, ok := a.(*net.IPNet); ok {
if ip4 := ipnet.IP.To4(); ip4 != nil {
addr = &net.IPNet{
IP: ip4,
Mask: ipnet.Mask[len(ipnet.Mask)-4:],
}
break
}
}
}
}
// Sanity-check that the interface has a good address.
if addr == nil {
return errors.New("no good IP network found")
} else if addr.IP[0] == 127 {
return errors.New("skipping localhost")
} else if addr.Mask[0] != 0xff || addr.Mask[1] != 0xff {
return errors.New("mask means network is too large")
}
log.Printf("Using network range %v for interface %v", addr, iface.Name)
// Open up a pcap handle for packet reads/writes.
handle, err := pcap.OpenLive(iface.Name, 65536, true, pcap.BlockForever)
if err != nil {
return err
}
defer handle.Close()
// Start up a goroutine to read in packet data.
stop := make(chan struct{})
go readARP(handle, iface, stop)
defer close(stop)
// for {
// Write our scan packets out to the handle.
if err := writeARP(handle, iface, addr); err != nil {
log.Printf("error writing packets on %v: %v", iface.Name, err)
return err
}
// We don't know exactly how long it'll take for packets to be
// sent back to us, but 10 seconds should be more than enough
// time ;)
time.Sleep(10 * time.Second)
// }
return err
}
// readARP watches a handle for incoming ARP responses we might care about, and prints them.
//
// readARP loops until 'stop' is closed.
func readARP(handle *pcap.Handle, iface *net.Interface, stop chan struct{}) {
src := gopacket.NewPacketSource(handle, layers.LayerTypeEthernet)
in := src.Packets()
for {
var packet gopacket.Packet
select {
case <-stop:
return
case packet = <-in:
arpLayer := packet.Layer(layers.LayerTypeARP)
if arpLayer == nil {
continue
}
arp := arpLayer.(*layers.ARP)
if arp.Operation != layers.ARPReply || bytes.Equal([]byte(iface.HardwareAddr), arp.SourceHwAddress) {
// This is a packet I sent.
continue
}
// Note: we might get some packets here that aren't responses to ones we've sent,
// if for example someone else sends US an ARP request. Doesn't much matter, though...
// all information is good information :)
log.Printf("IP %v is at %v", net.IP(arp.SourceProtAddress), net.HardwareAddr(arp.SourceHwAddress))
}
}
}
// writeARP writes an ARP request for each address on our local network to the
// pcap handle.
func writeARP(handle *pcap.Handle, iface *net.Interface, addr *net.IPNet) error {
// Set up all the layers' fields we can.
eth := layers.Ethernet{
SrcMAC: iface.HardwareAddr,
DstMAC: net.HardwareAddr{0xff, 0xff, 0xff, 0xff, 0xff, 0xff},
EthernetType: layers.EthernetTypeARP,
}
arp := layers.ARP{
AddrType: layers.LinkTypeEthernet,
Protocol: layers.EthernetTypeIPv4,
HwAddressSize: 6,
ProtAddressSize: 4,
Operation: layers.ARPRequest,
SourceHwAddress: []byte(iface.HardwareAddr),
SourceProtAddress: []byte(addr.IP),
DstHwAddress: []byte{0, 0, 0, 0, 0, 0},
}
// Set up buffer and options for serialization.
buf := gopacket.NewSerializeBuffer()
opts := gopacket.SerializeOptions{
FixLengths: true,
ComputeChecksums: true,
}
// Send one packet for every address.
for _, ip := range ips(addr) {
arp.DstProtAddress = []byte(ip)
gopacket.SerializeLayers(buf, opts, &eth, &arp)
if err := handle.WritePacketData(buf.Bytes()); err != nil {
return err
}
}
return nil
}
// ips is a simple and not very good method for getting all IPv4 addresses from a
// net.IPNet. It returns all IPs it can over the channel it sends back, closing
// the channel when done.
func ips(n *net.IPNet) (out []net.IP) {
num := binary.BigEndian.Uint32([]byte(n.IP))
mask := binary.BigEndian.Uint32([]byte(n.Mask))
network := num & mask
broadcast := network | ^mask
for network++; network < broadcast; network++ {
var buf [4]byte
binary.BigEndian.PutUint32(buf[:], network)
out = append(out, net.IP(buf[:]))
}
return
}

View file

@ -14,6 +14,7 @@ func Get(path string) (config models.Conf) {
viper.SetDefault("PORT", "8840")
viper.SetDefault("THEME", "solar")
viper.SetDefault("COLOR", "dark")
viper.SetDefault("NODEPATH", "")
viper.SetConfigFile(path)
viper.SetConfigType("yaml")
@ -26,6 +27,7 @@ func Get(path string) (config models.Conf) {
config.Port = viper.Get("PORT").(string)
config.Theme = viper.Get("THEME").(string)
config.Color = viper.Get("COLOR").(string)
config.NodePath = viper.Get("NODEPATH").(string)
return config
}
@ -40,6 +42,7 @@ func Write(config models.Conf) {
viper.Set("PORT", config.Port)
viper.Set("THEME", config.Theme)
viper.Set("COLOR", config.Color)
viper.Set("NODEPATH", config.NodePath)
err := viper.WriteConfig()
check.IfError(err)

View file

@ -8,7 +8,9 @@ type Conf struct {
Color string
DirPath string
ConfPath string
DBPath string
NodePath string
Ifaces []string
}
// Host - one host

View file

@ -8,7 +8,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/aceberg/WatchYourLAN/internal/check"
// "github.com/aceberg/WatchYourLAN/internal/conf"
"github.com/aceberg/WatchYourLAN/internal/conf"
"github.com/aceberg/WatchYourLAN/internal/models"
)
@ -30,21 +30,13 @@ func configHandler(c *gin.Context) {
func saveConfigHandler(c *gin.Context) {
// appConfig.Host = c.PostForm("host")
// appConfig.Port = c.PostForm("port")
// appConfig.Theme = c.PostForm("theme")
// appConfig.Color = c.PostForm("color")
// tStr := c.PostForm("timeout")
// hStr := c.PostForm("trim")
appConfig.Host = c.PostForm("host")
appConfig.Port = c.PostForm("port")
appConfig.Theme = c.PostForm("theme")
appConfig.Color = c.PostForm("color")
appConfig.NodePath = c.PostForm("node")
// if tStr != "" {
// appConfig.Timeout, _ = strconv.Atoi(tStr)
// }
// if hStr != "" {
// appConfig.HistTrim, _ = strconv.Atoi(hStr)
// }
// conf.Write(appConfig)
conf.Write(appConfig)
slog.Info("writing new config to " + appConfig.ConfPath)

View file

@ -1,10 +1,12 @@
package web
import (
"log"
"net/http"
"github.com/gin-gonic/gin"
"github.com/aceberg/WatchYourLAN/internal/arp"
"github.com/aceberg/WatchYourLAN/internal/models"
)
@ -12,6 +14,11 @@ func indexHandler(c *gin.Context) {
var guiData models.GuiData
guiData.Config = appConfig
res := arp.Scan(appConfig.Ifaces)
log.Println("ARPSCAN:", res)
go arp.GoScan()
c.HTML(http.StatusOK, "header.html", guiData)
c.HTML(http.StatusOK, "index.html", guiData)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View file

@ -35,12 +35,8 @@
</select></td>
</tr>
<tr>
<td>Timeout (minutes)</td>
<td><input name="timeout" type="number" class="form-control" value="{{ .Config.Timeout }}"></td>
</tr>
<tr>
<td>Trim History</td>
<td><input name="trim" type="number" class="form-control" value="{{ .Config.HistTrim }}"></td>
<td>Local node-bootstrap url</td>
<td><input name="node" type="text" class="form-control" value="{{ .Config.NodePath }}"></td>
</tr>
<tr>
<td><button type="submit" class="btn btn-primary">Save</button></td>
@ -58,59 +54,6 @@
</div>
</div>
<div class="col-md mt-4">
<div class="card border-primary">
<div class="card-header">InfluxDB2 config</div>
<div class="card-body table-responsive">
<table class="table table-borderless">
<form action="/config_influx/" method="post">
<tr>
<td>Enable</td>
<td>
<div class="form-check form-switch">
{{ if .Config.InfluxEnable }}
<input class="form-check-input" type="checkbox" name="enable" checked>
{{ else }}
<input class="form-check-input" type="checkbox" name="enable">
{{ end }}
</div>
</td>
</tr>
<tr>
<td>Address</td>
<td><input name="addr" type="text" class="form-control" value="{{ .Config.InfluxAddr }}"></td>
</tr>
<tr>
<td>Token</td>
<td><input name="token" type="text" class="form-control" value="{{ .Config.InfluxToken }}"></td>
</tr>
<tr>
<td>Org</td>
<td><input name="org" type="text" class="form-control" value="{{ .Config.InfluxOrg }}"></td>
</tr>
<tr>
<td>Bucket</td>
<td><input name="bucket" type="text" class="form-control" value="{{ .Config.InfluxBucket }}"></td>
</tr>
<tr>
<td>Skip TLS verify</td>
<td>
<div class="form-check form-switch">
{{ if .Config.InfluxSkipTLS }}
<input class="form-check-input" type="checkbox" name="skip" checked>
{{ else }}
<input class="form-check-input" type="checkbox" name="skip">
{{ end }}
</div>
</td>
</tr>
<tr>
<td><button type="submit" class="btn btn-primary">Save</button></td>
<td></td>
</tr>
</form>
</table>
</div>
</div>
<div class="card border-primary mt-4 mb-4">
<div class="card-header">About</div>
<div class="card-body">

View file

@ -23,6 +23,9 @@ func Gui(dirPath, nodePath string) {
appConfig.ConfPath = confPath
appConfig.NodePath = nodePath
// temp
appConfig.Ifaces = append(appConfig.Ifaces, "enp4s0")
slog.Info("config", "path", appConfig.DirPath)
address := appConfig.Host + ":" + appConfig.Port