From 041dbded00e31d760b62ccdb0acba8b6067790b9 Mon Sep 17 00:00:00 2001
From: David van Moolenbroek <david@minix3.org>
Date: Mon, 23 Jan 2017 23:04:39 +0000
Subject: [PATCH 1/2] Add support for IPv6 address scopes

This patch adds full support for IPv6 address scopes, thereby aiming
to be compliant with IPv6 standards in general and RFC 4007 in
particular. The high-level summary is that link-local addresses are
now meaningful only in the context of their own link, guaranteeing
full isolation between links (and their addresses) in this respect.
This isolation even allows multiple interfaces to have the same
link-local addresses locally assigned.

The implementation achieves this by extending the lwIP IPv6 address
structure with a zone field that, for addresses that have a scope,
carries the scope's zone in which that address has meaning. The zone
maps to one or more interfaces. By default, lwIP uses a policy that
provides a 1:1 mapping between links and interfaces, and considers
all other addresses unscoped, corresponding to the default policy
sketched in RFC 4007 Sec. 6. The implementation allows for replacing
the default policy with a custom policy if desired, though.

The lwIP core implementation has been changed to provide somewhat of
a balance between correctness and efficiency on on side, and backward
compatibility on the other. In particular, while the application would
ideally always provide a zone for a scoped address, putting this in as
a requirement would likely break many applications. Instead, the API
accepts both "properly zoned" IPv6 addresses and addresses that, while
scoped, "lack" a zone. lwIP will try to add a zone as soon as possible
for efficiency reasons, in particular from TCP/UDP/RAW PCB bind and
connect calls, but this may fail, and sendto calls may bypass that
anyway. Ultimately, a zone is always added when an IP packet is sent
when needed, because the link-layer lwIP code (and ND6 in particualar)
requires that all addresses be properly zoned for correctness: for
example, to provide isolation between links in the ND6 destination
cache. All this applies to packet output only, because on packet
input, all scoped addresses will be given a zone automatically.

It is also worth remarking that on output, no attempt is made to stop
outgoing packets with addresses for a zone not matching the outgoing
interface. However, unless the application explicitly provides
addresses that will result in such zone violations, the core API
implementation (and the IPv6 routing algorithm in particular) itself
will never take decisions that result in zone violations itself.

This patch adds a new header file, ip6_zone.h, which contains comments
that explain several implementation aspects in a bit more detail.

For now, it is possible to disable scope support by changing the new
LWIP_IPV6_SCOPES configuration option. For users of the core API, it
is important to note that scoped addresses that are locally assigned
to a netif must always have a zone set; the standard netif address
assignment functions always do this on behalf of the caller, though.
Also, core API users will want to enable LWIP_IPV6_SCOPES_DEBUG at
least initially when upgrading, to ensure that all addresses are
properly initialized.
---
 UPGRADING                   |  13 +++
 src/api/netdb.c             |   1 +
 src/api/sockets.c           |   5 +-
 src/core/ipv6/ethip6.c      |   3 +
 src/core/ipv6/icmp6.c       |  87 ++++++++++----
 src/core/ipv6/ip6.c         | 222 +++++++++++++++++++++++++----------
 src/core/ipv6/ip6_addr.c    |   2 +
 src/core/ipv6/ip6_frag.c    |  28 ++++-
 src/core/ipv6/mld6.c        |  53 +++++++--
 src/core/ipv6/nd6.c         |  76 ++++++++----
 src/core/netif.c            |  41 +++++--
 src/core/raw.c              |  17 +++
 src/core/tcp.c              |  34 +++++-
 src/core/udp.c              |  35 ++++--
 src/include/lwip/icmp6.h    |   2 +
 src/include/lwip/inet.h     |   7 +-
 src/include/lwip/ip6_addr.h |  86 +++++++++++---
 src/include/lwip/ip6_frag.h |   4 +
 src/include/lwip/ip6_zone.h | 278 ++++++++++++++++++++++++++++++++++++++++++++
 src/include/lwip/ip_addr.h  |  20 ++--
 src/include/lwip/opt.h      |  19 +++
 src/netif/lowpan6.c         |  13 ++-
 22 files changed, 876 insertions(+), 170 deletions(-)
 create mode 100644 src/include/lwip/ip6_zone.h

diff --git a/UPGRADING b/UPGRADING
index 3a984c9..3188efc 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -8,6 +8,19 @@ with newer versions.
 
   * [Enter new changes just after this line - do not remove this line]
 
+  ++ Application changes:
+
+  * The IPv6 implementation now supports address scopes. All addresses that have a scope according
+    to the default policy (link-local unicast addresses, interface-local and link-local multicast
+    addresses) should now have a zone set on them before being passed to the core API, although
+    lwIP will currently attempt to select a zone on the caller's behalf when necessary.
+    Applications that directly assign IPv6 addresses to interfaces (which is NOT recommended)
+    must now ensure that link-local addresses carry the netif's zone. See the new ip6_zone.h header
+    file for more information and relevant macros. For now it is still possible to turn off scopes
+    support through the new LWIP_IPV6_SCOPES option. When upgrading an implementation that uses the
+    core API directly, it is highly recommended to enable LWIP_IPV6_SCOPES_DEBUG at least for
+    a while, to ensure e.g. proper address initialization.
+
 (2.0.1)
 
   ++ Application changes:
diff --git a/src/api/netdb.c b/src/api/netdb.c
index ccd9586..90ccfe4 100644
--- a/src/api/netdb.c
+++ b/src/api/netdb.c
@@ -376,6 +376,7 @@ lwip_getaddrinfo(const char *nodename, const char *servname,
     sa6->sin6_family = AF_INET6;
     sa6->sin6_len = sizeof(struct sockaddr_in6);
     sa6->sin6_port = lwip_htons((u16_t)port_nr);
+    sa6->sin6_scope_id = ip6_addr_zone(&addr);
     ai->ai_family = AF_INET6;
 #endif /* LWIP_IPV6 */
   } else {
diff --git a/src/api/sockets.c b/src/api/sockets.c
index 0d0a1fa..02c1e83 100644
--- a/src/api/sockets.c
+++ b/src/api/sockets.c
@@ -96,9 +96,12 @@
       (sin6)->sin6_port = lwip_htons((port)); \
       (sin6)->sin6_flowinfo = 0; \
       inet6_addr_from_ip6addr(&(sin6)->sin6_addr, ipaddr); \
-      (sin6)->sin6_scope_id = 0; }while(0)
+      (sin6)->sin6_scope_id = ip6_addr_zone(ipaddr); }while(0)
 #define SOCKADDR6_TO_IP6ADDR_PORT(sin6, ipaddr, port) do { \
     inet6_addr_to_ip6addr(ip_2_ip6(ipaddr), &((sin6)->sin6_addr)); \
+    if (ip6_addr_has_scope(ip_2_ip6(ipaddr), IP6_UNKNOWN)) { \
+      ip6_addr_set_zone(ip_2_ip6(ipaddr), (sin6)->sin6_scope_id); \
+    } \
     (port) = lwip_ntohs((sin6)->sin6_port); }while(0)
 #endif /* LWIP_IPV6 */
 
diff --git a/src/core/ipv6/ethip6.c b/src/core/ipv6/ethip6.c
index 8f9a91b..904005c 100644
--- a/src/core/ipv6/ethip6.c
+++ b/src/core/ipv6/ethip6.c
@@ -82,6 +82,9 @@ ethip6_output(struct netif *netif, struct pbuf *q, const ip6_addr_t *ip6addr)
   const u8_t *hwaddr;
   err_t result;
 
+  /* The destination IP address must be properly zoned from here on down. */
+  IP6_ADDR_ZONECHECK_NETIF(ip6addr, netif);
+
   /* multicast destination IP address? */
   if (ip6_addr_ismulticast(ip6addr)) {
     /* Hash IP multicast address to MAC address.*/
diff --git a/src/core/ipv6/icmp6.c b/src/core/ipv6/icmp6.c
index 323b69a..c4fd377 100644
--- a/src/core/ipv6/icmp6.c
+++ b/src/core/ipv6/icmp6.c
@@ -65,7 +65,8 @@
 #endif
 
 /* Forward declarations */
-static void icmp6_send_response(struct pbuf *p, u8_t code, u32_t data, u8_t type);
+static void icmp6_send_response(struct pbuf *p, u8_t code, u32_t data,
+    u8_t type, const ip6_addr_t *src_addr, const ip6_addr_t *dest_addr);
 
 
 /**
@@ -209,6 +210,9 @@ icmp6_input(struct pbuf *p, struct netif *inp)
 /**
  * Send an icmpv6 'destination unreachable' packet.
  *
+ * This function must be used only in direct response to a packet that is being
+ * received right now. Otherwise, address zones would be lost.
+ *
  * @param p the input packet for which the 'unreachable' should be sent,
  *          p->payload pointing to the IPv6 header
  * @param c ICMPv6 code for the unreachable type
@@ -216,12 +220,15 @@ icmp6_input(struct pbuf *p, struct netif *inp)
 void
 icmp6_dest_unreach(struct pbuf *p, enum icmp6_dur_code c)
 {
-  icmp6_send_response(p, c, 0, ICMP6_TYPE_DUR);
+  icmp6_send_response(p, c, 0, ICMP6_TYPE_DUR, NULL, NULL);
 }
 
 /**
  * Send an icmpv6 'packet too big' packet.
  *
+ * This function must be used only in direct response to a packet that is being
+ * received right now. Otherwise, address zones would be lost.
+ *
  * @param p the input packet for which the 'packet too big' should be sent,
  *          p->payload pointing to the IPv6 header
  * @param mtu the maximum mtu that we can accept
@@ -229,25 +236,53 @@ icmp6_dest_unreach(struct pbuf *p, enum icmp6_dur_code c)
 void
 icmp6_packet_too_big(struct pbuf *p, u32_t mtu)
 {
-  icmp6_send_response(p, 0, mtu, ICMP6_TYPE_PTB);
+  icmp6_send_response(p, 0, mtu, ICMP6_TYPE_PTB, NULL, NULL);
 }
 
 /**
  * Send an icmpv6 'time exceeded' packet.
  *
- * @param p the input packet for which the 'unreachable' should be sent,
+ * This function must be used only in direct response to a packet that is being
+ * received right now. Otherwise, address zones would be lost.
+ *
+ * @param p the input packet for which the 'time exceeded' should be sent,
  *          p->payload pointing to the IPv6 header
  * @param c ICMPv6 code for the time exceeded type
  */
 void
 icmp6_time_exceeded(struct pbuf *p, enum icmp6_te_code c)
 {
-  icmp6_send_response(p, c, 0, ICMP6_TYPE_TE);
+  icmp6_send_response(p, c, 0, ICMP6_TYPE_TE, NULL, NULL);
+}
+
+/**
+ * Send an icmpv6 'time exceeded' packet, with explicit source and destination
+ * addresses.
+ *
+ * This function may be used to send a response sometime after receiving the
+ * packet for which this response is meant. The provided source and destination
+ * addresses are used primarily to retain their zone information.
+ *
+ * @param p the input packet for which the 'time exceeded' should be sent,
+ *          p->payload pointing to the IPv6 header
+ * @param c ICMPv6 code for the time exceeded type
+ * @param src_addr source address of the original packet, with zone information
+ * @param dest_addr destination address of the original packet, with zone
+ *                  information
+ */
+void
+icmp6_time_exceeded_with_addrs(struct pbuf *p, enum icmp6_te_code c,
+    const ip6_addr_t *src_addr, const ip6_addr_t *dest_addr)
+{
+  icmp6_send_response(p, c, 0, ICMP6_TYPE_TE, src_addr, dest_addr);
 }
 
 /**
  * Send an icmpv6 'parameter problem' packet.
  *
+ * This function must be used only in direct response to a packet that is being
+ * received right now. Otherwise, address zones would be lost.
+ *
  * @param p the input packet for which the 'param problem' should be sent,
  *          p->payload pointing to the IP header
  * @param c ICMPv6 code for the param problem type
@@ -256,27 +291,38 @@ icmp6_time_exceeded(struct pbuf *p, enum icmp6_te_code c)
 void
 icmp6_param_problem(struct pbuf *p, enum icmp6_pp_code c, u32_t pointer)
 {
-  icmp6_send_response(p, c, pointer, ICMP6_TYPE_PP);
+  icmp6_send_response(p, c, pointer, ICMP6_TYPE_PP, NULL, NULL);
 }
 
 /**
  * Send an ICMPv6 packet in response to an incoming packet.
  *
+ * If this packet is sent as a direct response of an incoming packet being
+ * processed right now, the information of that incoming packet will be used to
+ * determine where to reply. In that case, the src_addr and dest_addr parameters
+ * must be set to NULL. If this packet is NOT sent as a direct response to an
+ * incoming packet, but rather sometime later (e.g. for a fragment reassembly
+ * timeout), the caller must provide the zoned source and destination addresses
+ * from the original packet with the src_addr and dest_addr parameters. The
+ * reason for this approach is that while the addresses themselves are part of
+ * the original packet, their zone information is not, thus possibly resulting
+ * in a link-local response being sent over the wrong link.
+ *
  * @param p the input packet for which the response should be sent,
  *          p->payload pointing to the IPv6 header
  * @param code Code of the ICMPv6 header
  * @param data Additional 32-bit parameter in the ICMPv6 header
  * @param type Type of the ICMPv6 header
+ * @param src_addr original source address or NULL (see description)
+ * @param dest_addr original destination address or NULL (see description)
  */
 static void
-icmp6_send_response(struct pbuf *p, u8_t code, u32_t data, u8_t type)
+icmp6_send_response(struct pbuf *p, u8_t code, u32_t data, u8_t type,
+    const ip6_addr_t *src_addr, const ip6_addr_t *dest_addr)
 {
   struct pbuf *q;
   struct icmp6_hdr *icmp6hdr;
-  const ip6_addr_t *reply_src;
-  ip6_addr_t *reply_dest;
-  ip6_addr_t reply_src_local, reply_dest_local;
-  struct ip6_hdr *ip6hdr;
+  const ip6_addr_t *reply_src, *reply_dest;
   struct netif *netif;
 
   /* ICMPv6 header + IPv6 header + data */
@@ -300,17 +346,15 @@ icmp6_send_response(struct pbuf *p, u8_t code, u32_t data, u8_t type)
           IP6_HLEN + LWIP_ICMP6_DATASIZE);
 
   /* Get the destination address and netif for this ICMP message. */
-  if ((ip_current_netif() == NULL) ||
-      ((code == ICMP6_TE_FRAG) && (type == ICMP6_TYPE_TE))) {
+  if (src_addr != NULL) {
+    LWIP_ASSERT("must provide both source and destination", dest_addr != NULL);
     /* Special case, as ip6_current_xxx is either NULL, or points
-     * to a different packet than the one that expired.
-     * We must use the addresses that are stored in the expired packet. */
-    ip6hdr = (struct ip6_hdr *)p->payload;
-    /* copy from packed address to aligned address */
-    ip6_addr_copy(reply_dest_local, ip6hdr->src);
-    ip6_addr_copy(reply_src_local, ip6hdr->dest);
-    reply_dest = &reply_dest_local;
-    reply_src = &reply_src_local;
+     * to a different packet than the one that expired. */
+    IP6_ADDR_ZONECHECK(src_addr);
+    IP6_ADDR_ZONECHECK(dest_addr);
+    /* Swap source and destination for the reply. */
+    reply_dest = src_addr;
+    reply_src = dest_addr;
     netif = ip6_route(reply_src, reply_dest);
     if (netif == NULL) {
       /* drop */
@@ -321,6 +365,7 @@ icmp6_send_response(struct pbuf *p, u8_t code, u32_t data, u8_t type)
   }
   else {
     netif = ip_current_netif();
+    LWIP_ASSERT("icmpv6 packet not a direct response", netif != NULL);
     reply_dest = ip6_current_src_addr();
 
     /* Select an address to use as source. */
diff --git a/src/core/ipv6/ip6.c b/src/core/ipv6/ip6.c
index 01a40e6..f871b78 100644
--- a/src/core/ipv6/ip6.c
+++ b/src/core/ipv6/ip6.c
@@ -64,13 +64,15 @@
  * Finds the appropriate network interface for a given IPv6 address. It tries to select
  * a netif following a sequence of heuristics:
  * 1) if there is only 1 netif, return it
- * 2) if the destination is a link-local address, try to match the src address to a netif.
- *    this is a tricky case because with multiple netifs, link-local addresses only have
- *    meaning within a particular subnet/link.
- * 3) tries to match the destination subnet to a configured address
- * 4) tries to find a router-announced route
- * 5) tries to match the source address to the netif
- * 6) returns the default netif, if configured
+ * 2) if the destination is a zoned address, match its zone to a netif
+ * 3) if the either the source or destination address is a scoped address,
+ *    match the source address's zone (if set) or address (if not) to a netif
+ * 4) tries to match the destination subnet to a configured address
+ * 5) tries to find a router-announced route
+ * 6) tries to match the (unscoped) source address to the netif
+ * 7) returns the default netif, if configured
+ *
+ * Note that each of the two given addresses may or may not be properly zoned.
  *
  * @param src the source IPv6 address, if known
  * @param dest the destination IPv6 address for which to find the route
@@ -84,45 +86,87 @@ ip6_route(const ip6_addr_t *src, const ip6_addr_t *dest)
 
   /* If single netif configuration, fast return. */
   if ((netif_list != NULL) && (netif_list->next == NULL)) {
-    if (!netif_is_up(netif_list) || !netif_is_link_up(netif_list)) {
+    if (!netif_is_up(netif_list) || !netif_is_link_up(netif_list) ||
+        (ip6_addr_has_zone(dest) && !ip6_addr_test_zone(dest, netif_list))) {
       return NULL;
     }
     return netif_list;
   }
 
-  /* Special processing for link-local addresses. */
-  if (ip6_addr_islinklocal(dest)) {
-    if (ip6_addr_isany(src)) {
-      /* Use default netif, if Up. */
-      if (netif_default == NULL || !netif_is_up(netif_default) ||
-          !netif_is_link_up(netif_default)) {
-        return NULL;
-      }
-      return netif_default;
-    }
-
-    /* Try to find the netif for the source address, checking that link is up. */
+#if LWIP_IPV6_SCOPES
+  /* Special processing for zoned destination addresses. This includes link-
+   * local unicast addresses and interface/link-local multicast addresses. Use
+   * the zone to find a matching netif. If the address is not zoned, then there
+   * is technically no "wrong" netif to choose, and we leave routing to other
+   * rules; in most cases this should be the scoped-source rule below. */
+  if (ip6_addr_has_zone(dest)) {
+    IP6_ADDR_ZONECHECK(dest);
+    /* Find a netif based on the zone. For custom mappings, one zone may map
+     * to multiple netifs, so find one that can actually send a packet. */
     for (netif = netif_list; netif != NULL; netif = netif->next) {
-      if (!netif_is_up(netif) || !netif_is_link_up(netif)) {
-        continue;
+      if (ip6_addr_test_zone(dest, netif) &&
+          netif_is_up(netif) && netif_is_link_up(netif)) {
+        return netif;
       }
-      for (i = 0; i < LWIP_IPV6_NUM_ADDRESSES; i++) {
-        if (ip6_addr_isvalid(netif_ip6_addr_state(netif, i)) &&
-            ip6_addr_cmp(src, netif_ip6_addr(netif, i))) {
+    }
+    /* No matching netif found. Do no try to route to a different netif,
+     * as that would be a zone violation, resulting in any packets sent to
+     * that netif being dropped on output. */
+    return NULL;
+  }
+#endif /* LWIP_IPV6_SCOPES */
+
+  /* Special processing for scoped source and destination addresses. If we get
+   * here, the destination address does not have a zone, so either way we need
+   * to look at the source address, which may or may not have a zone. If it
+   * does, the zone is restrictive: there is (typically) only one matching
+   * netif for it, and we should avoid routing to any other netif as that would
+   * result in guaranteed zone violations. For scoped source addresses that do
+   * not have a zone, use (only) a netif that has that source address locally
+   * assigned. This case also applies to the loopback source address, which has
+   * an implied link-local scope. If only the destination address is scoped
+   * (but, again, not zoned), we still want to use only the source address to
+   * determine its zone because that's most likely what the user/application
+   * wants, regardless of whether the source address is scoped. Finally, some
+   * of this story also applies if scoping is disabled altogether. */
+#if LWIP_IPV6_SCOPES
+  if (ip6_addr_has_scope(dest, IP6_UNKNOWN) ||
+      ip6_addr_has_scope(src, IP6_UNICAST) ||
+#else /* LWIP_IPV6_SCOPES */
+  if (ip6_addr_islinklocal(dest) || ip6_addr_ismulticast_iflocal(dest) ||
+      ip6_addr_ismulticast_linklocal(dest) || ip6_addr_islinklocal(src) ||
+#endif /* LWIP_IPV6_SCOPES */
+      ip6_addr_isloopback(src)) {
+    if (ip6_addr_has_zone(src)) {
+      /* Find a netif matching the source zone (relatively cheap). */
+      for (netif = netif_list; netif != NULL; netif = netif->next) {
+        if (netif_is_up(netif) && netif_is_link_up(netif) &&
+            ip6_addr_test_zone(src, netif)) {
           return netif;
         }
       }
+    } else {
+      /* Find a netif matching the source address (relatively expensive). */
+      for (netif = netif_list; netif != NULL; netif = netif->next) {
+        if (!netif_is_up(netif) || !netif_is_link_up(netif)) {
+          continue;
+        }
+        for (i = 0; i < LWIP_IPV6_NUM_ADDRESSES; i++) {
+          if (ip6_addr_isvalid(netif_ip6_addr_state(netif, i)) &&
+              ip6_addr_cmp_zoneless(src, netif_ip6_addr(netif, i))) {
+            return netif;
+          }
+        }
+      }
     }
-
-    /* netif not found, use default netif, if up */
-    if (netif_default == NULL || !netif_is_up(netif_default) ||
-        !netif_is_link_up(netif_default)) {
-      return NULL;
-    }
-    return netif_default;
+    /* Again, do not use any other netif in this case, as that could result in
+     * zone boundary violations. */
+    return NULL;
   }
 
-  /* we come here for non-link-local addresses */
+  /* We come here only if neither source nor destination is scoped. */
+  IP6_ADDR_ZONECHECK(src);
+
 #ifdef LWIP_HOOK_IP6_ROUTE
   netif = LWIP_HOOK_IP6_ROUTE(src, dest);
   if (netif != NULL) {
@@ -156,7 +200,8 @@ ip6_route(const ip6_addr_t *src, const ip6_addr_t *dest)
     return netif;
   }
 
-  /* try with the netif that matches the source address. */
+  /* Try with the netif that matches the source address. Given the earlier rule
+   * for scoped source addresses, this applies to unscoped addresses only. */
   if (!ip6_addr_isany(src)) {
     for (netif = netif_list; netif != NULL; netif = netif->next) {
       if (!netif_is_up(netif) || !netif_is_link_up(netif)) {
@@ -215,7 +260,8 @@ ip6_route(const ip6_addr_t *src, const ip6_addr_t *dest)
  * proper implementation of Rule 8 would obviate the need to implement Rule 6.
  *
  * @param netif the netif on which to send a packet
- * @param dest the destination we are trying to reach
+ * @param dest the destination we are trying to reach (possibly not properly
+ *             zoned)
  * @return the most suitable source address to use, or NULL if no suitable
  *         source address is found
  */
@@ -270,7 +316,9 @@ ip6_select_source_address(struct netif *netif, const ip6_addr_t *dest)
     }
     cand_pref = ip6_addr_ispreferred(netif_ip6_addr_state(netif, i));
     /* @todo compute the actual common bits, for longest matching prefix. */
-    cand_bits = ip6_addr_netcmp(cand_addr, dest); /* just 1 or 0 for now */
+    /* We cannot count on the destination address having a proper zone
+     * assignment, so do not compare zones in this case. */
+    cand_bits = ip6_addr_netcmp_zoneless(cand_addr, dest); /* just 1 or 0 for now */
     if (cand_bits && ip6_addr_nethostcmp(cand_addr, dest)) {
       return netif_ip_addr6(netif, i); /* Rule 1 */
     }
@@ -336,6 +384,20 @@ ip6_forward(struct pbuf *p, struct ip6_hdr *iphdr, struct netif *inp)
     IP6_STATS_INC(ip6.drop);
     return;
   }
+#if LWIP_IPV6_SCOPES
+  /* Do not forward packets with a zoned (e.g., link-local) source address
+   * outside of their zone. We determined the zone a bit earlier, so we know
+   * that the address is properly zoned here, so we can safely use has_zone.
+   * Also skip packets with a loopback source address (link-local implied). */
+  if ((ip6_addr_has_zone(ip6_current_src_addr()) &&
+      !ip6_addr_test_zone(ip6_current_src_addr(), netif)) ||
+      ip6_addr_isloopback(ip6_current_src_addr())) {
+    LWIP_DEBUGF(IP6_DEBUG, ("ip6_forward: not forwarding packet beyond its source address zone.\n"));
+    IP6_STATS_INC(ip6.rterr);
+    IP6_STATS_INC(ip6.drop);
+    return;
+  }
+#endif /* LWIP_IPV6_SCOPES */
   /* Do not forward packets onto the same network interface on which
    * they arrived. */
   if (netif == inp) {
@@ -459,17 +521,23 @@ ip6_input(struct pbuf *p, struct netif *inp)
   pbuf_realloc(p, IP6_HLEN + IP6H_PLEN(ip6hdr));
 
   /* copy IP addresses to aligned ip6_addr_t */
-  ip_addr_copy_from_ip6(ip_data.current_iphdr_dest, ip6hdr->dest);
-  ip_addr_copy_from_ip6(ip_data.current_iphdr_src, ip6hdr->src);
+  ip_addr_copy_from_ip6_packed(ip_data.current_iphdr_dest, ip6hdr->dest);
+  ip_addr_copy_from_ip6_packed(ip_data.current_iphdr_src, ip6hdr->src);
 
-  /* Don't accept virtual IPv6 mapped IPv4 addresses */
+  /* Don't accept virtual IPv6 mapped IPv4 addresses.
+   * Don't accept multicast source addresses. */
   if (ip6_addr_isipv6mappedipv4(ip_2_ip6(&ip_data.current_iphdr_dest)) ||
-     ip6_addr_isipv6mappedipv4(ip_2_ip6(&ip_data.current_iphdr_src))     ) {
+     ip6_addr_isipv6mappedipv4(ip_2_ip6(&ip_data.current_iphdr_src)) ||
+     ip6_addr_ismulticast(ip_2_ip6(&ip_data.current_iphdr_src))) {
     IP6_STATS_INC(ip6.err);
     IP6_STATS_INC(ip6.drop);
     return ERR_OK;
   }
 
+  /* Set the appropriate zone identifier on the addresses. */
+  ip6_addr_assign_zone(ip_2_ip6(&ip_data.current_iphdr_dest), IP6_UNKNOWN, inp);
+  ip6_addr_assign_zone(ip_2_ip6(&ip_data.current_iphdr_src), IP6_UNICAST, inp);
+
   /* current header pointer. */
   ip_data.current_ip6_header = ip6hdr;
 
@@ -517,29 +585,46 @@ ip6_input(struct pbuf *p, struct netif *inp)
       /* interface is up? */
       if (netif_is_up(netif)) {
         /* unicast to this interface address? address configured? */
+        /* If custom scopes are used, the destination zone will be tested as
+         * part of the local-address comparison, but we need to test the source
+         * scope as well (e.g., is this interface on the same link?). */
         for (i = 0; i < LWIP_IPV6_NUM_ADDRESSES; i++) {
           if (ip6_addr_isvalid(netif_ip6_addr_state(netif, i)) &&
-              ip6_addr_cmp(ip6_current_dest_addr(), netif_ip6_addr(netif, i))) {
+              ip6_addr_cmp(ip6_current_dest_addr(), netif_ip6_addr(netif, i))
+#if IPV6_CUSTOM_SCOPES
+              && (!ip6_addr_has_zone(ip6_current_src_addr()) ||
+                  ip6_addr_test_zone(ip6_current_src_addr(), netif))
+#endif /* IPV6_CUSTOM_SCOPES */
+          ) {
             /* exit outer loop */
             goto netif_found;
           }
         }
       }
       if (first) {
-        if (ip6_addr_islinklocal(ip6_current_dest_addr())
+#if !IPV6_CUSTOM_SCOPES
+        /* Shortcut: stop looking for other interfaces if either the source or
+         * the destination has a scope constrained to this interface. Custom
+         * scopes may break the 1:1 link/interface mapping, however. */
+        if (ip6_addr_islinklocal(ip6_current_dest_addr()) ||
+            ip6_addr_islinklocal(ip6_current_src_addr())) {
+          netif = NULL;
+          break;
+        }
+#endif /* !IPV6_CUSTOM_SCOPES */
 #if !LWIP_NETIF_LOOPBACK || LWIP_HAVE_LOOPIF
-            || ip6_addr_isloopback(ip6_current_dest_addr())
-#endif /* !LWIP_NETIF_LOOPBACK || LWIP_HAVE_LOOPIF */
-        ) {
-          /* Do not match link-local addresses to other netifs. The loopback
-           * address is to be considered link-local and packets to it should be
-           * dropped on other interfaces, as per RFC 4291 Sec. 2.5.3. This
-           * requirement cannot be implemented in the case that loopback
-           * traffic is sent across a non-loopback interface, however.
-           */
+        /* The loopback address is to be considered link-local. Packets to it
+         * should be dropped on other interfaces, as per RFC 4291 Sec. 2.5.3.
+         * Its implied scope means packets *from* the loopback address should
+         * not be accepted on other interfaces, either. These requirements
+         * cannot be implemented in the case that loopback traffic is sent
+         * across a non-loopback interface, however. */
+        if (ip6_addr_isloopback(ip6_current_dest_addr()) ||
+            ip6_addr_isloopback(ip6_current_src_addr())) {
           netif = NULL;
           break;
         }
+#endif /* !LWIP_NETIF_LOOPBACK || LWIP_HAVE_LOOPIF */
         first = 0;
         netif = netif_list;
       } else {
@@ -818,8 +903,10 @@ ip6_input_cleanup:
             IPv6 header and p->payload points to that IPv6 header)
  * @param src the source IPv6 address to send from (if src == IP6_ADDR_ANY, an
  *         IP address of the netif is selected and used as source address.
- *         if src == NULL, IP6_ADDR_ANY is used as source)
- * @param dest the destination IPv6 address to send the packet to
+ *         if src == NULL, IP6_ADDR_ANY is used as source) (src is possibly not
+ *         properly zoned)
+ * @param dest the destination IPv6 address to send the packet to (possibly not
+ *             properly zoned)
  * @param hl the Hop Limit value to be set in the IPv6 header
  * @param tc the Traffic Class value to be set in the IPv6 header
  * @param nexth the Next Header to be set in the IPv6 header
@@ -864,6 +951,20 @@ ip6_output_if_src(struct pbuf *p, const ip6_addr_t *src, const ip6_addr_t *dest,
 
   /* Should the IPv6 header be generated or is it already included in p? */
   if (dest != LWIP_IP_HDRINCL) {
+#if LWIP_IPV6_SCOPES
+    /* If the destination address is scoped but lacks a zone, add a zone now,
+     * based on the outgoing interface. The lower layers (e.g., nd6) absolutely
+     * require addresses to be properly zoned for correctness. In some cases,
+     * earlier attempts will have been made to add a zone to the destination,
+     * but this function is the only one that is called in all (other) cases,
+     * so we must do this here. */
+    if (ip6_addr_lacks_zone(dest, IP6_UNKNOWN)) {
+      ip6_addr_copy(dest_addr, *dest);
+      ip6_addr_assign_zone(&dest_addr, IP6_UNKNOWN, netif);
+      dest = &dest_addr;
+    }
+#endif /* LWIP_IPV6_SCOPES */
+
     /* generate IPv6 header */
     if (pbuf_header(p, IP6_HLEN)) {
       LWIP_DEBUGF(IP6_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("ip6_output: not enough room for IPv6 header in pbuf\n"));
@@ -879,7 +980,7 @@ ip6_output_if_src(struct pbuf *p, const ip6_addr_t *src, const ip6_addr_t *dest,
     IP6H_NEXTH_SET(ip6hdr, nexth);
 
     /* dest cannot be NULL here */
-    ip6_addr_copy(ip6hdr->dest, *dest);
+    ip6_addr_copy_to_packed(ip6hdr->dest, *dest);
 
     IP6H_VTCFL_SET(ip6hdr, 6, tc, 0);
     IP6H_PLEN_SET(ip6hdr, p->tot_len - IP6_HLEN);
@@ -888,12 +989,13 @@ ip6_output_if_src(struct pbuf *p, const ip6_addr_t *src, const ip6_addr_t *dest,
       src = IP6_ADDR_ANY6;
     }
     /* src cannot be NULL here */
-    ip6_addr_copy(ip6hdr->src, *src);
+    ip6_addr_copy_to_packed(ip6hdr->src, *src);
 
   } else {
     /* IP header already included in p */
     ip6hdr = (struct ip6_hdr *)p->payload;
-    ip6_addr_copy(dest_addr, ip6hdr->dest);
+    ip6_addr_copy_from_packed(dest_addr, ip6hdr->dest);
+    ip6_addr_assign_zone(&dest_addr, IP6_UNKNOWN, netif);
     dest = &dest_addr;
   }
 
@@ -964,8 +1066,8 @@ ip6_output(struct pbuf *p, const ip6_addr_t *src, const ip6_addr_t *dest,
   } else {
     /* IP header included in p, read addresses. */
     ip6hdr = (struct ip6_hdr *)p->payload;
-    ip6_addr_copy(src_addr, ip6hdr->src);
-    ip6_addr_copy(dest_addr, ip6hdr->dest);
+    ip6_addr_copy_from_packed(src_addr, ip6hdr->src);
+    ip6_addr_copy_from_packed(dest_addr, ip6hdr->dest);
     netif = ip6_route(&src_addr, &dest_addr);
   }
 
@@ -1023,8 +1125,8 @@ ip6_output_hinted(struct pbuf *p, const ip6_addr_t *src, const ip6_addr_t *dest,
   } else {
     /* IP header included in p, read addresses. */
     ip6hdr = (struct ip6_hdr *)p->payload;
-    ip6_addr_copy(src_addr, ip6hdr->src);
-    ip6_addr_copy(dest_addr, ip6hdr->dest);
+    ip6_addr_copy_from_packed(src_addr, ip6hdr->src);
+    ip6_addr_copy_from_packed(dest_addr, ip6hdr->dest);
     netif = ip6_route(&src_addr, &dest_addr);
   }
 
diff --git a/src/core/ipv6/ip6_addr.c b/src/core/ipv6/ip6_addr.c
index aa06659..61a2442 100644
--- a/src/core/ipv6/ip6_addr.c
+++ b/src/core/ipv6/ip6_addr.c
@@ -160,6 +160,8 @@ ip6addr_aton(const char *cp, ip6_addr_t *addr)
     return 0;
   }
 
+  ip6_addr_clear_zone(addr);
+
   return 1;
 }
 
diff --git a/src/core/ipv6/ip6_frag.c b/src/core/ipv6/ip6_frag.c
index 50d7347..e97e952 100644
--- a/src/core/ipv6/ip6_frag.c
+++ b/src/core/ipv6/ip6_frag.c
@@ -169,7 +169,15 @@ ip6_reass_free_complete_datagram(struct ip6_reassdata *ipr)
       LWIP_ASSERT("ip6_reass_free: moving p->payload to ip6 header failed\n", 0);
     }
     else {
-      icmp6_time_exceeded(p, ICMP6_TE_FRAG);
+      /* Reconstruct the zoned source and destination addresses, so that we do
+       * not end up sending the ICMP response over the wrong link. */
+      ip6_addr_t src_addr, dest_addr;
+      ip6_addr_copy_from_packed(src_addr, IPV6_FRAG_SRC(ipr));
+      ip6_addr_set_zone(&src_addr, ipr->src_zone);
+      ip6_addr_copy_from_packed(dest_addr, IPV6_FRAG_DEST(ipr));
+      ip6_addr_set_zone(&dest_addr, ipr->dest_zone);
+      /* Send the actual ICMP response. */
+      icmp6_time_exceeded_with_addrs(p, ICMP6_TE_FRAG, &src_addr, &dest_addr);
     }
     clen = pbuf_clen(p);
     LWIP_ASSERT("pbufs_freed + clen <= 0xffff", pbufs_freed + clen <= 0xffff);
@@ -297,8 +305,8 @@ ip6_reass(struct pbuf *p)
        in the reassembly buffer. If so, we proceed with copying the
        fragment into the buffer. */
     if ((frag_hdr->_identification == ipr->identification) &&
-        ip6_addr_cmp(ip6_current_src_addr(), &(IPV6_FRAG_SRC(ipr))) &&
-        ip6_addr_cmp(ip6_current_dest_addr(), &(IPV6_FRAG_DEST(ipr)))) {
+        ip6_addr_cmp_packed(ip6_current_src_addr(), &(IPV6_FRAG_SRC(ipr)), ipr->src_zone) &&
+        ip6_addr_cmp_packed(ip6_current_dest_addr(), &(IPV6_FRAG_DEST(ipr)), ipr->dest_zone)) {
       IP6_FRAG_STATS_INC(ip6_frag.cachehit);
       break;
     }
@@ -345,7 +353,16 @@ ip6_reass(struct pbuf *p)
     MEMCPY(&ipr->src, &ip6_current_header()->src, sizeof(ipr->src));
     MEMCPY(&ipr->dest, &ip6_current_header()->dest, sizeof(ipr->dest));
 #endif /* IPV6_FRAG_COPYHEADER */
-
+#if LWIP_IPV6_SCOPES
+    /* Also store the address zone information.
+     * @todo It is possible that due to netif destruction and recreation, the
+     * stored zones end up resolving to a different interface. In that case, we
+     * risk sending a "time exceeded" ICMP response over the wrong link.
+     * Ideally, netif destruction would clean up matching pending reassembly
+     * structures, but custom zone mappings would make that non-trivial. */
+    ipr->src_zone = ip6_addr_zone(ip6_current_src_addr());
+    ipr->dest_zone = ip6_addr_zone(ip6_current_dest_addr());
+#endif /* LWIP_IPV6_SCOPES */
     /* copy the fragmented packet id. */
     ipr->identification = frag_hdr->_identification;
 
@@ -489,7 +506,8 @@ ip6_reass(struct pbuf *p)
      * overwrite, so that we can restore the original later. */
     MEMCPY(ipr->orig_hdr, p->payload, sizeof(*iprh));
     /* For IPV6_FRAG_COPYHEADER there is no need to copy src/dst again, as they
-     * will be the same as they were. */
+     * will be the same as they were. With LWIP_IPV6_SCOPES, the same applies
+     * to the source/destination zones. */
   }
   /* Only after the backup do we get to fill in the actual helper structure. */
   iprh->next_pbuf = next_pbuf;
diff --git a/src/core/ipv6/mld6.c b/src/core/ipv6/mld6.c
index 0cd0078..1db59af 100644
--- a/src/core/ipv6/mld6.c
+++ b/src/core/ipv6/mld6.c
@@ -293,11 +293,17 @@ mld6_input(struct pbuf *p, struct netif *inp)
 
 /**
  * @ingroup mld6
- * Join a group on a network interface.
+ * Join a group on one or all network interfaces.
+ *
+ * If the group is to be joined on all interfaces, the given group address must
+ * not have a zone set (i.e., it must have its zone index set to IP6_NO_ZONE).
+ * If the group is to be joined on one particular interface, the given group
+ * address may or may not have a zone set.
  *
- * @param srcaddr ipv6 address of the network interface which should
- *                join a new group. If IP6_ADDR_ANY, join on all netifs
- * @param groupaddr the ipv6 address of the group to join
+ * @param srcaddr ipv6 address (zoned) of the network interface which should
+ *                join a new group. If IP6_ADDR_ANY6, join on all netifs
+ * @param groupaddr the ipv6 address of the group to join (possibly but not
+ *                  necessarily zoned)
  * @return ERR_OK if group was joined on the netif(s), an err_t otherwise
  */
 err_t
@@ -330,13 +336,26 @@ mld6_joingroup(const ip6_addr_t *srcaddr, const ip6_addr_t *groupaddr)
  * Join a group on a network interface.
  *
  * @param netif the network interface which should join a new group.
- * @param groupaddr the ipv6 address of the group to join
+ * @param groupaddr the ipv6 address of the group to join (possibly but not
+ *                  necessarily zoned)
  * @return ERR_OK if group was joined on the netif, an err_t otherwise
  */
 err_t
 mld6_joingroup_netif(struct netif *netif, const ip6_addr_t *groupaddr)
 {
   struct mld_group *group;
+#if LWIP_IPV6_SCOPES
+  ip6_addr_t ip6addr;
+
+  /* If the address has a particular scope but no zone set, use the netif to
+   * set one now. Within the mld6 module, all addresses are properly zoned. */
+  if (ip6_addr_lacks_zone(groupaddr, IP6_MULTICAST)) {
+    ip6_addr_set(&ip6addr, groupaddr);
+    ip6_addr_assign_zone(&ip6addr, IP6_MULTICAST, netif);
+    groupaddr = &ip6addr;
+  }
+  IP6_ADDR_ZONECHECK_NETIF(groupaddr, netif);
+#endif /* LWIP_IPV6_SCOPES */
 
   /* find group or create a new one if not found */
   group = mld6_lookfor_group(netif, groupaddr);
@@ -368,9 +387,12 @@ mld6_joingroup_netif(struct netif *netif, const ip6_addr_t *groupaddr)
  * @ingroup mld6
  * Leave a group on a network interface.
  *
- * @param srcaddr ipv6 address of the network interface which should
- *                leave the group. If IP6_ISANY, leave on all netifs
- * @param groupaddr the ipv6 address of the group to leave
+ * Zoning of address follows the same rules as @ref mld6_joingroup.
+ *
+ * @param srcaddr ipv6 address (zoned) of the network interface which should
+ *                leave the group. If IP6_ADDR_ANY6, leave on all netifs
+ * @param groupaddr the ipv6 address of the group to leave (possibly, but not
+ *                  necessarily zoned)
  * @return ERR_OK if group was left on the netif(s), an err_t otherwise
  */
 err_t
@@ -403,13 +425,24 @@ mld6_leavegroup(const ip6_addr_t *srcaddr, const ip6_addr_t *groupaddr)
  * Leave a group on a network interface.
  *
  * @param netif the network interface which should leave the group.
- * @param groupaddr the ipv6 address of the group to leave
+ * @param groupaddr the ipv6 address of the group to leave (possibly, but not
+ *                  necessarily zoned)
  * @return ERR_OK if group was left on the netif, an err_t otherwise
  */
 err_t
 mld6_leavegroup_netif(struct netif *netif, const ip6_addr_t *groupaddr)
 {
   struct mld_group *group;
+#if LWIP_IPV6_SCOPES
+  ip6_addr_t ip6addr;
+
+  if (ip6_addr_lacks_zone(groupaddr, IP6_MULTICAST)) {
+    ip6_addr_set(&ip6addr, groupaddr);
+    ip6_addr_assign_zone(&ip6addr, IP6_MULTICAST, netif);
+    groupaddr = &ip6addr;
+  }
+  IP6_ADDR_ZONECHECK_NETIF(groupaddr, netif);
+#endif /* LWIP_IPV6_SCOPES */
 
   /* find group */
   group = mld6_lookfor_group(netif, groupaddr);
@@ -561,7 +594,7 @@ mld6_send(struct netif *netif, struct mld_group *group, u8_t type)
   mld_hdr->chksum = 0;
   mld_hdr->max_resp_delay = 0;
   mld_hdr->reserved = 0;
-  ip6_addr_set(&(mld_hdr->multicast_address), &(group->group_address));
+  ip6_addr_copy_to_packed(mld_hdr->multicast_address, group->group_address);
 
 #if CHECKSUM_GEN_ICMP6
   IF__NETIF_CHECKSUM_ENABLED(netif, NETIF_CHECKSUM_GEN_ICMP6) {
diff --git a/src/core/ipv6/nd6.c b/src/core/ipv6/nd6.c
index c6c12df..23a1208 100644
--- a/src/core/ipv6/nd6.c
+++ b/src/core/ipv6/nd6.c
@@ -238,6 +238,7 @@ nd6_process_autoconfig_prefix(struct netif *netif,
    * already did exist, resulting in that address being given lifetimes. */
   IP6_ADDR(&ip6addr, prefix_addr->addr[0], prefix_addr->addr[1],
     netif_ip6_addr(netif, 0)->addr[2], netif_ip6_addr(netif, 0)->addr[3]);
+  ip6_addr_assign_zone(&ip6addr, IP6_UNICAST, netif);
 
   free_idx = 0;
   for (i = 1; i < LWIP_IPV6_NUM_ADDRESSES; i++) {
@@ -294,8 +295,9 @@ nd6_input(struct pbuf *p, struct netif *inp)
 
     na_hdr = (struct na_header *)p->payload;
 
-    /* Create an aligned copy of the target address. */
-    ip6_addr_set(&target_address, &(na_hdr->target_address));
+    /* Create an aligned, zoned copy of the target address. */
+    ip6_addr_copy_from_packed(target_address, na_hdr->target_address);
+    ip6_addr_assign_zone(&target_address, IP6_UNICAST, inp);
 
     /* Check a subset of the other RFC 4861 Sec. 7.1.2 requirements. */
     if (IP6H_HOPLIM(ip6_current_header()) != ND6_HOPLIM || na_hdr->code != 0 ||
@@ -424,8 +426,9 @@ nd6_input(struct pbuf *p, struct netif *inp)
 
     ns_hdr = (struct ns_header *)p->payload;
 
-    /* Create an aligned copy of the target address. */
-    ip6_addr_set(&target_address, &(ns_hdr->target_address));
+    /* Create an aligned, zoned copy of the target address. */
+    ip6_addr_copy_from_packed(target_address, ns_hdr->target_address);
+    ip6_addr_assign_zone(&target_address, IP6_UNICAST, inp);
 
     /* Check a subset of the other RFC 4861 Sec. 7.1.1 requirements. */
     if (IP6H_HOPLIM(ip6_current_header()) != ND6_HOPLIM || ns_hdr->code != 0 ||
@@ -538,7 +541,7 @@ nd6_input(struct pbuf *p, struct netif *inp)
     u8_t *buffer; /* Used to copy options. */
     u16_t offset;
 #if LWIP_ND6_RDNSS_MAX_DNS_SERVERS
-    /* There can by multiple RDNSS options per RA */
+    /* There can be multiple RDNSS options per RA */
     u8_t rdnss_server_idx = 0;
 #endif /* LWIP_ND6_RDNSS_MAX_DNS_SERVERS */
 
@@ -662,7 +665,8 @@ nd6_input(struct pbuf *p, struct netif *inp)
         prefix_opt = (struct prefix_option *)buffer;
 
         /* Get a memory-aligned copy of the prefix. */
-        ip6_addr_set(&prefix_addr, &(prefix_opt->prefix));
+        ip6_addr_copy_from_packed(prefix_addr, prefix_opt->prefix);
+        ip6_addr_assign_zone(&prefix_addr, IP6_UNICAST, inp);
 
         if (!ip6_addr_islinklocal(&prefix_addr)) {
           if ((prefix_opt->flags & ND6_PREFIX_FLAG_ON_LINK) &&
@@ -710,8 +714,9 @@ nd6_input(struct pbuf *p, struct netif *inp)
         for (n = 0; (rdnss_server_idx < DNS_MAX_SERVERS) && (n < num); n++) {
           ip_addr_t rdnss_address;
 
-          /* Get a memory-aligned copy of the prefix. */
-          ip_addr_copy_from_ip6(rdnss_address, rdnss_opt->rdnss_address[n]);
+          /* Get a memory-aligned, zoned copy of the prefix. */
+          ip_addr_copy_from_ip6_packed(rdnss_address, rdnss_opt->rdnss_address[n]);
+          ip6_addr_assign_zone(ip_2_ip6(&rdnss_address), IP6_UNKNOWN, inp);
 
           if (htonl(rdnss_opt->lifetime) > 0) {
             /* TODO implement Lifetime > 0 */
@@ -758,8 +763,9 @@ nd6_input(struct pbuf *p, struct netif *inp)
 
     redir_hdr = (struct redirect_header *)p->payload;
 
-    /* Copy original destination address, to have an aligned copy. */
-    ip6_addr_set(&destination_address, &(redir_hdr->destination_address));
+    /* Create an aligned, zoned copy of the destination address. */
+    ip6_addr_copy_from_packed(destination_address, redir_hdr->destination_address);
+    ip6_addr_assign_zone(&destination_address, IP6_UNICAST, inp);
 
     /* Check a subset of the other RFC 4861 Sec. 8.1 requirements. */
     if (!ip6_addr_islinklocal(ip6_current_src_addr()) ||
@@ -792,22 +798,23 @@ nd6_input(struct pbuf *p, struct netif *inp)
       return;
     }
 
+    /* Create an aligned, zoned copy of the target address. */
+    ip6_addr_copy_from_packed(target_address, redir_hdr->target_address);
+    ip6_addr_assign_zone(&target_address, IP6_UNICAST, inp);
+
     /* Set the new target address. */
-    ip6_addr_set(&(destination_cache[i].next_hop_addr), &(redir_hdr->target_address));
+    ip6_addr_copy(destination_cache[i].next_hop_addr, target_address);
 
     /* If Link-layer address of other router is given, try to add to neighbor cache. */
     if (lladdr_opt != NULL) {
       if (lladdr_opt->type == ND6_OPTION_TYPE_TARGET_LLADDR) {
-        /* Copy target address to current source address, to have an aligned copy. */
-        ip6_addr_set(&target_address, &(redir_hdr->target_address));
-
         i = nd6_find_neighbor_cache_entry(&target_address);
         if (i < 0) {
           i = nd6_new_neighbor_cache_entry();
           if (i >= 0) {
             neighbor_cache[i].netif = inp;
             MEMCPY(neighbor_cache[i].lladdr, lladdr_opt->addr, inp->hwaddr_len);
-            ip6_addr_set(&(neighbor_cache[i].next_hop_address), &target_address);
+            ip6_addr_copy(neighbor_cache[i].next_hop_address, target_address);
 
             /* Receiving a message does not prove reachability: only in one direction.
              * Delay probe in case we get confirmation of reachability from upper layer (TCP). */
@@ -833,7 +840,7 @@ nd6_input(struct pbuf *p, struct netif *inp)
     struct icmp6_hdr *icmp6hdr; /* Packet too big message */
     struct ip6_hdr *ip6hdr; /* IPv6 header of the packet which caused the error */
     u32_t pmtu;
-    ip6_addr_t tmp;
+    ip6_addr_t destination_address;
 
     /* Check that ICMPv6 header + IPv6 header fit in payload */
     if (p->len < (sizeof(struct icmp6_hdr) + IP6_HLEN)) {
@@ -847,11 +854,12 @@ nd6_input(struct pbuf *p, struct netif *inp)
     icmp6hdr = (struct icmp6_hdr *)p->payload;
     ip6hdr = (struct ip6_hdr *)((u8_t*)p->payload + sizeof(struct icmp6_hdr));
 
-    /* Copy original destination address to current source address, to have an aligned copy. */
-    ip6_addr_set(&tmp, &(ip6hdr->dest));
+    /* Create an aligned, zoned copy of the destination address. */
+    ip6_addr_copy_from_packed(destination_address, ip6hdr->dest);
+    ip6_addr_assign_zone(&destination_address, IP6_UNKNOWN, inp);
 
     /* Look for entry in destination cache. */
-    i = nd6_find_destination_cache_entry(&tmp);
+    i = nd6_find_destination_cache_entry(&destination_address);
     if (i < 0) {
       /* Destination not in cache, drop packet. */
       pbuf_free(p);
@@ -1110,6 +1118,8 @@ nd6_send_ns(struct netif *netif, const ip6_addr_t *target_addr, u8_t flags)
   const ip6_addr_t *src_addr;
   u16_t lladdr_opt_len;
 
+  LWIP_ASSERT("target address is required", target_addr != NULL);
+
   if (!(flags & ND6_SEND_FLAG_ANY_SRC) &&
       ip6_addr_isvalid(netif_ip6_addr_state(netif,0))) {
     /* Use link-local address as source address. */
@@ -1136,7 +1146,7 @@ nd6_send_ns(struct netif *netif, const ip6_addr_t *target_addr, u8_t flags)
   ns_hdr->code = 0;
   ns_hdr->chksum = 0;
   ns_hdr->reserved = 0;
-  ip6_addr_set(&(ns_hdr->target_address), target_addr);
+  ip6_addr_copy_to_packed(ns_hdr->target_address, *target_addr);
 
   if (lladdr_opt_len != 0) {
     struct lladdr_option *lladdr_opt = (struct lladdr_option *)((u8_t*)p->payload + sizeof(struct ns_header));
@@ -1148,6 +1158,7 @@ nd6_send_ns(struct netif *netif, const ip6_addr_t *target_addr, u8_t flags)
   /* Generate the solicited node address for the target address. */
   if (flags & ND6_SEND_FLAG_MULTICAST_DEST) {
     ip6_addr_set_solicitednode(&multicast_address, target_addr->addr[3]);
+    ip6_addr_assign_zone(&multicast_address, IP6_MULTICAST, netif);
     target_addr = &multicast_address;
   }
 
@@ -1182,6 +1193,8 @@ nd6_send_na(struct netif *netif, const ip6_addr_t *target_addr, u8_t flags)
   const ip6_addr_t *dest_addr;
   u16_t lladdr_opt_len;
 
+  LWIP_ASSERT("target address is required", target_addr != NULL);
+
   /* Use link-local address as source address. */
   /* src_addr = netif_ip6_addr(netif, 0); */
   /* Use target address as source address. */
@@ -1206,7 +1219,7 @@ nd6_send_na(struct netif *netif, const ip6_addr_t *target_addr, u8_t flags)
   na_hdr->reserved[0] = 0;
   na_hdr->reserved[1] = 0;
   na_hdr->reserved[2] = 0;
-  ip6_addr_set(&(na_hdr->target_address), target_addr);
+  ip6_addr_copy_to_packed(na_hdr->target_address, *target_addr);
 
   lladdr_opt->type = ND6_OPTION_TYPE_TARGET_LLADDR;
   lladdr_opt->length = (u8_t)lladdr_opt_len;
@@ -1215,9 +1228,11 @@ nd6_send_na(struct netif *netif, const ip6_addr_t *target_addr, u8_t flags)
   /* Generate the solicited node address for the target address. */
   if (flags & ND6_SEND_FLAG_MULTICAST_DEST) {
     ip6_addr_set_solicitednode(&multicast_address, target_addr->addr[3]);
+    ip6_addr_assign_zone(&multicast_address, IP6_MULTICAST, netif);
     dest_addr = &multicast_address;
   } else if (flags & ND6_SEND_FLAG_ALLNODES_DEST) {
     ip6_addr_set_allnodes_linklocal(&multicast_address);
+    ip6_addr_assign_zone(&multicast_address, IP6_MULTICAST, netif);
     dest_addr = &multicast_address;
   } else {
     dest_addr = ip6_current_src_addr();
@@ -1262,6 +1277,7 @@ nd6_send_rs(struct netif *netif)
 
   /* Generate the all routers target address. */
   ip6_addr_set_allrouters_linklocal(&multicast_address);
+  ip6_addr_assign_zone(&multicast_address, IP6_MULTICAST, netif);
 
   /* Allocate a packet. */
   if (src_addr != IP6_ADDR_ANY6) {
@@ -1477,6 +1493,9 @@ static s8_t
 nd6_find_destination_cache_entry(const ip6_addr_t *ip6addr)
 {
   s8_t i;
+
+  IP6_ADDR_ZONECHECK(ip6addr);
+
   for (i = 0; i < LWIP_ND6_NUM_DESTINATIONS; i++) {
     if (ip6_addr_cmp(ip6addr, &(destination_cache[i].destination_addr))) {
       return i;
@@ -1693,6 +1712,8 @@ nd6_get_router(const ip6_addr_t *router_addr, struct netif *netif)
 {
   s8_t i;
 
+  IP6_ADDR_ZONECHECK_NETIF(router_addr, netif);
+
   /* Look for router. */
   for (i = 0; i < LWIP_ND6_NUM_ROUTERS; i++) {
     if ((default_router_list[i].neighbor_entry != NULL) &&
@@ -1720,6 +1741,8 @@ nd6_new_router(const ip6_addr_t *router_addr, struct netif *netif)
   s8_t free_router_index;
   s8_t neighbor_index;
 
+  IP6_ADDR_ZONECHECK_NETIF(router_addr, netif);
+
   /* Do we have a neighbor entry for this router? */
   neighbor_index = nd6_find_neighbor_cache_entry(router_addr);
   if (neighbor_index < 0) {
@@ -1838,6 +1861,8 @@ nd6_get_next_hop_entry(const ip6_addr_t *ip6addr, struct netif *netif)
 #endif /* LWIP_HOOK_ND6_GET_GW */
   s8_t i;
 
+  IP6_ADDR_ZONECHECK_NETIF(ip6addr, netif);
+
 #if LWIP_NETIF_HWADDRHINT
   if (netif->addr_hint != NULL) {
     /* per-pcb cached entry was given */
@@ -2106,7 +2131,9 @@ nd6_send_q(s8_t i)
     /* Get ipv6 header. */
     ip6hdr = (struct ip6_hdr *)(q->p->payload);
     /* Create an aligned copy. */
-    ip6_addr_set(&dest, &(ip6hdr->dest));
+    ip6_addr_copy_from_packed(dest, ip6hdr->dest);
+    /* Restore the zone, if applicable. */
+    ip6_addr_assign_zone(&dest, IP6_UNKNOWN, neighbor_cache[i].netif);
     /* send the queued IPv6 packet */
     (neighbor_cache[i].netif)->output_ip6(neighbor_cache[i].netif, q->p, &dest);
     /* free the queued IP packet */
@@ -2119,7 +2146,9 @@ nd6_send_q(s8_t i)
     /* Get ipv6 header. */
     ip6hdr = (struct ip6_hdr *)(neighbor_cache[i].q->payload);
     /* Create an aligned copy. */
-    ip6_addr_set(&dest, &(ip6hdr->dest));
+    ip6_addr_copy_from_packed(dest, ip6hdr->dest);
+    /* Restore the zone, if applicable. */
+    ip6_addr_assign_zone(&dest, IP6_UNKNOWN, neighbor_cache[i].netif);
     /* send the queued IPv6 packet */
     (neighbor_cache[i].netif)->output_ip6(neighbor_cache[i].netif, neighbor_cache[i].q, &dest);
     /* free the queued IP packet */
@@ -2317,6 +2346,7 @@ nd6_adjust_mld_membership(struct netif *netif, s8_t addr_idx, u8_t new_state)
 
   if (old_member != new_member) {
     ip6_addr_set_solicitednode(&multicast_address, netif_ip6_addr(netif, addr_idx)->addr[3]);
+    ip6_addr_assign_zone(&multicast_address, IP6_MULTICAST, netif);
 
     if (new_member) {
       mld6_joingroup_netif(netif, &multicast_address);
diff --git a/src/core/netif.c b/src/core/netif.c
index 5522f3f..afd1458 100644
--- a/src/core/netif.c
+++ b/src/core/netif.c
@@ -1042,6 +1042,7 @@ void
 netif_ip6_addr_set_parts(struct netif *netif, s8_t addr_idx, u32_t i0, u32_t i1, u32_t i2, u32_t i3)
 {
   const ip6_addr_t *old_addr;
+  ip_addr_t new_ipaddr;
   LWIP_ASSERT("netif != NULL", netif != NULL);
   LWIP_ASSERT("invalid index", addr_idx < LWIP_IPV6_NUM_ADDRESSES);
 
@@ -1051,11 +1052,10 @@ netif_ip6_addr_set_parts(struct netif *netif, s8_t addr_idx, u32_t i0, u32_t i1,
       (old_addr->addr[2] != i2) || (old_addr->addr[3] != i3)) {
     LWIP_DEBUGF(NETIF_DEBUG | LWIP_DBG_STATE, ("netif_ip6_addr_set: netif address being changed\n"));
 
+    IP_ADDR6(&new_ipaddr, i0, i1, i2, i3);
+    ip6_addr_assign_zone(ip_2_ip6(&new_ipaddr), IP6_UNICAST, netif);
+
     if (netif_ip6_addr_state(netif, addr_idx) & IP6_ADDR_VALID) {
-#if LWIP_TCP || LWIP_UDP
-      ip_addr_t new_ipaddr;
-      IP_ADDR6(&new_ipaddr, i0, i1, i2, i3);
-#endif /* LWIP_TCP || LWIP_UDP */
 #if LWIP_TCP
       tcp_netif_ip_addr_changed(netif_ip_addr6(netif, addr_idx), &new_ipaddr);
 #endif /* LWIP_TCP */
@@ -1068,8 +1068,7 @@ netif_ip6_addr_set_parts(struct netif *netif, s8_t addr_idx, u32_t i0, u32_t i1,
     }
     /* @todo: remove/readd mib2 ip6 entries? */
 
-    IP6_ADDR(ip_2_ip6(&(netif->ip6_addr[addr_idx])), i0, i1, i2, i3);
-    IP_SET_TYPE_VAL(netif->ip6_addr[addr_idx], IPADDR_TYPE_V6);
+    ip_addr_copy(netif->ip6_addr[addr_idx], new_ipaddr);
 
     if (netif_ip6_addr_state(netif, addr_idx) & IP6_ADDR_VALID) {
       netif_issue_reports(netif, NETIF_REPORT_TYPE_IPV6);
@@ -1130,6 +1129,8 @@ netif_ip6_addr_set_state(struct netif* netif, s8_t addr_idx, u8_t state)
 
     if (!old_valid && new_valid) {
       /* address added by setting valid */
+      /* This is a good moment to check that the address is properly zoned. */
+      IP6_ADDR_ZONECHECK_NETIF(netif_ip6_addr(netif, addr_idx), netif);
       /* @todo: add mib2 ip6 entries? */
       netif_issue_reports(netif, NETIF_REPORT_TYPE_IPV6);
     }
@@ -1150,6 +1151,10 @@ netif_ip6_addr_set_state(struct netif* netif, s8_t addr_idx, u8_t state)
  * index. Depending on its state, it may or may not be assigned to the
  * interface (as per RFC terminology).
  *
+ * The given address may or may not be zoned (i.e., have a zone index other
+ * than IP6_NO_ZONE). If the address is zoned, it must have the correct zone
+ * for the given netif, or no match will be found.
+ *
  * @param netif the netif to check
  * @param ip6addr the IPv6 address to find
  * @return >= 0: address found, this is its index
@@ -1159,9 +1164,16 @@ s8_t
 netif_get_ip6_addr_match(struct netif *netif, const ip6_addr_t *ip6addr)
 {
   s8_t i;
+
+#if LWIP_IPV6_SCOPES
+  if (ip6_addr_has_zone(ip6addr) && !ip6_addr_test_zone(ip6addr, netif)) {
+    return -1; /* wrong zone, no match */
+  }
+#endif /* LWIP_IPV6_SCOPES */
+
   for (i = 0; i < LWIP_IPV6_NUM_ADDRESSES; i++) {
     if (!ip6_addr_isinvalid(netif_ip6_addr_state(netif, i)) &&
-        ip6_addr_cmp(netif_ip6_addr(netif, i), ip6addr)) {
+        ip6_addr_cmp_zoneless(netif_ip6_addr(netif, i), ip6addr)) {
       return i;
     }
   }
@@ -1210,6 +1222,16 @@ netif_create_ip6_linklocal_address(struct netif *netif, u8_t from_mac_48bit)
     }
   }
 
+  /* Set a link-local zone. Even though the zone is implied by the owning
+   * netif, setting the zone anyway has two important conceptual advantages:
+   * 1) it avoids the need for a ton of exceptions in internal code, allowing
+   *    e.g. ip6_addr_cmp() to be used on local addresses;
+   * 2) the properly zoned address is visible externally, e.g. when any outside
+   *    code enumerates available addresses or uses one to bind a socket.
+   * Any external code unaware of address scoping is likely to just ignore the
+   * zone field, so this should not create any compatibility problems. */
+  ip6_addr_assign_zone(ip_2_ip6(&netif->ip6_addr[0]), IP6_UNICAST, netif);
+
   /* Set address state. */
 #if LWIP_IPV6_DUP_DETECT_ATTEMPTS
   /* Will perform duplicate address detection (DAD). */
@@ -1244,10 +1266,11 @@ netif_add_ip6_address(struct netif *netif, const ip6_addr_t *ip6addr, s8_t *chos
     return ERR_OK;
   }
 
-  /* Find a free slot -- musn't be the first one (reserved for link local) */
-  for (i = 1; i < LWIP_IPV6_NUM_ADDRESSES; i++) {
+  /* Find a free slot. The first one is reserved for link-local addresses. */
+  for (i = ip6_addr_islinklocal(ip6addr) ? 0 : 1; i < LWIP_IPV6_NUM_ADDRESSES; i++) {
     if (ip6_addr_isinvalid(netif_ip6_addr_state(netif, i))) {
       ip_addr_copy_from_ip6(netif->ip6_addr[i], *ip6addr);
+      ip6_addr_assign_zone(ip_2_ip6(&netif->ip6_addr[i]), IP6_UNICAST, netif);
       netif_ip6_addr_set_state(netif, i, IP6_ADDR_TENTATIVE);
       if (chosen_idx != NULL) {
         *chosen_idx = i;
diff --git a/src/core/raw.c b/src/core/raw.c
index 4828df6..42fe025 100644
--- a/src/core/raw.c
+++ b/src/core/raw.c
@@ -215,6 +215,15 @@ raw_bind(struct raw_pcb *pcb, const ip_addr_t *ipaddr)
     return ERR_VAL;
   }
   ip_addr_set_ipaddr(&pcb->local_ip, ipaddr);
+#if LWIP_IPV6 && LWIP_IPV6_SCOPES
+  /* If the given IP address should have a zone but doesn't, assign one now.
+   * This is legacy support: scope-aware callers should always provide properly
+   * zoned source addresses. */
+  if (IP_IS_V6(&pcb->local_ip) &&
+      ip6_addr_lacks_zone(ip_2_ip6(&pcb->local_ip), IP6_UNICAST)) {
+    ip6_addr_select_zone(ip_2_ip6(&pcb->local_ip), ip_2_ip6(&pcb->local_ip));
+  }
+#endif /* LWIP_IPV6 && LWIP_IPV6_SCOPES */
   return ERR_OK;
 }
 
@@ -239,6 +248,14 @@ raw_connect(struct raw_pcb *pcb, const ip_addr_t *ipaddr)
     return ERR_VAL;
   }
   ip_addr_set_ipaddr(&pcb->remote_ip, ipaddr);
+#if LWIP_IPV6 && LWIP_IPV6_SCOPES
+  /* If the given IP address should have a zone but doesn't, assign one now,
+   * using the bound address to make a more informed decision when possible. */
+  if (IP_IS_V6(&pcb->remote_ip) &&
+      ip6_addr_lacks_zone(ip_2_ip6(&pcb->remote_ip), IP6_UNKNOWN)) {
+    ip6_addr_select_zone(ip_2_ip6(&pcb->remote_ip), ip_2_ip6(&pcb->local_ip));
+  }
+#endif /* LWIP_IPV6 && LWIP_IPV6_SCOPES */
   pcb->flags |= RAW_FLAGS_CONNECTED;
   return ERR_OK;
 }
diff --git a/src/core/tcp.c b/src/core/tcp.c
index 75be86b..f24c7da 100644
--- a/src/core/tcp.c
+++ b/src/core/tcp.c
@@ -542,6 +542,9 @@ tcp_bind(struct tcp_pcb *pcb, const ip_addr_t *ipaddr, u16_t port)
   int i;
   int max_pcb_list = NUM_TCP_PCB_LISTS;
   struct tcp_pcb *cpcb;
+#if LWIP_IPV6 && LWIP_IPV6_SCOPES
+  ip_addr_t zoned_ipaddr;
+#endif /* LWIP_IPV6 && LWIP_IPV6_SCOPES */
 
 #if LWIP_IPV4
   /* Don't propagate NULL pointer (IPv4 ANY) to subsequent functions */
@@ -568,6 +571,18 @@ tcp_bind(struct tcp_pcb *pcb, const ip_addr_t *ipaddr, u16_t port)
   }
 #endif /* SO_REUSE */
 
+#if LWIP_IPV6 && LWIP_IPV6_SCOPES
+  /* If the given IP address should have a zone but doesn't, assign one now.
+   * This is legacy support: scope-aware callers should always provide properly
+   * zoned source addresses. Do the zone selection before the address-in-use
+   * check below; as such we have to make a temporary copy of the address. */
+  if (IP_IS_V6(ipaddr) && ip6_addr_lacks_zone(ip_2_ip6(ipaddr), IP6_UNICAST)) {
+    ip_addr_copy(zoned_ipaddr, *ipaddr);
+    ip6_addr_select_zone(ip_2_ip6(&zoned_ipaddr), ip_2_ip6(&zoned_ipaddr));
+    ipaddr = &zoned_ipaddr;
+  }
+#endif /* LWIP_IPV6 && LWIP_IPV6_SCOPES */
+
   if (port == 0) {
     port = tcp_new_port();
     if (port == 0) {
@@ -855,6 +870,7 @@ err_t
 tcp_connect(struct tcp_pcb *pcb, const ip_addr_t *ipaddr, u16_t port,
       tcp_connected_fn connected)
 {
+  struct netif *netif = NULL;
   err_t ret;
   u32_t iss;
   u16_t old_local_port;
@@ -872,7 +888,6 @@ tcp_connect(struct tcp_pcb *pcb, const ip_addr_t *ipaddr, u16_t port,
   /* check if we have a route to the remote host */
   if (ip_addr_isany(&pcb->local_ip)) {
     /* no local IP address set, yet. */
-    struct netif *netif;
     const ip_addr_t *local_ip;
     ip_route_get_local_ip(&pcb->local_ip, &pcb->remote_ip, netif, local_ip);
     if ((netif == NULL) || (local_ip == NULL)) {
@@ -882,7 +897,24 @@ tcp_connect(struct tcp_pcb *pcb, const ip_addr_t *ipaddr, u16_t port,
     }
     /* Use the address as local address of the pcb. */
     ip_addr_copy(pcb->local_ip, *local_ip);
+  } else {
+    netif = ip_route(&pcb->local_ip, &pcb->remote_ip);
+    if (netif == NULL) {
+      /* Don't even try to send a SYN packet if we have no route
+         since that will fail. */
+      return ERR_RTE;
+    }
+  }
+  LWIP_ASSERT("netif != NULL", netif != NULL);
+
+#if LWIP_IPV6 && LWIP_IPV6_SCOPES
+  /* If the given IP address should have a zone but doesn't, assign one now.
+   * Given that we already have the target netif, this is easy and cheap. */
+  if (IP_IS_V6(&pcb->remote_ip) &&
+      ip6_addr_lacks_zone(ip_2_ip6(&pcb->remote_ip), IP6_UNICAST)) {
+    ip6_addr_assign_zone(ip_2_ip6(&pcb->remote_ip), IP6_UNICAST, netif);
   }
+#endif /* LWIP_IPV6 && LWIP_IPV6_SCOPES */
 
   old_local_port = pcb->local_port;
   if (pcb->local_port == 0) {
diff --git a/src/core/udp.c b/src/core/udp.c
index a916414..a38cc37 100644
--- a/src/core/udp.c
+++ b/src/core/udp.c
@@ -522,16 +522,12 @@ udp_sendto_chksum(struct udp_pcb *pcb, struct pbuf *p, const ip_addr_t *dst_ip,
 
   LWIP_DEBUGF(UDP_DEBUG | LWIP_DBG_TRACE, ("udp_send\n"));
 
-#if LWIP_IPV6 || (LWIP_IPV4 && LWIP_MULTICAST_TX_OPTIONS)
+#if LWIP_IPV4 && LWIP_MULTICAST_TX_OPTIONS
   if (ip_addr_ismulticast(dst_ip_route)) {
 #if LWIP_IPV6
-    if (IP_IS_V6(dst_ip)) {
-      /* For multicast, find a netif based on source address. */
-      dst_ip_route = &pcb->local_ip;
-    } else
+    if (IP_IS_V4(dst_ip))
 #endif /* LWIP_IPV6 */
     {
-#if LWIP_IPV4 && LWIP_MULTICAST_TX_OPTIONS
       /* IPv4 does not use source-based routing by default, so we use an
          administratively selected interface for multicast by default.
          However, this can be overridden by setting an interface address
@@ -540,10 +536,9 @@ udp_sendto_chksum(struct udp_pcb *pcb, struct pbuf *p, const ip_addr_t *dst_ip,
           !ip4_addr_cmp(ip_2_ip4(&pcb->multicast_ip), IP4_ADDR_BROADCAST)) {
         dst_ip_route = &pcb->multicast_ip;
       }
-#endif /* LWIP_IPV4 && LWIP_MULTICAST_TX_OPTIONS */
     }
   }
-#endif /* LWIP_IPV6 || (LWIP_IPV4 && LWIP_MULTICAST_TX_OPTIONS) */
+#endif /* LWIP_IPV4 && LWIP_MULTICAST_TX_OPTIONS */
 
   /* find the outgoing network interface for this packet */
   if(IP_IS_ANY_TYPE_VAL(pcb->local_ip)) {
@@ -883,6 +878,9 @@ udp_bind(struct udp_pcb *pcb, const ip_addr_t *ipaddr, u16_t port)
 {
   struct udp_pcb *ipcb;
   u8_t rebind;
+#if LWIP_IPV6 && LWIP_IPV6_SCOPES
+  ip_addr_t zoned_ipaddr;
+#endif /* LWIP_IPV6 && LWIP_IPV6_SCOPES */
 
 #if LWIP_IPV4
   /* Don't propagate NULL pointer (IPv4 ANY) to subsequent functions */
@@ -910,6 +908,18 @@ udp_bind(struct udp_pcb *pcb, const ip_addr_t *ipaddr, u16_t port)
     }
   }
 
+#if LWIP_IPV6 && LWIP_IPV6_SCOPES
+  /* If the given IP address should have a zone but doesn't, assign one now.
+   * This is legacy support: scope-aware callers should always provide properly
+   * zoned source addresses. Do the zone selection before the address-in-use
+   * check below; as such we have to make a temporary copy of the address. */
+  if (IP_IS_V6(ipaddr) && ip6_addr_lacks_zone(ip_2_ip6(ipaddr), IP6_UNICAST)) {
+    ip_addr_copy(zoned_ipaddr, *ipaddr);
+    ip6_addr_select_zone(ip_2_ip6(&zoned_ipaddr), ip_2_ip6(&zoned_ipaddr));
+    ipaddr = &zoned_ipaddr;
+  }
+#endif /* LWIP_IPV6 && LWIP_IPV6_SCOPES */
+
   /* no port specified? */
   if (port == 0) {
     port = udp_new_port();
@@ -994,6 +1004,15 @@ udp_connect(struct udp_pcb *pcb, const ip_addr_t *ipaddr, u16_t port)
   }
 
   ip_addr_set_ipaddr(&pcb->remote_ip, ipaddr);
+#if LWIP_IPV6 && LWIP_IPV6_SCOPES
+  /* If the given IP address should have a zone but doesn't, assign one now,
+   * using the bound address to make a more informed decision when possible. */
+  if (IP_IS_V6(&pcb->remote_ip) &&
+      ip6_addr_lacks_zone(ip_2_ip6(&pcb->remote_ip), IP6_UNKNOWN)) {
+    ip6_addr_select_zone(ip_2_ip6(&pcb->remote_ip), ip_2_ip6(&pcb->local_ip));
+  }
+#endif /* LWIP_IPV6 && LWIP_IPV6_SCOPES */
+
   pcb->remote_port = port;
   pcb->flags |= UDP_FLAGS_CONNECTED;
 
diff --git a/src/include/lwip/icmp6.h b/src/include/lwip/icmp6.h
index a29dc8c..374ff44 100644
--- a/src/include/lwip/icmp6.h
+++ b/src/include/lwip/icmp6.h
@@ -57,6 +57,8 @@ void icmp6_input(struct pbuf *p, struct netif *inp);
 void icmp6_dest_unreach(struct pbuf *p, enum icmp6_dur_code c);
 void icmp6_packet_too_big(struct pbuf *p, u32_t mtu);
 void icmp6_time_exceeded(struct pbuf *p, enum icmp6_te_code c);
+void icmp6_time_exceeded_with_addrs(struct pbuf *p, enum icmp6_te_code c,
+    const ip6_addr_t *src_addr, const ip6_addr_t *dest_addr);
 void icmp6_param_problem(struct pbuf *p, enum icmp6_pp_code c, u32_t pointer);
 
 #endif /* LWIP_ICMP6 && LWIP_IPV6 */
diff --git a/src/include/lwip/inet.h b/src/include/lwip/inet.h
index 4a34f02..2982a0f 100644
--- a/src/include/lwip/inet.h
+++ b/src/include/lwip/inet.h
@@ -134,8 +134,6 @@ extern const struct in6_addr in6addr_any;
 
 #define inet_addr_from_ip4addr(target_inaddr, source_ipaddr) ((target_inaddr)->s_addr = ip4_addr_get_u32(source_ipaddr))
 #define inet_addr_to_ip4addr(target_ipaddr, source_inaddr)   (ip4_addr_set_u32(target_ipaddr, (source_inaddr)->s_addr))
-/* ATTENTION: the next define only works because both s_addr and ip4_addr_t are an u32_t effectively! */
-#define inet_addr_to_ip4addr_p(target_ip4addr_p, source_inaddr)   ((target_ip4addr_p) = (ip4_addr_t*)&((source_inaddr)->s_addr))
 
 /* directly map this to the lwip internal functions */
 #define inet_addr(cp)                   ipaddr_addr(cp)
@@ -153,9 +151,8 @@ extern const struct in6_addr in6addr_any;
 #define inet6_addr_to_ip6addr(target_ip6addr, source_in6addr)   {(target_ip6addr)->addr[0] = (source_in6addr)->un.u32_addr[0]; \
                                                                  (target_ip6addr)->addr[1] = (source_in6addr)->un.u32_addr[1]; \
                                                                  (target_ip6addr)->addr[2] = (source_in6addr)->un.u32_addr[2]; \
-                                                                 (target_ip6addr)->addr[3] = (source_in6addr)->un.u32_addr[3];}
-/* ATTENTION: the next define only works because both in6_addr and ip6_addr_t are an u32_t[4] effectively! */
-#define inet6_addr_to_ip6addr_p(target_ip6addr_p, source_in6addr)   ((target_ip6addr_p) = (ip6_addr_t*)(source_in6addr))
+                                                                 (target_ip6addr)->addr[3] = (source_in6addr)->un.u32_addr[3]; \
+                                                                 ip6_addr_clear_zone(target_ip6addr);}
 
 /* directly map this to the lwip internal functions */
 #define inet6_aton(cp, addr)            ip6addr_aton(cp, (ip6_addr_t*)addr)
diff --git a/src/include/lwip/ip6_addr.h b/src/include/lwip/ip6_addr.h
index 6215bc0..e111833 100644
--- a/src/include/lwip/ip6_addr.h
+++ b/src/include/lwip/ip6_addr.h
@@ -47,6 +47,7 @@
 
 #if LWIP_IPV6  /* don't build if not configured for use in lwipopts.h */
 
+#include "lwip/ip6_zone.h"
 
 #ifdef __cplusplus
 extern "C" {
@@ -57,6 +58,9 @@ extern "C" {
     used as local variable, on the stack, etc. */
 struct ip6_addr {
   u32_t addr[4];
+#if LWIP_IPV6_SCOPES
+  u8_t zone;
+#endif /* LWIP_IPV6_SCOPES */
 };
 
 /** IPv6 address */
@@ -72,7 +76,8 @@ typedef struct ip6_addr ip6_addr_t;
   (ip6addr)->addr[0] = idx0; \
   (ip6addr)->addr[1] = idx1; \
   (ip6addr)->addr[2] = idx2; \
-  (ip6addr)->addr[3] = idx3; } while(0)
+  (ip6addr)->addr[3] = idx3; \
+  ip6_addr_clear_zone(ip6addr); } while(0)
 
 /** Access address in 16-bit block */
 #define IP6_ADDR_BLOCK1(ip6addr) ((u16_t)((lwip_htonl((ip6addr)->addr[0]) >> 16) & 0xffff))
@@ -95,18 +100,34 @@ typedef struct ip6_addr ip6_addr_t;
 #define ip6_addr_copy(dest, src) do{(dest).addr[0] = (src).addr[0]; \
                                     (dest).addr[1] = (src).addr[1]; \
                                     (dest).addr[2] = (src).addr[2]; \
-                                    (dest).addr[3] = (src).addr[3];}while(0)
+                                    (dest).addr[3] = (src).addr[3]; \
+                                    ip6_addr_copy_zone((dest), (src)); }while(0)
 /** Safely copy one IPv6 address to another (src may be NULL) */
 #define ip6_addr_set(dest, src) do{(dest)->addr[0] = (src) == NULL ? 0 : (src)->addr[0]; \
                                    (dest)->addr[1] = (src) == NULL ? 0 : (src)->addr[1]; \
                                    (dest)->addr[2] = (src) == NULL ? 0 : (src)->addr[2]; \
-                                   (dest)->addr[3] = (src) == NULL ? 0 : (src)->addr[3];}while(0)
+                                   (dest)->addr[3] = (src) == NULL ? 0 : (src)->addr[3]; \
+                                   ip6_addr_set_zone((dest), (src) == NULL ? IP6_NO_ZONE : ip6_addr_zone(src)); }while(0)
+
+/** Copy packed IPv6 address to unpacked IPv6 address; zone is not set */
+#define ip6_addr_copy_from_packed(dest, src) do{(dest).addr[0] = (src).addr[0]; \
+                                    (dest).addr[1] = (src).addr[1]; \
+                                    (dest).addr[2] = (src).addr[2]; \
+                                    (dest).addr[3] = (src).addr[3]; \
+                                    ip6_addr_clear_zone(&dest); }while(0)
+
+/** Copy unpacked IPv6 address to packed IPv6 address; zone is lost */
+#define ip6_addr_copy_to_packed(dest, src) do{(dest).addr[0] = (src).addr[0]; \
+                                    (dest).addr[1] = (src).addr[1]; \
+                                    (dest).addr[2] = (src).addr[2]; \
+                                    (dest).addr[3] = (src).addr[3]; }while(0)
 
 /** Set complete address to zero */
 #define ip6_addr_set_zero(ip6addr)    do{(ip6addr)->addr[0] = 0; \
                                          (ip6addr)->addr[1] = 0; \
                                          (ip6addr)->addr[2] = 0; \
-                                         (ip6addr)->addr[3] = 0;}while(0)
+                                         (ip6addr)->addr[3] = 0; \
+                                         ip6_addr_clear_zone(ip6addr);}while(0)
 
 /** Set address to ipv6 'any' (no need for lwip_htonl()) */
 #define ip6_addr_set_any(ip6addr)       ip6_addr_set_zero(ip6addr)
@@ -114,33 +135,57 @@ typedef struct ip6_addr ip6_addr_t;
 #define ip6_addr_set_loopback(ip6addr) do{(ip6addr)->addr[0] = 0; \
                                           (ip6addr)->addr[1] = 0; \
                                           (ip6addr)->addr[2] = 0; \
-                                          (ip6addr)->addr[3] = PP_HTONL(0x00000001UL);}while(0)
+                                          (ip6addr)->addr[3] = PP_HTONL(0x00000001UL); \
+                                          ip6_addr_clear_zone(ip6addr);}while(0)
 /** Safely copy one IPv6 address to another and change byte order
  * from host- to network-order. */
 #define ip6_addr_set_hton(dest, src) do{(dest)->addr[0] = (src) == NULL ? 0 : lwip_htonl((src)->addr[0]); \
                                         (dest)->addr[1] = (src) == NULL ? 0 : lwip_htonl((src)->addr[1]); \
                                         (dest)->addr[2] = (src) == NULL ? 0 : lwip_htonl((src)->addr[2]); \
-                                        (dest)->addr[3] = (src) == NULL ? 0 : lwip_htonl((src)->addr[3]);}while(0)
+                                        (dest)->addr[3] = (src) == NULL ? 0 : lwip_htonl((src)->addr[3]); \
+                                        ip6_addr_set_zone((dest), (src) == NULL ? IP6_NO_ZONE : ip6_addr_zone(src));}while(0)
 
 
+/** Compare IPv6 networks, ignoring zone information. To be used sparingly! */
+#define ip6_addr_netcmp_zoneless(addr1, addr2) (((addr1)->addr[0] == (addr2)->addr[0]) && \
+                                               ((addr1)->addr[1] == (addr2)->addr[1]))
+
 /**
  * Determine if two IPv6 address are on the same network.
  *
- * @arg addr1 IPv6 address 1
- * @arg addr2 IPv6 address 2
- * @return !0 if the network identifiers of both address match
+ * @param addr1 IPv6 address 1
+ * @param addr2 IPv6 address 2
+ * @return 1 if the network identifiers of both address match, 0 if not
  */
-#define ip6_addr_netcmp(addr1, addr2) (((addr1)->addr[0] == (addr2)->addr[0]) && \
-                                       ((addr1)->addr[1] == (addr2)->addr[1]))
+#define ip6_addr_netcmp(addr1, addr2) (ip6_addr_netcmp_zoneless((addr1), (addr2)) && \
+                                       ip6_addr_cmp_zone((addr1), (addr2)))
 
 /* Exact-host comparison *after* ip6_addr_netcmp() succeeded, for efficiency. */
 #define ip6_addr_nethostcmp(addr1, addr2) (((addr1)->addr[2] == (addr2)->addr[2]) && \
                                            ((addr1)->addr[3] == (addr2)->addr[3]))
 
-#define ip6_addr_cmp(addr1, addr2) (((addr1)->addr[0] == (addr2)->addr[0]) && \
+/** Compare IPv6 addresses, ignoring zone information. To be used sparingly! */
+#define ip6_addr_cmp_zoneless(addr1, addr2) (((addr1)->addr[0] == (addr2)->addr[0]) && \
                                     ((addr1)->addr[1] == (addr2)->addr[1]) && \
                                     ((addr1)->addr[2] == (addr2)->addr[2]) && \
                                     ((addr1)->addr[3] == (addr2)->addr[3]))
+/**
+ * Determine if two IPv6 addresses are the same. In particular, the address
+ * part of both must be the same, and the zone must be compatible.
+ *
+ * @param addr1 IPv6 address 1
+ * @param addr2 IPv6 address 2
+ * @return 1 if the addresses are considered equal, 0 if not
+ */
+#define ip6_addr_cmp(addr1, addr2) (ip6_addr_cmp_zoneless((addr1), (addr2)) && \
+                                    ip6_addr_cmp_zone((addr1), (addr2)))
+
+/** Compare IPv6 address to packed address and zone */
+#define ip6_addr_cmp_packed(ip6addr, paddr, zone_idx) (((ip6addr)->addr[0] == (paddr)->addr[0]) && \
+                                    ((ip6addr)->addr[1] == (paddr)->addr[1]) && \
+                                    ((ip6addr)->addr[2] == (paddr)->addr[2]) && \
+                                    ((ip6addr)->addr[3] == (paddr)->addr[3]) && \
+                                    ip6_addr_equals_zone((ip6addr), (zone_idx)))
 
 #define ip6_get_subnet_id(ip6addr)   (lwip_htonl((ip6addr)->addr[2]) & 0x0000ffffUL)
 
@@ -187,7 +232,13 @@ typedef struct ip6_addr ip6_addr_t;
 #define ip6_addr_ismulticast_orglocal(ip6addr) (((ip6addr)->addr[0] & PP_HTONL(0xff8f0000UL)) == PP_HTONL(0xff080000UL))
 #define ip6_addr_ismulticast_global(ip6addr) (((ip6addr)->addr[0] & PP_HTONL(0xff8f0000UL)) == PP_HTONL(0xff0e0000UL))
 
-/* @todo define get/set for well-know multicast addresses, e.g. ff02::1 */
+/* Scoping note: while interface-local and link-local multicast addresses do
+ * have a scope (i.e., they are meaningful only in the context of a particular
+ * interface), the following functions are not assigning or comparing zone
+ * indices. The reason for this is backward compatibility. Any call site that
+ * produces a non-global multicast address must assign a multicast address as
+ * appropriate itself. */
+
 #define ip6_addr_isallnodes_iflocal(ip6addr) (((ip6addr)->addr[0] == PP_HTONL(0xff010000UL)) && \
     ((ip6addr)->addr[1] == 0UL) && \
     ((ip6addr)->addr[2] == 0UL) && \
@@ -200,7 +251,8 @@ typedef struct ip6_addr ip6_addr_t;
 #define ip6_addr_set_allnodes_linklocal(ip6addr) do{(ip6addr)->addr[0] = PP_HTONL(0xff020000UL); \
                 (ip6addr)->addr[1] = 0; \
                 (ip6addr)->addr[2] = 0; \
-                (ip6addr)->addr[3] = PP_HTONL(0x00000001UL);}while(0)
+                (ip6addr)->addr[3] = PP_HTONL(0x00000001UL); \
+                ip6_addr_clear_zone(ip6addr); }while(0)
 
 #define ip6_addr_isallrouters_linklocal(ip6addr) (((ip6addr)->addr[0] == PP_HTONL(0xff020000UL)) && \
     ((ip6addr)->addr[1] == 0UL) && \
@@ -209,7 +261,8 @@ typedef struct ip6_addr ip6_addr_t;
 #define ip6_addr_set_allrouters_linklocal(ip6addr) do{(ip6addr)->addr[0] = PP_HTONL(0xff020000UL); \
                 (ip6addr)->addr[1] = 0; \
                 (ip6addr)->addr[2] = 0; \
-                (ip6addr)->addr[3] = PP_HTONL(0x00000002UL);}while(0)
+                (ip6addr)->addr[3] = PP_HTONL(0x00000002UL); \
+                ip6_addr_clear_zone(ip6addr); }while(0)
 
 #define ip6_addr_issolicitednode(ip6addr) ( ((ip6addr)->addr[0] == PP_HTONL(0xff020000UL)) && \
         ((ip6addr)->addr[2] == PP_HTONL(0x00000001UL)) && \
@@ -218,7 +271,8 @@ typedef struct ip6_addr ip6_addr_t;
 #define ip6_addr_set_solicitednode(ip6addr, if_id) do{(ip6addr)->addr[0] = PP_HTONL(0xff020000UL); \
                 (ip6addr)->addr[1] = 0; \
                 (ip6addr)->addr[2] = PP_HTONL(0x00000001UL); \
-                (ip6addr)->addr[3] = (PP_HTONL(0xff000000UL) | (if_id));}while(0)
+                (ip6addr)->addr[3] = (PP_HTONL(0xff000000UL) | (if_id)); \
+                ip6_addr_clear_zone(ip6addr); }while(0)
 
 #define ip6_addr_cmp_solicitednode(ip6addr, sn_addr) (((ip6addr)->addr[0] == PP_HTONL(0xff020000UL)) && \
                                     ((ip6addr)->addr[1] == 0) && \
diff --git a/src/include/lwip/ip6_frag.h b/src/include/lwip/ip6_frag.h
index 3176d52..87e0e86 100644
--- a/src/include/lwip/ip6_frag.h
+++ b/src/include/lwip/ip6_frag.h
@@ -105,6 +105,10 @@ struct ip6_reassdata {
   u16_t datagram_len;
   u8_t nexth;
   u8_t timer;
+#if LWIP_IPV6_SCOPES
+  u8_t src_zone; /* zone of original packet's source address */
+  u8_t dest_zone; /* zone of original packet's destination address */
+#endif /* LWIP_IPV6_SCOPES */
 };
 
 #define ip6_reass_init() /* Compatibility define */
diff --git a/src/include/lwip/ip6_zone.h b/src/include/lwip/ip6_zone.h
new file mode 100644
index 0000000..d16a6b8
--- /dev/null
+++ b/src/include/lwip/ip6_zone.h
@@ -0,0 +1,278 @@
+/**
+ * @file
+ *
+ * IPv6 address scopes, zones, and scoping policy.
+ *
+ * This header provides the means to implement support for IPv6 address scopes,
+ * as per RFC 4007. An address scope can be either global or more constrained.
+ * In lwIP, we say that an address "has a scope" or "is scoped" when its scope
+ * is constrained, in which case the address is meaningful only in a specific
+ * "zone." For unicast addresses, only link-local addresses have a scope; in
+ * that case, the scope is the link. For multicast addresses, there are various
+ * scopes defined by RFC 4007 and others. For any constrained scope, a system
+ * must establish a (potentially one-to-many) mapping between zones and local
+ * interfaces. For example, a link-local address is valid on only one link (its
+ * zone). That link may be attached to one or more local interfaces. The
+ * decisions on which scopes are constrained and the mapping between zones and
+ * interfaces is together what we refer to as the "scoping policy" - more on
+ * this in a bit.
+ *
+ * In lwIP, each IPv6 address has an associated zone index. This zone index may
+ * be set to "no zone" (IP6_NO_ZONE, 0) or an actual zone. We say that an
+ * address "has a zone" or "is zoned" when its zone index is *not* set to "no
+ * zone." In lwIP, in principle, each address should be "properly zoned," which
+ * means that if the address has a zone if and only if has a scope. As such, it
+ * is a rule that an unscoped (e.g., global) address must never have a zone.
+ * Even though one could argue that there is always one zone even for global
+ * scopes, this rule exists for implementation simplicity. Violation of the
+ * rule will trigger assertions or otherwise result in undesired behavior.
+ *
+ * Backward compatibility prevents us from requiring that applications always
+ * provide properly zoned addresses. We do enforce the rule that the in the
+ * lwIP link layer (everything below netif->output_ip6() and in particular ND6)
+ * *all* addresses are properly zoned. Thus, on the output paths down the
+ * stack, various places deal with the case of addresses that lack a zone.
+ * Some of them are best-effort for efficiency (e.g. the PCB bind and connect
+ * API calls' attempts to add missing zones); ultimately the IPv6 output
+ * handler (@ref ip6_output_if_src) will set a zone if necessary.
+ *
+ * Aside from dealing with scoped addresses lacking a zone, a proper IPv6
+ * implementation must also ensure that a packet with a scoped source and/or
+ * destination address does not leave its zone. This is currently implemented
+ * in the input and forward functions. However, for output, these checks are
+ * deliberately omitted in order to keep the implementation lightweight. The
+ * routing algorithm in @ref ip6_route will take decisions such that it will
+ * not cause zone violations unless the application sets bad addresses, though.
+ *
+ * In terms of scoping policy, lwIP implements the default policy from RFC 4007
+ * using macros in this file. This policy considers link-local unicast
+ * addresses and (only) interface-local and link-local multicast addresses as
+ * having a scope. For all these addresses, the zone is equal to the interface.
+ * As shown below in this file, it is possible to implement a custom policy.
+ */
+
+/*
+ * Copyright (c) 2017 The MINIX 3 Project.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 3. The name of the author may not be used to endorse or promote products
+ *    derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
+ * SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+ * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
+ * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
+ * OF SUCH DAMAGE.
+ *
+ * This file is part of the lwIP TCP/IP stack.
+ *
+ * Author: David van Moolenbroek <david@minix3.org>
+ *
+ */
+#ifndef LWIP_HDR_IP6_ZONE_H
+#define LWIP_HDR_IP6_ZONE_H
+
+#if LWIP_IPV6  /* don't build if not configured for use in lwipopts.h */
+
+/** Identifier for "no zone". */
+#define IP6_NO_ZONE 0
+
+#if LWIP_IPV6_SCOPES
+
+/** Zone initializer for static IPv6 address initialization, including comma. */
+#define IPADDR6_ZONE_INIT , IP6_NO_ZONE
+
+/** Return the zone index of the given IPv6 address; possibly "no zone". */
+#define ip6_addr_zone(ip6addr) ((ip6addr)->zone)
+
+/** Does the given IPv6 address have a zone set? (0/1) */
+#define ip6_addr_has_zone(ip6addr) (ip6_addr_zone(ip6addr) != IP6_NO_ZONE)
+
+/** Set the zone field of an IPv6 address to a particular value. */
+#define ip6_addr_set_zone(ip6addr, zone_idx) ((ip6addr)->zone = (zone_idx))
+
+/** Clear the zone field of an IPv6 address, setting it to "no zone". */
+#define ip6_addr_clear_zone(ip6addr) ((ip6addr)->zone = IP6_NO_ZONE)
+
+/** Copy the zone field from the second IPv6 address to the first one. */
+#define ip6_addr_copy_zone(ip6addr1, ip6addr2) ((ip6addr1).zone = (ip6addr2).zone)
+
+/** Is the zone field of the given IPv6 address equal to the given zone index? (0/1) */
+#define ip6_addr_equals_zone(ip6addr, zone_idx) ((ip6addr)->zone == (zone_idx))
+
+/** Are the zone fields of the given IPv6 addresses equal? (0/1)
+ * This macro must only be used on IPv6 addresses of the same scope. */
+#define ip6_addr_cmp_zone(ip6addr1, ip6addr2) ((ip6addr1)->zone == (ip6addr2)->zone)
+
+/** Symbolic constants for the 'type' parameters in some of the macros below.
+ * These exist for efficiency only, allowing the macros to avoid certain tests
+ * when the address is known not to be of a certain type. Dead code elimination
+ * will do the rest. IP6_MULTICAST is supported but currently not optimized. */
+#define IP6_UNKNOWN     0
+#define IP6_UNICAST     1
+#define IP6_MULTICAST   2
+
+/** IPV6_CUSTOM_SCOPES: together, the following three macro definitions,
+ * @ref ip6_addr_has_scope, @ref ip6_addr_assign_zone, and
+ * @ref ip6_addr_test_zone, completely define the lwIP scoping policy.
+ * The definitions below implement the default policy from RFC 4007 Sec. 6.
+ * Should an implementation desire to implement a different policy, it can
+ * define IPV6_CUSTOM_SCOPES to 1 and supply its own definitions for the three
+ * macros instead.
+ */
+#ifndef IPV6_CUSTOM_SCOPES
+#define IPV6_CUSTOM_SCOPES 0
+#endif /* !IPV6_CUSTOM_SCOPES */
+
+#if !IPV6_CUSTOM_SCOPES
+
+/**
+ * Determine whether an IPv6 address has a constrained scope, and as such is
+ * meaningful only if accompanied by a zone index to identify the scope's zone.
+ * The given address type may be used to eliminate at compile time certain
+ * checks that will evaluate to false at run time anyway.
+ *
+ * This default implementation follows the default model of RFC 4007, where
+ * only interface-local and link-local scopes are defined.
+ *
+ * Even though the unicast loopback address does have an implied link-local
+ * scope, in this implementation it does not have an explicitly assigned zone
+ * index. As such it should not be tested for in this macro.
+ *
+ * @param ip6addr the IPv6 address (const); only its address part is examined.
+ * @param type address type; one of IP6_UNICAST, IP6_MULTICAST, IP6_UNKNOWN.
+ * @return 1 if the address has a constrained scope, 0 if it does not.
+ */
+#define ip6_addr_has_scope(ip6addr, type) \
+  (ip6_addr_islinklocal(ip6addr) || (((type) != IP6_UNICAST) && \
+   ip6_addr_ismulticast_iflocal(ip6addr) || \
+   ip6_addr_ismulticast_linklocal(ip6addr)))
+
+/**
+ * Assign a zone index to an IPv6 address, based on a network interface. If the
+ * given address has a scope, the assigned zone index is that scope's zone of
+ * the given netif; otherwise, the assigned zone index is "no zone".
+ *
+ * This default implementation follows the default model of RFC 4007, where
+ * only interface-local and link-local scopes are defined, and the zone index
+ * of both of those scopes always equals the index of the network interface.
+ * As such, this default implementation need not distinguish between different
+ * constrained scopes when assigning the zone.
+ *
+ * @param ip6addr the IPv6 address; its address part is examined, and its zone
+ *                index is assigned.
+ * @param type address type; one of IP6_UNICAST, IP6_MULTICAST, IP6_UNKNOWN.
+ * @param netif the network interface (const).
+ */
+#define ip6_addr_assign_zone(ip6addr, type, netif) \
+    (ip6_addr_set_zone((ip6addr), \
+      ip6_addr_has_scope((ip6addr), (type)) ? netif_num_to_index(netif) : 0))
+
+/**
+ * Test whether an IPv6 address is "zone-compatible" with a network interface.
+ * That is, test whether the network interface is part of the zone associated
+ * with the address. For efficiency, this macro is only ever called if the
+ * given address is either scoped or zoned, and thus, it need not test this.
+ * If an address is scoped but not zoned, or zoned and not scoped, it is
+ * considered not zone-compatible with any netif.
+ *
+ * This default implementation follows the default model of RFC 4007, where
+ * only interface-local and link-local scopes are defined, and the zone index
+ * of both of those scopes always equals the index of the network interface.
+ * As such, there is always only one matching netif for a specific zone index,
+ * but all call sites of this macro currently support multiple matching netifs
+ * as well (at no additional expense in the common case).
+ *
+ * @param ip6addr the IPv6 address (const).
+ * @param netif the network interface (const).
+ * @return 1 if the address is scope-compatible with the netif, 0 if not.
+ */
+#define ip6_addr_test_zone(ip6addr, netif) \
+    (ip6_addr_equals_zone((ip6addr), netif_num_to_index(netif)))
+
+#endif /* !IPV6_CUSTOM_SCOPES */
+
+/** Does the given IPv6 address have a scope, and as such should also have a
+ * zone to be meaningful, but does not actually have a zone? (0/1) */
+#define ip6_addr_lacks_zone(ip6addr, type) \
+    (!ip6_addr_has_zone(ip6addr) && ip6_addr_has_scope((ip6addr), (type)))
+
+/**
+ * Try to select a zone for a scoped address that does not yet have a zone.
+ * Called from PCB bind and connect routines, for two reasons: 1) to save on
+ * this (relatively expensive) selection for every individual packet route
+ * operation and 2) to allow the application to obtain the selected zone from
+ * the PCB as is customary for e.g. getsockname/getpeername BSD socket calls.
+ *
+ * Ideally, callers would always supply a properly zoned address, in which case
+ * this function would not be needed. It exists both for compatibility with the
+ * BSD socket API (which accepts zoneless destination addresses) and for
+ * backward compatibility with pre-scoping lwIP code.
+ *
+ * It may be impossible to select a zone, e.g. if there are no netifs.  In that
+ * case, the address's zone field will be left as is.
+ *
+ * @param dest the IPv6 address for which to select and set a zone.
+ * @param src source IPv6 address (const); may be equal to dest.
+ */
+#define ip6_addr_select_zone(dest, src) do { struct netif *selected_netif; \
+  selected_netif = ip6_route((src), (dest)); \
+  if (selected_netif != NULL) { \
+    ip6_addr_assign_zone((dest), IP6_UNKNOWN, selected_netif); \
+  } } while (0)
+
+#else /* LWIP_IPV6_SCOPES */
+
+#define IPADDR6_ZONE_INIT
+#define ip6_addr_zone(ip6addr) (IP6_NO_ZONE)
+#define ip6_addr_has_zone(ip6addr) (0)
+#define ip6_addr_set_zone(ip6addr, zone_idx)
+#define ip6_addr_clear_zone(ip6addr)
+#define ip6_addr_copy_zone(ip6addr1, ip6addr2)
+#define ip6_addr_equals_zone(ip6addr, zone_idx) (1)
+#define ip6_addr_cmp_zone(ip6addr1, ip6addr2) (1)
+#define IPV6_CUSTOM_SCOPES 0
+#define ip6_addr_has_scope(ip6addr, type) (0)
+#define ip6_addr_assign_zone(ip6addr, type, netif)
+#define ip6_addr_test_zone(ip6addr, netif) (1)
+#define ip6_addr_lacks_zone(ip6addr, type) (0)
+#define ip6_addr_select_zone(ip6addr, src)
+
+#endif /* LWIP_IPV6_SCOPES */
+
+#if LWIP_IPV6_SCOPES && LWIP_IPV6_SCOPES_DEBUG
+
+/** Verify that the given IPv6 address is properly zoned. */
+#define IP6_ADDR_ZONECHECK(ip6addr) LWIP_ASSERT("IPv6 zone check failed", \
+    ip6_addr_has_scope(ip6addr, IP6_UNKNOWN) == ip6_addr_has_zone(ip6addr))
+
+/** Verify that the given IPv6 address is properly zoned for the given netif. */
+#define IP6_ADDR_ZONECHECK_NETIF(ip6addr, netif) LWIP_ASSERT("IPv6 netif zone check failed", \
+    ip6_addr_has_scope(ip6addr, IP6_UNKNOWN) ? \
+    (ip6_addr_has_zone(ip6addr) && \
+     (((netif) == NULL) || ip6_addr_test_zone((ip6addr), (netif)))) : \
+    !ip6_addr_has_zone(ip6addr))
+
+#else /* LWIP_IPV6_SCOPES && LWIP_IPV6_SCOPES_DEBUG */
+
+#define IP6_ADDR_ZONECHECK(ip6addr)
+#define IP6_ADDR_ZONECHECK_NETIF(ip6addr, netif)
+
+#endif /* LWIP_IPV6_SCOPES && LWIP_IPV6_SCOPES_DEBUG */
+
+#endif /* LWIP_IPV6 */
+
+#endif /* LWIP_HDR_IP6_ADDR_H */
diff --git a/src/include/lwip/ip_addr.h b/src/include/lwip/ip_addr.h
index def2d08..844a06f 100644
--- a/src/include/lwip/ip_addr.h
+++ b/src/include/lwip/ip_addr.h
@@ -78,18 +78,19 @@ typedef struct ip_addr {
 extern const ip_addr_t ip_addr_any_type;
 
 /** @ingroup ip4addr */
-#define IPADDR4_INIT(u32val)          { { { { u32val, 0ul, 0ul, 0ul } } }, IPADDR_TYPE_V4 }
+#define IPADDR4_INIT(u32val)          { { { { u32val, 0ul, 0ul, 0ul } IPADDR6_ZONE_INIT } }, IPADDR_TYPE_V4 }
 /** @ingroup ip4addr */
 #define IPADDR4_INIT_BYTES(a,b,c,d)   IPADDR4_INIT(PP_HTONL(LWIP_MAKEU32(a,b,c,d)))
+
 /** @ingroup ip6addr */
-#define IPADDR6_INIT(a, b, c, d)      { { { { a, b, c, d } } }, IPADDR_TYPE_V6 }
+#define IPADDR6_INIT(a, b, c, d)      { { { { a, b, c, d } IPADDR6_ZONE_INIT } }, IPADDR_TYPE_V6 }
 /** @ingroup ip6addr */
-#define IPADDR6_INIT_HOST(a, b, c, d) { { { { PP_HTONL(a), PP_HTONL(b), PP_HTONL(c), PP_HTONL(d) } } }, IPADDR_TYPE_V6 }
+#define IPADDR6_INIT_HOST(a, b, c, d) { { { { PP_HTONL(a), PP_HTONL(b), PP_HTONL(c), PP_HTONL(d) } IPADDR6_ZONE_INIT } }, IPADDR_TYPE_V6 }
 
 /** @ingroup ipaddr */
 #define IP_IS_ANY_TYPE_VAL(ipaddr)    (IP_GET_TYPE(&ipaddr) == IPADDR_TYPE_ANY)
 /** @ingroup ipaddr */
-#define IPADDR_ANY_TYPE_INIT          { { { { 0ul, 0ul, 0ul, 0ul } } }, IPADDR_TYPE_ANY }
+#define IPADDR_ANY_TYPE_INIT          { { { { 0ul, 0ul, 0ul, 0ul } IPADDR6_ZONE_INIT } }, IPADDR_TYPE_ANY }
 
 /** @ingroup ip4addr */
 #define IP_IS_V4_VAL(ipaddr)          (IP_GET_TYPE(&ipaddr) == IPADDR_TYPE_V4)
@@ -132,6 +133,9 @@ extern const ip_addr_t ip_addr_any_type;
 /** @ingroup ip6addr */
 #define ip_addr_copy_from_ip6(dest, src)      do{ \
   ip6_addr_copy(*ip_2_ip6(&(dest)), src); IP_SET_TYPE_VAL(dest, IPADDR_TYPE_V6); }while(0)
+/** @ingroup ip6addr */
+#define ip_addr_copy_from_ip6_packed(dest, src)      do{ \
+  ip6_addr_copy_from_packed(*ip_2_ip6(&(dest)), src); IP_SET_TYPE_VAL(dest, IPADDR_TYPE_V6); }while(0)
 /** @ingroup ip4addr */
 #define ip_addr_copy_from_ip4(dest, src)      do{ \
   ip4_addr_copy(*ip_2_ip4(&(dest)), src); IP_SET_TYPE_VAL(dest, IPADDR_TYPE_V4); }while(0)
@@ -226,7 +230,8 @@ int ipaddr_aton(const char *cp, ip_addr_t *addr);
   (ip6addr)->addr[3] = (ip4addr)->addr; \
   (ip6addr)->addr[2] = PP_HTONL(0x0000FFFFUL); \
   (ip6addr)->addr[1] = 0; \
-  (ip6addr)->addr[0] = 0; } while(0);
+  (ip6addr)->addr[0] = 0; \
+  ip6_addr_clear_zone(ip6addr); } while(0);
 
 /** @ingroup ipaddr */
 #define unmap_ipv6_mapped_ipv4(ip4addr, ip6addr) \
@@ -288,8 +293,8 @@ typedef ip4_addr_t ip_addr_t;
 #else /* LWIP_IPV4 */
 
 typedef ip6_addr_t ip_addr_t;
-#define IPADDR6_INIT(a, b, c, d)                { { a, b, c, d } }
-#define IPADDR6_INIT_HOST(a, b, c, d)           { { PP_HTONL(a), PP_HTONL(b), PP_HTONL(c), PP_HTONL(d) } }
+#define IPADDR6_INIT(a, b, c, d)                { { a, b, c, d } IPADDR6_ZONE_INIT }
+#define IPADDR6_INIT_HOST(a, b, c, d)           { { PP_HTONL(a), PP_HTONL(b), PP_HTONL(c), PP_HTONL(d) } IP6ADDR_ZONE_INIT }
 #define IP_IS_V4_VAL(ipaddr)                    0
 #define IP_IS_V6_VAL(ipaddr)                    1
 #define IP_IS_V4(ipaddr)                        0
@@ -304,6 +309,7 @@ typedef ip6_addr_t ip_addr_t;
 
 #define ip_addr_copy(dest, src)                 ip6_addr_copy(dest, src)
 #define ip_addr_copy_from_ip6(dest, src)        ip6_addr_copy(dest, src)
+#define ip_addr_copy_from_ip6_packed(dest, src) ip6_addr_copy_from_packed(dest, src)
 #define ip_addr_set(dest, src)                  ip6_addr_set(dest, src)
 #define ip_addr_set_ipaddr(dest, src)           ip6_addr_set(dest, src)
 #define ip_addr_set_zero(ipaddr)                ip6_addr_set_zero(ipaddr)
diff --git a/src/include/lwip/opt.h b/src/include/lwip/opt.h
index e9a8182..dd91558 100644
--- a/src/include/lwip/opt.h
+++ b/src/include/lwip/opt.h
@@ -2165,6 +2165,25 @@
 #endif
 
 /**
+ * LWIP_IPV6_SCOPES==1: Enable support for IPv6 address scopes, ensuring that
+ * e.g. link-local addresses are really treated as link-local. Disable this
+ * setting only for single-interface configurations.
+ */
+#if !defined LWIP_IPV6_SCOPES || defined __DOXYGEN__
+#define LWIP_IPV6_SCOPES                (LWIP_IPV6)
+#endif
+
+/**
+ * LWIP_IPV6_SCOPES_DEBUG==1: Perform run-time checks to verify that addresses
+ * are properly zoned (see ip6_zone.h on what that means) where it matters.
+ * Enabling this setting is highly recommended when upgrading from an existing
+ * installation that is not yet scope-aware; otherwise it may be too expensive.
+ */
+#if !defined LWIP_IPV6_SCOPES_DEBUG || defined __DOXYGEN__
+#define LWIP_IPV6_SCOPES_DEBUG          0
+#endif
+
+/**
  * LWIP_IPV6_NUM_ADDRESSES: Number of IPv6 addresses per netif.
  */
 #if !defined LWIP_IPV6_NUM_ADDRESSES || defined __DOXYGEN__
diff --git a/src/netif/lowpan6.c b/src/netif/lowpan6.c
index 9a84cbc..deb09ff 100644
--- a/src/netif/lowpan6.c
+++ b/src/netif/lowpan6.c
@@ -262,8 +262,10 @@ lowpan6_frag(struct netif *netif, struct pbuf *p, const struct ieee_802154_addr
 
     /* Point to ip6 header and align copies of src/dest addresses. */
     ip6hdr = (struct ip6_hdr *)p->payload;
-    ip_addr_copy_from_ip6(ip_data.current_iphdr_dest, ip6hdr->dest);
-    ip_addr_copy_from_ip6(ip_data.current_iphdr_src, ip6hdr->src);
+    ip_addr_copy_from_ip6_packed(ip_data.current_iphdr_dest, ip6hdr->dest);
+    ip6_addr_assign_zone(ip_2_ip6(&ip_data.current_iphdr_dest), IP6_UNKNOWN, netif);
+    ip_addr_copy_from_ip6_packed(ip_data.current_iphdr_src, ip6hdr->src);
+    ip6_addr_assign_zone(ip_2_ip6(&ip_data.current_iphdr_src), IP6_UNKNOWN, netif);
 
     /* Basic length of 6LowPAN header, set dispatch and clear fields. */
     lowpan6_header_len = 2;
@@ -574,6 +576,8 @@ lowpan6_set_context(u8_t idx, const ip6_addr_t * context)
     return ERR_ARG;
   }
 
+  IP6_ADDR_ZONECHECK(context);
+
   ip6_addr_set(&lowpan6_context[idx], context);
 
   return ERR_OK;
@@ -627,7 +631,8 @@ lowpan6_output(struct netif *netif, struct pbuf *q, const ip6_addr_t *ip6addr)
 #if LWIP_6LOWPAN_INFER_SHORT_ADDRESS
   /* Check if we can compress source address (use aligned copy) */
   ip6_hdr = (struct ip6_hdr *)q->payload;
-  ip6_addr_set(&ip6_src, &ip6_hdr->src);
+  ip6_addr_copy_from_packed(ip6_src, ip6_hdr->src);
+  ip6_addr_assign_zone(&ip6_src, IP6_UNICAST, netif);
   if (lowpan6_get_address_mode(&ip6_src, &short_mac_addr) == 3) {
     src.addr_len = 2;
     src.addr[0] = short_mac_addr.addr[0];
@@ -656,7 +661,7 @@ lowpan6_output(struct netif *netif, struct pbuf *q, const ip6_addr_t *ip6addr)
     dest.addr_len = 2;
     dest.addr[0] = ((u8_t *)q->payload)[38];
     dest.addr[1] = ((u8_t *)q->payload)[39];
-    if ((src.addr_len == 2) && (ip6_addr_netcmp(&ip6_hdr->src, &ip6_hdr->dest)) &&
+    if ((src.addr_len == 2) && (ip6_addr_netcmp_zoneless(&ip6_hdr->src, &ip6_hdr->dest)) &&
         (lowpan6_get_address_mode(ip6addr, &dest) == 3)) {
       MIB2_STATS_NETIF_INC(netif, ifoutucastpkts);
       return lowpan6_frag(netif, q, &src, &dest);
-- 
2.5.2

