367 lines
9.0 KiB
Go
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,
|
|
},
|
|
}
|
|
}
|