Last active
November 13, 2022 10:04
-
-
Save webstrand/601e4f26fe4ba7a92eb16747138860b1 to your computer and use it in GitHub Desktop.
dnsmasq dhcp-script hook
This file contains hidden or 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
| #!/usr/bin/perl | |
| use strict; | |
| use warnings; | |
| use v5.22; | |
| use feature qw(signatures); | |
| no warnings qw(experimental::signatures); | |
| # This script will, when called by dnsmasq's dhcp-script hook, create a per-host | |
| # configuration file. The default configuration will instruct dnsmasq to route | |
| # all subdomains (*.hostname.$DOMAIN) to the hosts leased ip address. | |
| # | |
| # WARNING: NOT TO BE USED ON NETWORKS WITH POTENTIALLY MALICIOUS HOSTS! HOSTNAME | |
| # COLLISIONS AND LEASE RENEWAL SPAM ARE UNHANDLED. | |
| # | |
| # To install, add: | |
| # dhcp-script=/path/to/lease-change.pl | |
| # conf-dir=/var/run/dnsmasq.leases.d,*.conf | |
| # $CONF_DIR configures where this script will install the generated per-hostname | |
| # configuration file. | |
| my $CONF_DIR = "/var/run/dnsmasq.leases.d"; | |
| # $DOMAIN configures the parent domain name that hosts are added into. This | |
| # should match the local=/inside.example.com/ directive in dnsmasq.conf | |
| my $DOMAIN = "inside.example.com"; | |
| # Number of seconds that must pass between lease changes to trigger dnsmasq | |
| # restart. This prevents constant thrashing of the dnsmasq process on busy | |
| # networks. | |
| # | |
| # Note: It's possible for clients to effectively prevent configuration changes | |
| # by renewing their lease more frequently than this timeout. | |
| my $SETTLING_TIMEOUT = 1; | |
| # Generate the name of the per-host configuration file. | |
| sub generate_conf_path($mac, $ip, $hostname) { | |
| "$CONF_DIR/$hostname.conf" | |
| } | |
| # Generate the contents of the per-host configuration file. | |
| sub generate_conf($mac, $ip, $hostname) { | |
| <<CONTENTS | |
| address=/$hostname.$DOMAIN/$ip | |
| CONTENTS | |
| } | |
| sub restart_dnsmasq() { | |
| exec qw(service dnsmasq restart); | |
| } | |
| my $CHANGE_LOCK_FILE = "/tmp/dnsmasq-lease.lock"; | |
| my $RESTART_LOCK_FILE = "/tmp/dnsmasq-restart.lock"; | |
| #### end configuration ######################################################### | |
| use Errno qw(ENOENT); | |
| use Fcntl qw(:flock O_CREAT O_RDONLY); | |
| BEGIN { $^F = 1000000 } | |
| mkdir($CONF_DIR) or die "Unable to mkdir $CONF_DIR" | |
| unless -d $CONF_DIR; | |
| # The $CHANGE_LOCK_FILE is locked in shared, preventing | |
| # the restart of dnsmasq while files are being updated. | |
| sysopen(my $lock, $CHANGE_LOCK_FILE, O_CREAT | O_RDONLY) | |
| or die "Unable to open $CHANGE_LOCK_FILE: $!"; | |
| flock($lock, LOCK_SH); | |
| # Overwrite $path with $data and return true if contents are changed. | |
| sub overwrite($path, $data) { | |
| local $/ = undef; | |
| sysopen(my $in, $path, O_CREAT | O_RDONLY) | |
| or die "Unable to open $path: $!"; | |
| flock($in, LOCK_EX) or die "Unable to lock $path: $!"; | |
| my $contents = <$in>; | |
| if($contents ne $data) { | |
| open(my $out, ">", $path) or die "Unable to open $path: $!"; | |
| print $out $data or die "Unable to write to $path"; | |
| return 1; | |
| } | |
| else { | |
| return 0; | |
| } | |
| } | |
| # Unlink $path and return true if $path existed. | |
| sub remove($path) { | |
| unlink($path) or $! == ENOENT or die "Unable to unlink $path: $!"; | |
| } | |
| # Spawn a child process to wait for configuration changes to settle and then | |
| # restart dnsmasq. | |
| sub restart($path) { | |
| say STDERR "Changed $path"; | |
| return if fork; | |
| # The $RESTART_LOCK_FILE is locked exclusive, preventing | |
| # more than one restart worker from spawning. | |
| sysopen(my $restart_lock, $RESTART_LOCK_FILE, O_CREAT | O_RDONLY) | |
| or die "Unable to open $RESTART_LOCK_FILE: $!"; | |
| exit 0 unless flock($restart_lock, LOCK_EX | LOCK_NB); | |
| # Wait for changes to leases to settle before restarting dnsmasq. | |
| sleep $SETTLING_TIMEOUT; | |
| sleep $SETTLING_TIMEOUT until flock($lock, LOCK_EX | LOCK_NB); | |
| restart_dnsmasq or die "Unable to restart dnsmasq service"; | |
| die "restart_dnsmasq() returned true, but did not exec"; | |
| # We must exec here, otherwise we enter a race condition with restarted | |
| # dnsmasq's dhcp-script that gets invoked during startup while recovering | |
| # saved leases. | |
| } | |
| sub main($action, $mac, $ip, $hostname = "") { | |
| die "Invalid action: $action" unless $action; | |
| die "Invalid mac: $mac" unless $mac; | |
| die "Invalid ip: $ip" unless $ip; | |
| return say STDERR "Ignoring unknown hostname for $ip" unless($hostname); | |
| my $path = generate_conf_path($mac, $ip, $hostname) or return; | |
| if($action eq "add" or $action eq "old") { | |
| return restart($path) if overwrite($path, generate_conf($mac, $ip, $hostname)); | |
| say STDERR "Not writing $path: up-to-date"; | |
| } | |
| elsif($action eq "del") { | |
| return restart($path) if remove($path); | |
| say STDERR "Not removing $path: up-to-date"; | |
| } | |
| else { | |
| die "Unexpected action: $action"; | |
| } | |
| } | |
| main(@ARGV); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment