diff --git a/go.mod b/go.mod index 4d38921..0eb6273 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/apenella/go-ansible v1.1.7 github.com/gdamore/tcell/v2 v2.6.0 github.com/golang/mock v1.6.0 - github.com/imdario/mergo v0.3.16 + github.com/google/gopacket v1.1.19 github.com/jackpal/gateway v1.0.10 github.com/projectdiscovery/mapcidr v1.1.2 github.com/rivo/tview v0.0.0-20230621164836-6cc0565babaf diff --git a/go.sum b/go.sum index 48feea8..a14bab1 100644 --- a/go.sum +++ b/go.sum @@ -155,6 +155,8 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +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/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -203,8 +205,6 @@ github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2p github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= -github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= diff --git a/internal/core/core_test.go b/internal/core/core_test.go index 27953b5..64cc4c9 100644 --- a/internal/core/core_test.go +++ b/internal/core/core_test.go @@ -26,6 +26,7 @@ func TestCore(t *testing.T) { mockScanner := mock_discovery.NewMockScanner(ctrl) mockDetailsScanner := mock_discovery.NewMockDetailScanner(ctrl) + mockPacketScanner := mock_discovery.NewMockPacketScanner(ctrl) mockConfig := mock_config.NewMockService(ctrl) mockServerService := mock_server.NewMockService(ctrl) @@ -40,6 +41,7 @@ func TestCore(t *testing.T) { discoveryService := discovery.NewScannerService( mockScanner, mockDetailsScanner, + mockPacketScanner, mockServerService, ) diff --git a/internal/core/create.go b/internal/core/create.go index 6075cfb..a4a93de 100644 --- a/internal/core/create.go +++ b/internal/core/create.go @@ -83,7 +83,7 @@ func CreateNewAppCore(networkInfo *util.NetworkInfo) (*Core, error) { serverRepo := server.NewSqliteRepo(db) serverService := server.NewService(*conf, serverRepo) - netScanner, err := discovery.NewNetScanner(conf.Targets) + netScanner, err := discovery.NewNetScanner(networkInfo, conf.Targets) if err != nil { return nil, err @@ -91,9 +91,16 @@ func CreateNewAppCore(networkInfo *util.NetworkInfo) (*Core, error) { detailScanner := discovery.NewAnsibleIpScanner(*conf) + packetScanner, err := discovery.NewPCapScanner(conf.Targets, networkInfo) + + if err != nil { + return nil, err + } + scannerService := discovery.NewScannerService( netScanner, detailScanner, + packetScanner, serverService, ) diff --git a/internal/core/monitor.go b/internal/core/monitor.go index b6fdc7d..2debd3f 100644 --- a/internal/core/monitor.go +++ b/internal/core/monitor.go @@ -43,6 +43,7 @@ func (c *Core) handleServerEvent(evt *event.Event) { fields := map[string]interface{}{ "type": evt.Type, "id": payload.ID, + "mac": payload.MAC, "hostname": payload.Hostname, "os": payload.OS, "ip": payload.IP, diff --git a/internal/discovery/interface.go b/internal/discovery/interface.go index 195bbfc..5f0afb3 100644 --- a/internal/discovery/interface.go +++ b/internal/discovery/interface.go @@ -1,14 +1,23 @@ package discovery -import "context" +import ( + "context" -//go:generate mockgen -destination=../mock/discovery/mock_discovery.go -package=mock_discovery . DetailScanner,Scanner + "github.com/robgonnella/ops/internal/server" +) + +//go:generate mockgen -destination=../mock/discovery/mock_discovery.go -package=mock_discovery . DetailScanner,PacketScanner,Scanner // DetailScanner interface for gathering more details about a device type DetailScanner interface { GetServerDetails(ctx context.Context, ip string) (*Details, error) } +// PacketScanner +type PacketScanner interface { + ListenForPackets(resultChan chan *server.Server) +} + // Scanner interface for scanning a network for devices type Scanner interface { Scan(resultChan chan *DiscoveryResult) error diff --git a/internal/discovery/net.go b/internal/discovery/net.go index 632d307..c3deb59 100644 --- a/internal/discovery/net.go +++ b/internal/discovery/net.go @@ -13,6 +13,7 @@ import ( "github.com/projectdiscovery/mapcidr" "github.com/robgonnella/ops/internal/logger" "github.com/robgonnella/ops/internal/server" + "github.com/robgonnella/ops/internal/util" ) var cidrSuffix = regexp.MustCompile(`\/\d{2}$`) @@ -24,7 +25,7 @@ type NetScanner struct { log logger.Logger } -func NewNetScanner(targets []string) (*NetScanner, error) { +func NewNetScanner(networkInfo *util.NetworkInfo, targets []string) (*NetScanner, error) { ipList := []string{} for _, t := range targets { diff --git a/internal/discovery/pcap.go b/internal/discovery/pcap.go new file mode 100644 index 0000000..b9c29c2 --- /dev/null +++ b/internal/discovery/pcap.go @@ -0,0 +1,99 @@ +package discovery + +import ( + "context" + + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + "github.com/google/gopacket/pcap" + "github.com/projectdiscovery/mapcidr" + "github.com/robgonnella/ops/internal/logger" + "github.com/robgonnella/ops/internal/server" + "github.com/robgonnella/ops/internal/util" +) + +type PCapScanner struct { + ctx context.Context + cancel context.CancelFunc + networkInfo *util.NetworkInfo + targets []string + handle *pcap.Handle + packetSource *gopacket.PacketSource + log logger.Logger +} + +func NewPCapScanner( + targets []string, + networkInfo *util.NetworkInfo, +) (*PCapScanner, error) { + ipList := []string{} + + for _, t := range targets { + if cidrSuffix.MatchString(t) { + ips, err := mapcidr.IPAddresses(t) + + if err != nil { + return nil, err + } + + ipList = append(ipList, ips...) + } else { + ipList = append(ipList, t) + } + } + + handle, err := pcap.OpenLive( + networkInfo.Interface.Name, + int32(networkInfo.Interface.MTU), + true, + pcap.BlockForever, + ) + + if err != nil { + return nil, err + } + + packetSource := gopacket.NewPacketSource(handle, layers.LinkTypeEthernet) + + ctx, cancel := context.WithCancel(context.Background()) + + return &PCapScanner{ + ctx: ctx, + cancel: cancel, + networkInfo: networkInfo, + targets: ipList, + handle: handle, + packetSource: packetSource, + log: logger.New(), + }, nil +} + +func (s *PCapScanner) Stop() { + s.cancel() + s.handle.Close() +} + +func (s *PCapScanner) ListenForPackets(res chan *server.Server) { + for packet := range s.packetSource.Packets() { + ipLayer := packet.Layer(layers.LayerTypeIPv4) + ethLayer := packet.Layer(layers.LayerTypeEthernet) + + if ipLayer == nil || ethLayer == nil { + continue + } + + ipv4 := ipLayer.(*layers.IPv4) + eth := ethLayer.(*layers.Ethernet) + + if ipv4.SrcIP.Equal(s.networkInfo.UserIP) { + continue + } + + srcIP := ipv4.SrcIP.String() + srcMAC := eth.SrcMAC.String() + + if util.SliceIncludes(s.targets, srcIP) { + res <- &server.Server{IP: srcIP, MAC: srcMAC} + } + } +} diff --git a/internal/discovery/service.go b/internal/discovery/service.go index e0d9bb4..6ca95b5 100644 --- a/internal/discovery/service.go +++ b/internal/discovery/service.go @@ -14,6 +14,7 @@ type ScannerService struct { cancel context.CancelFunc scanner Scanner detailScanner DetailScanner + packetScanner PacketScanner serverService server.Service log logger.Logger } @@ -22,6 +23,7 @@ type ScannerService struct { func NewScannerService( scanner Scanner, detailScanner DetailScanner, + packetScanner PacketScanner, serverService server.Service, ) *ScannerService { log := logger.New() @@ -34,6 +36,7 @@ func NewScannerService( cancel: cancel, scanner: scanner, detailScanner: detailScanner, + packetScanner: packetScanner, serverService: serverService, log: log, } @@ -59,6 +62,10 @@ func (s *ScannerService) Stop() { func (s *ScannerService) pollNetwork() { ticker := time.NewTicker(time.Second * 30) resultChan := make(chan *DiscoveryResult) + packetResults := make(chan *server.Server) + + // start listening for packet updates + go s.packetScanner.ListenForPackets(packetResults) // start first scan // always scan in goroutine to prevent blocking result channel @@ -77,6 +84,13 @@ func (s *ScannerService) pollNetwork() { return case r := <-resultChan: s.handleDiscoveryResult(r) + case r := <-packetResults: + if err := s.serverService.UpdateMACByIP(r); err != nil { + s.log.Error(). + Err(err). + Interface("req", r). + Msg("failed to update server") + } case <-ticker.C: // always scan in goroutine to prevent blocking result channel go func() { diff --git a/internal/discovery/service_test.go b/internal/discovery/service_test.go index 2394535..e13ce46 100644 --- a/internal/discovery/service_test.go +++ b/internal/discovery/service_test.go @@ -19,11 +19,13 @@ func TestDiscoveryService(t *testing.T) { t.Run("monitors network for offline servers", func(st *testing.T) { mockScanner := mock_discovery.NewMockScanner(ctrl) mockDetailScanner := mock_discovery.NewMockDetailScanner(ctrl) + mockPacketScanner := mock_discovery.NewMockPacketScanner(ctrl) mockServerService := mock_server.NewMockService(ctrl) service := discovery.NewScannerService( mockScanner, mockDetailScanner, + mockPacketScanner, mockServerService, ) @@ -60,11 +62,13 @@ func TestDiscoveryService(t *testing.T) { t.Run("monitors network for online servers", func(st *testing.T) { mockScanner := mock_discovery.NewMockScanner(ctrl) mockDetailScanner := mock_discovery.NewMockDetailScanner(ctrl) + mockPacketScanner := mock_discovery.NewMockPacketScanner(ctrl) mockServerService := mock_server.NewMockService(ctrl) service := discovery.NewScannerService( mockScanner, mockDetailScanner, + mockPacketScanner, mockServerService, ) @@ -110,11 +114,13 @@ func TestDiscoveryService(t *testing.T) { t.Run("requests extra details when ssh is enabled", func(st *testing.T) { mockScanner := mock_discovery.NewMockScanner(ctrl) mockDetailScanner := mock_discovery.NewMockDetailScanner(ctrl) + mockPacketScanner := mock_discovery.NewMockPacketScanner(ctrl) mockServerService := mock_server.NewMockService(ctrl) service := discovery.NewScannerService( mockScanner, mockDetailScanner, + mockPacketScanner, mockServerService, ) diff --git a/internal/mock/discovery/mock_discovery.go b/internal/mock/discovery/mock_discovery.go index d0364a5..59b22f0 100644 --- a/internal/mock/discovery/mock_discovery.go +++ b/internal/mock/discovery/mock_discovery.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/robgonnella/ops/internal/discovery (interfaces: DetailScanner,Scanner) +// Source: github.com/robgonnella/ops/internal/discovery (interfaces: DetailScanner,PacketScanner,Scanner) // Package mock_discovery is a generated GoMock package. package mock_discovery @@ -10,6 +10,7 @@ import ( gomock "github.com/golang/mock/gomock" discovery "github.com/robgonnella/ops/internal/discovery" + server "github.com/robgonnella/ops/internal/server" ) // MockDetailScanner is a mock of DetailScanner interface. @@ -50,6 +51,41 @@ func (mr *MockDetailScannerMockRecorder) GetServerDetails(arg0, arg1 interface{} return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServerDetails", reflect.TypeOf((*MockDetailScanner)(nil).GetServerDetails), arg0, arg1) } +// MockPacketScanner is a mock of PacketScanner interface. +type MockPacketScanner struct { + ctrl *gomock.Controller + recorder *MockPacketScannerMockRecorder +} + +// MockPacketScannerMockRecorder is the mock recorder for MockPacketScanner. +type MockPacketScannerMockRecorder struct { + mock *MockPacketScanner +} + +// NewMockPacketScanner creates a new mock instance. +func NewMockPacketScanner(ctrl *gomock.Controller) *MockPacketScanner { + mock := &MockPacketScanner{ctrl: ctrl} + mock.recorder = &MockPacketScannerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPacketScanner) EXPECT() *MockPacketScannerMockRecorder { + return m.recorder +} + +// ListenForPackets mocks base method. +func (m *MockPacketScanner) ListenForPackets(arg0 chan *server.Server) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "ListenForPackets", arg0) +} + +// ListenForPackets indicates an expected call of ListenForPackets. +func (mr *MockPacketScannerMockRecorder) ListenForPackets(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListenForPackets", reflect.TypeOf((*MockPacketScanner)(nil).ListenForPackets), arg0) +} + // MockScanner is a mock of Scanner interface. type MockScanner struct { ctrl *gomock.Controller diff --git a/internal/mock/server/mock_server.go b/internal/mock/server/mock_server.go index 1174611..f53da20 100644 --- a/internal/mock/server/mock_server.go +++ b/internal/mock/server/mock_server.go @@ -245,3 +245,17 @@ func (mr *MockServiceMockRecorder) StreamEvents(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StreamEvents", reflect.TypeOf((*MockService)(nil).StreamEvents), arg0) } + +// UpdateMACByIP mocks base method. +func (m *MockService) UpdateMACByIP(arg0 *server.Server) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateMACByIP", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateMACByIP indicates an expected call of UpdateMACByIP. +func (mr *MockServiceMockRecorder) UpdateMACByIP(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateMACByIP", reflect.TypeOf((*MockService)(nil).UpdateMACByIP), arg0) +} diff --git a/internal/server/interface.go b/internal/server/interface.go index 187b2eb..a7bc8da 100644 --- a/internal/server/interface.go +++ b/internal/server/interface.go @@ -26,6 +26,7 @@ const ( // Server database model for a server type Server struct { ID string `gorm:"primaryKey"` + MAC string Status Status Hostname string IP string @@ -48,6 +49,7 @@ type Service interface { GetAllServers() ([]*Server, error) GetAllServersInNetworkTargets(targets []string) ([]*Server, error) AddOrUpdateServer(req *Server) error + UpdateMACByIP(req *Server) error MarkServerOffline(ip string) error StreamEvents(send chan *event.Event) int StopStream(id int) diff --git a/internal/server/repo.go b/internal/server/repo.go index cb839ce..d77ddde 100644 --- a/internal/server/repo.go +++ b/internal/server/repo.go @@ -45,9 +45,9 @@ func (r *SqliteRepo) GetServerByID(serverID string) (*Server, error) { // GetServerByIP returns a server from the SqliteRepo based on current ip func (r *SqliteRepo) GetServerByIP(ip string) (*Server, error) { - server := Server{IP: ip} + server := Server{} - if result := r.db.First(&server); result.Error != nil { + if result := r.db.First(&server, "ip = ?", ip); result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return nil, exception.ErrRecordNotFound } @@ -86,7 +86,7 @@ func (r *SqliteRepo) UpdateServer(server *Server) (*Server, error) { return nil, errors.New("server id cannot be empty") } - if result := r.db.Save(server); result.Error != nil { + if result := r.db.Updates(server); result.Error != nil { return nil, result.Error } diff --git a/internal/server/service.go b/internal/server/service.go index e0c9787..3c82e51 100644 --- a/internal/server/service.go +++ b/internal/server/service.go @@ -5,7 +5,6 @@ import ( "net" "sync" - "github.com/imdario/mergo" "github.com/robgonnella/ops/internal/config" "github.com/robgonnella/ops/internal/event" "github.com/robgonnella/ops/internal/exception" @@ -82,10 +81,6 @@ func (s *ServerService) GetAllServersInNetworkTargets(targets []string) ([]*Serv if err != nil { // non CIDR target just check if target matches IP if server.IP == target { - s.log.Debug(). - Str("serverIP", server.IP). - Str("target", target). - Msg("serverIP matches network target") result = append(result, server) break } @@ -105,12 +100,14 @@ func (s *ServerService) GetAllServersInNetworkTargets(targets []string) ([]*Serv } } + // s.log.Error().Interface("result", result).Msg("returning all servers in target") + return result, nil } // AddOrUpdateServer adds or updates a server func (s *ServerService) AddOrUpdateServer(req *Server) error { - currentServer, err := s.repo.GetServerByID(req.ID) + _, err := s.repo.GetServerByID(req.ID) if errors.Is(err, exception.ErrRecordNotFound) { // handle add case @@ -132,8 +129,25 @@ func (s *ServerService) AddOrUpdateServer(req *Server) error { // handle update case - mergo.Merge(req, currentServer) + updatedServer, err := s.repo.UpdateServer(req) + + if err != nil { + return err + } + + s.sendServerUpdateEvent(updatedServer) + + return nil +} + +func (s *ServerService) UpdateMACByIP(req *Server) error { + currentServer, err := s.repo.GetServerByIP(req.IP) + + if err != nil { + return err + } + req.ID = currentServer.ID updatedServer, err := s.repo.UpdateServer(req) if err != nil { diff --git a/internal/ui/component/event.go b/internal/ui/component/event.go index 2f2db61..b899358 100644 --- a/internal/ui/component/event.go +++ b/internal/ui/component/event.go @@ -24,7 +24,7 @@ func NewEventTable() *EventTable { "EVENT TYPE", "HOSTNAME", "IP", - "ID", + "MAC", "OS", "SSH", "STATUS", @@ -53,13 +53,13 @@ func (t *EventTable) UpdateTable(evt *event.Event) { status := string(payload.Status) ssh := string(payload.SshStatus) hostname := payload.Hostname - id := payload.ID + mac := payload.MAC ip := payload.IP os := payload.OS countStr := strconv.Itoa(int(t.count)) - row := []string{countStr, evtType, hostname, ip, id, os, ssh, status} + row := []string{countStr, evtType, hostname, ip, mac, os, ssh, status} rowIdx := t.table.GetRowCount() for col, text := range row { diff --git a/internal/ui/component/server.go b/internal/ui/component/server.go index 9237a30..8ed2af9 100644 --- a/internal/ui/component/server.go +++ b/internal/ui/component/server.go @@ -18,7 +18,7 @@ type ServerTable struct { // NewServerTable returns a new instance of ServerTable func NewServerTable(hostHostname, hostIP string, OnSSH func(ip string)) *ServerTable { - columnHeaders := []string{"HOSTNAME", "IP", "ID", "OS", "SSH", "STATUS"} + columnHeaders := []string{"HOSTNAME", "IP", "MAC", "OS", "SSH", "STATUS"} table := createTable("servers", columnHeaders) @@ -54,7 +54,7 @@ func (t *ServerTable) UpdateTable(servers []*server.Server) { status := string(svr.Status) ssh := string(svr.SshStatus) hostname := svr.Hostname - id := svr.ID + mac := svr.MAC ip := svr.IP os := svr.OS you := false @@ -64,7 +64,7 @@ func (t *ServerTable) UpdateTable(servers []*server.Server) { you = true } - row := []string{hostname, ip, id, os, ssh, status} + row := []string{hostname, ip, mac, os, ssh, status} for col, text := range row { if col == 0 && you {