Skip to content

Commit 6d6d057

Browse files
authored
Merge pull request #6 from karol-broda/feat/reverse-dns-lookup
2 parents fd4c550 + c58f2a2 commit 6d6d057

File tree

10 files changed

+324
-54
lines changed

10 files changed

+324
-54
lines changed

cmd/cli_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,8 @@ func resetGlobalFlags() {
364364
filterIPv4 = false
365365
filterIPv6 = false
366366
colorMode = "auto"
367-
numeric = false
367+
resolveAddrs = true
368+
resolvePorts = false
368369
}
369370

370371
// TestEnvironmentVariables tests that environment variables are properly handled

cmd/ls.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ var (
3030
sortBy string
3131
fields string
3232
colorMode string
33-
numeric bool
33+
resolveAddrs bool
34+
resolvePorts bool
3435
plainOutput bool
3536
)
3637

@@ -51,7 +52,7 @@ Available filters:
5152
}
5253

5354
func runListCommand(outputFormat string, args []string) {
54-
rt, err := NewRuntime(args, colorMode, numeric)
55+
rt, err := NewRuntime(args, colorMode)
5556
if err != nil {
5657
log.Fatal(err)
5758
}
@@ -98,14 +99,18 @@ func getFieldMap(c collector.Connection) map[string]string {
9899
lport := strconv.Itoa(c.Lport)
99100
rport := strconv.Itoa(c.Rport)
100101

101-
// Apply name resolution if not in numeric mode
102-
if !numeric {
102+
// apply address resolution
103+
if resolveAddrs {
103104
if resolvedLaddr := resolver.ResolveAddr(c.Laddr); resolvedLaddr != c.Laddr {
104105
laddr = resolvedLaddr
105106
}
106107
if resolvedRaddr := resolver.ResolveAddr(c.Raddr); resolvedRaddr != c.Raddr && c.Raddr != "*" && c.Raddr != "" {
107108
raddr = resolvedRaddr
108109
}
110+
}
111+
112+
// apply port resolution
113+
if resolvePorts {
109114
if resolvedLport := resolver.ResolvePort(c.Lport, c.Proto); resolvedLport != strconv.Itoa(c.Lport) {
110115
lport = resolvedLport
111116
}
@@ -395,7 +400,8 @@ func init() {
395400
lsCmd.Flags().StringVarP(&sortBy, "sort", "s", cfg.Defaults.SortBy, "Sort by column (e.g., pid:desc)")
396401
lsCmd.Flags().StringVarP(&fields, "fields", "f", strings.Join(cfg.Defaults.Fields, ","), "Comma-separated list of fields to show")
397402
lsCmd.Flags().StringVar(&colorMode, "color", cfg.Defaults.Color, "Color mode (auto, always, never)")
398-
lsCmd.Flags().BoolVarP(&numeric, "numeric", "n", cfg.Defaults.Numeric, "Don't resolve hostnames")
403+
lsCmd.Flags().BoolVar(&resolveAddrs, "resolve-addrs", !cfg.Defaults.Numeric, "Resolve IP addresses to hostnames")
404+
lsCmd.Flags().BoolVar(&resolvePorts, "resolve-ports", false, "Resolve port numbers to service names")
399405
lsCmd.Flags().BoolVarP(&plainOutput, "plain", "p", false, "Plain output (parsable, no styling)")
400406

401407
// shared filter flags

cmd/root.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ func init() {
4444
cfg := config.Get()
4545
rootCmd.Flags().StringVar(&topTheme, "theme", cfg.Defaults.Theme, "Theme for TUI (dark, light, mono, auto)")
4646
rootCmd.Flags().DurationVarP(&topInterval, "interval", "i", 0, "Refresh interval (default 1s)")
47+
rootCmd.Flags().BoolVar(&topResolveAddrs, "resolve-addrs", !cfg.Defaults.Numeric, "Resolve IP addresses to hostnames")
48+
rootCmd.Flags().BoolVar(&topResolvePorts, "resolve-ports", false, "Resolve port numbers to service names")
4749

4850
// shared filter flags for root command
4951
addFilterFlags(rootCmd)

cmd/runtime.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ type Runtime struct {
2020
Connections []collector.Connection
2121

2222
// common settings
23-
ColorMode string
24-
Numeric bool
23+
ColorMode string
24+
ResolveAddrs bool
25+
ResolvePorts bool
2526
}
2627

2728
// shared filter flags - used by all commands
@@ -73,7 +74,7 @@ func FetchConnections(filters collector.FilterOptions) ([]collector.Connection,
7374
}
7475

7576
// NewRuntime creates a runtime with fetched and filtered connections.
76-
func NewRuntime(args []string, colorMode string, numeric bool) (*Runtime, error) {
77+
func NewRuntime(args []string, colorMode string) (*Runtime, error) {
7778
color.Init(colorMode)
7879

7980
filters, err := BuildFilters(args)
@@ -87,10 +88,11 @@ func NewRuntime(args []string, colorMode string, numeric bool) (*Runtime, error)
8788
}
8889

8990
return &Runtime{
90-
Filters: filters,
91-
Connections: connections,
92-
ColorMode: colorMode,
93-
Numeric: numeric,
91+
Filters: filters,
92+
Connections: connections,
93+
ColorMode: colorMode,
94+
ResolveAddrs: resolveAddrs,
95+
ResolvePorts: resolvePorts,
9496
}, nil
9597
}
9698

cmd/top.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ import (
1212

1313
// top-specific flags
1414
var (
15-
topTheme string
16-
topInterval time.Duration
15+
topTheme string
16+
topInterval time.Duration
17+
topResolveAddrs bool
18+
topResolvePorts bool
1719
)
1820

1921
var topCmd = &cobra.Command{
@@ -28,8 +30,10 @@ var topCmd = &cobra.Command{
2830
}
2931

3032
opts := tui.Options{
31-
Theme: theme,
32-
Interval: topInterval,
33+
Theme: theme,
34+
Interval: topInterval,
35+
ResolveAddrs: topResolveAddrs,
36+
ResolvePorts: topResolvePorts,
3337
}
3438

3539
// if any filter flag is set, use exclusive mode
@@ -58,6 +62,8 @@ func init() {
5862
// top-specific flags
5963
topCmd.Flags().StringVar(&topTheme, "theme", cfg.Defaults.Theme, "Theme for TUI (dark, light, mono, auto)")
6064
topCmd.Flags().DurationVarP(&topInterval, "interval", "i", time.Second, "Refresh interval")
65+
topCmd.Flags().BoolVar(&topResolveAddrs, "resolve-addrs", !cfg.Defaults.Numeric, "Resolve IP addresses to hostnames")
66+
topCmd.Flags().BoolVar(&topResolvePorts, "resolve-ports", false, "Resolve port numbers to service names")
6167

6268
// shared filter flags
6369
addFilterFlags(topCmd)

internal/tui/helpers.go

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package tui
22

33
import (
4-
"fmt"
54
"regexp"
65
"github.com/karol-broda/snitch/internal/collector"
76
"strings"
@@ -44,10 +43,3 @@ func sortFieldLabel(f collector.SortField) string {
4443
}
4544
}
4645

47-
func formatRemote(addr string, port int) string {
48-
if addr == "" || addr == "*" || port == 0 {
49-
return "-"
50-
}
51-
return fmt.Sprintf("%s:%d", addr, port)
52-
}
53-

internal/tui/keys.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,28 @@ func (m model) handleNormalKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
210210
m.showKillConfirm = true
211211
}
212212
}
213+
214+
// toggle address resolution
215+
case "n":
216+
m.resolveAddrs = !m.resolveAddrs
217+
if m.resolveAddrs {
218+
m.statusMessage = "address resolution: on"
219+
} else {
220+
m.statusMessage = "address resolution: off"
221+
}
222+
m.statusExpiry = time.Now().Add(2 * time.Second)
223+
return m, clearStatusAfter(2 * time.Second)
224+
225+
// toggle port resolution
226+
case "N":
227+
m.resolvePorts = !m.resolvePorts
228+
if m.resolvePorts {
229+
m.statusMessage = "port resolution: on"
230+
} else {
231+
m.statusMessage = "port resolution: off"
232+
}
233+
m.statusExpiry = time.Now().Add(2 * time.Second)
234+
return m, clearStatusAfter(2 * time.Second)
213235
}
214236

215237
return m, nil

internal/tui/model.go

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ type model struct {
2828
sortField collector.SortField
2929
sortReverse bool
3030

31+
// display options
32+
resolveAddrs bool // when true, resolve IP addresses to hostnames
33+
resolvePorts bool // when true, resolve port numbers to service names
34+
3135
// ui state
3236
theme *theme.Theme
3337
showHelp bool
@@ -50,14 +54,16 @@ type model struct {
5054
}
5155

5256
type Options struct {
53-
Theme string
54-
Interval time.Duration
55-
TCP bool
56-
UDP bool
57-
Listening bool
58-
Established bool
59-
Other bool
60-
FilterSet bool // true if user specified any filter flags
57+
Theme string
58+
Interval time.Duration
59+
TCP bool
60+
UDP bool
61+
Listening bool
62+
Established bool
63+
Other bool
64+
FilterSet bool // true if user specified any filter flags
65+
ResolveAddrs bool // when true, resolve IP addresses to hostnames
66+
ResolvePorts bool // when true, resolve port numbers to service names
6167
}
6268

6369
func New(opts Options) model {
@@ -102,6 +108,8 @@ func New(opts Options) model {
102108
showEstablished: showEstablished,
103109
showOther: showOther,
104110
sortField: collector.SortByLport,
111+
resolveAddrs: opts.ResolveAddrs,
112+
resolvePorts: opts.ResolvePorts,
105113
theme: theme.GetTheme(opts.Theme),
106114
interval: interval,
107115
lastRefresh: time.Now(),

internal/tui/model_test.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,3 +301,132 @@ func TestTUI_ViewRenders(t *testing.T) {
301301
}
302302
}
303303

304+
func TestTUI_ResolutionOptions(t *testing.T) {
305+
// test default resolution settings
306+
m := New(Options{Theme: "dark", Interval: time.Hour})
307+
308+
if m.resolveAddrs != false {
309+
t.Error("expected resolveAddrs to be false by default (must be explicitly set)")
310+
}
311+
if m.resolvePorts != false {
312+
t.Error("expected resolvePorts to be false by default")
313+
}
314+
315+
// test with explicit options
316+
m2 := New(Options{
317+
Theme: "dark",
318+
Interval: time.Hour,
319+
ResolveAddrs: true,
320+
ResolvePorts: true,
321+
})
322+
323+
if m2.resolveAddrs != true {
324+
t.Error("expected resolveAddrs to be true when set")
325+
}
326+
if m2.resolvePorts != true {
327+
t.Error("expected resolvePorts to be true when set")
328+
}
329+
}
330+
331+
func TestTUI_ToggleResolution(t *testing.T) {
332+
m := New(Options{Theme: "dark", Interval: time.Hour, ResolveAddrs: true})
333+
334+
if m.resolveAddrs != true {
335+
t.Fatal("expected resolveAddrs to be true initially")
336+
}
337+
338+
// toggle address resolution with 'n'
339+
newModel, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'n'}})
340+
m = newModel.(model)
341+
342+
if m.resolveAddrs != false {
343+
t.Error("expected resolveAddrs to be false after toggle")
344+
}
345+
346+
// toggle back
347+
newModel, _ = m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'n'}})
348+
m = newModel.(model)
349+
350+
if m.resolveAddrs != true {
351+
t.Error("expected resolveAddrs to be true after second toggle")
352+
}
353+
354+
// toggle port resolution with 'N'
355+
if m.resolvePorts != false {
356+
t.Fatal("expected resolvePorts to be false initially")
357+
}
358+
359+
newModel, _ = m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'N'}})
360+
m = newModel.(model)
361+
362+
if m.resolvePorts != true {
363+
t.Error("expected resolvePorts to be true after toggle")
364+
}
365+
366+
// toggle back
367+
newModel, _ = m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'N'}})
368+
m = newModel.(model)
369+
370+
if m.resolvePorts != false {
371+
t.Error("expected resolvePorts to be false after second toggle")
372+
}
373+
}
374+
375+
func TestTUI_ResolveAddrHelper(t *testing.T) {
376+
m := New(Options{Theme: "dark", Interval: time.Hour})
377+
m.resolveAddrs = false
378+
379+
// when resolution is off, should return original address
380+
addr := m.resolveAddr("192.168.1.1")
381+
if addr != "192.168.1.1" {
382+
t.Errorf("expected original address when resolution off, got %s", addr)
383+
}
384+
385+
// empty and wildcard addresses should pass through unchanged
386+
if m.resolveAddr("") != "" {
387+
t.Error("expected empty string to pass through")
388+
}
389+
if m.resolveAddr("*") != "*" {
390+
t.Error("expected wildcard to pass through")
391+
}
392+
}
393+
394+
func TestTUI_ResolvePortHelper(t *testing.T) {
395+
m := New(Options{Theme: "dark", Interval: time.Hour})
396+
m.resolvePorts = false
397+
398+
// when resolution is off, should return port number as string
399+
port := m.resolvePort(80, "tcp")
400+
if port != "80" {
401+
t.Errorf("expected '80' when resolution off, got %s", port)
402+
}
403+
404+
port = m.resolvePort(443, "tcp")
405+
if port != "443" {
406+
t.Errorf("expected '443' when resolution off, got %s", port)
407+
}
408+
}
409+
410+
func TestTUI_FormatRemoteHelper(t *testing.T) {
411+
m := New(Options{Theme: "dark", Interval: time.Hour})
412+
m.resolveAddrs = false
413+
m.resolvePorts = false
414+
415+
// empty/wildcard addresses should return dash
416+
if m.formatRemote("", 80, "tcp") != "-" {
417+
t.Error("expected dash for empty address")
418+
}
419+
if m.formatRemote("*", 80, "tcp") != "-" {
420+
t.Error("expected dash for wildcard address")
421+
}
422+
if m.formatRemote("192.168.1.1", 0, "tcp") != "-" {
423+
t.Error("expected dash for zero port")
424+
}
425+
426+
// valid address:port should format correctly
427+
result := m.formatRemote("192.168.1.1", 443, "tcp")
428+
if result != "192.168.1.1:443" {
429+
t.Errorf("expected '192.168.1.1:443', got %s", result)
430+
}
431+
}
432+

0 commit comments

Comments
 (0)