diff --git a/.version b/.version index bb7777a..53be989 120000 --- a/.version +++ b/.version @@ -1 +1 @@ -internal/web/templates/version \ No newline at end of file +internal/web/public/version \ No newline at end of file diff --git a/Makefile b/Makefile index 365b9b8..7e79d57 100644 --- a/Makefile +++ b/Makefile @@ -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 \ No newline at end of file +check: fmt lint \ No newline at end of file diff --git a/go.mod b/go.mod index 02e048f..985c59c 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 2614f17..dbb03a5 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/arp/arpscan.go b/internal/arp/arpscan.go new file mode 100644 index 0000000..ae073bf --- /dev/null +++ b/internal/arp/arpscan.go @@ -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 +} diff --git a/internal/arp/goscan.go b/internal/arp/goscan.go new file mode 100644 index 0000000..01a464a --- /dev/null +++ b/internal/arp/goscan.go @@ -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, ð, &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 +} diff --git a/internal/conf/getconfig.go b/internal/conf/getconfig.go index 462478b..f3b5696 100644 --- a/internal/conf/getconfig.go +++ b/internal/conf/getconfig.go @@ -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) diff --git a/internal/models/models.go b/internal/models/models.go index f59dcb7..a0f3d7b 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -8,7 +8,9 @@ type Conf struct { Color string DirPath string ConfPath string + DBPath string NodePath string + Ifaces []string } // Host - one host diff --git a/internal/web/config.go b/internal/web/config.go index cb47e05..8675cd6 100644 --- a/internal/web/config.go +++ b/internal/web/config.go @@ -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) diff --git a/internal/web/index.go b/internal/web/index.go index b3f0424..93bdc8b 100644 --- a/internal/web/index.go +++ b/internal/web/index.go @@ -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) } diff --git a/internal/web/public/favicon.png b/internal/web/public/favicon.png index 16dd7e7..2f2d5ca 100644 Binary files a/internal/web/public/favicon.png and b/internal/web/public/favicon.png differ diff --git a/internal/web/templates/config.html b/internal/web/templates/config.html index a7fa462..cf9e871 100644 --- a/internal/web/templates/config.html +++ b/internal/web/templates/config.html @@ -35,12 +35,8 @@