/*
 * Worldvisions Tunnel Vision Software:
 *   Copyright (C) 1998 Worldvisions Computer Technology, Inc.
 * 
 * Classes for establishing a network tunnel on top of any stream and
 * managing encrypted communications over the tunnel.  See wvtunnel.h.
 */
#include "wvtunnel.h"
#include "wviproute.h"
#include <net/ethernet.h>
#include <time.h>

static ether_header real_ethhdr;
#define ETHER_EXTRA 16

WvTunnelList WvTunnel::all_tunnels;


//////////////////////////// WvTunnelListener



WvTunnelListener::WvTunnelListener(const WvIPPortAddr &_listenport,
				   WvStreamList *_list, WvConf &_cfg)
		: WvTCPListener(_listenport), cfg(_cfg)
{
    list = _list;
    if (list)
	setcallback(accept_callback, this);
}


int WvTunnelListener::accept_callback(WvStream &, void *userdata)
{
    WvTunnelListener &s = *(WvTunnelListener *)userdata;
    s.list->append(new WvTunnel(s.accept(), true, s.cfg), true);
    return 0;
}



/////////////////////////// WvTunnel



WvTunnel::WvTunnel(WvStream *_tunnel, bool _server_mode, WvConf &_cfg)
		: tap(tapinit()),
		  log(WvString("Tunnel#%s", tunlnum), WvLog::Info),
		  cfg(_cfg), ifc(WvString("tap%s", tunlnum))
{
    all_tunnels.append(this, false);
    
    if (!cfg.get("Tunnel Vision", "RSA Private Key", NULL))
    {
	log("Generating RSA key...");
	WvRSAKey k(1024);

	cfg.set("Tunnel Vision", "RSA Private Key", k.private_str());
	cfg.set("Tunnel Vision", "RSA Public Key", k.public_str());
	cfg.flush();
	log("Done.\n");
    }
	
    append(tunnel = _tunnel, true);
    
    server_mode = _server_mode;
    trusted = vpn_started = false;
    inpkt_size = 0;
    etunnel = tunnel;
    last_receive = time(NULL);
    
    real_ethhdr.ether_dhost[0] = 0xFE;
    real_ethhdr.ether_dhost[1] = 0xFD;
    real_ethhdr.ether_shost[0] = 0xFE;
    real_ethhdr.ether_shost[1] = 0xFD;
    real_ethhdr.ether_type = htons(ETHERTYPE_IP);
    
    if (!tunnel->isok())
    {
	log(WvLog::Error, "tunnel: %s\n", tunnel->errstr());
	return;
    }
    
    if (!tap)
    {
	WvString s("No available ethertap (/dev/tap*) devices!\n");
	if (tunnel->select(0, false, true))
	    tunnel->print("ERROR %s", s);
	log(WvLog::Error, s);
	return;
    }
    
    if (!tap->isok())
    {
	log(WvLog::Error, "tap%s: %s\n", tunlnum, tap->errstr());
	return;
    }
    
    ifc.up(false);
    
    if (server_mode)
    {
	log(WvLog::Info,
	    "Tunnel server connected to %s.\n", *tunnel->src());
	tunnel->print("HELLO Welcome to Tunnel Vision (0.0)\n");
	setcallback(server_callback, this);
    }
    else
    {
	log(WvLog::Info, 
	    "Tunnel client connected to %s.\n", *tunnel->src());
	setcallback(client_callback, this);
	clientstate = Greeting;
    }
}


WvTunnel::~WvTunnel()
{
    all_tunnels.unlink(this);
    
    log("Closing tunnel.\n");
    if (geterr())
	log(WvLog::Error, "Error was: %s\n", errstr());
    
    if (etunnel && etunnel != tunnel)
	delete etunnel;
    if (tap)
	delete tap;
    ifc.up(false);
    
    if (added_gwroute != WvIPAddr())
    {
	WvInterface i(added_ifc);
	i.delroute(added_gwroute);
    }

    if (added_route != WvIPAddr())
    {
	WvInterface i(added_ifc);
	i.delroute(added_route);
    }
}


void WvTunnel::close()
{
    if (etunnel)
	delete etunnel;
    if (tap)
	delete tap;
    etunnel = NULL;
    tap = NULL;
}


WvFile *WvTunnel::tapinit()
{
    WvFile *tap = NULL;
    
    for (tunlnum = 0; tunlnum < MAX_VTUNNELS; tunlnum++)
    {
	tap = new WvFile(WvString("/dev/tap%s", tunlnum), O_RDWR|O_EXCL);
	if (tap->isok())
	    break;
	else
	{
	    delete tap;
	    tap = NULL;
	}
    }
    
    return tap;
}


int WvTunnel::server_callback(WvStream &, void *userdata)
{
    WvTunnel &s = *(WvTunnel *)userdata;
    WvStream *et = s.etunnel;
    char *line;
    
    while ((line = et->getline(0)))
    {
	s.last_receive = time(NULL);
	
	line = trim_string(line);
	if (strlen(line) < 1)
	    continue;

	if (strncmp(line, "X ", 2))
	    s.log(WvLog::Debug1, "Command: %s\n", line);
	
	if (!strncmp(line, "OK ", 3) || !strncmp(line, "!! ", 3))
	    continue;
	else if (!strncmp(line, "X ", 2))
	    s.log(WvLog::Debug,
		  "Received %s bytes of connection randomizer.\n",
		  strlen(line)-2);
	else if (!strcasecmp(line, "help"))
	    et->print("OK Known commands: rsa, xor, blowfish, "
			     "password, ihave, vpn, quit\n");
	else if (!strncasecmp(line, "xor ", 4))
	{
	    et->print("OK Switching to XOR %s encryption.\n",
			     atoi(line+4));
	    s.newcrypt(new WvXORStream(s.tunnel, atoi(line+4)));
	    et = s.etunnel;
	}
	else if (!strncasecmp(line, "rsa ", 4))
	{
	    WvRSAKey pubkey(line+4, false);
	    WvRSAKey prvkey(s.cfg.get("Tunnel Vision", "RSA Private Key", ""),
			    true);
	    s.remote_rsa = pubkey.public_str();
	    if (s.cfg.get("Tunnel Vision Keys", s.remote_rsa.str, false))
	    {
		et->print("!! I recognize your public key.\n");
		s.trusted = true;
	    }
	    et->print("OK %s\n", prvkey.public_str());
	    s.newcrypt(new WvRSAStream(s.tunnel, prvkey, pubkey));
	    et = s.etunnel;
	}
	else if (!strncasecmp(line, "blowfish ", 9))
	{
	    int bytes = atoi(line + 9) / 8;
	    unsigned char *keybuf;
	    
	    if (bytes < 4)
	    {
		et->print("ERROR Key needs at least 32 bits.\n");
	    }
	    else
	    {
		keybuf = new unsigned char[bytes];
		
		et->read(keybuf, bytes);
		et->print("OK Switching to %s-bit blowfish "
				 "encryption.\n", bytes * 8);
		s.newcrypt(new WvBlowfishStream(s.tunnel, keybuf, bytes));
		et = s.etunnel;
		
		delete keybuf;
	    }
	}
	else if (!strncasecmp(line, "password ", 9))
	{
	    char *pass = s.cfg.get("Tunnel Vision", "Magic Password", NULL);
	    if (pass && !strcmp(line+9, pass))
	    {
		s.trusted = true;
		et->print("OK I'll trust you from now on.\n");
		
		if (s.remote_rsa.str)
		{
		    s.cfg.set("Tunnel Vision Keys", s.remote_rsa.str, true);
		    s.cfg.flush();
		}
	    }
	    else
	    {
		s.trusted = false;
		et->print("ERROR Nope.\n");

		if (s.remote_rsa.str)
		{
		    s.cfg.set("Tunnel Vision Keys", s.remote_rsa.str, false);
		    s.cfg.flush();
		}
	    }
	}
	else if (!strncasecmp(line, "ihave ", 6))
	{
	    if (s.trusted)
	    {
		s.generate_ihave();
		s.process_ihave(line + 6);
	    }
	    else
	    {
		et->print("ERROR I don't trust you.\n");
	    }
	}
	else if (!strcasecmp(line, "vpn"))
	{
	    if (s.trusted)
	    {
		et->print("OK Entering raw mode!\n");
		s.log("Starting to exchange packets.\n");
		
		s.append(s.tap, false);
		s.ifc.up(true);
		
		s.vpn_started = true;
		s.setcallback(vpn_callback, userdata);
		return 0;
	    }
	    else
	    {
		et->print("ERROR I don't trust you.\n");
	    }
	}
	else if (!strcmp(line, "quit"))
	{
	    et->print("OK Goodbye.\n");
	    et->close();
	}
	else
	    et->print("ERROR Unknown command.\n");
    }
    
    return 0;
}


int WvTunnel::client_callback(WvStream &, void *userdata)
{
    WvTunnel &s = *(WvTunnel *)userdata;
    WvStream *et = s.etunnel;
    char *line;
    bool okay;

    while ((line = et->getline(0)) != NULL)
    {
	s.last_receive = time(NULL);
	
	line = trim_string(line);
	
	if (strncmp(line, "X ", 2))
	    s.log(WvLog::Debug1, "Response: %s\n", line);
	
	if (s.clientstate==Greeting)
	    okay = !strncmp(line, "HELLO", 5);
	else if (!strncmp(line, "OK", 2))
	    okay = true;
	else if (!strncmp(line, "ERROR", 5))
	    okay = false;
	else if ((s.clientstate==IHAVE1 || s.clientstate==IHAVE2)
		 && !strncasecmp(line, "ihave ", 6))
	{
	    s.process_ihave(line + 6);
	    continue;
	}
	else
	    continue; // random gunk... and the protocol has some of that!
	
	switch (s.clientstate)
	{
	case Greeting:
	    if (okay)
	    {
		s.clientstate = RSA;
		WvRSAKey prvkey(s.cfg.get("Tunnel Vision",
					  "RSA Private Key", ""), true);
		et->print("rsa %s\n", prvkey.public_str());
	    }
	    else
		s.tunnel->close();
	    break;
	    
	case RSA:
	    if (okay)
	    {
		// switch to RSA
		WvRSAKey pubkey(line+3, false);
		WvRSAKey prvkey(s.cfg.get("Tunnel Vision",
					  "RSA Private Key", ""), true);
		s.newcrypt(new WvRSAStream(s.tunnel, prvkey, pubkey));
		et = s.etunnel;

		// and ask for Blowfish
		s.clientstate = Blowfish;
		WvRandomStream().read(s.keybuf, sizeof(s.keybuf));
		et->print("blowfish %s\n", sizeof(s.keybuf) * 8);
		et->write(s.keybuf, sizeof(s.keybuf));
	    }
	    else
		et->close();
	    break;
	    
	case Blowfish:
	    if (!okay)
		s.tunnel->close();
	    else
	    {
		s.clientstate = IHAVE1;
		s.newcrypt(new WvBlowfishStream(s.tunnel,
						s.keybuf, sizeof(s.keybuf)));
		et = s.etunnel;
		s.generate_ihave();
	    }
	    break;
	    
	case IHAVE1:
	    if (!okay)     // not authenticated yet
	    {
		s.clientstate = Password;
		et->print("password %s\n",
		      s.cfg.get("Tunnel Vision", "Magic Password", "DUNNO"));
		break;
	    }
	    
	    /* otherwise fall through! */
	    
	case IHAVE2:
	    if (!okay)     // server hates us
		s.tunnel->close();
	    else
	    {
		s.clientstate = VPN;
		et->print("vpn\n");
	    }
	    break;
	    
	case VPN:
	    if (!okay)     // server hates us
		s.tunnel->close();
	    else
	    {
		s.log("Starting to exchange packets.\n");
		s.ifc.up(true);
		s.append(s.tap, false);
		s.vpn_started = true;
		s.setcallback(vpn_callback, userdata);
		return 0;		// negotiation done!
	    }
	    break;

	case Password:
	    if (!okay)    // wrong password
		s.tunnel->close();
	    else
	    {
		s.clientstate = IHAVE2;
		s.generate_ihave();
	    }
	    break;
	}
    }
    
    
    return 0;
}


int WvTunnel::vpn_callback(WvStream &, void *userdata)
{
    WvTunnel &s = *(WvTunnel *)userdata;
    WvStream *et = s.etunnel;
    char buf[WVCRYPTO_BUFSIZE + ETHER_EXTRA];
    size_t len;
    __u16 *outpkt_size;
    
    while (s.select(0))
    {
	if (et->select(0))
	{
	    s.last_receive = time(NULL);
	    
	    if (!s.inpkt_size)
	    {
		et->queuemin(2);
		
		// queuemin() call may mean that data is no longer ready
		if (et->select(0))
		{
		    len = et->read(&s.inpkt_size, sizeof(s.inpkt_size));
		    if (len)
		    {
			s.inpkt_size = ntohs(s.inpkt_size);
			
			if (s.inpkt_size < WVCRYPTO_BUFSIZE)
			    et->queuemin(s.inpkt_size);
			else
			    et->queuemin(WVCRYPTO_BUFSIZE);
		    }
		}
	    }
	    
	    if (s.inpkt_size && et->select(0))
	    {
		len = et->read(buf + ETHER_EXTRA, s.inpkt_size);
		if (len)
		{
		    s.inpkt_size = 0;
		    et->queuemin(0);
		    
		    memcpy(buf + ETHER_EXTRA - sizeof(ether_header),
			   &real_ethhdr, sizeof(ether_header));
		    if (s.tap->select(0, false, true))
			s.tap->write(buf, len + ETHER_EXTRA);
		    s.log(WvLog::Debug4,
			  "%s-byte packet: Remote -> Local.\n",
			  len);
		}
	    }
	}
	
	if (s.tap->select(0))
	{
	    len = s.tap->read(buf, sizeof(buf));
	    if (len > 0)
	    {
		len -= ETHER_EXTRA;
		outpkt_size = ((__u16 *)(buf + ETHER_EXTRA)) - 1;
		*outpkt_size = htons(len);
		if (et->select(0, false, true))
		    et->write(outpkt_size, sizeof(*outpkt_size) + len);

		s.log(WvLog::Debug4, "%s-byte packet: Local -> Remote.\n",
		      len);
	    }
	}
    }
    
    return 0;
}


void WvTunnel::newcrypt(WvStream *_etunnel)
{
    unsigned char buf[72];
    size_t count;
    
    if (etunnel && etunnel != tunnel)
    {
	unlink(etunnel);
	delete etunnel;
    }

    if (_etunnel)
    {
	etunnel = _etunnel;
	append(etunnel, false);
    }
    else
	etunnel = tunnel;
    
    /*
     * initialize the stream with some randomness, to lessen the probability
     * of "known plaintext" attacks.
     */
    WvRandomStream().read(buf, sizeof(buf));
    for (count = 0; count < sizeof(buf); count++)
	while (buf[count] < 32) buf[count] += 7;
    
    count = sizeof(buf) - 1;
    count -= buf[count] / 4;
    buf[count] = 0;

    etunnel->print("X %s\n", (char *)buf);
}
    
    
bool WvTunnel::isok() const
{
    time_t now = time(NULL);
    
    if (last_receive + 10*60 < now)  // remote side idle timeout
	return false;
    else
	return tap && tunnel && tap->isok() && tunnel->isok();
}


int WvTunnel::geterr() const
{
    if (tunlnum < 0)
	return -1;
    else if (tunnel && !tunnel->isok())
	return tunnel->geterr();
    else if (tap)
	return tap->geterr();
    else
	return 0;
}


const char *WvTunnel::errstr() const
{
    if (tunlnum < 0)
	return "No available ethertap (/dev/tap*) devices";
    else if (!tunnel->isok())
	return tunnel->errstr();
    else
	return tap->errstr();
}


const WvAddr *WvTunnel::src() const
{
    return tunnel ? tunnel->src() : NULL;
}


void WvTunnel::process_ihave(char *line)
{
    char *endptr, *maskptr;
    WvIPAddr *remote = (WvIPPortAddr *)tunnel->src();
    WvIPNet net;
    WvInterfaceDict id(1);
    
    ifc.up(true);
    
    // make sure that any new IHAVE entries do not conflict with the route
    // to our tunnel vision partner: add an explicit route to him through
    // the right interface.
    WvIPRouteList rl;
    if (remote)
    {
	WvIPRoute *whichroute = rl.find(*remote);
	if (whichroute)
	{
	    WvInterface i(whichroute->ifc);

	    if (whichroute->gateway == WvIPAddr())
	    {
		if (!i.addroute(*remote))
		{
		    added_ifc = whichroute->ifc;
		    added_route = *remote;
		}
	    }
	    else // gateway does exist
	    {
		if (!i.addroute(whichroute->gateway))
		{
		    added_ifc = whichroute->ifc;
		    added_route = whichroute->gateway;
		}
	    
		if (!i.addroute(*remote, whichroute->gateway))
		{
		    added_ifc = whichroute->ifc;
		    added_gwroute = *remote;
		}
	    }
	}
    }
    

    line = trim_string(line);
    while (line && line[0])
    {
	endptr = strchr(line, ' ');
	if (endptr)
	    *endptr++ = 0;
	
	maskptr = strchr(line, '/');
	if (maskptr)
	{
	    *maskptr++ = 0;
	    net = WvIPNet(WvIPAddr(line), WvIPAddr(maskptr));
	}
	else
	    net = WvIPNet(WvIPAddr(line));
	
	if ((net.bits() < 32 || net.base() != *remote)
	    && net != WvIPNet() && !id.on_local_net(net))
	{
	    ifc.addroute(net);
	    etunnel->print("!! Routing %s to you through the tunnel.\n", net);
	    remote_nets.append(new WvIPNet(net), true);
	}
		       
	line = endptr ? trim_string(endptr) : NULL;
    }
#if 0
    // FIXME!  Oh BOY, FIXME!!!!!
    // this appears to be caused by a kernel bug.  Downing the ethertap
    // interface will cause the kernel to stop answering for its local
    // ppp address unless we set it up as an IP alias... sometimes.
    etunnel->print("!! Fixing my own address...\n");
    WvInterface xx("ppp0"), aa("lo:dual");
    xx.rescan();
    aa.setipaddr(xx.ipaddr());
    aa.up(false);
    aa.setipaddr(xx.ipaddr());
    aa.up(true);
#endif
    etunnel->print("OK I've configured my routing table.\n");
}


void WvTunnel::generate_ihave()
{
    char *stored = cfg.get("Tunnel Vision", "Local Nets", NULL);
    WvIPAddr lastaddr;
    
    if (stored)
    {
	ifc.setipaddr(WvIPNet(stored, 32));
	etunnel->print("IHAVE %s\n", stored);
    }
    else
    {
	WvInterfaceDict id(1);
	WvInterfaceDict::Iter i(id);
	
	etunnel->write("IHAVE ", 6);
	
	for (i.rewind(); i.next(); )
	{
	    WvInterface &ii = *i.data();
	    
	    if (!ii.valid
		|| ii.hwaddr().encap() == WvEncap::Loopback
		|| ii.ipaddr() == WvIPNet(WvIPAddr())
		|| !ii.isup())
		  continue;
	    
	    etunnel->print("%s ", ii.ipaddr());
	    
	    if (ii.hwaddr().encap() == WvEncap::Ethernet)
		lastaddr = ii.ipaddr();
	}
	
	ifc.setipaddr(WvIPNet(lastaddr, 32));
	etunnel->print("\n");
    }
}
