How to run a BASH script in response to a network event?

Posted on

QUESTION :

I’ve got a little BASH script I’d like to run on a few Mac’s (A mix of 10.8.5 and 10.9.4). I’d like to trigger the script whenever my mac attempts to connect to another one through a particular TCP port. The IP addresses of both of the computers are known, and I can see the traffic (connection on port 6472) when I watch via nettop or console.

What I haven’t been able to find – and perhaps I’m not searching correctly – is a tool that will run in the background and watch for that connection request, then run an executable when it occurs. I suppose I could cook one up, but this seems like a problem that others have already solved. Any direction toward an existing solution would be most welcome.

Thanks! Mike

ANSWER :

How about running something like this in the background, using tcpdump with the right filter and firing up your script each time it matches ?

tcpdump -l -i eth0 'dst host other_host and dst port 6472 and tcp[tcpflags] = tcp-syn' 2>/dev/null | while read f ; do your_script; done

replace eth0 with your network interface, other_host with the other machine’s IP address

If you don’t want a terminal window lying around you can make this a bash script and run it inside a screen session.

Another alternative, and possibly simpler solution to @ultrasawblade solution might be to run knockd. – It appears there is a version of it available for Mac.

Probably the most versatile and customised approach to finish your quest is to peruse dtrace.

Either use a generic socket or tcp provider and run the script in destructive mode to call system() from the kernel or use Function Boundary Tracing (FBT) as your provider. The latter will make your script dependent on the OSX version.

Toying around a bit, I came up with something that should give you enough fodder for your final solution using a socket provider (so it should work on both of your MacOSX machines):

#!/usr/sbin/dtrace -s

#pragma D option quiet
#pragma D option switchrate=10hz
#pragma D option destructive 

/* AF_INET{6} are unknown to dtrace, so replace with numbers */
inline int af_inet  =  2; /* AF_INET  */
inline int af_inet6 = 30; /* AF_INET6 */
inline const string procname = "nc";

dtrace:::BEGIN
{
    /* Add translations as desired from /usr/include/sys/errno.h */
    err[0]            = "Success";
    err[EINTR]        = "Interrupted syscall";
    err[EIO]          = "I/O error";
    err[EACCES]       = "Permission denied";
    err[ENETDOWN]     = "Network is down";
    err[ENETUNREACH]  = "Network unreachable";
    err[ECONNRESET]   = "Connection reset";
    err[ECONNREFUSED] = "Connection refused";
    err[ETIMEDOUT]    = "Timed out";
    err[EHOSTDOWN]    = "Host down";
    err[EHOSTUNREACH] = "No route to host";
    err[EINPROGRESS]  = "In progress";
    err[EADDRNOTAVAIL] = "Can't assign requested address";

    printf("%-6s %-20s %-8s %-21s %-21s %-8s %sn", 
        "PID", "PROCNAME", "FAMILY", "S_ADDR:S_PORT", "D_ADDR:D_PORT", 
        "LAT(us)", "RESULT");
}

/*  MacOSX connectx() syscall:

    connectx(arg0:int s, arg1:struct sockaddr *src, arg2:socklen_t srclen, 
                         arg3:struct sockaddr *dsts, arg4:socklen_t dstlen, 
                         arg5:uint32_t ifscope, arg6: associd_t aid, 
                         arg7:connid_t *cid);
*/

syscall::connectx:entry
{
    this->s = (struct sockaddr_in *) copyin(arg3, sizeof(struct sockaddr)); 
    this->f = this->s->sin_family;
} 

syscall::connectx:entry 
/ this->f == af_inet && execname == procname / 
{ 
    this->s = (struct sockaddr_in *) copyin(arg1, sizeof(struct sockaddr)); 
    self->address = inet_ntop(this->f, (void *) &this->s->sin_addr);
    self->port = ntohs(this->s->sin_port);
    self->s_addr = strjoin(strjoin(self->address, ":"), lltostr(self->port));

    this->d = (struct sockaddr_in *) copyin(arg3, sizeof(struct sockaddr)); 
    self->address = inet_ntop(this->f, (void *) &this->d->sin_addr);
    self->port = ntohs(this->d->sin_port);  
    self->d_addr = strjoin(strjoin(self->address, ":"), lltostr(self->port));

    self->ts = timestamp; 
}

syscall::connectx:entry
/ this->f == af_inet6 && execname == procname /
{
    this->s6 = (struct sockaddr_in6 *) copyin(arg1, sizeof(struct sockaddr_in6));
    self->port = ntohs(this->s6->sin6_port);
    self->address = inet_ntop(this->f, (void *) &this->s6->sin6_addr);
    self->s_addr = strjoin(strjoin(self->address, ":"), lltostr(self->port));

    this->d6 = (struct sockaddr_in6 *) copyin(arg3, sizeof(struct sockaddr_in6));
    self->port = ntohs(this->d6->sin6_port);
    self->address = inet_ntop(this->f, (void *) &this->d6->sin6_addr);
    self->d_addr = strjoin(strjoin(self->address, ":"), lltostr(self->port));

    self->ts = timestamp;
}

syscall::connectx:return 
/ self->ts / 
{ 
    this->delta = (timestamp - self->ts) / 1000; 
    this->errstr = err[errno] != NULL ? err[errno] : lltostr(errno);

    /* Basically anything can be called here */
    system("date");
    printf("%-6d %-20s %-8d %-21s %-21s %-8d %sn", 
        pid, execname, this->f, self->s_addr, self->d_addr,
        this->delta, this->errstr); 

    self->family = 0; 
    self->ts = 0; 
}

Save it to a file (let’s say ./socket_connect_mac_simple.d) and then call the code as follows:

$ sudo ./socket_connect_mac_simple.d

Open another terminal and fire up nc as a server:

$ nc -4 -k -l 127.0.0.1 6472 > /dev/null

And in yet another terminal, connect to it:

$ while :; do nc -4 -s 192.168.0.19 127.0.0.1 6472 < /usr/bin/true; sleep 0.5; done

Your output should look something along the lines of this:

$ sudo ./socket_connect_mac_simple.d
PID    PROCNAME         FAMILY   S_ADDR:S_PORT         D_ADDR:D_PORT         LAT(us)  RESULT
45823  nc               2        192.168.0.19:0        127.0.0.1:6472        240      Success
45825  nc               2        192.168.0.19:0        127.0.0.1:6472        234      Success
45827  nc               2        192.168.0.19:0        127.0.0.1:6472        236      Success
45829  nc               2        192.168.0.19:0        127.0.0.1:6472        240      Success
45831  nc               2        192.168.0.19:0        127.0.0.1:6472        241      Success
45833  nc               2        192.168.0.19:0        127.0.0.1:6472        238      Success
45835  nc               2        192.168.0.19:0        127.0.0.1:6472        234      Success
45837  nc               2        192.168.0.19:0        127.0.0.1:6472        241      Success
45839  nc               2        192.168.0.19:0        127.0.0.1:6472        236      Success
45841  nc               2        192.168.0.19:0        127.0.0.1:6472        237      Success

Obviously, you will be replacing nc with your daemon listing on port 6472 and you will also have to call your bash script where I put the system("date") code fragment. Other than that, it should pretty much just work as you described it.

Make sure that you read the fine documentation about the destructive mode dtrace call system(): Destructive Mode Calls inside DTrace

Leave a Reply

Your email address will not be published. Required fields are marked *