diff --git a/src/netaddress.h b/src/netaddress.h
--- a/src/netaddress.h
+++ b/src/netaddress.h
@@ -16,19 +16,49 @@
 #include <string>
 #include <vector>
 
+/**
+ * A network type.
+ * @note An address may belong to more than one network, for example `10.0.0.1`
+ * belongs to both `NET_UNROUTABLE` and `NET_IPV4`.
+ * Keep these sequential starting from 0 and `NET_MAX` as the last entry.
+ * We have loops like `for (int i = 0; i < NET_MAX; i++)` that expect to iterate
+ * over all enum values and also `GetExtNetwork()` "extends" this enum by
+ * introducing standalone constants starting from `NET_MAX`.
+ */
 enum Network {
+    /// Addresses from these networks are not publicly routable on the global
+    /// Internet.
     NET_UNROUTABLE = 0,
+
+    /// IPv4
     NET_IPV4,
+
+    /// IPv6
     NET_IPV6,
+
+    /// TORv2
     NET_ONION,
+
+    /// A set of dummy addresses that map a name to an IPv6 address. These
+    /// addresses belong to RFC4193's fc00::/7 subnet (unique-local addresses).
+    /// We use them to map a string or FQDN to an IPv6 address in CAddrMan to
+    /// keep track of which DNS seeds were used.
     NET_INTERNAL,
 
+    /// Dummy value to indicate the number of NET_* constants.
     NET_MAX,
 };
 
-/** IP address (IPv6, or IPv4 using mapped IPv6 range (::FFFF:0:0/96)) */
+/**
+ * Network address.
+ */
 class CNetAddr {
 protected:
+    /**
+     * Network to which this address belongs.
+     */
+    Network m_net{NET_IPV6};
+
     // in network byte order
     uint8_t ip[16];
     // for scoped/link-local ipv6 addresses
@@ -39,6 +69,14 @@
     explicit CNetAddr(const struct in_addr &ipv4Addr);
     void SetIP(const CNetAddr &ip);
 
+    /**
+     * Set from a legacy IPv6 address.
+     * Legacy IPv6 address may be a normal IPv6 address, or another address
+     * (e.g. IPv4) disguised as IPv6. This encoding is used in the legacy
+     * `addr` encoding.
+     */
+    void SetLegacyIPv6(const uint8_t ipv6[16]);
+
     /**
      * Set raw IPv4 or IPv6 address (in network byte order)
      * @note Only NET_IPV4 and NET_IPV6 are allowed for network.
@@ -127,7 +165,21 @@
     }
     friend bool operator<(const CNetAddr &a, const CNetAddr &b);
 
-    SERIALIZE_METHODS(CNetAddr, obj) { READWRITE(obj.ip); }
+    /**
+     * Serialize to a stream.
+     */
+    template <typename Stream> void Serialize(Stream &s) const { s << ip; }
+
+    /**
+     * Unserialize from a stream.
+     */
+    template <typename Stream> void Unserialize(Stream &s) {
+        uint8_t ip_temp[sizeof(ip)];
+        s >> ip_temp;
+        // Use SetLegacyIPv6() so that m_net is set correctly. For example
+        // ::FFFF:0102:0304 should be set as m_net=NET_IPV4 (1.2.3.4).
+        SetLegacyIPv6(ip_temp);
+    }
 
     friend class CSubNet;
 };
diff --git a/src/netaddress.cpp b/src/netaddress.cpp
--- a/src/netaddress.cpp
+++ b/src/netaddress.cpp
@@ -26,17 +26,33 @@
 }
 
 void CNetAddr::SetIP(const CNetAddr &ipIn) {
+    m_net = ipIn.m_net;
     memcpy(ip, ipIn.ip, sizeof(ip));
 }
 
+void CNetAddr::SetLegacyIPv6(const uint8_t ipv6[16]) {
+    if (memcmp(ipv6, pchIPv4, sizeof(pchIPv4)) == 0) {
+        m_net = NET_IPV4;
+    } else if (memcmp(ipv6, pchOnionCat, sizeof(pchOnionCat)) == 0) {
+        m_net = NET_ONION;
+    } else if (memcmp(ipv6, g_internal_prefix, sizeof(g_internal_prefix)) ==
+               0) {
+        m_net = NET_INTERNAL;
+    } else {
+        m_net = NET_IPV6;
+    }
+    memcpy(ip, ipv6, 16);
+}
+
 void CNetAddr::SetRaw(Network network, const uint8_t *ip_in) {
     switch (network) {
         case NET_IPV4:
+            m_net = NET_IPV4;
             memcpy(ip, pchIPv4, 12);
             memcpy(ip + 12, ip_in, 4);
             break;
         case NET_IPV6:
-            memcpy(ip, ip_in, 16);
+            SetLegacyIPv6(ip_in);
             break;
         default:
             assert(!"invalid network");
@@ -61,6 +77,7 @@
     if (name.empty()) {
         return false;
     }
+    m_net = NET_INTERNAL;
     uint8_t hash[32] = {};
     CSHA256().Write((const uint8_t *)name.data(), name.size()).Finalize(hash);
     memcpy(ip, g_internal_prefix, sizeof(g_internal_prefix));
@@ -87,6 +104,7 @@
         if (vchAddr.size() != 16 - sizeof(pchOnionCat)) {
             return false;
         }
+        m_net = NET_ONION;
         memcpy(ip, pchOnionCat, sizeof(pchOnionCat));
         for (unsigned int i = 0; i < 16 - sizeof(pchOnionCat); i++) {
             ip[i + sizeof(pchOnionCat)] = vchAddr[i];
@@ -121,11 +139,11 @@
 }
 
 bool CNetAddr::IsIPv4() const {
-    return (memcmp(ip, pchIPv4, sizeof(pchIPv4)) == 0);
+    return m_net == NET_IPV4;
 }
 
 bool CNetAddr::IsIPv6() const {
-    return !IsIPv4() && !IsTor() && !IsInternal();
+    return m_net == NET_IPV6;
 }
 
 bool CNetAddr::IsRFC1918() const {
@@ -156,48 +174,48 @@
 }
 
 bool CNetAddr::IsRFC3849() const {
-    return GetByte(15) == 0x20 && GetByte(14) == 0x01 && GetByte(13) == 0x0D &&
-           GetByte(12) == 0xB8;
+    return IsIPv6() && GetByte(15) == 0x20 && GetByte(14) == 0x01 &&
+           GetByte(13) == 0x0D && GetByte(12) == 0xB8;
 }
 
 bool CNetAddr::IsRFC3964() const {
-    return (GetByte(15) == 0x20 && GetByte(14) == 0x02);
+    return IsIPv6() && GetByte(15) == 0x20 && GetByte(14) == 0x02;
 }
 
 bool CNetAddr::IsRFC6052() const {
     static const uint8_t pchRFC6052[] = {0, 0x64, 0xFF, 0x9B, 0, 0,
                                          0, 0,    0,    0,    0, 0};
-    return (memcmp(ip, pchRFC6052, sizeof(pchRFC6052)) == 0);
+    return IsIPv6() && memcmp(ip, pchRFC6052, sizeof(pchRFC6052)) == 0;
 }
 
 bool CNetAddr::IsRFC4380() const {
-    return (GetByte(15) == 0x20 && GetByte(14) == 0x01 && GetByte(13) == 0 &&
-            GetByte(12) == 0);
+    return IsIPv6() && GetByte(15) == 0x20 && GetByte(14) == 0x01 &&
+           GetByte(13) == 0 && GetByte(12) == 0;
 }
 
 bool CNetAddr::IsRFC4862() const {
     static const uint8_t pchRFC4862[] = {0xFE, 0x80, 0, 0, 0, 0, 0, 0};
-    return (memcmp(ip, pchRFC4862, sizeof(pchRFC4862)) == 0);
+    return IsIPv6() && memcmp(ip, pchRFC4862, sizeof(pchRFC4862)) == 0;
 }
 
 bool CNetAddr::IsRFC4193() const {
-    return ((GetByte(15) & 0xFE) == 0xFC);
+    return IsIPv6() && (GetByte(15) & 0xFE) == 0xFC;
 }
 
 bool CNetAddr::IsRFC6145() const {
     static const uint8_t pchRFC6145[] = {0, 0, 0,    0,    0, 0,
                                          0, 0, 0xFF, 0xFF, 0, 0};
-    return (memcmp(ip, pchRFC6145, sizeof(pchRFC6145)) == 0);
+    return IsIPv6() && memcmp(ip, pchRFC6145, sizeof(pchRFC6145)) == 0;
 }
 
 bool CNetAddr::IsRFC4843() const {
-    return (GetByte(15) == 0x20 && GetByte(14) == 0x01 && GetByte(13) == 0x00 &&
-            (GetByte(12) & 0xF0) == 0x10);
+    return IsIPv6() && GetByte(15) == 0x20 && GetByte(14) == 0x01 &&
+           GetByte(13) == 0x00 && (GetByte(12) & 0xF0) == 0x10;
 }
 
 bool CNetAddr::IsRFC7343() const {
-    return (GetByte(15) == 0x20 && GetByte(14) == 0x01 && GetByte(13) == 0x00 &&
-            (GetByte(12) & 0xF0) == 0x20);
+    return IsIPv6() && GetByte(15) == 0x20 && GetByte(14) == 0x01 &&
+           GetByte(13) == 0x00 && (GetByte(12) & 0xF0) == 0x20;
 }
 
 bool CNetAddr::IsHeNet() const {
@@ -212,7 +230,7 @@
  * @see CNetAddr::SetSpecial(const std::string &)
  */
 bool CNetAddr::IsTor() const {
-    return (memcmp(ip, pchOnionCat, sizeof(pchOnionCat)) == 0);
+    return m_net == NET_ONION;
 }
 
 bool CNetAddr::IsLocal() const {
@@ -224,7 +242,7 @@
     // IPv6 loopback (::1/128)
     static const uint8_t pchLocal[16] = {0, 0, 0, 0, 0, 0, 0, 0,
                                          0, 0, 0, 0, 0, 0, 0, 1};
-    if (memcmp(ip, pchLocal, 16) == 0) {
+    if (IsIPv6() && memcmp(ip, pchLocal, 16) == 0) {
         return true;
     }
 
@@ -248,13 +266,13 @@
     // header20 vectorlen3 addr26 addr26 addr26 header20 vectorlen3 addr26
     // addr26 addr26... so if the first length field is garbled, it reads the
     // second batch of addr misaligned by 3 bytes.
-    if (memcmp(ip, pchIPv4 + 3, sizeof(pchIPv4) - 3) == 0) {
+    if (IsIPv6() && memcmp(ip, pchIPv4 + 3, sizeof(pchIPv4) - 3) == 0) {
         return false;
     }
 
     // unspecified IPv6 address (::/128)
     uint8_t ipNone6[16] = {};
-    if (memcmp(ip, ipNone6, 16) == 0) {
+    if (IsIPv6() && memcmp(ip, ipNone6, 16) == 0) {
         return false;
     }
 
@@ -306,7 +324,7 @@
  * @see CNetAddr::SetInternal(const std::string &)
  */
 bool CNetAddr::IsInternal() const {
-    return memcmp(ip, g_internal_prefix, sizeof(g_internal_prefix)) == 0;
+    return m_net == NET_INTERNAL;
 }
 
 enum Network CNetAddr::GetNetwork() const {
@@ -318,15 +336,7 @@
         return NET_UNROUTABLE;
     }
 
-    if (IsIPv4()) {
-        return NET_IPV4;
-    }
-
-    if (IsTor()) {
-        return NET_ONION;
-    }
-
-    return NET_IPV6;
+    return m_net;
 }
 
 std::string CNetAddr::ToStringIP() const {
@@ -366,11 +376,12 @@
 }
 
 bool operator==(const CNetAddr &a, const CNetAddr &b) {
-    return (memcmp(a.ip, b.ip, 16) == 0);
+    return a.m_net == b.m_net && memcmp(a.ip, b.ip, 16) == 0;
 }
 
 bool operator<(const CNetAddr &a, const CNetAddr &b) {
-    return (memcmp(a.ip, b.ip, 16) < 0);
+    return a.m_net < b.m_net ||
+           (a.m_net == b.m_net && memcmp(a.ip, b.ip, 16) < 0);
 }
 
 /**
@@ -841,7 +852,7 @@
  *          the specified address belongs in this subnet.
  */
 bool CSubNet::Match(const CNetAddr &addr) const {
-    if (!valid || !addr.IsValid()) {
+    if (!valid || !addr.IsValid() || network.m_net != addr.m_net) {
         return false;
     }
     for (int x = 0; x < 16; ++x) {
diff --git a/src/test/netbase_tests.cpp b/src/test/netbase_tests.cpp
--- a/src/test/netbase_tests.cpp
+++ b/src/test/netbase_tests.cpp
@@ -132,6 +132,13 @@
     BOOST_CHECK(addr1.IsRoutable());
 }
 
+BOOST_AUTO_TEST_CASE(embedded_test) {
+    CNetAddr addr1(ResolveIP("1.2.3.4"));
+    CNetAddr addr2(ResolveIP("::FFFF:0102:0304"));
+    BOOST_CHECK(addr2.IsIPv4());
+    BOOST_CHECK_EQUAL(addr1.ToString(), addr2.ToString());
+}
+
 BOOST_AUTO_TEST_CASE(subnet_test) {
     BOOST_CHECK(ResolveSubNet("1.2.3.0/24") ==
                 ResolveSubNet("1.2.3.0/255.255.255.0"));
@@ -158,12 +165,14 @@
     BOOST_CHECK(ResolveSubNet("1.2.2.1/24").Match(ResolveIP("1.2.2.4")));
     BOOST_CHECK(ResolveSubNet("1.2.2.110/31").Match(ResolveIP("1.2.2.111")));
     BOOST_CHECK(ResolveSubNet("1.2.2.20/26").Match(ResolveIP("1.2.2.63")));
-    // All-Matching IPv6 Matches arbitrary IPv4 and IPv6
+    // All-Matching IPv6 Matches arbitrary IPv6
     BOOST_CHECK(ResolveSubNet("::/0").Match(ResolveIP("1:2:3:4:5:6:7:1234")));
     // But not `::` or `0.0.0.0` because they are considered invalid addresses
     BOOST_CHECK(!ResolveSubNet("::/0").Match(ResolveIP("::")));
     BOOST_CHECK(!ResolveSubNet("::/0").Match(ResolveIP("0.0.0.0")));
-    BOOST_CHECK(ResolveSubNet("::/0").Match(ResolveIP("1.2.3.4")));
+    // Addresses from one network (IPv4) don't belong to subnets of another
+    // network (IPv6)
+    BOOST_CHECK(!ResolveSubNet("::/0").Match(ResolveIP("1.2.3.4")));
     // All-Matching IPv4 does not Match IPv6
     BOOST_CHECK(
         !ResolveSubNet("0.0.0.0/0").Match(ResolveIP("1:2:3:4:5:6:7:1234")));