dhcp.js is a dependency-free DHCPv4 client, server, and traffic watcher for Node.js. Version 1.0.0 provides native ECMAScript modules, CommonJS compatibility, and TypeScript declarations from the same package. It can allocate addresses, request leases, inspect DHCP traffic, serve multiple subnets through relay agents, and work with standard or custom DHCP options.
How DHCP Assigns an Address
A client initially has no usable IPv4 configuration and cannot assume that ordinary unicast routing works. DHCP therefore begins with broadcast messages over UDP. The usual four-message exchange is commonly abbreviated as DORA:
| Step | Message | Purpose |
|---|---|---|
| 1 | DHCPDISCOVER | The client searches for DHCP servers. |
| 2 | DHCPOFFER | A server proposes an address and configuration. |
| 3 | DHCPREQUEST | The client requests one offered address and identifies the selected server. |
| 4 | DHCPACK | The server confirms the lease and its options. |
Servers listen on UDP port 67 and clients on UDP port 68. Options carry the subnet mask, routers, DNS servers, lease duration, host name, boot information, vendor data, and many other values. DHCP is connectionless: a lease is protocol state maintained across datagrams, not a persistent client/server connection.
Use an isolated network while experimenting. A second DHCP server on a production LAN can give clients the wrong gateway or DNS configuration and interrupt connectivity. The process also needs permission to bind privileged UDP ports and an interface and firewall configuration that permit IPv4 broadcast traffic.
Installation
dhcp.js 1.0.0 requires Node.js 20 or newer and has no runtime dependencies:
npm install dhcp
The package name remains dhcp; dhcp.js is the project name. The main export works with both ESM and CommonJS:
import { createServer } from 'dhcp'; const dhcp = require('dhcp');
const server = dhcp.createServer({
range: ['192.168.3.10', '192.168.3.99'],
}); Create a DHCP Server
This server manages an inclusive dynamic range, reserves one fixed address, and sends the network configuration required by ordinary clients:
import { createServer } from 'dhcp';
const server = createServer({
range: ['192.168.3.10', '192.168.3.99'],
server: '192.168.3.1',
router: ['192.168.3.1'],
dns: ['1.1.1.1', '8.8.8.8'],
netmask: '255.255.255.0',
leaseTime: 86400,
static: {
'11:22:33:44:55:66': '192.168.3.100',
},
});
server.on('bound', (leases) => {
console.log('Active leases:', leases);
});
server.on('poolExhausted', (error, request) => {
console.error(error.message, request.chaddr);
});
server.on('error', (error) => {
console.error(error);
});
server.listen(); The server address and every static assignment are excluded from dynamic allocation even when they lie inside the configured range. Existing leases are reused, requested addresses are honored when available, expired leases are reclaimed, and an exhausted pool emits poolExhausted without producing an invalid reply.
Packet-Aware Options
Every registered option with a configuration name can be a fixed value or a callback. This makes DHCP policy part of the application instead of a separate configuration language:
const server = createServer({
range: ['192.168.3.10', '192.168.3.99'],
server: '192.168.3.1',
bootFile(packet) {
return packet?.clientId === 'sensor'
? 'sensor.bin'
: 'default.bin';
},
forceOptions: ['bootFile'],
}); Dynamic static-assignment callbacks receive the decoded request as well. If such a callback can return addresses that cannot be inferred in advance, list every possible address in staticReservations; otherwise the dynamic allocator cannot reserve them before the first matching packet arrives.
Address Conflict Detection
RFC 2131 recommends checking a newly selected address before sending an offer. The optional system-assisted probe triggers neighbor discovery and inspects the platform's ARP cache:
const server = createServer({
range: ['192.168.3.10', '192.168.3.99'],
server: '192.168.3.1',
addressProbe: true,
addressProbeTimeout: 250,
addressConflictHoldTime: 600,
});
server.on('addressConflict', (address, request, subnetId) => {
console.warn(`${address} is already in use on ${subnetId}`);
});
server.on('addressProbeInconclusive', (address) => {
console.warn(`Could not verify ${address}`);
}); Node.js has no portable raw-ARP API. If the operating-system tools or neighbor cache are unavailable, the built-in probe is deliberately fail-open and emits addressProbeInconclusive. Networks that require a strict guarantee should inject a native AddressProbe implementation.
Create a DHCP Client
The client performs discovery, tracks the selected lease, renews at T1, rebinds at T2, and reports expiry. An explicit MAC address is useful in a controlled test:
import { createClient } from 'dhcp';
const client = createClient({
mac: '12:34:56:78:90:AB',
clientId: 'asset-123',
vendorClassId: 'lab-client',
features: ['hostname', 'domainName', 'broadcast'],
});
client.on('bound', (lease) => {
console.log('Lease:', lease);
});
client.on('addressChanged', (previousAddress, currentAddress) => {
console.log(`${previousAddress} -> ${currentAddress}`);
});
client.on('error', (error) => {
console.error(error);
});
client.listen(() => client.sendDiscover()); The library intentionally does not change the host's interface, routing table, DNS resolver, or host name. The bound event exposes the negotiated state; applying it to the operating system remains the application's responsibility. Set autoRenew: false to control renewal manually with sendRenew() and sendRebind(), or call sendRelease() to release a bound lease.
Watch DHCP Traffic
A passive broadcast handler decodes local DHCP traffic without allocating addresses. It is useful for diagnostics, inventory feeds, and detecting unexpected offers:
import {
createBroadcastHandler,
DHCPDISCOVER,
} from 'dhcp';
const watcher = createBroadcastHandler({
logLevel: 'silent',
});
watcher.on('message', (packet) => {
if (packet.options[53] === DHCPDISCOVER) {
console.log({
mac: packet.chaddr,
hostname: packet.options[12],
vendorClass: packet.options[60],
});
}
});
watcher.listen(); Limited broadcasts are received portably only while binding to 0.0.0.0. Also, a MAC address is not a durable identity or proof of physical presence: clients can randomize or spoof it, and lease traffic can be delayed or absent. Treat the watcher as telemetry, not authentication or a safety-critical occupancy sensor.
Multiple Subnets and Relay Agents
DHCP broadcasts do not cross routers. A relay agent forwards requests from another subnet and records its gateway address in giaddr. dhcp.js can maintain independent pools with subnets and selects among them using an explicit match policy, subnet-selection option 118, giaddr, the requested address, or ciaddr.
const server = createServer({
subnets: [
{
id: 'office',
range: ['10.10.0.10', '10.10.0.99'],
server: '10.10.0.1',
router: ['10.10.0.1'],
netmask: '255.255.255.0',
},
{
id: 'lab',
range: ['10.20.0.10', '10.20.0.99'],
server: '10.20.0.1',
router: ['10.20.0.1'],
netmask: '255.255.255.0',
},
],
}); Relay Agent Information option 82 is preserved byte-for-byte in replies. Circuit ID and Remote ID can drive declarative relayBindings, including a static-only policy in which unknown clients receive no offer. A packet that cannot identify one of several pools is rejected instead of being assigned from an arbitrary subnet.
Custom Options and PXE
Unknown option codes remain available as Uint8Array values rather than being discarded. Applications can register immutable, instance-local option definitions when the wire format is known. DHCP options 93, 94, and 97 use the binary formats defined for PXE clients, while vendor-specific option 43 can be handled with the exported TLV helpers. This keeps custom hardware policy local to one server or client instead of modifying a global registry.
Authentication and Network Security
Classic DHCP does not establish that a server or client is trustworthy. Version 1.0.0 can validate RFC 3118 option 90 at the server boundary, including replay protection and Delayed Authentication with per-client keys. That RFC fixes the algorithm to legacy HMAC-MD5, and the bundled client does not currently originate authenticated exchanges. Use the feature only for compatible managed clients and combine it with switch-level DHCP snooping, restricted network segments, and protected key storage.
The server also supports RFC 3203 FORCERENEW, RFC 6842 client-identifier echoing, ACK-only options, maximum-message negotiation, and typed or opaque private options. These features improve interoperability; they do not turn DHCP into a general access-control protocol.
Command-Line Tools
A global installation provides dhcpd for a server and dhcp for a client. The server is quiet during normal operation; -v prints readable transitions and -vv adds structured debug logs:
npm install --global dhcp
sudo dhcpd \
--range 192.168.3.10-192.168.3.99 \
--server 192.168.3.1 \
--router 192.168.3.1 \
--dns 1.1.1.1 8.8.8.8 \
--verbose Run privileged network services with the narrowest permissions available on the host. A dedicated service account, container, network namespace, or explicit bind capability is preferable to leaving an application under an unrestricted root account.
References
- [RFC2131]R. Droms, Dynamic Host Configuration Protocol, RFC 2131, 1997.
- [RFC2132]S. Alexander and R. Droms, DHCP Options and BOOTP Vendor Extensions, RFC 2132, 1997.
- [RFC3046]M. Patrick, DHCP Relay Agent Information Option, RFC 3046, 2001.
- [RFC3118]R. Droms and W. Arbaugh, Authentication for DHCP Messages, RFC 3118, 2001.
- [RFC6842]T. Lemon and Q. Wu, Client Identifier Option in DHCP Server Replies, RFC 6842, 2013.