Skip to content

Commit

Permalink
f-93: Adds custom leases plugin with excluded ips
Browse files Browse the repository at this point in the history
Signed-off-by: Aleix Ramírez <aramirez@opennebula.io>
  • Loading branch information
aleixrm committed Nov 26, 2024
1 parent 92f34d6 commit 1e8ec97
Show file tree
Hide file tree
Showing 8 changed files with 403 additions and 11 deletions.
9 changes: 5 additions & 4 deletions appliances/VRouter/DHCP4v2/dhcpcore-onelease/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ dhcpcore-onelease

# Ignore client binary
client/dhcpcore_client
client/client

# Ignore the leases.txt file
leases.txt
# Ignore the leases file
*.sqlite3

# Ignore the default config file
config.yml
# Ignore config files
onelease-config.yml
9 changes: 6 additions & 3 deletions appliances/VRouter/DHCP4v2/dhcpcore-onelease/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,37 @@ go 1.20
require (
github.com/coredhcp/coredhcp v0.0.0-20240908184240-576af8676ffa
github.com/insomniacslk/dhcp v0.0.0-20240829085014-a3a4c1f04475
github.com/mattn/go-sqlite3 v1.14.22
github.com/sirupsen/logrus v1.9.3
github.com/spf13/pflag v1.0.6-0.20201009195203-85dd5c8bc61c
github.com/spf13/viper v1.19.0
github.com/stretchr/testify v1.9.0
gopkg.in/yaml.v3 v3.0.1
)

require (
github.com/bits-and-blooms/bitset v1.14.2 // indirect
github.com/chappjc/logrus-prefix v0.0.0-20180227015900-3a1d64819adb // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/google/gopacket v1.1.19 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/josharian/native v1.1.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/nxadm/tail v1.4.11 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/viper v1.19.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 // indirect
go.uber.org/multierr v1.11.0 // indirect
Expand All @@ -42,5 +46,4 @@ require (
golang.org/x/term v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
2 changes: 2 additions & 0 deletions appliances/VRouter/DHCP4v2/dhcpcore-onelease/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ github.com/coredhcp/coredhcp v0.0.0-20240908184240-576af8676ffa/go.mod h1:grzl9x
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
Expand Down Expand Up @@ -49,6 +50,7 @@ github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 h1:mZHayPoR0lNmnHyvtYjDeq0zlVHn9K/ZXoy17ylucdo=
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5/go.mod h1:GEXHk5HgEKCvEIIrSpFI3ozzG5xOKA2DVlEX/gGnewM=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
Expand Down
7 changes: 5 additions & 2 deletions appliances/VRouter/DHCP4v2/dhcpcore-onelease/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ import (
"sync"

oneleaseconfig "github.com/OpenNebula/one-apps/appliances/VRouterd/DHCP4v2/dhcpcore-onelease/pkg/config"

"github.com/coredhcp/coredhcp/logger"
"github.com/coredhcp/coredhcp/server"

dhcpcoreconfig "github.com/coredhcp/coredhcp/config"

pl_onelease "github.com/OpenNebula/one-apps/appliances/VRouterd/DHCP4v2/dhcpcore-onelease/plugins/onelease"
pl_onerange "github.com/OpenNebula/one-apps/appliances/VRouterd/DHCP4v2/dhcpcore-onelease/plugins/onerange"

"github.com/coredhcp/coredhcp/plugins"
pl_autoconfigure "github.com/coredhcp/coredhcp/plugins/autoconfigure"
pl_dns "github.com/coredhcp/coredhcp/plugins/dns"
Expand All @@ -24,7 +27,6 @@ import (
pl_nbp "github.com/coredhcp/coredhcp/plugins/nbp"
pl_netmask "github.com/coredhcp/coredhcp/plugins/netmask"
pl_prefix "github.com/coredhcp/coredhcp/plugins/prefix"
pl_range "github.com/coredhcp/coredhcp/plugins/range"
pl_router "github.com/coredhcp/coredhcp/plugins/router"
pl_searchdomains "github.com/coredhcp/coredhcp/plugins/searchdomains"
pl_serverid "github.com/coredhcp/coredhcp/plugins/serverid"
Expand Down Expand Up @@ -70,8 +72,9 @@ var desiredPlugins = []*plugins.Plugin{
&pl_nbp.Plugin,
&pl_netmask.Plugin,
&pl_onelease.Plugin,
&pl_onerange.Plugin,
&pl_prefix.Plugin,
&pl_range.Plugin,
//&pl_range.Plugin,
&pl_router.Plugin,
&pl_searchdomains.Plugin,
&pl_serverid.Plugin,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.

// Original code: https://github.com/coredhcp/coredhcp/tree/576af8676ffaff9c85800fae235f614cb65410bd/plugins/range
// Adapted by OpenNebula Systems for the VRouter appliance
// Copyright 2024-present OpenNebula Systems

package onerange

import (
"database/sql"
"encoding/binary"
"errors"
"fmt"
"net"
"strings"
"sync"
"time"

"github.com/coredhcp/coredhcp/handler"
"github.com/coredhcp/coredhcp/logger"
"github.com/coredhcp/coredhcp/plugins"
"github.com/coredhcp/coredhcp/plugins/allocators"
"github.com/coredhcp/coredhcp/plugins/allocators/bitmap"
"github.com/insomniacslk/dhcp/dhcpv4"
)

var log = logger.GetLogger("plugins/onerange")

// Plugin wraps plugin registration information
var Plugin = plugins.Plugin{
Name: "onerange",
Setup4: setupRange,
}

// Record holds an IP lease record
type Record struct {
IP net.IP
expires int
hostname string
}

// PluginState is the data held by an instance of the range plugin
type PluginState struct {
// Rough lock for the whole plugin, we'll get better performance once we use leasestorage
sync.Mutex
// Recordsv4 holds a MAC -> IP address and lease time mapping
Recordsv4 map[string]*Record
LeaseTime time.Duration
excludedIPs []net.IP
leasedb *sql.DB
allocator allocators.Allocator
}

// Handler4 handles DHCPv4 packets for the range plugin
func (p *PluginState) Handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
p.Lock()
defer p.Unlock()
record, ok := p.Recordsv4[req.ClientHWAddr.String()]
hostname := req.HostName()
if !ok {
// Allocating new address since there isn't one allocated
log.Printf("MAC address %s is new, leasing new IPv4 address", req.ClientHWAddr.String())
ip, err := p.allocator.Allocate(net.IPNet{})
if err != nil {
log.Errorf("Could not allocate IP for MAC %s: %v", req.ClientHWAddr.String(), err)
return nil, true
}
rec := Record{
IP: ip.IP.To4(),
expires: int(time.Now().Add(p.LeaseTime).Unix()),
hostname: hostname,
}
err = p.saveIPAddress(req.ClientHWAddr, &rec)
if err != nil {
log.Errorf("SaveIPAddress for MAC %s failed: %v", req.ClientHWAddr.String(), err)
}
p.Recordsv4[req.ClientHWAddr.String()] = &rec
record = &rec
} else {
// Ensure we extend the existing lease at least past when the one we're giving expires
expiry := time.Unix(int64(record.expires), 0)
if expiry.Before(time.Now().Add(p.LeaseTime)) {
record.expires = int(time.Now().Add(p.LeaseTime).Round(time.Second).Unix())
record.hostname = hostname
err := p.saveIPAddress(req.ClientHWAddr, record)
if err != nil {
log.Errorf("Could not persist lease for MAC %s: %v", req.ClientHWAddr.String(), err)
}
}
}
resp.YourIPAddr = record.IP
resp.Options.Update(dhcpv4.OptIPAddressLeaseTime(p.LeaseTime.Round(time.Second)))
log.Printf("found IP address %s for MAC %s", record.IP, req.ClientHWAddr.String())
return resp, false
}

func setupRange(args ...string) (handler.Handler4, error) {
var (
err error
p PluginState
)

if len(args) < 4 {
return nil, fmt.Errorf("invalid number of arguments, want: 4 (file name, start IP, end IP, lease time), got: %d", len(args))
}
filename := args[0]
if filename == "" {
return nil, errors.New("file name cannot be empty")
}
ipRangeStart := net.ParseIP(args[1])
if ipRangeStart.To4() == nil {
return nil, fmt.Errorf("invalid IPv4 address: %v", args[1])
}
ipRangeEnd := net.ParseIP(args[2])
if ipRangeEnd.To4() == nil {
return nil, fmt.Errorf("invalid IPv4 address: %v", args[2])
}
if binary.BigEndian.Uint32(ipRangeStart.To4()) >= binary.BigEndian.Uint32(ipRangeEnd.To4()) {
return nil, errors.New("start of IP range has to be lower than the end of an IP range")
}

p.allocator, err = bitmap.NewIPv4Allocator(ipRangeStart, ipRangeEnd)
if err != nil {
return nil, fmt.Errorf("could not create an allocator: %w", err)
}

p.LeaseTime, err = time.ParseDuration(args[3])
if err != nil {
return nil, fmt.Errorf("invalid lease duration: %v", args[3])
}

// parse a list of excluded IPs as fifth argument
if len(args) > 4 {
excludedIPs := args[4]
for _, ip := range strings.Split(excludedIPs, ",") {
excluded := net.ParseIP(strings.TrimSpace(ip))
if excluded.To4() == nil {
return nil, fmt.Errorf("invalid excluded IP address: %v", ip)
}
p.excludedIPs = append(p.excludedIPs, excluded)
}
}

// check that the excluded IPs belongs to the range and pre-allocate them
// preallocated IPs will not be stored in the lease database, but they will be kept at the allocator level
// who is the responsible of managing the IP availability
for _, excluded := range p.excludedIPs {
if binary.BigEndian.Uint32(excluded.To4()) < binary.BigEndian.Uint32(ipRangeStart.To4()) ||
binary.BigEndian.Uint32(excluded.To4()) > binary.BigEndian.Uint32(ipRangeEnd.To4()) {
return nil, fmt.Errorf("excluded IP %v is not in the range %v-%v", excluded, ipRangeStart, ipRangeEnd)
}
if _, err := p.allocator.Allocate(net.IPNet{IP: excluded}); err != nil {
return nil, fmt.Errorf("could not pre-allocate excluded IP %v: %w", excluded, err)
}
}

if err := p.registerBackingDB(filename); err != nil {
return nil, fmt.Errorf("could not setup lease storage: %w", err)
}
p.Recordsv4, err = loadRecords(p.leasedb)
if err != nil {
return nil, fmt.Errorf("could not load records from file: %v", err)
}

log.Printf("Loaded %d DHCPv4 leases from %s", len(p.Recordsv4), filename)

for _, v := range p.Recordsv4 {
ip, err := p.allocator.Allocate(net.IPNet{IP: v.IP})
if err != nil {
return nil, fmt.Errorf("failed to re-allocate leased ip %v: %v", v.IP.String(), err)
}
if ip.IP.String() != v.IP.String() {
return nil, fmt.Errorf("allocator did not re-allocate requested leased ip %v: %v", v.IP.String(), ip.String())
}
}

return p.Handler4, nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Copyright 2018-present the CoreDHCP Authors. All rights reserved
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.

// Original code: https://github.com/coredhcp/coredhcp/tree/576af8676ffaff9c85800fae235f614cb65410bd/plugins/range
// Adapted by OpenNebula Systems for the VRouter appliance
// Copyright 2024-present OpenNebula Systems

package onerange

import (
"database/sql"
"errors"
"fmt"
"net"

_ "github.com/mattn/go-sqlite3"
)

func loadDB(path string) (*sql.DB, error) {
db, err := sql.Open("sqlite3", fmt.Sprintf("file:%s", path))
if err != nil {
return nil, fmt.Errorf("failed to open database (%T): %w", err, err)
}
if _, err := db.Exec("create table if not exists leases4 (mac string not null, ip string not null, expiry int, hostname string not null, primary key (mac, ip))"); err != nil {
return nil, fmt.Errorf("table creation failed: %w", err)
}
return db, nil
}

// loadRecords loads the DHCPv6/v4 Records global map with records stored on
// the specified file. The records have to be one per line, a mac address and an
// IP address.
func loadRecords(db *sql.DB) (map[string]*Record, error) {
rows, err := db.Query("select mac, ip, expiry, hostname from leases4")
if err != nil {
return nil, fmt.Errorf("failed to query leases database: %w", err)
}
defer rows.Close()
var (
mac, ip, hostname string
expiry int
records = make(map[string]*Record)
)
for rows.Next() {
if err := rows.Scan(&mac, &ip, &expiry, &hostname); err != nil {
return nil, fmt.Errorf("failed to scan row: %w", err)
}
hwaddr, err := net.ParseMAC(mac)
if err != nil {
return nil, fmt.Errorf("malformed hardware address: %s", mac)
}
ipaddr := net.ParseIP(ip)
if ipaddr.To4() == nil {
return nil, fmt.Errorf("expected an IPv4 address, got: %v", ipaddr)
}
records[hwaddr.String()] = &Record{IP: ipaddr, expires: expiry, hostname: hostname}
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("failed lease database row scanning: %w", err)
}
return records, nil
}

// saveIPAddress writes out a lease to storage
func (p *PluginState) saveIPAddress(mac net.HardwareAddr, record *Record) error {
stmt, err := p.leasedb.Prepare(`insert or replace into leases4(mac, ip, expiry, hostname) values (?, ?, ?, ?)`)
if err != nil {
return fmt.Errorf("statement preparation failed: %w", err)
}
defer stmt.Close()
if _, err := stmt.Exec(
mac.String(),
record.IP.String(),
record.expires,
record.hostname,
); err != nil {
return fmt.Errorf("record insert/update failed: %w", err)
}
return nil
}

// registerBackingDB installs a database connection string as the backing store for leases
func (p *PluginState) registerBackingDB(filename string) error {
if p.leasedb != nil {
return errors.New("cannot swap out a lease database while running")
}
// We never close this, but that's ok because plugins are never stopped/unregistered
newLeaseDB, err := loadDB(filename)
if err != nil {
return fmt.Errorf("failed to open lease database %s: %w", filename, err)
}
p.leasedb = newLeaseDB
return nil
}
Loading

0 comments on commit 1e8ec97

Please sign in to comment.