diff --git a/gtests/net/packetdrill/ip_packet.c b/gtests/net/packetdrill/ip_packet.c
index 7c0aad16f4f25fa9f2e7f9882e3dc962845dac57..147728c293a25f1bae84b659e7882ef7d936f9e0 100644
--- a/gtests/net/packetdrill/ip_packet.c
+++ b/gtests/net/packetdrill/ip_packet.c
@@ -153,6 +153,37 @@ int ipv4_header_append(struct packet *packet,
 	return STATUS_OK;
 }
 
+int ipv6_header_append(struct packet *packet,
+		       const char *ip_src,
+		       const char *ip_dst,
+		       char **error)
+{
+	struct header *header = NULL;
+	const int ipv6_bytes = sizeof(struct ipv6);
+	struct ipv6 *ipv6 = NULL;
+
+	header = packet_append_header(packet, HEADER_IPV6, ipv6_bytes);
+	if (header == NULL) {
+		asprintf(error, "too many headers");
+		return STATUS_ERR;
+	}
+
+	ipv6 = header->h.ipv6;
+	set_ip_header(ipv6, AF_INET6, sizeof(struct ipv6), ECN_NONE, 0);
+
+	if (inet_pton(AF_INET6, ip_src, &ipv6->src_ip) != 1) {
+		asprintf(error, "bad IPv6 src address: '%s'\n", ip_src);
+		return STATUS_ERR;
+	}
+
+	if (inet_pton(AF_INET6, ip_dst, &ipv6->dst_ip) != 1) {
+		asprintf(error, "bad IPv6 dst address: '%s'\n", ip_dst);
+		return STATUS_ERR;
+	}
+
+	return STATUS_OK;
+}
+
 int ipv4_header_finish(struct packet *packet,
 		       struct header *header, struct header *next_inner)
 {
@@ -170,3 +201,20 @@ int ipv4_header_finish(struct packet *packet,
 
 	return STATUS_OK;
 }
+
+int ipv6_header_finish(struct packet *packet,
+		       struct header *header, struct header *next_inner)
+{
+	struct ipv6 *ipv6 = header->h.ipv6;
+	int ip_bytes = sizeof(struct ipv6) + next_inner->total_bytes;
+
+	assert(next_inner->total_bytes <= 0xffff);
+	ipv6->payload_len = htons(next_inner->total_bytes);
+	ipv6->next_header = header_type_info(next_inner->type)->ip_proto;
+
+	/* IPv6 has no header checksum. */
+
+	header->total_bytes = ip_bytes;
+
+	return STATUS_OK;
+}
diff --git a/gtests/net/packetdrill/ip_packet.h b/gtests/net/packetdrill/ip_packet.h
index ae39e578d036e0457a44bf06c7fbed88d89acac8..7531a2927d67b8975cf2596d137bfc5a45efb1d0 100644
--- a/gtests/net/packetdrill/ip_packet.h
+++ b/gtests/net/packetdrill/ip_packet.h
@@ -50,10 +50,25 @@ extern int ipv4_header_append(struct packet *packet,
 			      const char *ip_dst,
 			      char **error);
 
-/* Finalize the IPV4 header by filling in all necessary fields that
+/* Append an IPv6 header to the end of the given packet and fill in
+ * src/dst.  On success, return STATUS_OK; on error return STATUS_ERR
+ * and fill in a malloc-allocated error message in *error.
+ */
+extern int ipv6_header_append(struct packet *packet,
+			      const char *ip_src,
+			      const char *ip_dst,
+			      char **error);
+
+/* Finalize the IPv4 header by filling in all necessary fields that
  * were not filled in at parse time.
  */
 extern int ipv4_header_finish(struct packet *packet,
 			      struct header *header, struct header *next_inner);
 
+/* Finalize the IPv6 header by filling in all necessary fields that
+ * were not filled in at parse time.
+ */
+extern int ipv6_header_finish(struct packet *packet,
+			      struct header *header, struct header *next_inner);
+
 #endif /* __IP_PACKET_H__ */
diff --git a/gtests/net/packetdrill/lexer.l b/gtests/net/packetdrill/lexer.l
index 759f9bd09ad4ed04009b322894fd46fdf1c05cb4..398b434a625d3a09122b266fc7a6975892801c09 100644
--- a/gtests/net/packetdrill/lexer.l
+++ b/gtests/net/packetdrill/lexer.l
@@ -131,11 +131,33 @@ c_comment	\/\*(([^*])|(\*[^\/]))*\*\/
  */
 code		\%\{(([^}])|(\}[^\%]))*\}\%
 
-/* A regular experssion for an IP address
- * TODO(ncardwell): IPv6
- */
+/* IPv4: a regular experssion for an IPv4 address */
 ipv4_addr		[0-9]+[.][0-9]+[.][0-9]+[.][0-9]+
 
+/* IPv6: a regular experssion for an IPv6 address. The complexity is
+ * unfortunate, but we can't use a super-simple approach because TCP
+ * sequence number ranges like 1:1001 can look like IPv6 addresses if
+ * we use a naive approach.
+ */
+seg	[0-9a-fA-F]{1,4}
+v0	[:][:]
+v1	({seg}[:]){7,7}{seg}
+v2	({seg}[:]){1,7}[:]
+v3	({seg}[:]){1,6}[:]{seg}
+v4	({seg}[:]){1,5}([:]{seg}){1,2}
+v5	({seg}[:]){1,4}([:]{seg}){1,3}
+v6	({seg}[:]){1,3}([:]{seg}){1,4}
+v7	({seg}[:]){1,2}([:]{seg}){1,5}
+v8	{seg}[:](([:]{seg}){1,6})
+v9	[:]([:]{seg}){1,7}
+/* IPv4-mapped IPv6 address: */
+v10	[:][:]ffff[:]{ipv4_addr}
+/* IPv4-translated IPv6 address: */
+v11	[:][:]ffff[:](0){1,4}[:]{ipv4_addr}
+/* IPv4-embedded IPv6 addresses: */
+v12	({seg}[:]){1,4}[:]{ipv4_addr}
+ipv6_addr ({v0}|{v1}|{v2}|{v3}|{v4}|{v5}|{v6}|{v7}|{v8}|{v9}|{v10}|{v11}|{v12})
+
 %%
 sa_family		return SA_FAMILY;
 sin_port		return SIN_PORT;
@@ -150,6 +172,7 @@ onoff			return ONOFF;
 linger			return LINGER;
 htons			return _HTONS_;
 ipv4			return IPV4;
+ipv6			return IPV6;
 icmp			return ICMP;
 udp			return UDP;
 gre			return GRE;
@@ -186,4 +209,5 @@ ce			return CE;
 {c_comment}		/* ignore C-style comment */;
 {code}			yylval.string = code(yytext);   return CODE;
 {ipv4_addr}		yylval.string = strdup(yytext); return IPV4_ADDR;
+{ipv6_addr}		yylval.string = strdup(yytext); return IPV6_ADDR;
 %%
diff --git a/gtests/net/packetdrill/packet.c b/gtests/net/packetdrill/packet.c
index 8f814650356bbc99e5f045eb9e2c2cdc2046ba0e..310738a5d14bc551c8b998b170ba2e1e638ddc24 100644
--- a/gtests/net/packetdrill/packet.c
+++ b/gtests/net/packetdrill/packet.c
@@ -37,7 +37,7 @@
 struct header_type_info header_types[HEADER_NUM_TYPES] = {
 	{ "NONE",   0,			0,		NULL },
 	{ "IPV4",   IPPROTO_IPIP,	ETHERTYPE_IP,	ipv4_header_finish },
-	{ "IPV6",   IPPROTO_IPV6,	ETHERTYPE_IPV6, NULL },
+	{ "IPV6",   IPPROTO_IPV6,	ETHERTYPE_IPV6, ipv6_header_finish },
 	{ "GRE",    IPPROTO_GRE,	0,		gre_header_finish },
 	{ "TCP",    IPPROTO_TCP,	0,		NULL },
 	{ "UDP",    IPPROTO_UDP,	0,		NULL },
diff --git a/gtests/net/packetdrill/parser.y b/gtests/net/packetdrill/parser.y
index 636e2e0199e7e573f3183a8a5da2deefa46c7006..7c43792c506209c77dee022d831c110d6b146f94 100644
--- a/gtests/net/packetdrill/parser.y
+++ b/gtests/net/packetdrill/parser.y
@@ -473,11 +473,11 @@ static struct tcp_option *new_tcp_fast_open_option(const char *cookie_string,
 %token <reserved> ACK ECR EOL MSS NOP SACK SACKOK TIMESTAMP VAL WIN WSCALE PRO
 %token <reserved> FAST_OPEN
 %token <reserved> ECT0 ECT1 CE ECT01 NO_ECN
-%token <reserved> IPV4 ICMP UDP GRE MTU
+%token <reserved> IPV4 IPV6 ICMP UDP GRE MTU
 %token <reserved> OPTION
 %token <floating> FLOAT
 %token <integer> INTEGER HEX_INTEGER
-%token <string> WORD STRING BACK_QUOTED CODE IPV4_ADDR
+%token <string> WORD STRING BACK_QUOTED CODE IPV4_ADDR IPV6_ADDR
 %type <direction> direction
 %type <ip_ecn> opt_ip_info
 %type <ip_ecn> ip_ecn
@@ -550,7 +550,9 @@ option_value
 | WORD		{ $$ = $1; }
 | STRING	{ $$ = $1; }
 | IPV4_ADDR	{ $$ = $1; }
+| IPV6_ADDR	{ $$ = $1; }
 | IPV4		{ $$ = strdup("ipv4"); }
+| IPV6		{ $$ = strdup("ipv6"); }
 ;
 
 opt_init_command
@@ -751,6 +753,17 @@ packet_prefix
 	free(ip_dst);
 	$$ = packet;
 }
+| packet_prefix IPV6 IPV6_ADDR '>' IPV6_ADDR ':' {
+	char *error = NULL;
+	struct packet *packet = $1;
+	char *ip_src = $3;
+	char *ip_dst = $5;
+	if (ipv6_header_append(packet, ip_src, ip_dst, &error))
+		semantic_error(error);
+	free(ip_src);
+	free(ip_dst);
+	$$ = packet;
+}
 | packet_prefix GRE ':' {
 	char *error = NULL;
 	struct packet *packet = $1;