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