| // 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 | |
| } |