forked from mystiq/dex
192 lines
4.9 KiB
Go
192 lines
4.9 KiB
Go
// Copyright 2016 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package bpf_test
|
|
|
|
import (
|
|
"net"
|
|
"runtime"
|
|
"testing"
|
|
"time"
|
|
|
|
"golang.org/x/net/bpf"
|
|
"golang.org/x/net/ipv4"
|
|
)
|
|
|
|
// A virtualMachine is a BPF virtual machine which can process an
|
|
// input packet against a BPF program and render a verdict.
|
|
type virtualMachine interface {
|
|
Run(in []byte) (int, error)
|
|
}
|
|
|
|
// canUseOSVM indicates if the OS BPF VM is available on this platform.
|
|
func canUseOSVM() bool {
|
|
// OS BPF VM can only be used on platforms where x/net/ipv4 supports
|
|
// attaching a BPF program to a socket.
|
|
switch runtime.GOOS {
|
|
case "linux":
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// All BPF tests against both the Go VM and OS VM are assumed to
|
|
// be used with a UDP socket. As a result, the entire contents
|
|
// of a UDP datagram is sent through the BPF program, but only
|
|
// the body after the UDP header will ever be returned in output.
|
|
|
|
// testVM sets up a Go BPF VM, and if available, a native OS BPF VM
|
|
// for integration testing.
|
|
func testVM(t *testing.T, filter []bpf.Instruction) (virtualMachine, func(), error) {
|
|
goVM, err := bpf.NewVM(filter)
|
|
if err != nil {
|
|
// Some tests expect an error, so this error must be returned
|
|
// instead of fatally exiting the test
|
|
return nil, nil, err
|
|
}
|
|
|
|
mvm := &multiVirtualMachine{
|
|
goVM: goVM,
|
|
|
|
t: t,
|
|
}
|
|
|
|
// If available, add the OS VM for tests which verify that both the Go
|
|
// VM and OS VM have exactly the same output for the same input program
|
|
// and packet.
|
|
done := func() {}
|
|
if canUseOSVM() {
|
|
osVM, osVMDone := testOSVM(t, filter)
|
|
done = func() { osVMDone() }
|
|
mvm.osVM = osVM
|
|
}
|
|
|
|
return mvm, done, nil
|
|
}
|
|
|
|
// udpHeaderLen is the length of a UDP header.
|
|
const udpHeaderLen = 8
|
|
|
|
// A multiVirtualMachine is a virtualMachine which can call out to both the Go VM
|
|
// and the native OS VM, if the OS VM is available.
|
|
type multiVirtualMachine struct {
|
|
goVM virtualMachine
|
|
osVM virtualMachine
|
|
|
|
t *testing.T
|
|
}
|
|
|
|
func (mvm *multiVirtualMachine) Run(in []byte) (int, error) {
|
|
if len(in) < udpHeaderLen {
|
|
mvm.t.Fatalf("input must be at least length of UDP header (%d), got: %d",
|
|
udpHeaderLen, len(in))
|
|
}
|
|
|
|
// All tests have a UDP header as part of input, because the OS VM
|
|
// packets always will. For the Go VM, this output is trimmed before
|
|
// being sent back to tests.
|
|
goOut, goErr := mvm.goVM.Run(in)
|
|
if goOut >= udpHeaderLen {
|
|
goOut -= udpHeaderLen
|
|
}
|
|
|
|
// If Go output is larger than the size of the packet, packet filtering
|
|
// interop tests must trim the output bytes to the length of the packet.
|
|
// The BPF VM should not do this on its own, as other uses of it do
|
|
// not trim the output byte count.
|
|
trim := len(in) - udpHeaderLen
|
|
if goOut > trim {
|
|
goOut = trim
|
|
}
|
|
|
|
// When the OS VM is not available, process using the Go VM alone
|
|
if mvm.osVM == nil {
|
|
return goOut, goErr
|
|
}
|
|
|
|
// The OS VM will apply its own UDP header, so remove the pseudo header
|
|
// that the Go VM needs.
|
|
osOut, err := mvm.osVM.Run(in[udpHeaderLen:])
|
|
if err != nil {
|
|
mvm.t.Fatalf("error while running OS VM: %v", err)
|
|
}
|
|
|
|
// Verify both VMs return same number of bytes
|
|
var mismatch bool
|
|
if goOut != osOut {
|
|
mismatch = true
|
|
mvm.t.Logf("output byte count does not match:\n- go: %v\n- os: %v", goOut, osOut)
|
|
}
|
|
|
|
if mismatch {
|
|
mvm.t.Fatal("Go BPF and OS BPF packet outputs do not match")
|
|
}
|
|
|
|
return goOut, goErr
|
|
}
|
|
|
|
// An osVirtualMachine is a virtualMachine which uses the OS's BPF VM for
|
|
// processing BPF programs.
|
|
type osVirtualMachine struct {
|
|
l net.PacketConn
|
|
s net.Conn
|
|
}
|
|
|
|
// testOSVM creates a virtualMachine which uses the OS's BPF VM by injecting
|
|
// packets into a UDP listener with a BPF program attached to it.
|
|
func testOSVM(t *testing.T, filter []bpf.Instruction) (virtualMachine, func()) {
|
|
l, err := net.ListenPacket("udp4", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("failed to open OS VM UDP listener: %v", err)
|
|
}
|
|
|
|
prog, err := bpf.Assemble(filter)
|
|
if err != nil {
|
|
t.Fatalf("failed to compile BPF program: %v", err)
|
|
}
|
|
|
|
p := ipv4.NewPacketConn(l)
|
|
if err = p.SetBPF(prog); err != nil {
|
|
t.Fatalf("failed to attach BPF program to listener: %v", err)
|
|
}
|
|
|
|
s, err := net.Dial("udp4", l.LocalAddr().String())
|
|
if err != nil {
|
|
t.Fatalf("failed to dial connection to listener: %v", err)
|
|
}
|
|
|
|
done := func() {
|
|
_ = s.Close()
|
|
_ = l.Close()
|
|
}
|
|
|
|
return &osVirtualMachine{
|
|
l: l,
|
|
s: s,
|
|
}, done
|
|
}
|
|
|
|
// Run sends the input bytes into the OS's BPF VM and returns its verdict.
|
|
func (vm *osVirtualMachine) Run(in []byte) (int, error) {
|
|
go func() {
|
|
_, _ = vm.s.Write(in)
|
|
}()
|
|
|
|
vm.l.SetDeadline(time.Now().Add(50 * time.Millisecond))
|
|
|
|
var b [512]byte
|
|
n, _, err := vm.l.ReadFrom(b[:])
|
|
if err != nil {
|
|
// A timeout indicates that BPF filtered out the packet, and thus,
|
|
// no input should be returned.
|
|
if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
|
|
return n, nil
|
|
}
|
|
|
|
return n, err
|
|
}
|
|
|
|
return n, nil
|
|
}
|