Procházet zdrojové kódy

IPv4 server plugin (#40)

* Basic DHCPv4 server, needs more work

Signed-off-by: borna_blazevic <borna.blazevic@sartura.hr>
borna-blazevic před 6 roky
rodič
revize
1e3cb1982d

+ 2 - 0
.gitignore

@@ -1,5 +1,7 @@
 .*.swp
 cmds/coredhcp/leases.txt
+cmds/coredhcp/leases4.txt
 cmds/coredhcp/config.yml
 cmds/coredhcp/coredhcp
 cmds/client/client
+.vscode

+ 7 - 1
README.md

@@ -23,7 +23,13 @@ server6:
         # - dns: 8.8.8.8 8.8.4.4 2001:4860:4860::8888 2001:4860:4860::8844
 
 #server4:
-#    listen: '127.0.0.1:67'
+#    listen: '0.0.0.0:67'
+#    plugins:
+        # - server_id: 10.10.10.1
+        # - dns: 8.8.8.8 8.8.4.4
+        # - router: 10.10.10.1
+        # - netmask: 255.255.255.0
+        # - range: leases.txt 10.10.10.100 10.10.10.200 60s
 ```
 
 See also [config.yml.example](cmds/coredhcp/config.yml.example).

+ 5 - 1
cmds/coredhcp/config.yml.example

@@ -10,4 +10,8 @@ server4:
     listen: '0.0.0.0:67'
     interface: "eth0"
     plugins:
-        - server_id: 192.168.1.12
+        # - server_id: 10.10.10.1
+        # - dns: 8.8.8.8 8.8.4.4
+        # - router: 10.10.10.1
+        # - netmask: 255.255.255.0
+        # - range: leases.txt 10.10.10.100 10.10.10.200 60s

+ 12 - 1
coredhcp.go

@@ -177,6 +177,15 @@ func (s *Server) MainHandler4(conn net.PacketConn, peer net.Addr, req *dhcpv4.DH
 		log.Printf("MainHandler4: failed to build reply: %v", err)
 		return
 	}
+	switch mt := req.MessageType(); mt {
+	case dhcpv4.MessageTypeDiscover:
+		tmp.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeOffer))
+	case dhcpv4.MessageTypeRequest:
+		tmp.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeAck))
+	default:
+		log.Printf("plugins/server: Unhandled message type: %v", mt)
+		return
+	}
 
 	resp = tmp
 	for _, handler := range s.Handlers4 {
@@ -191,7 +200,9 @@ func (s *Server) MainHandler4(conn net.PacketConn, peer net.Addr, req *dhcpv4.DH
 			// TODO: make RFC8357 compliant
 			peer = &net.UDPAddr{IP: req.GatewayIPAddr, Port: dhcpv4.ServerPort}
 		}
-
+		if req.ClientIPAddr.IsUnspecified() {
+			peer = &net.UDPAddr{IP: net.IPv4(255, 255, 255, 255), Port: dhcpv4.ClientPort}
+		}
 		if _, err := conn.WriteTo(resp.ToBytes(), peer); err != nil {
 			log.Printf("MainHandler4: conn.Write to %v failed: %v", peer, err)
 		}

+ 56 - 0
plugins/dns/plugin.go

@@ -0,0 +1,56 @@
+package dns
+
+import (
+	"errors"
+	"net"
+
+	"github.com/coredhcp/coredhcp/handler"
+	"github.com/coredhcp/coredhcp/logger"
+	"github.com/coredhcp/coredhcp/plugins"
+	"github.com/insomniacslk/dhcp/dhcpv4"
+	"github.com/insomniacslk/dhcp/dhcpv6"
+)
+
+var log = logger.GetLogger()
+
+func init() {
+	plugins.RegisterPlugin("dns", setupDNS6, setupDNS4)
+}
+
+var (
+	dnsServers []net.IP
+)
+
+func setupDNS6(args ...string) (handler.Handler6, error) {
+	// TODO setup function for IPv6
+	log.Warning("plugins/dns: not implemented for IPv6")
+	return Handler6, nil
+}
+
+func setupDNS4(args ...string) (handler.Handler4, error) {
+	log.Printf("plugins/dns: loaded plugin for DHCPv4.")
+	if len(args) < 1 {
+		return nil, errors.New("need at least one DNS server")
+	}
+	for _, arg := range args {
+		DNSServer := net.ParseIP(arg)
+		if DNSServer.To4() == nil {
+			return Handler4, errors.New("expected an DNS server address, got: " + arg)
+		}
+		dnsServers = append(dnsServers, DNSServer)
+	}
+	log.Infof("plugins/dns: loaded %d DNS servers.", len(dnsServers))
+	return Handler4, nil
+}
+
+// Handler6 not implemented only IPv4
+func Handler6(req, resp dhcpv6.DHCPv6) (dhcpv6.DHCPv6, bool) {
+	// TODO add DNS servers for v6 to the response
+	return resp, false
+}
+
+//Handler4 handles DHCPv4 packets for the dns plugin
+func Handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
+	resp.Options.Update(dhcpv4.OptDNS(dnsServers...))
+	return resp, false
+}

+ 51 - 9
plugins/file/plugin.go

@@ -31,6 +31,39 @@ var (
 	DHCPv4Records map[string]net.IP
 )
 
+// LoadDHCPv4Records loads the DHCPv4Records global map with records stored on
+// the specified file. The records have to be one per line, a mac address and an
+// IPv4 address.
+func LoadDHCPv4Records(filename string) (map[string]net.IP, error) {
+	log.Printf("plugins/file: reading leases from %s", filename)
+	data, err := ioutil.ReadFile(filename)
+	if err != nil {
+		return nil, err
+	}
+	records := make(map[string]net.IP)
+	for _, lineBytes := range bytes.Split(data, []byte{'\n'}) {
+		line := string(lineBytes)
+		if len(line) == 0 {
+			continue
+		}
+		tokens := strings.Fields(line)
+		if len(tokens) != 2 {
+			return nil, fmt.Errorf("malformed line, want 2 fields, got %d: %s", len(tokens), line)
+		}
+		hwaddr, err := net.ParseMAC(tokens[0])
+		if err != nil {
+			return nil, fmt.Errorf("malformed hardware address: %s", tokens[0])
+		}
+		ipaddr := net.ParseIP(tokens[1])
+		if ipaddr.To4() == nil {
+			return nil, fmt.Errorf("expected an IPv4 address, got: %v", ipaddr)
+		}
+		records[hwaddr.String()] = ipaddr
+	}
+
+	return records, nil
+}
+
 // LoadDHCPv6Records loads the DHCPv6Records global map with records stored on
 // the specified file. The records have to be one per line, a mac address and an
 // IPv6 address.
@@ -113,9 +146,13 @@ func Handler6(req, resp dhcpv6.DHCPv6) (dhcpv6.DHCPv6, bool) {
 
 // Handler4 handles DHCPv4 packets for the file plugin
 func Handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
-	// TODO check the MAC address in the request
-	//      if it is present in StaticRecords, forge a response
-	//      and stop processing.
+	ipaddr, ok := StaticRecords[req.ClientHWAddr.String()]
+	if !ok {
+		log.Warningf("plugins/file: MAC address %s is unknown", req.ClientHWAddr.String())
+		return resp, false
+	}
+	resp.YourIPAddr = ipaddr
+	log.Printf("plugins/file: found IP address %s for MAC %s", ipaddr, req.ClientHWAddr.String())
 	return resp, true
 }
 
@@ -125,11 +162,13 @@ func setupFile6(args ...string) (handler.Handler6, error) {
 }
 
 func setupFile4(args ...string) (handler.Handler4, error) {
-	log.Print("plugins/file: loading `file` plugin for DHCPv4")
-	return nil, nil
+	_, h4, err := setupFile(false, args...)
+	return h4, err
 }
 
 func setupFile(v6 bool, args ...string) (handler.Handler6, handler.Handler4, error) {
+	var err error
+	var records map[string]net.IP
 	if len(args) < 1 {
 		return nil, nil, errors.New("plugins/file: need a file name")
 	}
@@ -137,12 +176,15 @@ func setupFile(v6 bool, args ...string) (handler.Handler6, handler.Handler4, err
 	if filename == "" {
 		return nil, nil, errors.New("plugins/file: got empty file name")
 	}
-	records, err := LoadDHCPv6Records(filename)
+	if v6 {
+		records, err = LoadDHCPv6Records(filename)
+	} else {
+		records, err = LoadDHCPv4Records(filename)
+	}
 	if err != nil {
-		return nil, nil, fmt.Errorf("plugins/file: failed to load DHCPv6 records: %v", err)
+		return nil, nil, fmt.Errorf("failed to load DHCPv6 records: %v", err)
 	}
-	log.Printf("plugins/file: loaded %d leases from %s", len(records), filename)
 	StaticRecords = records
-
+	log.Printf("plugins/file: loaded %d leases from %s", len(records), filename)
 	return Handler6, Handler4, nil
 }

+ 68 - 0
plugins/netmask/plugin.go

@@ -0,0 +1,68 @@
+package netmask
+
+import (
+	"encoding/binary"
+	"errors"
+	"net"
+
+	"github.com/coredhcp/coredhcp/handler"
+	"github.com/coredhcp/coredhcp/logger"
+	"github.com/coredhcp/coredhcp/plugins"
+	"github.com/insomniacslk/dhcp/dhcpv4"
+	"github.com/insomniacslk/dhcp/dhcpv6"
+)
+
+var log = logger.GetLogger()
+
+func init() {
+	plugins.RegisterPlugin("netmask", setupNetmask6, setupNetmask4)
+}
+
+var (
+	netmask net.IPMask
+)
+
+func setupNetmask6(args ...string) (handler.Handler6, error) {
+	// TODO setup function for IPv6
+	log.Warning("plugins/netmask: not implemented for IPv6")
+	return Handler6, nil
+}
+
+func setupNetmask4(args ...string) (handler.Handler4, error) {
+	log.Printf("plugins/netmask: loaded plugin for DHCPv4.")
+	if len(args) != 1 {
+		return nil, errors.New("need at least one netmask IP address")
+	}
+	netmaskIP := net.ParseIP(args[0])
+	if netmaskIP.IsUnspecified() {
+		return nil, errors.New("netmask is not valid, got: " + args[1])
+	}
+	netmaskIP = netmaskIP.To4()
+	if netmaskIP == nil {
+		return nil, errors.New("expected an netmask address, got: " + args[1])
+	}
+	netmask = net.IPv4Mask(netmaskIP[0], netmaskIP[1], netmaskIP[2], netmaskIP[3])
+	if !checkValidNetmask(netmask) {
+		return nil, errors.New("netmask is not valid, got: " + args[1])
+	}
+	log.Printf("plugins/netmask: loaded client netmask")
+	return Handler4, nil
+}
+
+// Handler6 not implemented only IPv4
+func Handler6(req, resp dhcpv6.DHCPv6) (dhcpv6.DHCPv6, bool) {
+	// TODO add IPv6 netmask to the response
+	return resp, false
+}
+
+//Handler4 handles DHCPv4 packets for the netmask plugin
+func Handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
+	resp.Options.Update(dhcpv4.OptSubnetMask(netmask))
+	return resp, false
+}
+func checkValidNetmask(netmask net.IPMask) bool {
+	netmaskInt := binary.BigEndian.Uint32(netmask)
+	x := ^netmaskInt
+	y := x + 1
+	return (y & x) == 0
+}

+ 226 - 0
plugins/range/plugin.go

@@ -0,0 +1,226 @@
+package rangeplugin
+
+import (
+	"bytes"
+	"encoding/binary"
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"math/rand"
+	"net"
+	"os"
+	"strings"
+	"time"
+
+	"github.com/coredhcp/coredhcp/handler"
+	"github.com/coredhcp/coredhcp/logger"
+	"github.com/coredhcp/coredhcp/plugins"
+	"github.com/insomniacslk/dhcp/dhcpv4"
+	"github.com/insomniacslk/dhcp/dhcpv6"
+)
+
+var log = logger.GetLogger()
+
+func init() {
+	plugins.RegisterPlugin("range", setupRange6, setupRange4)
+}
+
+//Record holds an IP lease record
+type Record struct {
+	IP      net.IP
+	expires time.Time
+}
+
+// Records holds a MAC -> IP address and lease time mapping
+var Records map[string]*Record
+
+// DHCPv6Records and DHCPv4Records are mappings between MAC addresses in
+// form of a string, to network configurations.
+var (
+	// TODO change DHCPv6Records to Record
+	DHCPv6Records map[string]*Record
+	DHCPv4Records map[string]*Record
+	LeaseTime     time.Duration
+	filename      string
+	ipRangeStart  net.IP
+	ipRangeEnd    net.IP
+)
+
+// LoadDHCPv6Records loads the DHCPv6Records global map with records stored on
+// the specified file. The records have to be one per line, a mac address and an
+// IPv6 address.
+func LoadDHCPv6Records(filename string) (map[string]*Record, error) {
+	// TODO load function for IPv6
+	return nil, errors.New("not implemented for IPv6")
+}
+
+// LoadDHCPv4Records loads the DHCPv4Records global map with records stored on
+// the specified file. The records have to be one per line, a mac address and an
+// IPv4 address.
+func LoadDHCPv4Records(filename string) (map[string]*Record, error) {
+	log.Printf("plugins/range: reading leases from %s", filename)
+	data, err := ioutil.ReadFile(filename)
+	if err != nil {
+		return nil, err
+	}
+	records := make(map[string]*Record)
+	for _, lineBytes := range bytes.Split(data, []byte{'\n'}) {
+		line := string(lineBytes)
+		if len(line) == 0 {
+			continue
+		}
+		tokens := strings.Fields(line)
+		if len(tokens) != 3 {
+			return nil, fmt.Errorf("malformed line, want 3 fields, got %d: %s", len(tokens), line)
+		}
+		hwaddr, err := net.ParseMAC(tokens[0])
+		if err != nil {
+			return nil, fmt.Errorf("malformed hardware address: %s", tokens[0])
+		}
+		ipaddr := net.ParseIP(tokens[1])
+		if ipaddr.To4() == nil {
+			return nil, fmt.Errorf("expected an IPv4 address, got: %v", ipaddr)
+		}
+		expires, err := time.Parse(time.RFC3339, tokens[2])
+		if err != nil {
+			return nil, fmt.Errorf("expected time of exipry in RFC3339 format, got: %v", tokens[2])
+		}
+		records[hwaddr.String()] = &Record{IP: ipaddr, expires: expires}
+	}
+	return records, nil
+}
+
+// Handler6 handles DHCPv6 packets for the file plugin
+func Handler6(req, resp dhcpv6.DHCPv6) (dhcpv6.DHCPv6, bool) {
+	// TODO add IPv6 netmask to the response
+	return resp, false
+}
+
+// Handler4 handles DHCPv4 packets for the range plugin
+func Handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
+	record, ok := Records[req.ClientHWAddr.String()]
+	if !ok {
+		log.Printf("plugins/file: MAC address %s is new, leasing new IP address", req.ClientHWAddr.String())
+		rec, err := createIP(ipRangeStart, ipRangeEnd)
+		if err != nil {
+			log.Error(err)
+			return nil, true
+		}
+		err = saveIPAddress(req.ClientHWAddr, rec)
+		if err != nil {
+			log.Printf("plugins/file: SaveIPAddress failed: %v", err)
+		}
+		Records[req.ClientHWAddr.String()] = rec
+		record = rec
+	}
+	resp.YourIPAddr = record.IP
+	resp.Options.Update(dhcpv4.OptIPAddressLeaseTime(LeaseTime))
+	log.Printf("plugins/file: found IP address %s for MAC %s", record.IP, req.ClientHWAddr.String())
+	return resp, false
+}
+
+func setupRange6(args ...string) (handler.Handler6, error) {
+	// TODO setup function for IPv6
+	log.Warning("plugins/range: not implemented for IPv6")
+	return Handler6, nil
+}
+
+func setupRange4(args ...string) (handler.Handler4, error) {
+	_, h4, err := setupRange(false, args...)
+	return h4, err
+}
+
+func setupRange(v6 bool, args ...string) (handler.Handler6, handler.Handler4, error) {
+	var err error
+	if len(args) < 4 {
+		return nil, nil, errors.New("need a file name, start of the IP range, end og the IP range and a lease time")
+	}
+	filename = args[0]
+	if filename == "" {
+		return nil, nil, errors.New("got empty file name")
+	}
+	ipRangeStart = net.ParseIP(args[1])
+	if ipRangeStart.To4() == nil {
+		return nil, nil, errors.New("expected an IP address, got: " + args[1])
+	}
+	ipRangeEnd = net.ParseIP(args[2])
+	if ipRangeEnd.To4() == nil {
+		return nil, nil, errors.New("expected an IP address, got: " + args[2])
+	}
+	if binary.BigEndian.Uint32(ipRangeStart.To4()) >= binary.BigEndian.Uint32(ipRangeEnd.To4()) {
+		return nil, nil, errors.New("start of IP range has to be lower than the end fo an IP range")
+	}
+	LeaseTime, err = time.ParseDuration(args[3])
+	if err != nil {
+		return Handler6, Handler4, errors.New("expected an uint32, got: " + args[3])
+	}
+	if v6 {
+		Records, err = LoadDHCPv6Records(filename)
+	} else {
+		Records, err = LoadDHCPv4Records(filename)
+	}
+	if err != nil {
+		return nil, nil, fmt.Errorf("failed to load DHCPv4 records: %v", err)
+	}
+	rand.Seed(time.Now().Unix())
+
+	log.Printf("plugins/range: loaded %d leases from %s", len(Records), filename)
+
+	return Handler6, Handler4, nil
+}
+func createIP(rangeStart net.IP, rangeEnd net.IP) (*Record, error) {
+	ip := make([]byte, 4)
+	rangeStartInt := binary.BigEndian.Uint32(rangeStart.To4())
+	rangeEndInt := binary.BigEndian.Uint32(rangeEnd.To4())
+	binary.BigEndian.PutUint32(ip, random(rangeStartInt, rangeEndInt))
+	taken := checkIfTaken(ip)
+	for taken {
+		ipInt := binary.BigEndian.Uint32(ip)
+		ipInt++
+		binary.BigEndian.PutUint32(ip, ipInt)
+		if ipInt > rangeEndInt {
+			break
+		}
+		taken = checkIfTaken(ip)
+	}
+	for taken {
+		ipInt := binary.BigEndian.Uint32(ip)
+		ipInt--
+		binary.BigEndian.PutUint32(ip, ipInt)
+		if ipInt < rangeStartInt {
+			return &Record{}, errors.New("no new IP addresses available")
+		}
+		taken = checkIfTaken(ip)
+	}
+	return &Record{IP: ip, expires: time.Now().Add(LeaseTime)}, nil
+
+}
+func random(min uint32, max uint32) uint32 {
+	return uint32(rand.Intn(int(max-min))) + min
+}
+func checkIfTaken(ip net.IP) bool {
+	taken := false
+	for _, v := range Records {
+		if v.IP.String() == ip.String() && (v.expires.After(time.Now())) {
+			taken = true
+			break
+		}
+	}
+	return taken
+}
+func saveIPAddress(mac net.HardwareAddr, record *Record) error {
+	f, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+	_, err = f.WriteString(mac.String() + " " + record.IP.String() + " " + record.expires.Format(time.RFC3339) + "\n")
+	if err != nil {
+		return err
+	}
+	err = f.Sync()
+	if err != nil {
+		return err
+	}
+	return nil
+}

+ 56 - 0
plugins/router/plugin.go

@@ -0,0 +1,56 @@
+package router
+
+import (
+	"errors"
+	"net"
+
+	"github.com/coredhcp/coredhcp/handler"
+	"github.com/coredhcp/coredhcp/logger"
+	"github.com/coredhcp/coredhcp/plugins"
+	"github.com/insomniacslk/dhcp/dhcpv4"
+	"github.com/insomniacslk/dhcp/dhcpv6"
+)
+
+var log = logger.GetLogger()
+
+func init() {
+	plugins.RegisterPlugin("router", setupRouter6, setupRouter4)
+}
+
+var (
+	routers []net.IP
+)
+
+func setupRouter6(args ...string) (handler.Handler6, error) {
+	// TODO setup function for IPv6
+	log.Warning("plugins/router: not implemented for IPv6")
+	return Handler6, nil
+}
+
+func setupRouter4(args ...string) (handler.Handler4, error) {
+	log.Printf("plugins/router: loaded plugin for DHCPv4.")
+	if len(args) < 1 {
+		return nil, errors.New("need at least one router IP address")
+	}
+	for _, arg := range args {
+		router := net.ParseIP(arg)
+		if router.To4() == nil {
+			return Handler4, errors.New("expected an router IP address, got: " + arg)
+		}
+		routers = append(routers, router)
+	}
+	log.Infof("plugins/router: loaded %d router IP addresses.", len(routers))
+	return Handler4, nil
+}
+
+// Handler6 not implemented only IPv4
+func Handler6(req, resp dhcpv6.DHCPv6) (dhcpv6.DHCPv6, bool) {
+	// TODO add router IPv6 addresses to the response
+	return resp, false
+}
+
+//Handler4 handles DHCPv4 packets for the router plugin
+func Handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
+	resp.Options.Update(dhcpv4.OptRouter(routers...))
+	return resp, false
+}

+ 1 - 0
plugins/server_id/plugin.go

@@ -58,6 +58,7 @@ func Handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
 	}
 	resp.ServerIPAddr = make(net.IP, net.IPv4len)
 	copy(resp.ServerIPAddr[:], V4ServerID)
+	resp.UpdateOption(dhcpv4.OptServerIdentifier(V4ServerID))
 	return resp, false
 }