├── main.go ├── pkg ├── style │ └── style.go └── models │ ├── errorlog │ └── errorlog.go │ ├── button │ └── button.go │ ├── interface-list │ └── Interface-list.go │ ├── interface-item │ └── interface-item.go │ └── filter │ └── filter.go ├── README.md ├── go.mod ├── LICENSE ├── cmd └── gosniff │ ├── keybindings.go │ ├── model.go │ ├── view.go │ └── update.go └── go.sum /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/c-grimshaw/gosniff/cmd/gosniff" 7 | tea "github.com/charmbracelet/bubbletea" 8 | ) 9 | 10 | func main() { 11 | p := tea.NewProgram(gosniff.NewModel(), tea.WithAltScreen(), tea.WithMouseCellMotion()) 12 | if err := p.Start(); err != nil { 13 | log.Fatalf("Alas, there's been an error: %v", err) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /pkg/style/style.go: -------------------------------------------------------------------------------- 1 | package style 2 | 3 | import "github.com/charmbracelet/lipgloss" 4 | 5 | var ( 6 | // Focused styles a focused item 7 | Focused = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("205")) 8 | 9 | // Placeholder styles a faded item 10 | Placeholder = lipgloss.NewStyle().Foreground(lipgloss.Color("240")) 11 | 12 | // None provides default styling 13 | None = lipgloss.NewStyle() 14 | ) 15 | -------------------------------------------------------------------------------- /pkg/models/errorlog/errorlog.go: -------------------------------------------------------------------------------- 1 | package errorlog 2 | 3 | import ( 4 | "fmt" 5 | 6 | tea "github.com/charmbracelet/bubbletea" 7 | ) 8 | 9 | // Model is the error log model struct 10 | type Model struct { 11 | content string 12 | } 13 | 14 | // New returns an error log model with default params 15 | func New() Model { 16 | return Model{} 17 | } 18 | 19 | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { 20 | return m, nil 21 | } 22 | 23 | func (m Model) View() string { 24 | return fmt.Sprintf("%s", m.content) 25 | } 26 | 27 | func (m *Model) SetContent(s string) { 28 | m.content = s 29 | } 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## GOSNIFF - A Textual User-Interface Network Sniffer 2 | 3 | 4 | 5 | https://user-images.githubusercontent.com/23296141/165829682-2fd4c2c6-af96-4a6a-818f-2e5e5cfb9104.mp4 6 | 7 | 8 | 9 | **GOSNIFF** is a TUI-based, *tcpdump*-inspired tool used to provide some graphical insight into networking on your computer. 10 | 11 | Currently, it supports 12 | [Berkeley Packet Filter (BPF) syntax](https://biot.com/capstats/bpf.html), identical to filtering syntax in tools like Wireshark or, y'know, tcpdump. 13 | 14 | [![Go Report Card](https://goreportcard.com/badge/github.com/c-grimshaw/gosniff)](https://goreportcard.com/report/github.com/c-grimshaw/gosniff) 15 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/c-grimshaw/gosniff 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/charmbracelet/bubbles v0.10.3 7 | github.com/charmbracelet/bubbletea v0.20.0 8 | github.com/charmbracelet/lipgloss v0.5.0 9 | github.com/google/gopacket v1.1.19 10 | ) 11 | 12 | require ( 13 | github.com/atotto/clipboard v0.1.4 // indirect 14 | github.com/containerd/console v1.0.3 // indirect 15 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 16 | github.com/mattn/go-isatty v0.0.14 // indirect 17 | github.com/mattn/go-runewidth v0.0.13 // indirect 18 | github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect 19 | github.com/muesli/reflow v0.3.0 // indirect 20 | github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 // indirect 21 | github.com/rivo/uniseg v0.2.0 // indirect 22 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect 23 | golang.org/x/term v0.0.0-20210422114643-f5beecf764ed // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 c-grimshaw 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pkg/models/button/button.go: -------------------------------------------------------------------------------- 1 | package button 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/c-grimshaw/gosniff/pkg/style" 7 | tea "github.com/charmbracelet/bubbletea" 8 | ) 9 | 10 | // Model is the button model struct 11 | type Model struct { 12 | name string 13 | focused bool 14 | } 15 | 16 | // New returns a button model with default parameters 17 | func New(name string) Model { 18 | return Model{ 19 | name: name, 20 | } 21 | } 22 | 23 | // Update contains the button's update loop, which currently does nothing 24 | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { 25 | return m, nil 26 | } 27 | 28 | // View renders the button into a text string 29 | func (m Model) View() string { 30 | if m.Focused() { 31 | return style.Focused.Render(fmt.Sprintf("[ %s ]", m.name)) 32 | } 33 | return fmt.Sprintf("[ %s ]", m.name) 34 | } 35 | 36 | // Focused returns the focus state of the button 37 | func (m Model) Focused() bool { 38 | return m.focused 39 | } 40 | 41 | // SetName changes the visible display of the button 42 | func (m *Model) SetName(name string) { 43 | m.name = name 44 | } 45 | 46 | // SetFocus changes the focus state of the button 47 | func (m *Model) SetFocus(focus bool) { 48 | m.focused = focus 49 | } 50 | -------------------------------------------------------------------------------- /pkg/models/interface-list/Interface-list.go: -------------------------------------------------------------------------------- 1 | package interfacelist 2 | 3 | import ( 4 | "fmt" 5 | 6 | interfaceitem "github.com/c-grimshaw/gosniff/pkg/models/interface-item" 7 | "github.com/charmbracelet/lipgloss" 8 | "github.com/google/gopacket/pcap" 9 | ) 10 | 11 | // Model defines the structure of the component 12 | type Model struct { 13 | cursor, checked int 14 | interfaces []interfaceitem.Model 15 | } 16 | 17 | // New returns a list of network interfaces with default parameters 18 | func New(cursor, checked string) Model { 19 | interfaces, err := getInterfaces() 20 | if err != nil { 21 | panic(err) 22 | } 23 | 24 | interfaceList := make([]interfaceitem.Model, len(interfaces)) 25 | for i, iface := range interfaces { 26 | interfaceList[i] = interfaceitem.New(iface, cursor, checked) 27 | } 28 | return Model{ 29 | interfaces: interfaceList, 30 | } 31 | } 32 | 33 | // View describes the string representation of the network interface list 34 | func (m Model) View() (view string) { 35 | view = "Interface:" 36 | for _, item := range m.interfaces { 37 | view = lipgloss.JoinVertical(lipgloss.Left, view, item.View()) 38 | } 39 | return view 40 | } 41 | 42 | // getInterfaces returns all host interfaces in string format 43 | func getInterfaces() (interfaces []pcap.Interface, err error) { 44 | interfaces, err = pcap.FindAllDevs() 45 | if err != nil { 46 | fmt.Println("Error: No host interfaces") 47 | return interfaces, err 48 | } 49 | return interfaces, nil 50 | } 51 | -------------------------------------------------------------------------------- /cmd/gosniff/keybindings.go: -------------------------------------------------------------------------------- 1 | package gosniff 2 | 3 | import "github.com/charmbracelet/bubbles/key" 4 | 5 | // KeyMap contains a list of key bindings 6 | type KeyMap struct { 7 | Up key.Binding 8 | Down key.Binding 9 | Left key.Binding 10 | Right key.Binding 11 | Exit key.Binding 12 | Enter key.Binding 13 | Help key.Binding 14 | } 15 | 16 | // ShortHelp returns keybindings to be shown in the mini help view 17 | func (k KeyMap) ShortHelp() []key.Binding { 18 | return []key.Binding{k.Help} 19 | } 20 | 21 | // FullHelp returns keybindings for the expanded help view 22 | func (k KeyMap) FullHelp() [][]key.Binding { 23 | return [][]key.Binding{ 24 | {k.Up, k.Down, k.Help}, // first column 25 | {k.Exit, k.Enter}, // second column 26 | } 27 | } 28 | 29 | // DefaultKeyMap describes the default key bindings 30 | var DefaultKeyMap = KeyMap{ 31 | Up: key.NewBinding( 32 | key.WithKeys("k", "up", "left", "shift+tab"), // actual keybindings 33 | key.WithHelp("↑/k", "Move up"), // corresponding help text 34 | ), 35 | Down: key.NewBinding( 36 | key.WithKeys("j", "down", "right", "tab"), 37 | key.WithHelp("↓/j", "Move down"), 38 | ), 39 | Exit: key.NewBinding( 40 | key.WithKeys("q", "ctrl+c"), 41 | key.WithHelp("q/ctrl-c", "Exit program"), 42 | ), 43 | Help: key.NewBinding( 44 | key.WithKeys("?"), 45 | key.WithHelp("?", "Toggle help"), 46 | ), 47 | Enter: key.NewBinding( 48 | key.WithKeys("enter", " "), 49 | key.WithHelp("enter/spacebar", "Check/Uncheck box"), 50 | ), 51 | } 52 | -------------------------------------------------------------------------------- /pkg/models/interface-item/interface-item.go: -------------------------------------------------------------------------------- 1 | package interfaceitem 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/c-grimshaw/gosniff/pkg/style" 7 | tea "github.com/charmbracelet/bubbletea" 8 | "github.com/charmbracelet/lipgloss" 9 | "github.com/google/gopacket/pcap" 10 | ) 11 | 12 | // Model is the error log model struct 13 | type Model struct { 14 | id int 15 | name, cursor, checked string 16 | addresses []string 17 | focused bool 18 | } 19 | 20 | // New returns an error log model with default params 21 | func New(i pcap.Interface, cursor, checked string) Model { 22 | return Model{ 23 | name: i.Name, 24 | addresses: getAddresses(i.Addresses), 25 | } 26 | } 27 | 28 | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { 29 | return m, nil 30 | } 31 | 32 | func (m Model) View() string { 33 | var view string 34 | if m.focused { 35 | view = style.Focused.Render(fmt.Sprintf("%s [%s] %s", m.cursor, m.checked, m.name)) 36 | } else { 37 | view = fmt.Sprintf("%s [%s] %s", m.cursor, " ", m.name) 38 | } 39 | 40 | for _, addr := range m.addresses { 41 | view = lipgloss.JoinVertical(lipgloss.Left, 42 | view, 43 | style.Placeholder.Render(fmt.Sprintf(" - [%s]", addr)), 44 | ) 45 | } 46 | return view 47 | } 48 | 49 | func (m *Model) SetFocus(state bool) { 50 | m.focused = state 51 | } 52 | 53 | // getAddresses returns all IPs associated with a given interface 54 | func getAddresses(addrs []pcap.InterfaceAddress) []string { 55 | addresses := make([]string, len(addrs)) 56 | for i, addr := range addrs { 57 | addresses[i] = addr.IP.String() 58 | } 59 | return addresses 60 | } 61 | -------------------------------------------------------------------------------- /cmd/gosniff/model.go: -------------------------------------------------------------------------------- 1 | package gosniff 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/c-grimshaw/gosniff/pkg/models/button" 8 | "github.com/c-grimshaw/gosniff/pkg/models/errorlog" 9 | "github.com/c-grimshaw/gosniff/pkg/models/filter" 10 | "github.com/charmbracelet/bubbles/help" 11 | "github.com/charmbracelet/bubbles/viewport" 12 | tea "github.com/charmbracelet/bubbletea" 13 | "github.com/google/gopacket" 14 | "github.com/google/gopacket/pcap" 15 | ) 16 | 17 | type model struct { 18 | interfaces []pcap.Interface 19 | selected, focus, inputs int 20 | recording bool 21 | content string 22 | keys KeyMap 23 | help help.Model 24 | submit button.Model 25 | clear button.Model 26 | filter filter.Model 27 | errorLog errorlog.Model 28 | viewport viewport.Model 29 | packetChan chan (gopacket.Packet) 30 | stopChan chan (struct{}) 31 | } 32 | 33 | // Init contains initial I/O commands executed by the model 34 | func (m model) Init() tea.Cmd { 35 | return tea.Batch( 36 | waitForPacket(m.packetChan), 37 | waitForStop(m.stopChan), 38 | ) 39 | } 40 | 41 | // NewModel returns a gosniff model with default parameters 42 | func NewModel() *model { 43 | interfaces, err := GetInterfaces() 44 | if err != nil { 45 | fmt.Println("Error: ", err) 46 | os.Exit(1) 47 | } 48 | 49 | submit, clear := button.New("Start"), button.New("Clear") 50 | 51 | help := help.New() 52 | help.ShowAll = true 53 | 54 | return &model{ 55 | interfaces: interfaces, 56 | keys: DefaultKeyMap, 57 | help: help, 58 | submit: submit, 59 | clear: clear, 60 | errorLog: errorlog.New(), 61 | filter: filter.New(), 62 | viewport: viewport.New(80, 30), 63 | packetChan: make(chan gopacket.Packet), 64 | stopChan: make(chan struct{}), 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /pkg/models/filter/filter.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "github.com/c-grimshaw/gosniff/pkg/style" 5 | "github.com/charmbracelet/bubbles/textinput" 6 | tea "github.com/charmbracelet/bubbletea" 7 | "github.com/charmbracelet/lipgloss" 8 | ) 9 | 10 | // Model is the filter model struct 11 | type Model struct { 12 | textinput textinput.Model 13 | focused bool 14 | } 15 | 16 | // New returns a filter model with default parameters 17 | func New() Model { 18 | ti := textinput.New() 19 | ti.Placeholder = "tcp and port 80" 20 | ti.CharLimit = 156 21 | ti.Width = 40 22 | 23 | return Model{textinput: ti} 24 | } 25 | 26 | // Init contains commands that are executed upon model initialization 27 | func (m Model) Init() tea.Cmd { 28 | return m.textinput.SetCursorMode(textinput.CursorBlink) 29 | } 30 | 31 | // Update contains the filter's update loop, which currently checks for focus 32 | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { 33 | if m.Focused() { 34 | m.textinput.Focus() 35 | m.textinput.PromptStyle = style.Focused 36 | m.textinput.TextStyle = style.Focused 37 | } else { 38 | m.textinput.Blur() 39 | m.textinput.PromptStyle = style.None 40 | m.textinput.TextStyle = style.None 41 | } 42 | 43 | var cmd tea.Cmd 44 | m.textinput, cmd = m.textinput.Update(msg) 45 | return m, cmd 46 | } 47 | 48 | // View renders the filter into a text string 49 | func (m Model) View() string { 50 | view := "Filter:" 51 | if m.Focused() || len(m.Value()) > 0 { 52 | return lipgloss.JoinVertical(lipgloss.Left, view, style.Focused.Render(m.textinput.View())) 53 | } 54 | return lipgloss.JoinVertical(lipgloss.Left, view, m.textinput.View()) 55 | } 56 | 57 | // Value returns the content of the filter as a string 58 | func (m Model) Value() string { 59 | return m.textinput.Value() 60 | } 61 | 62 | // SetFocus sets the focus state of the model 63 | func (m *Model) SetFocus(state bool) { 64 | m.focused = state 65 | } 66 | 67 | // Focused returns the focus state of the model 68 | func (m Model) Focused() bool { 69 | return m.focused 70 | } 71 | -------------------------------------------------------------------------------- /cmd/gosniff/view.go: -------------------------------------------------------------------------------- 1 | package gosniff 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/charmbracelet/lipgloss" 8 | ) 9 | 10 | var ( 11 | noStyle = lipgloss.NewStyle() 12 | focusedStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("205")) 13 | placeholderStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240")) 14 | viewportStyle = lipgloss.NewStyle().BorderStyle(lipgloss.RoundedBorder()).Padding(0, 1) 15 | ) 16 | 17 | func (m *model) View() string { 18 | var view strings.Builder 19 | buttonInputs := lipgloss.JoinVertical(lipgloss.Center, 20 | lipgloss.JoinHorizontal(lipgloss.Center, m.submit.View(), " ", m.clear.View()), 21 | gap(1), 22 | m.helpView()) 23 | view.WriteString( 24 | lipgloss.JoinVertical( 25 | lipgloss.Left, 26 | m.titleView(), 27 | gap(1), 28 | m.interfaceView(), 29 | gap(1), 30 | m.filter.View(), 31 | gap(1), 32 | buttonInputs), 33 | ) 34 | 35 | block := noStyle.MaxWidth(100).Render(view.String()) 36 | viewport := lipgloss.JoinVertical(lipgloss.Center, noStyle.MaxWidth(80).Render(m.viewportView()), m.errorLog.View()) 37 | return lipgloss.JoinHorizontal(lipgloss.Left, block, viewport) 38 | } 39 | 40 | func (m *model) titleView() (view string) { 41 | return "//GOSNIFF//" 42 | } 43 | 44 | func (m *model) interfaceView() (view string) { 45 | view = "Interface:" 46 | for i, choice := range m.interfaces { 47 | cursor := " " 48 | if m.focus == i && m.focusedInterfaces() { 49 | cursor = ">" 50 | } 51 | 52 | row := fmt.Sprintf("%s [ ] %s", cursor, choice.Description) 53 | if m.selected == i { 54 | row = focusedStyle.Render(fmt.Sprintf("%s [x] %s", cursor, choice.Description)) 55 | } 56 | 57 | view = lipgloss.JoinVertical(lipgloss.Left, view, row) 58 | for _, addr := range choice.Addresses { 59 | view = lipgloss.JoinVertical(lipgloss.Left, view, placeholderStyle.Render(fmt.Sprintf(" - [%v]", addr.IP))) 60 | } 61 | } 62 | return view 63 | } 64 | 65 | func (m model) headerView() (view string) { 66 | view = viewportStyle.Render("GOSNIFF - STOPPED") 67 | if m.recording { 68 | view = viewportStyle.Render("GOSNIFF - RECORDING") 69 | } 70 | line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(view))) 71 | return lipgloss.JoinHorizontal(lipgloss.Center, view, line) 72 | } 73 | 74 | func (m model) footerView() (view string) { 75 | view = viewportStyle.Render(fmt.Sprintf("%3.f%%", m.viewport.ScrollPercent()*100)) 76 | line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(view))) 77 | return lipgloss.JoinHorizontal(lipgloss.Center, line, view) 78 | } 79 | 80 | func (m model) helpView() string { 81 | return m.help.View(m.keys) 82 | } 83 | 84 | func (m model) viewportView() string { 85 | return fmt.Sprintf("%s\n%s\n%s", m.headerView(), m.viewport.View(), m.footerView()) 86 | } 87 | 88 | // gap is a helper function for layout 89 | func gap(n int) string { 90 | return strings.Repeat("\n", n) 91 | } 92 | -------------------------------------------------------------------------------- /cmd/gosniff/update.go: -------------------------------------------------------------------------------- 1 | package gosniff 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/charmbracelet/bubbles/key" 7 | tea "github.com/charmbracelet/bubbletea" 8 | "github.com/google/gopacket" 9 | "github.com/google/gopacket/pcap" 10 | ) 11 | 12 | var ( 13 | snaplen = int32(1600) 14 | promisc = false 15 | timeout = pcap.BlockForever 16 | ) 17 | 18 | // GetInterfaces returns all host interfaces in string format 19 | func GetInterfaces() (interfaces []pcap.Interface, err error) { 20 | interfaces, err = pcap.FindAllDevs() 21 | if err != nil { 22 | fmt.Println("Error: No host interfaces") 23 | return interfaces, err 24 | } 25 | return interfaces, nil 26 | } 27 | 28 | func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 29 | var ( 30 | cmd tea.Cmd 31 | cmds []tea.Cmd 32 | ) 33 | 34 | switch msg := msg.(type) { 35 | case packetMsg: 36 | m.content += msg.String() + "\n" 37 | m.viewport.SetContent(m.content) 38 | m.viewport.GotoBottom() 39 | return m, waitForPacket(m.packetChan) 40 | 41 | case stopMsg: 42 | m.recording = false 43 | m.stopChan <- struct{}{} 44 | return m, waitForStop(m.stopChan) 45 | 46 | case tea.WindowSizeMsg: 47 | m.help.Width = msg.Width 48 | 49 | case tea.KeyMsg: 50 | switch { 51 | case key.Matches(msg, DefaultKeyMap.Exit): 52 | return m, tea.Quit 53 | case key.Matches(msg, DefaultKeyMap.Up): 54 | m.cursorUp() 55 | case key.Matches(msg, DefaultKeyMap.Down): 56 | m.cursorDown() 57 | case key.Matches(msg, DefaultKeyMap.Enter): 58 | m.handleEnter() 59 | case key.Matches(msg, DefaultKeyMap.Help): 60 | if !(m.filter.Focused()) { 61 | m.help.ShowAll = !m.help.ShowAll 62 | } 63 | } 64 | } 65 | 66 | m.viewport, cmd = m.viewport.Update(msg) 67 | cmds = append(cmds, cmd) 68 | 69 | m.filter, cmd = m.filter.Update(msg) 70 | cmds = append(cmds, cmd) 71 | 72 | return m, tea.Batch(cmds...) 73 | } 74 | 75 | // packetMsg is a data message sent from the packet filter 76 | type packetMsg string 77 | 78 | func (p packetMsg) String() string { return string(p) } 79 | 80 | // stopMsg is a control message sent to stop the packet filter 81 | type stopMsg struct{} 82 | 83 | // start is used to turn on the packet filter with user-specified inputs 84 | func (m *model) listenForPackets() { 85 | iface := m.interfaces[m.selected].Name 86 | handle, err := pcap.OpenLive(iface, snaplen, promisc, timeout) 87 | if err != nil { 88 | m.errorLog.SetContent("Error: Invalid interface!") 89 | return 90 | } 91 | defer handle.Close() 92 | 93 | if err := handle.SetBPFFilter(m.filter.Value()); err != nil { 94 | m.errorLog.SetContent("Error: Invalid filter!") 95 | return 96 | } 97 | 98 | source := gopacket.NewPacketSource(handle, handle.LinkType()) 99 | for { 100 | select { 101 | case packet := <-source.Packets(): 102 | m.packetChan <- packet 103 | case <-m.stopChan: 104 | return 105 | } 106 | } 107 | } 108 | 109 | // cursorUp moves the cursor up under the interfaces input 110 | func (m *model) cursorUp() { 111 | m.focus = mod(m.focus-1, len(m.interfaces)+3) 112 | m.checkFocus() 113 | } 114 | 115 | // cursorDown moves the cursor down under the interfaces input 116 | func (m *model) cursorDown() { 117 | m.focus = mod(m.focus+1, len(m.interfaces)+3) 118 | m.checkFocus() 119 | } 120 | 121 | // handleEnter controls enter behaviour over input fields 122 | func (m *model) handleEnter() { 123 | switch m.focus { 124 | 125 | // cursor over filter 126 | case m.focusedFilterIdx(): 127 | break 128 | 129 | // cursor over submit 130 | case m.focusedSubmitIdx(): 131 | if !m.recording { 132 | m.recording = true 133 | m.submit.SetName("Stop") 134 | go m.listenForPackets() 135 | } else { 136 | m.submit.SetName("Start") 137 | m.stopChan <- struct{}{} 138 | } 139 | 140 | // cursor over clear 141 | case m.focusedClearIdx(): 142 | m.clearViewport() 143 | 144 | // default case: cursor over interfaces 145 | default: 146 | m.selected = m.focus 147 | } 148 | } 149 | 150 | func (m *model) clearViewport() { 151 | m.content = "" 152 | m.viewport.SetContent(m.content) 153 | } 154 | 155 | func (m *model) checkFocus() { 156 | switch m.focus { 157 | case m.focusedFilterIdx(): 158 | m.filter.SetFocus(true) 159 | m.submit.SetFocus(false) 160 | m.clear.SetFocus(false) 161 | case m.focusedSubmitIdx(): 162 | m.filter.SetFocus(false) 163 | m.submit.SetFocus(true) 164 | m.clear.SetFocus(false) 165 | case m.focusedClearIdx(): 166 | m.filter.SetFocus(false) 167 | m.submit.SetFocus(false) 168 | m.clear.SetFocus(true) 169 | default: 170 | m.filter.SetFocus(false) 171 | m.submit.SetFocus(false) 172 | m.clear.SetFocus(false) 173 | } 174 | } 175 | 176 | // waitForPacket is a listener that sends received packets to the main model for display 177 | // in the viewport component 178 | func waitForPacket(packet chan gopacket.Packet) tea.Cmd { 179 | return func() tea.Msg { 180 | return packetMsg((<-packet).String()) 181 | } 182 | } 183 | 184 | // waitForStop is a listener that emits a stopMsg when the recording is stopped 185 | func waitForStop(stop chan struct{}) tea.Cmd { 186 | return func() tea.Msg { 187 | <-stop 188 | return stopMsg{} 189 | } 190 | } 191 | 192 | func (m *model) focusedFilterIdx() int { return len(m.interfaces) } 193 | func (m *model) focusedSubmitIdx() int { return len(m.interfaces) + 1 } 194 | func (m *model) focusedClearIdx() int { return len(m.interfaces) + 2 } 195 | 196 | func (m *model) focusedInterfaces() bool { return m.focus < len(m.interfaces) } 197 | func (m *model) focusedSubmit() bool { return m.focus == len(m.interfaces)+1 } 198 | func (m *model) focusedClear() bool { return m.focus == len(m.interfaces)+2 } 199 | 200 | func mod(x, m int) int { 201 | return (x%m + m) % m 202 | } 203 | 204 | func max(a, b int) int { 205 | if a > b { 206 | return a 207 | } 208 | return b 209 | } 210 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= 2 | github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= 3 | github.com/charmbracelet/bubbles v0.10.3 h1:fKarbRaObLn/DCsZO4Y3vKCwRUzynQD9L+gGev1E/ho= 4 | github.com/charmbracelet/bubbles v0.10.3/go.mod h1:jOA+DUF1rjZm7gZHcNyIVW+YrBPALKfpGVdJu8UiJsA= 5 | github.com/charmbracelet/bubbletea v0.19.3/go.mod h1:VuXF2pToRxDUHcBUcPmCRUHRvFATM4Ckb/ql1rBl3KA= 6 | github.com/charmbracelet/bubbletea v0.20.0 h1:/b8LEPgCbNr7WWZ2LuE/BV1/r4t5PyYJtDb+J3vpwxc= 7 | github.com/charmbracelet/bubbletea v0.20.0/go.mod h1:zpkze1Rioo4rJELjRyGlm9T2YNou1Fm4LIJQSa5QMEM= 8 | github.com/charmbracelet/harmonica v0.1.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= 9 | github.com/charmbracelet/lipgloss v0.4.0/go.mod h1:vmdkHvce7UzX6xkyf4cca8WlwdQ5RQr8fzta+xl7BOM= 10 | github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8= 11 | github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs= 12 | github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ= 13 | github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= 14 | github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= 15 | github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= 16 | github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= 17 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 18 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 19 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 20 | github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 21 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 22 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 23 | github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 24 | github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 25 | github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= 26 | github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 27 | github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= 28 | github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= 29 | github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ= 30 | github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= 31 | github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= 32 | github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw= 33 | github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= 34 | github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 h1:QANkGiGr39l1EESqrE0gZw0/AJNYzIvoGLhIoVYtluI= 35 | github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= 36 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 37 | github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 38 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 39 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 40 | github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= 41 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 42 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 43 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 44 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 45 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 46 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= 47 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 48 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 49 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 50 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 51 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 52 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 53 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 54 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 55 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= 56 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 57 | golang.org/x/term v0.0.0-20210422114643-f5beecf764ed h1:Ei4bQjjpYUsS4efOUz+5Nz++IVkHk87n2zBA0NxBWc0= 58 | golang.org/x/term v0.0.0-20210422114643-f5beecf764ed/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 59 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 60 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 61 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 62 | --------------------------------------------------------------------------------