efi_loader: supply EFI network test

This patch provides an EFI application to check the correct function
of the Simple Network Protocol implementation.

It sends a DHCP request and analyzes the DHCP offer.

Different error conditions including a 10s timeout are checked.

A successful execution will look like this:

=> bootefi nettest
Scanning disk ide.blk#0...
Found 1 disks
WARNING: Invalid device tree, expect boot to fail
Network test
DHCP Discover
DHCP reply received from 192.168.76.2 (52:55:c0:a8:4c:02)
as broadcast message.
OK. The test was completed successfully.

Signed-off-by: Heinrich Schuchardt <xypron.glpk@gmx.de>
Reviewed-by: Simon Glass <sjg@chromium.org>
Signed-off-by: Alexander Graf <agraf@suse.de>
diff --git a/include/efi_selftest.h b/include/efi_selftest.h
index 2f0992f..7ec42a0 100644
--- a/include/efi_selftest.h
+++ b/include/efi_selftest.h
@@ -61,6 +61,17 @@
 		 __attribute__ ((format (__printf__, 1, 2)));
 
 /*
+ * Compare memory.
+ * We cannot use lib/string.c due to different CFLAGS values.
+ *
+ * @buf1:	first buffer
+ * @buf2:	second buffer
+ * @length:	number of bytes to compare
+ * @return:	0 if both buffers contain the same bytes
+ */
+int efi_st_memcmp(const void *buf1, const void *buf2, size_t length);
+
+/*
  * Reads an Unicode character from the input device.
  *
  * @return: Unicode character
diff --git a/lib/efi_selftest/Makefile b/lib/efi_selftest/Makefile
index 30f1960..e446046 100644
--- a/lib/efi_selftest/Makefile
+++ b/lib/efi_selftest/Makefile
@@ -15,12 +15,18 @@
 CFLAGS_REMOVE_efi_selftest_events.o := $(CFLAGS_NON_EFI)
 CFLAGS_efi_selftest_exitbootservices.o := $(CFLAGS_EFI)
 CFLAGS_REMOVE_efi_selftest_exitbootservices.o := $(CFLAGS_NON_EFI)
+CFLAGS_efi_selftest_snp.o := $(CFLAGS_EFI)
+CFLAGS_REMOVE_efi_selftest_snp.o := $(CFLAGS_NON_EFI)
 CFLAGS_efi_selftest_tpl.o := $(CFLAGS_EFI)
 CFLAGS_REMOVE_efi_selftest_tpl.o := $(CFLAGS_NON_EFI)
+CFLAGS_efi_selftest_util.o := $(CFLAGS_EFI)
+CFLAGS_REMOVE_efi_selftest_util.o := $(CFLAGS_NON_EFI)
 
 obj-$(CONFIG_CMD_BOOTEFI_SELFTEST) += \
 efi_selftest.o \
 efi_selftest_console.o \
 efi_selftest_events.o \
 efi_selftest_exitbootservices.o \
-efi_selftest_tpl.o
+efi_selftest_snp.o \
+efi_selftest_tpl.o \
+efi_selftest_util.o
diff --git a/lib/efi_selftest/efi_selftest_snp.c b/lib/efi_selftest/efi_selftest_snp.c
new file mode 100644
index 0000000..638be01
--- /dev/null
+++ b/lib/efi_selftest/efi_selftest_snp.c
@@ -0,0 +1,424 @@
+/*
+ * efi_selftest_snp
+ *
+ * Copyright (c) 2017 Heinrich Schuchardt <xypron.glpk@gmx.de>
+ *
+ * SPDX-License-Identifier:	GPL-2.0+
+ *
+ * This unit test covers the Simple Network Protocol as well as
+ * the CopyMem and SetMem boottime services.
+ *
+ * A DHCP discover message is sent. The test is successful if a
+ * DHCP reply is received.
+ *
+ * TODO: Once ConnectController and DisconnectController are implemented
+ *	 we should connect our code as controller.
+ */
+
+#include <efi_selftest.h>
+
+/*
+ * MAC address for broadcasts
+ */
+static const u8 BROADCAST_MAC[] = { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff};
+
+struct dhcp_hdr {
+	u8 op;
+#define BOOTREQUEST 1
+#define BOOTREPLY 2
+	u8 htype;
+# define HWT_ETHER 1
+	u8 hlen;
+# define HWL_ETHER 6
+	u8 hops;
+	u32 xid;
+	u16 secs;
+	u16 flags;
+#define DHCP_FLAGS_UNICAST	0x0000
+#define DHCP_FLAGS_BROADCAST	0x0080
+	u32 ciaddr;
+	u32 yiaddr;
+	u32 siaddr;
+	u32 giaddr;
+	u8 chaddr[16];
+	u8 sname[64];
+	u8 file[128];
+};
+
+/*
+ * Message type option.
+ */
+#define DHCP_MESSAGE_TYPE	0x35
+#define DHCPDISCOVER		1
+#define DHCPOFFER		2
+#define DHCPREQUEST		3
+#define DHCPDECLINE		4
+#define DHCPACK			5
+#define DHCPNAK			6
+#define DHCPRELEASE		7
+
+struct dhcp {
+	struct ethernet_hdr eth_hdr;
+	struct ip_udp_hdr ip_udp;
+	struct dhcp_hdr dhcp_hdr;
+	u8 opt[128];
+} __packed;
+
+static struct efi_boot_services *boottime;
+static struct efi_simple_network *net;
+static struct efi_event *timer;
+static const efi_guid_t efi_net_guid = EFI_SIMPLE_NETWORK_GUID;
+/* IP packet ID */
+static unsigned int net_ip_id;
+
+/*
+ * Compute the checksum of the IP header. We cover even values of length only.
+ * We cannot use net/checksum.c due to different CFLAGS values.
+ *
+ * @buf:	IP header
+ * @len:	length of header in bytes
+ * @return:	checksum
+ */
+static unsigned int efi_ip_checksum(const void *buf, size_t len)
+{
+	size_t i;
+	u32 sum = 0;
+	const u16 *pos = buf;
+
+	for (i = 0; i < len; i += 2)
+		sum += *pos++;
+
+	sum = (sum >> 16) + (sum & 0xffff);
+	sum += sum >> 16;
+	sum = ~sum & 0xffff;
+
+	return sum;
+}
+
+/*
+ * Transmit a DHCPDISCOVER message.
+ */
+static efi_status_t send_dhcp_discover(void)
+{
+	efi_status_t ret;
+	struct dhcp p = {};
+
+	/*
+	 * Fill ethernet header
+	 */
+	boottime->copy_mem(p.eth_hdr.et_dest, (void *)BROADCAST_MAC, ARP_HLEN);
+	boottime->copy_mem(p.eth_hdr.et_src, &net->mode->current_address,
+			   ARP_HLEN);
+	p.eth_hdr.et_protlen = htons(PROT_IP);
+	/*
+	 * Fill IP header
+	 */
+	p.ip_udp.ip_hl_v	= 0x45;
+	p.ip_udp.ip_len		= htons(sizeof(struct dhcp) -
+					sizeof(struct ethernet_hdr));
+	p.ip_udp.ip_id		= htons(++net_ip_id);
+	p.ip_udp.ip_off		= htons(IP_FLAGS_DFRAG);
+	p.ip_udp.ip_ttl		= 0xff; /* time to live */
+	p.ip_udp.ip_p		= IPPROTO_UDP;
+	boottime->set_mem(&p.ip_udp.ip_dst, 4, 0xff);
+	p.ip_udp.ip_sum		= efi_ip_checksum(&p.ip_udp, IP_HDR_SIZE);
+
+	/*
+	 * Fill UDP header
+	 */
+	p.ip_udp.udp_src	= htons(68);
+	p.ip_udp.udp_dst	= htons(67);
+	p.ip_udp.udp_len	= htons(sizeof(struct dhcp) -
+					sizeof(struct ethernet_hdr) -
+					sizeof(struct ip_hdr));
+	/*
+	 * Fill DHCP header
+	 */
+	p.dhcp_hdr.op		= BOOTREQUEST;
+	p.dhcp_hdr.htype	= HWT_ETHER;
+	p.dhcp_hdr.hlen		= HWL_ETHER;
+	p.dhcp_hdr.flags	= htons(DHCP_FLAGS_UNICAST);
+	boottime->copy_mem(&p.dhcp_hdr.chaddr,
+			   &net->mode->current_address, ARP_HLEN);
+	/*
+	 * Fill options
+	 */
+	p.opt[0]	= 0x63; /* DHCP magic cookie */
+	p.opt[1]	= 0x82;
+	p.opt[2]	= 0x53;
+	p.opt[3]	= 0x63;
+	p.opt[4]	= DHCP_MESSAGE_TYPE;
+	p.opt[5]	= 0x01; /* length */
+	p.opt[6]	= DHCPDISCOVER;
+	p.opt[7]	= 0x39; /* maximum message size */
+	p.opt[8]	= 0x02; /* length */
+	p.opt[9]	= 0x02; /* 576 bytes */
+	p.opt[10]	= 0x40;
+	p.opt[11]	= 0xff; /* end of options */
+
+	/*
+	 * Transmit DHCPDISCOVER message.
+	 */
+	ret = net->transmit(net, 0, sizeof(struct dhcp), &p, NULL, NULL, 0);
+	if (ret != EFI_SUCCESS)
+		efi_st_error("Sending a DHCP request failed\n");
+	else
+		efi_st_printf("DHCP Discover\n");
+	return ret;
+}
+
+/*
+ * Setup unit test.
+ *
+ * Create a 1 s periodic timer.
+ * Start the network driver.
+ *
+ * @handle:	handle of the loaded image
+ * @systable:	system table
+ * @return:	EFI_ST_SUCCESS for success
+ */
+static int setup(const efi_handle_t handle,
+		 const struct efi_system_table *systable)
+{
+	efi_status_t ret;
+
+	boottime = systable->boottime;
+
+	/*
+	 * Create a timer event.
+	 */
+	ret = boottime->create_event(EVT_TIMER, TPL_CALLBACK, NULL, NULL,
+				     &timer);
+	if (ret != EFI_SUCCESS) {
+		efi_st_error("Failed to create event\n");
+		return EFI_ST_FAILURE;
+	}
+	/*
+	 * Set timer period to 1s.
+	 */
+	ret = boottime->set_timer(timer, EFI_TIMER_PERIODIC, 10000000);
+	if (ret != EFI_SUCCESS) {
+		efi_st_error("Failed to locate simple network protocol\n");
+		return EFI_ST_FAILURE;
+	}
+	/*
+	 * Find an interface implementing the SNP protocol.
+	 */
+	ret = boottime->locate_protocol(&efi_net_guid, NULL, (void **)&net);
+	if (ret != EFI_SUCCESS) {
+		efi_st_error("Failed to locate simple network protocol\n");
+		return EFI_ST_FAILURE;
+	}
+	/*
+	 * Check hardware address size.
+	 */
+	if (!net->mode) {
+		efi_st_error("Mode not provided\n");
+		return EFI_ST_FAILURE;
+	}
+	if (net->mode->hwaddr_size != ARP_HLEN) {
+		efi_st_error("HwAddressSize = %u, expected %u\n",
+			     net->mode->hwaddr_size, ARP_HLEN);
+		return EFI_ST_FAILURE;
+	}
+	/*
+	 * Check that WaitForPacket event exists.
+	 */
+	if (!net->wait_for_packet) {
+		efi_st_error("WaitForPacket event missing\n");
+		return EFI_ST_FAILURE;
+	}
+	/*
+	 * Initialize network adapter.
+	 */
+	ret = net->initialize(net, 0, 0);
+	if (ret != EFI_SUCCESS) {
+		efi_st_error("Failed to initialize network adapter\n");
+		return EFI_ST_FAILURE;
+	}
+	/*
+	 * Start network adapter.
+	 */
+	ret = net->start(net);
+	if (ret != EFI_SUCCESS) {
+		efi_st_error("Failed to start network adapter\n");
+		return EFI_ST_FAILURE;
+	}
+	return EFI_ST_SUCCESS;
+}
+
+/*
+ * Execute unit test.
+ *
+ * A DHCP discover message is sent. The test is successful if a
+ * DHCP reply is received within 10 seconds.
+ *
+ * @return:	EFI_ST_SUCCESS for success
+ */
+static int execute(void)
+{
+	efi_status_t ret;
+	struct efi_event *events[2];
+	size_t index;
+	union {
+		struct dhcp p;
+		u8 b[PKTSIZE];
+	} buffer;
+	struct efi_mac_address srcaddr;
+	struct efi_mac_address destaddr;
+	size_t buffer_size;
+	u8 *addr;
+	/*
+	 * The timeout is to occur after 10 s.
+	 */
+	unsigned int timeout = 10;
+
+	/*
+	 * Send DHCP discover message
+	 */
+	ret = send_dhcp_discover();
+	if (ret != EFI_SUCCESS)
+		return EFI_ST_FAILURE;
+
+	/*
+	 * If we would call WaitForEvent only with the WaitForPacket event,
+	 * our code would block until a packet is received which might never
+	 * occur. By calling WaitFor event with both a timer event and the
+	 * WaitForPacket event we can escape this blocking situation.
+	 *
+	 * If the timer event occurs before we have received a DHCP reply
+	 * a further DHCP discover message is sent.
+	 */
+	events[0] = timer;
+	events[1] = net->wait_for_packet;
+	for (;;) {
+		/*
+		 * Wait for packet to be received or timer event.
+		 */
+		boottime->wait_for_event(2, events, &index);
+		if (index == 0) {
+			/*
+			 * The timer event occurred. Check for timeout.
+			 */
+			--timeout;
+			if (!timeout) {
+				efi_st_error("Timeout occurred\n");
+				return EFI_ST_FAILURE;
+			}
+			/*
+			 * Send further DHCP discover message
+			 */
+			ret = send_dhcp_discover();
+			if (ret != EFI_SUCCESS)
+				return EFI_ST_FAILURE;
+			continue;
+		}
+		/*
+		 * Receive packet
+		 */
+		buffer_size = sizeof(buffer);
+		net->receive(net, NULL, &buffer_size, &buffer,
+			     &srcaddr, &destaddr, NULL);
+		if (ret != EFI_SUCCESS) {
+			efi_st_error("Failed to receive packet");
+			return EFI_ST_FAILURE;
+		}
+		/*
+		 * Check the packet is meant for this system.
+		 * Unfortunately QEMU ignores the broadcast flag.
+		 * So we have to check for broadcasts too.
+		 */
+		if (efi_st_memcmp(&destaddr, &net->mode->current_address,
+				  ARP_HLEN) &&
+		    efi_st_memcmp(&destaddr, BROADCAST_MAC, ARP_HLEN))
+			continue;
+		/*
+		 * Check this is a DHCP reply
+		 */
+		if (buffer.p.eth_hdr.et_protlen != ntohs(PROT_IP) ||
+		    buffer.p.ip_udp.ip_hl_v != 0x45 ||
+		    buffer.p.ip_udp.ip_p != IPPROTO_UDP ||
+		    buffer.p.ip_udp.udp_src != ntohs(67) ||
+		    buffer.p.ip_udp.udp_dst != ntohs(68) ||
+		    buffer.p.dhcp_hdr.op != BOOTREPLY)
+			continue;
+		/*
+		 * We successfully received a DHCP reply.
+		 */
+		break;
+	}
+
+	/*
+	 * Write a log message.
+	 */
+	addr = (u8 *)&buffer.p.ip_udp.ip_src;
+	efi_st_printf("DHCP reply received from %u.%u.%u.%u (%pm) ",
+		      addr[0], addr[1], addr[2], addr[3], &srcaddr);
+	if (!efi_st_memcmp(&destaddr, BROADCAST_MAC, ARP_HLEN))
+		efi_st_printf("as broadcast message.\n");
+	else
+		efi_st_printf("as unicast message.\n");
+
+	return EFI_ST_SUCCESS;
+}
+
+/*
+ * Tear down unit test.
+ *
+ * Close the timer event created in setup.
+ * Shut down the network adapter.
+ *
+ * @return:	EFI_ST_SUCCESS for success
+ */
+static int teardown(void)
+{
+	efi_status_t ret;
+	int exit_status = EFI_ST_SUCCESS;
+
+	if (timer) {
+		/*
+		 * Stop timer.
+		 */
+		ret = boottime->set_timer(timer, EFI_TIMER_STOP, 0);
+		if (ret != EFI_SUCCESS) {
+			efi_st_error("Failed to stop timer");
+			exit_status = EFI_ST_FAILURE;
+		}
+		/*
+		 * Close timer event.
+		 */
+		ret = boottime->close_event(timer);
+		if (ret != EFI_SUCCESS) {
+			efi_st_error("Failed to close event");
+			exit_status = EFI_ST_FAILURE;
+		}
+	}
+	if (net) {
+		/*
+		 * Stop network adapter.
+		 */
+		ret = net->stop(net);
+		if (ret != EFI_SUCCESS) {
+			efi_st_error("Failed to stop network adapter\n");
+			exit_status = EFI_ST_FAILURE;
+		}
+		/*
+		 * Shut down network adapter.
+		 */
+		ret = net->shutdown(net);
+		if (ret != EFI_SUCCESS) {
+			efi_st_error("Failed to shut down network adapter\n");
+			exit_status = EFI_ST_FAILURE;
+		}
+	}
+
+	return exit_status;
+}
+
+EFI_UNIT_TEST(snp) = {
+	.name = "simple network protocol",
+	.phase = EFI_EXECUTE_BEFORE_BOOTTIME_EXIT,
+	.setup = setup,
+	.execute = execute,
+	.teardown = teardown,
+};
diff --git a/lib/efi_selftest/efi_selftest_util.c b/lib/efi_selftest/efi_selftest_util.c
new file mode 100644
index 0000000..c9c295e
--- /dev/null
+++ b/lib/efi_selftest/efi_selftest_util.c
@@ -0,0 +1,25 @@
+/*
+ * efi_selftest_util
+ *
+ * Copyright (c) 2017 Heinrich Schuchardt <xypron.glpk@gmx.de>
+ *
+ * SPDX-License-Identifier:	GPL-2.0+
+ *
+ * Utility functions
+ */
+
+#include <efi_selftest.h>
+
+int efi_st_memcmp(const void *buf1, const void *buf2, size_t length)
+{
+	const u8 *pos1 = buf1;
+	const u8 *pos2 = buf2;
+
+	for (; length; --length) {
+		if (*pos1 != *pos2)
+			return pos1 - pos2;
+		++pos1;
+		++pos2;
+	}
+	return EFI_ST_SUCCESS;
+}