From 1e3cb1982d8fd840fa4cedfa9d5c6d98fbc65c3a Mon Sep 17 00:00:00 2001 From: borna-blazevic <54031251+borna-blazevic@users.noreply.github.com> Date: Fri, 23 Aug 2019 10:53:34 +0200 Subject: [PATCH] IPv4 server plugin (#40) * Basic DHCPv4 server, needs more work Signed-off-by: borna_blazevic --- .gitignore | 2 + README.md | 8 +- cmds/coredhcp/config.yml.example | 6 +- coredhcp.go | 13 +- plugins/dns/plugin.go | 56 ++++++++ plugins/file/plugin.go | 62 +++++++-- plugins/netmask/plugin.go | 68 ++++++++++ plugins/range/plugin.go | 226 +++++++++++++++++++++++++++++++ plugins/router/plugin.go | 56 ++++++++ plugins/server_id/plugin.go | 1 + 10 files changed, 485 insertions(+), 13 deletions(-) create mode 100644 plugins/dns/plugin.go create mode 100644 plugins/netmask/plugin.go create mode 100644 plugins/range/plugin.go create mode 100644 plugins/router/plugin.go diff --git a/.gitignore b/.gitignore index fa708d1..b70a291 100644 --- a/.gitignore +++ b/.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 diff --git a/README.md b/README.md index 1183562..ba5b88c 100644 --- a/README.md +++ b/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). diff --git a/cmds/coredhcp/config.yml.example b/cmds/coredhcp/config.yml.example index f4fbbb9..c225a9b 100644 --- a/cmds/coredhcp/config.yml.example +++ b/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 diff --git a/coredhcp.go b/coredhcp.go index b7e6897..a8a68ed 100644 --- a/coredhcp.go +++ b/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) } diff --git a/plugins/dns/plugin.go b/plugins/dns/plugin.go new file mode 100644 index 0000000..cbc5c2f --- /dev/null +++ b/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 +} diff --git a/plugins/file/plugin.go b/plugins/file/plugin.go index fd63fb7..e0cba8e 100644 --- a/plugins/file/plugin.go +++ b/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 err != nil { - return nil, nil, fmt.Errorf("plugins/file: failed to load DHCPv6 records: %v", err) + if v6 { + records, err = LoadDHCPv6Records(filename) + } else { + records, err = LoadDHCPv4Records(filename) + } + if err != nil { + 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 } diff --git a/plugins/netmask/plugin.go b/plugins/netmask/plugin.go new file mode 100644 index 0000000..b2ba4af --- /dev/null +++ b/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 +} diff --git a/plugins/range/plugin.go b/plugins/range/plugin.go new file mode 100644 index 0000000..89663ac --- /dev/null +++ b/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 +} diff --git a/plugins/router/plugin.go b/plugins/router/plugin.go new file mode 100644 index 0000000..797f926 --- /dev/null +++ b/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 +} diff --git a/plugins/server_id/plugin.go b/plugins/server_id/plugin.go index 1abc8a1..3701912 100644 --- a/plugins/server_id/plugin.go +++ b/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 }