SoV.c - Viewing source

Download the source file

/*
**  SoV - Starcraft over VPN v0.1
**  -----------------------------
**  Written by Eric Doughty-Papassideris
**
**  Description
**  -----------
**  SoV allows to connect to a starcraft game
**  over networks that don't route broadcast
**  packets - such as OpenVPN (I hear version 2.0
**  fixed it though) or Hamachi on Mac OS X.
**
**  How to use it:
**  --------------
**  1. Start by connecting the VPN, and making sure you
**  have connectivity with the host. Use the opportunity
**  to figure out his IP address.
**
**  2. Get the host to create the game in starcraft and
**  wait for you to join. (Using UDP)
**
**  3. Run starcraft, and get to the game selection screen.
**
**  4. Run 'sudo path/sov en0 tun0 host' on the client.
**  In that command, you should replace:
**     path/ : by the path to the sov binary (its true!)
**     en0   : by an interface that is known to route
**               broadcast packets without surprises.
**               You should use your usual LAN interface.
**     tun0  : by the name of the interface your VPN
**               works on. Make sure its connected
**               starting.
**     host  : by the ip address of the game creator
**               (should be in tun0's subnet)
**
**  5. You should see the game in the list, and should be
**  able to join it.
**
**  6. Repeat on the computer of each player joining the
**  game.
**
**  7. hfgl
**
**  While its running it will show how packets it sees
**  going through and how many it re-injected.
**  Once you have joined the game, you can close sov
**  by hitting ctrl+c (but you don't have to. Are you
**  really only going to play a single game ? - tss.)
**
**  How it works:
**  -------------
**  When listing games, starcraft sends a broadcast
**  packet that servers respond to with game
**  details. SoV uses libpcap to see such messages,
**  and injects them over another interface (while
**  correcting the source address, and setting the
**  destination to the ip provided on the command
**  line). The server sees the packet, doesn't care
**  that its not a broadcast, and responds to the
**  client with a normal UDP packet.
**  The client sees the game, and the rest works
**  fine without broadcast.
**
**  How to build:
**  -------------
**  gcc -lpcap -o sov sov.c -Wall
**
**  Credits
**  -------
**    - sniffex.c sample by The Tcpdump Group
**    - B. Borde for his patience in testing this.
**
**  License
**  -------
**  IDC v2.0 ("I Dont Care")
**
**  Disclaimer
**  ----------
**  Carefull: I don't want to know about what
**    happened (or not) because of this code.
**    You are running it "as-is",
**    and it "is not my problem".
**
*/

#include <pcap.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <unistd.h>

#define BANNER "SoV - Starcraft over VPN - by EDP\n"
#define STARCRAFT_PORT 6111
#define PCAP_FILTER "udp port 6111"
#define SNAP_LEN 65535
#define SIZE_ETHERNET 14

/* Injection packet queue item */
typedef struct		buffer_s
{
  size_t		len;
  void			*ptr;
  struct buffer_s	*next;
}			buf_t;

/* Global variables */
typedef struct		globals_s
{
  int			nbclients;
  bpf_u_int32		*clients;
  bpf_u_int32		remote;

  /* Capture interface for good packets  */
  int			fd_pcap;
  pcap_t		*pcap_hnd;
  char			*pcap_dev;
  bpf_u_int32		pcap_mask;
  bpf_u_int32		pcap_net;
  struct bpf_program	pcap_bpfilter;

  /* Injection interface */
  int			fd_tun;
  char			*tun_dev;
  pcap_t		*tun_hnd;
  bpf_u_int32		tun_mask;
  bpf_u_int32		tun_net;

  /* Packet queue (yes, old school) */
  buf_t			*buf_head;
  buf_t			*buf_tail;

  /* Statistics */
  int			stats_cntcap;
  int			stats_cntsent;
  int			stats_cnterrorsent;
}			globals_t;

/* Ethernet, IP and UDP headers */
typedef struct	sniff_ethernet
{
        u_char	ether_dhost[6];
        u_char	ether_shost[6];
        u_short	ether_type;
}		ether_t;

typedef struct	sniff_ip {
        u_char  ip_vhl;                 /* version << 4 | header length >> 2 */
        u_char  ip_tos;                 /* type of service */
        u_short ip_len;                 /* total length */
        u_short ip_id;                  /* identification */
        u_short ip_off;                 /* fragment offset field */
        #define IP_RF 0x8000            /* reserved fragment flag */
        #define IP_DF 0x4000            /* dont fragment flag */
        #define IP_MF 0x2000            /* more fragments flag */
        #define IP_OFFMASK 0x1fff       /* mask for fragmenting bits */
        u_char  ip_ttl;                 /* time to live */
        u_char  ip_p;                   /* protocol */
        u_short ip_sum;                 /* checksum */
        struct  in_addr ip_src,ip_dst;  /* source and dest address */
}		ip_t;

#define IP_HL(ip)               (((ip)->ip_vhl) & 0x0f)
#define IP_V(ip)                (((ip)->ip_vhl) >> 4)

typedef struct	sniff_udp
{
  u_short	sport;
  u_short	dport;
  u_short	len;
  u_short	crc;
}		udp_t;

/* Raw packet building code */
unsigned short	csum (unsigned short *buf, int nwords)
{
  unsigned long	sum;
  for (sum = 0; nwords > 0; nwords--)
    sum += *buf++;
  sum = (sum >> 16) + (sum & 0xffff);
  sum += (sum >> 16);
  return ~sum;
}

void			buildrawudp(globals_t *g, u_char *data, size_t len, bpf_u_int32 source)
{
  u_char		*buf;
  ip_t			*ip;
  udp_t			*udp;
  struct sockaddr_in	sin;

  buf = (u_char *)malloc(20 + 8 + len);
  bzero((char *)buf, 20 + 8 + len);
  ip = (ip_t *)(buf);
  udp = (udp_t *)(buf + 20);

  // IP Layer
  ip->ip_vhl = 0x45;
  ip->ip_len = htons(20 + 8 + len);
  ip->ip_ttl = 64;
  ip->ip_id = htons(0x6a45); //<- Bogus non null value
  ip->ip_p = 0x11; //UDP
  ip->ip_dst.s_addr = g->remote;
  ip->ip_src.s_addr = source;
  ip->ip_sum = csum((unsigned short*)(buf), 20);

  // UDP Layer
  udp->sport = htons(STARCRAFT_PORT);
  udp->dport = htons(STARCRAFT_PORT);
  udp->len = htons(len + 8);
  memcpy(buf + 20 + 8, data, len);
  udp->crc = 0; //Really should calculate the correct packet. But this works for now.

  // Inject packet
  sin.sin_family = AF_INET;
  sin.sin_port = htons(STARCRAFT_PORT);
  sin.sin_addr.s_addr = g->remote;
  if ((pcap_inject(g->tun_hnd, buf, 20 + 8 + len)) < 0)
    {
      fprintf(stderr, "pcap_inject: Couldn't inject packet: %s.\n***Please check sov is running as root (sudo)\n",
	      pcap_geterr(g->tun_hnd));
      g->stats_cnterrorsent++;
    }
  else
    g->stats_cntsent++;
  free(buf);
}

/* pcap initialisation */
int		initpcap(globals_t *g)
{
  char		errbuf[PCAP_ERRBUF_SIZE];

  if (pcap_lookupnet(g->pcap_dev, &g->pcap_net, &g->pcap_mask, errbuf) == -1)
    {
      fprintf(stderr, "pcap_lookupnet: Couldn't get netmask for device: %s\n", errbuf);
      return 0;
    }
  g->pcap_hnd = pcap_open_live(g->pcap_dev, SNAP_LEN, 1, 100, errbuf);
  if (g->pcap_hnd == NULL)
    {
      fprintf(stderr, "pcap_open_live: Couldn't open device %s: %s\n", g->pcap_dev, errbuf);
      return 0;
    }
  if (pcap_datalink(g->pcap_hnd) != DLT_EN10MB)
    {
      fprintf(stderr, "pcap: %s is not an Ethernet device\n",  g->pcap_dev);
      return 0;
    }
  if (pcap_compile(g->pcap_hnd, &g->pcap_bpfilter, PCAP_FILTER, 0, g->pcap_net) == -1)
    {
      fprintf(stderr, "pcap_compile: Couldn't parse filter %s: %s\n",
	      "udp", pcap_geterr(g->pcap_hnd));
      return 0;
    }
  if (pcap_setfilter(g->pcap_hnd, &g->pcap_bpfilter) == -1)
    {
      fprintf(stderr, "pcap_setfilter: Couldn't install filter %s: %s\n",
	      "udp", pcap_geterr(g->pcap_hnd));
      return 0;
    }
  if (pcap_setnonblock(g->pcap_hnd, 1, errbuf) == -1)
    {
      fprintf(stderr, "pcap_setnonblock: Couldn't set device %s to non-blocking mode: %s\n", g->pcap_dev, errbuf);
      return 0;
    }
  if ((g->fd_pcap = pcap_get_selectable_fd(g->pcap_hnd)) == -1)
    {
      fprintf(stderr, "pcap_get_selectable_fd: Failed on %s: %s\n",
	      g->pcap_dev, pcap_geterr(g->pcap_hnd));
      return 0;
    }
  if (pcap_lookupnet(g->tun_dev, &g->tun_net, &g->tun_mask, errbuf) == -1)
    {
      fprintf(stderr, "pcap_lookupnet: Couldn't get netmask for device: %s\n", errbuf);
      fprintf(stderr, "  ** Please make sure you're already connected to the VPN!\n");
      return 0;
    }
  g->tun_hnd = pcap_open_live(g->tun_dev, SNAP_LEN, 1, 100, errbuf);
  if (g->tun_hnd == NULL)
    {
      fprintf(stderr, "pcap_open_live: Couldn't open tun device %s: %s\n", g->tun_dev, errbuf);
      return 0;
    }
  return 1;
}

/* Finalisation */
globals_t	*glob_toclean = 0;

void	cleanup(globals_t *g)
{
  if (g->fd_pcap > 0)
    close(g->fd_pcap);
  if (g->pcap_hnd)
    pcap_close(g->pcap_hnd);
  if (g->tun_hnd)
    pcap_close(g->tun_hnd);
  pcap_freecode(&g->pcap_bpfilter);
}

void	signal_handler(int signum)
{
  printf("\nCaught signal %i... cleaning up and exiting...\n", signum);
  pcap_breakloop(glob_toclean->pcap_hnd);
  cleanup(glob_toclean);
  exit(1);
}

void	setupsignals()
{
  struct sigaction new_action, old_action;

  new_action.sa_handler = signal_handler;
  sigemptyset (&new_action.sa_mask);
  new_action.sa_flags = 0;
  sigaction (SIGINT, NULL, &old_action);
  if (old_action.sa_handler != SIG_IGN)
    sigaction (SIGINT, &new_action, NULL);
  sigaction (SIGHUP, NULL, &old_action);
  if (old_action.sa_handler != SIG_IGN)
    sigaction (SIGHUP, &new_action, NULL);
  sigaction (SIGTERM, NULL, &old_action);
  if (old_action.sa_handler != SIG_IGN)
    sigaction (SIGTERM, &new_action, NULL);
}


/* Packet queue */
void		dequeuepacket(globals_t *g)
{
  buf_t		*cur;
  int		i;

  cur = g->buf_head;
  if (!cur) return;
  g->buf_head = cur->next;
  if (!cur->next)
    g->buf_tail = 0;
  buildrawudp(g, cur->ptr, cur->len, g->tun_net);
  for (i = 0; i < g->nbclients; i++)
    buildrawudp(g, cur->ptr, cur->len, g->clients[i]);
  free(cur);
}

void	enqueuepacket(globals_t *g, void *data, size_t len)
{
  buf_t	*cur;

  cur = (buf_t *)malloc(sizeof(buf_t) + len);
  cur->ptr = (void *)(cur + 1);
  memcpy(cur->ptr, data, len);
  cur->len = len;
  cur->next = 0;
  if (!g->buf_tail)
    {
      g->buf_head = cur;
      g->buf_tail = cur;
      return;
    }
  g->buf_tail->next = cur;
  g->buf_tail = cur;
}

/* libpcap callback */
void		got_packet(u_char *args, const struct pcap_pkthdr *header, const u_char *packet)
{
  globals_t	*g = (globals_t *)args;
  const ether_t	*ethernet;
  const ip_t	*ip;
  const udp_t	*udp;
  int		size_ip;

  // Double check packet is wanted
  g->stats_cntcap++;
  ethernet = (ether_t*)(packet);
  ip = (ip_t*)(packet + SIZE_ETHERNET);
  size_ip = IP_HL(ip)*4;
  if (size_ip < 20) return;
  if (ip->ip_p != IPPROTO_UDP) return;
  udp = (udp_t*)(packet + SIZE_ETHERNET + size_ip);
  if (ntohs(udp->dport) != STARCRAFT_PORT || ntohs(udp->sport) != STARCRAFT_PORT) return;
  enqueuepacket(g, (void *)(packet + SIZE_ETHERNET + size_ip + 8), ntohs(udp->len) - 8);
}

/* Printouts */
void			printstats(globals_t *g)
{
  struct timeval	ts;
  static long		last = 0;
  char			buf[255];
  int			len;

  gettimeofday(&ts, 0);
  if (ts.tv_sec == last) // Dont update statistics more than once per second
    return;
  last = ts.tv_sec;
  len = snprintf(buf, 255, "\rCaptured : %i   sent: %i   errors: %i    ",
		 g->stats_cntcap, g->stats_cntsent, g->stats_cnterrorsent);
  write(0, buf, strlen(buf));
}

void	printuse(char *prog)
{
  printf(BANNER "A relay tool that captures outgoing broadcast starcraft packets\n\
and injects them to a specific host.\n\n\
Use: %s capture_device inject_device game_creator_ip\n\
\tcapture_device:  the name of the interface to capture local starcraft packets from.\n\
\tinject_device:   the name of the interface to reinject packets into\n\
\tgame_creator_ip: the host to forward packets to\n", prog);
}

/* Main loop */
void			mainloop(globals_t *g)
{
  fd_set		readfds, writefds, errfds;
  int			maxfd;
  struct timeval	ts;

  while (1)
    {
      FD_ZERO(&readfds); FD_ZERO(&writefds); FD_ZERO(&errfds);
      FD_SET(g->fd_pcap, &readfds);
      FD_SET(g->fd_pcap, &errfds);
      maxfd = g->fd_pcap + 1;
      ts.tv_sec = 0;
      ts.tv_usec = g->buf_head ? 50 : 700;
      if (select(maxfd, &readfds, &writefds, &errfds, &ts) < 0)
	{
	  perror("select failed");
	  return;
	}
      pcap_dispatch(g->pcap_hnd, 1, got_packet, (u_char *)g);
      if (g->buf_head)
	dequeuepacket(g);
      printstats(g);
    }
}

bpf_u_int32		getip(struct hostent *host)
{
  bpf_u_int32		result;

  bcopy(host->h_addr, (char *)&result, sizeof(bpf_u_int32));
  return result;
}

/* Just a random utility function */
int			main(int ac, char**av)
{
  globals_t		globals;
  int			i;
  struct hostent	*host;

  bzero((char *)&globals, sizeof(globals));
  if (ac < 4)
    {
      printuse(*av);
      return 1;
    }
  globals.nbclients = ac - 4;
  globals.clients = malloc(sizeof(bpf_u_int32) * globals.nbclients);
  if ((host = gethostbyname(av[3])) == NULL)
    {
      perror("Error: SOV: Invalid game creator IP");
      printuse(*av);
      return 1;
    }
  else
    globals.remote = getip(host);
  printf(BANNER);
  for (i = 0; i < ac - 4; i++)
    if ((host = gethostbyname(av[4 + i])) == NULL)
      {
	perror("Error: SOV: Invalid additional client IP");
	return 1;
      }
    else
      {
	globals.clients[i] = getip(host);
	printf(" - Registered client '%s'\n", av[4 + i]);
      }
  globals.pcap_dev = av[1];
  globals.tun_dev = av[2];
  glob_toclean = &globals;

  if (initpcap(&globals))
    {
      setupsignals();
      mainloop(&globals);
      glob_toclean = NULL;
    }
  cleanup(&globals);
  return 0;
}