Send UDP packet to link-local IPv6 address?

Grant Edwards invalid at invalid.invalid
Wed Oct 29 11:29:13 EDT 2014


How do you send a UDP frame to an IPv6 link-local address?

Initially, I'm most concerned with Linux.

I'm using CPython 2.7.7, but so far all behavior seems to be identical
under 3.3.5, and I need to keep my code compatible with both 2.7 and
3.3/3.4.

I'm particularly interested in sending to the all-nodes multicast
address (ff02::1), but all the experimenting I've done seems to show
that multicast and unicast behave the same.

With link-local addresses you also need to specify which interface to
use. The normal way of doing this on Linux with command-line utilities
is append %<ifname> to the address/hostname (e.g. ping6 ff02::1%net1).

That doesn't work:

s.sendto(data, ("ff02::1%net1",port))
s.sendto(data, ("fe80::2c0:4eff:fe40:5f%net1",port))

The "%net1" appears to be ignored, and the packet will go out _some_
interface, but I can't figure out how it decides which one (it's not
always the same one).

The only way I've found to send to link-local IPv6 addresses is to use
the extended length destination address tuple of (address, port,
flow_info, scope_id) like so:

s.sendto(data, ("ff02::1",port,0,scope_id))
s.sendto(data, ("fe80::2c0:4eff:fe40:5f",port,0,scope_id))

Through trial and error, I've determined that the valid scope_id
values for my system are 0,2,3,4, and I've found that 4 corresponds to
interface "net1".

After re-reading the Python 2.7 socket module documentation, I can't
find any way to map an interface name to a scope id.  So, after
browsing around /proc, I found the file /proc/net/if_inet6 which
contains:

$ cat if_inet6 
fe80000000000000922b34fffe5e7edc 03 40 20 80     net0
00000000000000000000000000000001 01 80 10 80       lo
fdfedcba987600100000000000000001 04 40 00 80     net1
fe80000000000000021b21fffeb1d1e9 04 40 20 80     net1
fdfedcba987600080000000000000004 03 40 00 80     net0
fe80000000000000922b34fffe5e7ede 02 40 20 80     net2

The first column is obviously the IPv6 address and the last column
the interface name.

It appears that second column is the scope id, and some further
reading has lead me to believe that the fourth column is the scope
(global vs. link-local vs. internal). So I've done the following:

ip6scopeid = {}
for line in open("/proc/net/if_inet6"):
    addr, id, _, scope, _, ifacename = line.split()
    ip6scopeid[ifacename] = int(id)
    
This is highly non-portable, but now I can at least send to link-local
addresses like this:

s.sendto(data, ("ff02::1",port,0,ip6scopeid["net1"]))
s.sendto(data, ("fe80::2c0:4eff:fe40:5f",port,0,ip6scopeid["net1"]))

So, my program works (but only on Linux).


What's the pythonic, portable way to do this?


The only other thing I can think of is to make the user specify the
IPv6 address of the interface they want to send from, bind a socket to
that address, then ask for that socket's address:

 s.bind(("fdfe:dcba:9876:10::1",65432))
 print s.getsockname()

The above prints this:

 ('fdfe:dcba:9876:10::1', 5678, 0, 0)
 
so, it unfortunately appears that getsockname() doesn't return the
scope id if you bind to the global address assigned to an interface.  

Trying to bind to the link-local address fails unless you already know
the scope id and pass it to bind:

This fails:  s.bind(("fe80::21b:21ff:feb1:d1e9",5678))
This works:  s.bind(("fe80::21b:21ff:feb1:d1e9",5678,0,4))

But I'm trying to _find_ the scope id, so that isn't helpful.

Any suggestions?

Ideally, I'd like a solution that works on Windows and BSD as well...

-- 
Grant Edwards               grant.b.edwards        Yow! Am I SHOPLIFTING?
                                  at               
                              gmail.com            



More information about the Python-list mailing list