Skip to content

Commit

Permalink
Add a BPF filter and turn off promiscuous mode (#27)
Browse files Browse the repository at this point in the history
* Add a BPF filter
* Use net.Interface objects outside of package main
* Update travis to support ipv6
* Test processFlags better
* Turn off promiscuous mode.
  • Loading branch information
pboothe authored Dec 2, 2019
1 parent e6cd4a0 commit 626397f
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 26 deletions.
5 changes: 5 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -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 ./...
Expand Down
33 changes: 18 additions & 15 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,38 +65,41 @@ 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() {
defer mainCancel()

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")
Expand Down Expand Up @@ -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()
}()
Expand Down
17 changes: 15 additions & 2 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
54 changes: 46 additions & 8 deletions muxer/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ package muxer

import (
"context"
"log"
"net"
"strings"
"sync"
"time"

Expand All @@ -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()
Expand Down Expand Up @@ -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()
Expand Down
36 changes: 35 additions & 1 deletion muxer/interfaces_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit 626397f

Please sign in to comment.