Subject: [Anima] How GRASP uses interfaces and sockets

From: Brian E Carpenter <[email-masked]>
Date: Thu, 10 Nov 2016 14:55:46 +1300

Hi,

This message is part of an off-list discussion I've been having with Toerless, but
I hope it's of general interest. The topic is how GRASP will "see" the network
when the ACP is running. I've been concerned that the ACP draft isn't specific
enough about this: what interfaces, sockets and socket options will GRASP use
to communicate over the ACP.

Partly this is due to my own ignorance about practical aspects of VRFs.

So, the rest of this message is about exactly how the current GRASP prototype
handles interfaces and sockets. Hopefully with this information, the ACP team
can explain how this needs to change in the presence of the ACP.

I hope it's only the "WHO AM I" section that needs to change.

The prototype code is documented at
https://www.cs.auckland.ac.nz/~brian/graspy/graspy.pdf
and the Python3 code itself is at
https://www.cs.auckland.ac.nz/~brian/graspy/grasp.py
(under Simplified BSD license).

PART 1: WHO AM I?

When GRASP starts up, the first thing it does is discover what network
interfaces and link-local IPv6 addresses it has, and what global-scope IPv6 address
it will use. The way this is done is different between Windows (Winsock) systems
and POSIX systems. In this message I focus on POSIX (tested on Linux).
(This part is actually a lot easier in Winsock, but completely different.)

This is done by a function _get_my_address(). The guts of it, skipping
failure cases:

for _iid, _ in socket.if_nameindex():
    with socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) as _s:
        try:
            _s.connect(('fe80::100', 4096, 0, _iid))
            _addr = _s.getsockname()[0]
            if '%' in _addr:
                _addr, _zid = _addr.split('%') #strip any Zone ID
                _loc = ipaddress.IPv6Address(_addr)
                if _loc.is_link_local:
                    _ll_zone_ids.append([_zid, _loc])
        except:
            pass

#Convert interface (Zone) IDs to indexes
_ll_zone_ids = [[socket.if_nametoindex(zid), loc] for zid, loc in _ll_zone_ids]

#Now _ll_zone_ids[] contains [interface-index, LL-address] for each interface

#Get own IPv6 address somewhat portably...

_s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
try:
    #This is a hack. We send a bogon to a site-local multicast
    #address (reserved by IANA for 'any private experiment').
    #Then we can find the sending address from the socket.
    #Note that this used to use a bogus global unicast address
    #('2001:db8:f000:baaa:f000:baaa:f000:baaa') but that fails in
    #case of a ULA prefix with no default IPv6 route.
    #
    #To find a non-hack solution, google for 'getnifs.py'

    _s.connect(('ff05::114', GRASP_LISTEN_PORT))
    _s.send(b'0',0)
except:
    pass
_sn = _s.getsockname()[0]
_s.close()
if (not '%' in _sn) and _sn != '::':
    _new_locator = ipaddress.IPv6Address(_sn)

#_new_locator is the IPv6 address we will use

PART 2: MULTICASTING

GRASP needs to send link-local multicasts separately to each
interface. So it needs to create a socket for each interface
for this purpose.

for _x in _ll_zone_ids:
    _make_mcssock(_x[0])

where the function _make_mcssock(ifi) does this:

mcssock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
mcssock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
mcssock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, struct.pack('@I', ifi))
if not listen_self:
    #don't listen to yourself talking
    mcssock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_LOOP, 0)
mcssock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, 1)
_mcssocks.append([ifi, mcssock])

so now whenever we need to send a multicast packet on interface ifi, we do
something like:

_mcssocks[ifi][1].sendto(msgbytes,0,(str(ALL_GRASP_NEIGHBOR_6), GRASP_LISTEN_PORT))

PART 3: LISTENING FOR MULTICASTS

GRASP also needs to listen for incoming LL multicasts. That's done by a separate
thread called _mclisten(). Simplifying the code a bit:

mcrsock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
mcrsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
mcrsock.bind(('',GRASP_LISTEN_PORT))
#join LL multicast group on all interfaces
for x in _ll_zone_ids:
    mreq = ALL_GRASP_NEIGHBOR_6.packed + struct.pack('@I', x[0])
    mcrsock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq)
mcrsock.settimeout(120)

while True:
    try:
        rawmsg, send_addr = mcrsock.recvfrom(GRASP_DEF_MAX_SIZE)
        if "%" in send_addr[0]:
            a,b = send_addr[0].split('%')
            saddr = ipaddress.IPv6Address(a)
        else:
            saddr = ipaddress.IPv6Address(send_addr[0])
        sport = send_addr[1]
        ifn = send_addr[3]

At this point we have the raw message in rawmsg, the interface number
in ifn, the source address in saddr and the source port in sport.

PART 4: LISTENING FOR TCP

GRASP needs to listen for incoming discovery responses via TCP.
This is fairly straightforward use of a normal socket so I will
miss out the details -- see function _init_drsocks(i) and
the class of threads _drlisten(threading.Thread)

socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
bind(('',_port))
listen(5)
accept()
recvfrom(GRASP_DEF_MAX_SIZE)

There is one tricky bit - we bind the TCP socket to the port number
used for *sending* discovery multicasts on the same interface.
That's the point Michael Richardson and I discussed a few weeks back,
so I won't repeat it here.

The other TCP sockets used during GRASP synchronization and
negotiation are completely straightforward.

Regards
   Brian Carpenter