-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
f-93: Adds custom leases plugin with excluded ips
Signed-off-by: Aleix Ramírez <aramirez@opennebula.io>
- Loading branch information
Showing
8 changed files
with
403 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
180 changes: 180 additions & 0 deletions
180
appliances/VRouter/DHCP4v2/dhcpcore-onelease/plugins/onerange/plugin.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
95 changes: 95 additions & 0 deletions
95
appliances/VRouter/DHCP4v2/dhcpcore-onelease/plugins/onerange/storage.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.