/*
an implementation of the PPTP protocol
Copyright (C) 2000 Free Software Foundation
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
02110-1301, USA.
pptp.c ... client shell to launch call managers, data handlers, and
the pppd from the command line.
*/
#include <sys/types.h>
#include <sys/socket.h>
#if defined(__FreeBSD__)
#include <libutil.h>
#elif defined(__NetBSD__) || defined(__OpenBSD__)
#include <util.h>
#elif defined(__APPLE__)
#include <util.h>
#elif defined (__SVR4) && defined (__sun)
#else
#include <pty.h>
#endif
#ifdef USER_PPP
#include <fcntl.h>
#endif
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/un.h>
#include <netdb.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <syslog.h>
#include <unistd.h>
#include <signal.h>
#include <setjmp.h>
#include <errno.h>
#include <sys/wait.h>
#include <sys/param.h>
#if defined(__APPLE__)
#include "getopt.h"
#else
#include <getopt.h>
#endif
#include <limits.h>
#include "config.h"
#include "pptp_callmgr.h"
#include "pptp_gre.h"
#include "version.h"
#if defined(__linux__)
#include <sys/prctl.h>
#endif
#include "util.h"
#include "pptp_quirks.h"
#include "pqueue.h"
#include "pptp_options.h"
#include "pptp_compat.h"
#ifndef PPPD_BINARY
#define PPPD_BINARY "pppd"
#endif
int syncppp = 0;
int log_level = 1;
int disable_buffer = 0;
int test_type = 0;
int test_rate = 100;
int missing_window = MISSING_WINDOW;
struct in_addr get_ip_address(char *name);
int open_callmgr(struct in_addr inetaddr, char *phonenr, int argc,char **argv,char **envp, int pty_fd, int gre_fd);
void launch_callmgr(struct in_addr inetaddr, char *phonenr, int argc,char **argv,char **envp);
int get_call_id(int sock, pid_t gre, pid_t pppd,
u_int16_t *call_id, u_int16_t *peer_call_id);
void launch_pppd(char *ttydev, int argc, char **argv);
/*** print usage and exit *****************************************************/
void usage(char *progname)
{
fprintf(stderr,
"%s\n"
"Copyright (C) 2000 Free Software Foundation\n\n"
"This program comes with ABSOLUTELY NO WARRANTY; for details see source.\n"
"This is free software, and you are welcome to redistribute it under certain\n"
"conditions; see source for details.\n\n"
"Usage:\n"
" %s <hostname> [<pptp options>] [[--] <pppd options>]\n"
"\n"
"Or using pppd's pty option: \n"
" pppd pty \"%s <hostname> --nolaunchpppd <pptp options>\"\n"
"\n"
"Available pptp options:\n"
" --version Display version number and exit\n"
" --phone <number> Pass <number> to remote host as phone number\n"
" --nolaunchpppd Do not launch pppd, for use as a pppd pty\n"
" --quirks <quirk> Work around a buggy PPTP implementation\n"
" Currently recognised values are BEZEQ_ISRAEL only\n"
" --debug Run in foreground (for debugging with gdb)\n"
" --sync Enable Synchronous HDLC (pppd must use it too)\n"
" --timeout <secs> Time to wait for reordered packets (0.01 to 10 secs)\n"
" --nobuffer Disable packet buffering and reordering completely\n"
" --idle-wait Time to wait before sending echo request\n"
" --max-echo-wait Time to wait before giving up on lack of reply\n"
" --logstring <name> Use <name> instead of 'anon' in syslog messages\n"
" --localbind <addr> Bind to specified IP address instead of wildcard\n"
#ifdef SO_MARK
" --rtmark <n> Use specified policy routing mark for all packets\n"
#endif
" --nohostroute Do not add host route towards <hostname>\n"
" --loglevel <level> Sets the debugging level (0=low, 1=default, 2=high)\n"
" --test-type <type> Damage the packet stream by reordering\n"
" --test-rate <n> Do the test every n packets\n"
" --missing-window <n> Enable 'missing window' validation and set packet\n"
" polerance (300=default, 6000=recommended)\n",
version, progname, progname);
log("%s called with wrong arguments, program not started.", progname);
exit(1);
}
#if defined (__SVR4) && defined (__sun)
struct in_addr localbind = { .s_addr = INADDR_ANY };
#else
struct in_addr localbind = { INADDR_NONE };
#endif
int rtmark = 0;
int nohostroute = 0;
static int signaled = 0;
/*** do nothing signal handler ************************************************/
void do_nothing(int sig)
{
/* do nothing signal handler. Better than SIG_IGN. */
signaled = sig;
}
sigjmp_buf env;
/*** signal handler ***********************************************************/
void sighandler(int sig __attribute__ ((unused)))
{
siglongjmp(env, 1);
}
/*** report statistics signal handler (SIGUSR1) *******************************/
void sigstats(int sig __attribute__ ((unused)))
{
syslog(LOG_NOTICE, "GRE statistics:\n");
#define LOG(name,value) syslog(LOG_NOTICE, name "\n", stats .value)
LOG("rx accepted = %d", rx_accepted);
LOG("rx lost = %d", rx_lost);
LOG("rx under win = %d", rx_underwin);
LOG("rx over win = %d", rx_overwin);
LOG("rx buffered = %d", rx_buffered);
LOG("rx OS errors = %d", rx_errors);
LOG("rx truncated = %d", rx_truncated);
LOG("rx invalid = %d", rx_invalid);
LOG("rx acks = %d", rx_acks);
LOG("tx sent = %d", tx_sent);
LOG("tx failed = %d", tx_failed);
LOG("tx short = %d", tx_short);
LOG("tx acks = %d", tx_acks);
LOG("tx oversize = %d", tx_oversize);
LOG("round trip = %d usecs", rtt);
#undef LOG
}
/*** main *********************************************************************/
/* TODO: redesign to avoid longjmp/setjmp. Several variables here
have a volatile qualifier to silence warnings from gcc < 3.0.
Remove the volatile qualifiers if longjmp/setjmp are removed.
*/
int main(int argc, char **argv, char **envp)
{
struct in_addr inetaddr;
volatile int callmgr_sock = -1;
char ttydev[PATH_MAX];
char *pty_name;
int pty_fd, tty_fd, gre_fd, rc;
volatile pid_t parent_pid, child_pid;
u_int16_t call_id, peer_call_id;
char buf[128];
int pppdargc;
char **pppdargv;
char phonenrbuf[65]; /* maximum length of field plus one for the trailing
* '\0' */
char * volatile phonenr = NULL;
volatile int launchpppd = 1, debug = 0;
while(1){
/* structure with all recognised options for pptp */
static struct option long_options[] = {
{"phone", 1, 0, 0},
{"nolaunchpppd", 0, 0, 0},
{"quirks", 1, 0, 0},
{"debug", 0, 0, 0},
{"sync", 0, 0, 0},
{"timeout", 1, 0, 0},
{"logstring", 1, 0, 0},
{"localbind", 1, 0, 0},
{"loglevel", 1, 0, 0},
{"nobuffer", 0, 0, 0},
{"idle-wait", 1, 0, 0},
{"max-echo-wait", 1, 0, 0},
{"version", 0, 0, 0},
{"test-type", 1, 0, 0},
{"test-rate", 1, 0, 0},
{"rtmark", 1, 0, 0},
{"nohostroute", 0, 0, 0},
{"missing-window", 1, 0, 0},
{0, 0, 0, 0}
};
int option_index = 0;
int c;
c = getopt_long (argc, argv, "", long_options, &option_index);
if (c == -1) break; /* no more options */
switch (c) {
case 0:
if (option_index == 0) { /* --phone specified */
strncpy(phonenrbuf,optarg,sizeof(phonenrbuf));
phonenrbuf[sizeof(phonenrbuf) - 1] = '\0';
phonenr = phonenrbuf;
} else if (option_index == 1) {/* --nolaunchpppd specified */
launchpppd = 0;
} else if (option_index == 2) {/* --quirks specified */
if (set_quirk_index(find_quirk(optarg)))
usage(argv[0]);
} else if (option_index == 3) {/* --debug */
debug = 1;
} else if (option_index == 4) {/* --sync specified */
syncppp = 1;
} else if (option_index == 5) {/* --timeout */
float new_packet_timeout = atof(optarg);
if (new_packet_timeout < 0.0099 ||
new_packet_timeout > 10) {
fprintf(stderr, "Packet timeout %s (%f) out of range: "
"should be between 0.01 and 10 seconds\n",
optarg, new_packet_timeout);
log("Packet timeout %s (%f) out of range: should be"
"between 0.01 and 10 seconds", optarg,
new_packet_timeout);
exit(2);
} else {
packet_timeout_usecs = new_packet_timeout * 1000000;
}
} else if (option_index == 6) {/* --logstring */
log_string = strdup(optarg);
} else if (option_index == 7) {/* --localbind */
if (inet_pton(AF_INET, optarg, (void *) &localbind) < 1) {
fprintf(stderr, "Local bind address %s invalid\n",
optarg);
log("Local bind address %s invalid\n", optarg);
exit(2);
}
} else if (option_index == 8) { /* --loglevel */
log_level = atoi(optarg);
if (log_level < 0 || log_level > 2)
usage(argv[0]);
} else if (option_index == 9) { /* --nobuffer */
disable_buffer = 1;
} else if (option_index == 10) { /* --idle-wait */
int x = atoi(optarg);
if (x < 0) {
fprintf(stderr, "--idle-wait must not be negative\n");
log("--idle-wait must not be negative\n");
exit(2);
} else {
idle_wait = x;
}
} else if (option_index == 11) { /* --max-echo-wait */
int x = atoi(optarg);
if (x < 0) {
fprintf(stderr, "--max-echo-wait must not be negative\n");
log("--max-echo-wait must not be negative\n");
exit(2);
} else {
max_echo_wait = x;
}
fprintf(stderr, "--max-echo-wait ignored, not yet implemented\n");
} else if (option_index == 12) { /* --version */
fprintf(stdout, "%s\n", version);
exit(0);
} else if (option_index == 13) { /* --test-type */
test_type = atoi(optarg);
} else if (option_index == 14) { /* --test-rate */
test_rate = atoi(optarg);
} else if (option_index == 15) { /* --rtmark */
#ifdef SO_MARK
rtmark = atoi(optarg);
#else
fprintf(stderr, "--rtmark support was missing when "
"this binary was compiled.\n");
exit(2);
#endif
} else if (option_index == 16) { /* --nohostroute */
nohostroute = 1;
} else if (option_index == 17) { /* --missing window */
int x = atoi(optarg);
if (x <= 0) {
fprintf(stderr, "--missing-window must be integer "
"greater than zero\n");
log("--missing-window must be integer "
"greater than zero\n");
exit(2);
} else if (x < 300) {
fprintf(stderr, "--missing-window is set very low: "
"default=300, recommended=6000 - proceeding");
log("--missing-window is set very low: "
"default=300, recommended=6000 - proceeding\n");
} else {
fprintf(stderr, "--missing-window validation is active "
"and set to: %d\n", x);
log("--missing-window validation is active "
"and set to: %d\n", x);
missing_window = x;
}
}
break;
case '?': /* unrecognised option */
/* fall through */
default:
usage(argv[0]);
}
}
/* at least one argument is required */
if (argc <= optind)
usage(argv[0]);
/* Get IP address for the hostname in argv[1] */
inetaddr = get_ip_address(argv[optind]);
optind++;
/* Find the ppp options, extract phone number */
pppdargc = argc - optind;
pppdargv = argv + optind;
log("The synchronous pptp option is %sactivated\n", syncppp ? "" : "NOT ");
/* Now we have the peer address, bind the GRE socket early,
before starting pppd. This prevents the ICMP Unreachable bug
documented in <1026868263.2855.67.camel@jander> */
gre_fd = pptp_gre_bind(inetaddr);
if (gre_fd < 0) {
fatal("Cannot bind GRE socket, aborting.");
}
/* Find an open pty/tty pair. */
if(launchpppd){
rc = openpty (&pty_fd, &tty_fd, ttydev, NULL, NULL);
if (rc < 0) {
fatal("Could not find free pty.");
}
/* fork and wait. */
signal(SIGUSR1, do_nothing); /* don't die */
signal(SIGCHLD, do_nothing); /* don't ignore SIGCHLD */
parent_pid = getpid();
switch (child_pid = fork()) {
case -1:
fatal("Could not fork pppd process");
case 0: /* I'm the child! */
close (tty_fd);
signal(SIGUSR1, SIG_DFL);
child_pid = getpid();
break;
default: /* parent */
close (pty_fd);
/*
* There is still a very small race condition here. If a signal
* occurs after signaled is checked but before pause is called,
* things will hang.
*/
if (!signaled) {
pause(); /* wait for the signal */
}
if (signaled == SIGCHLD)
fatal("Child process died");
launch_pppd(ttydev, pppdargc, pppdargv); /* launch pppd */
perror("Error");
fatal("Could not launch pppd");
}
} else { /* ! launchpppd */
pty_fd = tty_fd = STDIN_FILENO;
/* close unused file descriptor, that is redirected to the pty */
close(STDOUT_FILENO);
child_pid = getpid();
parent_pid = 0; /* don't kill pppd */
}
do {
/*
* Open connection to call manager (Launch call manager if necessary.)
*/
callmgr_sock = open_callmgr(inetaddr, phonenr, argc, argv, envp,
pty_fd, gre_fd);
/* Exchange PIDs, get call ID */
} while (get_call_id(callmgr_sock, parent_pid, child_pid,
&call_id, &peer_call_id) < 0);
/* Send signal to wake up pppd task */
if (launchpppd) {
kill(parent_pid, SIGUSR1);
sleep(2);
/* become a daemon */
if (!debug && daemon(0, 0) != 0) {
perror("daemon");
}
} else {
/* re-open stderr as /dev/null to release it */
file2fd("/dev/null", "wb", STDERR_FILENO);
}
pty_name = ttyname(pty_fd);
snprintf(buf, sizeof(buf), "pptp: GRE-to-PPP gateway on %s",
pty_name ? pty_name : "(null)");
#ifdef PR_SET_NAME
rc = prctl(PR_SET_NAME, "pptpgw", 0, 0, 0);
if (rc != 0) perror("prctl");
#endif
if (sigsetjmp(env, 1)!= 0) goto shutdown;
signal(SIGINT, sighandler);
signal(SIGTERM, sighandler);
signal(SIGKILL, sighandler);
signal(SIGCHLD, sighandler);
signal(SIGUSR1, sigstats);
/* Do GRE copy until close. */
pptp_gre_copy(call_id, peer_call_id, pty_fd, gre_fd);
shutdown:
/* on close, kill all. */
if(launchpppd)
kill(parent_pid, SIGTERM);
close(pty_fd);
close(callmgr_sock);
exit(0);
}
/*** get the ipaddress coming from the command line ***************************/
struct in_addr get_ip_address(char *name)
{
int rc;
struct in_addr retval;
struct addrinfo hints, *ai;
memset(&hints, '\0', sizeof(hints));
hints.ai_family = AF_INET;
#if defined(__linux__) || defined(__FreeBSD__)
hints.ai_flags = AI_ADDRCONFIG; /* Unknown in OpenBSD. */
#endif
if ( (rc = getaddrinfo(name, NULL, &hints, &ai)) )
fatal("getaddrinfo(): %s", gai_strerror(rc));
if (ai->ai_addr->sa_family != AF_INET) /* Should never happen. */
fatal("Host '%s' possesses no IPv4 address", name);
memcpy(&retval.s_addr,
&(((struct sockaddr_in *) ai->ai_addr)->sin_addr.s_addr),
sizeof(retval.s_addr));
freeaddrinfo(ai);
return retval;
}
/*** start the call manager ***************************************************/
int open_callmgr(struct in_addr inetaddr, char *phonenr, int argc, char **argv,
char **envp, int pty_fd, int gre_fd)
{
/* Try to open unix domain socket to call manager. */
union {
struct sockaddr a;
struct sockaddr_un u;
} where;
const int NUM_TRIES = 3;
int i, fd;
pid_t pid;
int status;
/* Open socket */
if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) {
fatal("Could not create unix domain socket: %s", strerror(errno));
}
/* Make address */
callmgr_name_unixsock(&where.u, inetaddr, localbind);
for (i = 0; i < NUM_TRIES; i++) {
if (connect(fd, &where.a, sizeof(where)) < 0) {
/* couldn't connect. We'll have to launch this guy. */
unlink (where.u.sun_path); /* FIXME: potential race condition */
/* fork and launch call manager process */
switch (pid = fork()) {
case -1: /* failure */
fatal("fork() to launch call manager failed.");
case 0: /* child */
{
close (fd);
/* close the pty and gre in the call manager */
close(pty_fd);
close(gre_fd);
launch_callmgr(inetaddr, phonenr, argc, argv, envp);
}
default: /* parent */
waitpid(pid, &status, 0);
if (WEXITSTATUS(status) != 0)
fatal("Call manager exited with error %d", status);
break;
}
sleep(1);
}
else return fd;
}
close(fd);
fatal("Could not launch call manager after %d tries.", i);
return -1; /* make gcc happy */
}
/*** call the call manager main ***********************************************/
void launch_callmgr(struct in_addr inetaddr, char *phonenr, int argc __attribute__ ((unused)),
char**argv,char**envp)
{
char *my_argv[3] = { argv[0], inet_ntoa(inetaddr), phonenr };
char buf[128];
snprintf(buf, sizeof(buf), "pptp: call manager for %s", my_argv[1]);
#ifdef PR_SET_NAME
int rc;
rc = prctl(PR_SET_NAME, "pptpcm", 0, 0, 0);
if (rc != 0) perror("prctl");
#endif
exit(callmgr_main(3, my_argv, envp));
}
/*** exchange data with the call manager *************************************/
/* XXX need better error checking XXX */
int get_call_id(int sock, pid_t gre, pid_t pppd,
u_int16_t *call_id, u_int16_t *peer_call_id)
{
u_int16_t m_call_id, m_peer_call_id;
/* write pid's to socket */
/* don't bother with network byte order, because pid's are meaningless
* outside the local host.
*/
int rc;
rc = write(sock, &gre, sizeof(gre));
if (rc != sizeof(gre))
return -1;
rc = write(sock, &pppd, sizeof(pppd));
if (rc != sizeof(pppd))
return -1;
rc = read(sock, &m_call_id, sizeof(m_call_id));
if (rc != sizeof(m_call_id))
return -1;
rc = read(sock, &m_peer_call_id, sizeof(m_peer_call_id));
if (rc != sizeof(m_peer_call_id))
return -1;
/*
* XXX FIXME ... DO ERROR CHECKING & TIME-OUTS XXX
* (Rhialto: I am assuming for now that timeouts are not relevant
* here, because the read and write calls would return -1 (fail) when
* the peer goes away during the process. We know it is (or was)
* running because the connect() call succeeded.)
* (James: on the other hand, if the route to the peer goes away, we
* wouldn't get told by read() or write() for quite some time.)
*/
*call_id = m_call_id;
*peer_call_id = m_peer_call_id;
return 0;
}
/*** execvp pppd **************************************************************/
void launch_pppd(char *ttydev, int argc, char **argv)
{
char *new_argv[argc + 4];/* XXX if not using GCC, hard code a limit here. */
char str_pppd[] = PPPD_BINARY;
char str_direct[] __attribute__ ((unused)) = "-direct";
char str_38400[] = "38400";
int i = 0, j;
new_argv[i++] = str_pppd;
#ifdef USER_PPP
new_argv[i++] = str_direct;
/* ppp expects to have stdin connected to ttydev */
if ((j = open(ttydev, O_RDWR)) == -1)
fatal("Cannot open %s: %s", ttydev, strerror(errno));
if (dup2(j, 0) == -1)
fatal("dup2 failed: %s", strerror(errno));
close(j);
#else
new_argv[i++] = ttydev;
new_argv[i++] = str_38400;
#endif
for (j = 0; j < argc; j++)
new_argv[i++] = argv[j];
new_argv[i] = NULL;
execvp(new_argv[0], new_argv);
}