Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add basic setup for BubbleTea UI #1

Merged
merged 9 commits into from
Jul 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 3 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
![Simple CI](https://github.com/NamelessOne91/bisturi/actions/workflows/simple_ci.yml/badge.svg)

# bisturi
A toy project network packet analyzer
A TUI network packet analyzer toy project

The compiled binary executable for bisturi will attempt to create a new raw socket and bind it to a network interface using syscalls.
This will fail unless the program is run with root privileges, which is not advisable.
Expand All @@ -12,16 +12,6 @@ This is the default behaviour of the included Makefile's **build** command.

## Usage

You can build the binary executable with the `make build` command.

The following flags are available to customize bisturi's behaviour:

| Flag | Type | Default | Meaning
| :---:|:--:|:--:|:--|
| i | string | eth0 | network interface for which the packets will be analyzed |
| p | string | all | protocl filter - 'all' equals to no filter |

Running bisturi with the provided `make run` command is functionally equivalent to running bisturi with the following flags, which are its defaults:

`bisturi -i eth0 -p all`
You can build the binary executable with the `make build` command or build & run it with `make run`.

A [Bubbletea](https://github.com/charmbracelet/bubbletea) based TUI will ask you to select a network interface and a protocol to filter for - selecting 'all' equals to having no filter.
34 changes: 5 additions & 29 deletions cmd/bisturi/main.go
Original file line number Diff line number Diff line change
@@ -1,39 +1,15 @@
package main

import (
"flag"
"log"
"net"

"github.com/NamelessOne91/bisturi/sockets"
models "github.com/NamelessOne91/bisturi/tui/models"
tea "github.com/charmbracelet/bubbletea"
)

var iface = flag.String("i", "eth0", "The network interface to listen to")
var protocol = flag.String("p", "all", "Consider only packets for this protocol")

func main() {
flag.Parse()

// retrieve the network interface
networkInterface, err := net.InterfaceByName(*iface)
if err != nil {
log.Fatalf("Failed to get interface by name: %v", err)
}

// SYS_SOCKET syscall
rs, err := sockets.NewRawSocket(*protocol)
if err != nil {
log.Fatalf("Failed to open raw socket: %v", err)
p := tea.NewProgram(models.NewBisturiModel(), tea.WithAltScreen())
if _, err := p.Run(); err != nil {
log.Fatal("Error running program:", err)
}
defer rs.Close()

// bind the socket to the network interface
rs.Bind(*networkInterface)
if err != nil {
log.Fatalf("Failed to bind socket: %v", err)
}
log.Printf("listening for %s packets on interface: %s\n", *protocol, networkInterface.Name)

// SYS_RECVFROM syscall
rs.ReadPackets()
}
31 changes: 31 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,34 @@
module github.com/NamelessOne91/bisturi

go 1.21.10

require (
github.com/charmbracelet/bubbles v0.18.0
github.com/charmbracelet/bubbletea v0.26.6
github.com/charmbracelet/lipgloss v0.11.0
)

require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/x/ansi v0.1.2 // indirect
github.com/charmbracelet/x/input v0.1.2 // indirect
github.com/charmbracelet/x/term v0.1.1 // indirect
github.com/charmbracelet/x/windows v0.1.2 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/evertras/bubble-table v0.16.1 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sahilm/fuzzy v0.1.1 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect
)
61 changes: 61 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0=
github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw=
github.com/charmbracelet/bubbletea v0.26.6 h1:zTCWSuST+3yZYZnVSvbXwKOPRSNZceVeqpzOLN2zq1s=
github.com/charmbracelet/bubbletea v0.26.6/go.mod h1:dz8CWPlfCCGLFbBlTY4N7bjLiyOGDJEnd2Muu7pOWhk=
github.com/charmbracelet/lipgloss v0.11.0 h1:UoAcbQ6Qml8hDwSWs0Y1cB5TEQuZkDPH/ZqwWWYTG4g=
github.com/charmbracelet/lipgloss v0.11.0/go.mod h1:1UdRTH9gYgpcdNN5oBtjbu/IzNKtzVtb7sqN1t9LNn8=
github.com/charmbracelet/x/ansi v0.1.2 h1:6+LR39uG8DE6zAmbu023YlqjJHkYXDF1z36ZwzO4xZY=
github.com/charmbracelet/x/ansi v0.1.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/charmbracelet/x/input v0.1.2 h1:QJAZr33eOhDowkkEQ24rsJy4Llxlm+fRDf/cQrmqJa0=
github.com/charmbracelet/x/input v0.1.2/go.mod h1:LGBim0maUY4Pitjn/4fHnuXb4KirU3DODsyuHuXdOyA=
github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI=
github.com/charmbracelet/x/term v0.1.1/go.mod h1:wB1fHt5ECsu3mXYusyzcngVWWlu1KKUmmLhfgr/Flxw=
github.com/charmbracelet/x/windows v0.1.2 h1:Iumiwq2G+BRmgoayww/qfcvof7W/3uLoelhxojXlRWg=
github.com/charmbracelet/x/windows v0.1.2/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/evertras/bubble-table v0.16.1 h1:RKkOD+6LUoA3SifWceTSE7zchKyhBZy0f4B/K1/XN0o=
github.com/evertras/bubble-table v0.16.1/go.mod h1:SPOZKbIpyYWPHBNki3fyNpiPBQkvkULAtOT7NTD5fKY=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
2 changes: 1 addition & 1 deletion protocols/eth.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,5 @@ func EthFrameFromBytes(raw []byte) (*EthernetFrame, error) {
func (f *EthernetFrame) Info() string {
etv := etherTypesValues[f.etherType]

return fmt.Sprintf("%s Ethernet Frame from MAC %s to MAC %s", f.sourceMAC, f.destinationMAC, etv)
return fmt.Sprintf("%s Ethernet Frame from MAC %s to MAC %s", etv, f.sourceMAC, f.destinationMAC)
}
21 changes: 1 addition & 20 deletions sockets/raw_socket.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package sockets

import (
"errors"
"log"
"net"
"os"
Expand All @@ -13,20 +12,6 @@ import (

const mask = 0xff00

// maps the protocol names to Ethernet protocol types values
var protocolEthernetType = map[string]uint16{
"all": syscall.ETH_P_ALL,
"arp": syscall.ETH_P_ARP,
"ip": syscall.ETH_P_IP,
"ipv6": syscall.ETH_P_IPV6,
"udp": syscall.ETH_P_IP, // UDP and TCP are part of IP, need special handling if filtered specifically
"udp6": syscall.ETH_P_IPV6,
"tcp": syscall.ETH_P_IP,
"tcp6": syscall.ETH_P_IPV6,
}

var errUnsupportedProtocol = errors.New("unsupported protocol - must be one of: all, arp, ip, ipv6, udp, udp6, tcp, tcp6")

// hostToNetworkShort converts a short (uint16) from host (usually Little Endian)
// to network (Big Endian) byte order
func hostToNetworkShort(i uint16) uint16 {
Expand All @@ -45,11 +30,7 @@ type RawSocket struct {

// NewRawSocket opens a raw socket for the specified protocol by calling SYS_SOCKET
// and returns the struct representing it, or eventual errors
func NewRawSocket(protocol string) (*RawSocket, error) {
ethType, ok := protocolEthernetType[protocol]
if !ok {
return nil, errUnsupportedProtocol
}
func NewRawSocket(protocol string, ethType uint16) (*RawSocket, error) {

rawSocket := &RawSocket{
shutdownChan: make(chan os.Signal, 1),
Expand Down
156 changes: 156 additions & 0 deletions tui/models/bisturi_model.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package tui

import (
"fmt"
"net"
"strings"

"github.com/NamelessOne91/bisturi/sockets"
"github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)

type step uint8

const (
retrieveIfaces step = iota
selectIface
selectProtocol
receivePackets
)

type errMsg error

type bisturiModel struct {
step step
spinner spinner.Model
startMenu startMenuModel
packetsTable packetsTablemodel
selectedInterface net.Interface
selectedProtocol string
selectedEthType uint16
rawSocket *sockets.RawSocket
err error
}

func NewBisturiModel() *bisturiModel {
s := spinner.New(spinner.WithSpinner(spinner.Meter))
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("#00cc99"))

return &bisturiModel{
step: retrieveIfaces,
spinner: s,
}
}

func (m bisturiModel) Init() tea.Cmd {
return tea.Batch(m.spinner.Tick, fetchInterfaces())
}

func (m bisturiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch m.step {
case retrieveIfaces:
return m.updateLoading(msg)
case selectIface, selectProtocol:
return m.updateStartMenuSelection(msg)
case receivePackets:
return m.updateReceivingPacket(msg)
default:
return m, nil
}
}

func (m *bisturiModel) updateLoading(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case errMsg:
m.err = msg
return m, tea.Quit

case networkInterfacesMsg:
m.startMenu = newStartMenuModel(msg)
m.step = selectIface

return m, nil

case tea.KeyMsg:
switch msg.String() {
case "q", "esc", "ctrl+c":
return m, tea.Quit
}
}
var cmd tea.Cmd
m.spinner, cmd = m.spinner.Update(msg)
return m, cmd
}

func (m *bisturiModel) updateStartMenuSelection(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd

m.startMenu, cmd = m.startMenu.Update(msg)

switch msg := msg.(type) {
case selectedIfaceItemMsg:
iface, err := net.InterfaceByName(msg.name)
if err != nil {
m.err = err
return m, tea.Quit
}
m.selectedInterface = *iface
m.step = selectProtocol

return m, nil

case selectedProtocolItemMsg:
// SYS_SOCKET syscall
rs, err := sockets.NewRawSocket(msg.name, msg.ethType)
if err != nil {
return m, tea.Quit
}
// bind the socket to the network interface
err = rs.Bind(m.selectedInterface)
if err != nil {
m.err = err
return m, tea.Quit
}
m.selectedProtocol = msg.name
m.selectedEthType = msg.ethType
m.rawSocket = rs
m.step = receivePackets
m.packetsTable = newPacketsTable()

return m, nil
}
return m, cmd
}

func (m *bisturiModel) updateReceivingPacket(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd

m.packetsTable, cmd = m.packetsTable.Update(msg)

return m, cmd
}

func (m bisturiModel) View() string {
if m.err != nil {
if m.rawSocket != nil {
m.rawSocket.Close()
}
return fmt.Sprintf("Error: %s\n", m.err)
}

sb := strings.Builder{}
switch m.step {
case retrieveIfaces:
sb.WriteString(fmt.Sprintf("\n\n %s Retrieving network interfaces...\n\n", m.spinner.View()))
case selectIface, selectProtocol:
sb.WriteString(m.startMenu.View())
case receivePackets:
sb.WriteString(fmt.Sprintf("Receiving %s packets on %s ...\n", m.selectedProtocol, m.selectedInterface.Name))
sb.WriteString(m.packetsTable.View())
default:
sb.WriteString("The program is in an unkowqn state\n")
}
return sb.String()
}
Loading
Loading