Skip to content

Commit 0915a63

Browse files
committed
Add support for custom IPs based on client mac address
1 parent 7314c9b commit 0915a63

File tree

5 files changed

+117
-7
lines changed

5 files changed

+117
-7
lines changed

README.md

+6-1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ pools:
3333
routers: [ 172.17.0.1 ]
3434
dns: [ 1.1.1.1, 8.8.8.8 ]
3535

36+
# Optional static IPs by mac address
37+
hosts:
38+
- ip: 172.17.0.5
39+
hw: 0:1c:42:b4:6e:1d
40+
3641
interfaces: [ eth1 ]
3742
leasedir: /var/lib/godhcpd
3843
```
@@ -80,10 +85,10 @@ root@ubuntu2:~#
8085
- Bare minimum wire protocol for DHCPDISCOVER, DHCPOFFER, DHCPREQUEST, DHCPNAK, DHCPACK, and DHCPRELEASE to work
8186
- Supports relayed requests
8287
- Supports multiple IP Pools, sourced from configuration
88+
- Supports hosts in config with hardcoded IPs, based on mac address
8389

8490
## TODO
8591

8692
- Support acting as a relay
8793
- Support arbitrary options
88-
- Support hosts in config with hardcoded IPs, based on mac address
8994
- More Tests

conf.go

+23-3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"time"
1010
)
1111

12+
// Pool conf object
1213
type PoolConf struct {
1314
Name string `yaml:"name"`
1415
MyIp string `yaml:"myip"`
@@ -24,6 +25,10 @@ type PoolConf struct {
2425
Dns []string `yaml:"dns"`
2526

2627
LeaseTime uint32 `yaml:"leasetime"`
28+
29+
// TODO: add arbitrary options aside from just router/dns
30+
31+
ReservedHosts []HostConf `yaml:"hosts"`
2732
}
2833

2934
func (pc PoolConf) ToPool() (*Pool, error) {
@@ -53,15 +58,30 @@ func (pc PoolConf) ToPool() (*Pool, error) {
5358
pool.Dns = append(pool.Dns, net.ParseIP(ip))
5459
}
5560

61+
for _, host := range pc.ReservedHosts {
62+
if err := pool.AddReservedHost(host.ToHost()); err != nil {
63+
return nil, err
64+
}
65+
}
66+
5667
return pool, nil
5768
}
5869

5970
type HostConf struct {
60-
Ip string `yaml:"ip"`
61-
Mac string `yaml:"hw"`
62-
MacParsed MacAddress
71+
IP string `yaml:"ip"`
72+
Mac string `yaml:"hw"`
73+
Hostname string `yaml:"hostname"`
74+
// TODO: add custom options scoped to host
75+
}
76+
77+
func (hc *HostConf) ToHost() *ReservedHost {
78+
return &ReservedHost{
79+
Mac: StrToMac(hc.Mac),
80+
IP: IpToFixedV4(net.ParseIP(hc.IP)),
81+
}
6382
}
6483

84+
// Root yaml conf
6585
type Conf struct {
6686
Pools []PoolConf `yaml:"pools"`
6787
Leasedir string `yaml:"leasedir"`

conf.yaml

+5
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ pools:
99
routers: [ 172.17.0.1 ]
1010
dns: [ 1.1.1.1, 8.8.8.8 ]
1111

12+
# Optional static IPs by mac address
13+
hosts:
14+
- ip: 172.17.0.5
15+
hw: 0:1c:42:b4:6e:1d
16+
1217
# Example pool acting as a relay, using a separate
1318
# net on eth2
1419
- name: other network

pool.go

+47-3
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ func (l *Lease) Expired() bool {
2424
return time.Now().After(l.Expiration)
2525
}
2626

27+
type ReservedHost struct {
28+
Mac MacAddress
29+
Hostname string
30+
IP FixedV4
31+
}
32+
2733
type Pool struct {
2834
Name string
2935
Network net.IP
@@ -37,19 +43,32 @@ type Pool struct {
3743
LeaseTime time.Duration
3844
Persistence Persistence
3945

46+
// Internal lease database
4047
leasesByMac map[MacAddress]*Lease
4148
leaseByIp map[FixedV4]*Lease
42-
m sync.RWMutex
49+
50+
// Internal database of fixed mac addresses to IPs for hosts,
51+
// sourced from configuration
52+
reservedByMac map[MacAddress]*ReservedHost
53+
reservedByIp map[FixedV4]*ReservedHost
54+
55+
m sync.RWMutex
4356
}
4457

4558
func NewPool() *Pool {
4659
p := &Pool{}
4760
p.clearLeases()
61+
p.clearReservedHosts()
4862
return p
4963
}
5064

5165
// Hacky, terrible, naive impl. I want an ordered int set!
52-
func (p *Pool) getFreeIp() (FixedV4, error) {
66+
func (p *Pool) getFreeIp(mac MacAddress) (FixedV4, error) {
67+
68+
// If there is a reserved IP for this mac address, use that
69+
if host, ok := p.reservedByMac[mac]; ok {
70+
return host.IP, nil
71+
}
5372

5473
// Try to find the next free IP within our range, while keeping
5574
// track of the first expired lease we found, in case we have no
@@ -60,6 +79,10 @@ func (p *Pool) getFreeIp() (FixedV4, error) {
6079
var foundExpired *Lease = nil
6180

6281
for ipLong := start; ipLong <= end; ipLong++ {
82+
// Skip over any IPs in our range which are reserved
83+
if _, ok := p.reservedByIp[ipLong]; ok {
84+
continue
85+
}
6386
if lease, ok := p.leaseByIp[ipLong]; !ok {
6487
return ipLong, nil
6588
} else {
@@ -94,6 +117,27 @@ func (p *Pool) deleteLease(lease *Lease) {
94117
delete(p.leaseByIp, lease.IP)
95118
}
96119

120+
func (p *Pool) clearReservedHosts() {
121+
p.reservedByMac = map[MacAddress]*ReservedHost{}
122+
p.reservedByIp = map[FixedV4]*ReservedHost{}
123+
}
124+
125+
func (p *Pool) insertReservedHost(host *ReservedHost) {
126+
p.reservedByMac[host.Mac] = host
127+
p.reservedByIp[host.IP] = host
128+
}
129+
130+
func (p *Pool) AddReservedHost(host *ReservedHost) error {
131+
if _, ok := p.reservedByIp[host.IP]; ok {
132+
return errors.New("Reserved hosts with duplicate IPs")
133+
}
134+
if _, ok := p.reservedByMac[host.Mac]; ok {
135+
return errors.New("Reserved hosts with duplicate mac addresses")
136+
}
137+
p.insertReservedHost(host)
138+
return nil
139+
}
140+
97141
func (p *Pool) TouchLeaseByMac(mac MacAddress) (*Lease, bool) {
98142
p.m.Lock()
99143
defer p.m.Unlock()
@@ -110,7 +154,7 @@ func (p *Pool) GetNextLease(mac MacAddress, hostname string) (*Lease, error) {
110154
p.m.Lock()
111155
defer p.m.Unlock()
112156

113-
ip, err := p.getFreeIp()
157+
ip, err := p.getFreeIp(mac)
114158
if err != nil {
115159
return nil, err
116160
}

pool_test.go

+36
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,39 @@ func TestIpAllocation(t *testing.T) {
5757
require.Equal(t, "host3", lease3.Hostname)
5858
require.Equal(t, IpToFixedV4(net.ParseIP("172.0.0.10")), lease3.IP)
5959
}
60+
61+
// Test IP allocation with reserved mac addresses
62+
func TestIpReservedAllocation(t *testing.T) {
63+
pool := NewPool()
64+
pool.Start = net.ParseIP("172.0.0.10")
65+
pool.End = net.ParseIP("172.0.0.12")
66+
pool.Netmask = net.ParseIP("255.255.255.0")
67+
pool.LeaseTime = time.Duration(1) * time.Hour
68+
69+
mac1 := MacAddress{0, 0, 0, 0, 0, 1}
70+
mac2 := MacAddress{0, 0, 0, 0, 0, 2}
71+
72+
// Bind mac2 to 172.0.0.10. Deliberately choose an IP in our range to
73+
// verify that overlaps are ignored
74+
err := pool.AddReservedHost(&ReservedHost{
75+
Mac: mac2,
76+
IP: IpToFixedV4(net.ParseIP("172.0.0.10")),
77+
})
78+
require.Nil(t, err)
79+
80+
// Verify initial IP lease acquisition chooses the IP after the reserved
81+
lease1, err := pool.GetNextLease(mac1, "host1")
82+
require.Nil(t, err)
83+
require.Equal(t, IpToFixedV4(net.ParseIP("172.0.0.11")), lease1.IP)
84+
require.Equal(t, mac1, lease1.Mac)
85+
require.Equal(t, "host1", lease1.Hostname)
86+
require.False(t, lease1.Expired())
87+
88+
// Verify custom allocation works
89+
lease2, err := pool.GetNextLease(mac2, "host2")
90+
require.Nil(t, err)
91+
require.Equal(t, IpToFixedV4(net.ParseIP("172.0.0.10")), lease2.IP)
92+
require.Equal(t, mac2, lease2.Mac)
93+
require.Equal(t, "host2", lease2.Hostname)
94+
require.False(t, lease2.Expired())
95+
}

0 commit comments

Comments
 (0)