[efi] Extract basic network settings from loaded image device path

The UEFI HTTP boot mechanism is extraordinarily badly designed, even
by the standards of the UEFI specification in general.  It has the
symptoms of a feature that has been designed entirely in terms of user
stories, without any consideration at all being given to the
underlying technical architecture.  It does work, provided that you
are doing precisely and only what was envisioned by the product owner.
If you want to try anything outside the bounds of the product owner's
extremely limited imagination, then you are almost certainly about to
enter a world of pain.

As one very minor example of this: the cached DHCP packet is not
available when using HTTP boot.  The UEFI HTTP boot code does perform
DHCP, but it pointlessly and unhelpfully throws away the DHCP packet
and trashes the network interface configuration before handing over to
the downloaded executable.

Work around this imbecility by parsing and applying the few network
configuration settings that are persisted into the loaded image's
device path.  This is limited to very basic information such as the IP
address, gateway address, and DNS server address, but it does at least
provide enough for a functional routing table.

Signed-off-by: Michael Brown <mcb30@ipxe.org>
diff --git a/src/include/ipxe/settings.h b/src/include/ipxe/settings.h
index e042b97..424188d 100644
--- a/src/include/ipxe/settings.h
+++ b/src/include/ipxe/settings.h
@@ -445,6 +445,8 @@
 extern const struct setting
 gateway6_setting __setting ( SETTING_IP6, gateway6 );
 extern const struct setting
+dns6_setting __setting ( SETTING_IP6_EXTRA, dns6 );
+extern const struct setting
 hostname_setting __setting ( SETTING_HOST, hostname );
 extern const struct setting
 domain_setting __setting ( SETTING_IP_EXTRA, domain );
diff --git a/src/interface/efi/efi_path.c b/src/interface/efi/efi_path.c
index 23f1bb8..ac3c049 100644
--- a/src/interface/efi/efi_path.c
+++ b/src/interface/efi/efi_path.c
@@ -34,6 +34,8 @@
 #include <ipxe/fcp.h>
 #include <ipxe/ib_srp.h>
 #include <ipxe/usb.h>
+#include <ipxe/settings.h>
+#include <ipxe/dhcp.h>
 #include <ipxe/efi/efi.h>
 #include <ipxe/efi/efi_driver.h>
 #include <ipxe/efi/efi_path.h>
@@ -44,6 +46,40 @@
  *
  */
 
+/** An EFI device path settings block */
+struct efi_path_settings {
+	/** Settings interface */
+	struct settings settings;
+	/** Device path */
+	EFI_DEVICE_PATH_PROTOCOL *path;
+};
+
+/** An EFI device path setting */
+struct efi_path_setting {
+	/** Setting */
+	const struct setting *setting;
+	/**
+	 * Fetch setting
+	 *
+	 * @v pathset		Path setting
+	 * @v path		Device path
+	 * @v data		Buffer to fill with setting data
+	 * @v len		Length of buffer
+	 * @ret len		Length of setting data, or negative error
+	 */
+	int ( * fetch ) ( struct efi_path_setting *pathset,
+			  EFI_DEVICE_PATH_PROTOCOL *path,
+			  void *data, size_t len );
+	/** Path type */
+	uint8_t type;
+	/** Path subtype */
+	uint8_t subtype;
+	/** Offset within device path */
+	uint8_t offset;
+	/** Length (if fixed) */
+	uint8_t len;
+};
+
 /**
  * Find next element in device path
  *
@@ -657,3 +693,208 @@
 	intf_put ( dest );
 	return path;
 }
+
+/**
+ * Fetch an EFI device path fixed-size setting
+ *
+ * @v pathset		Path setting
+ * @v path		Device path
+ * @v data		Buffer to fill with setting data
+ * @v len		Length of buffer
+ * @ret len		Length of setting data, or negative error
+ */
+static int efi_path_fetch_fixed ( struct efi_path_setting *pathset,
+				  EFI_DEVICE_PATH_PROTOCOL *path,
+				  void *data, size_t len ) {
+
+	/* Copy data */
+	if ( len > pathset->len )
+		len = pathset->len;
+	memcpy ( data, ( ( ( void * ) path ) + pathset->offset ), len );
+
+	return pathset->len;
+}
+
+/**
+ * Fetch an EFI device path DNS setting
+ *
+ * @v pathset		Path setting
+ * @v path		Device path
+ * @v data		Buffer to fill with setting data
+ * @v len		Length of buffer
+ * @ret len		Length of setting data, or negative error
+ */
+static int efi_path_fetch_dns ( struct efi_path_setting *pathset,
+				EFI_DEVICE_PATH_PROTOCOL *path,
+				void *data, size_t len ) {
+	DNS_DEVICE_PATH *dns = container_of ( path, DNS_DEVICE_PATH, Header );
+	unsigned int count;
+	unsigned int i;
+	size_t frag_len;
+
+	/* Check applicability */
+	if ( ( !! dns->IsIPv6 ) !=
+	     ( pathset->setting->type == &setting_type_ipv6 ) )
+		return -ENOENT;
+
+	/* Calculate number of addresses */
+	count = ( ( ( ( path->Length[1] << 8 ) | path->Length[0] ) -
+		    pathset->offset ) / sizeof ( dns->DnsServerIp[0] ) );
+
+	/* Copy data */
+	for ( i = 0 ; i < count ; i++ ) {
+		frag_len = len;
+		if ( frag_len > pathset->len )
+			frag_len = pathset->len;
+		memcpy ( data, &dns->DnsServerIp[i], frag_len );
+		data += frag_len;
+		len -= frag_len;
+	}
+
+	return ( count * pathset->len );
+}
+
+/** EFI device path settings */
+static struct efi_path_setting efi_path_settings[] = {
+	{ &ip_setting, efi_path_fetch_fixed, MESSAGING_DEVICE_PATH,
+	  MSG_IPv4_DP, offsetof ( IPv4_DEVICE_PATH, LocalIpAddress ),
+	  sizeof ( struct in_addr ) },
+	{ &netmask_setting, efi_path_fetch_fixed, MESSAGING_DEVICE_PATH,
+	  MSG_IPv4_DP, offsetof ( IPv4_DEVICE_PATH, SubnetMask ),
+	  sizeof ( struct in_addr ) },
+	{ &gateway_setting, efi_path_fetch_fixed, MESSAGING_DEVICE_PATH,
+	  MSG_IPv4_DP, offsetof ( IPv4_DEVICE_PATH, GatewayIpAddress ),
+	  sizeof ( struct in_addr ) },
+	{ &ip6_setting, efi_path_fetch_fixed, MESSAGING_DEVICE_PATH,
+	  MSG_IPv6_DP, offsetof ( IPv6_DEVICE_PATH, LocalIpAddress ),
+	  sizeof ( struct in6_addr ) },
+	{ &len6_setting, efi_path_fetch_fixed, MESSAGING_DEVICE_PATH,
+	  MSG_IPv6_DP, offsetof ( IPv6_DEVICE_PATH, PrefixLength ),
+	  sizeof ( uint8_t ) },
+	{ &gateway6_setting, efi_path_fetch_fixed, MESSAGING_DEVICE_PATH,
+	  MSG_IPv6_DP, offsetof ( IPv6_DEVICE_PATH, GatewayIpAddress ),
+	  sizeof ( struct in6_addr ) },
+	{ &dns_setting, efi_path_fetch_dns, MESSAGING_DEVICE_PATH,
+	  MSG_DNS_DP, offsetof ( DNS_DEVICE_PATH, DnsServerIp ),
+	  sizeof ( struct in_addr ) },
+	{ &dns6_setting, efi_path_fetch_dns, MESSAGING_DEVICE_PATH,
+	  MSG_DNS_DP, offsetof ( DNS_DEVICE_PATH, DnsServerIp ),
+	  sizeof ( struct in6_addr ) },
+};
+
+/**
+ * Fetch value of EFI device path setting
+ *
+ * @v settings		Settings block
+ * @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 efi_path_fetch ( struct settings *settings, struct setting *setting,
+			    void *data, size_t len ) {
+	struct efi_path_settings *pathsets =
+		container_of ( settings, struct efi_path_settings, settings );
+	EFI_DEVICE_PATH_PROTOCOL *path = pathsets->path;
+	EFI_DEVICE_PATH_PROTOCOL *next;
+	struct efi_path_setting *pathset;
+	unsigned int i;
+	int ret;
+
+	/* Find matching path setting, if any */
+	for ( i = 0 ; i < ( sizeof ( efi_path_settings ) /
+			    sizeof ( efi_path_settings[0] ) ) ; i++ ) {
+
+		/* Check for a matching setting */
+		pathset = &efi_path_settings[i];
+		if ( setting_cmp ( setting, pathset->setting ) != 0 )
+			continue;
+
+		/* Find matching device path element, if any */
+		for ( ; ( next = efi_path_next ( path ) ) ; path = next ) {
+
+			/* Check for a matching path type */
+			if ( ( path->Type != pathset->type ) ||
+			     ( path->SubType != pathset->subtype ) )
+				continue;
+
+			/* Fetch value */
+			if ( ( ret = pathset->fetch ( pathset, path,
+						      data, len ) ) < 0 )
+				return ret;
+
+			/* Apply default type, if not already set */
+			if ( ! setting->type )
+				setting->type = pathset->setting->type;
+
+			return ret;
+		}
+		break;
+	}
+
+	return -ENOENT;
+}
+
+/** EFI device path settings operations */
+static struct settings_operations efi_path_settings_operations = {
+	.fetch = efi_path_fetch,
+};
+
+/**
+ * Create per-netdevice EFI path settings
+ *
+ * @v netdev		Network device
+ * @v priv		Private data
+ * @ret rc		Return status code
+ */
+static int efi_path_net_probe ( struct net_device *netdev, void *priv ) {
+	struct efi_path_settings *pathsets = priv;
+	struct settings *settings = &pathsets->settings;
+	EFI_DEVICE_PATH_PROTOCOL *path = efi_loaded_image_path;
+	unsigned int vlan;
+	void *mac;
+	int rc;
+
+	/* Check applicability */
+	pathsets->path = path;
+	mac = efi_path_mac ( path );
+	vlan = efi_path_vlan ( path );
+	if ( ( mac == NULL ) ||
+	     ( memcmp ( mac, netdev->ll_addr,
+			netdev->ll_protocol->ll_addr_len ) != 0 ) ||
+	     ( vlan != vlan_tag ( netdev ) ) ) {
+		DBGC ( settings, "EFI path %s does not apply to %s\n",
+		       efi_devpath_text ( path ), netdev->name );
+		return 0;
+	}
+
+	/* Never override a real DHCP settings block */
+	if ( find_child_settings ( netdev_settings ( netdev ),
+				   DHCP_SETTINGS_NAME ) ) {
+		DBGC ( settings, "EFI path %s not overriding %s DHCP "
+		       "settings\n", efi_devpath_text ( path ), netdev->name );
+		return 0;
+	}
+
+	/* Initialise and register settings */
+	settings_init ( settings, &efi_path_settings_operations,
+			&netdev->refcnt, NULL );
+	if ( ( rc = register_settings ( settings, netdev_settings ( netdev ),
+					DHCP_SETTINGS_NAME ) ) != 0 ) {
+		DBGC ( settings, "EFI path %s could not register for %s: %s\n",
+		       efi_devpath_text ( path ), netdev->name,
+		       strerror ( rc ) );
+		return rc;
+	}
+	DBGC ( settings, "EFI path %s registered for %s\n",
+	       efi_devpath_text ( path ), netdev->name );
+
+	return 0;
+}
+
+/** EFI path settings per-netdevice driver */
+struct net_driver efi_path_net_driver __net_driver = {
+	.name = "EFI path",
+	.priv_len = sizeof ( struct efi_path_settings ),
+	.probe = efi_path_net_probe,
+};