ICMP reply not received

Ex Windows programmer trying to evolve to Mac... please go easy. :-) As part of my transition I'm trying to get some older stuff I wrote for myself to work.

I have a command line utility program which uses socket(), connect(), send(), select/recv() to create a RAW socket and send a ICMP_ECHO packet to a host and then await its return. Essentially, PING with some minor variation in its output.

Got it built in Xcode. Found that socket() with SOCK_RAW failed until I ran as root (via SUDO). OK, no problem.

Now it sends, but it never receives a response. The select() always times out rather than receiving the reply. When I run my Windows cmd line version on the same machine under Parallels, it works fine. So I'm confident the other host is able to be reached from my network, etc. I can use actual PING from the command line just fine too.

Is there some other permission facility in MacOS that would prevent me from receiving the reply packets? What would it be called, how would I either turn it off or work with it?

Thanks for any help!

Answered by DTS Engineer in 814001022
Ex Windows programmer trying to evolve to Mac

Welcome!

A raw socket should work for sending and receiving ICMP. Having said that, you can don’t need to use a raw socket. You can do this using an ICMP socket, and that has one key advantage: It doesn’t require privileges. So you can just build and run in Xcode rather than using sudo.

There’s a really old sample, SimplePing, that shows the basics. Sadly, I’ve not had time to update it (in the last 8 years, wow). But it think it might be enough to get you unblocked. If not, write back with details about where you’re stuck.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Ex Windows programmer trying to evolve to Mac

Welcome!

A raw socket should work for sending and receiving ICMP. Having said that, you can don’t need to use a raw socket. You can do this using an ICMP socket, and that has one key advantage: It doesn’t require privileges. So you can just build and run in Xcode rather than using sudo.

There’s a really old sample, SimplePing, that shows the basics. Sadly, I’ve not had time to update it (in the last 8 years, wow). But it think it might be enough to get you unblocked. If not, write back with details about where you’re stuck.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Hi Quinn, thanks for the kind attention and example. I looked it over, and while I'm not too up on Swift I don't quite know why mine wouldn't work as I think(?) the basic network calls are similar. Though... there are some higher level wrappers that I'm not highly familiar with.

I boiled my C++ (but barely) code down to its minimum to illustrate the issue. I compile and run this on MacOS with g++, as well as Windows with g++ (using mingw64 tools). Works fine on Windows, both on Win11/Parallels on my Mac as well as my 12+ yr old Win10 machine.

I've also pasted in snapshots of the output. On MacOS it always times out in the select(), i.e. never receives anything.

MacOS output (terminal)

~/source/test % g++ pingtest.cpp                                                         ~/source/test % a.out www.yahoo.com                                                      connecting to www.yahoo.com (69.147.88.8)
Timed out with no reply received
~/source/test % 

Windows output (cmd), built on Win11/Parallels but same output when the exe is copied to and run on old Win10

M:\source\test>g++ -D_WINDOWS pingtest.cpp -lws2_32
M:\source\test>a.exe www.yahoo.com
connecting to www.yahoo.com (69.147.88.8)
Reply was received, 0.013000 secs
M:\source\test>

And the C++ source code

// pingtest.cpp: MacOS times out on select() (line 165)
#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#include <assert.h>
#include <unistd.h>
#include <time.h>
#include <errno.h>
#include <string.h>

#ifndef _WINDOWS
// Compiling on MacOS needs these for various definitions
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

// windows-y terms for compiling in unix land
typedef int SOCKET;
#define INVALID_SOCKET -1
#define SOCKET_ERROR -1

// SOCK_DGRAM means no need to run as sudo on MacOS
// (but select() times out either way, still illustrating the problem)
#define MY_SOCK_TYPE SOCK_DGRAM

#else	 // _WINDOWS

#include <winsock.h>

// create a RAW socket b/c get error WSAPROTONOSUPPORT w/SOCK_DGRAM
#define MY_SOCK_TYPE SOCK_RAW

#endif  // _WINDOWS

#define ICMP_ECHO 8
#define ICMP_ECHOREPLY 0

// The IP header
struct IpHeader {
	unsigned char h_len_version;	  // Whoa, g++ doesn't always pack bit fields
// 	unsigned int h_len:4;          // length of the header
// 	unsigned int version:4;        // Version of IP
	unsigned char tos;				  // Type of service
	unsigned short total_len;		  // total length of the packet
	unsigned short ident;			  // unique identifier
	unsigned short frag_and_flags;  // flags
	unsigned char  ttl; 
	unsigned char proto;           // protocol (TCP, UDP etc)
	unsigned short checksum;       // IP checksum

	unsigned int sourceIP;
	unsigned int destIP;
};

//
// ICMP header
//
struct IcmpHeader {
	unsigned char i_type;
	unsigned char i_code;							// type sub code
	unsigned short i_cksum;
	unsigned short i_id;
	unsigned short i_seq;

	// timestamp as a payload
	unsigned int timestamp;
};

const int ICMP_LEN=32;								// actual total length of IcmpHeader


static SOCKET m_s=INVALID_SOCKET;


// simple error handling for test program - just print a message and terminate
static void FAIL(const char* msg)
{
	printf("%s: %s\n",msg,strerror(errno));
	exit(1);
}

static void init_socket(const char* szHost)
{
	assert(szHost != NULL);							// valid string needed
	assert(m_s == INVALID_SOCKET);				// only call once, please

 	m_s = socket(AF_INET,MY_SOCK_TYPE,IPPROTO_ICMP);
	if(m_s == INVALID_SOCKET)
		FAIL("socket()");

	sockaddr_in dest;
	memset(&dest,0,sizeof(dest));
	dest.sin_family = AF_INET;

	// convert host string from either x.x.x.x or hostname to ip address
	if((dest.sin_addr.s_addr = inet_addr(szHost)) == INADDR_NONE) {

		hostent* hp = gethostbyname(szHost);

		if(!hp) FAIL("Unknown host");
		if(hp->h_addrtype != AF_INET)	FAIL("Host is not an IP address");

		assert(hp->h_length == sizeof(dest.sin_addr.s_addr));
		memcpy(&(dest.sin_addr),hp->h_addr,hp->h_length);
	}
	// quick message confirming hostname translated
	printf("connecting to %s (%s)\n",szHost,inet_ntoa(dest.sin_addr));

	if(connect(m_s,(sockaddr*)&dest,sizeof(dest)) != 0)
		FAIL("connect()");
}


static unsigned short checksum(const unsigned short* buf,int size)
{
	unsigned int cksum=0;
	while(size >= sizeof(unsigned short)) {
		cksum += *buf++;
		size -= sizeof(unsigned short);
	}
	const unsigned char* bbuf = (unsigned char*)buf;
	while(size-- > 0)
		cksum += *buf++;
   cksum = (cksum >> 16) + (cksum & 0xffff);
	cksum += (cksum >> 16);
	return (unsigned short)(~cksum);
}

static void do_ping(void)
{
	static unsigned short m_seq=100;			 // start non-zero just for grins
	char buf[1024];								 // just a byte buffer for send/recv

	IcmpHeader* icmp = (IcmpHeader*)buf;

	icmp->i_type = ICMP_ECHO;
	icmp->i_code = 0;
	icmp->i_id = (unsigned short)getpid();
	icmp->i_cksum = 0;
	icmp->i_seq = m_seq++;
	icmp->timestamp = clock();								// arbitrary thing to pass around
	memset(icmp+1,'E',ICMP_LEN-sizeof(IcmpHeader));	// fill with some junk
	icmp->i_cksum = checksum((const unsigned short*)icmp,ICMP_LEN);


	//
	// Send the ICMP request
	//
   size_t n = send(m_s,buf,ICMP_LEN,0);
	if(n == SOCKET_ERROR)
		FAIL("send()");

	assert(n == ICMP_LEN);

	//
	// Wait for the ICMP response
	//
	fd_set fd;
	timeval tmo;
	FD_ZERO(&fd);
	FD_SET(m_s,&fd);
	tmo.tv_sec = 10;									// timeout in secs
	tmo.tv_usec = 0;
	n = select(m_s,&fd,0,0,&tmo);
	if(n == SOCKET_ERROR)							// error?
		FAIL("select()");

	if(n == 0) {											// timed out waiting
		//
		// THIS IS THE PROBLEM ON MACOS - NEVER GETS PAST HERE, ALWAYS TIMES OUT
		//
		printf("Timed out with no reply received\n");
		return;
	}

	assert(n == 1 && FD_ISSET(m_s,&fd));

	//
	// Receive the ICMP response
	//
	n = recv(m_s,buf,sizeof(buf),0);
	if(n == SOCKET_ERROR)							// error?
		FAIL("recv()");
	if(n < sizeof(IpHeader))
		FAIL("Received invalid IP packet");

	IpHeader* iphdr = (IpHeader*)buf;
	int h_len = (iphdr->h_len_version & 0x0F); // g++ bit packing hack
	if(n < (int)(h_len*4+sizeof(IcmpHeader)))
		FAIL("Received too few ICMP bytes in reply");

	icmp = (IcmpHeader*)(buf + h_len*4);
	if(icmp->i_type != ICMP_ECHOREPLY) {
		printf("Received ICMP non-echo type %d\n",icmp->i_type);
		return;
	}
	if(icmp->i_id != (unsigned short)getpid())
		printf("warning: received someone else's packet!\n");

	if((icmp->i_seq+1) != m_seq)
 		printf("warning: received reply out of sequence\n");

	printf("Reply was received, %f secs\n",(double)(clock() - icmp->timestamp)/CLOCKS_PER_SEC);
}


static void close_socket(void)
{
	if(m_s != INVALID_SOCKET)
		close(m_s);
	m_s = INVALID_SOCKET;
}


int main(int argc, char* argv[])
{
#ifdef _WINDOWS
	WSADATA wsaData;
	if(WSAStartup(MAKEWORD(2,1),&wsaData) != 0)
		printf("WSAStartup failed: 0x%08x\n",::GetLastError());
#endif

	if(argc < 2) {
		printf("Usage: %s host\n",argv[0]);
		return 0;
	}

	int n = -1;

	init_socket(argv[1]);

	do_ping();

	close_socket();

	return 0;
}


I noticed you’re connecting your socket. SimplePing doesn’t do that, and I wouldn’t be surprised if that is the cause of your problem. Try removing that and see if it helps.

ps You could simplify your code by using getaddrinfo. The old school DNS APIs are a world of unpleasantness.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Thanks for the thought, but... I don't think connect does anything except tell the socket what address to send to. Supporting this theory, I did switch to skip calling connect(), followed by using sendto() with the destination address instead. No change in behavior, both platforms act the same as before.

It still kinda feels like somewhere there's some kind of OS filtering maybe. I did turn off the System Settings->Network->Firewall switch and run again, but no change. Might the OS filter inbound ICMP packets at a low level in some way? Or maybe the outbound ones, actually, it is possible it never got sent.

Thanks for the idea on getaddrinfo(), will change that (haven't tried yet tho). But it feels tangential to whatever is actually going on.

I have kind of figured out the problem, but I don't "understand" it.

I switched the code from making a select() followed by recv(), to just calling recvfrom(). The select() call always times out. But recvfrom() works!

However, my understanding is the semantics of recvfrom() are to block until there is something to read. But if I send a ping and it doesn't work for whatever of many possible reasons, I need to not wait forever and time out after a few seconds of waiting.

Key semantic question: why does select not wake up when there is (or should be?) a data packet there? I inserted some sleep() time before the recvfrom() just to make sure it would receive something even if it was buffered somewhere.

Also just FWIW, I confirmed that the desired packets were being received on wire by using the following in a separate shell window:

$ sudo tcpdump 'icmp'

This saw my packets get sent and received, and led me to try other things.

Accepted Answer

OK, egg on my face, I have figured this out.

Having not worked on Unix-y systems much, I neglected to understand what the first argument to select() was. In Windows, it turns out to be basically ignored. In Unix, it is the max number of file descriptors passed in the FD_SET. So, basically, s+1 where s is the file descriptor of the socket. Obviously rookie mistake.

DOH. Thanks for the help and while it took me some personal head scratching I learned something about unix. :-)

ICMP reply not received
 
 
Q