Compare commits

...

8 Commits

Author SHA1 Message Date
Adrian Rumpold
fbf0b5d388 fix: Delete options for virtual network script 2024-01-09 14:05:28 +01:00
Adrian Rumpold
ac7185bfb5 fix: Fix connection interface binding, JSON lease DB 2024-01-09 12:23:41 +01:00
Adrian Rumpold
cb8b6c03b6 feat(hack): Add script for venv-based virtual network 2024-01-09 12:20:50 +01:00
Adrian Rumpold
b2dc964861 chore: Add Skaffold and ko configs 2024-01-09 12:19:01 +01:00
Adrian Rumpold
0bef42e761 chore: Ignore direnv file 2024-01-09 12:18:38 +01:00
Adrian Rumpold
d11855bfd0 feat: Turn into Cobra application, add CLI flag for listen interface 2024-01-04 19:27:09 +01:00
Adrian Rumpold
7d3a40987b deps: Upgrade zap to v1.26.0 2024-01-04 18:50:10 +01:00
Adrian Rumpold
001517eb15 chore: Refactor package structure 2024-01-04 18:49:54 +01:00
17 changed files with 772 additions and 408 deletions

3
.gitignore vendored
View File

@@ -21,3 +21,6 @@ go-dhcp
# Go workspace file
go.work
# direnv
.envrc

1
.ko.yaml Normal file
View File

@@ -0,0 +1 @@
defaultBaseImage: alpine:latest

70
cmd/conn_unix.go Normal file
View File

@@ -0,0 +1,70 @@
// Copied with modifications from https://github.com/insomniacslk/dhcp/blob/master/dhcpv4/server4/conn_unix.go (under BSD-3-Clause license)
//go:build !windows
package cmd
import (
"errors"
"fmt"
"net"
"os"
"github.com/adrianokf/go-dhcp/pkg/types"
"golang.org/x/sys/unix"
)
// NewIPv4UDPConn returns a UDP connection bound to both the interface and port
// given based on a IPv4 DGRAM socket. The UDP connection allows broadcasting.
//
// The interface must already be configured.
func NewIPv4UDPConn(iface string, addr *net.UDPAddr) (*net.UDPConn, error) {
fd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, unix.IPPROTO_UDP)
if err != nil {
return nil, fmt.Errorf("cannot get a UDP socket: %v", err)
}
f := os.NewFile(uintptr(fd), "")
// net.FilePacketConn dups the FD, so we have to close this in any case.
defer f.Close()
// Allow broadcasting.
if err := unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_BROADCAST, 1); err != nil {
return nil, fmt.Errorf("cannot set broadcasting on socket: %v", err)
}
// Allow reusing the addr to aid debugging.
if err := unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_REUSEADDR, 1); err != nil {
return nil, fmt.Errorf("cannot set reuseaddr on socket: %v", err)
}
// Allow reusing the port to aid debugging and testing.
if err := unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_REUSEPORT, 1); err != nil {
return nil, fmt.Errorf("cannot set reuseport on socket: %v", err)
}
if len(iface) != 0 {
// Bind directly to the interface.
if err := unix.BindToDevice(fd, iface); err != nil {
return nil, fmt.Errorf("cannot bind to interface %s: %v", iface, err)
}
}
if addr == nil {
addr = &net.UDPAddr{Port: types.ServerPort}
}
// Bind to the port.
saddr := unix.SockaddrInet4{Port: addr.Port}
if addr.IP != nil && addr.IP.To4() == nil {
return nil, fmt.Errorf("wrong address family (expected v4) for %s", addr.IP)
}
copy(saddr.Addr[:], addr.IP.To4())
if err := unix.Bind(fd, &saddr); err != nil {
return nil, fmt.Errorf("cannot bind to port %d: %v", addr.Port, err)
}
conn, err := net.FilePacketConn(f)
if err != nil {
return nil, err
}
udpconn, ok := conn.(*net.UDPConn)
if !ok {
return nil, errors.New("BUG: incorrect socket type, expected UDP")
}
return udpconn, nil
}

339
cmd/root.go Normal file
View File

@@ -0,0 +1,339 @@
package cmd
import (
"bytes"
"encoding/binary"
"fmt"
"net"
"os"
"unsafe"
"github.com/adrianokf/go-dhcp/pkg/leases"
"github.com/adrianokf/go-dhcp/pkg/messages"
"github.com/adrianokf/go-dhcp/pkg/types"
"github.com/adrianokf/go-dhcp/pkg/util"
"github.com/spf13/cobra"
"go.uber.org/zap"
)
var listenInterface string
type Handler struct {
conn *net.UDPConn
leaseManager leases.LeaseManager
}
var magic = [4]byte{0x63, 0x82, 0x53, 0x63}
func parseOptions(data []byte) messages.Options {
i := 0
options := make(messages.Options)
out:
for i < len(data) {
code := messages.OptionCode(data[i])
switch code {
case messages.OptionEnd:
zap.S().Debug("Found END option at offset ", i)
break out
case messages.OptionPad:
zap.S().Debug("Found padding option at offset ", i)
i += 1
continue
}
size := int(data[i+1])
payload := data[i+2 : i+2+size]
zap.S().Debugf("code=%d, size=%d, payload=%x", code, size, payload)
options[code] = messages.Option{Code: code, Data: data[i+1 : i+2+size]}
i += size + 2
}
zap.S().Debugf("Parsed options: ", options)
return options
}
func prepareOffer(request messages.DhcpMessage, lease leases.Lease) messages.DhcpMessage {
var sname [64]byte
var file [128]byte
var siaddr types.Ipv4Addr
copy(sname[:], "go-dhcp-server")
copy(siaddr[:], net.IPv4(10, 0, 0, 1).To4())
dhcp := messages.DhcpMessage{
Op: messages.BOOTREPLY,
Htype: 1, // Ethernet
Hlen: 6, // Ethernet address length
Hops: 0,
Secs: 0,
Flags: request.Flags,
Xid: lease.TransactionId,
Siaddr: siaddr,
Ciaddr: [4]byte{0, 0, 0, 0},
Yiaddr: lease.Address,
Giaddr: request.Giaddr,
Chaddr: request.Chaddr,
Magic: magic,
Sname: sname,
File: file,
}
return dhcp
}
func prepareAck(request messages.DhcpMessage, lease leases.Lease) messages.DhcpMessage {
var sname [64]byte
var file [128]byte
copy(sname[:], "go-dhcp-server")
dhcp := messages.DhcpMessage{
Op: messages.BOOTREPLY,
Htype: 1, // Ethernet
Hlen: 6, // Ethernet address length
Hops: 0,
Secs: 0,
Flags: 0,
Xid: request.Xid,
Siaddr: lease.Address,
Ciaddr: request.Ciaddr,
Yiaddr: lease.Address,
Giaddr: request.Giaddr,
Chaddr: request.Chaddr,
Magic: magic,
Sname: sname,
File: file,
}
return dhcp
}
// sendMessage transmits a DHCP message with options via a UDP connection
// The end option (code 255) is automatically appended and does not need to
// be passed explicitly.
func (h Handler) sendMessage(remote *net.UDPAddr, message messages.DhcpMessage, options []messages.Option) error {
// Send packets for 0.0.0.0 to broadcast address (255.255.255.255) instead
var destination *net.UDPAddr
if remote.IP.IsUnspecified() {
destination = &net.UDPAddr{
IP: net.ParseIP("255.255.255.255"),
Port: types.ClientPort,
}
} else {
destination = remote
}
buf := make([]byte, 0)
w := bytes.NewBuffer(buf)
err := binary.Write(w, binary.BigEndian, message)
if err != nil {
return err
}
for _, v := range options {
err = w.WriteByte(byte(v.Code))
if err != nil {
return err
}
_, err = w.Write(v.Data)
if err != nil {
return err
}
}
// Automatically add END option, so the caller doesn't need to specify it for every invocation.
err = w.WriteByte(byte(messages.OptionEnd))
if err != nil {
return err
}
msg := w.Bytes()
zap.S().Debugf("Local addr: %s, remote addr: %s ", h.conn.LocalAddr(), destination)
zap.S().Debug("Msg data: ", msg)
_, err = h.conn.WriteToUDP(msg, destination)
if err != nil {
return err
}
return nil
}
func (h Handler) sendOffer(dhcp messages.DhcpMessage, remote *net.UDPAddr) error {
s := zap.S()
lease, err := h.leaseManager.Request(dhcp.Xid, dhcp.Chaddr)
if err != nil {
panic(err)
}
offer := prepareOffer(dhcp, *lease)
s.Info("Sending DHCPOFFER...")
options := []messages.Option{
{
Code: messages.OptionDHCPMessageType,
Data: []byte{1, byte(messages.MessageTypeOffer)},
},
}
h.sendMessage(remote, offer, options)
return nil
}
func (h Handler) sendAck(dhcp messages.DhcpMessage, remote *net.UDPAddr) error {
s := zap.S()
lease, err := h.leaseManager.Lookup(dhcp.Xid)
if err != nil {
panic(err)
}
ack := prepareAck(dhcp, *lease)
options := []messages.Option{
{
Code: messages.OptionDHCPMessageType,
Data: []byte{1, byte(messages.MessageTypeAck)},
},
{
Code: messages.OptionIPAddressLeaseTime,
Data: append([]byte{4}, util.U32ToByte(3600)...),
},
}
s.Debug("Options: ", options)
s.Info("Sending DHCPACK")
h.sendMessage(remote, ack, options)
lease, err = h.leaseManager.Request(dhcp.Xid, dhcp.Chaddr)
if err != nil {
return err
}
s.Debug("Found lease: ", lease)
return nil
}
func (h Handler) handleMsg(data []byte, remote *net.UDPAddr) {
s := zap.S()
s.Debugf("Connection from client %v", remote.IP)
var dhcp messages.DhcpMessage
reader := bytes.NewReader(data)
binary.Read(reader, binary.BigEndian, &dhcp)
dhcp.Debug(s)
if dhcp.Magic != magic {
panic("Invalid DHCP magic field")
}
optDataOffset := int(unsafe.Sizeof(dhcp))
optData := data[optDataOffset:]
s.Debug("Raw options data:", optData)
options := parseOptions(optData)
dhcpMsgType := options[messages.OptionDHCPMessageType]
s.Info("DHCP message type ", dhcpMsgType)
switch messages.MessageType(dhcpMsgType.Data[1]) {
case messages.MessageTypeDiscover:
go h.sendOffer(dhcp, remote)
case messages.MessageTypeRequest:
go h.sendAck(dhcp, remote)
}
}
func runServer(interfaceName string) {
var addr *net.UDPAddr
if interfaceName == "all" {
zap.L().Debug("Listening on all interfaces")
addr, _ = net.ResolveUDPAddr("udp4", ":67")
} else {
zap.S().Debug("Listening on interface ", interfaceName)
iface, err := net.InterfaceByName(interfaceName)
if err != nil {
panic(err)
}
addrs, err := iface.Addrs()
if err != nil {
panic(err)
}
// Find first IPv4 address associated with the interface
var ip net.IP = nil
for _, ifaddr := range addrs {
switch a := ifaddr.(type) {
case *net.IPAddr:
ip = a.IP
case *net.IPNet:
ip = a.IP
}
ip = ip.To4()
if ip != nil {
break
}
}
if ip == nil {
zap.S().Panic("No IPv4 address associated with interface ", interfaceName)
}
addr, _ = net.ResolveUDPAddr("udp4", ":67")
}
var conn *net.UDPConn
var err error
if interfaceName != "all" {
conn, err = NewIPv4UDPConn(interfaceName, addr)
} else {
conn, err = net.ListenUDP("udp4", addr)
}
if err != nil {
panic(err)
}
defer conn.Close()
handler := Handler{
conn: conn,
leaseManager: *leases.NewLeaseManager(),
}
zap.S().Infof("Listening for incoming connections on %s", addr.String())
for {
buf := make([]byte, 1024)
rlen, remote, err := conn.ReadFrom(buf[:])
if err != nil {
panic(err)
}
// Do stuff with the read bytes
remoteAddr, ok := remote.(*net.UDPAddr)
if !ok {
zap.S().Warn("Not a valid remote IP address: ", remote)
continue
}
go handler.handleMsg(buf[0:rlen], remoteAddr)
}
}
var rootCmd = &cobra.Command{
Use: "go-dhcp",
Short: "go-dhcp is a simple DHCP server written in Go",
Run: func(cmd *cobra.Command, args []string) {
runServer(listenInterface)
},
}
func init() {
// Set up logging
logger, _ := zap.NewDevelopment()
defer logger.Sync() // flushes buffer, if any
zap.ReplaceGlobals(logger)
rootCmd.PersistentFlags().StringVarP(&listenInterface, "interface", "i", "all", "Interface to listen on")
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}

12
go.mod
View File

@@ -2,9 +2,15 @@ module github.com/adrianokf/go-dhcp
go 1.17
require go.uber.org/zap v1.21.0
require go.uber.org/zap v1.26.0
require (
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
)
require (
github.com/spf13/cobra v1.8.0
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/sys v0.16.0
)

45
go.sum
View File

@@ -1,28 +1,37 @@
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8=
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
@@ -38,6 +47,8 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -50,8 +61,6 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

51
hack/virtual-network.sh Executable file
View File

@@ -0,0 +1,51 @@
#!/bin/bash -ex
# Manage a venv-based virtual network for testing, consisting of:
#
# - venv0: Interface for the DHCP server (static address: 10.0.0.254/24)
# - venv1: Interface for DHCP client
# - br-dhcp: A bridge for communication between these interfaces
#
# Default behavior is to create the network, the `-d` option removes it.
BR=br-dhcp
SERVERADDR="10.0.0.254/24"
# Parse arguments
DELETE=0
while getopts "d" opt; do
case $opt in
d)
DELETE=1
;;
*)
;;
esac
done
if [ "$DELETE" != "0" ]; then
for i in {0..1}; do
IFNAME="veth${i}"
ip link del "$IFNAME" || true
done
ip link set "$BR" down
brctl delbr $BR
exit 0
fi
brctl addbr $BR
for i in {0..1}; do
IFNAME="veth${i}"
PEERNAME="${IFNAME}s"
ip link add "$IFNAME" type veth peer name "$PEERNAME"
ip link set "$IFNAME" up
ip link set "$PEERNAME" up
brctl addif $BR "$PEERNAME"
done
ip addr add dev veth0 "$SERVERADDR"
ip link set "$BR" up

View File

@@ -1,67 +0,0 @@
package main
import (
"errors"
"fmt"
"time"
"go.uber.org/zap"
)
type ILeaseManager interface {
Request(xid TxId, clientAddr HwAddr) (*Lease, error)
Release(l Lease) error
Lookup(xid TxId) error
}
type LeaseManager struct {
leases map[TxId]Lease
count byte
}
func NewLeaseManager() *LeaseManager {
m := &LeaseManager{
leases: make(map[TxId]Lease),
}
return m
}
func (m *LeaseManager) Request(xid TxId, clientAddr HwAddr) (*Lease, error) {
zap.S().Debugf("LeaseManager.Request(%v, %v)", xid, clientAddr)
if m.count > 254 {
return nil, errors.New("lease address pool exhausted")
}
assigned := [4]byte{10, 0, 0, m.count + 2}
lease := Lease{
TransactionId: xid,
TTL: time.Now().Add(1 * time.Hour),
State: Requested,
ClientAddr: clientAddr,
Address: assigned,
}
m.leases[xid] = lease
m.count += 1
zap.S().Debug("lease=", lease)
return &lease, nil
}
func (m LeaseManager) Release(l Lease) error {
_, found := m.leases[l.TransactionId]
if !found {
return fmt.Errorf("invalid lease %v", l)
}
delete(m.leases, l.TransactionId)
return nil
}
func (m LeaseManager) Lookup(xid TxId) (*Lease, error) {
lease, found := m.leases[xid]
if found {
return &lease, nil
}
return nil, fmt.Errorf("no lease found for xid %v", xid)
}

278
main.go
View File

@@ -1,283 +1,9 @@
package main
import (
"bytes"
"encoding/binary"
"net"
"unsafe"
"go.uber.org/zap"
"github.com/adrianokf/go-dhcp/cmd"
)
var manager = NewLeaseManager()
var leases []Lease = make([]Lease, 0)
func (dhcp DhcpMessage) Debug(s *zap.SugaredLogger) {
s.Debugf("op=%x, htype=%x, hlen=%x, hops=%x", dhcp.Op, dhcp.Htype, dhcp.Hlen, dhcp.Hops)
s.Debugf("xid=%x", dhcp.Xid)
s.Debugf("secs=%d, flags=%x", dhcp.Secs, dhcp.Flags)
s.Debugf("ciaddr=%x", dhcp.Ciaddr)
s.Debugf("siaddr=%x", dhcp.Siaddr)
s.Debugf("giaddr=%x", dhcp.Giaddr)
s.Debugf("chaddr=%x", dhcp.Chaddr)
s.Debugf("sname=%s", string(dhcp.Sname[:]))
s.Debugf("file=%s", string(dhcp.File[:]))
s.Debugf("magic=%x", dhcp.Magic)
}
func parseOptions(data []byte) Options {
s := zap.S()
i := 0
options := make(Options)
out:
for i < len(data) {
code := OptionCode(data[i])
switch code {
case OptionEnd:
s.Debug("Found END option at offset ", i)
break out
case OptionPad:
s.Debug("Found padding option at offset ", i)
i += 1
continue
}
size := int(data[i+1])
payload := data[i+2 : i+2+size]
s.Debugf("code=%d, size=%d, payload=%x", code, size, payload)
options[code] = Option{Code: code, Data: data[i+1 : i+2+size]}
i += size + 2
}
s.Debugf("Parsed options: ", options)
return options
}
func prepareOffer(request DhcpMessage, lease Lease) DhcpMessage {
var sname [64]byte
var file [128]byte
var siaddr Ipv4Addr
copy(sname[:], "go-dhcp-server")
copy(siaddr[:], net.IPv4(10, 0, 0, 1).To4())
dhcp := DhcpMessage{
Op: BOOTREPLY,
Htype: 1, // Ethernet
Hlen: 6, // Ethernet address length
Hops: 0,
Secs: 0,
Flags: request.Flags,
Xid: lease.TransactionId,
Siaddr: siaddr,
Ciaddr: [4]byte{0, 0, 0, 0},
Yiaddr: lease.Address,
Giaddr: request.Giaddr,
Chaddr: request.Chaddr,
Magic: magic,
Sname: sname,
File: file,
}
return dhcp
}
func prepareAck(request DhcpMessage, lease Lease) DhcpMessage {
var sname [64]byte
var file [128]byte
copy(sname[:], "go-dhcp-server")
dhcp := DhcpMessage{
Op: BOOTREPLY,
Htype: 1, // Ethernet
Hlen: 6, // Ethernet address length
Hops: 0,
Secs: 0,
Flags: 0,
Xid: request.Xid,
Siaddr: lease.Address,
Ciaddr: request.Ciaddr,
Yiaddr: lease.Address,
Giaddr: request.Giaddr,
Chaddr: request.Chaddr,
Magic: magic,
Sname: sname,
File: file,
}
return dhcp
}
// sendMessage transmits a DHCP message with options via a UDP connection
// The end option (code 255) is automatically appended and does not need to
// be passed explicitly.
func sendMessage(conn *net.UDPConn, message DhcpMessage, options []Option) error {
buf := make([]byte, 0)
w := bytes.NewBuffer(buf)
err := binary.Write(w, binary.BigEndian, message)
if err != nil {
return err
}
for _, v := range options {
err = w.WriteByte(byte(v.Code))
if err != nil {
return err
}
_, err = w.Write(v.Data)
if err != nil {
return err
}
}
// Automatically add END option, so the caller doesn't
// need to specificy it for every invocation.
err = w.WriteByte(byte(OptionEnd))
if err != nil {
return err
}
msg := w.Bytes()
zap.S().Debug("Msg", msg)
_, err = conn.Write(msg)
if err != nil {
return err
}
return nil
}
func handleOffer(dhcp DhcpMessage, remote *net.UDPAddr) error {
s := zap.S()
lease, err := manager.Request(dhcp.Xid, dhcp.Chaddr)
if err != nil {
panic(err)
}
offer := prepareOffer(dhcp, *lease)
localAddr, _ := net.ResolveUDPAddr("udp", "172.17.0.1:68")
clientAddr, _ := net.ResolveUDPAddr("udp", "255.255.255.255:68")
conn, err := net.DialUDP("udp", localAddr, clientAddr)
if err != nil {
panic(err)
}
defer conn.Close()
s.Info("Sending DHCPOFFER...")
options := []Option{
{
Code: OptionDHCPMessageType,
Data: []byte{1, byte(MessageTypeOffer)},
},
}
sendMessage(conn, offer, options)
return nil
}
func handleAck(dhcp DhcpMessage, remote *net.UDPAddr) error {
s := zap.S()
lease, err := manager.Lookup(dhcp.Xid)
if err != nil {
panic(err)
}
ack := prepareAck(dhcp, *lease)
options := []Option{
{
Code: OptionDHCPMessageType,
Data: []byte{1, byte(MessageTypeAck)},
},
{
Code: OptionIPAddressLeaseTime,
Data: append([]byte{4}, u32tob(3600)...),
},
}
s.Debug("Options: ", options)
localAddr, _ := net.ResolveUDPAddr("udp", "172.17.0.1:68")
clientAddr, _ := net.ResolveUDPAddr("udp", "255.255.255.255:68")
conn, err := net.DialUDP("udp", localAddr, clientAddr)
if err != nil {
panic(err)
}
defer conn.Close()
s.Info("Sending DHCPACK")
sendMessage(conn, ack, options)
lease, err = manager.Request(dhcp.Xid, dhcp.Chaddr)
if err != nil {
return err
}
s.Debug("Found lease", lease)
return nil
}
func handleMsg(data []byte, remote *net.UDPAddr) {
s := zap.S()
s.Debugf("Connection from client %v", remote.IP)
var dhcp DhcpMessage
reader := bytes.NewReader(data)
binary.Read(reader, binary.BigEndian, &dhcp)
dhcp.Debug(s)
if dhcp.Magic != [4]byte{0x63, 0x82, 0x53, 0x63} {
panic("Invalid DHCP magic field")
}
optDataOffset := int(unsafe.Sizeof(dhcp))
optData := data[optDataOffset:]
s.Debug("Raw options data:", optData)
options := parseOptions(optData)
dhcpMsgType := options[OptionDHCPMessageType]
s.Info("DHCP message type ", dhcpMsgType)
switch MessageType(dhcpMsgType.Data[1]) {
case MessageTypeDiscover:
go handleOffer(dhcp, remote)
case MessageTypeRequest:
go handleAck(dhcp, remote)
}
}
func main() {
// Set up logging
logger, _ := zap.NewDevelopment()
defer logger.Sync() // flushes buffer, if any
zap.ReplaceGlobals(logger)
addr, _ := net.ResolveUDPAddr("udp4", ":67")
conn, err := net.ListenUDP("udp4", addr)
if err != nil {
panic(err)
}
defer conn.Close()
zap.L().Info("Listening for incoming connections")
for {
buf := make([]byte, 1024)
rlen, remote, err := conn.ReadFrom(buf[:])
if err != nil {
panic(err)
}
// Do stuff with the read bytes
remoteAddr, ok := remote.(*net.UDPAddr)
if !ok {
zap.S().Warn("Not a valid remote IP address: ", remote)
continue
}
go handleMsg(buf[0:rlen], remoteAddr)
}
cmd.Execute()
}

136
pkg/leases/leases.go Normal file
View File

@@ -0,0 +1,136 @@
package leases
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"time"
"github.com/adrianokf/go-dhcp/pkg/types"
"go.uber.org/zap"
)
type LeaseState uint8
const (
Offered LeaseState = iota
Requested
)
const DumpFile = "/var/lib/go-dhcp/leases.json"
type Lease struct {
TransactionId types.TxId `json:"TransactionID"`
TTL time.Time `json:"TTL"`
State LeaseState `json:"State"`
ClientAddr types.HwAddr `json:"ClientAddr"`
Address types.Ipv4Addr `json:"Address"`
}
type ILeaseManager interface {
Request(xid types.TxId, clientAddr types.HwAddr) (*Lease, error)
Release(l Lease) error
Lookup(xid types.TxId) error
}
type LeaseManager struct {
leases map[types.TxId]Lease
count byte
}
func (m LeaseManager) DumpLeases() {
err := os.MkdirAll(filepath.Dir(DumpFile), 0755)
if err != nil {
zap.S().Panic(err)
}
f, err := os.Create(DumpFile)
if err != nil {
zap.S().Panic(err)
}
defer f.Close()
leases := make([]Lease, len(m.leases))
for _, l := range m.leases {
leases = append(leases, l)
}
enc := json.NewEncoder(f)
enc.Encode(leases)
f.Sync()
}
func LoadLeases() (map[types.TxId]Lease, error) {
var leaseMap = make(map[types.TxId]Lease)
data, err := os.ReadFile(DumpFile)
if err != nil {
return leaseMap, err
}
var leases []Lease
json.Unmarshal(data, &leases)
for _, l := range leases {
leaseMap[l.TransactionId] = l
}
return leaseMap, nil
}
func NewLeaseManager() *LeaseManager {
leases, err := LoadLeases()
if err != nil {
zap.S().Warnf("Could not restore saved leases from %s: %s", DumpFile, err)
}
m := &LeaseManager{
leases: leases,
}
return m
}
func (m *LeaseManager) Request(xid types.TxId, clientAddr types.HwAddr) (*Lease, error) {
zap.S().Debugf("LeaseManager.Request(%v, %v)", xid, clientAddr)
if m.count > 254 {
return nil, errors.New("lease address pool exhausted")
}
assigned := [4]byte{10, 0, 0, m.count + 2}
lease := Lease{
TransactionId: xid,
TTL: time.Now().Add(1 * time.Hour),
State: Requested,
ClientAddr: clientAddr,
Address: assigned,
}
m.leases[xid] = lease
m.count += 1
zap.S().Debug("lease=", lease)
m.DumpLeases()
return &lease, nil
}
func (m LeaseManager) Release(l Lease) error {
_, found := m.leases[l.TransactionId]
if !found {
return fmt.Errorf("invalid lease %v", l)
}
delete(m.leases, l.TransactionId)
m.DumpLeases()
return nil
}
func (m LeaseManager) Lookup(xid types.TxId) (*Lease, error) {
lease, found := m.leases[xid]
if found {
return &lease, nil
}
return nil, fmt.Errorf("no lease found for xid %v", xid)
}

View File

@@ -1,9 +1,15 @@
package main
package messages
import (
"time"
"github.com/adrianokf/go-dhcp/pkg/types"
"go.uber.org/zap"
)
type Message interface {
Marshal() ([]byte, error)
Unmarshal([]byte) (Message, error)
}
type OptionCode byte
// DHCPv4 Options
@@ -195,40 +201,35 @@ const (
MessageTypeInform
)
type LeaseState uint8
type TxId [4]byte
type Ipv4Addr [4]byte
type HwAddr [16]byte
var magic = [4]byte{0x63, 0x82, 0x53, 0x63}
type DhcpMessage struct {
Op DhcpOp
Htype MessageType
Hlen byte
Hops byte
Xid TxId
Xid types.TxId
Secs uint16
Flags uint16
Ciaddr Ipv4Addr
Yiaddr Ipv4Addr
Siaddr Ipv4Addr
Giaddr Ipv4Addr
Chaddr HwAddr
Ciaddr types.Ipv4Addr
Yiaddr types.Ipv4Addr
Siaddr types.Ipv4Addr
Giaddr types.Ipv4Addr
Chaddr types.HwAddr
Sname [64]byte
File [128]byte
Magic [4]byte
}
const (
Offered LeaseState = iota
Requested
)
func (dhcp DhcpMessage) Debug(s *zap.SugaredLogger) {
s.Debugf("op=%x, htype=%x, hlen=%x, hops=%x", dhcp.Op, dhcp.Htype, dhcp.Hlen, dhcp.Hops)
s.Debugf("xid=%x", dhcp.Xid)
s.Debugf("secs=%d, flags=%x", dhcp.Secs, dhcp.Flags)
type Lease struct {
TransactionId TxId
TTL time.Time
State LeaseState
ClientAddr HwAddr
Address Ipv4Addr
s.Debugf("ciaddr=%x", dhcp.Ciaddr)
s.Debugf("siaddr=%x", dhcp.Siaddr)
s.Debugf("giaddr=%x", dhcp.Giaddr)
s.Debugf("chaddr=%x", dhcp.Chaddr)
s.Debugf("sname=%s", string(dhcp.Sname[:]))
s.Debugf("file=%s", string(dhcp.File[:]))
s.Debugf("magic=%x", dhcp.Magic)
}

View File

@@ -1,4 +1,4 @@
package main
package messages
type Option struct {
Code OptionCode

46
pkg/types/json.go Normal file
View File

@@ -0,0 +1,46 @@
package types
import (
"fmt"
"net"
"strconv"
util "github.com/adrianokf/go-dhcp/pkg/util"
)
func (txid *TxId) UnmarshalText(text []byte) (err error) {
result, err := strconv.ParseUint(string(text), 16, 32)
if err != nil {
return err
}
copy(txid[:], util.U32ToByte(uint32(result)))
return nil
}
func (id TxId) MarshalText() ([]byte, error) {
return []byte(fmt.Sprintf("%x", id)), nil
}
func (addr *Ipv4Addr) UnmarshalText(text []byte) (err error) {
ip := net.ParseIP(string(text))
fmt.Println(ip)
if ip == nil {
return fmt.Errorf("could not parse IP address: '%s'", text)
}
*addr = Ipv4AddrFromNetIP(ip)
return nil
}
func (addr Ipv4Addr) MarshalText() ([]byte, error) {
return addr.ToNetIPv4().MarshalText()
}
func (addr HwAddr) MarshalText() ([]byte, error) {
return []byte(addr.String()), nil
}
func (addr *HwAddr) UnmarshalText(text []byte) (err error) {
*addr, err = HwAddrFromString(string(text))
return err
}

37
pkg/types/types.go Normal file
View File

@@ -0,0 +1,37 @@
package types
import (
"fmt"
"net"
)
type TxId [4]byte
type Ipv4Addr [4]byte
type HwAddr [16]byte
const (
ServerPort = 67
ClientPort = 68
)
func Ipv4AddrFromNetIP(ip net.IP) Ipv4Addr {
ipv4 := ip.To4()
return Ipv4Addr{ipv4[0], ipv4[1], ipv4[2], ipv4[3]}
}
func (addr Ipv4Addr) ToNetIPv4() net.IP {
return net.IPv4(addr[0], addr[1], addr[2], addr[3])
}
func (addr Ipv4Addr) String() string {
return addr.ToNetIPv4().String()
}
func (addr HwAddr) String() string {
return fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x", addr[0], addr[1], addr[2], addr[3], addr[4], addr[5])
}
func HwAddrFromString(s string) (addr HwAddr, err error) {
_, err = fmt.Sscanf(s, "%02x:%02x:%02x:%02x:%02x:%02x", &addr[0], &addr[1], &addr[2], &addr[3], &addr[4], &addr[5])
return addr, err
}

11
pkg/util/util.go Normal file
View File

@@ -0,0 +1,11 @@
package util
import (
"encoding/binary"
)
func U32ToByte(u uint32) []byte {
buf := make([]byte, 4)
binary.BigEndian.PutUint32(buf, u)
return buf
}

13
skaffold.yaml Normal file
View File

@@ -0,0 +1,13 @@
apiVersion: skaffold/v4beta8
kind: Config
metadata:
name: go-dhcp
build:
local:
push: false
artifacts:
- image: go-dhcp
ko: {}
deploy:
docker:
images: [go-dhcp]

18
util.go
View File

@@ -1,18 +0,0 @@
package main
import (
"encoding/binary"
"net"
)
func int2ip(nn uint32) net.IP {
ip := make(net.IP, 4)
binary.BigEndian.PutUint32(ip, nn)
return ip
}
func u32tob(u uint32) []byte {
buf := make([]byte, 4)
binary.BigEndian.PutUint32(buf, u)
return buf
}