3

I am trying to send an icmpv6 ping packet across running as root (python 2.7 on Linux)

I understand that sendto uses two tuple struct in case of ipv4 (and it works) and know that ipv6 uses a 4 tuple struct. Still i can't get it to work.

It either results in an "invalid argument" or "socket.gaierror: [Errno -2] Name or service not known"

Following is a bare minimum example showing what i am attempting. I am even fine if i can get it to work with local host in case of ipv6 i.e. ::1

import socket

def main(dest_name):
    #dest_addr = socket.gethostbyname(dest_name)
    addrs = socket.getaddrinfo(dest_name, 0, socket.AF_INET6, 0, socket.SOL_IP)

    print addrs
    dest = addrs[2]

    port = 33434 # just some random number because of icmp
    icmp = socket.getprotobyname('ipv6-icmp')
    #print icmp

    send_socket = socket.socket(socket.AF_INET6, socket.SOCK_RAW, icmp)
    print "sent to " + str(dest[4])
    send_socket.sendto('', (str(dest[4]), port))
    send_socket.close()

if __name__ == '__main__':
    main('ipv6.google.com')

I actually tried each tuple from the addr list, but the result is same.

Update:

Also tried alternates with sendto's params, but it results in invalid arguments whether i use local host or google ipv6 address

send_socket.sendto('', dest[4])

Update 2:

For reference, working ipv4 code follows (as asked in comments)

def main(dest_name):
    dest_addr = socket.gethostbyname(dest_name)
    icmp = socket.getprotobyname('icmp')

    send_socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, icmp)
    print "sent to " + dest_name#str(dest[4])
    send_socket.sendto('', (dest_addr, 0))
    send_socket.close()

if __name__ == '__main__':
    main('www.google.com')

Update 3:

When i run the v6 version with dest[4] as the only parameter (no string, just the tuple and NO port), following is output on my machine (Mint 15) which includes printing interfaces

sudo python test_v6.py 
[(10, 1, 6, '', ('::1', 0, 0, 0)), (10, 2, 17, '', ('::1', 0, 0, 0)), (10, 3, 0, '', ('::1', 0, 0, 0))]
sent to ('::1', 0, 0, 0)
Traceback (most recent call last):
  File "test_v6.py", line 18, in <module>
    main('::1')
  File "test_v6.py", line 14, in main
    send_socket.sendto('', dest[4])
socket.error: [Errno 22] Invalid argument

I am not sure why it still produces invalid argument

fkl
  • 5,412
  • 4
  • 28
  • 68
  • Can you show your working IPv4 code? I have no idea why you expected `str(dest[4])` to work with IPv6's 4-tuple, when it doesn't work with IPv4's 2-tuple. Either way, that creates a string representation of a Python tuple, which looks nothing like what `sendto` wants. – abarnert Nov 11 '13 at 09:12
  • Meanwhile, please show the actual exception you get. When I try this code with `dest[4]` (with or without `dest[4][1] = port` first), it succeeds for localhost addresses, and returns an `ENETUNREACH` error for Google (which makes sense, since I don't have an IPv6 internet connection). – abarnert Nov 11 '13 at 09:19
  • Finally, can you make IPv6 TCP connections to Google? Can you `traceroute6` or `ping6` them from the shell? – abarnert Nov 11 '13 at 09:20
  • I only converted dest[4] to string, when i first time put it and the error was, 'function expects string or buffer, not tuple'. I only realized latter that it was because of another argument 'port' that i unnecessarily appended. – fkl Nov 11 '13 at 09:50
  • The first thing to note is that your IPv4 code doesn't do most of the same things your IPv6 code does. It just uses `dest_name` as-is rather than trying to figure out what to pass out of `dest_addr`, and it uses port `0`. If you change it to match the IPv6 code, does it still work? If you change the IPv6 to match the IPv4, does it still break? And meanwhile, can you answer the other questions? – abarnert Nov 11 '13 at 09:58
  • Yes, it breaks if i try to match the ipv4 code. First gethostbyname is ipv4 only api, and it is officially recommended to use getaddrinfo for ipv6. Other than that i only tried changing min things i.e. instead of v4, specified v6 where ever needed. i Also tried with port 0 for ipv6. Infact the 4 tuple that results for ipv6 in fact does contain all 0's. You mentioned it succeeded for local host addresses. Please let me know what you did exactly since i haven't been able to do that. Meanwhile i will post precise errors. – fkl Nov 11 '13 at 10:12
  • Again, you're not even using the result of `gethostbyname` in your IPv4 code, you're just sending `(dest_name, 0)`. So, that's a pretty drastic change—you're not writing the equivalent IPv6 code to the IPv4 code you have working, you're writing the equivalent IPv6 code to IPv4 code that you're not testing at all. – abarnert Nov 11 '13 at 10:14
  • Meanwhile, what worked for me is just using `dest[4]`, without trying to turn the tuple into a string, replace the port, or anything else complicated. It's working on an old Fedora box; it's not working on a Mac (with, for some reason an `ENOBUFS` error on the `sendto`, which implies that my IPv6 lo interface has a full out queue, which makes no sense… but anyway, you're using linux, so that's not important). It might help to know which linux distro/version you're using; I'll bet it's much newer than mine and has very different IPv6 configs… – abarnert Nov 11 '13 at 10:19
  • Ah, i missed editing that in code above. Actually gethostbyname returns the name as string unchanged if passed name instead of numeric value. I am using Linux Mint 15 which is pretty latest and i doubt distro would have any role in this. Rather probably my understanding of the api. – fkl Nov 11 '13 at 10:24
  • Added logs with the case that seems to work with you locally (i put in local host i.e. ::1 instead of google url). Also i used each of the interfaces one by one, but it doesn't change any thing on the error. – fkl Nov 11 '13 at 10:30
  • Seems like others have been facing similar situation although in some of the cases, it was just the link prefix that solved the problem. I am trying on loop back so i guess that excludes my scenario http://stackoverflow.com/questions/3801701/how-to-create-ipv6-socket-at-python-wQhy-got-the-socket-error-22-invalid-arg?rq=1 – fkl Nov 11 '13 at 10:59
  • Have you tried `bind`ing the socket to the appropriate local address for your loopback interface (for the `::1` test) or for `eth0` (or whatever) (for the Google test) before calling `sendto`? – abarnert Nov 11 '13 at 20:08
  • No, documentation says a socket passed to sendto Shouldn't have been bound to a socket. It is primarily used for disconnected sockets. – fkl Nov 12 '13 at 04:36
  • Where does the documentation say that? How else could you possibly set the source address of a socket except by binding it? – abarnert Nov 12 '13 at 04:41
  • "Send data to the socket. The socket should not be connected to a remote socket, since the destination socket is specified by address" http://docs.python.org/2/library/socket.html – fkl Nov 12 '13 at 04:47
  • I guess i mixed the two, you meant binding to local interface and the statement talks about being connected to a remote socket. I will try that out and let you know thanks – fkl Nov 12 '13 at 04:47
  • Right, if you call `connect`, then you generally want to call `send` instead of `sendto`. As I explained in my answer. But `bind` isn't relevant to that. – abarnert Nov 12 '13 at 04:51

2 Answers2

2

Your original problem was that bizarre things like a 2-tuple whose first member is a Python string representation of the 4-tuple address are not even close to valid ways to specify an address.

You can fix that by just using dest[4] itself—that is, the tuple you got back as the sockaddr part of getaddrinfo—as the address. (As Sander Steffann's answer explains, you're not exactly doing this cleanly. But in your case, at least for '::1' or 'localhost' with the other values you've specified, you're going to get back the right values to use.) You should also probably use addrs[0] rather than addrs[2].

Anyway, in your Update 3, you appear to have done exactly that, and you're getting socket.error: [Errno 22] Invalid argument. But there are two arguments to sendto, and it's the other one that's invalid: '' is not a valid ICMP6 packet because it has no ICMP6 header.

You can test this pretty easily by first connecting to dest[4], which will succeed, and then doing a plain send, which will fail with the same error.

For some reason, on Fedora 10 (ancient linux), the call seems to succeed anyway. I don't know what goes out over the wire (if anything). But on Ubuntu 13.10 (current linux), it fails with EINVAL, exactly as it should. On OS X 10.7.5 and 10.9.0, it fails with ENOBUFS, which is bizarre. In all three cases, if I split the sendto into a connect and a send, it's the send that fails.

'\x80\0\0\0\0\0\0\0' is a valid ICMP6 packet (an Echo service request header with no data). If I use that instead of your empty string, it now works on all four machines.

(Of course I still get ENETUNREACH or EHOSTUNREACH when I try to hit something on the Internet, because I don't have an IPv6-routable connection.)

abarnert
  • 354,177
  • 51
  • 601
  • 671
  • Thanks, i am going to try this out and verify i have made any mistake in entering tuples and get back to you. – fkl Nov 12 '13 at 04:44
  • It works, great - thanks. I never bothered with the payload itself because i thought it works with ipv4 i.e. does not give error and tries to spit out the packet, so it must do the same with ipv6 too. I was basically narrowing down the problem. So it means the actual issue is in the manual header construction itself that i mentioned in comments. Really appreciate help from both of you. – fkl Nov 12 '13 at 08:35
  • @fayyazkl: IPv4 ICMP shouldn't work with an empty packet either, but it seems to do so on more platforms than IPv6 ICMP. From what I can tell, linux 2.6 allows empty ICMP4 and 6, linux 3.10 allows 4 but not 6, OS X 10.7 and 10.9 allow neither. I have no idea what happens farther up the chain—does the kernel silently drop the packet? If not, what does a router do with an ICMP packet with no ICMP type? Might be fun to play with. – abarnert Nov 12 '13 at 18:38
1

All the answers you are looking for are pretty much in the manual.

First, the port number is part of the info that getaddrinfo returns. Call it like this:

def main(dest_name):
    # A minimal ICMP6-echo message (thanks to abarnert)
    data = '\x80\0\0\0\0\0\0\0'

    # Parameters for getaddrinfo
    req_port = 0
    req_family = socket.AF_INET6
    req_socktype = socket.SOCK_RAW
    req_proto = socket.getprotobyname('ipv6-icmp')

    # Resolve the name and get the addrinfo
    addrs = socket.getaddrinfo(dest_name, req_port, req_family, req_socktype, req_proto)

    # This gives me: [(30, 3, 58, '', ('2a00:1450:4013:c01::63', 0, 0, 0))]
    # Which is what you use in your calls to `socket` and `sendto`, like:
    success = False
    for addr in addrs:
        try:
            (family, socktype, proto, canonname, sockaddr) = addr
            send_socket = socket.socket(family, socktype, proto)
            sent = send_socket.sendto(data, sockaddr)
            send_socket.close()
        except socket.error:
            # Try the next address
            continue

        # Stop if it worked
        if sent == len(data):
            success = True
            break

    return success

When now running main('ipv6.google.com') you can see the ping and the reply using i.e. tcpdump:

01:14:46.763160 IP6 2a00:8640::5ce4 > 2a00:1450:4013:c01::63: ICMP6, echo request, seq 0, length 8
01:14:46.785060 IP6 2a00:1450:4013:c01::63 > 2a00:8640::5ce4: ICMP6, echo reply, seq 0, length 8
Sander Steffann
  • 9,509
  • 35
  • 40
  • That's all true, but if you look at his code, those are the exact same values he will end up with if he just fixes the `sendto` line to use `send_socket.sendto('', dest[4])`, as I suggested in a comment, and as he says he already did (see the **Update** and **Update3** in his question). So, it can't possibly solve his problem. – abarnert Nov 11 '13 at 20:04
  • Thanks, you are right. Just trying to teach people how to do network code properly :-) I completely forgot to specify `data` in my example, which indeed could be `'\x80\0\0\0\0\0\0\0'` for a minimal working example. Fixed! – Sander Steffann Nov 12 '13 at 00:11
  • Data part is not important here. I have been writing code for network processors and a lot of other networking stacks for over a decade and respectfully, i don't exactly see what is here to be taught. It is true that i have mostly coded in c and am new to python. Still with the example given above, i actually used the data[4] which was exactly the sock_addr struct with no improvement. In my actual code, i am building ip header too which is passed in place of data, but is not relevant to the problem mentioned above, hence i removed it – fkl Nov 12 '13 at 04:41
  • (along with the setsock option to build header myself). Thanks for help though. Appreciate that. – fkl Nov 12 '13 at 04:44
  • @fayyazkl: But it _is_ relevant to the problem here. And the fact that you apparently didn't know that EINVAL could mean that you were sending an invalid ICMP packet, rather than that you were sending to an invalid IPv6 address, proves that you don't know everything. (Which isn't surprising, given that the person who figured it out—me—certainly doesn't know even close to everything.) – abarnert Nov 12 '13 at 04:49
  • Ouch! That's interesting, because the ipv4 code just runs fine with the same data being empty. That was the reason i didn't consider it to be the source of problem. Any way i will try with adding data to see if it works that way. A lot of things are less obvious with broader api in python. – fkl Nov 12 '13 at 04:53
  • Agreed! When i put in the exact contents of data mentioned above, it worked. So there was nothing actually wrong with the tuple being used. Thanks a million. – fkl Nov 12 '13 at 08:29
  • On Linux Mint 18 with python3.6 and Centos7 with python3.4, this code gives the error `socket.gaierror: [Errno -8] Servname not supported for ai_socktype` on the `socket.getaddrinfo` call. Tested with multiple input hostnames. – ocket8888 Aug 29 '18 at 16:12