blob: 39765c70525dca88ee31451b9fed131500ca8d9e [file] [log] [blame]
.. SPDX-License-Identifier: Apache-2.0
.. include:: ../common-defs.rst
.. default-domain:: cpp
.. highlight:: cpp
*************
IP Networking
*************
Synopsis
********
:code:`#include "swoc/swoc_ip.h"`
Usage
*****
This library is for storing and manipulating IP addresses as data. It has no support for actual
network operations.
IPEndpoint
==========
:libswoc:`swoc::IPEndpoint` is a wrapper around :code:`sockaddr` and provides a number of utilities.
It enables constructing an instance from the string representation of an address, supporting IPv4
and IPv6. It will also parse and store the port if that is part of the string. Some of the internal
logic is exposed via :libswoc:`swoc::IPEndpoint::tokenize` which finds and returns the elements of
an address string, the host (address), port, and any trailing remnants. This is useful for doing
syntax checks or more specialized processing of the address string.
IPAddr
======
The classes :libswoc:`swoc::IPAddr`, :libswoc:`swoc::IP4Addr`, and :libswoc:`swoc::IP6Addr` are used
to hold IP addresses. :code:`IP4Addr` and :code:`IP6Addr` are family specific and hold
(respectively) IPv4 and IPv6 addresses. :code:`IPAddr` acts as a union of these two types along with
an IP family specifier that indicates the type of address contained. The type specific classes
provide performance and storage benefits when the type of the address is known or forced, while
:code:`IPAddr` provides a generic type useful for interfaces.
These classes provide support for parsing and formatting IP addresses. The constructor can take
a string and, if a valid address, will initialize the instance to that address. The downside is
there is no indication of failure other than the instance initializing to the zero or "any"
address. This can be reasonable in situations where those addresses are not valid either. However
in general the :libswoc:`swoc::IPAddr::load` method should be used, which both initializes the
instance and provides an indication of whether the input was valid.
Conversions to and from :code:`sockaddr` are provided. This is handier with :code:`IPAddr` as it
will conform to the family of the address in the :code:`sockaddr`.
IPSrv
=====
A container for an address and a port. There is no really good name for this therefore I used the
DNS term for such an object. This consists of the usual triplet of classes, :swoc:`IP4Srv`, :swoc:`IP6Srv`,
and :swoc:`IPSrv`. The first two are protocol family specific and the third holds an instance of
either an :code:`IP4Srv` or an `IP6Srv`. The address and port can be manipulated separately.
IPRange
=======
The classes :libswoc:`swoc::IPRange`, :libswoc:`swoc::IP4Range`, and :libswoc:`swoc::IP6Range` are
used to hold ranges of IP addresses. :code:`IP4Range` and :code:`IP6Range` are family specific and
hold (respectively) IPv4 and IPv6 addresses. :code:`IPAddr` acts as a union of these two types along
with an IP family specifier that indicates the type of address contained. The type specific classes
provide performance and storage benefits when the type of the address is known or forced, while
:code:`IPRange` provides a generic type useful for interfaces. Note that an :code:`IPRange` holds a
range of addresses of a single family, it can never hold a range that is of mixed families.
These classes provide support for parsing and formatting ranges of IP adddresses. The parsing logic
accepts three forms of a range. In all cases the lower and upper limits of the range must be the
same IP address family.
Range
Two addresses, separated by a dash ("-") character. E.g.
172.26.13.4-172.27.12.9
Network
An address and a CIDR based mask, separated by a slash ("/") character. E.g.
1337:0:0:ded:BEEF::/48
Singleton
A single IP address, which is interpreted as a range of size 1.
Such a string can be passed to the constructor, which will initialize to the corresponding range if
properly formatted, otherwise the range will be default constructed to an invalid range. There is
also the :libswoc:`swoc::IPRange::load` method which returns a :code:`bool` to indicate if the
parsing was successful.
This class has formatting support in "bwf_ip.h". In addition to all of the formatting supported for
:code:`sockaddr`, the additional extension code 'c' can be used to indicate compact range formatting.
Compact means a singleton range will be written as just the single address, and if the range is
also a network it will be printed in CIDR format.
======================= =======================
Range Compact
======================= =======================
10.1.0.0-10.1.0.127 10.1.0.0/25
10.2.0.1-10.2.0.127 10.2.0.1-10.2.0.127
10.3.0.0-10.3.0.126 10.3.0.0-10.3.0.126
10.4.1.1-10.4.1.1 10.4.1.1
======================= =======================
Conversions
===========
Most conversions between types should be straight forward but in some cases there is some indirection
in order to avoid bad conversions due to differences in address families.
Conversion from :libswoc:`swoc::IPEndpoint` to :libswoc:`swoc::IPAddr` is direct as the latter can
be explicitly constructed from the former. For :libswoc:`swoc::IP6Addr` and :libswoc:`swoc::IP4Addr`
the family must be checked first. The expected way to do this is ::
if ( auto sa = ep.ip4() ; sa ) {
IP4Addr addr(sa);
// ....
}
This is intended to be similar to how dynamic casts are handled when it is not guaranteed the generic
type contains an instance of the more specific type.
Conversion from address types to socket addresses can be done by constructing an :code:`IPEndpoint` or,
if the socket address structure already exists, using the :code:`copy_to` methods on the address types,
such as :libswoc:`swoc::IP4Addr::copy_to`. Note converting an address types sets only the family and
address. Converting a service type also sets the port.
.. _ip-space:
IPSpace
=======
The :libswoc:`swoc::IPSpace` class is designed as a container for ranges of IP addresses. Lookup is
done by single addresses. Conceptually, for each possible IP address there is a *payload*. Populating
the container is done by applying a specific payload to a range of addresses. After populating an
IP address can be looked up to find the corresponding payload.
The payload is a template argument to the class, as with standard containers. There is no template
argument for the key, as that is always an IP address.
Applying payloads to the space is analogized to painting, each distinct payload considered a different
"color" for an address. There are several methods used to paint the space, depending on the desired
effect on existing payloads.
mark
:libswoc:`swoc::IPSpace::mark` applies the :arg:`payload` to the range, replacing any existing
payload present. This is modeled on the "painter's algorithm" where the most recent coloring
replaces any prior colors in the region.
fill
:libswoc:`swoc::IPSpace::fill` applies the :arg:`payload` to the range but only where there is not
already a payload. This is modeled on "backfilling" a background. This is useful for "first match"
logic, as which ever payload is first put in to the container will remain.
blend
:libswoc:`swoc::IPSpace::blend` applies the :arg:`payload` to the range by combining ("blending")
the payloads. The result of blending can be "uncolored" which results in those addresses being
removed from the space. This is useful for applying different properties in sequence, where the
result is a combination of the properties.
Blend
+++++
Blending is different than marking or filling, as the latter two apply the payload passed to the
method. That is, if an address is marked by either method, it is marked with precisely the payload
passed to the method. :code:`blend` is different because it can cause an address to be marked by
a payload that was not explicitly passed in to any coloring method. Instead of replacing an
existing payload, it enables computing the resulting payload from the existing payload and a value
passed to the method.
The :libswoc:`swoc::IPSpace::blend` method requires a range and a "blender", which is a functor
that blends a :arg:`color` into a :code:`PAYLOAD` instances. The signature is ::
bool blender(PAYLOAD & payload, U const& color)
The type :code:`U` is that same as the template argument :code:`U` to the :code:`blend` method,
which must be compatible with the second argument to the :code:`blend` method. The argument passed
to :code:`blender` is the second argument to :code:`blend`.
The method is modeled on C++ `compound assignment operators
<https://en.cppreference.com/w/cpp/language/operator_assignment#Builtin_compound_assignment>`__. If
the blend operation is thought of as the "@" operator, then the blend functor performs
:code:`lhs @=rhs`. That is, :arg:`lhs` is modified to be the combination of :arg:`lhs` and :arg`rhs`.
:arg:`lhs` is always the previous payload already in the space, and :arg:`rhs` is the :arg:`color`
argument to the :code:`blend` method. The internal logic handles copying the payload instances as
needed.
The return value of the blender indicates whether the combined result in :arg:`lhs` is a valid
payload or not. If it is the method should return :code:`true`. In general most implementations will
:code:`return true;` in all cases. If the method returns :code:`false` then the address(es) for the
combined payload are removed from the container. This allows payloads to be "unblended", for one
payload to cancel out another, or to do selective erasing of ranges.
As an example, consider the case where the payload is a bitmask. It might be reasonable to keep
empty bitmasks in the container, but it would also be reasonable to decide the empty bitmask and any
address mapped to it should removed entirely from the container. In such a case, a blender that
clears bits in the payloads should return :code:`false` when the result is the empty bitmask.
Similarly, if the goal is to remove ranges that have a specific payload, then a blender that returns
:code:`false` if :arg:`lhs` matches that specific payload and :code:`true` if not, should be used.
There is a small implementation wrinkle, however, in dealing with unmapped addresses. The
:arg:`color` is not necessarily a :code:`PAYLOAD` and therefore must be converted in to one. This
is done by default constructing a :code:`PAYLOAD` instance and then calling :code:`blend` on that
and the :arg:`color`. If this returns :code:`false` then unmapped addresses will remain unmapped.
Examples
********
Blending Bitsets
================
.. sidebar:: Code
Some details are omitted for brevity and because they aren't directly relevant. The full
implementation, which is run as a unit test to verify its correctness,
`is available here <https://github.com/SolidWallOfCode/libswoc/blob/1.4.12/unit_tests/ex_ipspace_properties.cc>`__.
You can compile and step through the code to see how it works in more detail, or experiment
with changing some of the example data.
As an example of blending, consider a mapping of IP addresses to a bit set, each bit representing
some independent property of the address (e.g., production, externally accessible, secure, etc.). It
might be the case that each of these was in a separate data source. In that case one approach would
be to blend each data source into the IPSpace, combining the bits in the blending functor. If
`std::bitset <http://www.cplusplus.com/reference/bitset/bitset/>`__ is used to hold the bits, the
declarations could be done as
.. literalinclude:: ../../unit_tests/ex_ipspace_properties.cc
:start-after: "IPSpace bitset blending"
:lines: 1-4
:emphasize-lines: 2,4
To do the blending, a blending functor is needed.
.. literalinclude:: ../../unit_tests/ex_ipspace_properties.cc
:start-after: // Bitset blend functor
:lines: 1-4
This always returns :code:`true` because blending any bits never yields a zero result. A lambda is
provided to do the marking of the example data for convience. This takes a list of example data
items defined as a range and a list of bits to set.
.. literalinclude:: ../../unit_tests/ex_ipspace_properties.cc
:start-after: // Example data type.
:lines: 1
The marking logic is
.. literalinclude:: ../../unit_tests/ex_ipspace_properties.cc
:start-after: // Example marking functor.
:lines: 1-7
Let's try it out. For the first pass this data will be used.
.. literalinclude:: ../../unit_tests/ex_ipspace_properties.cc
:start-after: // test ranges 1
:lines: 2-8
After using this, the space contents are ::
7 ranges
100.0.0.0-100.0.0.255 : 10000000000000000000000000000000
100.0.1.0-100.0.1.255 : 01000000000000000000000000000000
100.0.2.0-100.0.2.255 : 00100000000000000000000000000000
100.0.3.0-100.0.3.255 : 00010000000000000000000000000000
100.0.4.0-100.0.4.255 : 00001000000000000000000000000000
100.0.5.0-100.0.5.255 : 00000100000000000000000000000000
100.0.6.0-100.0.6.255 : 00000010000000000000000000000000
Those are non-overlapping intervals and therefore are not really blended. Suppose the following
ranges are also blended - note these overlap the first two ranges from the previous ranges.
.. literalinclude:: ../../unit_tests/ex_ipspace_properties.cc
:start-after: // test ranges 2
:lines: 2-4
This yields ::
9 ranges
100.0.0.0-100.0.0.255 : 10000000000000000000000000000001
100.0.1.0-100.0.1.255 : 01000000000000000000000000000010
100.0.2.0-100.0.2.127 : 00100000000000000000000000000000
100.0.2.128-100.0.2.255 : 00100000000000000000000000000100
100.0.3.0-100.0.3.127 : 00010000000000000000000000000100
100.0.3.128-100.0.3.255 : 00010000000000000000000000000000
100.0.4.0-100.0.4.255 : 00001000000000000000000000000000
100.0.5.0-100.0.5.255 : 00000100000000000000000000000000
100.0.6.0-100.0.6.255 : 00000010000000000000000000000000
The additional bits are now present on the other side of the bit set. Note there are now more ranges
because the last range overlapped two of the previously existing ranges. Those are split because of
the now differing payloads for the new ranges.
What happens if this range and data is blended into the space?
.. literalinclude:: ../../unit_tests/ex_ipspace_properties.cc
:start-after: // test ranges 3
:lines: 2
The result is ::
6 ranges
100.0.0.0-100.0.0.255 : 10000000000000000000000000000001
100.0.1.0-100.0.1.255 : 01000000000000000000000000000010
100.0.2.0-100.0.3.255 : 00110000000000000000000000000100
100.0.4.0-100.0.4.255 : 00111000000000000000000000000100
100.0.5.0-100.0.5.255 : 00000100000000000000000000000000
100.0.6.0-100.0.6.255 : 00000010000000000000000000000000
Note the ".2" and ".3" ranges have collapsed in to a single range with the bits 2,3,29 set. The
".4" range remains separate because it also has bit 4 set, which is distinct.
Blending allows selective erasing. Let's erase bits 2,3,29 in all ranges. First a blending functor
is needed to erase bits instead of settting them.
.. literalinclude:: ../../unit_tests/ex_ipspace_properties.cc
:start-after: // reset blend functor
:lines: 1-5
Note this returns :code:`false` if the result of clearing the bits is an empty bitset. Just to be
thorough, let's clear those bits for all IPv4 addresses.
.. literalinclude:: ../../unit_tests/ex_ipspace_properties.cc
:start-after: // erase bits
:lines: 1
The result is ::
5 ranges
100.0.0.0-100.0.0.255 : 10000000000000000000000000000001
100.0.1.0-100.0.1.255 : 01000000000000000000000000000010
100.0.4.0-100.0.4.255 : 00001000000000000000000000000000
100.0.5.0-100.0.5.255 : 00000100000000000000000000000000
100.0.6.0-100.0.6.255 : 00000010000000000000000000000000
The ".2" and ".3" ranges have disappeared, as this bit clearing cleared all the bits in those
ranges. The ".4" range remains back to its original state, the extra bits having been cleared. The
other ranges are unchanged because this operation did not change their payloads. No new ranges have
been added because the result of unsetting bits where no bits are set is also the empty bit set.
This means the blender returns :code:`false` and this prevents the range from being created.
As a final note, although the data used here was network based, that is in no way required.
This line of code being executed
.. literalinclude:: ../../unit_tests/ex_ipspace_properties.cc
:start-after: // ragged boundaries
:lines: 1
yields the result ::
7 ranges
100.0.0.0-100.0.0.255 : 10000000000000000000000000000001
100.0.1.0-100.0.1.255 : 01000000000000000000000000000010
100.0.2.19-100.0.3.255 : 00000000000000001010100000000000
100.0.4.0-100.0.4.255 : 00001000000000001010100000000000
100.0.5.0-100.0.5.117 : 00000100000000001010100000000000
100.0.5.118-100.0.5.255 : 00000100000000000000000000000000
100.0.6.0-100.0.6.255 : 00000010000000000000000000000000
Although the examples up to now have used :code:`PAYLOAD` as the argument type to the
:code:`blend` method, this is not required in general. The type of the second argument
to :code:`blend` is determined by the second argumen to the functor, so that data other
than strictly :code:`PAYLOAD` can be blended into the space. For instance the blending
functor might directly take a list of bit indices -
.. literalinclude:: ../../unit_tests/ex_ipspace_properties.cc
:start-after: // bit list blend functor
:lines: 1-5
In this case the call to :code:`blend` must also take a list of bit indices, not a :code:`PAYLOAD`
(e.g. a :code:`std::bitset<32>`).
.. literalinclude:: ../../unit_tests/ex_ipspace_properties.cc
:start-after: // bit list blend functor
:lines: 7-8
That call to :code:`blend` will blend bits 10 and 11 into all IPv4 addresses except the first and
last, yielding ::
10 ranges
0.0.0.1-99.255.255.255 : 00000000001100000000000000000000
100.0.0.0-100.0.0.255 : 10000000001100000000000000000001
100.0.1.0-100.0.1.255 : 01000000001100000000000000000010
100.0.2.0-100.0.2.18 : 00000000001100000000000000000000
100.0.2.19-100.0.3.255 : 00000000001100001010100000000000
100.0.4.0-100.0.4.255 : 00001000001100001010100000000000
100.0.5.0-100.0.5.117 : 00000100001100001010100000000000
100.0.5.118-100.0.5.255 : 00000100001100000000000000000000
100.0.6.0-100.0.6.255 : 00000010001100000000000000000000
100.0.7.0-255.255.255.254 : 00000000001100000000000000000000
History
*******
This is based (loosely) on the :code:`IpMap` class in Apache Traffic Server, which in turn is based
on IP addresses classes developed by Network Geographics for the Infosecter product. The code in
Apach Traffic Server was a much simplified version of the original work and this is a reversion
to that richer and more complete set of classes and draws much of its structure from the Network
Geographics work directly.
I want to thank `Uthira Mohan <https://www.linkedin.com/in/uthiramohan>`__ for being my intial
tester and feature requestor - in particular the design of blending is a result of her feature
demands.