#!/usr/bin/perl # # Set PXELINUX hard-coded options # use Socket; # For gethostbyname use Fcntl; use bytes; %option_names = ( 6 => 'domain-name-servers', 15 => 'domain-name', 54 => 'next-server', 209 => 'config-file', 210 => 'path-prefix', 211 => 'reboottime' ); @fmt_oneip = ("ip-address", \&parse_oneip, \&show_ip); @fmt_multiip = ("ip-address-list", \&parse_multiip, \&show_ip); @fmt_string = ("string", \&parse_string, \&show_string); @fmt_uint32 = ("uint32", \&parse_uint32, \&show_uint32); %option_format = ( 6 => \@fmt_multiip, 15 => \@fmt_string, 54 => \@fmt_oneip, 67 => \@fmt_string, 209 => \@fmt_string, 210 => \@fmt_string, 211 => \@fmt_uint32 ); sub parse_oneip($) { my($s) = @_; my($name,$aliases,$addrtype,$length,@addrs) = gethostbyname($s); return ($addrtype == AF_INET) ? $addrs[0] : undef; } sub parse_multiip($) { my($l) = @_; my $s; my @a = (); my $addr; my $d = ''; foreach $s (split(/,/, $l)) { my($name,$aliases,$addrtype,$length,@addrs) = gethostbyname($s); if ($addrtype == AF_INET) { foreach $addr (@addrs) { $d .= $addr; } } } return $d ne '' ? $d : undef; } sub show_ip($) { my($l) = @_; if (length($l) & 3) { return undef; } else { my @h = (); my $i; for ($i = 0; $i < length($l); $i += 4) { push(@h, inet_ntoa(substr($l, $i, 4))); } return join(',', @h); } } sub parse_string($) { return $_[0]; } sub show_string($) { my($s) = @_; my $o, $i, $c; $o = "\'"; for ($i = 0; $i < length($s); $i++) { $c = substr($s, $i, 1); if ($c eq "\'" || $c eq '!') { $o .= "\'\\$c\'"; } else { $o .= $c; } } $o .= "\'"; return $o; } sub parse_uint32($) { my($s) = @_; if ($s =~ /^[0-9]+$/) { return pack("N", $s); } else { return undef; } } sub show_uint32($) { my($l) = @_; if (length($l) == 4) { return unpack("N", $l); } else { return undef; } } sub parse_generic($) { my($s) = @_; if ($s =~ /^[0-9a-f]{1,2}(:[0-9a-f]{1,2})*$/) { my $h; my @b = (); foreach $h (split(/\:/, $s)) { push(@b, hex $h); } return pack("C", @b); } else { return undef; } } sub show_generic($) { my($l) = @_; my $i; my @h; for ($i = 0; $i < length($l); $i++) { push(@h, sprintf("%02x", unpack("C", substr($l, $i, $1)))); } return join(':', @h); } sub parse_option($$) { my($opt, $arg) = @_; my $v; if (defined($option_format{$opt})) { $v = $option_format{$opt}[1]($arg); return $v if (defined($v)); } return parse_generic($arg); } sub show_option($$) { my($opt, $arg) = @_; my $v; if (defined($option_format{$opt})) { $v = $option_format{$opt}[2]($arg); return $v if (defined($v)); } return show_generic($arg); } sub option_number($) { my($n) = @_; if (defined($option_rnames{$n})) { return $option_rnames{$n}; } elsif ($n =~ /^[0-9]+$/ && $n >= 1 && $n <= 254) { return $n+0; } else { return undef; } } sub read_optsets($) { my($file) = @_; my $data, $bdata, $adata; my $patch_start = (stat($file))[7]; my $hdroffset = 0; # 0 means non-deep-embedded my $bufsize = 0; my $junk; my %hdr; return undef unless (seek($file, 0, SEEK_SET)); return undef unless (read($file, $data, 48) == 48); my($mzmagic, $junk, $magic, $len, $flags, $boff, $blen, $aoff, $alen) = unpack("va[6]VVVVVVV", $data); if ($mzmagic == 0x5a4d) { # It is an EFI file... search for the magic number $hdroffset = 48; my $magic = pack("VVVV", 0x2a171ead, 0x0600e65e, 0x4025a4e4, 0x42388fc8); while (1) { return undef unless (read($file, $data, 16) == 16); last if ($data eq $magic); $hdroffset += 16; } return undef unless (read($file, $data, 16) == 16); ($blen, $alen, $bufsize, $junk) = unpack("VVVV", $data); $patch_start = $boff = $hdroffset + 32; $aoff = $boff + $blen; $hdr{'deep'} = 1; $hdr{'bufsize'} = $bufsize; $hdr{'hdroffset'} = $hdroffset; } else { # It is a BIOS PXE file return undef if ($magic != 0x2983c8ac); return undef if ($len < 7*4); $hdr{'deep'} = 0; } if ($blen == 0) { $bdata = ''; } else { return undef unless (seek($file, $boff, SEEK_SET)); return undef unless (read($file, $bdata, $blen) == $blen); $patch_start = $boff if ($boff < patch_start); } if ($alen == 0) { $adata = ''; } else { return undef unless (seek($file, $aoff, SEEK_SET)); return undef unless (read($file, $adata, $alen) == $alen); $patch_start = $aoff if ($aoff < $patch_start); } $hdr{'patch_start'} = $patch_start; return (\%hdr, $bdata, $adata); } sub write_optsets($$@) { my($file, $hdr, $bdata, $adata) = @_; my $boff = 0; my $aoff = 0; my $bufsize = 0; my $patch_start = $hdr->{'patch_start'}; my $len; $bdata .= "\xff" unless ($bdata eq ''); $adata .= "\xff" unless ($adata eq ''); $len = length($bdata) + length($adata); if (defined($hdr->{'bufsize'})) { return undef unless ($len <= $hdr->{'bufsize'}); } return undef unless (seek($file, $patch_start, SEEK_SET)); if (length($bdata)) { $boff = $patch_start; return undef unless (print $file $bdata); $patch_start += length($bdata); } if (length($adata)) { $aoff = $patch_start; return undef unless (print $file $adata); $patch_start += length($adata); } if ($hdr->{'deep'}) { return undef unless (print $file "\0" x ($hdr->{'bufsize'} - $len)); return undef unless (seek($file, $hdr->{'hdroffset'} + 16, SEEK_SET)); my $hdr = pack("VV", length($bdata), length($adata)); return undef unless (print $file $hdr); } else { my $hdr = pack("VVVV", $boff, length($bdata), $aoff, length($adata)); return undef unless (seek($file, 8+3*4, SEEK_SET)); return undef unless (print $file $hdr); truncate($file, $patch_start); } return 1; } sub delete_option($$) { my ($num, $block) = @_; my $o, $l, $c, $x; $x = 0; while ($x < length($block)) { ($o, $l) = unpack("CC", substr($block, $x, 2)); if ($o == $num) { # Delete this option substr($block, $x, $l+2) = ''; } elsif ($o == 0) { # Delete a null option substr($block, $x, 1) = ''; } elsif ($o == 255) { # End marker - truncate block $block = substr($block, 0, $x); last; } else { # Skip to the next option $x += $l+2; } } return $block; } sub add_option($$$) { my ($num, $data, $block) = @_; $block = delete_option($num, $block); if (length($data) == 0) { return $block; } elsif (length($data) > 255) { die "$0: option $num has too much data (max 255 bytes)\n"; } else { return $block . pack("CC", $num, length($data)) . $data; } } sub list_options($$) { my($pfx, $data) = @_; my $x, $o, $l; while ($x < length($data)) { ($o, $l) = unpack("CC", substr($data, $x, 2)); if ($o == 0) { $x++; } elsif ($o == 255) { last; } else { my $odata = substr($data, $x+2, $l); last if (length($odata) != $l); # Incomplete option printf "%s%-20s %s\n", $pfx, $option_names{$o} || sprintf("%d", $o), show_option($o, $odata); $x += $l+2; } } } sub usage() { my $i; print STDERR "Usage: $0 options pxelinux.0\n"; print STDERR "Options:\n"; print STDERR "--before option value -b Add an option before DHCP data\n"; print STDERR "--after option value -a Add an option after DHCP data\n"; print STDERR "--delete option -d Delete an option\n"; print STDERR "--list -l List set options\n"; print STDERR "--dry-run -n Don't modify the target file\n"; print STDERR "--help -h Display this help text\n"; print STDERR "\n"; print STDERR "The following DHCP options are currently recognized:\n"; printf STDERR "%-23s %-3s %s\n", 'Name', 'Num', 'Value Format'; foreach $i (sort { $a <=> $b } keys(%option_names)) { printf STDERR "%-23s %3d %s\n", $option_names{$i}, $i, $option_format{$i}[0]; } } %option_rnames = (); foreach $opt (keys(%option_names)) { $option_rnames{$option_names{$opt}} = $opt; } %before = (); %after = (); @clear = (); $usage = 0; $err = 0; $list = 0; $no_write = 0; undef $file; while (defined($opt = shift(@ARGV))) { if ($opt !~ /^-/) { if (defined($file)) { $err = $usage = 1; last; } $file = $opt; } elsif ($opt eq '-b' || $opt eq '--before') { $oname = shift(@ARGV); $odata = shift(@ARGV); if (!defined($odata)) { $err = $usage = 1; last; } $onum = option_number($oname); if (!defined($onum)) { print STDERR "$0: unknown option name: $oname\n"; $err = 1; next; } $odata = parse_option($onum, $odata); if (!defined($odata)) { print STDERR "$0: unable to parse data for option $oname\n"; $err = 1; next; } delete $after{$onum}; $before{$onum} = $odata; push(@clear, $onum); } elsif ($opt eq '-a' || $opt eq '--after') { $oname = shift(@ARGV); $odata = shift(@ARGV); if (!defined($odata)) { $err = $usage = 1; last; } $onum = option_number($oname); if (!defined($onum)) { print STDERR "$0: unknown option name: $oname\n"; $err = 1; next; } $odata = parse_option($onum, $odata); if (!defined($odata)) { print STDERR "$0: unable to parse data for option $oname\n"; $err = 1; next; } delete $before{$onum}; $after{$onum} = $odata; push(@clear, $onum); } elsif ($opt eq '-d' || $opt eq '--delete') { $oname = shift(@ARGV); if (!defined($oname)) { $err = $usage = 1; last; } $onum = option_number($oname); if (!defined($onum)) { print STDERR "$0: unknown option name: $oname\n"; $err = 1; next; } push(@clear, $onum); delete $before{$onum}; delete $after{$onum}; } elsif ($opt eq '-n' || $opt eq '--no-write' || $opt eq '--dry-run') { $no_write = 1; } elsif ($opt eq '-l' || $opt eq '--list') { $list = 1; } elsif ($opt eq '-h' || $opt eq '--help') { $usage = 1; } else { print STDERR "Invalid option: $opt\n"; $err = $usage = 1; } } if (!defined($file) && !$usage) { $err = $usage = 1; } if ($usage) { usage(); } if ($err || $usage) { exit($err); } if (!scalar(@clear)) { $no_write = 1; # No modifications requested } $mode = $no_write ? '<' : '+<'; open(FILE, $mode, $file) or die "$0: cannot open: $file: $!\n"; ($hdrinfo, @data) = read_optsets(\*FILE); if (!defined($hdrinfo)) { die "$0: $file: patch block not found or file corrupt\n"; } foreach $o (@clear) { $data[0] = delete_option($o, $data[0]); $data[1] = delete_option($o, $data[1]); } foreach $o (keys(%before)) { $data[0] = add_option($o, $before{$o}, $data[0]); } foreach $o (keys(%after)) { $data[1] = add_option($o, $after{$o}, $data[1]); } if ($list) { list_options('-b ', $data[0]); list_options('-a ', $data[1]); } if (!$no_write) { if (!write_optsets(\*FILE, $hdrinfo, @data)) { die "$0: $file: failed to write options: $!\n"; } } close(FILE); exit 0;