From 3bb888b8f635b2678a358f79d22931fff9c2cdbd Mon Sep 17 00:00:00 2001 From: Daniel Morandini Date: Wed, 2 Oct 2019 21:59:55 +0200 Subject: [PATCH 01/26] Add travis CI --- .travis.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..5236a18 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,5 @@ +language: go +go: +- 1.13.x + +script: make test From 5769fcf6d8f2fb2850029aaa4f2f4212acb63beb Mon Sep 17 00:00:00 2001 From: Daniel Morandini Date: Wed, 2 Oct 2019 22:00:11 +0200 Subject: [PATCH 02/26] Add 2 more badges --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 5921af9..596e811 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ [![GoDoc](https://godoc.org/github.com/booster-proj/lsaddr?status.svg)](https://godoc.org/github.com/booster-proj/lsaddr) [![Go Report Card](https://goreportcard.com/badge/github.com/booster-proj/lsaddr)](https://goreportcard.com/report/github.com/booster-proj/lsaddr) [![Release](https://img.shields.io/github/release/booster-proj/lsaddr.svg)](https://github.com/booster-proj/lsaddr/releases/latest) +[![Build Status](https://travis-ci.org/jecoz/lsaddr.svg?branch=master)](https://travis-ci.org/jecoz/lsaddr) +[![Reviewed by Hound](https://img.shields.io/badge/Reviewed_by-Hound-8E64B0.svg)](https://houndci.com) ## Before we start #### Supported OS From 17d403ff4f71c8186f08e12746312ad48eae6e6b Mon Sep 17 00:00:00 2001 From: Daniel Morandini Date: Wed, 9 Oct 2019 18:07:50 +0200 Subject: [PATCH 03/26] Add lsof package --- onf/internal/lsof/lsof.go | 141 +++++++++++++++++++++++++++++++++ onf/internal/lsof/lsof_test.go | 94 ++++++++++++++++++++++ 2 files changed, 235 insertions(+) create mode 100644 onf/internal/lsof/lsof.go create mode 100644 onf/internal/lsof/lsof_test.go diff --git a/onf/internal/lsof/lsof.go b/onf/internal/lsof/lsof.go new file mode 100644 index 0000000..b66960d --- /dev/null +++ b/onf/internal/lsof/lsof.go @@ -0,0 +1,141 @@ +// Copyright © 2019 Jecoz +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package lsof + +import ( + "fmt" + "strings" + "net" + "bytes" + "io" + "log" + "time" + "strconv" + + "github.com/jecoz/lsaddr/onf/internal" + "gopkg.in/pipe.v2" +) + +type ONF struct { + Command string + Pid int + User string + Fd string + Type string + Device string + State string // (ENSTABLISHED), (LISTEN), ... + SrcAddr net.Addr // Source address + DstAddr net.Addr // Destination address +} + +func Run() ([]ONF, error) { + acc := []ONF{} + p := pipe.Exec("lsof", "-i", "-n", "-P") + out, err := pipe.OutputTimeout(p, time.Millisecond*100) + if err != nil { + return acc, fmt.Errorf("unable to run lsof: %w", err) + } + buf := bytes.NewBuffer(out) + return ParseOutput(buf) +} + +// ParseOutput expects "r" to contain the output of +// an ``lsof -i -n -P'' call. The output is splitted into each new line, +// and each line that ``ParseONF'' is able to parse +// is appended to the final output. +// Returns an error only if reading from "r" produces an error +// different from ``io.EOF''. +func ParseOutput(r io.Reader) ([]ONF, error) { + set := []ONF{} + err := internal.ScanLines(r, func(line string) error { + onf, err := ParseONF(line) + if err != nil { + log.Printf("skipping onf \"%s\": %v", line, err) + return nil + } + set = append(set, *onf) + return nil + }) + return set, err +} + +// ParseONF expectes "line" to be a single line output from +// ``lsof -i -n -P'' call. The line is unmarshaled into an ``ONF'' +// only if is splittable by " " into a slice of at least 9 items. "line" should +// not end with a "\n" delimitator, otherwise it will end up in the last +// unmarshaled item. +// +// "line" examples: +// "postgres 676 danielmorandini 10u IPv6 0x25c5bf0997ca88e3 0t0 UDP [::1]:60051->[::1]:60051" +// "Dropbox 614 danielmorandini 247u IPv4 0x25c5bf09a393d583 0t0 TCP 192.168.0.61:58282->162.125.18.133:https (ESTABLISHED)" +func ParseONF(line string) (*ONF, error) { + chunks, err := internal.ChunkLine(line, " ", 9) + if err != nil { + return nil, err + } + pid, err := strconv.Atoi(chunks[1]) + if err != nil { + return nil, fmt.Errorf("error parsing pid: %w", err) + } + + f := &ONF{ + Command: chunks[0], + Pid: pid, + User: chunks[2], + Fd: chunks[3], + Type: chunks[4], + Device: chunks[5], + } + src, dst := ParseName(chunks[7], chunks[8]) + f.SrcAddr = src + f.DstAddr = dst + if len(chunks) >= 10 { + f.State = chunks[9] + } + + return f, nil +} + +// ParseName parses `lsof`'s name field, which by default is in the form: +// [46][protocol][@hostname|hostaddr][:service|port] +// but we're disabling hostname conversion with the ``-n'' option +// and port conversion with the ``-P'' option, so the output +// in printed in the more decodable format: ``addr:port->addr:port''. +func ParseName(node, name string) (net.Addr, net.Addr) { + chunks := strings.Split(name, "->") + if len(chunks) == 0 { + return addr{}, addr{} + } + src := addr{net: strings.ToLower(node), addr: chunks[0]} + if len(chunks) == 1 { + return src, addr{} + } + + return src, addr{net: strings.ToLower(node), addr: chunks[1]} +} + +// addr is a net.Addr implementation. +type addr struct { + addr string + net string +} + +func (a addr) String() string { + return a.addr +} + +func (a addr) Network() string { + return a.net +} diff --git a/onf/internal/lsof/lsof_test.go b/onf/internal/lsof/lsof_test.go new file mode 100644 index 0000000..66e274f --- /dev/null +++ b/onf/internal/lsof/lsof_test.go @@ -0,0 +1,94 @@ +// Copyright © 2019 Jecoz +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package lsof + +import ( + "bytes" + "testing" +) + +func TestParseONF(t *testing.T) { + t.Parallel() + + line := "Spotify 11778 danielmorandini 128u IPv4 0x25c5bf09993eff03 0t0 TCP 192.168.0.61:51291->35.186.224.47:443 (ESTABLISHED)" + onf, err := ParseONF(line) + if err != nil { + t.Fatalf("Unexpcted error: %v", err) + } + + assert(t, "Spotify", onf.Command) + assert(t, 11778, onf.Pid) + assert(t, "danielmorandini", onf.User) + assert(t, "128u", onf.Fd) + assert(t, "IPv4", onf.Type) + assert(t, "0x25c5bf09993eff03", onf.Device) + assert(t, "192.168.0.61:51291", onf.SrcAddr.String()) + assert(t, "35.186.224.47:443", onf.DstAddr.String()) + assert(t, "(ESTABLISHED)", onf.State) +} + +func assert(t *testing.T, exp, x interface{}) { + if exp != x { + t.Fatalf("Assert failed: expected %v, found %v", exp, x) + } +} + +const lsofExample = `Dropbox 614 danielmorandini 236u IPv4 0x25c5bf09a4161583 0t0 TCP 192.168.0.61:58122->162.125.66.7:https (ESTABLISHED) +Dropbox 614 danielmorandini 247u IPv4 0x25c5bf09a393d583 0t0 TCP 192.168.0.61:58282->162.125.18.133:https (ESTABLISHED) +postgres 676 danielmorandini 10u IPv6 0x25c5bf0997ca88e3 0t0 UDP [::1]:60051->[::1]:60051 +` + +func TestParseOutput(t *testing.T) { + t.Parallel() + + buf := bytes.NewBufferString(lsofExample) + onfset, err := ParseOutput(buf) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if len(onfset) != 3 { + t.Fatalf("Unexpected onfset length: wanted 3, found %d: %v", len(onfset), onfset) + } +} + +func TestParseName(t *testing.T) { + t.Parallel() + + tt := []struct { + node string + name string + src string + dst string + net string + }{ + {"TCP", "127.0.0.1:49161->127.0.01:9090", "127.0.0.1:49161", "127.0.01:9090", "tcp"}, + {"TCP", "127.0.0.1:5432", "127.0.0.1:5432", "", "tcp"}, + {"UDP", "192.168.0.61:50940->192.168.0.2:53", "192.168.0.61:50940", "192.168.0.2:53", "udp"}, + {"TCP", "[fe80:c::d5d5:601e:981b:c79d]:1024->[fe80:c::f9b9:5ecb:eeca:58e9]:1024", "[fe80:c::d5d5:601e:981b:c79d]:1024", "[fe80:c::f9b9:5ecb:eeca:58e9]:1024", "tcp"}, + } + + for i, v := range tt { + src, dst := ParseName(v.node, v.name) + if src.String() != v.src { + t.Fatalf("%d: Unexpected src: wanted %s, found %s", i, v.src, src.String()) + } + if dst.String() != v.dst { + t.Fatalf("%d: Unexpected dst: wanted %s, found %s", i, v.dst, dst.String()) + } + if src.Network() != v.net { + t.Fatalf("%d: Unexpected net: wanted %s, found %s", i, v.net, src.Network()) + } + } +} From 8f8e5203ffe24d0e9351c81529f7d772e761a888 Mon Sep 17 00:00:00 2001 From: Daniel Morandini Date: Thu, 10 Oct 2019 11:17:40 +0200 Subject: [PATCH 04/26] Add tasklist module --- onf/internal/tasklist/tasklist.go | 125 +++++++++++++++++++++++++ onf/internal/tasklist/tasklist_test.go | 95 +++++++++++++++++++ 2 files changed, 220 insertions(+) create mode 100644 onf/internal/tasklist/tasklist.go create mode 100644 onf/internal/tasklist/tasklist_test.go diff --git a/onf/internal/tasklist/tasklist.go b/onf/internal/tasklist/tasklist.go new file mode 100644 index 0000000..c1abac5 --- /dev/null +++ b/onf/internal/tasklist/tasklist.go @@ -0,0 +1,125 @@ +// Copyright © 2019 Jecoz +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package tasklist + +import ( + "log" + "fmt" + "strings" + "io" + "bytes" + "strconv" + + "github.com/jecoz/lsaddr/onf/internal" +) + +type Task struct { + Pid int + Image string +} + +// ParseOutput expects "r" to contain the output of +// a ``tasklist'' call. The output is splitted into lines, and +// each line that ``UnmarshakTasklistLine'' is able to Unmarshal is +// appended to the final output, with the expections of the first lines +// that come before the separator line composed by only "=". Those lines +// are considered part of the "header". +// +// As of ``ParseTask'', this function returns an error only +// if reading from "r" produces an error different from ``io.EOF''. +func ParseOutput(r io.Reader) ([]Task, error) { + ll := []Task{} + delim := "=" + headerTrimmed := false + segLengths := []int{} + err := internal.ScanLines(r, func(line string) error { + if !headerTrimmed { + if strings.HasPrefix(line, delim) && strings.HasSuffix(line, delim) { + headerTrimmed = true + // This is the header delimiter! + chunks, err := internal.ChunkLine(line, " ", 5) + if err != nil { + return fmt.Errorf("unexpected header format: %w", err) + } + for _, v := range chunks { + segLengths = append(segLengths, len(v)) + } + } + // Still in the header + return nil + } + + t, err := ParseTask(line, segLengths) + if err != nil { + log.Printf("skipping tasklist line \"%s\": %v", line, err) + return nil + } + ll = append(ll, *t) + return nil + }) + return ll, err +} + +// ParseTask expectes "line" to be a single line output from +// ``tasklist'' call. The line is unmarshaled into a ``Task'' and the operation +// is performed by readying bytes equal to "segLengths"[i], in order. "segLengths" +// should be computed using the header delimitator and counting the number of +// "=" in each segment of the header (split it by " ") +// +// "line" should not end with a "\n" delimitator, otherwise it will end up in the last +// unmarshaled item. +// The "header" lines (see below) should not be passed to this function. +// +// Example header: +// Image Name PID Session Name Session# Mem Usage +// ========================= ======== ================ =========== ============ +// +// Example line: +// svchost.exe 940 Services 0 52,336 K +func ParseTask(line string, segLengths []int) (*Task, error) { + buf := bytes.NewBufferString(line) + p := make([]byte, 32) + + var image, pidRaw string + for i, v := range segLengths[:2] { + n, err := buf.Read(p[:v+1]) + if err != nil { + return nil, fmt.Errorf("unable to read tasklist chunk: %w", err) + } + s := strings.Trim(string(p[:n]), " ") + switch i { + case 0: + image = s + case 1: + pidRaw = s + default: + } + } + if image == "" { + return nil, fmt.Errorf("couldn't decode image from line") + } + if pidRaw == "" { + return nil, fmt.Errorf("couldn't decode pid from line") + } + pid, err := strconv.Atoi(pidRaw) + if err != nil { + return nil, fmt.Errorf("error parsing pid: %w", err) + } + + return &Task{ + Image: image, + Pid: pid, + }, nil +} diff --git a/onf/internal/tasklist/tasklist_test.go b/onf/internal/tasklist/tasklist_test.go new file mode 100644 index 0000000..3ebb00b --- /dev/null +++ b/onf/internal/tasklist/tasklist_test.go @@ -0,0 +1,95 @@ +// Copyright © 2019 Jecoz +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package tasklist + +import ( + "bytes" + "testing" + "reflect" +) + +const tasklistExample = ` +Image Name PID Session Name Session# Mem Usage +========================= ======== ================ =========== ============ +System Idle Process 0 Services 0 4 K +System 4 Services 0 15,376 K +smss.exe 296 Services 0 1,008 K +csrss.exe 380 Services 0 4,124 K +wininit.exe 452 Services 0 4,828 K +services.exe 588 Services 0 6,284 K +lsass.exe 596 Services 0 12,600 K +svchost.exe 688 Services 0 17,788 K +svchost.exe 748 Services 0 8,980 K +svchost.exe 888 Services 0 21,052 K +svchost.exe 904 Services 0 21,200 K +svchost.exe 940 Services 0 52,336 K +WUDFHost.exe 464 Services 0 6,128 K +svchost.exe 1036 Services 0 14,524 K +svchost.exe 1044 Services 0 27,488 K +svchost.exe 1104 Services 0 28,428 K +WUDFHost.exe 1240 Services 0 6,888 K +` + +func TestParseOutput(t *testing.T) { + t.Parallel() + buf := bytes.NewBufferString(tasklistExample) + ll, err := ParseOutput(buf) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if len(ll) != 17 { + t.Fatalf("Unexpected ll length: wanted 17, found: %d: %v", len(ll), ll) + } +} + +func TestParseTask(t *testing.T) { + t.Parallel() + tt := []struct { + line string + segs []int + image string + pid int + }{ + { + line: "svchost.exe 940 Services 0 52,336 K", + segs: []int{25, 8, 16, 11, 12}, + image: "svchost.exe", + pid: 940, + }, + { + line: "System Idle Process 0 Services 0 4 K", + segs: []int{25, 8, 16, 11, 12}, + image: "System Idle Process", + pid: 0, + }, + } + + for i, v := range tt { + task, err := ParseTask(v.line, v.segs) + if err != nil { + t.Fatalf("%d: unexpected error: %v", i, err) + } + assert(t, v.image, task.Image) + assert(t, v.pid, task.Pid) + } +} + +func assert(t *testing.T, exp, x interface{}) { + if !reflect.DeepEqual(exp, x) { + t.Fatalf("Assert failed: expected %v, found %v", exp, x) + } +} + From cbc26056b18116b815d4e170c83b97dddc7e6072 Mon Sep 17 00:00:00 2001 From: Daniel Morandini Date: Thu, 10 Oct 2019 11:21:22 +0200 Subject: [PATCH 05/26] Assert with concrete type --- onf/internal/lsof/lsof_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/onf/internal/lsof/lsof_test.go b/onf/internal/lsof/lsof_test.go index 66e274f..1643c63 100644 --- a/onf/internal/lsof/lsof_test.go +++ b/onf/internal/lsof/lsof_test.go @@ -39,7 +39,7 @@ func TestParseONF(t *testing.T) { assert(t, "(ESTABLISHED)", onf.State) } -func assert(t *testing.T, exp, x interface{}) { +func assert(t *testing.T, exp, x string) { if exp != x { t.Fatalf("Assert failed: expected %v, found %v", exp, x) } From e31f4b148d4a2f908ee7e9a36753f3f981f14568 Mon Sep 17 00:00:00 2001 From: Daniel Morandini Date: Thu, 10 Oct 2019 11:22:29 +0200 Subject: [PATCH 06/26] Format codebase --- onf/internal/lsof/lsof.go | 8 ++++---- onf/internal/tasklist/tasklist.go | 6 +++--- onf/internal/tasklist/tasklist_test.go | 3 +-- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/onf/internal/lsof/lsof.go b/onf/internal/lsof/lsof.go index b66960d..cba588d 100644 --- a/onf/internal/lsof/lsof.go +++ b/onf/internal/lsof/lsof.go @@ -15,14 +15,14 @@ package lsof import ( - "fmt" - "strings" - "net" "bytes" + "fmt" "io" "log" - "time" + "net" "strconv" + "strings" + "time" "github.com/jecoz/lsaddr/onf/internal" "gopkg.in/pipe.v2" diff --git a/onf/internal/tasklist/tasklist.go b/onf/internal/tasklist/tasklist.go index c1abac5..f2da8ce 100644 --- a/onf/internal/tasklist/tasklist.go +++ b/onf/internal/tasklist/tasklist.go @@ -15,12 +15,12 @@ package tasklist import ( - "log" + "bytes" "fmt" - "strings" "io" - "bytes" + "log" "strconv" + "strings" "github.com/jecoz/lsaddr/onf/internal" ) diff --git a/onf/internal/tasklist/tasklist_test.go b/onf/internal/tasklist/tasklist_test.go index 3ebb00b..64954f1 100644 --- a/onf/internal/tasklist/tasklist_test.go +++ b/onf/internal/tasklist/tasklist_test.go @@ -17,8 +17,8 @@ package tasklist import ( "bytes" - "testing" "reflect" + "testing" ) const tasklistExample = ` @@ -92,4 +92,3 @@ func assert(t *testing.T, exp, x interface{}) { t.Fatalf("Assert failed: expected %v, found %v", exp, x) } } - From 35144b172b6123e172b6df1cba0eed90325dab50 Mon Sep 17 00:00:00 2001 From: Daniel Morandini Date: Thu, 10 Oct 2019 23:18:40 +0200 Subject: [PATCH 07/26] Add netstat tool --- onf/internal/lsof/lsof.go | 53 ++++++++------- onf/internal/lsof/lsof_test.go | 43 ++++++------ onf/internal/netstat/netstat.go | 99 ++++++++++++++++++++++++++++ onf/internal/netstat/netstat_test.go | 69 +++++++++++++++++++ onf/internal/utils.go | 66 +++++++++++++++++++ onf/onf.go | 31 +++++++++ onf/set.go | 21 ++++++ 7 files changed, 337 insertions(+), 45 deletions(-) create mode 100644 onf/internal/netstat/netstat.go create mode 100644 onf/internal/netstat/netstat_test.go create mode 100644 onf/internal/utils.go create mode 100644 onf/onf.go create mode 100644 onf/set.go diff --git a/onf/internal/lsof/lsof.go b/onf/internal/lsof/lsof.go index cba588d..133b26f 100644 --- a/onf/internal/lsof/lsof.go +++ b/onf/internal/lsof/lsof.go @@ -28,7 +28,7 @@ import ( "gopkg.in/pipe.v2" ) -type ONF struct { +type OpenFile struct { Command string Pid int User string @@ -40,8 +40,8 @@ type ONF struct { DstAddr net.Addr // Destination address } -func Run() ([]ONF, error) { - acc := []ONF{} +func Run() ([]OpenFile, error) { + acc := []OpenFile{} p := pipe.Exec("lsof", "-i", "-n", "-P") out, err := pipe.OutputTimeout(p, time.Millisecond*100) if err != nil { @@ -53,14 +53,14 @@ func Run() ([]ONF, error) { // ParseOutput expects "r" to contain the output of // an ``lsof -i -n -P'' call. The output is splitted into each new line, -// and each line that ``ParseONF'' is able to parse +// and each line that ``ParseOpenFile'' is able to parse // is appended to the final output. // Returns an error only if reading from "r" produces an error // different from ``io.EOF''. -func ParseOutput(r io.Reader) ([]ONF, error) { - set := []ONF{} +func ParseOutput(r io.Reader) ([]OpenFile, error) { + set := []OpenFile{} err := internal.ScanLines(r, func(line string) error { - onf, err := ParseONF(line) + onf, err := ParseOpenFile(line) if err != nil { log.Printf("skipping onf \"%s\": %v", line, err) return nil @@ -71,8 +71,8 @@ func ParseOutput(r io.Reader) ([]ONF, error) { return set, err } -// ParseONF expectes "line" to be a single line output from -// ``lsof -i -n -P'' call. The line is unmarshaled into an ``ONF'' +// ParseOpenFile expectes "line" to be a single line output from +// ``lsof -i -n -P'' call. The line is unmarshaled into an ``OpenFile'' // only if is splittable by " " into a slice of at least 9 items. "line" should // not end with a "\n" delimitator, otherwise it will end up in the last // unmarshaled item. @@ -80,7 +80,7 @@ func ParseOutput(r io.Reader) ([]ONF, error) { // "line" examples: // "postgres 676 danielmorandini 10u IPv6 0x25c5bf0997ca88e3 0t0 UDP [::1]:60051->[::1]:60051" // "Dropbox 614 danielmorandini 247u IPv4 0x25c5bf09a393d583 0t0 TCP 192.168.0.61:58282->162.125.18.133:https (ESTABLISHED)" -func ParseONF(line string) (*ONF, error) { +func ParseOpenFile(line string) (*OpenFile, error) { chunks, err := internal.ChunkLine(line, " ", 9) if err != nil { return nil, err @@ -90,7 +90,7 @@ func ParseONF(line string) (*ONF, error) { return nil, fmt.Errorf("error parsing pid: %w", err) } - f := &ONF{ + onf := &OpenFile{ Command: chunks[0], Pid: pid, User: chunks[2], @@ -98,14 +98,17 @@ func ParseONF(line string) (*ONF, error) { Type: chunks[4], Device: chunks[5], } - src, dst := ParseName(chunks[7], chunks[8]) - f.SrcAddr = src - f.DstAddr = dst + src, dst, err := ParseName(chunks[7], chunks[8]) + if err != nil { + return nil, fmt.Errorf("error parsing name: %w", err) + } + onf.SrcAddr = src + onf.DstAddr = dst if len(chunks) >= 10 { - f.State = chunks[9] + onf.State = chunks[9] } - return f, nil + return onf, nil } // ParseName parses `lsof`'s name field, which by default is in the form: @@ -113,17 +116,23 @@ func ParseONF(line string) (*ONF, error) { // but we're disabling hostname conversion with the ``-n'' option // and port conversion with the ``-P'' option, so the output // in printed in the more decodable format: ``addr:port->addr:port''. -func ParseName(node, name string) (net.Addr, net.Addr) { +func ParseName(node, name string) (net.Addr, net.Addr, error) { chunks := strings.Split(name, "->") if len(chunks) == 0 { - return addr{}, addr{} + return nil, nil, fmt.Errorf("unable to split name by ->") + } + src, err := internal.ParseNetAddr(node, chunks[0]) + if err != nil { + return nil, nil, err } - src := addr{net: strings.ToLower(node), addr: chunks[0]} if len(chunks) == 1 { - return src, addr{} + return src, addr{}, nil } - - return src, addr{net: strings.ToLower(node), addr: chunks[1]} + dst, err := internal.ParseNetAddr(node, chunks[1]) + if err != nil { + return nil, nil, err + } + return src, dst, nil } // addr is a net.Addr implementation. diff --git a/onf/internal/lsof/lsof_test.go b/onf/internal/lsof/lsof_test.go index 1643c63..bfb9a6e 100644 --- a/onf/internal/lsof/lsof_test.go +++ b/onf/internal/lsof/lsof_test.go @@ -19,27 +19,27 @@ import ( "testing" ) -func TestParseONF(t *testing.T) { +func TestParseOpenFile(t *testing.T) { t.Parallel() line := "Spotify 11778 danielmorandini 128u IPv4 0x25c5bf09993eff03 0t0 TCP 192.168.0.61:51291->35.186.224.47:443 (ESTABLISHED)" - onf, err := ParseONF(line) + of, err := ParseOpenFile(line) if err != nil { t.Fatalf("Unexpcted error: %v", err) } - assert(t, "Spotify", onf.Command) - assert(t, 11778, onf.Pid) - assert(t, "danielmorandini", onf.User) - assert(t, "128u", onf.Fd) - assert(t, "IPv4", onf.Type) - assert(t, "0x25c5bf09993eff03", onf.Device) - assert(t, "192.168.0.61:51291", onf.SrcAddr.String()) - assert(t, "35.186.224.47:443", onf.DstAddr.String()) - assert(t, "(ESTABLISHED)", onf.State) + assert(t, "Spotify", of.Command) + assert(t, 11778, of.Pid) + assert(t, "danielmorandini", of.User) + assert(t, "128u", of.Fd) + assert(t, "IPv4", of.Type) + assert(t, "0x25c5bf09993eff03", of.Device) + assert(t, "192.168.0.61:51291", of.SrcAddr.String()) + assert(t, "35.186.224.47:443", of.DstAddr.String()) + assert(t, "(ESTABLISHED)", of.State) } -func assert(t *testing.T, exp, x string) { +func assert(t *testing.T, exp, x interface{}) { if exp != x { t.Fatalf("Assert failed: expected %v, found %v", exp, x) } @@ -73,22 +73,19 @@ func TestParseName(t *testing.T) { dst string net string }{ - {"TCP", "127.0.0.1:49161->127.0.01:9090", "127.0.0.1:49161", "127.0.01:9090", "tcp"}, + {"TCP", "127.0.0.1:49161->127.0.01:9090", "127.0.0.1:49161", "127.0.0.1:9090", "tcp"}, {"TCP", "127.0.0.1:5432", "127.0.0.1:5432", "", "tcp"}, {"UDP", "192.168.0.61:50940->192.168.0.2:53", "192.168.0.61:50940", "192.168.0.2:53", "udp"}, {"TCP", "[fe80:c::d5d5:601e:981b:c79d]:1024->[fe80:c::f9b9:5ecb:eeca:58e9]:1024", "[fe80:c::d5d5:601e:981b:c79d]:1024", "[fe80:c::f9b9:5ecb:eeca:58e9]:1024", "tcp"}, } - for i, v := range tt { - src, dst := ParseName(v.node, v.name) - if src.String() != v.src { - t.Fatalf("%d: Unexpected src: wanted %s, found %s", i, v.src, src.String()) - } - if dst.String() != v.dst { - t.Fatalf("%d: Unexpected dst: wanted %s, found %s", i, v.dst, dst.String()) - } - if src.Network() != v.net { - t.Fatalf("%d: Unexpected net: wanted %s, found %s", i, v.net, src.Network()) + for _, v := range tt { + src, dst, err := ParseName(v.node, v.name) + if err != nil { + t.Fatal(err) } + assert(t, v.src, src.String()) + assert(t, v.dst, dst.String()) + assert(t, v.net, src.Network()) } } diff --git a/onf/internal/netstat/netstat.go b/onf/internal/netstat/netstat.go new file mode 100644 index 0000000..e44a7bc --- /dev/null +++ b/onf/internal/netstat/netstat.go @@ -0,0 +1,99 @@ +// Copyright © 2019 Jecoz +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package netstat + +import ( + "fmt" + "io" + "log" + "net" + "strconv" + + "github.com/jecoz/lsaddr/onf/internal" +) + +type ActiveConnection struct { + Proto string + SrcAddr net.Addr + DstAddr net.Addr + State string + Pid int +} + +// ParseOutput expects "r" to contain the output of +// a ``netstat -nao'' call. The output is splitted into lines, and +// each line that ``ParseActiveConnection'' is able to Unmarshal is +// appended to the final output. +// Returns an error only if reading from "r" produces an error +// different from ``io.EOF''. +func ParseOutput(r io.Reader) ([]ActiveConnection, error) { + set := []ActiveConnection{} + err := internal.ScanLines(r, func(line string) error { + af, err := ParseActiveConnection(line) + if err != nil { + log.Printf("skipping netstat active connection \"%s\": %v", line, err) + return nil + } + set = append(set, *af) + return nil + }) + return set, err +} + +// ParseActiveConnection expectes "line" to be a single line output from +// ``netstat -nao'' call. The line is unmarshaled into an ``ActiveConnection'' +// only if is splittable by " " into a slice of at least 4 items. "line" should +// not end with a "\n" delimitator, otherwise it will end up in the last +// unmarshaled item. +// +// "line" examples: +// " TCP 0.0.0.0:5357 0.0.0.0:0 LISTENING 4" +// " UDP [::1]:62261 *:* 1036" +func ParseActiveConnection(line string) (*ActiveConnection, error) { + chunks, err := internal.ChunkLine(line, " ", 4) + if err != nil { + return nil, err + } + + proto := chunks[0] + + var src, dst net.Addr + src, err = internal.ParseNetAddr(proto, chunks[1]) + dst, err = internal.ParseNetAddr(proto, chunks[2]) + if err != nil && src == dst { + // We where not able to parse an address. We consider this + // connection not usable. + return nil, fmt.Errorf("unable to parse addresses: %w", err) + } + + ac := &ActiveConnection{ + Proto: proto, + SrcAddr: src, + DstAddr: dst, + } + hasState := len(chunks) > 4 + pidIndex := 3 + if hasState { + pidIndex = 4 + ac.State = chunks[3] + } + pid, err := strconv.Atoi(chunks[pidIndex]) + if err != nil { + return nil, fmt.Errorf("error parsing pid: %w", err) + } + ac.Pid = pid + + return ac, nil +} diff --git a/onf/internal/netstat/netstat_test.go b/onf/internal/netstat/netstat_test.go new file mode 100644 index 0000000..218728b --- /dev/null +++ b/onf/internal/netstat/netstat_test.go @@ -0,0 +1,69 @@ +// Copyright © 2019 Jecoz +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package netstat + +import ( + "bytes" + "reflect" + "testing" +) + +const netstatExample = ` +Active Connections + + Proto Local Address Foreign Address State PID + TCP 0.0.0.0:135 0.0.0.0:0 LISTENING 748 + RpcSs + [svchost.exe] + TCP 0.0.0.0:445 0.0.0.0:0 LISTENING 4 + Can not obtain ownership information + TCP 0.0.0.0:5357 0.0.0.0:0 LISTENING 4 + [svchost.exe] + UDP [::1]:62261 *:* 1036 +` + +func TestParseOutput(t *testing.T) { + t.Parallel() + + buf := bytes.NewBufferString(netstatExample) + ll, err := ParseOutput(buf) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if len(ll) != 4 { + t.Fatalf("Unexpected ll length: wanted 4, found %d: %v", len(ll), ll) + } +} + +func TestParseActiveConnection(t *testing.T) { + t.Parallel() + line := " TCP 0.0.0.0:135 0.0.0.0:0 LISTENING 748" + ac, err := ParseActiveConnection(line) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + assert(t, "TCP", ac.Proto) + assert(t, "0.0.0.0:135", ac.SrcAddr.String()) + assert(t, "0.0.0.0:0", ac.DstAddr.String()) + assert(t, "LISTENING", ac.State) + assert(t, 748, ac.Pid) +} + +func assert(t *testing.T, exp, x interface{}) { + if !reflect.DeepEqual(exp, x) { + t.Fatalf("Assert failed: expected %v, found %v", exp, x) + } +} diff --git a/onf/internal/utils.go b/onf/internal/utils.go new file mode 100644 index 0000000..dfda13e --- /dev/null +++ b/onf/internal/utils.go @@ -0,0 +1,66 @@ +// Copyright © 2019 Jecoz +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package internal + +import ( + "bufio" + "fmt" + "io" + "net" + "strings" +) + +func ChunkLine(line string, sep string, min int) ([]string, error) { + items := strings.Split(line, sep) + chunks := make([]string, 0, len(items)) + for _, v := range items { + if v == "" { + continue + } + chunks = append(chunks, v) + } + n := len(chunks) + if n < min { + return chunks, fmt.Errorf("unable to chunk line: expected at least %d items, found %d: line \"%s\"", min, n, chunks) + } + + return chunks, nil +} + +func ScanLines(r io.Reader, f func(string) error) error { + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := scanner.Text() + line = strings.Trim(line, "\n") + line = strings.Trim(line, "\r") + if err := f(line); err != nil { + return err + } + } + return scanner.Err() +} + +func ParseNetAddr(network, addr string) (net.Addr, error) { + network = strings.ToLower(network) + switch { + case strings.Contains(network, "tcp"): + return net.ResolveTCPAddr(network, addr) + case strings.Contains(network, "udp"): + return net.ResolveUDPAddr(network, addr) + default: + return nil, fmt.Errorf("unsupported network %v", network) + } +} diff --git a/onf/onf.go b/onf/onf.go new file mode 100644 index 0000000..eb640c1 --- /dev/null +++ b/onf/onf.go @@ -0,0 +1,31 @@ +// Copyright © 2019 Jecoz +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package onf + +import ( + "net" + "time" +) + +// NetFile represents a network file. +type Onf struct { + Raw string // raw string that produced this result + Cmd string // command associated with Pid + Pid int // pid of the owner + Src net.Addr // source address + Dst net.Addr // destination address + CreatedAt time.Time +} diff --git a/onf/set.go b/onf/set.go new file mode 100644 index 0000000..c333472 --- /dev/null +++ b/onf/set.go @@ -0,0 +1,21 @@ +// Copyright © 2019 Jecoz +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package onf + +func FetchAll() ([]Onf, error) { + internal.ToolSet.FetchAll() + +} From 2e0395ef771054860031338f61d0b7ff23bcacb9 Mon Sep 17 00:00:00 2001 From: Daniel Morandini Date: Thu, 10 Oct 2019 23:54:47 +0200 Subject: [PATCH 08/26] Cleanup, add runtime fetchAll implementations --- cmd/root.go | 82 ++-- go.mod | 3 +- go.sum | 3 + install.sh | 379 ------------------ lookup/internal/decoder.go | 365 ----------------- lookup/internal/decoder_test.go | 253 ------------ lookup/internal/filter.go | 176 -------- lookup/internal/filter_test.go | 59 --- lookup/internal/lookup.go | 52 --- lookup/internal/runtime_linux.go | 30 -- lookup/lookup.go | 66 --- main.go | 2 +- onf/.onf.go.swo | Bin 0 -> 12288 bytes onf/internal/lsof/lsof.go | 2 + onf/internal/netstat/netstat.go | 13 + onf/onf.go | 2 +- .../runtime_darwin.go => onf/runtime_unix.go | 33 +- {lookup/internal => onf}/runtime_windows.go | 29 +- onf/set.go | 5 +- 19 files changed, 97 insertions(+), 1457 deletions(-) delete mode 100644 install.sh delete mode 100644 lookup/internal/decoder.go delete mode 100644 lookup/internal/decoder_test.go delete mode 100644 lookup/internal/filter.go delete mode 100644 lookup/internal/filter_test.go delete mode 100644 lookup/internal/lookup.go delete mode 100644 lookup/internal/runtime_linux.go delete mode 100644 lookup/lookup.go create mode 100644 onf/.onf.go.swo rename lookup/internal/runtime_darwin.go => onf/runtime_unix.go (61%) rename {lookup/internal => onf}/runtime_windows.go (65%) diff --git a/cmd/root.go b/cmd/root.go index 9c96375..df04ea6 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,4 +1,4 @@ -// Copyright © 2019 booster authors +// Copyright © 2019 Jecoz // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or @@ -15,27 +15,23 @@ package cmd import ( - "bufio" "fmt" - "io" "io/ioutil" "log" "os" "strings" - "github.com/booster-proj/lsaddr/bpf" - "github.com/booster-proj/lsaddr/csv" - "github.com/booster-proj/lsaddr/lookup" + "github.com/jecoz/lsaddr/onf" "github.com/spf13/cobra" ) -var debug bool -var output string +var debug, raw bool +var format string // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ Use: "lsaddr", - Short: "Show a subset of all network addresses being used by the system", + Short: "Show a subset of all network addresses being used by your apps", Long: usage, Args: cobra.MaximumNArgs(1), PersistentPreRun: func(cmd *cobra.Command, args []string) { @@ -44,31 +40,28 @@ var rootCmd = &cobra.Command{ log.SetOutput(ioutil.Discard) } - output = strings.ToLower(output) - if err := validateOutput(output); err != nil { - fmt.Fprintf(os.Stderr, "%v\n", err) + format = strings.ToLower(format) + if err := validateFormat(format); err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } }, Run: func(cmd *cobra.Command, args []string) { - var s string - if len(args) > 0 { - s = args[0] - } - - ff, err := lookup.OpenNetFiles(s) + set, err := onf.FetchAll() if err != nil { - fmt.Printf("unable to find open network files for %s: %v\n", s, err) + fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } - log.Printf("# of open files: %d", len(ff)) - - w := bufio.NewWriter(os.Stdout) - if err := writeOutputTo(w, output, ff); err != nil { - fmt.Fprintf(os.Stderr, "unable to write output: %v", err) - os.Exit(1) + //fset, err := onf.Filter(set, s) + //if err != nil { + // fmt.Fprintf(os.Stderr, "error: unable to filter with %s: %w\n", s, err") + // os.Exit(1) + //} + + log.Printf("# of open network files: %d", len(set)) + for _, v := range set { + fmt.Printf("%v\n", v) } - w.Flush() }, } @@ -82,38 +75,22 @@ func Execute() { } func init() { - rootCmd.PersistentFlags().BoolVarP(&debug, "debug", "", false, "increment logger verbosity") - rootCmd.PersistentFlags().StringVarP(&output, "out", "o", "csv", "choose type of output produced ") -} - -func writeOutputTo(w io.Writer, output string, ff []lookup.NetFile) error { - switch output { - case "csv": - return csv.NewEncoder(w).Encode(ff) - case "bpf": - var expr bpf.Expr - for _, v := range ff { - src := string(bpf.FromAddr(bpf.NODIR, v.Src).Wrap()) - dst := string(bpf.FromAddr(bpf.NODIR, v.Dst).Wrap()) - expr = expr.Or(src).Or(dst) - } - _, err := io.Copy(w, expr.NewReader()) - return err - } - return nil + rootCmd.PersistentFlags().BoolVarP(&debug, "debug", "d", false, "increment logger verbosity") + rootCmd.PersistentFlags().BoolVarP(&raw, "raw", "r", false, "increment logger verbosity") + rootCmd.PersistentFlags().StringVarP(&format, "format", "f", "csv", "choose format of output produced ") } -func validateOutput(output string) error { - switch output { +func validateFormat(format string) error { + switch format { case "csv", "bpf": return nil default: - return fmt.Errorf("unrecognised output %s", output) + return fmt.Errorf("unrecognised format option %s", format) } } const usage = ` -'lsaddr' takes the entire list of currently open network connections and filters it out +'lsaddr' takes the entire list of open network files and filters it out using the argument provided, which can either be: - "*.app" (macOS): It will be recognised as the path leading to the root directory of @@ -123,12 +100,11 @@ an Application. The tool will then: 3. Build a regular expression out of them - a regular expression: which will be used to filter out the list of open files. Each line that -does not match against the regex will be discarded. On macOS, the list of open files is fetched -using 'lsof -i -n -P'. +does not match against the regex will be discarded (e.g. "chrome.exe", "Safari", "104|405"). Check out https://golang.org/pkg/regexp/ to learn how to properly format your regex. -It is possible to configure with the '-out' flag which output 'lsaddr' will produce. Possible -values are: +Using the "--format" or "-f" flag, it is possible to decide the format/encoding of the output +produced. Possible values are: - "bpf": produces a Berkley Packet Filter expression, which, if given to a tool that supports bpfs, will make it capture only the packets headed to/coming from the destination addresses of the open network files collected. diff --git a/go.mod b/go.mod index 4176233..77f5d66 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,9 @@ -module github.com/booster-proj/lsaddr +module github.com/jecoz/lsaddr go 1.12 require ( + github.com/booster-proj/lsaddr v0.5.1 github.com/spf13/cobra v0.0.5 gopkg.in/pipe.v2 v2.0.0-20140414041502-3c2ca4d52544 howett.net/plist v0.0.0-20181124034731-591f970eefbb diff --git a/go.sum b/go.sum index c4c8c26..486beaa 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/booster-proj/lsaddr v0.5.1 h1:LZvL8TObqJT8c4FzbY0gxQsYuFie0M1G15lLhf0310Q= +github.com/booster-proj/lsaddr v0.5.1/go.mod h1:WwydaFu1mGdyWBazFtJZQWk7ybiFvNvSToln+EIMar8= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -9,6 +11,7 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jecoz/lsaddr v0.5.1 h1:A1fTRc5rQrkmV+Do/oZB70Ynj/YFxRsd+8wD/vux40o= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= diff --git a/install.sh b/install.sh deleted file mode 100644 index 4e6a8c4..0000000 --- a/install.sh +++ /dev/null @@ -1,379 +0,0 @@ -#!/bin/sh -set -e -# Code generated by godownloader on 2019-06-13T13:53:28Z. DO NOT EDIT. -# - -usage() { - this=$1 - cat </dev/null -} -echoerr() { - echo "$@" 1>&2 -} -log_prefix() { - echo "$0" -} -_logp=6 -log_set_priority() { - _logp="$1" -} -log_priority() { - if test -z "$1"; then - echo "$_logp" - return - fi - [ "$1" -le "$_logp" ] -} -log_tag() { - case $1 in - 0) echo "emerg" ;; - 1) echo "alert" ;; - 2) echo "crit" ;; - 3) echo "err" ;; - 4) echo "warning" ;; - 5) echo "notice" ;; - 6) echo "info" ;; - 7) echo "debug" ;; - *) echo "$1" ;; - esac -} -log_debug() { - log_priority 7 || return 0 - echoerr "$(log_prefix)" "$(log_tag 7)" "$@" -} -log_info() { - log_priority 6 || return 0 - echoerr "$(log_prefix)" "$(log_tag 6)" "$@" -} -log_err() { - log_priority 3 || return 0 - echoerr "$(log_prefix)" "$(log_tag 3)" "$@" -} -log_crit() { - log_priority 2 || return 0 - echoerr "$(log_prefix)" "$(log_tag 2)" "$@" -} -uname_os() { - os=$(uname -s | tr '[:upper:]' '[:lower:]') - case "$os" in - msys_nt) os="windows" ;; - esac - echo "$os" -} -uname_arch() { - arch=$(uname -m) - case $arch in - x86_64) arch="amd64" ;; - x86) arch="386" ;; - i686) arch="386" ;; - i386) arch="386" ;; - aarch64) arch="arm64" ;; - armv5*) arch="armv5" ;; - armv6*) arch="armv6" ;; - armv7*) arch="armv7" ;; - esac - echo ${arch} -} -uname_os_check() { - os=$(uname_os) - case "$os" in - darwin) return 0 ;; - dragonfly) return 0 ;; - freebsd) return 0 ;; - linux) return 0 ;; - android) return 0 ;; - nacl) return 0 ;; - netbsd) return 0 ;; - openbsd) return 0 ;; - plan9) return 0 ;; - solaris) return 0 ;; - windows) return 0 ;; - esac - log_crit "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib" - return 1 -} -uname_arch_check() { - arch=$(uname_arch) - case "$arch" in - 386) return 0 ;; - amd64) return 0 ;; - arm64) return 0 ;; - armv5) return 0 ;; - armv6) return 0 ;; - armv7) return 0 ;; - ppc64) return 0 ;; - ppc64le) return 0 ;; - mips) return 0 ;; - mipsle) return 0 ;; - mips64) return 0 ;; - mips64le) return 0 ;; - s390x) return 0 ;; - amd64p32) return 0 ;; - esac - log_crit "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib" - return 1 -} -untar() { - tarball=$1 - case "${tarball}" in - *.tar.gz | *.tgz) tar -xzf "${tarball}" ;; - *.tar) tar -xf "${tarball}" ;; - *.zip) unzip "${tarball}" ;; - *) - log_err "untar unknown archive format for ${tarball}" - return 1 - ;; - esac -} -http_download_curl() { - local_file=$1 - source_url=$2 - header=$3 - if [ -z "$header" ]; then - code=$(curl -w '%{http_code}' -sL -o "$local_file" "$source_url") - else - code=$(curl -w '%{http_code}' -sL -H "$header" -o "$local_file" "$source_url") - fi - if [ "$code" != "200" ]; then - log_debug "http_download_curl received HTTP status $code" - return 1 - fi - return 0 -} -http_download_wget() { - local_file=$1 - source_url=$2 - header=$3 - if [ -z "$header" ]; then - wget -q -O "$local_file" "$source_url" - else - wget -q --header "$header" -O "$local_file" "$source_url" - fi -} -http_download() { - log_debug "http_download $2" - if is_command curl; then - http_download_curl "$@" - return - elif is_command wget; then - http_download_wget "$@" - return - fi - log_crit "http_download unable to find wget or curl" - return 1 -} -http_copy() { - tmp=$(mktemp) - http_download "${tmp}" "$1" "$2" || return 1 - body=$(cat "$tmp") - rm -f "${tmp}" - echo "$body" -} -github_release() { - owner_repo=$1 - version=$2 - test -z "$version" && version="latest" - giturl="https://github.com/${owner_repo}/releases/${version}" - json=$(http_copy "$giturl" "Accept:application/json") - test -z "$json" && return 1 - version=$(echo "$json" | tr -s '\n' ' ' | sed 's/.*"tag_name":"//' | sed 's/".*//') - test -z "$version" && return 1 - echo "$version" -} -hash_sha256() { - TARGET=${1:-/dev/stdin} - if is_command gsha256sum; then - hash=$(gsha256sum "$TARGET") || return 1 - echo "$hash" | cut -d ' ' -f 1 - elif is_command sha256sum; then - hash=$(sha256sum "$TARGET") || return 1 - echo "$hash" | cut -d ' ' -f 1 - elif is_command shasum; then - hash=$(shasum -a 256 "$TARGET" 2>/dev/null) || return 1 - echo "$hash" | cut -d ' ' -f 1 - elif is_command openssl; then - hash=$(openssl -dst openssl dgst -sha256 "$TARGET") || return 1 - echo "$hash" | cut -d ' ' -f a - else - log_crit "hash_sha256 unable to find command to compute sha-256 hash" - return 1 - fi -} -hash_sha256_verify() { - TARGET=$1 - checksums=$2 - if [ -z "$checksums" ]; then - log_err "hash_sha256_verify checksum file not specified in arg2" - return 1 - fi - BASENAME=${TARGET##*/} - want=$(grep "${BASENAME}" "${checksums}" 2>/dev/null | tr '\t' ' ' | cut -d ' ' -f 1) - if [ -z "$want" ]; then - log_err "hash_sha256_verify unable to find checksum for '${TARGET}' in '${checksums}'" - return 1 - fi - got=$(hash_sha256 "$TARGET") - if [ "$want" != "$got" ]; then - log_err "hash_sha256_verify checksum for '$TARGET' did not verify ${want} vs $got" - return 1 - fi -} -cat /dev/null <. - -package internal - -import ( - "bufio" - "bytes" - "fmt" - "io" - "log" - "net" - "strconv" - "strings" - - "howett.net/plist" -) - -// Lsof section - -type OpenFile struct { - Command string - Pid int - User string - Fd string - Type string - Device string - Node string // contains L4 proto - Name string // contains src->dst addresses - State string // (ENSTABLISHED), (LISTEN), ... -} - -func (f *OpenFile) String() string { - return fmt.Sprintf("{Pid: %d, Proto: %s, Conn: %s}", f.Pid, f.Node, f.Name) -} - -// UnmarshalName unmarshals `lsof`'s name field, which by default is in the form: -// [46][protocol][@hostname|hostaddr][:service|port] -// but we're disabling hostname conversion with the ``-n'' option -// and port conversion with the ``-P'' option, so the output -// in printed in the more decodable format: ``addr:port->addr:port''. -func (f *OpenFile) UnmarshalName() (net.Addr, net.Addr) { - chunks := strings.Split(f.Name, "->") - if len(chunks) == 0 { - return addr{}, addr{} - } - src := addr{net: strings.ToLower(f.Node), addr: chunks[0]} - if len(chunks) == 1 { - return src, addr{} - } - - return src, addr{net: strings.ToLower(f.Node), addr: chunks[1]} -} - -// DecodeLsofOutput expects "r" to contain the output of -// an ``lsof -i -n -P'' call. The output is splitted into each new line, -// and each line that ``UnmarshalLsofLine'' is able to Unmarshal -// is appended to the final output. -// Returns an error only if reading from "r" produces an error -// different from ``io.EOF''. -func DecodeLsofOutput(r io.Reader) ([]*OpenFile, error) { - ll := []*OpenFile{} - err := scanLines(r, func(line string) error { - f, err := UnmarshalLsofLine(line) - if err != nil { - log.Printf("skipping lsof line \"%s\": %v", line, err) - return nil - } - ll = append(ll, f) - return nil - }) - return ll, err -} - -// UnmarshalLsofLine expectes "line" to be a single line output from -// ``lsof -i -n -P'' call. The line is unmarshaled into an ``OpenFile'' -// only if is splittable by " " into a slice of at least 9 items. "line" should -// not end with a "\n" delimitator, otherwise it will end up in the last -// unmarshaled item. -// -// "line" examples: -// "postgres 676 danielmorandini 10u IPv6 0x25c5bf0997ca88e3 0t0 UDP [::1]:60051->[::1]:60051" -// "Dropbox 614 danielmorandini 247u IPv4 0x25c5bf09a393d583 0t0 TCP 192.168.0.61:58282->162.125.18.133:https (ESTABLISHED)" -func UnmarshalLsofLine(line string) (*OpenFile, error) { - chunks, err := chunkLine(line, " ", 9) - if err != nil { - return nil, err - } - pid, err := strconv.Atoi(chunks[1]) - if err != nil { - return nil, fmt.Errorf("error parsing pid: %w", err) - } - - f := &OpenFile{ - Command: chunks[0], - Pid: pid, - User: chunks[2], - Fd: chunks[3], - Type: chunks[4], - Device: chunks[5], - Node: chunks[7], - Name: chunks[8], - } - if len(chunks) >= 10 { - f.State = chunks[9] - } - return f, nil -} - -// Netstat - -// DecodeNetstatOutput expects "r" to contain the output of -// a ``netstat -ano'' call. The output is splitted into lines, and -// each line that ``UnmarshalNetstatLine'' is able to Unmarshal is -// appended to the final output. -// As of ``DecodeLsofOutput'', this function returns an error only -// if reading from "r" produces an error different from ``io.EOF''. -func DecodeNetstatOutput(r io.Reader) ([]*OpenFile, error) { - ll := []*OpenFile{} - err := scanLines(r, func(line string) error { - f, err := UnmarshalNetstatLine(line) - if err != nil { - log.Printf("skipping netstat line \"%s\": %v", line, err) - return nil - } - ll = append(ll, f) - return nil - }) - return ll, err -} - -// UnmarshalNetstatLine expectes "line" to be a single line output from -// ``netstat -ano'' call. The line is unmarshaled into an ``OpenFile'' -// only if is splittable by " " into a slice of at least 4 items. "line" should -// not end with a "\n" delimitator, otherwise it will end up in the last -// unmarshaled item. -// -// "line" examples: -// " TCP 0.0.0.0:5357 0.0.0.0:0 LISTENING 4" -// " UDP [::1]:62261 *:* 1036" -func UnmarshalNetstatLine(line string) (*OpenFile, error) { - chunks, err := chunkLine(line, " ", 4) - if err != nil { - return nil, err - } - - from := chunks[1] - to := chunks[2] - if !isValidAddress(from) { - return nil, fmt.Errorf("unrecognised source ip address: %s", from) - } - if !isValidAddress(to) { - return nil, fmt.Errorf("unrecognised destination ip address: %s", to) - } - - f := &OpenFile{ - Node: chunks[0], - Name: from + "->" + to, - } - hasState := len(chunks) > 4 - pidIndex := 3 - if hasState { - pidIndex = 4 - f.State = chunks[3] - } - pid, err := strconv.Atoi(chunks[pidIndex]) - if err != nil { - return nil, fmt.Errorf("error parsing pid: %w", err) - } - f.Pid = pid - - return f, nil -} - -// Tasklist - -type Task struct { - Pid int - Image string -} - -func (t *Task) String() string { - return fmt.Sprintf("{Image: %s, Pid: %d}", t.Image, t.Pid) -} - -// DecodeTasklistOutput expects "r" to contain the output of -// a ``tasklist'' call. The output is splitted into lines, and -// each line that ``UnmarshakTasklistLine'' is able to Unmarshal is -// appended to the final output, with the expections of the first lines -// that come before the separator line composed by only "=". Those lines -// are considered part of the "header". -// -// As of ``DecodeLsofOutput'', this function returns an error only -// if reading from "r" produces an error different from ``io.EOF''. -func DecodeTasklistOutput(r io.Reader) ([]*Task, error) { - ll := []*Task{} - delim := "=" - headerTrimmed := false - segLengths := []int{} - err := scanLines(r, func(line string) error { - if !headerTrimmed { - if strings.HasPrefix(line, delim) && strings.HasSuffix(line, delim) { - headerTrimmed = true - // This is the header delimiter! - chunks, err := chunkLine(line, " ", 5) - if err != nil { - return fmt.Errorf("unexpected header format: %w", err) - } - for _, v := range chunks { - segLengths = append(segLengths, len(v)) - } - } - // Still in the header - return nil - } - - t, err := UnmarshalTasklistLine(line, segLengths) - if err != nil { - log.Printf("skipping tasklist line \"%s\": %v", line, err) - return nil - } - ll = append(ll, t) - return nil - }) - return ll, err -} - -// UnmarshalTasklistLine expectes "line" to be a single line output from -// ``tasklist'' call. The line is unmarshaled into a ``Task'' and the operation -// is performed by readying bytes equal to "segLengths"[i], in order. "segLengths" -// should be computed using the header delimitator and counting the number of -// "=" in each segment of the header (split it by " ") -// -// "line" should not end with a "\n" delimitator, otherwise it will end up in the last -// unmarshaled item. -// The "header" lines (see below) should not be passed to this function. -// -// Example header: -// Image Name PID Session Name Session# Mem Usage -// ========================= ======== ================ =========== ============ -// -// Example line: -// svchost.exe 940 Services 0 52,336 K -func UnmarshalTasklistLine(line string, segLengths []int) (*Task, error) { - buf := bytes.NewBufferString(line) - p := make([]byte, 32) - - var image, pidRaw string - for i, v := range segLengths[:2] { - n, err := buf.Read(p[:v+1]) - if err != nil { - return nil, fmt.Errorf("unable to read tasklist chunk: %w", err) - } - s := strings.Trim(string(p[:n]), " ") - switch i { - case 0: - image = s - case 1: - pidRaw = s - default: - } - } - if image == "" { - return nil, fmt.Errorf("couldn't decode image from line") - } - if pidRaw == "" { - return nil, fmt.Errorf("couldn't decode pid from line") - } - pid, err := strconv.Atoi(pidRaw) - if err != nil { - return nil, fmt.Errorf("error parsing pid: %w", err) - } - - return &Task{ - Image: image, - Pid: pid, - }, nil -} - -// Plist - -// ExtractAppName is used to find the value of the "CFBundleExecutable" key. -// "r" is expected to be an ".plist" encoded file. -func ExtractAppName(r io.Reader) (string, error) { - rs, ok := r.(io.ReadSeeker) - if !ok { - var buf bytes.Buffer - if _, err := io.Copy(&buf, r); err != nil { - return "", err - } - rs = bytes.NewReader(buf.Bytes()) - } - - var data struct { - Name string `plist:"CFBundleExecutable"` - } - if err := plist.NewDecoder(rs).Decode(&data); err != nil { - return "", err - } - - return data.Name, nil -} - -// Private helpers - -func chunkLine(line string, sep string, min int) ([]string, error) { - items := strings.Split(line, sep) - chunks := make([]string, 0, len(items)) - for _, v := range items { - if v == "" { - continue - } - chunks = append(chunks, v) - } - n := len(chunks) - if n < min { - return chunks, fmt.Errorf("unable to chunk line: expected at least %d items, found %d: line \"%s\"", min, n, chunks) - } - - return chunks, nil -} - -func scanLines(r io.Reader, f func(string) error) error { - scanner := bufio.NewScanner(r) - for scanner.Scan() { - line := scanner.Text() - line = strings.Trim(line, "\n") - line = strings.Trim(line, "\r") - if err := f(line); err != nil { - return err - } - } - return scanner.Err() -} - -func isValidAddress(s string) bool { - _, _, err := net.SplitHostPort(s) - return err == nil -} - -// addr is a net.Addr implementation. -type addr struct { - addr string - net string -} - -func (a addr) String() string { - return a.addr -} - -func (a addr) Network() string { - return a.net -} diff --git a/lookup/internal/decoder_test.go b/lookup/internal/decoder_test.go deleted file mode 100644 index 7e6caee..0000000 --- a/lookup/internal/decoder_test.go +++ /dev/null @@ -1,253 +0,0 @@ -// Copyright © 2019 booster authors -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package internal_test - -import ( - "bytes" - "testing" - - "github.com/booster-proj/lsaddr/lookup/internal" -) - -// Lsof - -func TestUnmarshalLsofLine(t *testing.T) { - t.Parallel() - line := "Spotify 11778 danielmorandini 128u IPv4 0x25c5bf09993eff03 0t0 TCP 192.168.0.61:51291->35.186.224.47:https (ESTABLISHED)" - f, err := internal.UnmarshalLsofLine(line) - if err != nil { - t.Fatalf("Unexpcted error: %v", err) - } - - assert(t, "Spotify", f.Command) - assert(t, 11778, f.Pid) - assert(t, "danielmorandini", f.User) - assert(t, "128u", f.Fd) - assert(t, "IPv4", f.Type) - assert(t, "0x25c5bf09993eff03", f.Device) - assert(t, "TCP", f.Node) - assert(t, "192.168.0.61:51291->35.186.224.47:https", f.Name) - assert(t, "(ESTABLISHED)", f.State) -} - -const lsofExample = `Dropbox 614 danielmorandini 236u IPv4 0x25c5bf09a4161583 0t0 TCP 192.168.0.61:58122->162.125.66.7:https (ESTABLISHED) -Dropbox 614 danielmorandini 247u IPv4 0x25c5bf09a393d583 0t0 TCP 192.168.0.61:58282->162.125.18.133:https (ESTABLISHED) -postgres 676 danielmorandini 10u IPv6 0x25c5bf0997ca88e3 0t0 UDP [::1]:60051->[::1]:60051 -` - -func TestDecodeLsofOutput(t *testing.T) { - t.Parallel() - buf := bytes.NewBufferString(lsofExample) - ll, err := internal.DecodeLsofOutput(buf) - if err != nil { - t.Fatalf("Unexpected error: %v", err) - } - if len(ll) != 3 { - t.Fatalf("Unexpected ll length: wanted 3, found %d: %v", len(ll), ll) - } -} - -func TestUnmarshalName(t *testing.T) { - t.Parallel() - tt := []struct { - node string - name string - src string - dst string - net string - }{ - {"TCP", "127.0.0.1:49161->127.0.01:9090", "127.0.0.1:49161", "127.0.01:9090", "tcp"}, - {"TCP", "127.0.0.1:5432", "127.0.0.1:5432", "", "tcp"}, - {"UDP", "192.168.0.61:50940->192.168.0.2:53", "192.168.0.61:50940", "192.168.0.2:53", "udp"}, - {"TCP", "[fe80:c::d5d5:601e:981b:c79d]:1024->[fe80:c::f9b9:5ecb:eeca:58e9]:1024", "[fe80:c::d5d5:601e:981b:c79d]:1024", "[fe80:c::f9b9:5ecb:eeca:58e9]:1024", "tcp"}, - } - - for i, v := range tt { - f := internal.OpenFile{Node: v.node, Name: v.name} - src, dst := f.UnmarshalName() - if src.String() != v.src { - t.Fatalf("%d: Unexpected src: wanted %s, found %s", i, v.src, src.String()) - } - if dst.String() != v.dst { - t.Fatalf("%d: Unexpected dst: wanted %s, found %s", i, v.dst, dst.String()) - } - if src.Network() != v.net { - t.Fatalf("%d: Unexpected net: wanted %s, found %s", i, v.net, src.Network()) - } - } -} - -// Netstat - -const netstatExample = ` -Active Connections - - Proto Local Address Foreign Address State PID - TCP 0.0.0.0:135 0.0.0.0:0 LISTENING 748 - RpcSs - [svchost.exe] - TCP 0.0.0.0:445 0.0.0.0:0 LISTENING 4 - Can not obtain ownership information - TCP 0.0.0.0:5357 0.0.0.0:0 LISTENING 4 - [svchost.exe] - UDP [::1]:62261 *:* 1036 -` - -func TestDecodeNetstatOutput(t *testing.T) { - t.Parallel() - buf := bytes.NewBufferString(netstatExample) - ll, err := internal.DecodeNetstatOutput(buf) - if err != nil { - t.Fatalf("Unexpected error: %v", err) - } - if len(ll) != 4 { - t.Fatalf("Unexpected ll length: wanted 4, found %d: %v", len(ll), ll) - } -} - -func TestUnmarshalNetstatLine(t *testing.T) { - t.Parallel() - line := " TCP 0.0.0.0:135 0.0.0.0:0 LISTENING 748" - f, err := internal.UnmarshalNetstatLine(line) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - assert(t, "TCP", f.Node) - assert(t, "0.0.0.0:135->0.0.0.0:0", f.Name) - assert(t, "LISTENING", f.State) - assert(t, 748, f.Pid) -} - -// Tasklist - -const tasklistExample = ` -Image Name PID Session Name Session# Mem Usage -========================= ======== ================ =========== ============ -System Idle Process 0 Services 0 4 K -System 4 Services 0 15,376 K -smss.exe 296 Services 0 1,008 K -csrss.exe 380 Services 0 4,124 K -wininit.exe 452 Services 0 4,828 K -services.exe 588 Services 0 6,284 K -lsass.exe 596 Services 0 12,600 K -svchost.exe 688 Services 0 17,788 K -svchost.exe 748 Services 0 8,980 K -svchost.exe 888 Services 0 21,052 K -svchost.exe 904 Services 0 21,200 K -svchost.exe 940 Services 0 52,336 K -WUDFHost.exe 464 Services 0 6,128 K -svchost.exe 1036 Services 0 14,524 K -svchost.exe 1044 Services 0 27,488 K -svchost.exe 1104 Services 0 28,428 K -WUDFHost.exe 1240 Services 0 6,888 K -` - -func TestDecodeTasklistOutput(t *testing.T) { - t.Parallel() - buf := bytes.NewBufferString(tasklistExample) - ll, err := internal.DecodeTasklistOutput(buf) - if err != nil { - t.Fatalf("Unexpected error: %v", err) - } - if len(ll) != 17 { - t.Fatalf("Unexpected ll length: wanted 17, found: %d: %v", len(ll), ll) - } -} - -func TestUnmarshalTasklistLine(t *testing.T) { - t.Parallel() - tt := []struct { - line string - segs []int - image string - pid int - }{ - { - line: "svchost.exe 940 Services 0 52,336 K", - segs: []int{25, 8, 16, 11, 12}, - image: "svchost.exe", - pid: 940, - }, - { - line: "System Idle Process 0 Services 0 4 K", - segs: []int{25, 8, 16, 11, 12}, - image: "System Idle Process", - pid: 0, - }, - } - - for i, v := range tt { - task, err := internal.UnmarshalTasklistLine(v.line, v.segs) - if err != nil { - t.Fatalf("%d: unexpected error: %v", i, err) - } - assert(t, v.image, task.Image) - assert(t, v.pid, task.Pid) - } -} - -// Plist - -const infoExample = ` - - - - CFBundleExecutable - pico8 - CFBundleGetInfoString - pico8 - CFBundleIconFile - pico8.icns - CFBundleIdentifier - com.Lexaloffle.pico8 - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - pico8 - CFBundlePackageType - APPL - CFBundleShortVersionString - pico8 - CFBundleSignature - ???? - CFBundleVersion - pico8 - LSMinimumSystemVersion - 10.1 - - -` - -func TestExtractAppName(t *testing.T) { - t.Parallel() - r := bytes.NewBufferString(infoExample) - name, err := internal.ExtractAppName(r) - if err != nil { - t.Fatalf("Unexpected error: %v", err) - } - exp := "pico8" - if name != exp { - t.Fatalf("Unexpected name: found %s, wanted %s", name, exp) - } -} - -// Private helpers - -func assert(t *testing.T, exp, x interface{}) { - if exp != x { - t.Fatalf("Assert failed: expected %v, found %v", exp, x) - } -} diff --git a/lookup/internal/filter.go b/lookup/internal/filter.go deleted file mode 100644 index cf7e352..0000000 --- a/lookup/internal/filter.go +++ /dev/null @@ -1,176 +0,0 @@ -// Copyright © 2019 booster authors -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package internal - -import ( - "bytes" - "log" - "os" - "path/filepath" - "regexp" - "strconv" - "strings" - - "gopkg.in/pipe.v2" -) - -// BuildNFFilter compiles a regular expression out of "s". Some manipulation -// may be performed on "s" before it is compiled, depending on the hosting -// operating system: on macOS for example, if "s" ends with ".app", it -// will be trated as the root path to an application. -func BuildNFFilter(s string) (*regexp.Regexp, error) { - expr := runtime.PrepareNFExprFunc(s) - rgx, err := regexp.Compile(expr) - if err != nil { - return nil, err - } - - return rgx, nil -} - -// Private helpers - -// Darwin helpers - -// prepareExprDarwin returns "s" untouched if it does not end with ".app". In that case, -// "s" is used as the root directory of a macOS application. The "CFBundleExecutable" -// value of the app is searched, and used to build the an expression that will match -// each string that contains a process identifer owned by the "target" app. -func prepareNFExprDarwin(s string) string { - if _, err := os.Stat(s); err != nil { - // this is not a path - return s - } - path := strings.TrimRight(s, "/") - if !strings.HasSuffix(path, ".app") { - // it is a path, but not one that we know how to handle. - return s - } - - // we suppose that "s" points to the root directory - // of an application. - name, err := appName(path) - if err != nil { - log.Printf("unable to find app name: %v", err) - return s - } - log.Printf("app name: %s, path: %s", name, path) - - // Find process identifier associated with this app. - pids := pgrep(name) - if len(pids) == 0 { - log.Printf("cannot find any PID associated with %s", name) - return s - } - - return strings.Join(pids, "|") -} - -// appName finds the "BundeExecutable" identifier from "Info.plist" file -// contained in the "Contents" subdirectory in "path". -// "path" should point to the target app root directory. -func appName(path string) (string, error) { - info := filepath.Join(path, "Contents", "Info.plist") - f, err := os.Open(info) - if err != nil { - return "", err - } - defer f.Close() - return ExtractAppName(f) -} - -// pgrep executes ``pgrep'' passing "expr" to it as first argument. -func pgrep(expr string) []string { - p := pipe.Exec("pgrep", expr) - output, err := pipe.Output(p) - if err != nil { - log.Printf("unable to find pids with pgrep: %v", err) - return []string{} - } - - var builder strings.Builder - builder.Write(output) - - trimmed := strings.Trim(builder.String(), "\n") - return strings.Split(trimmed, "\n") -} - -// Windows helpers - -func prepareNFExprWin(s string) string { - // TODO: what if "s" is a symlink? - if !strings.HasSuffix(s, ".exe") { - // we're not able to use something that is not - // an executable name. - log.Printf("\"%s\" does not lead to an .exe", s) - return s - } - if _, err := os.Stat(s); err == nil { - // take only last path component - log.Printf("Taking only last path component of \"%s\"", s) - s = filepath.Base(s) - } - - tasks := tasklist(s) - if len(tasks) == 0 { - log.Printf("cannot find any task associated with %s", s) - return s - } - filtered := FilterTasks(tasks, s) - if len(filtered) == 0 { - log.Printf("cannot find any PID associated with %s", s) - return s - } - pids := make([]string, len(filtered)) - for _, v := range filtered { - pids = append(pids, strconv.Itoa(v.Pid)) - } - - return strings.Join(pids, "|") -} - -// tasks executes the ``tasklist'' command, which is only -// available on windows. -func tasklist(image string) []*Task { - empty := []*Task{} - p := pipe.Exec("tasklist") - output, err := pipe.Output(p) - if err != nil { - log.Printf("unable to execute tasklist: %v", err) - return empty - } - - r := bytes.NewReader(output) - tasks, err := DecodeTasklistOutput(r) - if err != nil { - log.Printf("unable to decode tasklist's output: %v", err) - return empty - } - return tasks -} - -// FilterTasks takes "tasks", iterates over them and filters out tasks -// that do not have their image field == "image". -func FilterTasks(tasks []*Task, image string) []*Task { - acc := make([]*Task, 0, len(tasks)) - for _, v := range tasks { - if v.Image != image { - continue - } - acc = append(acc, v) - } - return acc -} diff --git a/lookup/internal/filter_test.go b/lookup/internal/filter_test.go deleted file mode 100644 index 2d1371e..0000000 --- a/lookup/internal/filter_test.go +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright © 2019 booster authors -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package internal_test - -import ( - "testing" - - "github.com/booster-proj/lsaddr/lookup/internal" -) - -func TestFilterTasks(t *testing.T) { - t.Parallel() - tasks := []*internal.Task{ - newTask("foo", 1), - newTask("foo", 2), - newTask("foo", 3), - newTask("bar", 21), - newTask("bar", 22), - newTask("baz", 31), - } - - assertPids(t, []int{1, 2, 3}, internal.FilterTasks(tasks, "foo")) - assertPids(t, []int{21, 22}, internal.FilterTasks(tasks, "bar")) - assertPids(t, []int{31}, internal.FilterTasks(tasks, "baz")) - assertPids(t, []int{}, internal.FilterTasks(tasks, "invalid")) -} - -// Private helpers - -func newTask(image string, pid int) *internal.Task { - return &internal.Task{ - Image: image, - Pid: pid, - } -} - -func assertPids(t *testing.T, exp []int, prod []*internal.Task) { - if len(exp) != len(prod) { - t.Fatalf("Unexpected list length. Wanted %d, found %d", len(exp), len(prod)) - } - for i := range exp { - if exp[i] != prod[i].Pid { - t.Fatalf("Unexpected item in list. Wanted %v, found %v. Content: %v", exp[i], prod[i].Pid, prod) - } - } -} diff --git a/lookup/internal/lookup.go b/lookup/internal/lookup.go deleted file mode 100644 index a63a27b..0000000 --- a/lookup/internal/lookup.go +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright © 2019 booster authors -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package internal - -import ( - "bytes" - "io" - "log" - "regexp" - - "gopkg.in/pipe.v2" -) - -var Logger *log.Logger - -type Runtime struct { - OFCmd pipe.Pipe // Open Files Command - OFDecoder func(io.Reader) ([]*OpenFile, error) // Open Files Decoder - PrepareNFExprFunc func(string) string -} - -// OpenNetFiles uses ``lsof'' (or its platform dependent equivalent) to find -// the list of open network files. It then filters the result using "rgx": -// each line that does not match is discarded. -func OpenNetFiles(rgx *regexp.Regexp) ([]*OpenFile, error) { - p := pipe.Line( - runtime.OFCmd, - pipe.Filter(func(line []byte) bool { - return rgx.Match(line) - }), - ) - output, err := pipe.Output(p) - if err != nil { - return []*OpenFile{}, err - } - - buf := bytes.NewBuffer(output) - return runtime.OFDecoder(buf) -} diff --git a/lookup/internal/runtime_linux.go b/lookup/internal/runtime_linux.go deleted file mode 100644 index a663098..0000000 --- a/lookup/internal/runtime_linux.go +++ /dev/null @@ -1,30 +0,0 @@ -// +build linux - -// Copyright © 2019 booster authors -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package internal - -import ( - "gopkg.in/pipe.v2" -) - -var runtime = Runtime{ - OFCmd: pipe.Exec("lsof", "-i", "-n", "-P"), - OFDecoder: DecodeLsofOutput, - PrepareNFExprFunc: func(s string) string { - return s - }, -} diff --git a/lookup/lookup.go b/lookup/lookup.go deleted file mode 100644 index 70c3e0d..0000000 --- a/lookup/lookup.go +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright © 2019 booster authors -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package lookup - -import ( - "log" - "net" - - "github.com/booster-proj/lsaddr/lookup/internal" -) - -// NetFile represents a network file. -type NetFile struct { - Cmd string // command associated with Pid - Pid int // pid of the owner - Src net.Addr // source address - Dst net.Addr // destination address -} - -// OpenNetFiles compiles a regular expression out of "s". Some manipulation -// may be performed on "s" before it is compiled, depending on the hosting -// operating system: on macOS for example, if "s" ends with ".app", it -// will be trated as the root path to an application, otherwise "s" will be -// compiled untouched. -// It then uses ``lsof'' (or its platform dependent equivalent) tool to find -// the list of open files, filtering out the list by taking only the lines that -// match against the regular expression built. -func OpenNetFiles(s string) ([]NetFile, error) { - rgx, err := internal.BuildNFFilter(s) - if err != nil { - return []NetFile{}, err - } - - log.Printf("regexp built: \"%s\"", rgx.String()) - - ll, err := internal.OpenNetFiles(rgx) - if err != nil { - return []NetFile{}, err - } - - // map ``internal.OpenFile'' to ``NetFile'' - ff := make([]NetFile, len(ll)) - for i, v := range ll { - src, dst := v.UnmarshalName() - ff[i] = NetFile{ - Cmd: v.Command, - Pid: v.Pid, - Src: src, - Dst: dst, - } - } - return ff, nil -} diff --git a/main.go b/main.go index 6726fe2..c9c7837 100644 --- a/main.go +++ b/main.go @@ -15,7 +15,7 @@ package main -import "github.com/booster-proj/lsaddr/cmd" +import "github.com/jecoz/lsaddr/cmd" // Version and BuildTime are filled in during build by the Makefile var ( diff --git a/onf/.onf.go.swo b/onf/.onf.go.swo new file mode 100644 index 0000000000000000000000000000000000000000..542c840054901b2a7326f1ecd19e7a91b64496c7 GIT binary patch literal 12288 zcmeI2&ubn<7{{kYYF@Qk6bl|oA0t8{aW`o%H5FkS^CqvRZ^HY-5TwGqyR+|(-FN0? zb~Z0jVlVyyUj2a{#e(1uJbLlsQNiL#EQrN}D0)&)`kn0?Us3~wf``&s_++!Y&pi9g zXJ!XNrZ@jW<2;=S<{6G-jD7$9dl&xbojCmYkg8^*d~^nH{cC;1Kxl);0<^K-hemY4R{0Iz-?%NCyc3k89QJ@5ut1uNhzSOg2;2`~)^oCJU2JwJhu!Rw$0&VdtP3b;D% z0>7iCufbQ~Q}7YUz$4(wJ&b(_E`vUJ9Q;_rJ#ZPk1dfA;K@CiSzo!`c6Z{5#1;2nV z!DrwSXn}cf4E%gIW8Z=+;2ZD>co)0_-Uj~g2D|}pz#H%e{!atDtY(BQjiOZ8IE(Up zX12!CQpHsN_Xf?pcwt?ob0mz>W|m%M4DFVT;8rA&B`XIa=->ydJzH2(!Uhrc*HIW! zA_^-Nw^A!|d+tP0uv**1`+^ExnpjMpZ^(qCvQ!;NGQ_n`$!3D|NC|^?4-zV@k*bdv zL||**06PeY7ln?cD~m?h^he0VYArt67i=ciHsh9tMki&ACN`4=p~Nz`%Yq0_Eq z8C&!juW1Tfm$^X95Me^ug1GC+NSlk4;z_{REUQgBA=5bH(KM5Tp*EIg7#s5VBJZOU zDrGJm&ow$w6VXD}yrFeOP3-MpP8u68KL zq%yQEOo3Zff8BJf8+KvQAa(T>m4HtttcEm^smtS1En(&u+0QI} v@d2HhUwDe1b9->v?(RWr#=a%vfPmdQWyNUL{fhD5mBp_Z?vuJhSB!rE&`ucJ literal 0 HcmV?d00001 diff --git a/onf/internal/lsof/lsof.go b/onf/internal/lsof/lsof.go index 133b26f..ecf4cfe 100644 --- a/onf/internal/lsof/lsof.go +++ b/onf/internal/lsof/lsof.go @@ -29,6 +29,7 @@ import ( ) type OpenFile struct { + Raw string Command string Pid int User string @@ -91,6 +92,7 @@ func ParseOpenFile(line string) (*OpenFile, error) { } onf := &OpenFile{ + Raw: line, Command: chunks[0], Pid: pid, User: chunks[2], diff --git a/onf/internal/netstat/netstat.go b/onf/internal/netstat/netstat.go index e44a7bc..dbbfbaf 100644 --- a/onf/internal/netstat/netstat.go +++ b/onf/internal/netstat/netstat.go @@ -25,6 +25,7 @@ import ( ) type ActiveConnection struct { + Raw string Proto string SrcAddr net.Addr DstAddr net.Addr @@ -32,6 +33,17 @@ type ActiveConnection struct { Pid int } +func Run() ([]ActiveConnection, error) { + acc := []ActiveConnection{} + p := pipe.Exec("netstat", "-nao") + out, err := pipe.OutputTimeout(p, time.Millisecond*100) + if err != nil { + return acc, fmt.Errorf("unable to run netstat: %w", err) + } + buf := bytes.NewBuffer(out) + return ParseOutput(buf) +} + // ParseOutput expects "r" to contain the output of // a ``netstat -nao'' call. The output is splitted into lines, and // each line that ``ParseActiveConnection'' is able to Unmarshal is @@ -79,6 +91,7 @@ func ParseActiveConnection(line string) (*ActiveConnection, error) { } ac := &ActiveConnection{ + Raw: line, Proto: proto, SrcAddr: src, DstAddr: dst, diff --git a/onf/onf.go b/onf/onf.go index eb640c1..23342b4 100644 --- a/onf/onf.go +++ b/onf/onf.go @@ -21,7 +21,7 @@ import ( ) // NetFile represents a network file. -type Onf struct { +type ONF struct { Raw string // raw string that produced this result Cmd string // command associated with Pid Pid int // pid of the owner diff --git a/lookup/internal/runtime_darwin.go b/onf/runtime_unix.go similarity index 61% rename from lookup/internal/runtime_darwin.go rename to onf/runtime_unix.go index 20d6452..e210775 100644 --- a/lookup/internal/runtime_darwin.go +++ b/onf/runtime_unix.go @@ -1,6 +1,4 @@ -// +build darwin - -// Copyright © 2019 booster authors +// Copyright © 2019 Jecoz // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by @@ -15,14 +13,31 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -package internal +// +build !windows + +package onf import ( - "gopkg.in/pipe.v2" + "time" + + "github.com/jecoz/lsaddr/onf/internal/lsof" ) -var runtime = Runtime{ - OFCmd: pipe.Exec("lsof", "-i", "-n", "-P"), - OFDecoder: DecodeLsofOutput, - PrepareNFExprFunc: prepareNFExprDarwin, +func fetchAll() ([]ONF, error) { + set, err := lsof.Run() + if err != nil { + return []ONF{}, err + } + mapped := make([]ONF, len(set)) + for i, v := range set { + mapped[i] = ONF{ + Raw: v.Raw, + Cmd: v.Command, + Pid: v.Pid, + Src: v.SrcAddr, + Dst: v.DstAddr, + CreatedAt: time.Now(), + } + } + return mapped, nil } diff --git a/lookup/internal/runtime_windows.go b/onf/runtime_windows.go similarity index 65% rename from lookup/internal/runtime_windows.go rename to onf/runtime_windows.go index 9a891b2..2c3fef6 100644 --- a/lookup/internal/runtime_windows.go +++ b/onf/runtime_windows.go @@ -1,6 +1,4 @@ -// +build windows - -// Copyright © 2019 booster authors +// Copyright © 2019 Jecoz // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by @@ -15,14 +13,27 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -package internal +// +build windows + +package onf import ( - "gopkg.in/pipe.v2" + "github.com/jecoz/lsaddr/onf/internal/netstat" ) -var runtime = Runtime{ - OFCmd: pipe.Exec("netstat", "-ano"), - OFDecoder: DecodeNetstatOutput, - PrepareNFExprFunc: prepareNFExprWin, +func fetchAll() ([]ONF, error) { + set, err := netstat.Run() + if err != nil { + return []ONF{}, err + } + mapped := make([]ONF, len(set)) + for i, v := range set { + mapped[i] = ONF{ + Raw: v.Raw, + Pid: v.Pid, + Src: v.SrcAddr, + Dst: v.DstAddr, + CreatedAt: time.Now(), + } + } } diff --git a/onf/set.go b/onf/set.go index c333472..1dbae61 100644 --- a/onf/set.go +++ b/onf/set.go @@ -15,7 +15,6 @@ package onf -func FetchAll() ([]Onf, error) { - internal.ToolSet.FetchAll() - +func FetchAll() ([]ONF, error) { + return fetchAll() } From 9ab65e4cd224f6d4fcf5547b3ba2f4e35f8492d0 Mon Sep 17 00:00:00 2001 From: Daniel Morandini Date: Thu, 10 Oct 2019 23:58:50 +0200 Subject: [PATCH 09/26] Fix windows build --- onf/.onf.go.swo | Bin 12288 -> 0 bytes onf/internal/netstat/netstat.go | 3 +++ onf/runtime_windows.go | 3 +++ 3 files changed, 6 insertions(+) delete mode 100644 onf/.onf.go.swo diff --git a/onf/.onf.go.swo b/onf/.onf.go.swo deleted file mode 100644 index 542c840054901b2a7326f1ecd19e7a91b64496c7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeI2&ubn<7{{kYYF@Qk6bl|oA0t8{aW`o%H5FkS^CqvRZ^HY-5TwGqyR+|(-FN0? zb~Z0jVlVyyUj2a{#e(1uJbLlsQNiL#EQrN}D0)&)`kn0?Us3~wf``&s_++!Y&pi9g zXJ!XNrZ@jW<2;=S<{6G-jD7$9dl&xbojCmYkg8^*d~^nH{cC;1Kxl);0<^K-hemY4R{0Iz-?%NCyc3k89QJ@5ut1uNhzSOg2;2`~)^oCJU2JwJhu!Rw$0&VdtP3b;D% z0>7iCufbQ~Q}7YUz$4(wJ&b(_E`vUJ9Q;_rJ#ZPk1dfA;K@CiSzo!`c6Z{5#1;2nV z!DrwSXn}cf4E%gIW8Z=+;2ZD>co)0_-Uj~g2D|}pz#H%e{!atDtY(BQjiOZ8IE(Up zX12!CQpHsN_Xf?pcwt?ob0mz>W|m%M4DFVT;8rA&B`XIa=->ydJzH2(!Uhrc*HIW! zA_^-Nw^A!|d+tP0uv**1`+^ExnpjMpZ^(qCvQ!;NGQ_n`$!3D|NC|^?4-zV@k*bdv zL||**06PeY7ln?cD~m?h^he0VYArt67i=ciHsh9tMki&ACN`4=p~Nz`%Yq0_Eq z8C&!juW1Tfm$^X95Me^ug1GC+NSlk4;z_{REUQgBA=5bH(KM5Tp*EIg7#s5VBJZOU zDrGJm&ow$w6VXD}yrFeOP3-MpP8u68KL zq%yQEOo3Zff8BJf8+KvQAa(T>m4HtttcEm^smtS1En(&u+0QI} v@d2HhUwDe1b9->v?(RWr#=a%vfPmdQWyNUL{fhD5mBp_Z?vuJhSB!rE&`ucJ diff --git a/onf/internal/netstat/netstat.go b/onf/internal/netstat/netstat.go index dbbfbaf..54077e8 100644 --- a/onf/internal/netstat/netstat.go +++ b/onf/internal/netstat/netstat.go @@ -15,13 +15,16 @@ package netstat import ( + "bytes" "fmt" "io" "log" "net" "strconv" + "time" "github.com/jecoz/lsaddr/onf/internal" + "gopkg.in/pipe.v2" ) type ActiveConnection struct { diff --git a/onf/runtime_windows.go b/onf/runtime_windows.go index 2c3fef6..3460ca0 100644 --- a/onf/runtime_windows.go +++ b/onf/runtime_windows.go @@ -18,6 +18,8 @@ package onf import ( + "time" + "github.com/jecoz/lsaddr/onf/internal/netstat" ) @@ -36,4 +38,5 @@ func fetchAll() ([]ONF, error) { CreatedAt: time.Now(), } } + return mapped, nil } From 2ac0099537059a2d9d14145e6bc21bf3d4c74631 Mon Sep 17 00:00:00 2001 From: Daniel Morandini Date: Thu, 10 Oct 2019 23:58:59 +0200 Subject: [PATCH 10/26] Format codebase --- onf/onf.go | 10 +++++----- onf/runtime_unix.go | 10 +++++----- onf/runtime_windows.go | 8 ++++---- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/onf/onf.go b/onf/onf.go index 23342b4..0dc48b9 100644 --- a/onf/onf.go +++ b/onf/onf.go @@ -22,10 +22,10 @@ import ( // NetFile represents a network file. type ONF struct { - Raw string // raw string that produced this result - Cmd string // command associated with Pid - Pid int // pid of the owner - Src net.Addr // source address - Dst net.Addr // destination address + Raw string // raw string that produced this result + Cmd string // command associated with Pid + Pid int // pid of the owner + Src net.Addr // source address + Dst net.Addr // destination address CreatedAt time.Time } diff --git a/onf/runtime_unix.go b/onf/runtime_unix.go index e210775..323865f 100644 --- a/onf/runtime_unix.go +++ b/onf/runtime_unix.go @@ -31,11 +31,11 @@ func fetchAll() ([]ONF, error) { mapped := make([]ONF, len(set)) for i, v := range set { mapped[i] = ONF{ - Raw: v.Raw, - Cmd: v.Command, - Pid: v.Pid, - Src: v.SrcAddr, - Dst: v.DstAddr, + Raw: v.Raw, + Cmd: v.Command, + Pid: v.Pid, + Src: v.SrcAddr, + Dst: v.DstAddr, CreatedAt: time.Now(), } } diff --git a/onf/runtime_windows.go b/onf/runtime_windows.go index 3460ca0..d453d80 100644 --- a/onf/runtime_windows.go +++ b/onf/runtime_windows.go @@ -31,10 +31,10 @@ func fetchAll() ([]ONF, error) { mapped := make([]ONF, len(set)) for i, v := range set { mapped[i] = ONF{ - Raw: v.Raw, - Pid: v.Pid, - Src: v.SrcAddr, - Dst: v.DstAddr, + Raw: v.Raw, + Pid: v.Pid, + Src: v.SrcAddr, + Dst: v.DstAddr, CreatedAt: time.Now(), } } From b1b664da0b2cac5e7656df486d2bec2e5b40a1a3 Mon Sep 17 00:00:00 2001 From: Daniel Morandini Date: Tue, 15 Oct 2019 14:11:49 +0200 Subject: [PATCH 11/26] Introduce Filter function. Remove set --- cmd/root.go | 14 +++++++++----- onf/onf.go | 21 ++++++++++++++++++++- onf/set.go | 20 -------------------- 3 files changed, 29 insertions(+), 26 deletions(-) delete mode 100644 onf/set.go diff --git a/cmd/root.go b/cmd/root.go index df04ea6..345b32b 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -52,11 +52,15 @@ var rootCmd = &cobra.Command{ fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } - //fset, err := onf.Filter(set, s) - //if err != nil { - // fmt.Fprintf(os.Stderr, "error: unable to filter with %s: %w\n", s, err") - // os.Exit(1) - //} + pivot := "*" + if len(args) > 0 { + pivot = args[0] + } + set, err = onf.Filter(set, pivot) + if err != nil { + fmt.Fprintf(os.Stderr, "error: unable to filter with %s: %v\n", pivot, err) + os.Exit(1) + } log.Printf("# of open network files: %d", len(set)) for _, v := range set { diff --git a/onf/onf.go b/onf/onf.go index 0dc48b9..f9d7879 100644 --- a/onf/onf.go +++ b/onf/onf.go @@ -16,11 +16,12 @@ package onf import ( + "fmt" "net" "time" ) -// NetFile represents a network file. +// ONF represents an open network file. type ONF struct { Raw string // raw string that produced this result Cmd string // command associated with Pid @@ -29,3 +30,21 @@ type ONF struct { Dst net.Addr // destination address CreatedAt time.Time } + +// FetchAll retrieves the complete list of open network files. It does +// so using an external tool, `netstat` for windows and `lsof` for unix +// based systems. +func FetchAll() ([]ONF, error) { + // fetchAll implementations may be found insiede the + // runtime_*.go files. + return fetchAll() +} + +func Filter(set []ONF, pivot string) ([]ONF, error) { + if pivot == "" || pivot == "*" { + return set, nil + } + acc := make([]ONF, 0, len(set)) + return acc, fmt.Errorf("not implemented yet") +} + diff --git a/onf/set.go b/onf/set.go deleted file mode 100644 index 1dbae61..0000000 --- a/onf/set.go +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright © 2019 Jecoz -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package onf - -func FetchAll() ([]ONF, error) { - return fetchAll() -} From a503cbe895d97710987e8d24ab7d93aa4f41ebc6 Mon Sep 17 00:00:00 2001 From: Daniel Morandini Date: Tue, 15 Oct 2019 16:13:29 +0200 Subject: [PATCH 12/26] Remove usused tool --- onf/internal/tasklist/tasklist.go | 125 ------------------------- onf/internal/tasklist/tasklist_test.go | 94 ------------------- 2 files changed, 219 deletions(-) delete mode 100644 onf/internal/tasklist/tasklist.go delete mode 100644 onf/internal/tasklist/tasklist_test.go diff --git a/onf/internal/tasklist/tasklist.go b/onf/internal/tasklist/tasklist.go deleted file mode 100644 index f2da8ce..0000000 --- a/onf/internal/tasklist/tasklist.go +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright © 2019 Jecoz -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package tasklist - -import ( - "bytes" - "fmt" - "io" - "log" - "strconv" - "strings" - - "github.com/jecoz/lsaddr/onf/internal" -) - -type Task struct { - Pid int - Image string -} - -// ParseOutput expects "r" to contain the output of -// a ``tasklist'' call. The output is splitted into lines, and -// each line that ``UnmarshakTasklistLine'' is able to Unmarshal is -// appended to the final output, with the expections of the first lines -// that come before the separator line composed by only "=". Those lines -// are considered part of the "header". -// -// As of ``ParseTask'', this function returns an error only -// if reading from "r" produces an error different from ``io.EOF''. -func ParseOutput(r io.Reader) ([]Task, error) { - ll := []Task{} - delim := "=" - headerTrimmed := false - segLengths := []int{} - err := internal.ScanLines(r, func(line string) error { - if !headerTrimmed { - if strings.HasPrefix(line, delim) && strings.HasSuffix(line, delim) { - headerTrimmed = true - // This is the header delimiter! - chunks, err := internal.ChunkLine(line, " ", 5) - if err != nil { - return fmt.Errorf("unexpected header format: %w", err) - } - for _, v := range chunks { - segLengths = append(segLengths, len(v)) - } - } - // Still in the header - return nil - } - - t, err := ParseTask(line, segLengths) - if err != nil { - log.Printf("skipping tasklist line \"%s\": %v", line, err) - return nil - } - ll = append(ll, *t) - return nil - }) - return ll, err -} - -// ParseTask expectes "line" to be a single line output from -// ``tasklist'' call. The line is unmarshaled into a ``Task'' and the operation -// is performed by readying bytes equal to "segLengths"[i], in order. "segLengths" -// should be computed using the header delimitator and counting the number of -// "=" in each segment of the header (split it by " ") -// -// "line" should not end with a "\n" delimitator, otherwise it will end up in the last -// unmarshaled item. -// The "header" lines (see below) should not be passed to this function. -// -// Example header: -// Image Name PID Session Name Session# Mem Usage -// ========================= ======== ================ =========== ============ -// -// Example line: -// svchost.exe 940 Services 0 52,336 K -func ParseTask(line string, segLengths []int) (*Task, error) { - buf := bytes.NewBufferString(line) - p := make([]byte, 32) - - var image, pidRaw string - for i, v := range segLengths[:2] { - n, err := buf.Read(p[:v+1]) - if err != nil { - return nil, fmt.Errorf("unable to read tasklist chunk: %w", err) - } - s := strings.Trim(string(p[:n]), " ") - switch i { - case 0: - image = s - case 1: - pidRaw = s - default: - } - } - if image == "" { - return nil, fmt.Errorf("couldn't decode image from line") - } - if pidRaw == "" { - return nil, fmt.Errorf("couldn't decode pid from line") - } - pid, err := strconv.Atoi(pidRaw) - if err != nil { - return nil, fmt.Errorf("error parsing pid: %w", err) - } - - return &Task{ - Image: image, - Pid: pid, - }, nil -} diff --git a/onf/internal/tasklist/tasklist_test.go b/onf/internal/tasklist/tasklist_test.go deleted file mode 100644 index 64954f1..0000000 --- a/onf/internal/tasklist/tasklist_test.go +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright © 2019 Jecoz -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package tasklist - -import ( - "bytes" - "reflect" - "testing" -) - -const tasklistExample = ` -Image Name PID Session Name Session# Mem Usage -========================= ======== ================ =========== ============ -System Idle Process 0 Services 0 4 K -System 4 Services 0 15,376 K -smss.exe 296 Services 0 1,008 K -csrss.exe 380 Services 0 4,124 K -wininit.exe 452 Services 0 4,828 K -services.exe 588 Services 0 6,284 K -lsass.exe 596 Services 0 12,600 K -svchost.exe 688 Services 0 17,788 K -svchost.exe 748 Services 0 8,980 K -svchost.exe 888 Services 0 21,052 K -svchost.exe 904 Services 0 21,200 K -svchost.exe 940 Services 0 52,336 K -WUDFHost.exe 464 Services 0 6,128 K -svchost.exe 1036 Services 0 14,524 K -svchost.exe 1044 Services 0 27,488 K -svchost.exe 1104 Services 0 28,428 K -WUDFHost.exe 1240 Services 0 6,888 K -` - -func TestParseOutput(t *testing.T) { - t.Parallel() - buf := bytes.NewBufferString(tasklistExample) - ll, err := ParseOutput(buf) - if err != nil { - t.Fatalf("Unexpected error: %v", err) - } - if len(ll) != 17 { - t.Fatalf("Unexpected ll length: wanted 17, found: %d: %v", len(ll), ll) - } -} - -func TestParseTask(t *testing.T) { - t.Parallel() - tt := []struct { - line string - segs []int - image string - pid int - }{ - { - line: "svchost.exe 940 Services 0 52,336 K", - segs: []int{25, 8, 16, 11, 12}, - image: "svchost.exe", - pid: 940, - }, - { - line: "System Idle Process 0 Services 0 4 K", - segs: []int{25, 8, 16, 11, 12}, - image: "System Idle Process", - pid: 0, - }, - } - - for i, v := range tt { - task, err := ParseTask(v.line, v.segs) - if err != nil { - t.Fatalf("%d: unexpected error: %v", i, err) - } - assert(t, v.image, task.Image) - assert(t, v.pid, task.Pid) - } -} - -func assert(t *testing.T, exp, x interface{}) { - if !reflect.DeepEqual(exp, x) { - t.Fatalf("Assert failed: expected %v, found %v", exp, x) - } -} From f31592e854e161747dd07754d41d7236eafcf34e Mon Sep 17 00:00:00 2001 From: Daniel Morandini Date: Tue, 15 Oct 2019 16:27:20 +0200 Subject: [PATCH 13/26] Extract packages for handling external tools --- {onf/internal => internal}/utils.go | 0 {onf/internal/lsof => lsof}/lsof.go | 34 +++++++++++++------ {onf/internal/lsof => lsof}/lsof_test.go | 0 {onf/internal/netstat => netstat}/netstat.go | 2 +- .../netstat => netstat}/netstat_test.go | 0 5 files changed, 25 insertions(+), 11 deletions(-) rename {onf/internal => internal}/utils.go (100%) rename {onf/internal/lsof => lsof}/lsof.go (86%) rename {onf/internal/lsof => lsof}/lsof_test.go (100%) rename {onf/internal/netstat => netstat}/netstat.go (98%) rename {onf/internal/netstat => netstat}/netstat_test.go (100%) diff --git a/onf/internal/utils.go b/internal/utils.go similarity index 100% rename from onf/internal/utils.go rename to internal/utils.go diff --git a/onf/internal/lsof/lsof.go b/lsof/lsof.go similarity index 86% rename from onf/internal/lsof/lsof.go rename to lsof/lsof.go index ecf4cfe..42e101f 100644 --- a/onf/internal/lsof/lsof.go +++ b/lsof/lsof.go @@ -15,6 +15,7 @@ package lsof import ( + "bufio" "bytes" "fmt" "io" @@ -24,7 +25,7 @@ import ( "strings" "time" - "github.com/jecoz/lsaddr/onf/internal" + "github.com/jecoz/lsaddr/internal" "gopkg.in/pipe.v2" ) @@ -60,18 +61,31 @@ func Run() ([]OpenFile, error) { // different from ``io.EOF''. func ParseOutput(r io.Reader) ([]OpenFile, error) { set := []OpenFile{} - err := internal.ScanLines(r, func(line string) error { - onf, err := ParseOpenFile(line) + err := scanLines(r, func(line string) error { + of, err := ParseOpenFile(line) if err != nil { - log.Printf("skipping onf \"%s\": %v", line, err) + log.Printf("skipping open file \"%s\": %v", line, err) return nil } - set = append(set, *onf) + set = append(set, *of) return nil }) return set, err } +func scanLines(r io.Reader, f func(string) error) error { + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := scanner.Text() + line = strings.Trim(line, "\n") + line = strings.Trim(line, "\r") + if err := f(line); err != nil { + return err + } + } + return scanner.Err() +} + // ParseOpenFile expectes "line" to be a single line output from // ``lsof -i -n -P'' call. The line is unmarshaled into an ``OpenFile'' // only if is splittable by " " into a slice of at least 9 items. "line" should @@ -91,7 +105,7 @@ func ParseOpenFile(line string) (*OpenFile, error) { return nil, fmt.Errorf("error parsing pid: %w", err) } - onf := &OpenFile{ + of := &OpenFile{ Raw: line, Command: chunks[0], Pid: pid, @@ -104,13 +118,13 @@ func ParseOpenFile(line string) (*OpenFile, error) { if err != nil { return nil, fmt.Errorf("error parsing name: %w", err) } - onf.SrcAddr = src - onf.DstAddr = dst + of.SrcAddr = src + of.DstAddr = dst if len(chunks) >= 10 { - onf.State = chunks[9] + of.State = chunks[9] } - return onf, nil + return of, nil } // ParseName parses `lsof`'s name field, which by default is in the form: diff --git a/onf/internal/lsof/lsof_test.go b/lsof/lsof_test.go similarity index 100% rename from onf/internal/lsof/lsof_test.go rename to lsof/lsof_test.go diff --git a/onf/internal/netstat/netstat.go b/netstat/netstat.go similarity index 98% rename from onf/internal/netstat/netstat.go rename to netstat/netstat.go index 54077e8..20ce38b 100644 --- a/onf/internal/netstat/netstat.go +++ b/netstat/netstat.go @@ -23,7 +23,7 @@ import ( "strconv" "time" - "github.com/jecoz/lsaddr/onf/internal" + "github.com/jecoz/lsaddr/internal" "gopkg.in/pipe.v2" ) diff --git a/onf/internal/netstat/netstat_test.go b/netstat/netstat_test.go similarity index 100% rename from onf/internal/netstat/netstat_test.go rename to netstat/netstat_test.go From a0a0b1ad286cd8861042d0c769e5c587bda452a2 Mon Sep 17 00:00:00 2001 From: Daniel Morandini Date: Tue, 15 Oct 2019 16:27:46 +0200 Subject: [PATCH 14/26] Refactor flags and usage --- cmd/root.go | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 345b32b..5b592fd 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -25,7 +25,7 @@ import ( "github.com/spf13/cobra" ) -var debug, raw bool +var verbose bool var format string // rootCmd represents the base command when called without any subcommands @@ -33,10 +33,10 @@ var rootCmd = &cobra.Command{ Use: "lsaddr", Short: "Show a subset of all network addresses being used by your apps", Long: usage, - Args: cobra.MaximumNArgs(1), + Args: cobra.ExactArgs(1), PersistentPreRun: func(cmd *cobra.Command, args []string) { log.SetPrefix("[lsaddr] ") - if !debug { + if !verbose { log.SetOutput(ioutil.Discard) } @@ -79,9 +79,8 @@ func Execute() { } func init() { - rootCmd.PersistentFlags().BoolVarP(&debug, "debug", "d", false, "increment logger verbosity") - rootCmd.PersistentFlags().BoolVarP(&raw, "raw", "r", false, "increment logger verbosity") - rootCmd.PersistentFlags().StringVarP(&format, "format", "f", "csv", "choose format of output produced ") + rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Increment logger verbosity.") + rootCmd.PersistentFlags().StringVarP(&format, "format", "f", "csv", "Choose output format.") } func validateFormat(format string) error { @@ -94,14 +93,9 @@ func validateFormat(format string) error { } const usage = ` -'lsaddr' takes the entire list of open network files and filters it out -using the argument provided, which can either be: +'lsaddr' -- "*.app" (macOS): It will be recognised as the path leading to the root directory of -an Application. The tool will then: - 1. Extract the Application's CFBundleExecutable value - 2. Use it to find the list of Pids associated with the program - 3. Build a regular expression out of them +TODO: describe - a regular expression: which will be used to filter out the list of open files. Each line that does not match against the regex will be discarded (e.g. "chrome.exe", "Safari", "104|405"). From 154363487967411f1bad73b71ef8306137b18166 Mon Sep 17 00:00:00 2001 From: Daniel Morandini Date: Tue, 15 Oct 2019 16:27:57 +0200 Subject: [PATCH 15/26] Add filtering --- onf/onf.go | 25 +++++++++++++++++++++++-- onf/runtime_unix.go | 2 +- onf/runtime_windows.go | 2 +- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/onf/onf.go b/onf/onf.go index f9d7879..36a6f03 100644 --- a/onf/onf.go +++ b/onf/onf.go @@ -17,7 +17,9 @@ package onf import ( "fmt" + "log" "net" + "regexp" "time" ) @@ -31,6 +33,10 @@ type ONF struct { CreatedAt time.Time } +func (f ONF) String() string { + return fmt.Sprintf("{Cmd: %s, Pid: %d, Conn: %v->%v}", f.Cmd, f.Pid, f.Src, f.Dst) +} + // FetchAll retrieves the complete list of open network files. It does // so using an external tool, `netstat` for windows and `lsof` for unix // based systems. @@ -40,11 +46,26 @@ func FetchAll() ([]ONF, error) { return fetchAll() } +// Filter returns a filtered list of open network files removing the elements +// that do not "match" with `pivot`. How the filtering changes depending on the +// hosting operating system. +// If an error occurs, it is returned together with the original list. func Filter(set []ONF, pivot string) ([]ONF, error) { if pivot == "" || pivot == "*" { return set, nil } + + rgx, err := regexp.Compile(pivot) + if err != nil { + return set, fmt.Errorf("unable to filter open network file set: %w", err) + } acc := make([]ONF, 0, len(set)) - return acc, fmt.Errorf("not implemented yet") + for _, v := range set { + if !rgx.MatchString(v.Raw) { + log.Printf("[DEBUG] filtering open network file: %v", v) + continue + } + acc = append(acc, v) + } + return acc, nil } - diff --git a/onf/runtime_unix.go b/onf/runtime_unix.go index 323865f..72316a5 100644 --- a/onf/runtime_unix.go +++ b/onf/runtime_unix.go @@ -20,7 +20,7 @@ package onf import ( "time" - "github.com/jecoz/lsaddr/onf/internal/lsof" + "github.com/jecoz/lsaddr/lsof" ) func fetchAll() ([]ONF, error) { diff --git a/onf/runtime_windows.go b/onf/runtime_windows.go index d453d80..1ae39ce 100644 --- a/onf/runtime_windows.go +++ b/onf/runtime_windows.go @@ -20,7 +20,7 @@ package onf import ( "time" - "github.com/jecoz/lsaddr/onf/internal/netstat" + "github.com/jecoz/lsaddr/netstat" ) func fetchAll() ([]ONF, error) { From 14977806cf441543ec4c87f79f2b75bb14292a8a Mon Sep 17 00:00:00 2001 From: Daniel Morandini Date: Wed, 16 Oct 2019 09:10:56 +0200 Subject: [PATCH 16/26] Print version using flags --- cmd/root.go | 24 +++++++++++++++++++++--- cmd/version.go | 41 ----------------------------------------- 2 files changed, 21 insertions(+), 44 deletions(-) delete mode 100644 cmd/version.go diff --git a/cmd/root.go b/cmd/root.go index 5b592fd..b44e6d5 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -25,15 +25,26 @@ import ( "github.com/spf13/cobra" ) -var verbose bool -var format string +// Build information. +var ( + Version = "N/A" + Commit = "N/A" + BuildTime = "N/A" +) + +// Flags. +var ( + verbose bool + version bool + format string +) // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ Use: "lsaddr", Short: "Show a subset of all network addresses being used by your apps", Long: usage, - Args: cobra.ExactArgs(1), + Args: cobra.MaximumNArgs(1), PersistentPreRun: func(cmd *cobra.Command, args []string) { log.SetPrefix("[lsaddr] ") if !verbose { @@ -47,6 +58,11 @@ var rootCmd = &cobra.Command{ } }, Run: func(cmd *cobra.Command, args []string) { + if version { + fmt.Printf("Version: %s, Commit: %s, Built at: %s\n\n", Version, Commit, BuildTime) + os.Exit(0) + } + set, err := onf.FetchAll() if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) @@ -66,6 +82,7 @@ var rootCmd = &cobra.Command{ for _, v := range set { fmt.Printf("%v\n", v) } + os.Exit(0) }, } @@ -80,6 +97,7 @@ func Execute() { func init() { rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Increment logger verbosity.") + rootCmd.PersistentFlags().BoolVarP(&version, "version", "", false, "Print build information such as version, commit and build time.") rootCmd.PersistentFlags().StringVarP(&format, "format", "f", "csv", "Choose output format.") } diff --git a/cmd/version.go b/cmd/version.go deleted file mode 100644 index 0c1c1a3..0000000 --- a/cmd/version.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright © 2019 booster authors -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package cmd - -import ( - "fmt" - - "github.com/spf13/cobra" -) - -var ( - Version = "N/A" - Commit = "N/A" - BuildTime = "N/A" -) - -// versionCmd represents the version command -var versionCmd = &cobra.Command{ - Use: "version", - Short: "print version information", - Run: func(cmd *cobra.Command, args []string) { - fmt.Printf("Version: %s, Commit: %s, Built at: %s\n\n", Version, Commit, BuildTime) - }, -} - -func init() { - rootCmd.AddCommand(versionCmd) -} From 9f17de2c6ae16daec5e7f8564b671a0fb6cd6ffd Mon Sep 17 00:00:00 2001 From: Daniel Morandini Date: Wed, 16 Oct 2019 09:11:07 +0200 Subject: [PATCH 17/26] Improve doc --- onf/onf.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/onf/onf.go b/onf/onf.go index 36a6f03..f9b9fcc 100644 --- a/onf/onf.go +++ b/onf/onf.go @@ -46,9 +46,8 @@ func FetchAll() ([]ONF, error) { return fetchAll() } -// Filter returns a filtered list of open network files removing the elements -// that do not "match" with `pivot`. How the filtering changes depending on the -// hosting operating system. +// Filter takes `pivot` and creates a compiled regex out of it. It then uses +// it to filter `set`, removing every open network file that do not match. // If an error occurs, it is returned together with the original list. func Filter(set []ONF, pivot string) ([]ONF, error) { if pivot == "" || pivot == "*" { From a9b9daa16d4c310e3e2bccf5def4e83c9cf3c677 Mon Sep 17 00:00:00 2001 From: Daniel Morandini Date: Wed, 16 Oct 2019 09:29:08 +0200 Subject: [PATCH 18/26] Format --- cmd/root.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/root.go b/cmd/root.go index b44e6d5..a6c4734 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -36,7 +36,7 @@ var ( var ( verbose bool version bool - format string + format string ) // rootCmd represents the base command when called without any subcommands From 6ee0de3ed81236a7ad0593e99d0e0b9c382d1e1d Mon Sep 17 00:00:00 2001 From: Daniel Morandini Date: Wed, 16 Oct 2019 09:29:15 +0200 Subject: [PATCH 19/26] Fix test --- lsof/lsof_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lsof/lsof_test.go b/lsof/lsof_test.go index bfb9a6e..e740c42 100644 --- a/lsof/lsof_test.go +++ b/lsof/lsof_test.go @@ -73,7 +73,7 @@ func TestParseName(t *testing.T) { dst string net string }{ - {"TCP", "127.0.0.1:49161->127.0.01:9090", "127.0.0.1:49161", "127.0.0.1:9090", "tcp"}, + {"TCP", "127.0.0.1:49161->127.0.0.1:9090", "127.0.0.1:49161", "127.0.0.1:9090", "tcp"}, {"TCP", "127.0.0.1:5432", "127.0.0.1:5432", "", "tcp"}, {"UDP", "192.168.0.61:50940->192.168.0.2:53", "192.168.0.61:50940", "192.168.0.2:53", "udp"}, {"TCP", "[fe80:c::d5d5:601e:981b:c79d]:1024->[fe80:c::f9b9:5ecb:eeca:58e9]:1024", "[fe80:c::d5d5:601e:981b:c79d]:1024", "[fe80:c::f9b9:5ecb:eeca:58e9]:1024", "tcp"}, From 40953bcc085f109d9b810507311a42eb1cf3a1a9 Mon Sep 17 00:00:00 2001 From: Daniel Morandini Date: Wed, 16 Oct 2019 11:49:46 +0200 Subject: [PATCH 20/26] Restore csv encoder --- csv/encoder.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/csv/encoder.go b/csv/encoder.go index c575f5f..e21e814 100644 --- a/csv/encoder.go +++ b/csv/encoder.go @@ -1,5 +1,4 @@ -// Copyright © 2019 booster authors -// +// Copyright © 2019 Jecoz // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or @@ -20,7 +19,7 @@ import ( "io" "strconv" - "github.com/booster-proj/lsaddr/lookup" + "github.com/jecoz/lsaddr/onf" ) // Encoder returns an Encoder which encodes a list @@ -37,7 +36,7 @@ func NewEncoder(w io.Writer) *Encoder { // Encode writes `l` into encoder's writer in CSV format. Some data may have been // written to the writer even upon error. -func (e *Encoder) Encode(l []lookup.NetFile) error { +func (e *Encoder) Encode(l []onf.ONF) error { header := []string{"PID", "CMD", "NET", "SRC", "DST"} if err := e.w.Write(header); err != nil { return err From a9bd7ad515ea647a5a9033b6e3795be405854522 Mon Sep 17 00:00:00 2001 From: Daniel Morandini Date: Wed, 16 Oct 2019 11:49:57 +0200 Subject: [PATCH 21/26] Add bpf encoder --- bpf/encoder.go | 43 +++++++++++++++++++++++++++++++++++++++++++ bpf/expr_test.go | 2 +- 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 bpf/encoder.go diff --git a/bpf/encoder.go b/bpf/encoder.go new file mode 100644 index 0000000..262200e --- /dev/null +++ b/bpf/encoder.go @@ -0,0 +1,43 @@ +// Copyright © 2019 Jecoz +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package bpf + +import ( + "fmt" + "io" + + "github.com/jecoz/lsaddr/onf" +) + +type Encoder struct { + w io.Writer +} + +func NewEncoder(w io.Writer) *Encoder { + return &Encoder{w: w} +} + +func (e *Encoder) Encode(set []onf.ONF) error { + var expr Expr + for _, v := range set { + src := string(FromAddr(NODIR, v.Src).Wrap()) + dst := string(FromAddr(NODIR, v.Dst).Wrap()) + expr = expr.Or(src).Or(dst) + } + if _, err := io.Copy(e.w, expr.NewReader()); err != nil { + return fmt.Errorf("unable to encode open network files: %w", err) + } + return nil +} diff --git a/bpf/expr_test.go b/bpf/expr_test.go index d33bb61..7b4b343 100644 --- a/bpf/expr_test.go +++ b/bpf/expr_test.go @@ -18,7 +18,7 @@ import ( "strings" "testing" - "github.com/booster-proj/lsaddr/bpf" + "github.com/jecoz/lsaddr/bpf" ) func TestJoin(t *testing.T) { From df71dece29acede4b652d18878b7732c2d5cba9e Mon Sep 17 00:00:00 2001 From: Daniel Morandini Date: Wed, 16 Oct 2019 11:50:49 +0200 Subject: [PATCH 22/26] Fix license header --- bpf/expr.go | 2 +- bpf/expr_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bpf/expr.go b/bpf/expr.go index 01bb630..cc1ad4e 100644 --- a/bpf/expr.go +++ b/bpf/expr.go @@ -1,4 +1,4 @@ -// Copyright © 2019 booster authors +// Copyright © 2019 Jecoz // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or diff --git a/bpf/expr_test.go b/bpf/expr_test.go index 7b4b343..1de7264 100644 --- a/bpf/expr_test.go +++ b/bpf/expr_test.go @@ -1,4 +1,4 @@ -// Copyright © 2019 booster authors +// Copyright © 2019 Jecoz // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or From dc534c72539e54574076c775a7cb9dcd1e4e4fc7 Mon Sep 17 00:00:00 2001 From: Daniel Morandini Date: Wed, 16 Oct 2019 11:50:59 +0200 Subject: [PATCH 23/26] Restore encoders --- cmd/root.go | 46 +++++++++++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index a6c4734..4e84537 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -15,12 +15,16 @@ package cmd import ( + "bufio" "fmt" + "io" "io/ioutil" "log" "os" "strings" + "github.com/jecoz/lsaddr/bpf" + "github.com/jecoz/lsaddr/csv" "github.com/jecoz/lsaddr/onf" "github.com/spf13/cobra" ) @@ -50,18 +54,18 @@ var rootCmd = &cobra.Command{ if !verbose { log.SetOutput(ioutil.Discard) } - - format = strings.ToLower(format) - if err := validateFormat(format); err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n", err) - os.Exit(1) - } }, Run: func(cmd *cobra.Command, args []string) { if version { fmt.Printf("Version: %s, Commit: %s, Built at: %s\n\n", Version, Commit, BuildTime) os.Exit(0) } + w := bufio.NewWriter(os.Stdout) + enc, err := newEncoder(w, format) + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } set, err := onf.FetchAll() if err != nil { @@ -79,13 +83,30 @@ var rootCmd = &cobra.Command{ } log.Printf("# of open network files: %d", len(set)) - for _, v := range set { - fmt.Printf("%v\n", v) + if err := enc.Encode(set); err != nil { + fmt.Fprintf(os.Stderr, "error: unable to encode output: %v\n", err) + os.Exit(1) } + w.Flush() os.Exit(0) }, } +type Encoder interface { + Encode([]onf.ONF) error +} + +func newEncoder(w io.Writer, format string) (Encoder, error) { + switch strings.ToLower(format) { + case "csv": + return csv.NewEncoder(w), nil + case "bpf": + return bpf.NewEncoder(w), nil + default: + return nil, fmt.Errorf("unrecognised format option %s", format) + } +} + // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. func Execute() { @@ -101,15 +122,6 @@ func init() { rootCmd.PersistentFlags().StringVarP(&format, "format", "f", "csv", "Choose output format.") } -func validateFormat(format string) error { - switch format { - case "csv", "bpf": - return nil - default: - return fmt.Errorf("unrecognised format option %s", format) - } -} - const usage = ` 'lsaddr' From 1663d973fcc04ff1e5d14fc18b94c138724ae6b5 Mon Sep 17 00:00:00 2001 From: Daniel Morandini Date: Wed, 16 Oct 2019 12:01:09 +0200 Subject: [PATCH 24/26] Update usage --- cmd/root.go | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 4e84537..0e40dc3 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -46,7 +46,7 @@ var ( // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ Use: "lsaddr", - Short: "Show a subset of all network addresses being used by your apps", + Short: "List used network addresses.", Long: usage, Args: cobra.MaximumNArgs(1), PersistentPreRun: func(cmd *cobra.Command, args []string) { @@ -122,17 +122,9 @@ func init() { rootCmd.PersistentFlags().StringVarP(&format, "format", "f", "csv", "Choose output format.") } -const usage = ` -'lsaddr' +const usage = `List open network connections. Results can be filtered passing a raw regular expression as argument (check out https://golang.org/pkg/regexp/ to learn how to properly format your regex). -TODO: describe - -- a regular expression: which will be used to filter out the list of open files. Each line that -does not match against the regex will be discarded (e.g. "chrome.exe", "Safari", "104|405"). -Check out https://golang.org/pkg/regexp/ to learn how to properly format your regex. - -Using the "--format" or "-f" flag, it is possible to decide the format/encoding of the output -produced. Possible values are: +Using the "--format" or "-f" flag, it is possible to decide the format/encoding of the output produced. Possible values are: - "bpf": produces a Berkley Packet Filter expression, which, if given to a tool that supports bpfs, will make it capture only the packets headed to/coming from the destination addresses of the open network files collected. From b4d4451d27a64186596ed583a8dfb45dbc554b11 Mon Sep 17 00:00:00 2001 From: Daniel Morandini Date: Wed, 16 Oct 2019 12:20:49 +0200 Subject: [PATCH 25/26] Close #13 --- README.md | 90 ++++++++++--------------------------------------------- 1 file changed, 16 insertions(+), 74 deletions(-) diff --git a/README.md b/README.md index 596e811..6bd7252 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ #### Supported OS - `macOS` - `linux` -- `windows` (**NEW** 💥) +- `windows #### External dependencies OS | Dep | Notes @@ -29,85 +29,27 @@ Choose one Big thanks to [goreleaser](https://github.com/goreleaser/goreleaser) and [godownloader](https://github.com/goreleaser/godownloader) which made the releasing process **FUN**! 🤩 ## Usage -The idea is to easily filter the list of open network files of a specific application. The list is filtered with a regular expression: only -the lines that match against it are kept, the others discarded. You can pass to `lsaddr` either directly the regex, or the root folder of the -target app (supported only on macOS for now). Check out some examples: +The idea is to easily filter the list of open network files of a specific application. The list is filtered with a regular expression: only the lines that match against it are kept, the others discarded. - -#### Example #1 -"Spotify" is used as a regular expression. -``` -$ bin/lsaddr Spotify -COMMAND,NET,SRC,DST -Spotify,tcp,192.168.0.98:54862,104.199.64.69:4070 -Spotify,tcp,*:57621, -Spotify,tcp,*:54850, -Spotify,udp,*:57621, -Spotify,udp,*:1900, -Spotify,udp,*:61152, -Spotify,udp,*:51535, -Spotify,tcp,192.168.0.98:54878,35.186.224.47:443 -Spotify,tcp,192.168.0.98:54872,35.186.224.53:443 -``` - -#### Example #2 -"/Applications/Spotify.app" is used to find the application's name, then its -process identifiers are used to build the regular expression. +## Examples +#### Find connections opened by "Spotify" ``` -$ bin/lsaddr /Applications/Spotify.app/ -COMMAND,NET,SRC,DST -Spotify,tcp,192.168.0.98:54862,104.199.64.69:4070 -Spotify,tcp,*:57621, -Spotify,tcp,*:54850, -Spotify,udp,*:57621, -Spotify,udp,*:1900, -Spotify,udp,*:61152, -Spotify,udp,*:51535, -Spotify,tcp,192.168.0.98:54878,35.186.224.47:443 -Spotify,tcp,192.168.0.98:54872,35.186.224.53:443 +% bin/lsaddr Spotify +PID,CMD,NET,SRC,DST +62822,Spotify,tcp,10.7.152.118:52213,104.199.64.50:80 +62822,Spotify,tcp,10.7.152.118:52255,35.186.224.47:443 +62826,Spotify,tcp,10.7.152.118:52196,35.186.224.53:443 ``` -#### Example #3 -`--debug` information is printed to `stderr`, command's output to `stdout`. +#### Increment verbosity (debugging) +Note: `debug` information is printed to `stderr`, command's output to `stdout`. ``` -$ bin/lsaddr /Applications/Spotify.app/ --debug -[lsaddr] 2019/07/12 14:29:50 app name: Spotify, path: /Applications/Spotify.app -[lsaddr] 2019/07/12 14:29:50 regexp built: "48042|48044|48045|48047" -[lsaddr] 2019/07/12 14:29:50 # of open files: 9 -COMMAND,NET,SRC,DST -Spotify,tcp,192.168.0.98:54862,104.199.64.69:4070 -Spotify,tcp,*:57621, -Spotify,tcp,*:54850, -Spotify,udp,*:57621, -Spotify,udp,*:1900, -Spotify,udp,*:61152, -Spotify,udp,*:51535, -Spotify,tcp,192.168.0.98:54878,35.186.224.47:443 -Spotify,tcp,192.168.0.98:54872,35.186.224.53:443 +% bin/lsaddr Spotify --verbose +... ``` +(output omitted for readiness) -#### Example #4 -- you can encode the output either in csv or as a [bpf](https://en.wikipedia.org/wiki/Berkeley_Packet_Filter) (hint: very useful for packet capturing tools). -- only the unique destination addresses are taken into consideration when building the filter, -ignoring the ports and without specifing if the "direction" (incoming or outgoing) that we want to -filter. This is because the expected behaviour has not yet been defined. -``` -$ bin/lsaddr /Applications/Mail.app --out=bpf -(tcp and host 192.168.0.98 and port 58100) or (tcp and host 64.233.184.108 and port 993) or (tcp and host 192.168.0.98 and port 58100) or (tcp and host 64.233.184.108 and port 993) or (tcp and host 192.168.0.98 and port 57213) or (tcp and host 10.0.0.1 and port 993) or (tcp and host 192.168.0.98 and port 57213) or (tcp and host 10.0.0.1 and port 993) or (tcp and host 192.168.0.98 and port 57214) or (tcp and host 10.0.0.1 and port 993) or (tcp and host 192.168.0.98 and port 57214) or (tcp and host 10.0.0.1 and port 993) or (tcp and host 192.168.0.98 and port 57216) or (tcp and host 17.56.136.197 and port 993) or (tcp and host 192.168.0.98 and port 57216) or (tcp and host 17.56.136.197 and port 993) or (tcp and host 192.168.0.98 and port 57217) or (tcp and host 17.56.136.197 and port 993) or (tcp and host 192.168.0.98 and port 57217) or (tcp and host 17.56.136.197 and port 993) -``` -#### Example #5 -At the moment on Windows you can pass the absulute path of the program you want (or straight `.exe`) -to analyze. +#### Dump Spotify's network traffic using tcpdump ``` -> lsaddr.exe "chrome.exe" -COMMAND,NET,SRC,DST -chrome.exe,tcp,10.211.55.3:50551,216.58.205.163:443 -chrome.exe,tcp,10.211.55.3:50556,216.58.205.195:443 -chrome.exe,tcp,10.211.55.3:50558,216.58.205.67:443 -chrome.exe,tcp,10.211.55.3:50567,216.58.205.106:443 -chrome.exe,udp,0.0.0.0:5353,*:* -chrome.exe,udp,0.0.0.0:5353,*:* -chrome.exe,udp,0.0.0.0:5353,*:* -chrome.exe,udp,[::]:5353,*:* -chrome.exe,udp,[::]:5353,*:* +% bin/lsaddr -f bpf Spotify | xargs -0 sudo tcpdump ``` From b29859477fa447cf248f5c18b4cde3f7189af981 Mon Sep 17 00:00:00 2001 From: Daniel Morandini Date: Wed, 16 Oct 2019 12:21:02 +0200 Subject: [PATCH 26/26] Improve logging --- lsof/lsof.go | 4 +++- netstat/netstat.go | 4 +++- onf/onf.go | 3 ++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lsof/lsof.go b/lsof/lsof.go index 42e101f..2b2bbcd 100644 --- a/lsof/lsof.go +++ b/lsof/lsof.go @@ -43,8 +43,10 @@ type OpenFile struct { } func Run() ([]OpenFile, error) { - acc := []OpenFile{} + log.Printf("Executing: lsof -i -n -P") p := pipe.Exec("lsof", "-i", "-n", "-P") + + acc := []OpenFile{} out, err := pipe.OutputTimeout(p, time.Millisecond*100) if err != nil { return acc, fmt.Errorf("unable to run lsof: %w", err) diff --git a/netstat/netstat.go b/netstat/netstat.go index 20ce38b..d6f11b3 100644 --- a/netstat/netstat.go +++ b/netstat/netstat.go @@ -37,8 +37,10 @@ type ActiveConnection struct { } func Run() ([]ActiveConnection, error) { - acc := []ActiveConnection{} + log.Printf("Executing: netstat -nao") p := pipe.Exec("netstat", "-nao") + + acc := []ActiveConnection{} out, err := pipe.OutputTimeout(p, time.Millisecond*100) if err != nil { return acc, fmt.Errorf("unable to run netstat: %w", err) diff --git a/onf/onf.go b/onf/onf.go index f9b9fcc..cbaa585 100644 --- a/onf/onf.go +++ b/onf/onf.go @@ -54,6 +54,7 @@ func Filter(set []ONF, pivot string) ([]ONF, error) { return set, nil } + log.Printf("Building regex from: %v", pivot) rgx, err := regexp.Compile(pivot) if err != nil { return set, fmt.Errorf("unable to filter open network file set: %w", err) @@ -61,7 +62,7 @@ func Filter(set []ONF, pivot string) ([]ONF, error) { acc := make([]ONF, 0, len(set)) for _, v := range set { if !rgx.MatchString(v.Raw) { - log.Printf("[DEBUG] filtering open network file: %v", v) + log.Printf("Filtering open network file: %v", v) continue } acc = append(acc, v)