Mowson.org/karl

Netxfer

This script is used for downloading flash image files to the Evo T20, instead of using the standard netxfer software under windows. It also acts as a PXE server if you give it suitable command-line arguments - netxfer and PXE are almost identical, just different ports and filenames. The servers are not run as daemons - they're killed on script exit - this is usually most useful to me because I'm just testing things temporarily.

It works for me (well, it did last time I checked), but I'm happy to hear of problems and will try to fix them if practical.

Don't forget to punch a hole through your firewall if you're running one on the interface that is supposed to serve the files! (T20 uses TCP port 10067 for dhcp, and UDP port 10069 for tftp. Look at the script for port numbers for other modes)

Under Debian Lenny, the dhcp server I have used is called dhcp3-server and the tftp server is tftpd-hpa (at install time, if it asks you whether inetd should start tftpd, say "no" - the script here will start and stop it as required).

Whilst the image download is one file, the T20 fetches it in 2 parts. The first part is small - when it is done, the T20 pauses to do a few things (setup? erase flash? program bios chip?). After that, the rest of the file is downloaded. The default settings of some tftp servers have too short a timeout for the slow old T20 - the transfer fails when the T20 pauses for too long. The solution is just to have a longer timeout! The pause is longer for devices with more memory - maybe a minute for a 256M T20.

Note that if you have a particularly old tftp server, it may be limited to 32M transfers (512 byte blocks and 16bit block count). This is no good for us - you need a better tftp server (I use tftp-hpa).

The dhcp3 server that I use may not be available on SuSE. Apparently, there's a package called "ISC DHCP Server" which works if you create a symlink from /usr/sbin/dhcpd3 that points to /usr/sbin/dhcpd (or you could edit the script)(the server package is dhcp-server-3.0.5-9.i586.rpm in directory ftp://ftp.suse.com/pub/suse/update/10.2/rpm/i586/ although there may be a newer version by the time you read this). Thanks to Philip Loewen for this info.


PXE Serving

Running the script with command-line options of -p or -a gets you PXE mode, on either standard ports (67-69) or alternate ports (69,1067-1068). To use alternate mode, you need a client compiled to use those ports - it's one of the options for http://www.etherboot.org or http://rom-o-matic.net

This is my /tftpboot dir structure:

$ tree -p /tftpboot
/tftpboot
|-- [-r-xr--r--]  linux24
|-- [-rw-r--r--]  minirt24.gz
|-- [-rw-r--r--]  pxelinux.0
`-- [drwxr-xr-x]  pxelinux.cfg
    `-- [-rw-r--r--]  default

It is essential that these files and directories be readable by "other" (directories also need to be executable by "other"). Without this, the PXE client won't be able to read the files, and that would be silly.

You can get pxelinux.0 from http://syslinux.zytor.com/pxe.php (along with more info about PXE in general)

This is the content of /tftpboot/pxelinux.cfg/default

prompt 0
label linux
  kernel linux24
  append ramdisk_size=100000 init=/etc/init host=DEvoSL frominitrd nopcmcia noswap initrd=minirt24.gz

The script:

The script is my own construction, but the config options mostly came from assorted websites, with some hints from Markku Mähönen and Bartek Szurgot too. Malte Stretz provided a patch to cope with dhcpd dropping privileges, and to trap ctrl-c cleanly. I plan to tidy things up a bit, but I haven't had time for that yet - the script seems to work well as it is.

Contact me if you have any problems!

I have unimaginatively called the script netxfer.sh

#! /bin/sh
# Title:
#    netxfer.sh
#
# Author:
#    Karl Mowatt-Wilson
#    http://mowson.org/karl
#    copyright: 2007 Karl Mowatt-Wilson
#    licence: GPL v2
#
# Revisions:
#    v0.1  - 21 Jun 2007 - first release
#    v0.11 - 23 Jun 2007 - add 'readability' check of full image path
#    v0.12 - 23 Jun 2007 - improve readability check
#    v0.13 -  6 Jul 2007 - add 'tail' of syslog
#                        - put port numbers in variables
#                        - use getopts
#                        - incorporate PXE serving modes
#                        - improve tidying on exit
#    v0.14 - 28 Sep 2007 - patch kindly provided by Malte Stretz
#                           - cope with dhcpd3 dropping privileges
#                           - exit gracefully on ctrl+c
#    v0.15 - 16 Jan 2008 - Changed method of getting IP address of interface
#                           - old method was susceptible to translation problems

Usage () {
cat <<-EOF

        USAGE:
           netxfer.sh [mode] [-i interface] [directory] [file]

        This script sets up dhcpd/tftpd to temporarily serve files for either
        flashing an Evo T20 with new firmware, or booting a PXE client.  The
        netxfer mode is meant to replicate the function of the netxfer tool
        used under windows.  The servers are killed on script exit.

        If neither directory nor file are specified, both are set to defaults as per
        the mode (see MODE below).
        If only a directory is specified, the file is set to default.
        If only a file is specified, the file is set to the filename and the directory
        is set to the directory of the file.
        If both directory and file are specified, the file is assumed to be specified
        relative to the directory.

        Note that all files to be served must be readable by 'other'.  Directories to
        be served from must also be executable by 'other'.  Otherwise the tftp client
        will complain of not being able to find the file or access denied.
        Note also that long image filenames may not work (maybe a tftp problem, maybe
        a T20 problem).

        In theory, netxfer mode is safe to run on a network that already has
        a dhcp server, since we are using non-standard ports which will not
        interfere with any existing normal server.  The same is true with the
        alternate PXE mode.

        MODE:
           -a  Setup as PXE server with alternate dhcp ports (1067,1068), so as not
               to clash with any existing dhcp server on the same network.  Default
               directory to serve is '/tftpboot' and default file is 'pxelinux.0'
               You need a non-standard PXE client for this (it is one of the options
               with etherboot/rom-o-matic though).

           -n  This is the default mode.  Setup as Netxfer server for flashing
               firmware of Evo T20.  Uses ports 10067 & 10068 for dhcp, and
               port 10069 for tftp.  Default directory to serve is './' and default
               file is 'bootp.bin'

           -p  Setup as PXE server.  Used ports 67 & 68 for dhcp, and port 69 for
               tftp.  Default directory to serve is '/tftpboot' and default file
               is 'pxelinux.0'

        OPTIONS:
           -i interface
               Specify an alternate interface to listen on.  Default is eth0.

        EXAMPLES:
           netxfer.sh
              - plain netxfer of ./bootp.bin, using ports 10067-10069 on eth0.

           netxfer.sh -a -i eth1 /pxedir extradir/file
              - PXE serve /pxedir/extradir/file, using ports 69, 1067-1068 on eth1.

EOF
}
###########################################################################

# Define interface to listen on - probably 'eth0'
INTERFACE="eth0"

# Define range of IPs to hand out to clients: x.x.x.START - x.x.x.STOP
# First 3 octets come from IP of INTERFACE, which we figure out automatically.
IP_LAST_OCTET_START=230
IP_LAST_OCTET_STOP=240


# Define set of options for files to serve
NETXFER_TFTP_BASE="."
NETXFER_TFTP_FILE="bootp.bin"

PXE_TFTP_BASE="/tftpboot"
PXE_TFTP_FILE="pxelinux.0"

ALT_PXE_TFTP_BASE="/tftpboot"
ALT_PXE_TFTP_FILE="pxelinux.0"


# Define ports to use
NETXFER_DHCP_PORT=10067
NETXFER_TFTP_PORT=10069

PXE_DHCP_PORT=67
PXE_TFTP_PORT=69

ALT_PXE_DHCP_PORT=1067
ALT_PXE_TFTP_PORT=69


# set the defaults (can be overridden by commandline options)
DHCP_PORT="$NETXFER_DHCP_PORT"
TFTP_PORT="$NETXFER_TFTP_PORT"
TFTP_BASE="$NETXFER_TFTP_BASE"
TFTP_FILE="$NETXFER_TFTP_FILE"


SYSLOG="/var/log/syslog"

###########################################################################
## Function to exit with error message.
## First param is return code, remaining params are lines of error message.
#
Fail() {
   ExitCode=$1
   shift
   echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" >&2
   echo "$(basename "$0"): FATAL ERROR" >&2
   while [ $# -gt 0 ]; do
      echo "$1" >&2
      shift
   done
   sleep 2
   TidyUp
   exit $ExitCode
}

###########################################################################
## Function to tidy up temp files/dirs before exit.
#
TidyUp() {
   echo "=== Tidying ========================================================="
   [ "$TFTPD_PID" ] \
      && ps --pid $TFTPD_PID >/dev/null 2>/dev/null \
         && kill $TFTPD_PID

   [ "$DHCPD_PID_TMP" ] && [ -s "$DHCPD_PID_TMP" ] \
      && kill $(cat "$DHCPD_PID_TMP") >/dev/null 2>/dev/null

   [ -d "$DHCPD_TMP" ] && rm -r "$DHCPD_TMP"
}

###########################################################################
## Function to check a list of desired tools are available.
#
Toolcheck() {
   while [ $# -gt 0 ]; do
      TOOL="$1"
      shift
      echo "Checking '$TOOL'"
      which "$TOOL" >/dev/null 2>/dev/null \
         || Fail 3 "'which' failed for '$TOOL' - can't find this command."
   done
}


###########################################################################
#==========================================================================
# Parse command-line options.

while getopts "ai:np" OPT; do
   case $OPT in
      a)   # Alternate PXE setup
           DHCP_PORT="$ALT_PXE_DHCP_PORT"
           TFTP_PORT="$ALT_PXE_TFTP_PORT"
           TFTP_BASE="$ALT_PXE_TFTP_BASE"
           TFTP_FILE="$ALT_PXE_TFTP_FILE"
           ;;
      i)   # choose Interface to listen on
           INTERFACE=$OPTARG
           ;;
      n)   # Netxfer setup
           DHCP_PORT="$NETXFER_DHCP_PORT"
           TFTP_PORT="$NETXFER_TFTP_PORT"
           TFTP_BASE="$NETXFER_TFTP_BASE"
           TFTP_FILE="$NETXFER_TFTP_FILE"
           ;;
      p)   # PXE setup
           DHCP_PORT="$PXE_DHCP_PORT"
           TFTP_PORT="$PXE_TFTP_PORT"
           TFTP_BASE="$PXE_TFTP_BASE"
           TFTP_FILE="$PXE_TFTP_FILE"
           ;;
      *)   Usage
           exit
           ;;
    esac
done
shift $(($OPTIND - 1)); OPTIND=1

# Accept dir and file specified on commandline
[ "$1" ] && TFTP_BASE="$1"
[ "$2" ] && TFTP_FILE="$2"

# Canonicalise the path
TFTP_BASE="$(readlink -f "$TFTP_BASE")"

# Deal with only a file specified
[ -f "$TFTP_BASE" ] && {
   # Split filespec into dir & file
   TFTP_FILE="$(basename "$TFTP_BASE")"
   TFTP_BASE="$(dirname  "$TFTP_BASE")"
   [ "$2" ] && {
      # If a file is specified first then there should be no second param
      echo "WARNING: '$1' is a file, so am ignoring '$2'"
      sleep 2
   }
}

#==========================================================================
# Check that we have a hope of any of this working.
WARN_PAUSE=""

[ "$(id -u)" -eq 0 ] || {
   echo "WARNING: You don't seem to be root - this probably won't work..."
   WARN_PAUSE="TRUE"
}

ps -C in.tftpd >/dev/null && {
   echo "WARNING: There is already a tftpd running..."
   WARN_PAUSE="TRUE"
}

ps -C dhcpd >/dev/null && {
   echo "WARNING: There is already a dhcpd running..."
   WARN_PAUSE="TRUE"
}

[ "$WARN_PAUSE" ] && {
   echo "This may or may not be a problem!"
   sleep 2
   echo "Continuing anyway."
}

#==========================================================================
echo "=== Checking tools =================================================="
Toolcheck   \
   dhcpd3   \
   in.tftpd \
   ifconfig \
   grep     \
   head

#==========================================================================
# Get the IP address for the desired interface (usually eth0).
# Old version which fails with i18n of 'addr':
#  IP=$(ifconfig "$INTERFACE" | grep -oE 'addr:[0-9.]+' | grep -oE '[0-9.]+')
IP=$(ifconfig "$INTERFACE" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | head -n1)
[ "$IP" ] || Fail 3 "Could not get IP address for '$INTERFACE'"

IP_3_OCTETS=$(echo $IP | grep -oE '^[0-9]+\.[0-9]+\.[0-9]+')

# define range of IPs to hand out to clients
IP_START="$IP_3_OCTETS.$IP_LAST_OCTET_START"
IP_STOP="$IP_3_OCTETS.$IP_LAST_OCTET_STOP"

IP_SUBNET="$IP_3_OCTETS.0"
IP_NETMASK="255.255.255.0"

#==========================================================================
# Check that IMAGE is good & readable.

TFTP_FULLPATH="$TFTP_BASE/$TFTP_FILE"

echo "Checking access to '$TFTP_FULLPATH'"
[ -d "$TFTP_BASE" ]     || Fail 3 "'$TFTP_BASE' appears not to be a directory!"
[ -e "$TFTP_FULLPATH" ] || Fail 3 "'$TFTP_FULLPATH' doesn't exist!"
[ -f "$TFTP_FULLPATH" ] || Fail 3 "'$TFTP_FULLPATH' appears not to be a file!"

# Traverse the path and check that *every* element is readable by 'other'.
# (this might be overkill)
BADPATH=""
CHKPATH="$TFTP_FULLPATH"
while [ "$CHKPATH" ]; do
   PERMS="$(stat "$CHKPATH" --format="%A")"
   [ "$(echo "$PERMS" | cut -c8)" = 'r' ] || {
      echo "WARNING: 'other' can't read   '$CHKPATH'"
      BADPATH="true"
   }
   [ -d "$CHKPATH" ] && {
      [ "$(echo "$PERMS" | cut -c10)" = 'x' ] || {
         echo "WARNING: 'other' can't access '$CHKPATH'"
         BADPATH="true"
      }
   }
   if [ "$CHKPATH" = "/" ]; then
      CHKPATH=""
   else
      CHKPATH="$(dirname "$CHKPATH")"
   fi
done

[ "$BADPATH" ] && {
   echo "Unreadability might prevent tftpd from being able to serve files."
   echo "You probably need to do 'chmod o+r' on files or 'chmod o+rx' on directories."
   sleep 2
}


#==========================================================================
# If we have lsof available, check that nothing is already bound to our ports
#which lsof >/dev/null 2>/dev/null && {
#   ERR="$(lsof -i $INTERFACE:$DHCP_PORT)"
#   [ "$ERR" ] && \
#      Fail 3 "Something is already bound to our intended dhcp port" "$ERR"
#}

#==========================================================================
echo \
"==========================================================================
OK - everything looks workable so far:
   Interface:   $INTERFACE
   Our IP:      $IP
   Serving IPs: $IP_START .. $IP_STOP
   DHCP port:   $DHCP_PORT
   TFTP port:   $TFTP_PORT
   TFTP root:   '$TFTP_BASE'
   TFTP file:   '$TFTP_FILE'
=========================================================================="

#==========================================================================
# Make temp files for storing dhcpd info; it will drop privileges

DHCPD_CHROOT_CF=/conf
DHCPD_CHROOT_RW=/rw
DHCPD_CHROOT_PF=$DHCPD_CHROOT_RW/pid
DHCPD_CHROOT_LF=$DHCPD_CHROOT_RW/leases

# create a ro temp directory
DHCPD_TMP="$(mktemp -t $(basename $0)-dhcpd.XXXXXXXXXX)" \
  || Fail 3 "Couldn't mktemp for dhcpd temp files"
rm "$DHCPD_TMP" \
  || Fail 3 "Couldn't rm for dhcpd temp files"
mkdir -m 711 "$DHCPD_TMP" \
  || Fail 3 "Couldn't mkdir for dhcpd temp files"

# create a rw sub temp directory
mkdir -m 777 "$DHCPD_TMP/$DHCPD_CHROOT_RW" \
  || Fail 3 "Couldn't mkdir rw temp dir"

# create empty pid and lease file in rw space
DHCPD_PID_TMP="$DHCPD_TMP$DHCPD_CHROOT_PF"
DHCPD_LF_TMP="$DHCPD_TMP$DHCPD_CHROOT_LF"
touch $DHCPD_PID_TMP $DHCPD_LF_TMP
chmod 666 $DHCPD_PID_TMP $DHCPD_LF_TMP

# create ro conf file
DHCPD_CONF_TMP="$DHCPD_TMP$DHCPD_CHROOT_CF"
touch $DHCPD_CONF_TMP
chmod 644 $DHCPD_CONF_TMP
cat >"$DHCPD_CONF_TMP" <<-EOF
        # We might as well be authoritative.
        authoritative;

        # We don't need ddns updating
        ddns-update-style none;

        # Let addresses be recycled quickly - 10minutes
        default-lease-time 600;
        max-lease-time 600;

        subnet $IP_SUBNET netmask $IP_NETMASK {
           # Range of dynamic IP addresses to hand out.
           range dynamic-bootp $IP_START $IP_STOP;
           # IP address for client to request tftp from.
           next-server $IP;
           # File for client to request via tftp.
           filename "$TFTP_FILE";
        }
EOF

#==========================================================================
# Start log display, setup to quit when this script exits.
if [ -r "$SYSLOG" ]; then
   echo "Starting syslog display..."
   (tail "$SYSLOG" -f -n 0 -q --pid=$$ \
      | grep -E '(dhcpd|tftpd)' \
   ) &
else
   echo "WARNING: No access to $SYSLOG - not going to display log info."
   SYSLOG=""
fi


echo ==========================================================================
echo "Starting tftpd..."

# in.tftpd options:
#    -a = specify address/port
#    -l = standalone (listen) mode, not inetd mode
#    -s = change root dir on startup
#    -v = verbose logging (may be specified multiple times)
#    -B = max block size - too big can be a problem if it causes fragmentation
#    -R = server port range
#    -T = timeout in microseconds, before first pkt is retransmitted
in.tftpd -l -v -a $IP:$TFTP_PORT -s "$TFTP_BASE" -B 65464 -R 30000:39999 -T 6000000 \
   || Fail 3 "tftpd failed"

# Try to get the pid of the most recently started tftpd.
# Not very precise, but better than using killall!
# If you have lsof installed, you could use something like
# this to get the pid:  lsof -Fp -ni @$IP:$PORT | grep -Eo '[0-9]+'
# Or maybe use pidof / netstat?
TFTPD_PID=$(ps kstart_time -o pid -C in.tftpd --no-headers | tail)

[ "$SYSLOG" ] \
   || echo "SYSLOG not defined, so TFTP progress will not be displayed here."

echo ==========================================================================
echo "Starting dhcpd..."

# dhcpd3 options:
#    -d  = log to stderr - only do this if syslog tail failed above.
#    -p  = port to listen on
#    -q  = quiet - don't print licence info on startup
#    -cf = config file
#    -lf = lease file
#    -pf = pid file
if [ "$SYSLOG" ]; then
   LOGOPTIONS="-q"
else
   LOGOPTIONS="-d"
fi
dhcpd3 $LOGOPTIONS -p $DHCP_PORT -cf "$DHCPD_CONF_TMP" -lf "$DHCPD_LF_TMP" -pf "$DHCPD_PID_TMP" "$INTERFACE" &

#==========================================================================
# Shutdown daemons and remove the temp files on exit
trap TidyUp EXIT
trap exit INT
#==========================================================================

#==========================================================================
# Check that daemons haven't failed on us.
sleep 1
ps --pid $TFTPD_PID --no-headers >/dev/null 2>/dev/null || {
   Fail 3 "tftpd seems not to be running!"
}
ps --pid $(cat $DHCPD_PID_TMP) --no-headers >/dev/null 2>/dev/null || {
   Fail 3 "dhcpd seems not to be running!"
}
#==========================================================================
# Wait around until quitting time.
echo ==========================================================================
echo 'Press <ENTER> to quit.'
while ! read DUMMY; do sleep 0.5; done