[dhcp] Split PXE menuing code out of dhcp.c
authorMichael Brown <mcb30@etherboot.org>
Sat, 31 Jan 2009 07:36:05 +0000 (07:36 +0000)
committerMichael Brown <mcb30@etherboot.org>
Sun, 1 Feb 2009 01:21:40 +0000 (01:21 +0000)
The DHCP client code now implements only the mechanism of the DHCP and
PXE Boot Server protocols.  Boot Server Discovery can be initiated
manually using the "pxebs" command.  The menuing code is separated out
into a user-level function on a par with boot_root_path(), and is
entered in preference to a normal filename boot if the DHCP vendor
class is "PXEClient" and the PXE boot menu option exists.

17 files changed:
src/arch/i386/interface/pxe/pxe_preboot.c
src/hci/commands/dhcp_cmd.c
src/hci/mucurses/ansi_screen.c
src/hci/mucurses/wininit.c
src/include/gpxe/dhcp.h
src/include/gpxe/dhcppkt.h
src/include/gpxe/errfile.h
src/include/gpxe/fakedhcp.h
src/include/gpxe/settings.h
src/include/usr/autoboot.h
src/include/usr/dhcpmgmt.h
src/net/dhcppkt.c
src/net/fakedhcp.c
src/net/udp/dhcp.c
src/usr/autoboot.c
src/usr/dhcpmgmt.c
src/usr/pxemenu.c [new file with mode: 0644]

index 8220d1f..193abc3 100644 (file)
@@ -82,7 +82,7 @@ struct pxe_dhcp_packet_creator {
 static struct pxe_dhcp_packet_creator pxe_dhcp_packet_creators[] = {
        [CACHED_INFO_DHCPDISCOVER] = { create_fakedhcpdiscover },
        [CACHED_INFO_DHCPACK] = { create_fakedhcpack },
-       [CACHED_INFO_BINL] = { create_fakeproxydhcpack },
+       [CACHED_INFO_BINL] = { create_fakepxebsack },
 };
 
 /* The case in which the caller doesn't supply a buffer is really
index 07acc61..b44433e 100644 (file)
@@ -26,6 +26,7 @@
 #include <assert.h>
 #include <getopt.h>
 #include <gpxe/netdevice.h>
+#include <gpxe/in.h>
 #include <gpxe/command.h>
 #include <usr/dhcpmgmt.h>
 
@@ -60,7 +61,7 @@ static int dhcp_exec ( int argc, char **argv ) {
                { "help", 0, NULL, 'h' },
                { NULL, 0, NULL, 0 },
        };
-       const char *name;
+       const char *netdev_txt;
        struct net_device *netdev;
        int c;
        int rc;
@@ -82,14 +83,16 @@ static int dhcp_exec ( int argc, char **argv ) {
                dhcp_syntax ( argv );
                return 1;
        }
-       name = argv[optind];
+       netdev_txt = argv[optind];
 
-       /* Perform DHCP */
-       netdev = find_netdev ( name );
+       /* Parse arguments */
+       netdev = find_netdev ( netdev_txt );
        if ( ! netdev ) {
-               printf ( "No such interface: %s\n", name );
+               printf ( "No such interface: %s\n", netdev_txt );
                return 1;
        }
+
+       /* Perform DHCP */
        if ( ( rc = dhcp ( netdev ) ) != 0 ) {
                printf ( "Could not configure %s: %s\n", netdev->name,
                         strerror ( rc ) );
@@ -99,10 +102,96 @@ static int dhcp_exec ( int argc, char **argv ) {
        return 0;
 }
 
+/**
+ * "pxebs" command syntax message
+ *
+ * @v argv             Argument list
+ */
+static void pxebs_syntax ( char **argv ) {
+       printf ( "Usage:\n"
+                "  %s <interface> <discovery_ip> <server_type>\n"
+                "\n"
+                "Perform PXE Boot Server discovery\n",
+                argv[0] );
+}
+
+/**
+ * The "pxebs" command
+ *
+ * @v argc             Argument count
+ * @v argv             Argument list
+ * @ret rc             Exit code
+ */
+static int pxebs_exec ( int argc, char **argv ) {
+       static struct option longopts[] = {
+               { "help", 0, NULL, 'h' },
+               { NULL, 0, NULL, 0 },
+       };
+       const char *netdev_txt;
+       const char *pxe_server_txt;
+       const char *pxe_type_txt;
+       struct net_device *netdev;
+       struct in_addr pxe_server;
+       unsigned int pxe_type;
+       char *end;
+       int c;
+       int rc;
+
+       /* Parse options */
+       while ( ( c = getopt_long ( argc, argv, "h", longopts, NULL ) ) >= 0 ){
+               switch ( c ) {
+               case 'h':
+                       /* Display help text */
+               default:
+                       /* Unrecognised/invalid option */
+                       pxebs_syntax ( argv );
+                       return 1;
+               }
+       }
+
+       /* Need exactly one interface name remaining after the options */
+       if ( optind != ( argc - 3 ) ) {
+               pxebs_syntax ( argv );
+               return 1;
+       }
+       netdev_txt = argv[optind];
+       pxe_server_txt = argv[ optind + 1 ];
+       pxe_type_txt = argv[ optind + 2 ];
+
+       /* Parse arguments */
+       netdev = find_netdev ( netdev_txt );
+       if ( ! netdev ) {
+               printf ( "No such interface: %s\n", netdev_txt );
+               return 1;
+       }
+       if ( inet_aton ( pxe_server_txt, &pxe_server ) == 0 ) {
+               printf ( "Bad discovery IP address: %s\n", pxe_server_txt );
+               return 1;
+       }
+       pxe_type = strtoul ( pxe_type_txt, &end, 0 );
+       if ( *end ) {
+               printf ( "Bad server type: %s\n", pxe_type_txt );
+               return 1;
+       }
+
+       /* Perform Boot Server Discovery */
+       if ( ( rc = pxebs ( netdev, pxe_server, pxe_type ) ) != 0 ) {
+               printf ( "Could not discover boot server on %s: %s\n",
+                        netdev->name, strerror ( rc ) );
+               return 1;
+       }
+
+       return 0;
+}
+
 /** DHCP management commands */
 struct command dhcp_commands[] __command = {
        {
                .name = "dhcp",
                .exec = dhcp_exec,
        },
+       {
+               .name = "pxebs",
+               .exec = pxebs_exec,
+       },
 };
index 0742a7d..468bac0 100644 (file)
@@ -15,7 +15,7 @@ static void ansiscr_reset ( struct _curses_screen *scr ) {
        scr->attrs = 0;
        scr->curs_x = 0;
        scr->curs_y = 0;
-       printf ( "\033[0m\033[2J\033[1;1H" );
+       printf ( "\033[0m" );
 }
 
 static void ansiscr_movetoyx ( struct _curses_screen *scr,
index dfd0ca0..cd27f9f 100644 (file)
@@ -18,7 +18,7 @@ WINDOW *initscr ( void ) {
        stdscr->scr->init( stdscr->scr );
        stdscr->height = LINES;
        stdscr->width = COLS;
-       erase();
+       move ( 0, 0 );
        return stdscr;
 }
 
@@ -29,7 +29,7 @@ WINDOW *initscr ( void ) {
 int endwin ( void ) {
        attrset ( 0 );
        color_set ( 0, NULL );
-       erase();
+       mvprintw ( ( LINES - 1 ), 0, "\n" );
        stdscr->scr->exit( stdscr->scr );
        return OK;
 }
index d49ba7f..2fddb40 100644 (file)
 #include <gpxe/list.h>
 #include <gpxe/refcnt.h>
 #include <gpxe/tables.h>
+#include <gpxe/uuid.h>
+#include <gpxe/netdevice.h>
 
-struct net_device;
 struct job_interface;
 struct dhcp_options;
 struct dhcp_packet;
-struct dhcp_pxe_boot_menu_item;
 
 /** BOOTP/DHCP server port */
 #define BOOTPS_PORT 67
@@ -88,12 +88,53 @@ struct dhcp_pxe_boot_menu_item;
 /** PXE boot menu */
 #define DHCP_PXE_BOOT_MENU DHCP_ENCAP_OPT ( DHCP_VENDOR_ENCAP, 9 )
 
+/** PXE boot menu */
+struct dhcp_pxe_boot_menu {
+       /** "Type" */
+       uint16_t type;
+       /** Description length */
+       uint8_t desc_len;
+       /** Description */
+       char desc[0];
+} __attribute__ (( packed ));
+
 /** PXE boot menu prompt */
 #define DHCP_PXE_BOOT_MENU_PROMPT DHCP_ENCAP_OPT ( DHCP_VENDOR_ENCAP, 10 )
 
+/** PXE boot menu prompt */
+struct dhcp_pxe_boot_menu_prompt {
+       /** Timeout
+        *
+        * A value of 0 means "time out immediately and select first
+        * boot item, without displaying the prompt".  A value of 255
+        * means "display menu immediately with no timeout".  Any
+        * other value means "display prompt, wait this many seconds
+        * for keypress, if key is F8, display menu, otherwise select
+        * first boot item".
+        */
+       uint8_t timeout;
+       /** Prompt to press F8 */
+       char prompt[0];
+} __attribute__ (( packed ));
+
 /** PXE boot menu item */
 #define DHCP_PXE_BOOT_MENU_ITEM DHCP_ENCAP_OPT ( DHCP_VENDOR_ENCAP, 71 )
 
+/** PXE boot menu item */
+struct dhcp_pxe_boot_menu_item {
+       /** "Type"
+        *
+        * This field actually identifies the specific boot server (or
+        * cluster of boot servers offering identical boot files).
+        */
+       uint16_t type;
+       /** "Layer"
+        *
+        * Just don't ask.
+        */
+       uint16_t layer;
+} __attribute__ (( packed ));
+
 /** Requested IP address */
 #define DHCP_REQUESTED_ADDRESS 50
 
@@ -140,6 +181,14 @@ struct dhcp_pxe_boot_menu_item;
 /** Client identifier */
 #define DHCP_CLIENT_ID 61
 
+/** Client identifier */
+struct dhcp_client_id {
+       /** Link-layer protocol */
+       uint8_t ll_proto;
+       /** Link-layer address */
+       uint8_t ll_addr[MAX_LL_ADDR_LEN];
+} __attribute__ (( packed ));
+
 /** TFTP server name
  *
  * This option replaces the fixed "sname" field, when that field is
@@ -163,6 +212,16 @@ struct dhcp_pxe_boot_menu_item;
 /** UUID client identifier */
 #define DHCP_CLIENT_UUID 97
 
+/** UUID client identifier */
+struct dhcp_client_uuid {
+       /** Identifier type */
+       uint8_t type;
+       /** UUID */
+       union uuid uuid;
+} __attribute__ (( packed ));
+
+#define DHCP_CLIENT_UUID_TYPE 0
+
 /** Etherboot-specific encapsulated options
  *
  * This encapsulated options field is used to contain all options
@@ -213,7 +272,7 @@ struct dhcp_pxe_boot_menu_item;
 /** Skip PXE DHCP protocol extensions such as ProxyDHCP
  *
  * If set to a non-zero value, gPXE will not wait for ProxyDHCP offers
- * and will ignore any PXE-specific DHCP offers that it receives.
+ * and will ignore any PXE-specific DHCP options that it receives.
  */
 #define DHCP_EB_NO_PXEDHCP DHCP_ENCAP_OPT ( DHCP_EB_ENCAP, 0xb0 )
 
@@ -230,6 +289,16 @@ struct dhcp_pxe_boot_menu_item;
  */
 #define DHCP_EB_BUS_ID DHCP_ENCAP_OPT ( DHCP_EB_ENCAP, 0xb1 )
 
+/** Network device descriptor */
+struct dhcp_netdev_desc {
+       /** Bus type ID */
+       uint8_t type;
+       /** Vendor ID */
+       uint16_t vendor;
+       /** Device ID */
+       uint16_t device;
+} __attribute__ (( packed ));
+
 /** BIOS drive number
  *
  * This is the drive number for a drive emulated via INT 13.  0x80 is
@@ -480,13 +549,13 @@ struct dhcphdr {
  */
 #define DHCP_MIN_LEN 552
 
-/** Maximum time that we will wait for ProxyDHCP responses */
-#define PROXYDHCP_WAIT_TIME ( 2 * TICKS_PER_SEC )
-
 /** Timeouts for sending DHCP packets */
 #define DHCP_MIN_TIMEOUT ( 1 * TICKS_PER_SEC )
 #define DHCP_MAX_TIMEOUT ( 10 * TICKS_PER_SEC )
 
+/** Maximum time that we will wait for ProxyDHCP responses */
+#define PROXYDHCP_MAX_TIMEOUT ( 2 * TICKS_PER_SEC )
+
 /** Settings block name used for DHCP responses */
 #define DHCP_SETTINGS_NAME "dhcp"
 
@@ -494,19 +563,18 @@ struct dhcphdr {
 #define PROXYDHCP_SETTINGS_NAME "proxydhcp"
 
 /** Setting block name used for BootServerDHCP responses */
-#define BSDHCP_SETTINGS_NAME "bs"
+#define PXEBS_SETTINGS_NAME "pxebs"
 
 extern int dhcp_create_packet ( struct dhcp_packet *dhcppkt,
                                struct net_device *netdev, uint8_t msgtype,
-                               struct dhcp_options *options, 
+                               const void *options, size_t options_len,
                                void *data, size_t max_len );
 extern int dhcp_create_request ( struct dhcp_packet *dhcppkt,
                                 struct net_device *netdev,
                                 unsigned int msgtype, struct in_addr ciaddr,
-                                struct in_addr server,
-                                struct in_addr requested_ip,
-                                struct dhcp_pxe_boot_menu_item *menu_item,
                                 void *data, size_t max_len );
 extern int start_dhcp ( struct job_interface *job, struct net_device *netdev );
+extern int start_pxebs ( struct job_interface *job, struct net_device *netdev,
+                        struct in_addr pxe_server, unsigned int pxe_type );
 
 #endif /* _GPXE_DHCP_H */
index 179be2f..e8f8faf 100644 (file)
@@ -9,27 +9,54 @@
 
 #include <gpxe/dhcp.h>
 #include <gpxe/dhcpopts.h>
+#include <gpxe/refcnt.h>
 
 /**
  * A DHCP packet
  *
  */
 struct dhcp_packet {
+       /** Reference counter */
+       struct refcnt refcnt;
        /** The DHCP packet contents */
        struct dhcphdr *dhcphdr;
        /** Maximum length of the DHCP packet buffer */
        size_t max_len;
        /** Used length of the DHCP packet buffer */
        size_t len;
-       /** DHCP option blocks */
+       /** DHCP options */
        struct dhcp_options options;
+       /** Settings interface */
+       struct settings settings;
 };
 
+/**
+ * Increment reference count on DHCP packet
+ *
+ * @v dhcppkt          DHCP packet
+ * @ret dhcppkt                DHCP packet
+ */
+static inline __attribute__ (( always_inline )) struct dhcp_packet *
+dhcppkt_get ( struct dhcp_packet *dhcppkt ) {
+       ref_get ( &dhcppkt->refcnt );
+       return dhcppkt;
+}
+
+/**
+ * Decrement reference count on DHCP packet
+ *
+ * @v dhcppkt          DHCP packet
+ */
+static inline __attribute__ (( always_inline )) void
+dhcppkt_put ( struct dhcp_packet *dhcppkt ) {
+       ref_put ( &dhcppkt->refcnt );
+}
+
 extern int dhcppkt_store ( struct dhcp_packet *dhcppkt, unsigned int tag,
                           const void *data, size_t len );
 extern int dhcppkt_fetch ( struct dhcp_packet *dhcppkt, unsigned int tag,
                           void *data, size_t len );
 extern void dhcppkt_init ( struct dhcp_packet *dhcppkt, 
-                          void *data, size_t len );
+                          struct dhcphdr *data, size_t len );
 
 #endif /* _GPXE_DHCPPKT_H */
index 6b7f3a4..df3717f 100644 (file)
 #define ERRFILE_smbios               ( ERRFILE_OTHER | 0x00120000 )
 #define ERRFILE_smbios_settings              ( ERRFILE_OTHER | 0x00130000 )
 #define ERRFILE_efi_smbios           ( ERRFILE_OTHER | 0x00140000 )
+#define ERRFILE_pxemenu                      ( ERRFILE_OTHER | 0x00150000 )
 
 /** @} */
 
index 990b56a..550b74f 100644 (file)
@@ -15,7 +15,7 @@ extern int create_fakedhcpdiscover ( struct net_device *netdev,
                                     void *data, size_t max_len );
 extern int create_fakedhcpack ( struct net_device *netdev,
                                void *data, size_t max_len );
-extern int create_fakeproxydhcpack ( struct net_device *netdev,
-                                    void *data, size_t max_len );
+extern int create_fakepxebsack ( struct net_device *netdev,
+                                void *data, size_t max_len );
 
 #endif /* _GPXE_FAKEDHCP_H */
index 60244d3..bf80b1e 100644 (file)
@@ -304,4 +304,16 @@ static inline int delete_named_setting ( const char *name ) {
        return storef_named_setting ( name, NULL );
 }
 
+/**
+ * Check existence of setting
+ *
+ * @v settings         Settings block, or NULL to search all blocks
+ * @v setting          Setting to fetch
+ * @ret exists         Setting exists
+ */
+static inline int setting_exists ( struct settings *settings,
+                                  struct setting *setting ) {
+       return ( fetch_setting_len ( settings, setting ) >= 0 );
+}
+
 #endif /* _GPXE_SETTINGS_H */
index b64cbb8..1e9647c 100644 (file)
@@ -7,9 +7,15 @@
  *
  */
 
+#include <gpxe/in.h>
+struct net_device;
+
 extern int shutdown_exit_flags;
 
 extern void autoboot ( void );
+extern int boot_next_server_and_filename ( struct in_addr next_server,
+                                          const char *filename );
 extern int boot_root_path ( const char *root_path );
+extern int pxe_menu_boot ( struct net_device *netdev );
 
 #endif /* _USR_AUTOBOOT_H */
index 2757a1c..dc9de7b 100644 (file)
@@ -9,6 +9,8 @@
 
 struct net_device;
 
-int dhcp ( struct net_device *netdev );
+extern int dhcp ( struct net_device *netdev );
+extern int pxebs ( struct net_device *netdev, struct in_addr pxe_server,
+                  unsigned int pxe_type );
 
 #endif /* _USR_DHCPMGMT_H */
index c8bf215..1f2d373 100644 (file)
  *
  */
 
+/****************************************************************************
+ *
+ * DHCP packet raw interface
+ *
+ */
+
 /**
  * Calculate used length of an IPv4 field within a DHCP packet
  *
@@ -193,21 +199,79 @@ int dhcppkt_fetch ( struct dhcp_packet *dhcppkt, unsigned int tag,
        return dhcpopt_fetch ( &dhcppkt->options, tag, data, len );
 }
 
+/****************************************************************************
+ *
+ * DHCP packet settings interface
+ *
+ */
+
+/**
+ * Store value of DHCP setting
+ *
+ * @v settings         Settings block
+ * @v setting          Setting to store
+ * @v data             Setting data, or NULL to clear setting
+ * @v len              Length of setting data
+ * @ret rc             Return status code
+ */
+static int dhcppkt_settings_store ( struct settings *settings,
+                                   struct setting *setting,
+                                   const void *data, size_t len ) {
+       struct dhcp_packet *dhcppkt =
+               container_of ( settings, struct dhcp_packet, settings );
+
+       return dhcppkt_store ( dhcppkt, setting->tag, data, len );
+}
+
+/**
+ * Fetch value of DHCP setting
+ *
+ * @v settings         Settings block, or NULL to search all blocks
+ * @v setting          Setting to fetch
+ * @v data             Buffer to fill with setting data
+ * @v len              Length of buffer
+ * @ret len            Length of setting data, or negative error
+ */
+static int dhcppkt_settings_fetch ( struct settings *settings,
+                                   struct setting *setting,
+                                   void *data, size_t len ) {
+       struct dhcp_packet *dhcppkt =
+               container_of ( settings, struct dhcp_packet, settings );
+
+       return dhcppkt_fetch ( dhcppkt, setting->tag, data, len );
+}
+
+/** DHCP settings operations */
+static struct settings_operations dhcppkt_settings_operations = {
+       .store = dhcppkt_settings_store,
+       .fetch = dhcppkt_settings_fetch,
+};
+
+/****************************************************************************
+ *
+ * Constructor
+ *
+ */
+
 /**
- * Initialise prepopulated DHCP packet
+ * Initialise DHCP packet
  *
- * @v dhcppkt          Uninitialised DHCP packet
- * @v data             Memory for DHCP packet data
- * @v max_len          Length of memory for DHCP packet data
+ * @v dhcppkt          DHCP packet structure to fill in
+ * @v data             DHCP packet raw data
+ * @v max_len          Length of raw data buffer
  *
- * The memory content must already be filled with valid DHCP options.
- * A zeroed block counts as a block of valid DHCP options.
+ * Initialise a DHCP packet structure from a data buffer containing a
+ * DHCP packet.
  */
-void dhcppkt_init ( struct dhcp_packet *dhcppkt, void *data, size_t len ) {
+void dhcppkt_init ( struct dhcp_packet *dhcppkt, struct dhcphdr *data,
+                   size_t len ) {
        dhcppkt->dhcphdr = data;
        dhcppkt->max_len = len;
        dhcpopt_init ( &dhcppkt->options, &dhcppkt->dhcphdr->options,
                       ( len - offsetof ( struct dhcphdr, options ) ) );
        dhcppkt->len = ( offsetof ( struct dhcphdr, options ) +
                         dhcppkt->options.len );
+       settings_init ( &dhcppkt->settings,
+                       &dhcppkt_settings_operations, &dhcppkt->refcnt,
+                       DHCP_SETTINGS_NAME, 0 );
 }
index 2fb08c6..0518789 100644 (file)
@@ -108,12 +108,11 @@ static int copy_settings ( struct dhcp_packet *dest,
 int create_fakedhcpdiscover ( struct net_device *netdev,
                              void *data, size_t max_len ) {
        struct dhcp_packet dhcppkt;
-       struct in_addr dummy_addr = { 0 };
+       struct in_addr ciaddr = { 0 };
        int rc;
 
        if ( ( rc = dhcp_create_request ( &dhcppkt, netdev, DHCPDISCOVER,
-                                         dummy_addr, dummy_addr, dummy_addr,
-                                         NULL, data, max_len ) ) != 0 ) {
+                                         ciaddr, data, max_len ) ) != 0 ) {
                DBG ( "Could not create DHCPDISCOVER: %s\n",
                      strerror ( rc ) );
                return rc;
@@ -138,7 +137,7 @@ int create_fakedhcpack ( struct net_device *netdev,
        int rc;
 
        /* Create base DHCPACK packet */
-       if ( ( rc = dhcp_create_packet ( &dhcppkt, netdev, DHCPACK, NULL,
+       if ( ( rc = dhcp_create_packet ( &dhcppkt, netdev, DHCPACK, NULL, 0,
                                         data, max_len ) ) != 0 ) {
                DBG ( "Could not create DHCPACK: %s\n", strerror ( rc ) );
                return rc;
@@ -164,7 +163,7 @@ int create_fakedhcpack ( struct net_device *netdev,
 }
 
 /**
- * Create ProxyDHCPACK packet
+ * Create fake PXE Boot Server ACK packet
  *
  * @v netdev           Network device
  * @v data             Buffer for DHCP packet
@@ -173,43 +172,43 @@ int create_fakedhcpack ( struct net_device *netdev,
  *
  * Used by external code.
  */
-int create_fakeproxydhcpack ( struct net_device *netdev,
-                             void *data, size_t max_len ) {
+int create_fakepxebsack ( struct net_device *netdev,
+                         void *data, size_t max_len ) {
        struct dhcp_packet dhcppkt;
-       struct settings *settings;
-       struct settings *bs_settings;
+       struct settings *proxy_settings;
+       struct settings *pxebs_settings;
        int rc;
 
-       /* Identify ProxyDHCP settings */
-       settings = find_settings ( PROXYDHCP_SETTINGS_NAME );
-
-       /* No ProxyDHCP settings => use normal DHCPACK */
-       if ( ! settings )
+       /* Identify available settings */
+       proxy_settings = find_settings ( PROXYDHCP_SETTINGS_NAME );
+       pxebs_settings = find_settings ( PXEBS_SETTINGS_NAME );
+       if ( ( ! proxy_settings ) && ( ! pxebs_settings ) ) {
+               /* No PXE boot server; return the regular DHCPACK */
                return create_fakedhcpack ( netdev, data, max_len );
+       }
 
        /* Create base DHCPACK packet */
-       if ( ( rc = dhcp_create_packet ( &dhcppkt, netdev, DHCPACK, NULL,
+       if ( ( rc = dhcp_create_packet ( &dhcppkt, netdev, DHCPACK, NULL, 0,
                                         data, max_len ) ) != 0 ) {
-               DBG ( "Could not create ProxyDHCPACK: %s\n",
+               DBG ( "Could not create PXE BS ACK: %s\n",
                      strerror ( rc ) );
                return rc;
        }
 
        /* Merge in ProxyDHCP options */
-       if ( ( rc = copy_settings ( &dhcppkt, settings ) ) != 0 ) {
-               DBG ( "Could not set ProxyDHCPACK settings: %s\n",
+       if ( proxy_settings &&
+            ( ( rc = copy_settings ( &dhcppkt, proxy_settings ) ) != 0 ) ) {
+               DBG ( "Could not copy ProxyDHCP settings: %s\n",
                      strerror ( rc ) );
                return rc;
        }
 
        /* Merge in BootServerDHCP options, if present */
-       bs_settings = find_settings ( BSDHCP_SETTINGS_NAME );
-       if ( bs_settings ) {
-               if ( ( rc = copy_settings ( &dhcppkt, bs_settings ) ) != 0 ) {
-                       DBG ( "Could not set BootServerDHCPACK settings: "
-                             "%s\n", strerror ( rc ) );
-                       return rc;
-               }
+       if ( pxebs_settings &&
+            ( ( rc = copy_settings ( &dhcppkt, pxebs_settings ) ) != 0 ) ) {
+               DBG ( "Could not copy PXE BS settings: %s\n",
+                     strerror ( rc ) );
+               return rc;
        }
 
        return 0;
index c7e1f88..3554b40 100644 (file)
@@ -23,7 +23,6 @@
 #include <errno.h>
 #include <assert.h>
 #include <byteswap.h>
-#include <console.h>
 #include <gpxe/if_ether.h>
 #include <gpxe/netdevice.h>
 #include <gpxe/device.h>
 #include <gpxe/tcpip.h>
 #include <gpxe/ip.h>
 #include <gpxe/uuid.h>
-#include <gpxe/dhcp.h>
 #include <gpxe/timer.h>
 #include <gpxe/settings.h>
 #include <gpxe/dhcp.h>
 #include <gpxe/dhcpopts.h>
 #include <gpxe/dhcppkt.h>
 #include <gpxe/features.h>
-#include <gpxe/keys.h>
 
 /** @file
  *
@@ -49,6 +46,9 @@
  *
  */
 
+struct dhcp_session;
+static int dhcp_tx ( struct dhcp_session *dhcp );
+
 /**
  * DHCP operation types
  *
@@ -86,13 +86,6 @@ static uint8_t dhcp_request_options_data[] = {
        DHCP_END
 };
 
-/** Options common to all DHCP requests */
-static struct dhcp_options dhcp_request_options = {
-       .data = dhcp_request_options_data,
-       .max_len = sizeof ( dhcp_request_options_data ),
-       .len = sizeof ( dhcp_request_options_data ),
-};
-
 /** DHCP feature codes */
 static uint8_t dhcp_features[0] __table_start ( uint8_t, dhcp_features );
 static uint8_t dhcp_features_end[0] __table_end ( uint8_t, dhcp_features );
@@ -100,77 +93,13 @@ static uint8_t dhcp_features_end[0] __table_end ( uint8_t, dhcp_features );
 /** Version number feature */
 FEATURE_VERSION ( VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH );
 
-/** DHCP network device descriptor */
-struct dhcp_netdev_desc {
-       /** Bus type ID */
-       uint8_t type;
-       /** Vendor ID */
-       uint16_t vendor;
-       /** Device ID */
-       uint16_t device;
-} __attribute__ (( packed ));
-
-/** DHCP client identifier */
-struct dhcp_client_id {
-       /** Link-layer protocol */
-       uint8_t ll_proto;
-       /** Link-layer address */
-       uint8_t ll_addr[MAX_LL_ADDR_LEN];
-} __attribute__ (( packed ));
-
-/** DHCP client UUID */
-struct dhcp_client_uuid {
-       /** Identifier type */
-       uint8_t type;
-       /** UUID */
-       union uuid uuid;
-} __attribute__ (( packed ));
-
-#define DHCP_CLIENT_UUID_TYPE 0
-
-/** DHCP PXE boot prompt */
-struct dhcp_pxe_boot_prompt {
-       /** Timeout
-        *
-        * A value of 0 means "time out immediately and select first
-        * boot item, without displaying the prompt".  A value of 255
-        * means "display menu immediately with no timeout".  Any
-        * other value means "display prompt, wait this many seconds
-        * for keypress, if key is F8, display menu, otherwise select
-        * first boot item".
-        */
-       uint8_t timeout;
-       /** Prompt to press F8 */
-       char prompt[0];
-} __attribute__ (( packed ));
-
-/** DHCP PXE boot menu item description */
-struct dhcp_pxe_boot_menu_item_desc {
-       /** "Type" */
-       uint16_t type;
-       /** Description length */
-       uint8_t desc_len;
-       /** Description */
-       char desc[0];
-} __attribute__ (( packed ));
-
-/** DHCP PXE boot menu item */
-struct dhcp_pxe_boot_menu_item {
-       /** "Type"
-        *
-        * This field actually identifies the specific boot server (or
-        * cluster of boot servers offering identical boot files).
-        */
-       uint16_t type;
-       /** "Layer"
-        *
-        * Just don't ask.
-        */
-       uint16_t layer;
-} __attribute__ (( packed ));
-
-/** Maximum allowed number of PXE boot menu items */
-#define PXE_BOOT_MENU_MAX_ITEMS 20
+/** DHCP server address setting */
+struct setting dhcp_server_setting __setting = {
+       .name = "dhcp-server",
+       .description = "DHCP server address",
+       .tag = DHCP_SERVER_IDENTIFIER,
+       .type = &setting_type_ipv4,
+};
 
 /**
  * Name a DHCP packet type
@@ -212,228 +141,616 @@ static uint32_t dhcp_xid ( struct net_device *netdev ) {
 
 /****************************************************************************
  *
- * DHCP settings
+ * DHCP session
  *
  */
 
-/** A DHCP settings block */
-struct dhcp_settings {
+struct dhcp_session;
+
+/** DHCP session state operations */
+struct dhcp_session_state {
+       /** State name */
+       const char *name;
+       /**
+        * Construct transmitted packet
+        *
+        * @v dhcp              DHCP session
+        * @v dhcppkt           DHCP packet
+        * @v peer              Destination address
+        */
+       int ( * tx ) ( struct dhcp_session *dhcp,
+                      struct dhcp_packet *dhcppkt,
+                      struct sockaddr_in *peer );
+       /** Handle received packet
+        *
+        * @v dhcp              DHCP session
+        * @v dhcppkt           DHCP packet
+        * @v peer              DHCP server address
+        * @v msgtype           DHCP message type
+        */
+       void ( * rx ) ( struct dhcp_session *dhcp,
+                       struct dhcp_packet *dhcppkt,
+                       struct sockaddr_in *peer,
+                       uint8_t msgtype );
+       /** Handle timer expiry
+        *
+        * @v dhcp              DHCP session
+        */
+       void ( * expired ) ( struct dhcp_session *dhcp );
+       /** Transmitted message type */
+       uint8_t tx_msgtype;
+       /** Apply minimum timeout */
+       uint8_t apply_min_timeout;
+};
+
+static struct dhcp_session_state dhcp_state_discover;
+static struct dhcp_session_state dhcp_state_request;
+static struct dhcp_session_state dhcp_state_proxy;
+static struct dhcp_session_state dhcp_state_pxebs;
+
+/** A DHCP session */
+struct dhcp_session {
        /** Reference counter */
        struct refcnt refcnt;
-       /** DHCP packet */
-       struct dhcp_packet dhcppkt;
-       /** Setting interface */
-       struct settings settings;
+       /** Job control interface */
+       struct job_interface job;
+       /** Data transfer interface */
+       struct xfer_interface xfer;
+
+       /** Network device being configured */
+       struct net_device *netdev;
+       /** Local socket address */
+       struct sockaddr_in local;
+       /** State of the session */
+       struct dhcp_session_state *state;
+
+       /** Offered IP address */
+       struct in_addr offer;
+       /** DHCP server */
+       struct in_addr server;
+       /** DHCP offer priority */
+       int priority;
+
+       /** ProxyDHCP protocol extensions should be ignored */
+       int no_pxedhcp;
+       /** ProxyDHCP server */
+       struct in_addr proxy_server;
+       /** ProxyDHCP server priority */
+       int proxy_priority;
+
+       /** PXE Boot Server */
+       struct in_addr pxe_server;
+       /** PXE Boot Server type */
+       uint16_t pxe_type;
+
+       /** Retransmission timer */
+       struct retry_timer timer;
+       /** Start time of the current state (in ticks) */
+       unsigned long start;
 };
 
 /**
- * Increment reference count on DHCP settings block
+ * Free DHCP session
  *
- * @v dhcpset          DHCP settings block
- * @ret dhcpset                DHCP settings block
+ * @v refcnt           Reference counter
  */
-static inline __attribute__ (( always_inline )) struct dhcp_settings *
-dhcpset_get ( struct dhcp_settings *dhcpset ) {
-       ref_get ( &dhcpset->refcnt );
-       return dhcpset;
+static void dhcp_free ( struct refcnt *refcnt ) {
+       struct dhcp_session *dhcp =
+               container_of ( refcnt, struct dhcp_session, refcnt );
+
+       netdev_put ( dhcp->netdev );
+       free ( dhcp );
 }
 
 /**
- * Decrement reference count on DHCP settings block
+ * Mark DHCP session as complete
  *
- * @v dhcpset          DHCP settings block
+ * @v dhcp             DHCP session
+ * @v rc               Return status code
  */
-static inline __attribute__ (( always_inline )) void
-dhcpset_put ( struct dhcp_settings *dhcpset ) {
-       ref_put ( &dhcpset->refcnt );
+static void dhcp_finished ( struct dhcp_session *dhcp, int rc ) {
+
+       /* Block futher incoming messages */
+       job_nullify ( &dhcp->job );
+       xfer_nullify ( &dhcp->xfer );
+
+       /* Stop retry timer */
+       stop_timer ( &dhcp->timer );
+
+       /* Free resources and close interfaces */
+       xfer_close ( &dhcp->xfer, rc );
+       job_done ( &dhcp->job, rc );
 }
 
 /**
- * Store value of DHCP setting
+ * Transition to new DHCP session state
  *
- * @v settings         Settings block
- * @v setting          Setting to store
- * @v data             Setting data, or NULL to clear setting
- * @v len              Length of setting data
- * @ret rc             Return status code
+ * @v dhcp             DHCP session
+ * @v state            New session state
  */
-static int dhcpset_store ( struct settings *settings, struct setting *setting,
-                          const void *data, size_t len ) {
-       struct dhcp_settings *dhcpset =
-               container_of ( settings, struct dhcp_settings, settings );
+static void dhcp_set_state ( struct dhcp_session *dhcp,
+                            struct dhcp_session_state *state ) {
 
-       return dhcppkt_store ( &dhcpset->dhcppkt, setting->tag, data, len );
+       DBGC ( dhcp, "DHCP %p entering %s state\n", dhcp, state->name );
+       dhcp->state = state;
+       dhcp->start = currticks();
+       stop_timer ( &dhcp->timer );
+       dhcp->timer.min_timeout =
+               ( state->apply_min_timeout ? DHCP_MIN_TIMEOUT : 0 );
+       dhcp->timer.max_timeout = DHCP_MAX_TIMEOUT;
+       start_timer_nodelay ( &dhcp->timer );
 }
 
+/****************************************************************************
+ *
+ * DHCP state machine
+ *
+ */
+
 /**
- * Fetch value of DHCP setting
+ * Construct transmitted packet for DHCP discovery
  *
- * @v settings         Settings block, or NULL to search all blocks
- * @v setting          Setting to fetch
- * @v data             Buffer to fill with setting data
- * @v len              Length of buffer
- * @ret len            Length of setting data, or negative error
+ * @v dhcp             DHCP session
+ * @v dhcppkt          DHCP packet
+ * @v peer             Destination address
  */
-static int dhcpset_fetch ( struct settings *settings, struct setting *setting,
-                          void *data, size_t len ) {
-       struct dhcp_settings *dhcpset =
-               container_of ( settings, struct dhcp_settings, settings );
+static int dhcp_discovery_tx ( struct dhcp_session *dhcp,
+                              struct dhcp_packet *dhcppkt __unused,
+                              struct sockaddr_in *peer ) {
+
+       DBGC ( dhcp, "DHCP %p DHCPDISCOVER\n", dhcp );
+
+       /* Set server address */
+       peer->sin_addr.s_addr = INADDR_BROADCAST;
+       peer->sin_port = htons ( BOOTPS_PORT );
 
-       return dhcppkt_fetch ( &dhcpset->dhcppkt, setting->tag, data, len );
+       return 0;
 }
 
-/** DHCP settings operations */
-static struct settings_operations dhcpset_settings_operations = {
-       .store = dhcpset_store,
-       .fetch = dhcpset_fetch,
-};
+/**
+ * Handle received packet during DHCP discovery
+ *
+ * @v dhcp             DHCP session
+ * @v dhcppkt          DHCP packet
+ * @v peer             DHCP server address
+ * @v msgtype          DHCP message type
+ */
+static void dhcp_discovery_rx ( struct dhcp_session *dhcp,
+                               struct dhcp_packet *dhcppkt,
+                               struct sockaddr_in *peer, uint8_t msgtype ) {
+       struct in_addr server_id = { 0 };
+       struct in_addr ip;
+       char vci[9]; /* "PXEClient" */
+       int vci_len;
+       int has_pxeclient;
+       int8_t priority = 0;
+       uint8_t no_pxedhcp = 0;
+       unsigned long elapsed;
+
+       DBGC ( dhcp, "DHCP %p %s from %s:%d", dhcp,
+              dhcp_msgtype_name ( msgtype ), inet_ntoa ( peer->sin_addr ),
+              ntohs ( peer->sin_port ) );
+
+       /* Identify server ID */
+       dhcppkt_fetch ( dhcppkt, DHCP_SERVER_IDENTIFIER,
+                       &server_id, sizeof ( server_id ) );
+       if ( server_id.s_addr != peer->sin_addr.s_addr )
+               DBGC ( dhcp, " (%s)", inet_ntoa ( server_id ) );
+
+       /* Identify offered IP address */
+       ip = dhcppkt->dhcphdr->yiaddr;
+       if ( ip.s_addr )
+               DBGC ( dhcp, " for %s", inet_ntoa ( ip ) );
+
+       /* Identify "PXEClient" vendor class */
+       vci_len = dhcppkt_fetch ( dhcppkt, DHCP_VENDOR_CLASS_ID,
+                                 vci, sizeof ( vci ) );
+       has_pxeclient = ( ( vci_len >= ( int ) sizeof ( vci ) ) &&
+                         ( strncmp ( "PXEClient", vci, sizeof (vci) ) == 0 ));
+       if ( has_pxeclient )
+               DBGC ( dhcp, " pxe" );
+
+       /* Identify priority */
+       dhcppkt_fetch ( dhcppkt, DHCP_EB_PRIORITY, &priority,
+                       sizeof ( priority ) );
+       if ( priority )
+               DBGC ( dhcp, " pri %d", priority );
+
+       /* Identify ignore-PXE flag */
+       dhcppkt_fetch ( dhcppkt, DHCP_EB_NO_PXEDHCP, &no_pxedhcp,
+                       sizeof ( no_pxedhcp ) );
+       if ( no_pxedhcp )
+               DBGC ( dhcp, " nopxe" );
+       DBGC ( dhcp, "\n" );
+
+       /* Select as DHCP offer, if applicable */
+       if ( ip.s_addr && ( peer->sin_port == htons ( BOOTPS_PORT ) ) &&
+            ( ( msgtype == DHCPOFFER ) || ( ! msgtype /* BOOTP */ ) ) &&
+            ( priority >= dhcp->priority ) ) {
+               dhcp->offer = ip;
+               dhcp->server = server_id;
+               dhcp->priority = priority;
+               dhcp->no_pxedhcp = no_pxedhcp;
+       }
+
+       /* Select as ProxyDHCP offer, if applicable */
+       if ( has_pxeclient && ( msgtype == DHCPOFFER ) &&
+            ( priority >= dhcp->proxy_priority ) ) {
+               dhcp->proxy_server = server_id;
+               dhcp->proxy_priority = priority;
+       }
+
+       /* We can exit the discovery state when we have a valid
+        * DHCPOFFER, and either:
+        *
+        *  o  The DHCPOFFER instructs us to ignore ProxyDHCPOFFERs, or
+        *  o  We have a valid ProxyDHCPOFFER, or
+        *  o  We have allowed sufficient time for ProxyDHCPOFFERs.
+        */
+
+       /* If we don't yet have a DHCPOFFER, do nothing */
+       if ( ! dhcp->offer.s_addr )
+               return;
+
+       /* If we can't yet transition to DHCPREQUEST, do nothing */
+       elapsed = ( currticks() - dhcp->start );
+       if ( ! ( dhcp->no_pxedhcp || dhcp->proxy_server.s_addr ||
+                ( elapsed > PROXYDHCP_MAX_TIMEOUT ) ) )
+               return;
+
+       /* Transition to DHCPREQUEST */
+       dhcp_set_state ( dhcp, &dhcp_state_request );
+}
 
 /**
- * Create DHCP setting block
+ * Handle timer expiry during DHCP discovery
  *
- * @v dhcphdr          DHCP packet
- * @v len              Length of DHCP packet
- * @ret dhcpset                DHCP settings block
+ * @v dhcp             DHCP session
  */
-static struct dhcp_settings * dhcpset_create ( const struct dhcphdr *dhcphdr,
-                                              size_t len ) {
-       struct dhcp_settings *dhcpset;
-       void *data;
-
-       dhcpset = zalloc ( sizeof ( *dhcpset ) + len );
-       if ( dhcpset ) {
-               data = ( ( ( void * ) dhcpset ) + sizeof ( *dhcpset ) );
-               memcpy ( data, dhcphdr, len );
-               dhcppkt_init ( &dhcpset->dhcppkt, data, len );
-               settings_init ( &dhcpset->settings,
-                               &dhcpset_settings_operations, &dhcpset->refcnt,
-                               DHCP_SETTINGS_NAME, 0 );
+static void dhcp_discovery_expired ( struct dhcp_session *dhcp ) {
+       unsigned long elapsed = ( currticks() - dhcp->start );
+
+       /* Give up waiting for ProxyDHCP before we reach the failure point */
+       if ( dhcp->offer.s_addr && ( elapsed > PROXYDHCP_MAX_TIMEOUT ) ) {
+               dhcp_set_state ( dhcp, &dhcp_state_request );
+               return;
        }
-       return dhcpset;
+
+       /* Otherwise, retransmit current packet */
+       dhcp_tx ( dhcp );
 }
 
-/** DHCP server address setting */
-struct setting dhcp_server_setting __setting = {
-       .name = "dhcp-server",
-       .description = "DHCP server address",
-       .tag = DHCP_SERVER_IDENTIFIER,
-       .type = &setting_type_ipv4,
+/** DHCP discovery state operations */
+static struct dhcp_session_state dhcp_state_discover = {
+       .name                   = "discovery",
+       .tx                     = dhcp_discovery_tx,
+       .rx                     = dhcp_discovery_rx,
+       .expired                = dhcp_discovery_expired,
+       .tx_msgtype             = DHCPDISCOVER,
+       .apply_min_timeout      = 1,
 };
 
-/****************************************************************************
+/**
+ * Construct transmitted packet for DHCP request
  *
- * DHCP session
+ * @v dhcp             DHCP session
+ * @v dhcppkt          DHCP packet
+ * @v peer             Destination address
+ */
+static int dhcp_request_tx ( struct dhcp_session *dhcp,
+                            struct dhcp_packet *dhcppkt,
+                            struct sockaddr_in *peer ) {
+       int rc;
+
+       DBGC ( dhcp, "DHCP %p DHCPREQUEST to %s:%d",
+              dhcp, inet_ntoa ( dhcp->server ), BOOTPS_PORT );
+       DBGC ( dhcp, " for %s\n", inet_ntoa ( dhcp->offer ) );
+
+       /* Set server ID */
+       if ( ( rc = dhcppkt_store ( dhcppkt, DHCP_SERVER_IDENTIFIER,
+                                   &dhcp->server,
+                                   sizeof ( dhcp->server ) ) ) != 0 )
+               return rc;
+
+       /* Set requested IP address */
+       if ( ( rc = dhcppkt_store ( dhcppkt, DHCP_REQUESTED_ADDRESS,
+                                   &dhcp->offer,
+                                   sizeof ( dhcp->offer ) ) ) != 0 )
+               return rc;
+
+       /* Set server address */
+       peer->sin_addr.s_addr = INADDR_BROADCAST;
+       peer->sin_port = htons ( BOOTPS_PORT );
+
+       return 0;
+}
+
+/**
+ * Handle received packet during DHCP request
+ *
+ * @v dhcp             DHCP session
+ * @v dhcppkt          DHCP packet
+ * @v peer             DHCP server address
+ * @v msgtype          DHCP message type
+ */
+static void dhcp_request_rx ( struct dhcp_session *dhcp,
+                             struct dhcp_packet *dhcppkt,
+                             struct sockaddr_in *peer, uint8_t msgtype ) {
+       struct in_addr server_id = { 0 };
+       struct in_addr ip;
+       struct settings *parent;
+       int rc;
+
+       DBGC ( dhcp, "DHCP %p %s from %s:%d", dhcp,
+              dhcp_msgtype_name ( msgtype ), inet_ntoa ( peer->sin_addr ),
+              ntohs ( peer->sin_port ) );
+
+       /* Identify server ID */
+       dhcppkt_fetch ( dhcppkt, DHCP_SERVER_IDENTIFIER,
+                       &server_id, sizeof ( server_id ) );
+       if ( server_id.s_addr != peer->sin_addr.s_addr )
+               DBGC ( dhcp, " (%s)", inet_ntoa ( server_id ) );
+
+       /* Identify leased IP address */
+       ip = dhcppkt->dhcphdr->yiaddr;
+       if ( ip.s_addr )
+               DBGC ( dhcp, " for %s", inet_ntoa ( ip ) );
+       DBGC ( dhcp, "\n" );
+
+       /* Filter out unacceptable responses */
+       if ( peer->sin_port != htons ( BOOTPS_PORT ) )
+               return;
+       if ( msgtype /* BOOTP */ && ( msgtype != DHCPACK ) )
+               return;
+       if ( server_id.s_addr != dhcp->server.s_addr )
+               return;
+
+       /* Record assigned address */
+       dhcp->local.sin_addr = ip;
+
+       /* Register settings */
+       parent = netdev_settings ( dhcp->netdev );
+       if ( ( rc = register_settings ( &dhcppkt->settings, parent ) ) != 0 ){
+               DBGC ( dhcp, "DHCP %p could not register settings: %s\n",
+                      dhcp, strerror ( rc ) );
+               dhcp_finished ( dhcp, rc );
+               return;
+       }
+
+       /* Start ProxyDHCPREQUEST if applicable */
+       if ( dhcp->proxy_server.s_addr && ( ! dhcp->no_pxedhcp ) ) {
+               dhcp_set_state ( dhcp, &dhcp_state_proxy );
+               return;
+       }
+
+       /* Terminate DHCP */
+       dhcp_finished ( dhcp, 0 );
+}
+
+/**
+ * Handle timer expiry during DHCP discovery
  *
+ * @v dhcp             DHCP session
  */
+static void dhcp_request_expired ( struct dhcp_session *dhcp ) {
 
-/** DHCP session states */
-enum dhcp_session_state {
-       /** Sending DHCPDISCOVERs, collecting DHCPOFFERs and ProxyDHCPOFFERs */
-       DHCP_STATE_DISCOVER = 0,
-       /** Sending DHCPREQUESTs, waiting for DHCPACK */
-       DHCP_STATE_REQUEST,
-       /** Sending ProxyDHCPREQUESTs, waiting for ProxyDHCPACK */
-       DHCP_STATE_PROXYREQUEST,
-       /** Sending BootServerDHCPREQUESTs, waiting for BootServerDHCPACK */
-       DHCP_STATE_BSREQUEST,
+       /* Retransmit current packet */
+       dhcp_tx ( dhcp );
+}
+
+/** DHCP request state operations */
+static struct dhcp_session_state dhcp_state_request = {
+       .name                   = "request",
+       .tx                     = dhcp_request_tx,
+       .rx                     = dhcp_request_rx,
+       .expired                = dhcp_request_expired,
+       .tx_msgtype             = DHCPREQUEST,
+       .apply_min_timeout      = 0,
 };
 
 /**
- * Name a DHCP session state
+ * Construct transmitted packet for ProxyDHCP request
+ *
+ * @v dhcp             DHCP session
+ * @v dhcppkt          DHCP packet
+ * @v peer             Destination address
+ */
+static int dhcp_proxy_tx ( struct dhcp_session *dhcp,
+                          struct dhcp_packet *dhcppkt,
+                          struct sockaddr_in *peer ) {
+       int rc;
+
+       DBGC ( dhcp, "DHCP %p ProxyDHCP REQUEST to %s:%d\n",
+              dhcp, inet_ntoa ( dhcp->proxy_server ), PXE_PORT );
+
+       /* Set server ID */
+       if ( ( rc = dhcppkt_store ( dhcppkt, DHCP_SERVER_IDENTIFIER,
+                                   &dhcp->proxy_server,
+                                   sizeof ( dhcp->proxy_server ) ) ) != 0 )
+               return rc;
+
+       /* Set server address */
+       peer->sin_addr = dhcp->proxy_server;
+       peer->sin_port = htons ( PXE_PORT );
+
+       return 0;
+}
+
+/**
+ * Handle received packet during ProxyDHCP request
  *
- * @v state            DHCP session state
- * @ret string         DHCP session state name
+ * @v dhcp             DHCP session
+ * @v dhcppkt          DHCP packet
+ * @v peer             DHCP server address
+ * @v msgtype          DHCP message type
  */
-static inline const char * dhcp_state_name ( enum dhcp_session_state state ) {
-       switch ( state ) {
-       case DHCP_STATE_DISCOVER:       return "DHCPDISCOVER";
-       case DHCP_STATE_REQUEST:        return "DHCPREQUEST";
-       case DHCP_STATE_PROXYREQUEST:   return "ProxyDHCPREQUEST";
-       case DHCP_STATE_BSREQUEST:      return "BootServerREQUEST";
-       default:                        return "<invalid>";
+static void dhcp_proxy_rx ( struct dhcp_session *dhcp,
+                           struct dhcp_packet *dhcppkt,
+                           struct sockaddr_in *peer, uint8_t msgtype ) {
+       struct in_addr server_id = { 0 };
+       int rc;
+
+       DBGC ( dhcp, "DHCP %p %s from %s:%d", dhcp,
+              dhcp_msgtype_name ( msgtype ), inet_ntoa ( peer->sin_addr ),
+              ntohs ( peer->sin_port ) );
+
+       /* Identify server ID */
+       dhcppkt_fetch ( dhcppkt, DHCP_SERVER_IDENTIFIER,
+                       &server_id, sizeof ( server_id ) );
+       if ( server_id.s_addr != peer->sin_addr.s_addr )
+               DBGC ( dhcp, " (%s)", inet_ntoa ( server_id ) );
+       DBGC ( dhcp, "\n" );
+
+       /* Filter out unacceptable responses */
+       if ( peer->sin_port != htons ( PXE_PORT ) )
+               return;
+       if ( msgtype != DHCPACK )
+               return;
+       if ( server_id.s_addr /* Linux PXE server omits server ID */ &&
+            ( server_id.s_addr != dhcp->proxy_server.s_addr ) )
+               return;
+
+       /* Register settings */
+       dhcppkt->settings.name = PROXYDHCP_SETTINGS_NAME;
+       if ( ( rc = register_settings ( &dhcppkt->settings, NULL ) ) != 0 ) {
+               DBGC ( dhcp, "DHCP %p could not register settings: %s\n",
+                      dhcp, strerror ( rc ) );
+               dhcp_finished ( dhcp, rc );
+               return;
        }
+
+       /* Terminate DHCP */
+       dhcp_finished ( dhcp, 0 );
 }
 
-/** A DHCP session */
-struct dhcp_session {
-       /** Reference counter */
-       struct refcnt refcnt;
-       /** Job control interface */
-       struct job_interface job;
-       /** Data transfer interface */
-       struct xfer_interface xfer;
+/**
+ * Handle timer expiry during ProxyDHCP request
+ *
+ * @v dhcp             DHCP session
+ */
+static void dhcp_proxy_expired ( struct dhcp_session *dhcp ) {
+       unsigned long elapsed = ( currticks() - dhcp->start );
 
-       /** Network device being configured */
-       struct net_device *netdev;
+       /* Give up waiting for ProxyDHCP before we reach the failure point */
+       if ( elapsed > PROXYDHCP_MAX_TIMEOUT ) {
+               dhcp_finished ( dhcp, 0 );
+               return;
+       }
 
-       /** State of the session
-        *
-        * This is a value for the @c DHCP_MESSAGE_TYPE option
-        * (e.g. @c DHCPDISCOVER).
-        */
-       enum dhcp_session_state state;
-       /** DHCPOFFER obtained during DHCPDISCOVER containing IP address */
-       struct dhcp_settings *dhcpoffer;
-       /** DHCPOFFER obtained during DHCPDISCOVER containing "PXEClient" */
-       struct dhcp_settings *pxedhcpoffer;
-       /** DHCPACK obtained during DHCPREQUEST containing IP address */
-       struct dhcp_settings *dhcpack;
-       /** DHCPACK obtained during DHCPREQUEST or ProxyDHCPREQUEST
-        * containing "PXEClient"
-        */
-       struct dhcp_settings *pxedhcpack;
-       /** BootServerDHCPACK obtained during BootServerDHCPREQUEST */
-       struct dhcp_settings *bsdhcpack;
-       /** PXE boot menu item */
-       struct dhcp_pxe_boot_menu_item menu_item;
+       /* Retransmit current packet */
+       dhcp_tx ( dhcp );
+}
 
-       /** Retransmission timer */
-       struct retry_timer timer;
-       /** Start time of the current state (in ticks) */
-       unsigned long start;
+/** ProxyDHCP request state operations */
+static struct dhcp_session_state dhcp_state_proxy = {
+       .name                   = "ProxyDHCP",
+       .tx                     = dhcp_proxy_tx,
+       .rx                     = dhcp_proxy_rx,
+       .expired                = dhcp_proxy_expired,
+       .tx_msgtype             = DHCPREQUEST,
+       .apply_min_timeout      = 0,
 };
 
 /**
- * Free DHCP session
+ * Construct transmitted packet for PXE Boot Server Discovery
  *
- * @v refcnt           Reference counter
+ * @v dhcp             DHCP session
+ * @v dhcppkt          DHCP packet
+ * @v peer             Destination address
  */
-static void dhcp_free ( struct refcnt *refcnt ) {
-       struct dhcp_session *dhcp =
-               container_of ( refcnt, struct dhcp_session, refcnt );
+static int dhcp_pxebs_tx ( struct dhcp_session *dhcp,
+                          struct dhcp_packet *dhcppkt,
+                          struct sockaddr_in *peer ) {
+       struct dhcp_pxe_boot_menu_item menu_item = { 0, 0 };
+       int rc;
 
-       netdev_put ( dhcp->netdev );
-       dhcpset_put ( dhcp->dhcpoffer );
-       dhcpset_put ( dhcp->pxedhcpoffer );
-       dhcpset_put ( dhcp->dhcpack );
-       dhcpset_put ( dhcp->pxedhcpack );
-       dhcpset_put ( dhcp->bsdhcpack );
-       free ( dhcp );
+       DBGC ( dhcp, "DHCP %p PXEBS REQUEST to %s:%d for type %d\n",
+              dhcp, inet_ntoa ( dhcp->pxe_server ), PXE_PORT,
+              ntohs ( dhcp->pxe_type ) );
+
+       /* Set boot menu item */
+       menu_item.type = dhcp->pxe_type;
+       if ( ( rc = dhcppkt_store ( dhcppkt, DHCP_PXE_BOOT_MENU_ITEM,
+                                   &menu_item, sizeof ( menu_item ) ) ) != 0 )
+               return rc;
+
+       /* Set server address */
+       peer->sin_addr = dhcp->pxe_server;
+       peer->sin_port = htons ( PXE_PORT );
+
+       return 0;
 }
 
 /**
- * Mark DHCP session as complete
+ * Handle received packet during PXE Boot Server Discovery
  *
  * @v dhcp             DHCP session
- * @v rc               Return status code
+ * @v dhcppkt          DHCP packet
+ * @v peer             DHCP server address
+ * @v msgtype          DHCP message type
  */
-static void dhcp_finished ( struct dhcp_session *dhcp, int rc ) {
+static void dhcp_pxebs_rx ( struct dhcp_session *dhcp,
+                           struct dhcp_packet *dhcppkt,
+                           struct sockaddr_in *peer, uint8_t msgtype ) {
+       struct dhcp_pxe_boot_menu_item menu_item = { 0, 0 };
+       int rc;
 
-       /* Block futher incoming messages */
-       job_nullify ( &dhcp->job );
-       xfer_nullify ( &dhcp->xfer );
+       DBGC ( dhcp, "DHCP %p %s from %s:%d", dhcp,
+              dhcp_msgtype_name ( msgtype ), inet_ntoa ( peer->sin_addr ),
+              ntohs ( peer->sin_port ) );
 
-       /* Stop retry timer */
-       stop_timer ( &dhcp->timer );
+       /* Identify boot menu item */
+       dhcppkt_fetch ( dhcppkt, DHCP_PXE_BOOT_MENU_ITEM,
+                       &menu_item, sizeof ( menu_item ) );
+       if ( menu_item.type )
+               DBGC ( dhcp, " for type %d", ntohs ( menu_item.type ) );
+       DBGC ( dhcp, "\n" );
 
-       /* Free resources and close interfaces */
-       xfer_close ( &dhcp->xfer, rc );
-       job_done ( &dhcp->job, rc );
+       /* Filter out unacceptable responses */
+       if ( peer->sin_port != htons ( PXE_PORT ) )
+               return;
+       if ( msgtype != DHCPACK )
+               return;
+       if ( menu_item.type != dhcp->pxe_type )
+               return;
+
+       /* Register settings */
+       dhcppkt->settings.name = PXEBS_SETTINGS_NAME;
+       if ( ( rc = register_settings ( &dhcppkt->settings, NULL ) ) != 0 ) {
+               DBGC ( dhcp, "DHCP %p could not register settings: %s\n",
+                      dhcp, strerror ( rc ) );
+               dhcp_finished ( dhcp, rc );
+               return;
+       }
+
+       /* Terminate DHCP */
+       dhcp_finished ( dhcp, 0 );
+}
+
+/**
+ * Handle timer expiry during PXE Boot Server Discovery
+ *
+ * @v dhcp             DHCP session
+ */
+static void dhcp_pxebs_expired ( struct dhcp_session *dhcp ) {
+
+       /* Retransmit current packet */
+       dhcp_tx ( dhcp );
 }
 
+/** PXE Boot Server Discovery state operations */
+static struct dhcp_session_state dhcp_state_pxebs = {
+       .name                   = "PXEBS",
+       .tx                     = dhcp_pxebs_tx,
+       .rx                     = dhcp_pxebs_rx,
+       .expired                = dhcp_pxebs_expired,
+       .tx_msgtype             = DHCPREQUEST,
+       .apply_min_timeout      = 1,
+};
+
 /****************************************************************************
  *
- * Data transfer interface
+ * Packet construction
  *
  */
 
@@ -444,24 +761,23 @@ static void dhcp_finished ( struct dhcp_session *dhcp, int rc ) {
  * @v netdev           Network device
  * @v msgtype          DHCP message type
  * @v options          Initial options to include (or NULL)
+ * @v options_len      Length of initial options
  * @v data             Buffer for DHCP packet
  * @v max_len          Size of DHCP packet buffer
  * @ret rc             Return status code
  *
- * Creates a DHCP packet in the specified buffer, and fills out a @c
- * dhcp_packet structure.
+ * Creates a DHCP packet in the specified buffer, and initialise a
+ * DHCP packet structure.
  */
 int dhcp_create_packet ( struct dhcp_packet *dhcppkt,
                         struct net_device *netdev, uint8_t msgtype,
-                        struct dhcp_options *options, 
+                        const void *options, size_t options_len,
                         void *data, size_t max_len ) {
        struct dhcphdr *dhcphdr = data;
-       size_t options_len;
        unsigned int hlen;
        int rc;
 
        /* Sanity check */
-       options_len = ( options ? options->len : 0 );
        if ( max_len < ( sizeof ( *dhcphdr ) + options_len ) )
                return -ENOSPC;
 
@@ -481,7 +797,7 @@ int dhcp_create_packet ( struct dhcp_packet *dhcppkt,
        }
        dhcphdr->hlen = hlen;
        memcpy ( dhcphdr->chaddr, netdev->ll_addr, hlen );
-       memcpy ( dhcphdr->options, options->data, options_len );
+       memcpy ( dhcphdr->options, options, options_len );
 
        /* Initialise DHCP packet structure */
        memset ( dhcppkt, 0, sizeof ( *dhcppkt ) );
@@ -501,20 +817,17 @@ int dhcp_create_packet ( struct dhcp_packet *dhcppkt,
  * @v dhcppkt          DHCP packet structure to fill in
  * @v netdev           Network device
  * @v msgtype          DHCP message type
- * @v ciaddr           Client IP address, if applicable
- * @v server           Server identifier, if applicable
- * @v requested_ip     Requested address, if applicable
- * @v menu_item                PXE menu item, if applicable
+ * @v ciaddr           Client IP address
  * @v data             Buffer for DHCP packet
  * @v max_len          Size of DHCP packet buffer
  * @ret rc             Return status code
+ *
+ * Creates a DHCP request packet in the specified buffer, and
+ * initialise a DHCP packet structure.
  */
 int dhcp_create_request ( struct dhcp_packet *dhcppkt,
                          struct net_device *netdev, unsigned int msgtype,
-                         struct in_addr ciaddr, struct in_addr server,
-                         struct in_addr requested_ip,
-                         struct dhcp_pxe_boot_menu_item *menu_item,
-                         void *data, size_t max_len ) {
+                         struct in_addr ciaddr, void *data, size_t max_len ) {
        struct device_description *desc = &netdev->dev->desc;
        struct dhcp_netdev_desc dhcp_desc;
        struct dhcp_client_id client_id;
@@ -525,8 +838,9 @@ int dhcp_create_request ( struct dhcp_packet *dhcppkt,
 
        /* Create DHCP packet */
        if ( ( rc = dhcp_create_packet ( dhcppkt, netdev, msgtype,
-                                        &dhcp_request_options, data,
-                                        max_len ) ) != 0 ) {
+                                        dhcp_request_options_data,
+                                        sizeof ( dhcp_request_options_data ),
+                                        data, max_len ) ) != 0 ) {
                DBG ( "DHCP could not create DHCP packet: %s\n",
                      strerror ( rc ) );
                return rc;
@@ -535,25 +849,6 @@ int dhcp_create_request ( struct dhcp_packet *dhcppkt,
        /* Set client IP address */
        dhcppkt->dhcphdr->ciaddr = ciaddr;
 
-       /* Set server ID, if present */
-       if ( server.s_addr &&
-            ( ( rc = dhcppkt_store ( dhcppkt, DHCP_SERVER_IDENTIFIER,
-                                     &server, sizeof ( server ) ) ) != 0 ) ) {
-                       DBG ( "DHCP could not set server ID: %s\n",
-                             strerror ( rc ) );
-                       return rc;
-       }
-
-       /* Set requested IP address, if present */
-       if ( requested_ip.s_addr &&
-            ( ( rc = dhcppkt_store ( dhcppkt, DHCP_REQUESTED_ADDRESS,
-                                     &requested_ip,
-                                     sizeof ( requested_ip ) ) ) != 0 ) ) {
-               DBG ( "DHCP could not set requested address: %s\n",
-                     strerror ( rc ) );
-               return rc;
-       }
-
        /* Add options to identify the feature list */
        dhcp_features_len = ( dhcp_features_end - dhcp_features );
        if ( ( rc = dhcppkt_store ( dhcppkt, DHCP_EB_ENCAP, dhcp_features,
@@ -601,19 +896,15 @@ int dhcp_create_request ( struct dhcp_packet *dhcppkt,
                }
        }
 
-       /* Set PXE boot menu item, if present */
-       if ( menu_item && menu_item->type &&
-            ( ( rc = dhcppkt_store ( dhcppkt, DHCP_PXE_BOOT_MENU_ITEM,
-                                     menu_item,
-                                     sizeof ( *menu_item ) ) ) != 0 ) ) {
-               DBG ( "DHCP could not set PXE menu item: %s\n",
-                     strerror ( rc ) );
-               return rc;
-       }
-
        return 0;
 }
 
+/****************************************************************************
+ *
+ * Data transfer interface
+ *
+ */
+
 /**
  * Transmit DHCP request
  *
@@ -621,23 +912,17 @@ int dhcp_create_request ( struct dhcp_packet *dhcppkt,
  * @ret rc             Return status code
  */
 static int dhcp_tx ( struct dhcp_session *dhcp ) {
-       static struct sockaddr_in dest = {
-               .sin_family = AF_INET,
-               .sin_port = htons ( PXE_PORT ),
-       };
-       static struct sockaddr_in src = {
+       static struct sockaddr_in peer = {
                .sin_family = AF_INET,
-               .sin_port = htons ( BOOTPC_PORT ),
        };
        struct xfer_metadata meta = {
                .netdev = dhcp->netdev,
+               .src = ( struct sockaddr * ) &dhcp->local,
+               .dest = ( struct sockaddr * ) &peer,
        };
        struct io_buffer *iobuf;
+       uint8_t msgtype = dhcp->state->tx_msgtype;
        struct dhcp_packet dhcppkt;
-       struct in_addr ciaddr = { 0 };
-       struct in_addr server = { 0 };
-       struct in_addr requested_ip = { 0 };
-       unsigned int msgtype;
        int rc;
 
        /* Start retry timer.  Do this first so that failures to
@@ -645,93 +930,25 @@ static int dhcp_tx ( struct dhcp_session *dhcp ) {
         */
        start_timer ( &dhcp->timer );
 
-       /* Determine packet contents based on current state */
-       switch ( dhcp->state ) {
-       case DHCP_STATE_DISCOVER:
-               msgtype = DHCPDISCOVER;
-               break;
-       case DHCP_STATE_REQUEST:
-               assert ( dhcp->dhcpoffer );
-               msgtype = DHCPREQUEST;
-               dhcppkt_fetch ( &dhcp->dhcpoffer->dhcppkt,
-                               DHCP_SERVER_IDENTIFIER, &server,
-                               sizeof ( server ) );
-               requested_ip = dhcp->dhcpoffer->dhcppkt.dhcphdr->yiaddr;
-               break;
-       case DHCP_STATE_PROXYREQUEST:
-               assert ( dhcp->dhcpoffer );
-               assert ( dhcp->pxedhcpoffer );
-               assert ( dhcp->dhcpack );
-               msgtype = DHCPREQUEST;
-               ciaddr = dhcp->dhcpoffer->dhcppkt.dhcphdr->yiaddr;
-               dhcppkt_fetch ( &dhcp->pxedhcpoffer->dhcppkt,
-                               DHCP_SERVER_IDENTIFIER, &dest.sin_addr,
-                               sizeof ( dest.sin_addr ) );
-               meta.dest = ( struct sockaddr * ) &dest;
-               server = dest.sin_addr;
-               assert ( dest.sin_addr.s_addr );
-               assert ( ciaddr.s_addr );
-               break;
-       case DHCP_STATE_BSREQUEST:
-               assert ( dhcp->dhcpoffer );
-               assert ( dhcp->pxedhcpoffer );
-               assert ( dhcp->dhcpack );
-               assert ( dhcp->pxedhcpack );
-               msgtype = DHCPREQUEST;
-               ciaddr = dhcp->dhcpoffer->dhcppkt.dhcphdr->yiaddr;
-               dhcppkt_fetch ( &dhcp->pxedhcpack->dhcppkt,
-                               DHCP_PXE_BOOT_SERVER_MCAST,
-                               &dest.sin_addr, sizeof ( dest.sin_addr ) );
-               meta.dest = ( struct sockaddr * ) &dest;
-               assert ( dest.sin_addr.s_addr );
-               assert ( ciaddr.s_addr );
-               break;
-       default:
-               assert ( 0 );
-               return -EINVAL;
-       }
-
-       DBGC ( dhcp, "DHCP %p %s", dhcp, dhcp_msgtype_name ( msgtype ) );
-       if ( server.s_addr )
-               DBGC ( dhcp, " to %s", inet_ntoa ( server ) );
-       if ( meta.dest ) {
-               if ( dest.sin_addr.s_addr == server.s_addr ) {
-                       DBGC ( dhcp, ":%d (unicast)",
-                              ntohs ( dest.sin_port ) );
-               } else {
-                       DBGC ( dhcp, " via %s:%d", inet_ntoa ( dest.sin_addr ),
-                              ntohs ( dest.sin_port ) );
-               }
-       } else {
-               DBGC ( dhcp, " (broadcast)" );
-       }
-       if ( requested_ip.s_addr )
-               DBGC ( dhcp, " for %s", inet_ntoa ( requested_ip ) );
-       if ( dhcp->menu_item.type ) {
-               DBGC ( dhcp, " for item %04x",
-                      ntohs ( dhcp->menu_item.type ) );
-       }
-       DBGC ( dhcp, "\n" );
-
        /* Allocate buffer for packet */
        iobuf = xfer_alloc_iob ( &dhcp->xfer, DHCP_MIN_LEN );
        if ( ! iobuf )
                return -ENOMEM;
 
-       /* Create DHCP packet in temporary buffer */
+       /* Create basic DHCP packet in temporary buffer */
        if ( ( rc = dhcp_create_request ( &dhcppkt, dhcp->netdev, msgtype,
-                                         ciaddr, server, requested_ip,
-                                         &dhcp->menu_item, iobuf->data,
+                                         dhcp->local.sin_addr, iobuf->data,
                                          iob_tailroom ( iobuf ) ) ) != 0 ) {
                DBGC ( dhcp, "DHCP %p could not construct DHCP request: %s\n",
                       dhcp, strerror ( rc ) );
                goto done;
        }
 
-       /* Explicitly specify source address, if available. */
-       if ( ciaddr.s_addr ) {
-               src.sin_addr = ciaddr;
-               meta.src = ( struct sockaddr * ) &src;
+       /* Fill in packet based on current state */
+       if ( ( rc = dhcp->state->tx ( dhcp, &dhcppkt, &peer ) ) != 0 ) {
+               DBGC ( dhcp, "DHCP %p could not fill DHCP request: %s\n",
+                      dhcp, strerror ( rc ) );
+               goto done;
        }
 
        /* Transmit the packet */
@@ -749,471 +966,6 @@ static int dhcp_tx ( struct dhcp_session *dhcp ) {
        return rc;
 }
 
-/**
- * Prompt for PXE boot menu selection
- *
- * @v pxedhcpack       PXEDHCPACK packet
- * @ret rc             Return status code
- *
- * Note that a success return status indicates that the PXE boot menu
- * should be displayed.
- */
-static int dhcp_pxe_boot_menu_prompt ( struct dhcp_packet *pxedhcpack ) {
-       union {
-               uint8_t buf[80];
-               struct dhcp_pxe_boot_prompt prompt;
-       } u;
-       ssize_t slen;
-       unsigned long start;
-       int key;
-
-       /* Parse menu prompt */
-       memset ( &u, 0, sizeof ( u ) );
-       if ( ( slen = dhcppkt_fetch ( pxedhcpack, DHCP_PXE_BOOT_MENU_PROMPT,
-                                     &u, sizeof ( u ) ) ) <= 0 ) {
-               /* If prompt is not present, we should always display
-                * the menu.
-                */
-               return 0;
-       }
-
-       /* Display prompt, if applicable */
-       if ( u.prompt.timeout )
-               printf ( "\n%s\n", u.prompt.prompt );
-
-       /* Timeout==0xff means display menu immediately */
-       if ( u.prompt.timeout == 0xff )
-               return 0;
-
-       /* Wait for F8 or other key press */
-       start = currticks();
-       while ( ( currticks() - start ) <
-               ( u.prompt.timeout * TICKS_PER_SEC ) ) {
-               if ( iskey() ) {
-                       key = getkey();
-                       return ( ( key == KEY_F8 ) ? 0 : -ECANCELED );
-               }
-       }
-
-       return -ECANCELED;
-}
-
-/**
- * Perform PXE boot menu selection
- *
- * @v pxedhcpack       PXEDHCPACK packet
- * @v menu_item                PXE boot menu item to fill in
- * @ret rc             Return status code
- *
- * Note that a success return status indicates that a PXE boot menu
- * item has been selected, and that the DHCP session should perform a
- * boot server request/ack.
- */
-static int dhcp_pxe_boot_menu ( struct dhcp_packet *pxedhcpack,
-                               struct dhcp_pxe_boot_menu_item *menu_item ) {
-       uint8_t buf[256];
-       ssize_t slen;
-       size_t menu_len;
-       struct dhcp_pxe_boot_menu_item_desc *menu_item_desc;
-       size_t menu_item_desc_len;
-       struct {
-               uint16_t type;
-               char *desc;
-       } menu[PXE_BOOT_MENU_MAX_ITEMS];
-       size_t offset = 0;
-       unsigned int num_menu_items = 0;
-       unsigned int i;
-       unsigned int selected_menu_item;
-       int key;
-       int rc;
-
-       /* Check for boot menu */
-       memset ( &buf, 0, sizeof ( buf ) );
-       if ( ( slen = dhcppkt_fetch ( pxedhcpack, DHCP_PXE_BOOT_MENU,
-                                     &buf, sizeof ( buf ) ) ) <= 0 ) {
-               DBGC2 ( pxedhcpack, "PXEDHCPACK %p has no boot menu\n",
-                       pxedhcpack );
-               return slen;
-       }
-       menu_len = slen;
-
-       /* Parse boot menu */
-       while ( offset < menu_len ) {
-               menu_item_desc = ( ( void * ) ( buf + offset ) );
-               menu_item_desc_len = ( sizeof ( *menu_item_desc ) +
-                                      menu_item_desc->desc_len );
-               if ( ( offset + menu_item_desc_len ) > menu_len ) {
-                       DBGC ( pxedhcpack, "PXEDHCPACK %p has malformed "
-                              "boot menu\n", pxedhcpack );
-                       return -EINVAL;
-               }
-               menu[num_menu_items].type = menu_item_desc->type;
-               menu[num_menu_items].desc = menu_item_desc->desc;
-               /* Set type to 0; this ensures that the description
-                * for the previous menu item is NUL-terminated.
-                * (Final item is NUL-terminated anyway.)
-                */
-               menu_item_desc->type = 0;
-               offset += menu_item_desc_len;
-               num_menu_items++;
-               if ( num_menu_items == ( sizeof ( menu ) /
-                                        sizeof ( menu[0] ) ) ) {
-                       DBGC ( pxedhcpack, "PXEDHCPACK %p has too many "
-                              "menu items\n", pxedhcpack );
-                       /* Silently ignore remaining items */
-                       break;
-               }
-       }
-       if ( ! num_menu_items ) {
-               DBGC ( pxedhcpack, "PXEDHCPACK %p has no menu items\n",
-                      pxedhcpack );
-               return -EINVAL;
-       }
-
-       /* Default to first menu item */
-       menu_item->type = menu[0].type;
-
-       /* Prompt for menu, if necessary */
-       if ( ( rc = dhcp_pxe_boot_menu_prompt ( pxedhcpack ) ) != 0 ) {
-               /* Failure to display menu means we should just
-                * continue with the boot.
-                */
-               return 0;
-       }
-
-       /* Display menu */
-       for ( i = 0 ; i < num_menu_items ; i++ ) {
-               printf ( "%c. %s\n", ( 'A' + i ), menu[i].desc );
-       }
-
-       /* Obtain selection */
-       while ( 1 ) {
-               key = getkey();
-               selected_menu_item = ( toupper ( key ) - 'A' );
-               if ( selected_menu_item < num_menu_items ) {
-                       menu_item->type = menu[selected_menu_item].type;
-                       return 0;
-               }
-       }
-}
-
-/**
- * Transition to new DHCP session state
- *
- * @v dhcp             DHCP session
- * @v state            New session state
- */
-static void dhcp_set_state ( struct dhcp_session *dhcp,
-                            enum dhcp_session_state state ) {
-       DBGC ( dhcp, "DHCP %p entering %s state\n",
-              dhcp, dhcp_state_name ( state ) );
-       dhcp->state = state;
-       dhcp->start = currticks();
-       dhcp->timer.min_timeout = 0;
-       start_timer_nodelay ( &dhcp->timer );
-}
-
-/**
- * Transition to next DHCP state
- *
- * @v dhcp             DHCP session
- */
-static void dhcp_next_state ( struct dhcp_session *dhcp ) {
-
-       stop_timer ( &dhcp->timer );
-
-       switch ( dhcp->state ) {
-       case DHCP_STATE_DISCOVER:
-               dhcp_set_state ( dhcp, DHCP_STATE_REQUEST );
-               break;
-       case DHCP_STATE_REQUEST:
-               if ( dhcp->pxedhcpoffer ) {
-                       /* Store DHCPACK as PXEDHCPACK.  This handles
-                        * the case in which the DHCP server itself
-                        * responds with "PXEClient" and PXE options
-                        * but there is no ProxyDHCP server resident
-                        * on the machine.
-                        */
-                       dhcp->pxedhcpack = dhcpset_get ( dhcp->dhcpack );
-                       dhcp_set_state ( dhcp, DHCP_STATE_PROXYREQUEST );
-                       break;
-               }
-               /* Fall through */
-       case DHCP_STATE_PROXYREQUEST:
-               if ( dhcp->pxedhcpack ) {
-                       dhcp_pxe_boot_menu ( &dhcp->pxedhcpack->dhcppkt,
-                                            &dhcp->menu_item );
-                       if ( dhcp->menu_item.type ) {
-                               dhcp_set_state ( dhcp, DHCP_STATE_BSREQUEST );
-                               break;
-                       }
-               }
-               /* Fall through */
-       case DHCP_STATE_BSREQUEST:
-               dhcp_finished ( dhcp, 0 );
-               break;
-       default:
-               assert ( 0 );
-               return;
-       }
-
-}
-
-/**
- * Store received DHCPOFFER
- *
- * @v dhcp             DHCP session
- * @v dhcpoffer                Received DHCPOFFER
- * @v stored_dhcpoffer Location to store DHCPOFFER
- *
- * The DHCPOFFER will be stored in place of the existing stored
- * DHCPOFFER if its priority is equal to or greater than the stored
- * DHCPOFFER.
- */
-static void dhcp_store_dhcpoffer ( struct dhcp_session *dhcp,
-                                  struct dhcp_settings *dhcpoffer,
-                                  struct dhcp_settings **stored_dhcpoffer ) {
-       uint8_t stored_priority = 0;
-       uint8_t priority = 0;
-
-       /* Get priorities of the two DHCPOFFERs */
-       if ( *stored_dhcpoffer ) {
-               dhcppkt_fetch ( &(*stored_dhcpoffer)->dhcppkt,
-                               DHCP_EB_PRIORITY, &stored_priority,
-                               sizeof ( stored_priority ) );
-       }
-       dhcppkt_fetch ( &dhcpoffer->dhcppkt, DHCP_EB_PRIORITY, &priority,
-                       sizeof ( priority ) );
-
-       /* Replace stored offer only if priority is equal or greater */
-       if ( priority >= stored_priority ) {
-               if ( *stored_dhcpoffer ) {
-                       DBGC ( dhcp, "DHCP %p stored DHCPOFFER %p discarded\n",
-                              dhcp, *stored_dhcpoffer );
-               }
-               DBGC ( dhcp, "DHCP %p DHCPOFFER %p stored\n",
-                      dhcp, dhcpoffer );
-               dhcpset_put ( *stored_dhcpoffer );
-               *stored_dhcpoffer = dhcpset_get ( dhcpoffer );
-       }
-}
-
-/**
- * Handle received DHCPOFFER
- *
- * @v dhcp             DHCP session
- * @v dhcpoffer                Received DHCPOFFER
- */
-static void dhcp_rx_dhcpoffer ( struct dhcp_session *dhcp,
-                               struct dhcp_settings *dhcpoffer ) {
-       struct in_addr server_id = { 0 };
-       char vci[9]; /* "PXEClient" */
-       int len;
-       uint8_t ignore_pxe = 0;
-       unsigned long elapsed;
-
-       /* Check for presence of DHCP server ID */
-       if ( dhcppkt_fetch ( &dhcpoffer->dhcppkt, DHCP_SERVER_IDENTIFIER,
-                            &server_id, sizeof ( server_id ) )
-            != sizeof ( server_id ) ) {
-               DBGC ( dhcp, "DHCP %p DHCPOFFER %p missing server ID\n",
-                      dhcp, dhcpoffer );
-               /* Could be a valid BOOTP offer; do not abort processing */
-       }
-
-       /* If there is an IP address, it's a normal DHCPOFFER */
-       if ( dhcpoffer->dhcppkt.dhcphdr->yiaddr.s_addr != 0 ) {
-               DBGC ( dhcp, "DHCP %p DHCPOFFER %p from %s",
-                      dhcp, dhcpoffer, inet_ntoa ( server_id ) );
-               DBGC ( dhcp, " has IP %s\n",
-                      inet_ntoa ( dhcpoffer->dhcppkt.dhcphdr->yiaddr ) );
-               dhcp_store_dhcpoffer ( dhcp, dhcpoffer, &dhcp->dhcpoffer );
-       }
-
-       /* If there is a "PXEClient" vendor class ID, it's a
-        * PXEDHCPOFFER.  Note that it could be both a normal
-        * DHCPOFFER and a PXEDHCPOFFER.
-        */
-       len = dhcppkt_fetch ( &dhcpoffer->dhcppkt, DHCP_VENDOR_CLASS_ID,
-                             vci, sizeof ( vci ) );
-       if ( ( server_id.s_addr != 0 ) &&
-            ( len >= ( int ) sizeof ( vci ) ) &&
-            ( strncmp ( "PXEClient", vci, sizeof ( vci ) ) == 0 ) ) {
-               DBGC ( dhcp, "DHCP %p DHCPOFFER %p from %s has PXE options\n",
-                      dhcp, dhcpoffer, inet_ntoa ( server_id ) );
-               dhcp_store_dhcpoffer ( dhcp, dhcpoffer,
-                                      &dhcp->pxedhcpoffer );
-       }
-
-       /* We can transition to making the DHCPREQUEST when we have a
-        * valid DHCPOFFER, and either:
-        *
-        *  o  The DHCPOFFER instructs us to ignore PXEDHCPOFFERs, or
-        *  o  We have a valid PXEDHCPOFFER, or
-         *  o  We have allowed sufficient time for ProxyDHCPOFFERs.
-        */
-
-       /* If we don't yet have a DHCPOFFER, do nothing */
-       if ( ! dhcp->dhcpoffer )
-               return;
-
-       /* If the DHCPOFFER instructs us to ignore PXEDHCP, discard
-        * any PXEDHCPOFFER
-        */
-       dhcppkt_fetch ( &dhcp->dhcpoffer->dhcppkt, DHCP_EB_NO_PXEDHCP,
-                       &ignore_pxe, sizeof ( ignore_pxe ) );
-       if ( ignore_pxe && dhcp->pxedhcpoffer ) {
-               DBGC ( dhcp, "DHCP %p discarding PXEDHCPOFFER\n", dhcp );
-               dhcpset_put ( dhcp->pxedhcpoffer );
-               dhcp->pxedhcpoffer = NULL;
-       }
-
-       /* If we can't yet transition to DHCPREQUEST, do nothing */
-       elapsed = ( currticks() - dhcp->start );
-       if ( ! ( ignore_pxe || dhcp->pxedhcpoffer ||
-                ( elapsed > PROXYDHCP_WAIT_TIME ) ) )
-               return;
-
-       /* Transition to DHCPREQUEST */
-       dhcp_next_state ( dhcp );
-}
-
-/**
- * Store received DHCPACK
- *
- * @v dhcp             DHCP session
- * @v dhcpack          Received DHCPACK
- *
- * The DHCPACK will be registered as a settings block.
- */
-static int dhcp_store_dhcpack ( struct dhcp_session *dhcp,
-                               struct dhcp_settings *dhcpack,
-                               struct settings *parent ) {
-       struct settings *settings = &dhcpack->settings;
-       struct settings *old_settings;
-       int rc;
-
-       /* Unregister any old settings obtained via DHCP */
-       if ( ( old_settings = find_child_settings ( parent, settings->name ) ))
-               unregister_settings ( old_settings );
-
-       /* Register new settings */
-       if ( ( rc = register_settings ( settings, parent ) ) != 0 ) {
-               DBGC ( dhcp, "DHCP %p could not register settings: %s\n",
-                      dhcp, strerror ( rc ) );
-               dhcp_finished ( dhcp, rc ); /* This is a fatal error */
-               return rc;
-       }
-
-       return 0;
-}
-
-/**
- * Handle received DHCPACK
- *
- * @v dhcp             DHCP session
- * @v dhcpack          Received DHCPACK
- */
-static void dhcp_rx_dhcpack ( struct dhcp_session *dhcp,
-                             struct dhcp_settings *dhcpack ) {
-       struct settings *parent;
-       struct in_addr offer_server_id = { 0 };
-       struct in_addr ack_server_id = { 0 };
-       int rc;
-
-       /* Verify server ID matches */
-       assert ( dhcp->dhcpoffer != NULL );
-       dhcppkt_fetch ( &dhcp->dhcpoffer->dhcppkt, DHCP_SERVER_IDENTIFIER,
-                       &offer_server_id, sizeof ( offer_server_id ) );
-       dhcppkt_fetch ( &dhcpack->dhcppkt, DHCP_SERVER_IDENTIFIER,
-                       &ack_server_id, sizeof ( ack_server_id ) );
-       if ( offer_server_id.s_addr != ack_server_id.s_addr ) {
-               DBGC ( dhcp, "DHCP %p ignoring DHCPACK with wrong server ID "
-                      "%s\n", dhcp, inet_ntoa ( ack_server_id ) );
-               return;
-       }
-
-       /* Record DHCPACK */
-       assert ( dhcp->dhcpack == NULL );
-       dhcp->dhcpack = dhcpset_get ( dhcpack );
-
-       /* Register settings */
-       parent = netdev_settings ( dhcp->netdev );
-       if ( ( rc = dhcp_store_dhcpack ( dhcp, dhcpack, parent ) ) != 0 )
-               return;
-
-       /* Transition to next state */
-       dhcp_next_state ( dhcp );
-}
-
-/**
- * Handle received ProxyDHCPACK
- *
- * @v dhcp             DHCP session
- * @v proxydhcpack     Received ProxyDHCPACK
- */
-static void dhcp_rx_proxydhcpack ( struct dhcp_session *dhcp,
-                                  struct dhcp_settings *proxydhcpack ) {
-       struct in_addr offer_server_id = { 0 };
-       struct in_addr ack_server_id = { 0 };
-       int rc;
-
-       /* Verify server ID matches, if present */
-       assert ( dhcp->pxedhcpoffer != NULL );
-       if ( ( rc = dhcppkt_fetch ( &proxydhcpack->dhcppkt,
-                                   DHCP_SERVER_IDENTIFIER, &ack_server_id,
-                                   sizeof ( ack_server_id ) ) ) > 0 ) {
-               dhcppkt_fetch ( &dhcp->pxedhcpoffer->dhcppkt,
-                               DHCP_SERVER_IDENTIFIER, &offer_server_id,
-                               sizeof ( offer_server_id ) );
-               if ( offer_server_id.s_addr != ack_server_id.s_addr ) {
-                       DBGC ( dhcp, "DHCP %p ignoring ProxyDHCPACK with "
-                              "wrong server ID %s\n",
-                              dhcp, inet_ntoa ( ack_server_id ) );
-                       return;
-               }
-       }
-
-       /* Rename settings */
-       proxydhcpack->settings.name = PROXYDHCP_SETTINGS_NAME;
-
-       /* Record ProxyDHCPACK as PXEDHCPACK */
-       dhcpset_put ( dhcp->pxedhcpack );
-       dhcp->pxedhcpack = dhcpset_get ( proxydhcpack );
-
-       /* Register settings */
-       if ( ( rc = dhcp_store_dhcpack ( dhcp, proxydhcpack, NULL ) ) != 0 )
-               return;
-
-       /* Transition to next state */
-       dhcp_next_state ( dhcp );
-}
-
-/**
- * Handle received BootServerDHCPACK
- *
- * @v dhcp             DHCP session
- * @v bsdhcpack                Received BootServerDHCPACK
- */
-static void dhcp_rx_bsdhcpack ( struct dhcp_session *dhcp,
-                               struct dhcp_settings *bsdhcpack ) {
-       int rc;
-
-       /* Rename settings */
-       bsdhcpack->settings.name = BSDHCP_SETTINGS_NAME;
-
-       /* Record BootServerDHCPACK */
-       assert ( dhcp->bsdhcpack == NULL );
-       dhcp->bsdhcpack = dhcpset_get ( bsdhcpack );
-
-       /* Register settings */
-       if ( ( rc = dhcp_store_dhcpack ( dhcp, bsdhcpack, NULL ) ) != 0 )
-               return;
-
-       /* Transition to next state */
-       dhcp_next_state ( dhcp );
-}
-
 /**
  * Receive new data
  *
@@ -1227,9 +979,9 @@ static int dhcp_deliver_iob ( struct xfer_interface *xfer,
                              struct xfer_metadata *meta ) {
        struct dhcp_session *dhcp =
                container_of ( xfer, struct dhcp_session, xfer );
-       struct sockaddr_in *sin_src;
-       unsigned int src_port;
-       struct dhcp_settings *dhcpset;
+       struct sockaddr_in *peer;
+       size_t data_len;
+       struct dhcp_packet *dhcppkt;
        struct dhcphdr *dhcphdr;
        uint8_t msgtype = 0;
        int rc = 0;
@@ -1247,63 +999,43 @@ static int dhcp_deliver_iob ( struct xfer_interface *xfer,
                rc = -EINVAL;
                goto err_no_src;
        }
-       sin_src = ( struct sockaddr_in * ) meta->src;
-       src_port = sin_src->sin_port;
+       peer = ( struct sockaddr_in * ) meta->src;
 
-       /* Convert packet into a DHCP settings block */
-       dhcpset = dhcpset_create ( iobuf->data, iob_len ( iobuf ) );
-       if ( ! dhcpset ) {
-               DBGC ( dhcp, "DHCP %p could not store DHCP packet\n", dhcp );
+       /* Create a DHCP packet containing the I/O buffer contents.
+        * Whilst we could just use the original buffer in situ, that
+        * would waste the unused space in the packet buffer, and also
+        * waste a relatively scarce fully-aligned I/O buffer.
+        */
+       data_len = iob_len ( iobuf );
+       dhcppkt = zalloc ( sizeof ( *dhcppkt ) + data_len );
+       if ( ! dhcppkt ) {
                rc = -ENOMEM;
-               goto err_dhcpset_create;
+               goto err_alloc_dhcppkt;
        }
-       dhcphdr = dhcpset->dhcppkt.dhcphdr;
+       dhcphdr = ( ( ( void * ) dhcppkt ) + sizeof ( *dhcppkt ) );
+       memcpy ( dhcphdr, iobuf->data, data_len );
+       dhcppkt_init ( dhcppkt, dhcphdr, data_len );
 
        /* Identify message type */
-       dhcppkt_fetch ( &dhcpset->dhcppkt, DHCP_MESSAGE_TYPE, &msgtype,
+       dhcppkt_fetch ( dhcppkt, DHCP_MESSAGE_TYPE, &msgtype,
                        sizeof ( msgtype ) );
-       DBGC ( dhcp, "DHCP %p %s %p from %s:%d\n", dhcp,
-              dhcp_msgtype_name ( msgtype ), dhcpset,
-              inet_ntoa ( sin_src->sin_addr ), ntohs ( src_port ) );
 
        /* Check for matching transaction ID */
        if ( dhcphdr->xid != dhcp_xid ( dhcp->netdev ) ) {
-               DBGC ( dhcp, "DHCP %p %s %p has bad transaction ID\n",
-                      dhcp, dhcp_msgtype_name ( msgtype ), dhcpset );
+               DBGC ( dhcp, "DHCP %p %s from %s:%d has bad transaction "
+                      "ID\n", dhcp, dhcp_msgtype_name ( msgtype ),
+                      inet_ntoa ( peer->sin_addr ),
+                      ntohs ( peer->sin_port ) );
                rc = -EINVAL;
                goto err_xid;
        };
 
        /* Handle packet based on current state */
-       switch ( dhcp->state ) {
-       case DHCP_STATE_DISCOVER:
-               if ( ( ( msgtype == DHCPOFFER ) || ( msgtype == DHCPNONE ) ) &&
-                    ( src_port == htons ( BOOTPS_PORT ) ) )
-                       dhcp_rx_dhcpoffer ( dhcp, dhcpset );
-               break;
-       case DHCP_STATE_REQUEST:
-               if ( ( ( msgtype == DHCPACK ) || ( msgtype == DHCPNONE ) ) &&
-                    ( src_port == htons ( BOOTPS_PORT ) ) )
-                       dhcp_rx_dhcpack ( dhcp, dhcpset );
-               break;
-       case DHCP_STATE_PROXYREQUEST:
-               if ( ( msgtype == DHCPACK ) &&
-                    ( src_port == htons ( PXE_PORT ) ) )
-                       dhcp_rx_proxydhcpack ( dhcp, dhcpset );
-               break;
-       case DHCP_STATE_BSREQUEST:
-               if ( ( msgtype == DHCPACK ) &&
-                    ( src_port == htons ( PXE_PORT ) ) )
-                       dhcp_rx_bsdhcpack ( dhcp, dhcpset );
-               break;
-       default:
-               assert ( 0 );
-               break;
-       }
+       dhcp->state->rx ( dhcp, dhcppkt, peer, msgtype );
 
  err_xid:
-       dhcpset_put ( dhcpset );
- err_dhcpset_create:
+       dhcppkt_put ( dhcppkt );
+ err_alloc_dhcppkt:
  err_no_src:
  err_no_meta:
        free_iob ( iobuf );
@@ -1329,7 +1061,6 @@ static struct xfer_interface_operations dhcp_xfer_operations = {
 static void dhcp_timer_expired ( struct retry_timer *timer, int fail ) {
        struct dhcp_session *dhcp =
                container_of ( timer, struct dhcp_session, timer );
-       unsigned long elapsed = ( currticks() - dhcp->start );
 
        /* If we have failed, terminate DHCP */
        if ( fail ) {
@@ -1337,14 +1068,8 @@ static void dhcp_timer_expired ( struct retry_timer *timer, int fail ) {
                return;
        }
 
-       /* Give up waiting for ProxyDHCP before we reach the failure point */
-       if ( dhcp->dhcpoffer && ( elapsed > PROXYDHCP_WAIT_TIME ) ) {
-               dhcp_next_state ( dhcp );
-               return;
-       }
-
-       /* Otherwise, retransmit current packet */
-       dhcp_tx ( dhcp );
+       /* Handle timer expiry based on current state */
+       dhcp->state->expired ( dhcp );
 }
 
 /****************************************************************************
@@ -1375,32 +1100,33 @@ static struct job_interface_operations dhcp_job_operations = {
 
 /****************************************************************************
  *
- * Instantiator
+ * Instantiators
+ *
+ */
+
+/**
+ * DHCP peer address for socket opening
  *
+ * This is a dummy address; the only useful portion is the socket
+ * family (so that we get a UDP connection).  The DHCP client will set
+ * the IP address and source port explicitly on each transmission.
  */
+static struct sockaddr dhcp_peer = {
+       .sa_family = AF_INET,
+};
 
 /**
- * Start DHCP on a network device
+ * Start DHCP state machine on a network device
  *
  * @v job              Job control interface
  * @v netdev           Network device
- * @v register_options DHCP option block registration routine
  * @ret rc             Return status code
  *
- * Starts DHCP on the specified network device.  If successful, the @c
- * register_options() routine will be called with the acquired
- * options.
+ * Starts DHCP on the specified network device.  If successful, the
+ * DHCPACK (and ProxyDHCPACK, if applicable) will be registered as
+ * option sources.
  */
 int start_dhcp ( struct job_interface *job, struct net_device *netdev ) {
-       static struct sockaddr_in server = {
-               .sin_family = AF_INET,
-               .sin_addr.s_addr = INADDR_BROADCAST,
-               .sin_port = htons ( BOOTPS_PORT ),
-       };
-       static struct sockaddr_in client = {
-               .sin_family = AF_INET,
-               .sin_port = htons ( BOOTPC_PORT ),
-       };
        struct dhcp_session *dhcp;
        int rc;
 
@@ -1412,19 +1138,70 @@ int start_dhcp ( struct job_interface *job, struct net_device *netdev ) {
        job_init ( &dhcp->job, &dhcp_job_operations, &dhcp->refcnt );
        xfer_init ( &dhcp->xfer, &dhcp_xfer_operations, &dhcp->refcnt );
        dhcp->netdev = netdev_get ( netdev );
+       dhcp->local.sin_family = AF_INET;
+       dhcp->local.sin_port = htons ( BOOTPC_PORT );
        dhcp->timer.expired = dhcp_timer_expired;
-       dhcp->timer.min_timeout = DHCP_MIN_TIMEOUT;
-       dhcp->timer.max_timeout = DHCP_MAX_TIMEOUT;
-       dhcp->start = currticks();
 
        /* Instantiate child objects and attach to our interfaces */
-       if ( ( rc = xfer_open_socket ( &dhcp->xfer, SOCK_DGRAM,
-                                      ( struct sockaddr * ) &server,
-                                      ( struct sockaddr * ) &client ) ) != 0 )
+       if ( ( rc = xfer_open_socket ( &dhcp->xfer, SOCK_DGRAM, &dhcp_peer,
+                                 ( struct sockaddr * ) &dhcp->local ) ) != 0 )
                goto err;
 
-       /* Start timer to initiate initial DHCPREQUEST */
-       start_timer_nodelay ( &dhcp->timer );
+       /* Enter DHCPDISCOVER state */
+       dhcp_set_state ( dhcp, &dhcp_state_discover );
+
+       /* Attach parent interface, mortalise self, and return */
+       job_plug_plug ( &dhcp->job, job );
+       ref_put ( &dhcp->refcnt );
+       return 0;
+
+ err:
+       dhcp_finished ( dhcp, rc );
+       ref_put ( &dhcp->refcnt );
+       return rc;
+}
+
+/**
+ * Start PXE Boot Server Discovery on a network device
+ *
+ * @v job              Job control interface
+ * @v netdev           Network device
+ * @v pxe_server       PXE server (may be a multicast address)
+ * @v pxe_type         PXE server type
+ * @ret rc             Return status code
+ *
+ * Starts PXE Boot Server Discovery on the specified network device.
+ * If successful, the Boot Server ACK will be registered as an option
+ * source.
+ */
+int start_pxebs ( struct job_interface *job, struct net_device *netdev,
+                 struct in_addr pxe_server, unsigned int pxe_type ) {
+       struct dhcp_session *dhcp;
+       int rc;
+
+       /* Allocate and initialise structure */
+       dhcp = zalloc ( sizeof ( *dhcp ) );
+       if ( ! dhcp )
+               return -ENOMEM;
+       dhcp->refcnt.free = dhcp_free;
+       job_init ( &dhcp->job, &dhcp_job_operations, &dhcp->refcnt );
+       xfer_init ( &dhcp->xfer, &dhcp_xfer_operations, &dhcp->refcnt );
+       dhcp->netdev = netdev_get ( netdev );
+       dhcp->local.sin_family = AF_INET;
+       fetch_ipv4_setting ( netdev_settings ( netdev ), &ip_setting,
+                            &dhcp->local.sin_addr );
+       dhcp->local.sin_port = htons ( BOOTPC_PORT );
+       dhcp->pxe_server = pxe_server;
+       dhcp->pxe_type = htons ( pxe_type );
+       dhcp->timer.expired = dhcp_timer_expired;
+
+       /* Instantiate child objects and attach to our interfaces */
+       if ( ( rc = xfer_open_socket ( &dhcp->xfer, SOCK_DGRAM, &dhcp_peer,
+                                 ( struct sockaddr * ) &dhcp->local ) ) != 0 )
+               goto err;
+
+       /* Enter PXEBS state */
+       dhcp_set_state ( dhcp, &dhcp_state_pxebs );
 
        /* Attach parent interface, mortalise self, and return */
        job_plug_plug ( &dhcp->job, job );
index f5f7f7d..41f1341 100644 (file)
@@ -89,8 +89,8 @@ static int boot_embedded_image ( void ) {
  * @v filename         Boot filename
  * @ret rc             Return status code
  */
-static int boot_next_server_and_filename ( struct in_addr next_server,
-                                          const char *filename ) {
+int boot_next_server_and_filename ( struct in_addr next_server,
+                                   const char *filename ) {
        struct uri *uri;
        struct image *image;
        char buf[ 23 /* tftp://xxx.xxx.xxx.xxx/ */ + strlen(filename) + 1 ];
@@ -167,6 +167,7 @@ int boot_root_path ( const char *root_path ) {
  * @ret rc             Return status code
  */
 static int netboot ( struct net_device *netdev ) {
+       struct setting tmp_setting = { .name = NULL };
        char buf[256];
        struct in_addr next_server;
        int rc;
@@ -194,6 +195,16 @@ static int netboot ( struct net_device *netdev ) {
        if ( rc != ENOENT )
                return rc;
 
+       /* Try PXE menu boot, if we have PXE menu options */
+       tmp_setting.tag = DHCP_VENDOR_CLASS_ID;
+       fetch_string_setting ( NULL, &tmp_setting, buf, sizeof ( buf ) );
+       tmp_setting.tag = DHCP_PXE_BOOT_MENU;
+       if ( ( strcmp ( buf, "PXEClient" ) == 0 ) &&
+            setting_exists ( NULL, &tmp_setting ) ) {
+               printf ( "Booting from PXE menu\n" );
+               return pxe_menu_boot ( netdev );
+       }
+
        /* Try to download and boot whatever we are given as a filename */
        fetch_ipv4_setting ( NULL, &next_server_setting, &next_server );
        fetch_string_setting ( NULL, &filename_setting, buf, sizeof ( buf ) );
index 2e429cd..c68808b 100644 (file)
@@ -46,3 +46,17 @@ int dhcp ( struct net_device *netdev ) {
 
        return rc;
 }
+
+int pxebs ( struct net_device *netdev, struct in_addr pxe_server,
+           unsigned int pxe_type ) {
+       int rc;
+
+       /* Perform PXE Boot Server Discovery */
+       printf ( "PXEBS (%s %s type %d)",
+                netdev->name, inet_ntoa ( pxe_server ), pxe_type );
+       if ( ( rc = start_pxebs ( &monojob, netdev, pxe_server,
+                                 pxe_type ) ) == 0 )
+               rc = monojob_wait ( "" );
+
+       return rc;
+}
diff --git a/src/usr/pxemenu.c b/src/usr/pxemenu.c
new file mode 100644 (file)
index 0000000..3f5bfc8
--- /dev/null
@@ -0,0 +1,336 @@
+/*
+ * Copyright (C) 2009 Michael Brown <mbrown@fensystems.co.uk>.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ */
+
+#include <stdint.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <errno.h>
+#include <ctype.h>
+#include <byteswap.h>
+#include <curses.h>
+#include <console.h>
+#include <gpxe/dhcp.h>
+#include <gpxe/vsprintf.h>
+#include <gpxe/keys.h>
+#include <gpxe/timer.h>
+#include <usr/dhcpmgmt.h>
+#include <usr/autoboot.h>
+
+/** @file
+ *
+ * PXE Boot Menus
+ *
+ */
+
+/* Colour pairs */
+#define CPAIR_NORMAL   1
+#define CPAIR_SELECT   2
+
+/** A PXE boot menu item */
+struct pxe_menu_item {
+       /** Boot Server type */
+       unsigned int type;
+       /** Description */
+       char *desc;
+};
+
+/**
+ * A PXE boot menu
+ *
+ * This structure encapsulates the menu information provided via DHCP
+ * options.
+ */
+struct pxe_menu {
+       /** Boot Server address */
+       struct in_addr server;
+       /** Timeout (in seconds)
+        *
+        * Negative indicates no timeout (i.e. wait indefinitely)
+        */
+       int timeout;
+       /** Number of menu items */
+       unsigned int num_items;
+       /** Selected menu item */
+       unsigned int selection;
+       /** Menu items */
+       struct pxe_menu_item items[0];
+};
+
+/**
+ * Parse and allocate PXE boot menu
+ *
+ * @v menu             PXE boot menu to fill in
+ * @ret rc             Return status code
+ *
+ * It is the callers responsibility to eventually free the allocated
+ * boot menu.
+ */
+static int pxe_menu_parse ( struct pxe_menu **menu ) {
+       struct setting tmp_setting = { .name = NULL };
+       struct in_addr server;
+       struct dhcp_pxe_boot_menu_prompt prompt = { .timeout = 0 };
+       uint8_t raw_menu[256];
+       int raw_menu_len;
+       struct dhcp_pxe_boot_menu *raw_menu_item;
+       void *raw_menu_end;
+       unsigned int num_menu_items;
+       unsigned int i;
+       int rc;
+
+       /* Fetch relevant settings */
+       tmp_setting.tag = DHCP_PXE_BOOT_SERVER_MCAST;
+       server.s_addr = INADDR_BROADCAST;
+       fetch_ipv4_setting ( NULL, &tmp_setting, &server );
+       tmp_setting.tag = DHCP_PXE_BOOT_MENU_PROMPT;
+       fetch_setting ( NULL, &tmp_setting, &prompt, sizeof ( prompt ) );
+       tmp_setting.tag = DHCP_PXE_BOOT_MENU;
+       memset ( raw_menu, 0, sizeof ( raw_menu ) );
+       if ( ( raw_menu_len = fetch_setting ( NULL, &tmp_setting, raw_menu,
+                                             sizeof ( raw_menu ) ) ) < 0 ) {
+               rc = raw_menu_len;
+               DBG ( "Could not retrieve raw PXE boot menu: %s\n",
+                     strerror ( rc ) );
+               return rc;
+       }
+       if ( raw_menu_len >= ( int ) sizeof ( raw_menu ) ) {
+               DBG ( "Raw PXE boot menu too large for buffer\n" );
+               return -ENOSPC;
+       }
+       raw_menu_end = ( raw_menu + raw_menu_len );
+
+       /* Count menu items */
+       num_menu_items = 0;
+       raw_menu_item = ( ( void * ) raw_menu );
+       while ( 1 ) {
+               if ( ( ( ( void * ) raw_menu_item ) +
+                      sizeof ( *raw_menu_item ) ) > raw_menu_end )
+                       break;
+               if ( ( ( ( void * ) raw_menu_item ) +
+                      sizeof ( *raw_menu_item ) +
+                      raw_menu_item->desc_len ) > raw_menu_end )
+                       break;
+               num_menu_items++;
+               raw_menu_item = ( ( ( void * ) raw_menu_item ) +
+                                 sizeof ( *raw_menu_item ) +
+                                 raw_menu_item->desc_len );
+       }
+
+       /* Allocate space for parsed menu */
+       *menu = zalloc ( sizeof ( **menu ) +
+                        ( num_menu_items * sizeof ( (*menu)->items[0] ) ) +
+                        raw_menu_len + 1 /* NUL */ );
+       if ( ! *menu ) {
+               DBG ( "Could not allocate PXE boot menu\n" );
+               return -ENOMEM;
+       }
+
+       /* Fill in parsed menu */
+       (*menu)->server = server;
+       (*menu)->timeout =
+               ( ( prompt.timeout == 0xff ) ? -1 : prompt.timeout );
+       (*menu)->num_items = num_menu_items;
+       raw_menu_item = ( ( ( void * ) (*menu) ) + sizeof ( **menu ) +
+                         ( num_menu_items * sizeof ( (*menu)->items[0] ) ) );
+       memcpy ( raw_menu_item, raw_menu, raw_menu_len );
+       for ( i = 0 ; i < num_menu_items ; i++ ) {
+               (*menu)->items[i].type = ntohs ( raw_menu_item->type );
+               (*menu)->items[i].desc = raw_menu_item->desc;
+               /* Set type to 0; this ensures that the description
+                * for the previous menu item is NUL-terminated.
+                * (Final item is NUL-terminated anyway.)
+                */
+               raw_menu_item->type = 0;
+               raw_menu_item = ( ( ( void * ) raw_menu_item ) +
+                                 sizeof ( *raw_menu_item ) +
+                                 raw_menu_item->desc_len );
+       }
+
+       return 0;
+}
+
+/**
+ * Draw PXE boot menu item
+ *
+ * @v menu             PXE boot menu
+ * @v index            Index of item to draw
+ */
+static void pxe_menu_draw_item ( struct pxe_menu *menu,
+                                unsigned int index ) {
+       int selected = ( menu->selection == index );
+       char buf[COLS+1];
+       char *tmp = buf;
+       ssize_t remaining = sizeof ( buf );
+       size_t len;
+       unsigned int row;
+
+       /* Prepare space-padded row content */
+       len = ssnprintf ( tmp, remaining, " %c. %s",
+                         ( 'A' + index ), menu->items[index].desc );
+       tmp += len;
+       remaining -= len;
+       if ( selected && ( menu->timeout > 0 ) ) {
+               len = ssnprintf ( tmp, remaining, " (%d)", menu->timeout );
+               tmp += len;
+               remaining -= len;
+       }
+       for ( ; remaining > 1 ; tmp++, remaining-- )
+               *tmp = ' ';
+       *tmp = '\0';
+
+       /* Draw row */
+       row = ( LINES - menu->num_items + index );
+       color_set ( ( selected ? CPAIR_SELECT : CPAIR_NORMAL ), NULL );
+       mvprintw ( row, 0, "%s", buf );
+       move ( row, 1 );
+}
+
+/**
+ * Make selection from PXE boot menu
+ *
+ * @v menu             PXE boot menu
+ * @ret rc             Return status code
+ */
+int pxe_menu_select ( struct pxe_menu *menu ) {
+       unsigned long start = currticks();
+       unsigned long now;
+       unsigned long elapsed;
+       unsigned int old_selection;
+       int key;
+       unsigned int key_selection;
+       unsigned int i;
+       int rc = 0;
+
+       /* Initialise UI */
+       initscr();
+       start_color();
+       init_pair ( CPAIR_NORMAL, COLOR_WHITE, COLOR_BLACK );
+       init_pair ( CPAIR_SELECT, COLOR_BLACK, COLOR_WHITE );
+       color_set ( CPAIR_NORMAL, NULL );
+
+       /* Draw initial menu */
+       for ( i = 0 ; i < menu->num_items ; i++ )
+               printf ( "\n" );
+       for ( i = 0 ; i < menu->num_items ; i++ )
+               pxe_menu_draw_item ( menu, ( menu->num_items - i - 1 ) );
+
+       while ( 1 ) {
+
+               /* Decrease timeout if necessary */
+               if ( menu->timeout > 0 ) {
+                       now = currticks();
+                       elapsed = ( now - start );
+                       if ( elapsed >= TICKS_PER_SEC ) {
+                               start = now;
+                               menu->timeout--;
+                               pxe_menu_draw_item ( menu, menu->selection );
+                       }
+               }
+
+               /* Select current item if we have timed out */
+               if ( menu->timeout == 0 )
+                       break;
+
+               /* Check for keyboard input */
+               if ( ! iskey() )
+                       continue;
+               key = getkey();
+
+               /* Any keyboard input cancels the timeout */
+               menu->timeout = -1;
+               pxe_menu_draw_item ( menu, menu->selection );
+
+               /* Act upon key */
+               old_selection = menu->selection;
+               if ( ( key == CR ) || ( key == LF ) ) {
+                       break;
+               } else if ( key == CTRL_C ) {
+                       rc = -ECANCELED;
+                       break;
+               } else if ( key == KEY_UP ) {
+                       if ( menu->selection > 0 )
+                               menu->selection--;
+               } else if ( key == KEY_DOWN ) {
+                       if ( menu->selection < ( menu->num_items - 1 ) )
+                               menu->selection++;
+               } else if ( ( key < KEY_MIN ) &&
+                           ( ( key_selection = ( toupper ( key ) - 'A' ) )
+                             < menu->num_items ) ) {
+                       menu->selection = key_selection;
+                       menu->timeout = 0;
+               }
+               pxe_menu_draw_item ( menu, old_selection );
+               pxe_menu_draw_item ( menu, menu->selection );
+       }
+
+       /* Shut down UI */
+       endwin();
+
+       return rc;
+}
+
+/**
+ * Boot using PXE boot menu
+ *
+ * @ret rc             Return status code
+ *
+ * Note that a success return status indicates that a PXE boot menu
+ * item has been selected, and that the DHCP session should perform a
+ * boot server request/ack.
+ */
+int pxe_menu_boot ( struct net_device *netdev ) {
+       struct pxe_menu *menu;
+       struct in_addr pxe_server;
+       unsigned int pxe_type;
+       struct settings *pxebs_settings;
+       struct in_addr next_server;
+       char filename[256];
+       int rc;
+
+       /* Parse and allocate boot menu */
+       if ( ( rc = pxe_menu_parse ( &menu ) ) != 0 )
+               return rc;
+
+       /* Make selection from boot menu */
+       if ( ( rc = pxe_menu_select ( menu ) ) != 0 ) {
+               free ( menu );
+               return rc;
+       }
+       pxe_server = menu->server;
+       pxe_type = menu->items[menu->selection].type;
+
+       /* Free boot menu */
+       free ( menu );
+
+       /* Return immediately if local boot selected */
+       if ( ! pxe_type )
+               return 0;
+
+       /* Attempt PXE Boot Server Discovery */
+       if ( ( rc = pxebs ( netdev, pxe_server, pxe_type ) ) != 0 )
+               return rc;
+
+       /* Attempt boot */
+       pxebs_settings = find_settings ( PXEBS_SETTINGS_NAME );
+       assert ( pxebs_settings );
+       fetch_ipv4_setting ( pxebs_settings, &next_server_setting,
+                            &next_server );
+       fetch_string_setting ( pxebs_settings, &filename_setting,
+                              filename, sizeof ( filename ) );
+       return boot_next_server_and_filename ( next_server, filename );
+}