Writing to the socket with send fails, telling me that I need to specify an address.
It does not work because the send() function can only be used for connected sockets (as indicated here ). Usually you use send() to communicate over TCP (connection-oriented) and sendto() can be used to send UDP datagrams (without establishing a connection).
Since you want to send ping packets, or rather ICMP datagrams that are not explicitly installed, you must use the sendto() function.
It seems that no additional fields in sendto actually used to a large extent. So, is there a technical reason why I should use sendto instead of send or is it just oversight in the API?
Short answer:
If you are not allowed to use send() , then only one option remains, called sendto() .
Long answer:
This is not just oversight in the API. If you want to send a UDP datagram using a regular socket (for example, SOCK_DGRAM ), sendto() needs information about the destination address and port that you specified in struct sockaddr_in , right? The kernel inserts this information into the resulting IP header, as struct sockaddr_in is the only place you specify who will be the recipient. Or in other words: in this case, the kernel should get the destination information from your structure, since you are not providing an additional IP header.
Since sendto() used not only for UDP, but also for raw sockets, it should be a more or less "universal" function that can cover all the different use cases, even if some parameters, such as the port number, are not related to / used at the end.
For example, using IPPROTO_RAW (which automatically implies IP_HDRINCL ), you indicate that you want to create an IP header yourself. So the last two arguments to sendto() are actually redundant information, as they are already included in the data buffer that you pass to sendto() as the second argument. Note that even if you use IP_HDRINCL with your raw socket, the kernel will fill in the source address and checksum of your IP datagram if you set the corresponding fields to 0 .
If you want to write your own ping program, you can also change the last argument in your socket() function from IPPROTO_RAW to IPPROTO_ICMP and let the kernel create an IP header for you, so you have one less thing to worry about. Now you can easily see how the two sendto() parameters *dest_addr and addrlen become significant again, because this is the only place you specify the destination address.
The language and APIs are very old and have grown over time. Some APIs may look strange from today's point of view, but you cannot change old interfaces without breaking a huge amount of existing code. Sometimes you just need to get used to things that were defined / developed many years or decades ago.
Hope that answers your question.