-
-
Notifications
You must be signed in to change notification settings - Fork 128
Expand file tree
/
Copy pathDHCPServer.inc
More file actions
555 lines (509 loc) · 24.2 KB
/
DHCPServer.inc
File metadata and controls
555 lines (509 loc) · 24.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
<?php
namespace RESTAPI\Models;
require_once 'RESTAPI/autoloader.inc';
use RESTAPI;
use RESTAPI\Core\Model;
use RESTAPI\Core\ModelSet;
use RESTAPI\Dispatchers\DHCPServerApplyDispatcher;
use RESTAPI\Fields\BooleanField;
use RESTAPI\Fields\IntegerField;
use RESTAPI\Fields\InterfaceField;
use RESTAPI\Fields\NestedModelField;
use RESTAPI\Fields\StringField;
use RESTAPI\Responses\ConflictError;
use RESTAPI\Responses\ValidationError;
use RESTAPI\Validators\IPAddressValidator;
use RESTAPI\Validators\MACAddressValidator;
/**
* Defines a Model that interacts with the DHCP server for a given interface.
*/
class DHCPServer extends Model {
public InterfaceField $interface;
public BooleanField $enable;
public StringField $range_from;
public StringField $range_to;
public StringField $domain;
public StringField $failover_peerip;
public StringField $mac_allow;
public StringField $mac_deny;
public StringField $domainsearchlist;
public IntegerField $defaultleasetime;
public IntegerField $maxleasetime;
public StringField $gateway;
public StringField $dnsserver;
public StringField $winsserver;
public StringField $ntpserver;
public BooleanField $staticarp;
public BooleanField $ignorebootp;
public BooleanField $ignoreclientuids;
public BooleanField $nonak;
public BooleanField $disablepingcheck;
public BooleanField $dhcpleaseinlocaltime;
public BooleanField $statsgraph;
public StringField $denyunknown;
public NestedModelField $pool;
public NestedModelField $numberoptions;
public NestedModelField $staticmap;
public function __construct(mixed $id = null, mixed $parent_id = null, mixed $data = [], mixed ...$options) {
# Define Model attributes
$this->config_path = 'dhcpd';
$this->id_type = 'string';
$this->many = true;
$this->subsystem = 'dhcpd';
$this->update_strategy = 'merge';
$this->verbose_name = 'DHCP Server';
# Define Model Fields
$this->interface = new InterfaceField(
required: true,
representation_only: true,
help_text: 'The interface to configure the DHCP server for. This field is only necessary when you want' .
'to change the interface (ID) of an existing DHCP server, or you are replacing all DHCP server objects ' .
'with a new configuration. Note that specifying an interface in this field will update the ID of the ' .
'DHCP server to match the interface specified here. Leaving this field empty will retain the ' .
'existing interface.',
);
$this->enable = new BooleanField(default: false, help_text: 'Enable the DHCP server for this interface.');
$this->range_from = new StringField(
default: '',
allow_empty: true,
maximum_length: 15,
internal_name: 'from',
internal_namespace: 'range',
validators: [new IPAddressValidator(allow_ipv4: true, allow_ipv6: false)],
help_text: 'The starting IP address for the primary DHCP pool. This address must be less than or equal ' .
'to the `range_to` field.',
);
$this->range_to = new StringField(
default: '',
allow_empty: true,
maximum_length: 15,
internal_name: 'to',
internal_namespace: 'range',
validators: [new IPAddressValidator(allow_ipv4: true, allow_ipv6: false)],
help_text: 'The ending IP address for the primary DHCP pool. This address must be greater than or equal ' .
'to the `range_to` field.',
);
$this->domain = new StringField(
default: '',
allow_empty: true,
maximum_length: 255,
help_text: 'The domain to be assigned via DHCP.',
);
$this->failover_peerip = new StringField(
default: '',
allow_empty: true,
maximum_length: 255,
validators: [new IPAddressValidator(allow_ipv4: true, allow_ipv6: true, allow_fqdn: true)],
help_text: 'The interface IP address of the other firewall (failover peer) in this subnet. Leave ' .
'empty to disable failover peering.',
);
$this->mac_allow = new StringField(
default: [],
allow_empty: true,
many: true,
maximum_length: 17,
validators: [new MACAddressValidator()],
help_text: 'MAC addresses this DHCP server is allowed to provide leases for.',
);
$this->mac_deny = new StringField(
default: [],
allow_empty: true,
many: true,
maximum_length: 17,
validators: [new MACAddressValidator()],
help_text: 'MAC addresses this DHCP server is not allowed to provide leases for.',
);
$this->domainsearchlist = new StringField(
default: [],
allow_empty: true,
many: true,
maximum_length: 255,
delimiter: ';',
validators: [new IPAddressValidator(allow_ipv4: false, allow_ipv6: false, allow_fqdn: true)],
help_text: 'The domain search list to provide via DHCP.',
);
$this->defaultleasetime = new IntegerField(
default: 7200,
allow_null: true,
minimum: 60,
help_text: 'The default DHCP lease validity period (in seconds). This is used for clients that do not ask ' .
'for a specific expiration time.',
);
$this->maxleasetime = new IntegerField(
default: 86400,
allow_null: true,
minimum: 60,
help_text: 'The maximum DHCP lease validity period (in seconds) a client can request.',
);
$this->gateway = new StringField(
default: '',
allow_empty: true,
maximum_length: 15,
validators: [new IPAddressValidator(allow_ipv4: true, allow_ipv6: false, allow_keywords: ['none'])],
help_text: 'The gateway IPv4 address to provide via DHCP. This is only necessary if you are not using ' .
"the interface's IP as the gateway. Specify `none` for no gateway assignment.",
);
$this->dnsserver = new StringField(
default: [],
allow_empty: true,
many: true,
many_maximum: 4,
maximum_length: 15,
delimiter: null,
validators: [new IPAddressValidator(allow_ipv4: true, allow_ipv6: false)],
help_text: 'The DNS servers to provide via DHCP. Leave empty to default to system nameservers.',
);
$this->winsserver = new StringField(
default: [],
allow_empty: true,
many: true,
many_maximum: 2,
maximum_length: 15,
delimiter: null,
validators: [new IPAddressValidator(allow_ipv4: true, allow_ipv6: false)],
help_text: 'The WINS servers to provide via DHCP.',
);
$this->ntpserver = new StringField(
default: [],
allow_empty: true,
many: true,
many_maximum: 4,
maximum_length: 256,
delimiter: null,
validators: [new IPAddressValidator(allow_ipv4: true, allow_ipv6: false, allow_fqdn: true)],
help_text: 'The NTP servers to provide via DHCP.',
);
$this->staticarp = new BooleanField(
default: false,
help_text: 'Assign static ARP entries for DHCP leases provided by this server.',
);
$this->ignorebootp = new BooleanField(
default: false,
help_text: 'Force this DHCP server to ignore BOOTP queries.',
);
$this->ignoreclientuids = new BooleanField(
default: false,
help_text: 'Prevent recording a unique identifier (UID) in client lease data if present in the client ' .
'DHCP request. This option may be useful when a client can dual boot using different client ' .
'identifiers but the same hardware (MAC) address. Note that the resulting server behavior violates ' .
'the official DHCP specification.',
);
$this->nonak = new BooleanField(
default: false,
help_text: 'Ignore denied clients rather than reject. This option is not compatible with failover and ' .
'cannot be enabled when a Failover Peer IP address is configured.',
);
$this->disablepingcheck = new BooleanField(
default: false,
indicates_true: 'yes',
help_text: 'Prevent the DHCP server from sending a ping to the address being assigned, where if no ' .
'response has been heard, it assigns the address.',
);
$this->dhcpleaseinlocaltime = new BooleanField(
default: false,
indicates_true: 'yes',
help_text: 'Display the DHCP lease times in local time instead of UTC.',
);
$this->statsgraph = new BooleanField(
default: false,
indicates_true: 'yes',
help_text: 'Enable adding DHCP lease statistics to the pfSense Monitoring graphs.',
);
$this->denyunknown = new StringField(
default: null,
choices: ['enabled', 'class'],
allow_empty: true,
allow_null: true,
help_text: 'Define how to handle unknown clients requesting DHCP leases. When set to `null`, any DHCP ' .
'client will get an IP address within this scope/range on this interface. If set to `enabled`, ' .
'any DHCP client with a MAC address listed in a static mapping on any scope(s)/interface(s) will ' .
'get an IP address. If set to `class`, only MAC addresses listed in static mappings on this interface ' .
'will get an IP address within this scope/range.',
);
$this->pool = new NestedModelField(
model_class: 'DHCPServerAddressPool',
default: [],
allow_empty: true,
help_text: 'Additional address pools applied to this DHCP server.',
);
$this->numberoptions = new NestedModelField(
model_class: 'DHCPServerCustomOption',
default: [],
allow_empty: true,
help_text: 'The custom DHCP options to apply to this DHCP server.',
);
$this->staticmap = new NestedModelField(
model_class: 'DHCPServerStaticMapping',
default: [],
allow_empty: true,
help_text: 'Static mappings applied to this DHCP server.',
);
# Ensure all interfaces have DHCP server objects initialized
$this->init_interfaces();
parent::__construct($id, $parent_id, $data, ...$options);
}
/**
* Initializes configuration objects for defined interface that have not yet configured the DHCP server
*/
private function init_interfaces(): void {
# Variables
$ifs_using_dhcp_server = array_keys($this->get_config(path: $this->config_path, default: []));
# Loop through each defined interface
foreach ($this->get_config('interfaces', []) as $if_id => $if) {
# Skip this interface if it is not a static interface or the subnet value is greater than or equal to 31
if (empty($if['ipaddr']) or !is_ipaddrv4($if['ipaddr']) or $if->subnet->value >= 31) {
continue;
}
# Otherwise, make this interface eligible for a DHCP server
if (!in_array($if_id, $ifs_using_dhcp_server)) {
$this->set_config(path: "$this->config_path/$if_id", value: ['range' => ['from' => '', 'to' => '']]);
}
}
}
/**
* Obtains the internal `interface` field value since it is not stored in the config.
* @return string The internal interface ID of the DHCPServer.
*/
public function from_internal_interface(): string {
return $this->interface->_from_internal($this->id);
}
/**
* Provides extra validation for this entire Model object.
*/
public function validate_extra(): void {
# Do not allow this DHCPServers primary address pool to overlap with other objects
$overlapped_model = $this->get_range_overlap($this->range_from->value, $this->range_to->value);
if ($overlapped_model) {
throw new ConflictError(
message: "This DHCP server's primary address pool overlaps with $overlapped_model->verbose_name " .
"object with ID `$overlapped_model->id`. Adjust your `range_from` and `range_to` values.",
response_id: 'DHCP_SERVER_PRIMARY_POOL_OVERLAPS_EXISTING_OBJECT',
);
}
}
/**
* Provides extra validation for the `enable` field.
* @param bool $enable The incoming `enable` field value to validate.
* @return bool The validated `enable` field value to be assigned.
* @throws ValidationError When `enable` is `true` but interface associated with `id` does not have a static IP.
* @throws ValidationError When `enable` is `true`, but there is a DHCP relay running on this interface.
*/
public function validate_enable(bool $enable): bool {
# Get the interface associated with this DHCP server
$interface = NetworkInterface::query(['id' => $this->id])->first();
# Do not allow the DHCP server to be enabled if the interface does not have a static IP address assigned
if ($enable and $interface->typev4->value !== 'static') {
throw new ValidationError(
message: "DHCP server cannot be enabled because interface `{$this->id}` does not have a static IPv4 " .
'address assigned.',
response_id: 'DHCP_SERVER_CANNOT_ENABLE_WITHOUT_STATIC_IPV4',
);
}
# Do not allow the DHCP server to be enabled if any interface is running a DHCP relay
$dhcp_relay = new DHCPRelay();
if ($dhcp_relay->enable->value) {
throw new ValidationError(
message: 'DHCP server cannot be enabled while the DHCP Relay is enabled.',
response_id: 'DHCP_SERVER_CANNOT_BE_ENABLED_WITH_DHCP_RELAY',
);
}
return $enable;
}
/**
* Provides extra validation for the `range_from` field.
* @param string $range_from The incoming `range_from` field value to validate.
* @return string The validated `range_from` field value to be assigned.
* @throws ValidationError When `range_from` is the interface's network address.
* @throws ValidationError When `range_from` is not in the interface's subnet.
* @throws ValidationError When `range_from` is greater than the `range_to` IP.
*/
public function validate_range_from(string $range_from): string {
# Get the parent interface for this DHCP server
$interface = NetworkInterface::query(['id' => $this->id])->first();
# Do not allow this IP to be the network address
if ($interface->get_network_ipv4() == $range_from) {
throw new ValidationError(
message: "DHCP server `range_from` cannot be the interface subnet's network address.",
response_id: 'DHCP_SERVER_RANGE_FROM_CANNOT_BE_NETWORK_ADDRESS',
);
}
# Do not allow the `range_from` to be greater `range_to`
if (ip_greater_than($range_from, $this->range_to->value)) {
throw new ValidationError(
message: 'DHCP server `range_from` cannot be greater than `range_to`.',
response_id: 'DHCP_SERVER_RANGE_FROM_CANNOT_BE_GREATER_THAN_RANGE_TO',
);
}
# Do not allow the `range_from` IP to be outside the interface's configured subnet
if (!$interface->is_ipv4_in_cidr($range_from)) {
throw new ValidationError(
message: "DHCP server `range_from` must lie within the interface's subnet.",
response_id: 'DHCP_SERVER_RANGE_FROM_OUTSIDE_OF_SUBNET',
);
}
return $range_from;
}
/**
* Provides extra validation for the `range_from` field.
* @param string $range_to The incoming `range_from` field value to validate.
* @return string The validated `range_from` field value to be assigned.
* @throws ValidationError When `range_from` is the interface's network address.
* @throws ValidationError When `range_from` is not in the interface's subnet.
* @throws ValidationError When `range_from` is greater than the `range_to` IP.
*/
public function validate_range_to(string $range_to): string {
# Get the parent interface for this DHCP server
$interface = NetworkInterface::query(['id' => $this->id])->first();
# Do not allow this IP to be the network address
if ($interface->get_broadcast_ipv4() === $range_to) {
throw new ValidationError(
message: "DHCP server `range_to` cannot be the interface subnet's broadcast address.",
response_id: 'DHCP_SERVER_RANGE_FROM_CANNOT_BE_BROADCAST_ADDRESS',
);
}
# Do not allow the `range_to` IP to be outside the interface's configured subnet
if (!$interface->is_ipv4_in_cidr($range_to)) {
throw new ValidationError(
message: "DHCP server `range_to` must lie within the interface's subnet.",
response_id: 'DHCP_SERVER_RANGE_TO_OUTSIDE_OF_SUBNET',
);
}
return $range_to;
}
/**
* Provides extra validation for the `maxleasetime` field.
* @param int $maxleasetime The incoming `maxleasetime` field value to validate.
* @return int The validated `maxleasetime` field value to be assigned.
* @throws ValidationError When `maxleasetime` is less than the `defaultleasetime`
*/
public function validate_maxleasetime(int $maxleasetime): int {
if ($maxleasetime < $this->defaultleasetime->value) {
throw new ValidationError(
message: 'DHCP server `maxleasetime` cannot be less than the `defaultleasetime`',
response_id: 'DHCP_SERVER_MAX_LEASE_TIME_LESS_THAN_DEFAULT',
);
}
return $maxleasetime;
}
/**
* Provides extra validation for the `gateway` field.
* @param string $gateway The incoming `gateway` field value to validate.
* @return string The validated `gateway` field value to be assigned.
* @throws ValidationError When `gateway` is not an IP within the interface's subnet
*/
public function validate_gateway(string $gateway): string {
# Get the parent interface for this DHCP server
$interface = NetworkInterface::query(['id' => $this->id])->first();
# Do not allow non-empty gateway values that are not within the interface's subnet
if ($gateway and !$interface->is_ipv4_in_cidr($gateway)) {
throw new ValidationError(
message: "DHCP server `gateway` must be within interface's subnet.",
response_id: 'DHCP_SERVER_GATEWAY_NOT_WITHIN_SUBNET',
);
}
return $gateway;
}
/**
* Provides extra validation for the `nonak` field.
* @param bool $nonak The incoming `nonak` field value to validate.
* @return bool The validated `nonak` field value to be assigned.
* @throws ValidationError When `nonak` is `true` and a `failover_peerip` is assigned.
*/
public function validate_nonak(bool $nonak): bool {
# Do not allow `nonak` to be enabled if a `failover_peerip` is set.
if ($nonak and $this->failover_peerip->value) {
throw new ValidationError(
'DHCP server `nonak` cannot be enabled while a `failover_peerip` is assigned.',
response_id: 'DHCP_SERVER_NONAK_WITH_FAILOVER_PEERIP_NOT_ALLOWED',
);
}
return $nonak;
}
/**
* Provides extra validation for the `staticarp` field.
* @param bool $staticarp The incoming `staticarp` field value to validate.
* @return bool The validated `staticarp` field value to be assigned.
* @throws ValidationError When `staticarp` is `true` but there are configured static mappings without IPs.
*/
public function validate_staticarp(bool $staticarp): bool {
# Do not allow `staticarp` to be enabled if there are any configured static mappings without IPs
$static_mappings = DHCPServerStaticMapping::read_all(parent_id: $this->id);
foreach ($static_mappings->model_objects as $static_mapping) {
if ($staticarp and !$static_mapping->ipaddr->value) {
throw new ValidationError(
'DHCP server `staticarp` cannot be enabled while there are DHCP server static mappings ' .
'configured without an assigned IP.',
response_id: 'DHCP_SERVER_STATICARP_WITH_NO_IP_STATIC_MAPPINGS',
);
}
}
return $staticarp;
}
/**
* Checks if a given address range overlaps with reserved IPs such as static mappings, pools and virtual IPs.
* @param string $range_from The start address of the range.
* @param string $range_to The end address of the range.
* @return Model|null Returns the Model object that this range overlaps with, returns
* null if the range did not overlap with an existing Model object.
*/
private function get_range_overlap(string $range_from, string $range_to): ?Model {
# Ensure range does not overlap with existing static mappings
$static_mappings = DHCPServerStaticMapping::read_all(parent_id: $this->id);
foreach ($static_mappings->model_objects as $static_mapping) {
if (is_inrange_v4($static_mapping->ipaddr->value, $range_from, $range_to)) {
return $static_mapping;
}
}
# Ensure range does not overlap with existing address pools `range_from` or `range_to` addresses
$pools = DHCPServerAddressPool::read_all(parent_id: $this->id);
foreach ($pools->model_objects as $pool) {
if (is_inrange_v4($pool->range_from->value, $range_from, $range_to)) {
return $pool;
}
if (is_inrange_v4($pool->range_to->value, $range_from, $range_to)) {
return $pool;
}
}
# Ensure range does not overlap with existing virtual IPs
$vips = VirtualIP::read_all();
foreach ($vips->model_objects as $vip) {
if (is_inrange_v4($vip->subnet->value, $range_from, $range_to)) {
return $vip;
}
}
return null;
}
/**
* Obtains the next available ID for a new DHCP server object.
* @return string The interface ID associated with `interface` field value.
* @note This is not the conventional use case for this method, but it is necessary for the DHCPServer::replace_all()
* method to associate DHCP server objects with the requested interface.
*/
public function get_next_id(): string {
return $this->interface->_to_internal($this->interface->value) ?: 'unknown';
}
/**
* Applies DHCP server changes after replacement of all existing DHCP server objects.
*/
public function apply_replace_all(ModelSet $initial_objects, ModelSet $new_objects): void {
# Always configure RRD after replacing all DHCP server objects
enable_rrd_graphing();
# Always reset the dhcpd db files after replacing all DHCP server objects
mwexec('/bin/rm -rf /var/dhcpd/var/db/*');
(new DHCPServerApplyDispatcher(async: $this->async))->spawn_process();
}
/**
* Applies the pending DHCP server changes.
*/
public function apply(): void {
# Enable RRD graphing for DHCP if `statsgraph` changed
if ($this->initial_object->statsgraph->value !== $this->statsgraph->value) {
enable_rrd_graphing();
}
# Remove the existing dhcpd db files if the `failover_peerip` changed
if ($this->initial_object->failover_peerip->value !== $this->failover_peerip->value) {
mwexec('/bin/rm -rf /var/dhcpd/var/db/*');
}
(new DHCPServerApplyDispatcher(async: $this->async))->spawn_process();
}
}