GitHub - ytmknd/PSNanoDHCPServ: This script doesn't work yet. · GitHub
Skip to content

About

This script doesn't work yet.

Resources

Stars

0 stars

Watchers

1 watching

Forks

Latest commit

 

History

77 Commits

Folders and files

Repository files navigation

PSNanoDHCPServ

PSNanoDHCPServ is a single-file DHCP server, still small enough to read start to finish, that has grown from a minimal "Rescue Tool" into a fairly full-featured server: multiple DNS/router addresses, an extensible custom-option mechanism covering most of the standard DHCP option space, PXE/network boot, multi-scope JSON configuration, and basic operations tooling (log file, status snapshot, graceful Ctrl+C shutdown, packet statistics).

Description

This server hands out network settings to PCs on a network that has no other DHCP server. It is not fully compliant with RFC2131, but implements most of what a small deployment needs.

It can be run temporarily (its original "rescue tool" use case) or left running as a small persistent DHCP server for a lab/home network with lease persistence, static reservations and multiple scopes.

It answers DHCPDISCOVER with DHCPOFFER, valid DHCPREQUEST/DHCPINFORM messages with DHCPACK, and invalid address requests with DHCPNAK. Addresses are leased from the configured range, reused for the same client while the lease is active, and released on DHCPRELEASE or after the configured lease time expires.

Core RFC 2131 behaviors are implemented:

  • Clients are tracked by their client identifier (option 61) when present, falling back to the hardware MAC.
  • A DHCPOFFER only reserves the address for a short window (-offerTimeoutSeconds); the lease is committed when the DHCPREQUEST arrives.
  • DHCPREQUEST is classified into its SELECTING / INIT-REBOOT / RENEWING-REBINDING client states, each with the reply rules from RFC 2131 §4.3.2 (including staying silent for unknown INIT-REBOOT clients and freeing the offer when the client selects another server).
  • Replies follow the §4.1 addressing rules: back through the relay agent (giaddr) when present, unicast to ciaddr for renewals/informs, broadcast otherwise.
  • Reply options honor the parameter request list (option 55) and its order, and include renewal times T1/T2 (options 58/59). Packets are sized to their content (minimum 300 bytes, capped by the client's option 57).

Usage

sudo pwsh ./psnanodhcpserv.ps1 -clientIPAddresses 192.168.1.1-192.168.1.5 \
    -subnetMask 255.255.255.0 -defaultGatewayAddress 192.168.1.254 -dnsServerAddress 8.8.8.8
  • You must run as administrator (or root) because it binds to UDP port 67.
  • Currently supports IPv4 addresses only.
  • -clientIPAddresses accepts a single address (a.b.c.d) or an inclusive range (a.b.c.d-a.b.c.e).
  • -defaultGatewayAddress and -dnsServerAddress each accept a single address or a comma-separated list (192.168.1.1,192.168.1.2) to hand out multiple routers or DNS servers (options 3/6). When a list is given for the gateway, the first address is used as the DHCP server identifier (option 54) unless -serverIdentifier is set explicitly.
  • Use -noreplymode to simply listen to DHCP traffic on your network without replying.

Optional parameters

Parameter Default Description
-domainName localdomain DHCP option 15 (Domain Name).
-serverIdentifier first gateway DHCP option 54. Defaults to the first -defaultGatewayAddress entry.
-netbiosNameServer (none) DHCP option 44. Omitted when not specified.
-leaseTimeSeconds 86400 DHCP option 51 (lease time), in seconds.
-offerTimeoutSeconds 60 How long a DHCPOFFER reserves an address.
-leaseFile (none) Persist leases to this JSON file and restore them on startup.
-staticLeases (none) Static reservations, each mac-or-clientid=a.b.c.d (MAC may use : or -).
-excludeAddresses (none) Addresses never handed out: a.b.c.d or a.b.c.d-a.b.c.e (repeatable).
-conflictDetection off Ping an address before offering it; quarantine ones that answer.
-customOptions (none) Extra DHCP options, each code=type:value. See Custom options below.
-nextServer (none) PXE next-server address (BOOTP siaddr). See PXE / network boot.
-bootFile (none) PXE boot file name (BOOTP file field + option 67).
-tftpServerName (none) PXE TFTP server name (option 66).
-configFile (none) JSON file describing one or more scopes; overrides all scope-related CLI parameters above. See Config file / multiple scopes.
-logFile (none) Append plain-text INFO/WARN/ERROR log lines here in addition to the console.
-statusFile (none) Write a JSON snapshot of leases/offers/stats to this path after every processed packet.

Lease management

  • With -leaseFile, the lease database (leases and declined addresses) is written as JSON after every change and restored on startup. Entries that are expired or no longer fit the configuration are dropped during restore.
  • Static reservations always win over dynamic allocation, may live outside the dynamic range, and their addresses are never handed to other clients.
  • Conflict detection probes only fresh allocations (not existing leases, offers, or static reservations); an address that answers the ping is quarantined for one lease time, like a declined address.

Custom options

-customOptions sends additional DHCP options (any code 1-254) that aren't covered by a named parameter. Each entry has the form code=type:value; the option is always included in replies, whether or not the client's parameter request list (option 55) asked for it. Supported types:

Type Value format Example
ip, ip-list one address, or comma-separated addresses 42=ip-list:10.0.0.5,10.0.0.6 (NTP servers)
uint16 unsigned 16-bit integer 26=uint16:1500 (interface MTU)
uint32 unsigned 32-bit integer
int32 signed 32-bit integer 2=int32:-32400 (time offset, seconds)
byte single byte (0-255) 46=byte:8 (WINS/NetBIOS node type)
string ASCII text 252=string:http://wpad/wpad.dat
hex raw bytes as a hex string 43=hex:0601... (vendor-specific info)
domain-list one domain, or comma-separated domains (RFC 3397, uncompressed labels) 119=domain-list:corp.example.com,example.com
classless-routes one route, or comma-separated network/prefix=gateway (RFC 3442) 121=classless-routes:10.0.1.0/24=10.0.0.1

The legacy static-route option (33) doesn't need a dedicated type: its wire format is just alternating destination/gateway address pairs, which ip-list already produces, e.g. 33=ip-list:10.0.1.0,10.0.0.1.

New-DhcpOption -Code <n> -Value <v> -Type <type> (used internally to encode -customOptions) is also how any option code without a named parameter — e.g. broadcast address (28) — can be sent: -customOptions @('28=ip:192.168.1.255').

PXE / network boot

-nextServer, -bootFile, and -tftpServerName configure PXE/network boot:

  • -nextServer sets the BOOTP siaddr field (the "next server" clients should contact, typically the TFTP server's own address).
  • -bootFile sets the BOOTP file field (max 127 bytes) and is sent as option 67 (Bootfile Name) for clients that look at the option instead.
  • -tftpServerName is sent as option 66 (TFTP Server Name).

None of these three are written into a DHCPNAK.

Config file / multiple scopes

For more than one subnet (or just to avoid a long command line), pass -configFile path/to/config.json instead of the scope-related CLI parameters. Every top-level key except scopes is a default applied to all scopes; a key with the same name inside a scope object overrides that default. Key names match the CLI parameter names (clientIPAddresses, subnetMask, defaultGatewayAddress, dnsServerAddress, domainName, serverIdentifier, netbiosNameServer, leaseTimeSeconds, offerTimeoutSeconds, leaseFile, staticLeases, excludeAddresses, conflictDetection, customOptions, nextServer, bootFile, tftpServerName):

{
  "leaseFile": "leases.json",
  "conflictDetection": true,
  "scopes": [
    {
      "clientIPAddresses": "192.168.1.100-192.168.1.199",
      "subnetMask": "255.255.255.0",
      "defaultGatewayAddress": "192.168.1.1",
      "dnsServerAddress": "8.8.8.8"
    },
    {
      "clientIPAddresses": "10.0.0.50-10.0.0.200",
      "subnetMask": "255.255.255.0",
      "defaultGatewayAddress": "10.0.0.1",
      "dnsServerAddress": "1.1.1.1",
      "domainName": "b.lan"
    }
  ]
}
  • scopes must contain at least one entry.
  • The server binds one UDP socket and dispatches each incoming packet to the matching scope: a relayed packet (non-zero giaddr) is matched to the scope whose subnet contains the relay address; a packet with no giaddr (a client on the server's own segment) always goes to the first scope. A relayed packet that matches no scope's subnet is dropped. With only one scope configured (whether via -configFile or the plain CLI parameters), this matching is skipped entirely and every packet goes to that one scope, preserving the original single-scope behavior regardless of relay topology.
  • A leaseFile inherited from the top-level default gets a scope number inserted automatically so scopes never share one file (leases.json becomes leases.1.json, leases.2.json, ...). A scope that sets its own leaseFile explicitly is used exactly as given, with no suffix.

Operations

  • -logFile appends every console line to a plain-text file as <UTC timestamp> [INFO|WARN|ERROR] <message>, in addition to printing to the console as before.
  • -statusFile writes a JSON snapshot (uptime, packet/message-type counters, and a per-scope summary of active leases/offers/declines) after every processed packet, for an external monitor to read.
  • Ctrl+C requests a graceful shutdown: the server stops accepting new packets, saves every scope's lease file and a final status snapshot, logs a stats summary, and closes the socket before exiting.

Requirement

PowerShell 7+ (pwsh). Available on macOS, Linux and Windows.

Building a Windows executable (PS2EXE)

For distributing to a Windows machine as a single .exe (no need for the target PC to already have pwsh, and double-clicking can prompt for the admin elevation the server needs), compile the script with PS2EXE.

This step must be run on Windows. PS2EXE compiles the script with .NET's CodeDom/CSharpCodeProvider, which throws PlatformNotSupportedException ("Operation is not supported on this platform") on macOS and Linux, even under PowerShell 7 — confirmed while writing these instructions. The module installs fine on any OS, but only Windows can actually produce the .exe.

# On Windows, in either Windows PowerShell 5.1 or PowerShell 7:
Install-Module -Name ps2exe -Scope CurrentUser -Force

Invoke-ps2exe -inputFile .\psnanodhcpserv.ps1 -outputFile .\psnanodhcpserv.exe `
    -title 'PSNanoDHCPServ' -description 'Single-file DHCP server' `
    -version '1.0.0.0' -requireAdmin
  • -requireAdmin embeds a manifest that triggers the UAC elevation prompt when the .exe is launched, since binding UDP/67 needs administrator rights (the same requirement as running the .ps1 with sudo/an elevated shell).
  • Omit -noConsole — the server logs every DISCOVER/OFFER/REQUEST/ACK to the console (and optionally -logFile), so keep the console window PS2EXE creates by default.
  • The parameters documented above (-clientIPAddresses, -customOptions, -configFile, -logFile, etc.) all still work; call the .exe the same way you'd call the .ps1, e.g. .\psnanodhcpserv.exe -clientIPAddresses 192.168.1.1-192.168.1.5 -subnetMask 255.255.255.0 -defaultGatewayAddress 192.168.1.254 -dnsServerAddress 8.8.8.8.
  • Re-run Invoke-ps2exe after every change to psnanodhcpserv.ps1 — the .exe is a point-in-time compiled snapshot, not kept in sync automatically.

Tests

Tests use Pester 5.

Install-Module Pester -MinimumVersion 5.0.0 -Scope CurrentUser -Force
Invoke-Pester ./tests -Output Detailed

The test suite dot-sources the script (which only loads its functions, without starting the server) and verifies the IP/range parsing, lease allocation, DHCP option encoding (including the generic -Type encoder, custom options, and PXE fields), full DISCOVER→OFFER / REQUEST→ACK packet round-trips, and the config-file/multi-scope loading and selection logic.

Start-DhcpServer itself (the socket bind, Ctrl+C handling, and receive loop) is a thin I/O wrapper with no automated coverage — verify it by actually running the server after making changes there.

License

MIT

About

This script doesn't work yet.

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages