Skip to content

Commit

Permalink
Add support for AF_UNIX SOCK_DGRAM
Browse files Browse the repository at this point in the history
Unfortunately these aren't supported by libuv directly, so we need to
implement our own separate system. It doesn't need to be particularly
scalable as the expected use-case is to send and receive ethernet frames.

The server will listen on a Unix domain socket and receive connected
SOCK_DGRAM sockets which will have ethernet frames on them.

Signed-off-by: David Scott <[email protected]>
  • Loading branch information
djs55 committed Oct 10, 2022
1 parent ac3b17b commit df53b9f
Show file tree
Hide file tree
Showing 34 changed files with 2,237 additions and 781 deletions.
41 changes: 41 additions & 0 deletions go/cmd/vmnet-example/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package main

import (
"context"
"flag"
"fmt"
"log"
"os"

"github.com/google/uuid"
"github.com/moby/vpnkit/go/pkg/vmnet"
)

var path string

func main() {
flag.StringVar(&path, "path", "", "path to vmnet socket")
flag.Parse()
if path == "" {
fmt.Fprintf(os.Stderr, "Please supply a --path argument\n")
}
vm, err := vmnet.Connect(context.Background(), vmnet.Config{
Path: path,
})
if err != nil {
log.Fatal(err)
}
defer vm.Close()
log.Println("connected to vmnet service")
u, err := uuid.NewRandom()
if err != nil {
log.Fatal(err)
}
vif, err := vm.ConnectVif(u)
if err != nil {
log.Fatal(err)
}
defer vif.Close()
log.Printf("VIF has IP %s", vif.IP)
log.Printf("SOCK_DGRAM fd: %d", vif.Ethernet.Fd)
}
50 changes: 50 additions & 0 deletions go/pkg/vmnet/datagram.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package vmnet

/*
// FIXME: Needed because we call C.send. Perhaps we could use syscall instead?
#include <stdlib.h>
#include <sys/socket.h>
*/
import "C"

import (
"syscall"

"github.com/pkg/errors"
)

// Datagram sends and receives ethernet frames via send/recv over a SOCK_DGRAM fd.
type Datagram struct {
Fd int // Underlying SOCK_DGRAM file descriptor.
pcap *PcapWriter
}

func (e Datagram) Recv(buf []byte) (int, error) {
num, _, err := syscall.Recvfrom(e.Fd, buf, 0)
if e.pcap != nil {
if err := e.pcap.Write(buf[0:num]); err != nil {
return 0, errors.Wrap(err, "writing to pcap")
}
}
return num, err
}

func (e Datagram) Send(packet []byte) (int, error) {
if e.pcap != nil {
if err := e.pcap.Write(packet); err != nil {
return 0, errors.Wrap(err, "writing to pcap")
}
}
result, err := C.send(C.int(e.Fd), C.CBytes(packet), C.size_t(len(packet)), 0)
if result == -1 {
return 0, err
}
return len(packet), nil
}

func (e Datagram) Close() error {
return syscall.Close(e.Fd)
}

var _ sendReceiver = Datagram{}
114 changes: 114 additions & 0 deletions go/pkg/vmnet/dhcp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package vmnet

import (
"net"
"time"
)

// dhcp queries the IP by DHCP
func dhcpRequest(packet sendReceiver, clientMAC net.HardwareAddr) (net.IP, error) {
broadcastMAC := []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}
broadcastIP := []byte{0xff, 0xff, 0xff, 0xff}
unknownIP := []byte{0, 0, 0, 0}

dhcpRequest := NewDhcpRequest(clientMAC).Bytes()
ipv4 := NewIpv4(broadcastIP, unknownIP)

udpv4 := NewUdpv4(ipv4, 68, 67, dhcpRequest)
ipv4.setData(udpv4.Bytes())

ethernet := NewEthernetFrame(broadcastMAC, clientMAC, 0x800)
ethernet.setData(ipv4.Bytes())
finished := false
go func() {
for !finished {
if _, err := packet.Send(ethernet.Bytes()); err != nil {
panic(err)
}
time.Sleep(time.Second)
}
}()

buf := make([]byte, 1500)
for {
n, err := packet.Recv(buf)
if err != nil {
return nil, err
}
response := buf[0:n]
ethernet, err = ParseEthernetFrame(response)
if err != nil {
continue
}
for i, x := range ethernet.Dst {
if i > len(clientMAC) || clientMAC[i] != x {
// intended for someone else
continue
}
}
ipv4, err = ParseIpv4(ethernet.Data)
if err != nil {
// probably not an IPv4 packet
continue
}
udpv4, err = ParseUdpv4(ipv4.Data)
if err != nil {
// probably not a UDPv4 packet
continue
}
if udpv4.Src != 67 || udpv4.Dst != 68 {
// not a DHCP response
continue
}
if len(udpv4.Data) < 243 {
// truncated
continue
}
if udpv4.Data[240] != 53 || udpv4.Data[241] != 1 || udpv4.Data[242] != 2 {
// not a DHCP offer
continue
}
var ip net.IP
ip = udpv4.Data[16:20]
finished = true // will terminate sending goroutine
return ip, nil
}
}

// DhcpRequest is a simple DHCP request
type DhcpRequest struct {
MAC net.HardwareAddr
}

// NewDhcpRequest constructs a DHCP request
func NewDhcpRequest(MAC net.HardwareAddr) *DhcpRequest {
if len(MAC) != 6 {
panic("MAC address must be 6 bytes")
}
return &DhcpRequest{MAC}
}

// Bytes returns the marshalled DHCP request
func (d *DhcpRequest) Bytes() []byte {
bs := []byte{
0x01, // OP
0x01, // HTYPE
0x06, // HLEN
0x00, // HOPS
0x01, 0x00, 0x00, 0x00, // XID
0x00, 0x00, // SECS
0x80, 0x00, // FLAGS
0x00, 0x00, 0x00, 0x00, // CIADDR
0x00, 0x00, 0x00, 0x00, // YIADDR
0x00, 0x00, 0x00, 0x00, // SIADDR
0x00, 0x00, 0x00, 0x00, // GIADDR
d.MAC[0], d.MAC[1], d.MAC[2], d.MAC[3], d.MAC[4], d.MAC[5],
}
bs = append(bs, make([]byte, 202)...)
bs = append(bs, []byte{
0x63, 0x82, 0x53, 0x63, // Magic cookie
0x35, 0x01, 0x01, // DHCP discover
0xff, // Endmark
}...)
return bs
}
66 changes: 66 additions & 0 deletions go/pkg/vmnet/ethernet.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package vmnet

import (
"bytes"
"encoding/binary"
"io"
"net"

"github.com/pkg/errors"
)

// EthernetFrame is an ethernet frame
type EthernetFrame struct {
Dst net.HardwareAddr
Src net.HardwareAddr
Type uint16
Data []byte
}

// NewEthernetFrame constructs an Ethernet frame
func NewEthernetFrame(Dst, Src net.HardwareAddr, Type uint16) *EthernetFrame {
Data := make([]byte, 0)
return &EthernetFrame{Dst, Src, Type, Data}
}

func (e *EthernetFrame) setData(data []byte) {
e.Data = data
}

// Write marshals an Ethernet frame
func (e *EthernetFrame) Write(w io.Writer) error {
if err := binary.Write(w, binary.BigEndian, e.Dst); err != nil {
return err
}
if err := binary.Write(w, binary.BigEndian, e.Src); err != nil {
return err
}
if err := binary.Write(w, binary.BigEndian, e.Type); err != nil {
return err
}
if err := binary.Write(w, binary.BigEndian, e.Data); err != nil {
return err
}
return nil
}

// ParseEthernetFrame parses the ethernet frame
func ParseEthernetFrame(frame []byte) (*EthernetFrame, error) {
if len(frame) < (6 + 6 + 2) {
return nil, errors.New("Ethernet frame is too small")
}
Dst := frame[0:6]
Src := frame[6:12]
Type := uint16(frame[12])<<8 + uint16(frame[13])
Data := frame[14:]
return &EthernetFrame{Dst, Src, Type, Data}, nil
}

// Bytes returns the marshalled ethernet frame
func (e *EthernetFrame) Bytes() []byte {
buf := bytes.NewBufferString("")
if err := e.Write(buf); err != nil {
panic(err)
}
return buf.Bytes()
}
54 changes: 54 additions & 0 deletions go/pkg/vmnet/framing.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package vmnet

import (
"encoding/binary"
"io"
)

// Messages sent to vpnkit can either be
// - fixed-size, no length prefix
// - variable-length, with a length prefix

// fixedSizeSendReceiver sends and receives fixed-size control messages with no length prefix.
type fixedSizeSendReceiver struct {
rw io.ReadWriter
}

var _ sendReceiver = fixedSizeSendReceiver{}

func (f fixedSizeSendReceiver) Recv(buf []byte) (int, error) {
return io.ReadFull(f.rw, buf)
}

func (f fixedSizeSendReceiver) Send(buf []byte) (int, error) {
return f.rw.Write(buf)
}

// lengthPrefixer sends and receives variable-length control messages with a length prefix.
type lengthPrefixer struct {
rw io.ReadWriter
}

var _ sendReceiver = lengthPrefixer{}

func (e lengthPrefixer) Recv(buf []byte) (int, error) {
var len uint16
if err := binary.Read(e.rw, binary.LittleEndian, &len); err != nil {
return 0, err
}
if err := binary.Read(e.rw, binary.LittleEndian, &buf); err != nil {
return 0, err
}
return int(len), nil
}

func (e lengthPrefixer) Send(packet []byte) (int, error) {
len := uint16(len(packet))
if err := binary.Write(e.rw, binary.LittleEndian, len); err != nil {
return 0, err
}
if err := binary.Write(e.rw, binary.LittleEndian, packet); err != nil {
return 0, err
}
return int(len), nil
}
68 changes: 68 additions & 0 deletions go/pkg/vmnet/ipv4.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package vmnet

import (
"net"

"github.com/pkg/errors"
)

// Ipv4 is an IPv4 frame
type Ipv4 struct {
Dst net.IP
Src net.IP
Data []byte
Checksum uint16
}

// NewIpv4 constructs a new empty IPv4 packet
func NewIpv4(Dst, Src net.IP) *Ipv4 {
Checksum := uint16(0)
Data := make([]byte, 0)
return &Ipv4{Dst, Src, Data, Checksum}
}

// ParseIpv4 parses an IP packet
func ParseIpv4(packet []byte) (*Ipv4, error) {
if len(packet) < 20 {
return nil, errors.New("IPv4 packet too small")
}
ihl := int((packet[0] & 0xf) * 4) // in octets
if len(packet) < ihl {
return nil, errors.New("IPv4 packet too small")
}
Dst := packet[12:16]
Src := packet[16:20]
Data := packet[ihl:]
Checksum := uint16(0) // assume offload
return &Ipv4{Dst, Src, Data, Checksum}, nil
}

func (i *Ipv4) setData(data []byte) {
i.Data = data
i.Checksum = uint16(0) // as if we were using offload
}

// HeaderBytes returns the marshalled form of the IPv4 header
func (i *Ipv4) HeaderBytes() []byte {
len := len(i.Data) + 20
length := [2]byte{byte(len >> 8), byte(len & 0xff)}
checksum := [2]byte{byte(i.Checksum >> 8), byte(i.Checksum & 0xff)}
return []byte{
0x45, // version + IHL
0x00, // DSCP + ECN
length[0], length[1], // total length
0x7f, 0x61, // Identification
0x00, 0x00, // Flags + Fragment offset
0x40, // TTL
0x11, // Protocol
checksum[0], checksum[1],
0x00, 0x00, 0x00, 0x00, // source
0xff, 0xff, 0xff, 0xff, // destination
}
}

// Bytes returns the marshalled IPv4 packet
func (i *Ipv4) Bytes() []byte {
header := i.HeaderBytes()
return append(header, i.Data...)
}
Loading

0 comments on commit df53b9f

Please sign in to comment.