From 626397f6a797606864025059f1c5cd1e3969f9d7 Mon Sep 17 00:00:00 2001 From: Peter Boothe Date: Mon, 2 Dec 2019 14:38:19 -0500 Subject: [PATCH] Add a BPF filter and turn off promiscuous mode (#27) * Add a BPF filter * Use net.Interface objects outside of package main * Update travis to support ipv6 * Test processFlags better * Turn off promiscuous mode. --- .travis.yml | 5 ++++ main.go | 33 +++++++++++++----------- main_test.go | 17 +++++++++++-- muxer/interfaces.go | 54 ++++++++++++++++++++++++++++++++++------ muxer/interfaces_test.go | 36 ++++++++++++++++++++++++++- 5 files changed, 119 insertions(+), 26 deletions(-) diff --git a/.travis.yml b/.travis.yml index 31d73e8..28a9c41 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,10 @@ language: go +# From https://github.com/travis-ci/travis-ci/issues/8891#issuecomment-353403729 +before_install: +- echo '{"ipv6":true,"fixed-cidr-v6":"2001:db8:1::/64"}' | sudo tee /etc/docker/daemon.json +- sudo service docker restart + install: - sudo apt-get update && sudo apt-get install -y libpcap-dev - go get -v -t ./... diff --git a/main.go b/main.go index 6b9edbd..4d48b57 100644 --- a/main.go +++ b/main.go @@ -65,30 +65,32 @@ func catch(sig os.Signal) { var netInterfaces = net.Interfaces -func processFlags() error { +func processFlags() ([]net.Interface, error) { // Verify that capture duration is always longer than uuid wait duration. if *uuidWaitDuration > *captureDuration { - return fmt.Errorf("Capture duration must be greater than UUID wait duration: %s vs %s", + return nil, fmt.Errorf("Capture duration must be greater than UUID wait duration: %s vs %s", *captureDuration, *uuidWaitDuration) } // Special case for argument "-interface": if no specific interface was - // specified, then "all of them" was implicitly specified. If new interfaces - // are created after capture is started, traffic on those interfaces will be - // ignored. If interfaces disappear, the effects are unknown. The number of - // interfaces with a running capture is tracked in the - // pcap_muxer_interfaces_with_captures metric. + // explicitly specified, then "all of them" was implicitly specified. If new + // interfaces are created after capture is started, traffic on those + // interfaces will be ignored. If interfaces disappear, the effects are + // unknown. The number of interfaces with a running capture is tracked in + // the pcap_muxer_interfaces_with_captures metric. if len(interfaces) == 0 { log.Println("No interfaces specified, will listen for packets on all available interfaces.") - ifaces, err := netInterfaces() + return netInterfaces() + } + ifaces := []net.Interface{} + for _, iface := range interfaces { + i, err := net.InterfaceByName(iface) if err != nil { - return fmt.Errorf("Could not list interfaces: %s", err) - } - for _, iface := range ifaces { - interfaces = append(interfaces, iface.Name) + return ifaces, err } + ifaces = append(ifaces, *i) } - return nil + return ifaces, nil } func main() { @@ -96,7 +98,8 @@ func main() { flag.Parse() rtx.Must(flagx.ArgsFromEnv(flag.CommandLine), "Could not get args from env") - rtx.Must(processFlags(), "Failed to process flags") + ifaces, err := processFlags() + rtx.Must(err, "Failed to process flags") psrv := prometheusx.MustServeMetrics() defer warnonerror.Close(psrv, "Could not stop metric server") @@ -132,7 +135,7 @@ func main() { // Capture packets on every interface. cleanupWG.Add(1) go func() { - muxer.MustCaptureTCPOnInterfaces(mainCtx, interfaces, packets, pcapOpenLive, int32(*maxHeaderSize)) + muxer.MustCaptureTCPOnInterfaces(mainCtx, ifaces, packets, pcapOpenLive, int32(*maxHeaderSize)) mainCancel() cleanupWG.Done() }() diff --git a/main_test.go b/main_test.go index b1da4f0..31e1692 100644 --- a/main_test.go +++ b/main_test.go @@ -30,20 +30,33 @@ func TestProcessFlags(t *testing.T) { defer func() { // Reset function pointer. netInterfaces = net.Interfaces + interfaces = flagx.StringArray{} }() - err := processFlags() + _, err := processFlags() if err == nil { t.Fatalf("processFlags() return wrong error; got nil, want %q", err) } + interfaces = flagx.StringArray{"lo"} + ifaces, err := processFlags() + if err != nil || len(ifaces) != 1 { + t.Fatalf("processFlags() did not get the loopback: %s, %+v", err, ifaces) + } + + interfaces = flagx.StringArray{"doesnotexist"} + _, err = processFlags() + if err == nil { + t.Fatalf("processFlags() return wrong error; got nil") + } + // Artificially set uuid wait duration to be longer than capture duration. *uuidWaitDuration = 2 * *captureDuration defer func() { *uuidWaitDuration = *captureDuration / 2 }() - err = processFlags() + _, err = processFlags() if err == nil { t.Fatalf("processFlags() return wrong error; got nil, want %q", err) } diff --git a/muxer/interfaces.go b/muxer/interfaces.go index 439e72b..6ea379e 100644 --- a/muxer/interfaces.go +++ b/muxer/interfaces.go @@ -5,6 +5,9 @@ package muxer import ( "context" + "log" + "net" + "strings" "sync" "time" @@ -15,6 +18,16 @@ import ( "github.com/m-lab/packet-headers/metrics" ) +// PcapHandleOpener is a type to allow injection of fake packet captures to aid +// in testing. It is exactly the type of pcap.OpenLive, and in production code +// every variable of this type should be set to pcap.OpenLive. +type PcapHandleOpener func(device string, snaplen int32, promisc bool, timeout time.Duration) (handle *pcap.Handle, _ error) + +// Injected functions to support whitebox testing. +var ( + netInterfaceByName = net.InterfaceByName +) + func forwardPackets(ctx context.Context, in <-chan gopacket.Packet, out chan<- gopacket.Packet, wg *sync.WaitGroup) { defer wg.Done() metrics.InterfacesBeingCaptured.Inc() @@ -45,22 +58,47 @@ func muxPackets(ctx context.Context, in []<-chan gopacket.Packet, out chan<- gop close(out) } -// PcapHandleOpener is a type to allow injection of fake packet captures to aid -// in testing. It is exactly the type of pcap.OpenLive, and in production code -// every variable of this type should be set to pcap.OpenLive. -type PcapHandleOpener func(device string, snaplen int32, promisc bool, timeout time.Duration) (handle *pcap.Handle, _ error) +func mustMakeFilter(interfaces []net.Interface) string { + filters := []string{} + for _, iface := range interfaces { + if iface.Flags&net.FlagLoopback != 0 { + // Skip nil interfaces and loopback addresses + continue + } + addrs, err := iface.Addrs() + rtx.Must(err, "Could not get addresses for interface %s", iface.Name) + for _, addr := range addrs { + a := addr.String() + if strings.Contains(a, "/") { + a = strings.Split(a, "/")[0] + } + if strings.Contains(a, ":") { + filters = append(filters, "ip6 host "+a) + } else { + filters = append(filters, "ip host "+a) + } + } + } + if len(filters) == 0 { + return "tcp" + } + return "tcp and ( " + strings.Join(filters, " or ") + ")" +} // MustCaptureTCPOnInterfaces fires off a packet capture on every one of the // passed-in list of interfaces, and then muxes the resulting packet streams to // all be sent to the passed-in packets channel. -func MustCaptureTCPOnInterfaces(ctx context.Context, interfaces []string, packets chan<- gopacket.Packet, opener PcapHandleOpener, maxHeaderSize int32) { +func MustCaptureTCPOnInterfaces(ctx context.Context, interfaces []net.Interface, packets chan<- gopacket.Packet, pcapOpenLive PcapHandleOpener, maxHeaderSize int32) { // Capture packets on every interface. packetCaptures := make([]<-chan gopacket.Packet, 0) + // Only capture packets destined for a non-localhost local IP. + filter := mustMakeFilter(interfaces) + log.Printf("Using BPF filter %q\n", filter) for _, iface := range interfaces { - // Open a packet capture - handle, err := opener(iface, maxHeaderSize, true, pcap.BlockForever) + // Open a packet capture. "false" means promiscuous mode is off. + handle, err := pcapOpenLive(iface.Name, maxHeaderSize, false, pcap.BlockForever) rtx.Must(err, "Could not create libpcap client for %q", iface) - rtx.Must(handle.SetBPFFilter("tcp"), "Could not set up BPF filter for TCP") + rtx.Must(handle.SetBPFFilter(filter), "Could not set up BPF filter for TCP") // Stop packet capture when this function exits. defer handle.Close() diff --git a/muxer/interfaces_test.go b/muxer/interfaces_test.go index 3c52a27..4b23013 100644 --- a/muxer/interfaces_test.go +++ b/muxer/interfaces_test.go @@ -2,10 +2,13 @@ package muxer import ( "context" + "net" "sync" "testing" "time" + "github.com/google/gopacket/layers" + "github.com/google/gopacket" "github.com/google/gopacket/pcap" "github.com/m-lab/go/rtx" @@ -79,18 +82,49 @@ func TestMuxPacketsUntilContextCancellation(t *testing.T) { } +func TestMustMakeFilter(t *testing.T) { + lo, err := net.InterfaceByName("lo") + rtx.Must(err, "Could not get loopback interface") + f := mustMakeFilter([]net.Interface{*lo}) + if f != "tcp" { + t.Error("loopback resulted in non-empty filter") + } + f = mustMakeFilter([]net.Interface{}) + if f != "tcp" { + t.Error("empty interface list resulted in non-empty filter") + } + + // Now check all the interfaces on the local host, just as a sanity check. + ifaces, err := net.Interfaces() + rtx.Must(err, "Could not get interface list") + f = mustMakeFilter(ifaces) + if f == "" { + t.Error("Non-empty interface list resulted in empty filter") + } + // Verify that the filter is a well-formed BPF filter. + _, err = pcap.NewBPF(layers.LinkTypeEthernet, 256, f) + rtx.Must(err, "Could not parse bpf %q", f) +} + func fakePcapOpenLive(filename string, _ int32, _ bool, _ time.Duration) (*pcap.Handle, error) { return pcap.OpenOffline(filename) } +func fakeInterfaceByName(name string) (*net.Interface, error) { + return nil, nil +} + func TestMustCaptureOnInterfaces(t *testing.T) { + netInterfaceByName = fakeInterfaceByName + defer func() { netInterfaceByName = net.InterfaceByName }() + wg := sync.WaitGroup{} packets := make(chan gopacket.Packet) wg.Add(1) go func() { MustCaptureTCPOnInterfaces( context.Background(), - []string{"../testdata/v4.pcap", "../testdata/v6.pcap"}, + []net.Interface{{Name: "../testdata/v4.pcap"}, {Name: "../testdata/v6.pcap"}}, packets, fakePcapOpenLive, 0,