Files

367 lines
9.0 KiB
Go

// +build darwin dragonfly freebsd netbsd openbsd
package raw
import (
"errors"
"fmt"
"net"
"os"
"runtime"
"sync"
"syscall"
"time"
"unsafe"
"golang.org/x/net/bpf"
)
const (
// bpfDIn tells BPF to pass through only incoming packets, so we do not
// receive the packets we send using BPF.
bpfDIn = 0
// osFreeBSD is the GOOS name for FreeBSD.
osFreeBSD = "freebsd"
)
// bpfLen returns the length of the BPF header prepended to each incoming ethernet
// frame. FreeBSD uses a slightly modified header from other BSD variants.
func bpfLen() int {
// Majority of BSD family systems use the bpf_hdr struct, but FreeBSD
// has replaced this with bpf_xhdr, which is longer.
const (
bpfHeaderLen = 18
bpfXHeaderLen = 26
)
if runtime.GOOS == osFreeBSD {
return bpfXHeaderLen
}
return bpfHeaderLen
}
var (
// Must implement net.PacketConn at compile-time.
_ net.PacketConn = &packetConn{}
)
// packetConn is the Linux-specific implementation of net.PacketConn for this
// package.
type packetConn struct {
proto uint16
ifi *net.Interface
f *os.File
fd int
buflen int
// Timeouts set via Set{Read,}Deadline, guarded by mutex
timeoutMu sync.RWMutex
rtimeout time.Time
}
// listenPacket creates a net.PacketConn which can be used to send and receive
// data at the device driver level.
func listenPacket(ifi *net.Interface, proto uint16, _ Config) (*packetConn, error) {
// Config is, as of now, unused on BSD.
// TODO(mdlayher): consider porting NoTimeouts option to BSD if it pans out.
var f *os.File
var err error
// Try to find an available BPF device
for i := 0; i <= 10; i++ {
bpfPath := fmt.Sprintf("/dev/bpf%d", i)
f, err = os.OpenFile(bpfPath, os.O_RDWR, 0666)
if err == nil {
// Found a usable device
break
}
// Device is busy, try the next one
if perr, ok := err.(*os.PathError); ok {
if perr.Err.(syscall.Errno) == syscall.EBUSY {
continue
}
}
return nil, err
}
if f == nil {
return nil, errors.New("unable to open BPF device")
}
fd := int(f.Fd())
if fd == -1 {
return nil, errors.New("unable to open BPF device")
}
// Configure BPF device to send and receive data
buflen, err := configureBPF(fd, ifi, proto)
if err != nil {
return nil, err
}
return &packetConn{
proto: proto,
ifi: ifi,
f: f,
fd: fd,
buflen: buflen,
}, nil
}
// ReadFrom implements the net.PacketConn.ReadFrom method.
func (p *packetConn) ReadFrom(b []byte) (int, net.Addr, error) {
p.timeoutMu.Lock()
deadline := p.rtimeout
p.timeoutMu.Unlock()
buf := make([]byte, p.buflen)
var n int
for {
var timeout time.Duration
if deadline.IsZero() {
timeout = readTimeout
} else {
timeout = deadline.Sub(time.Now())
if timeout > readTimeout {
timeout = readTimeout
}
}
tv, err := newTimeval(timeout)
if err != nil {
return 0, nil, err
}
if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, uintptr(p.fd), syscall.BIOCSRTIMEOUT, uintptr(unsafe.Pointer(tv))); err != 0 {
return 0, nil, syscall.Errno(err)
}
// Attempt to receive on socket
// The read sycall will NOT be interrupted by closing of the socket
n, err = syscall.Read(p.fd, buf)
if err != nil {
return n, nil, err
}
if n > 0 {
break
}
}
// TODO(mdlayher): consider parsing BPF header if it proves useful.
// BPF header length depends on the platform this code is running on
bpfl := bpfLen()
// Retrieve source MAC address of ethernet header
mac := make(net.HardwareAddr, 6)
copy(mac, buf[bpfl+6:bpfl+12])
// Skip past BPF header to retrieve ethernet frame
out := copy(b, buf[bpfl:bpfl+n])
return out, &Addr{
HardwareAddr: mac,
}, nil
}
// WriteTo implements the net.PacketConn.WriteTo method.
func (p *packetConn) WriteTo(b []byte, _ net.Addr) (int, error) {
return syscall.Write(p.fd, b)
}
// Close closes the connection.
func (p *packetConn) Close() error {
return p.f.Close()
}
// LocalAddr returns the local network address.
func (p *packetConn) LocalAddr() net.Addr {
return &Addr{
HardwareAddr: p.ifi.HardwareAddr,
}
}
// SetDeadline implements the net.PacketConn.SetDeadline method.
func (p *packetConn) SetDeadline(t time.Time) error {
return p.SetReadDeadline(t)
}
// SetReadDeadline implements the net.PacketConn.SetReadDeadline method.
func (p *packetConn) SetReadDeadline(t time.Time) error {
p.timeoutMu.Lock()
p.rtimeout = t
p.timeoutMu.Unlock()
return nil
}
// SetWriteDeadline implements the net.PacketConn.SetWriteDeadline method.
func (p *packetConn) SetWriteDeadline(t time.Time) error {
return ErrNotImplemented
}
// SetBPF attaches an assembled BPF program to a raw net.PacketConn.
func (p *packetConn) SetBPF(filter []bpf.RawInstruction) error {
// Base filter filters traffic based on EtherType
base, err := bpf.Assemble(baseFilter(p.proto))
if err != nil {
return err
}
// Append user filter to base filter, translate to raw format,
// and apply to BPF device
return syscall.SetBpf(p.fd, assembleBpfInsn(append(base, filter...)))
}
// SetPromiscuous enables or disables promiscuous mode on the interface, allowing it
// to receive traffic that is not addressed to the interface.
func (p *packetConn) SetPromiscuous(b bool) error {
m := 1
if !b {
m = 0
}
return syscall.SetBpfPromisc(p.fd, m)
}
// Stats retrieves statistics from the Conn.
func (p *packetConn) Stats() (*Stats, error) {
return nil, ErrNotImplemented
}
// configureBPF configures a BPF device with the specified file descriptor to
// use the specified network and interface and protocol.
func configureBPF(fd int, ifi *net.Interface, proto uint16) (int, error) {
// Use specified interface with BPF device
if err := syscall.SetBpfInterface(fd, ifi.Name); err != nil {
return 0, err
}
// Inform BPF to send us its data immediately
if err := syscall.SetBpfImmediate(fd, 1); err != nil {
return 0, err
}
// Check buffer size of BPF device
buflen, err := syscall.BpfBuflen(fd)
if err != nil {
return 0, err
}
// Do not automatically complete source address in ethernet headers
if err := syscall.SetBpfHeadercmpl(fd, 1); err != nil {
return 0, err
}
// Only retrieve incoming traffic using BPF device
if err := setBPFDirection(fd, bpfDIn); err != nil {
return 0, err
}
// Build and apply base BPF filter which checks for correct EtherType
// on incoming packets
prog, err := bpf.Assemble(baseInterfaceFilter(proto, ifi.MTU))
if err != nil {
return 0, err
}
if err := syscall.SetBpf(fd, assembleBpfInsn(prog)); err != nil {
return 0, err
}
// Flush any packets currently in the BPF device's buffer
if err := syscall.FlushBpf(fd); err != nil {
return 0, err
}
return buflen, nil
}
// setBPFDirection enables filtering traffic traveling in a specific direction
// using BPF, so that traffic sent by this package is not captured when reading
// using this package.
func setBPFDirection(fd int, direction int) error {
_, _, err := syscall.Syscall(
syscall.SYS_IOCTL,
uintptr(fd),
// Even though BIOCSDIRECTION is preferred on FreeBSD, BIOCSSEESENT continues
// to work, and is required for other BSD platforms
syscall.BIOCSSEESENT,
uintptr(unsafe.Pointer(&direction)),
)
if err != 0 {
return syscall.Errno(err)
}
return nil
}
// assembleBpfInsn assembles a slice of bpf.RawInstructions to the format required by
// package syscall.
func assembleBpfInsn(filter []bpf.RawInstruction) []syscall.BpfInsn {
// Copy each bpf.RawInstruction into syscall.BpfInsn. If needed,
// the structures have the same memory layout and could probably be
// unsafely cast to each other for speed.
insns := make([]syscall.BpfInsn, 0, len(filter))
for _, ins := range filter {
insns = append(insns, syscall.BpfInsn{
Code: ins.Op,
Jt: ins.Jt,
Jf: ins.Jf,
K: ins.K,
})
}
return insns
}
// baseInterfaceFilter creates a base BPF filter which filters traffic based
// on its EtherType and returns up to "mtu" bytes of data for processing.
func baseInterfaceFilter(proto uint16, mtu int) []bpf.Instruction {
return append(
// Filter traffic based on EtherType
baseFilter(proto),
// Accept the packet bytes up to the interface's MTU
bpf.RetConstant{
Val: uint32(mtu),
},
)
}
// baseFilter creates a base BPF filter which filters traffic based on its
// EtherType. baseFilter can be prepended to other filters to handle common
// filtering tasks.
func baseFilter(proto uint16) []bpf.Instruction {
// Offset | Length | Comment
// -------------------------
// 00 | 06 | Ethernet destination MAC address
// 06 | 06 | Ethernet source MAC address
// 12 | 02 | Ethernet EtherType
const (
etherTypeOffset = 12
etherTypeLength = 2
)
return []bpf.Instruction{
// Load EtherType value from Ethernet header
bpf.LoadAbsolute{
Off: etherTypeOffset,
Size: etherTypeLength,
},
// If EtherType is equal to the protocol we are using, jump to instructions
// added outside of this function.
bpf.JumpIf{
Cond: bpf.JumpEqual,
Val: uint32(proto),
SkipTrue: 1,
},
// EtherType does not match our protocol
bpf.RetConstant{
Val: 0,
},
}
}