/*********************************************************************** * * pppoe.c * * Implementation of user-space PPPoE redirector for Linux. * * Copyright (C) 2000-2015 by Roaring Penguin Software Inc. * Copyright (C) 2018-2023 Dianne Skoll * * This program may be distributed according to the terms of the GNU * General Public License, version 2 or (at your option) any later version. * * SPDX-License-Identifier: GPL-2.0-or-later * ***********************************************************************/ #define _GNU_SOURCE 1 #include #include #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_N_HDLC #ifndef N_HDLC #include #endif #endif #include "pppoe.h" /* Default interface if no -I option given */ #define DEFAULT_IF "eth0" /* Global variables -- options */ int optInactivityTimeout = 0; /* Inactivity timeout */ int optClampMSS = 0; /* Clamp MSS to this value */ int optSkipSession = 0; /* Perform discovery, print session info and exit */ int optFloodDiscovery = 0; /* Flood server with discovery requests. USED FOR STRESS-TESTING ONLY. DO NOT USE THE -F OPTION AGAINST A REAL ISP */ PPPoEConnection *Connection = NULL; /* Must be global -- used in signal handler */ /*********************************************************************** *%FUNCTION: sendSessionPacket *%ARGUMENTS: * conn -- PPPoE connection * packet -- the packet to send * len -- length of data to send *%RETURNS: * Nothing *%DESCRIPTION: * Transmits a session packet to the peer. ***********************************************************************/ void sendSessionPacket(PPPoEConnection *conn, PPPoEPacket *packet, int len) { packet->length = htons(len); if (optClampMSS) { clampMSS(packet, "outgoing", optClampMSS); } if (sendPacket(conn, conn->sessionSocket, packet, len + HDR_SIZE) < 0) { if (errno == ENOBUFS) { /* No buffer space is a transient error */ return; } exit(EXIT_FAILURE); } #ifdef DEBUGGING_ENABLED if (conn->debugFile) { dumpPacket(conn->debugFile, packet, "SENT"); fprintf(conn->debugFile, "\n"); fflush(conn->debugFile); } #endif } /********************************************************************** *%FUNCTION: sessionDiscoveryPacket *%ARGUMENTS: * conn -- PPPoE connection *%RETURNS: * Nothing *%DESCRIPTION: * We got a discovery packet during the session stage. This most likely * means a PADT. ***********************************************************************/ static void sessionDiscoveryPacket(PPPoEConnection *conn) { PPPoEPacket packet; int len; if (receivePacket(conn->discoverySocket, &packet, &len) < 0) { return; } /* Check length */ if (ntohs(packet.length) + HDR_SIZE > len) { syslog(LOG_ERR, "Bogus PPPoE length field (%u)", (unsigned int) ntohs(packet.length)); return; } /* Is it for our session? */ if (packet.session != conn->session) { /* Nope, ignore it */ return; } /* Is it for our Ethernet interface? */ if (memcmp(packet.ethHdr.h_dest, conn->myEth, ETH_ALEN)) { /* Nope, ignore it */ return; } /* Is it from our peer's Ethernet interface? */ if (memcmp(packet.ethHdr.h_source, conn->peerEth, ETH_ALEN)) { /* Nope, ignore it */ return; } if (packet.code != CODE_PADT) { /* Not PADT; ignore it */ return; } #ifdef DEBUGGING_ENABLED if (conn->debugFile) { dumpPacket(conn->debugFile, &packet, "RCVD"); fprintf(conn->debugFile, "\n"); fflush(conn->debugFile); } #endif syslog(LOG_INFO, "Session %d terminated -- received PADT from peer", (int) ntohs(packet.session)); parsePacket(&packet, parseLogErrs, NULL); sendPADT(conn, "Received PADT from peer"); exit(EXIT_SUCCESS); } /********************************************************************** *%FUNCTION: session *%ARGUMENTS: * conn -- PPPoE connection info *%RETURNS: * Nothing *%DESCRIPTION: * Handles the "session" phase of PPPoE ***********************************************************************/ void session(PPPoEConnection *conn) { fd_set readable; PPPoEPacket packet; struct timeval tv; struct timeval *tvp = NULL; int maxFD = 0; int r; /* Drop privileges */ dropPrivs(); /* Prepare for select() */ if (conn->sessionSocket > maxFD) maxFD = conn->sessionSocket; if (conn->discoverySocket > maxFD) maxFD = conn->discoverySocket; maxFD++; /* Fill in the constant fields of the packet to save time */ memcpy(packet.ethHdr.h_dest, conn->peerEth, ETH_ALEN); memcpy(packet.ethHdr.h_source, conn->myEth, ETH_ALEN); packet.ethHdr.h_proto = htons(Eth_PPPOE_Session); packet.vertype = PPPOE_VER_TYPE(1, 1); packet.code = CODE_SESS; packet.session = conn->session; initPPP(); for (;;) { if (optInactivityTimeout > 0) { tv.tv_sec = optInactivityTimeout; tv.tv_usec = 0; tvp = &tv; } FD_ZERO(&readable); FD_SET(0, &readable); /* ppp packets come from stdin */ if (conn->discoverySocket >= 0) { FD_SET(conn->discoverySocket, &readable); } FD_SET(conn->sessionSocket, &readable); while(1) { r = select(maxFD, &readable, NULL, NULL, tvp); if (r >= 0 || errno != EINTR) break; } if (r < 0) { fatalSys("select (session)"); } if (r == 0) { /* Inactivity timeout */ syslog(LOG_ERR, "Inactivity timeout... something wicked happened on session %d", (int) ntohs(conn->session)); sendPADT(conn, "RP-PPPoE: Inactivity timeout"); exit(EXIT_FAILURE); } /* Handle ready sockets */ if (FD_ISSET(0, &readable)) { if (conn->synchronous) { syncReadFromPPP(conn, &packet); } else { asyncReadFromPPP(conn, &packet); } } if (FD_ISSET(conn->sessionSocket, &readable)) { if (conn->synchronous) { syncReadFromEth(conn, conn->sessionSocket, optClampMSS); } else { asyncReadFromEth(conn, conn->sessionSocket, optClampMSS); } } if (conn->discoverySocket >= 0) { if (FD_ISSET(conn->discoverySocket, &readable)) { sessionDiscoveryPacket(conn); } } } } /*********************************************************************** *%FUNCTION: sigPADT *%ARGUMENTS: * src -- signal received *%RETURNS: * Nothing *%DESCRIPTION: * If an established session exists send PADT to terminate from session * from our end ***********************************************************************/ static void sigPADT(int src) { syslog(LOG_DEBUG,"Received signal %d on session %d.", (int)src, (int) ntohs(Connection->session)); sendPADTf(Connection, "RP-PPPoE: Client terminated by signal %d", src); exit(EXIT_SUCCESS); } /********************************************************************** *%FUNCTION: usage *%ARGUMENTS: * argv0 -- program name *%RETURNS: * Nothing *%DESCRIPTION: * Prints usage information and exits. ***********************************************************************/ void usage(char const *argv0) { fprintf(stderr, "Usage: %s [options]\n", argv0); fprintf(stderr, "Options:\n"); fprintf(stderr, " -I if_name -- Specify interface (default %s.)\n", DEFAULT_IF); #ifdef DEBUGGING_ENABLED fprintf(stderr, " -D filename -- Log debugging information in filename.\n"); #endif fprintf(stderr, " -T timeout -- Specify inactivity timeout in seconds.\n" " -t timeout -- Initial timeout for discovery packets in seconds\n" " -V -- Print version and exit.\n" " -A -- Print access concentrator names and exit.\n" " -S name -- Set desired service name.\n" " -C name -- Set desired access concentrator name.\n" " -U -- Use Host-Unique to allow multiple PPPoE sessions.\n" " -W value -- Use Host-Unique set to 'value' specifically.\n" " -s -- Use synchronous PPP encapsulation.\n" " -m MSS -- Clamp incoming and outgoing MSS options.\n" " -p pidfile -- Write process-ID to pidfile.\n" " -e sess:mac -- Skip discovery phase; use existing session.\n" " -n -- Do not open discovery socket.\n" " -k -- Kill a session with PADT (requires -e)\n" " -d -- Perform discovery, print session info and exit.\n" " -f disc:sess -- Set Ethernet frame types (hex).\n" " -h -- Print usage information.\n\n" "RP-PPPoE Version %s, Copyright (C) 2001-2018 Roaring Penguin Software Inc.\n" " %*s Copyright (C) 2018-2023 Dianne Skoll\n" "RP-PPPoE comes with ABSOLUTELY NO WARRANTY.\n" "This is free software, and you are welcome to redistribute it under the terms\n" "of the GNU General Public License, version 2 or any later version.\n" "https://dianne.skoll.ca/projects/rp-pppoe/\n", RP_VERSION, (int) strlen(RP_VERSION), ""); exit(EXIT_SUCCESS); } /********************************************************************** *%FUNCTION: main *%ARGUMENTS: * argc, argv -- count and values of command-line arguments *%RETURNS: * Nothing *%DESCRIPTION: * Main program ***********************************************************************/ int main(int argc, char *argv[]) { int opt; int n; unsigned int m[6]; /* MAC address in -e option */ unsigned int s; /* Temporary to hold session */ FILE *pidfile; unsigned int discoveryType, sessionType; char const *options; PPPoEConnection conn; #ifdef HAVE_N_HDLC int disc = N_HDLC; long flags; #endif if (getuid() != geteuid() || getgid() != getegid()) { IsSetID = 1; } /* Initialize connection info */ memset(&conn, 0, sizeof(conn)); conn.discoverySocket = -1; conn.sessionSocket = -1; conn.discoveryTimeout = PADI_TIMEOUT; /* For signal handler */ Connection = &conn; /* Initialize syslog */ openlog("pppoe", LOG_PID, LOG_DAEMON); #ifdef DEBUGGING_ENABLED options = "I:VAT:D:hS:C:UW:sm:np:e:kdf:F:t:"; #else options = "I:VAT:hS:C:UW:sm:np:e:kdf:F:t:"; #endif while((opt = getopt(argc, argv, options)) != -1) { switch(opt) { case 't': if (sscanf(optarg, "%d", &conn.discoveryTimeout) != 1) { fprintf(stderr, "Illegal argument to -t: Should be -t timeout\n"); exit(EXIT_FAILURE); } if (conn.discoveryTimeout < 1) { conn.discoveryTimeout = 1; } break; case 'F': if (sscanf(optarg, "%d", &optFloodDiscovery) != 1) { fprintf(stderr, "Illegal argument to -F: Should be -F numFloods\n"); exit(EXIT_FAILURE); } if (optFloodDiscovery < 1) optFloodDiscovery = 1; fprintf(stderr, "WARNING: DISCOVERY FLOOD IS MEANT FOR STRESS-TESTING\n" "A PPPOE SERVER WHICH YOU OWN. DO NOT USE IT AGAINST\n" "A REAL ISP. YOU HAVE 5 SECONDS TO ABORT.\n"); sleep(5); break; case 'f': if (sscanf(optarg, "%x:%x", &discoveryType, &sessionType) != 2) { fprintf(stderr, "Illegal argument to -f: Should be disc:sess in hex\n"); exit(EXIT_FAILURE); } Eth_PPPOE_Discovery = (uint16_t) discoveryType; Eth_PPPOE_Session = (uint16_t) sessionType; break; case 'd': optSkipSession = 1; break; case 'k': conn.killSession = 1; break; case 'n': /* Do not even open a discovery socket -- used when invoked by pppoe-server */ conn.noDiscoverySocket = 1; break; case 'e': /* Existing session: "sess:xx:yy:zz:aa:bb:cc" where "sess" is session-ID, and xx:yy:zz:aa:bb:cc is MAC-address of peer */ n = sscanf(optarg, "%u:%2x:%2x:%2x:%2x:%2x:%2x", &s, &m[0], &m[1], &m[2], &m[3], &m[4], &m[5]); if (n != 7) { fprintf(stderr, "Illegal argument to -e: Should be sess:xx:yy:zz:aa:bb:cc\n"); exit(EXIT_FAILURE); } /* Copy MAC address of peer */ for (n=0; n<6; n++) { conn.peerEth[n] = (unsigned char) m[n]; } /* Convert session */ conn.session = htons(s); /* Skip discovery phase! */ conn.skipDiscovery = 1; break; case 'p': switchToRealID(); pidfile = fopen(optarg, "w"); if (pidfile) { fprintf(pidfile, "%lu\n", (unsigned long) getpid()); fclose(pidfile); } switchToEffectiveID(); break; case 'S': SET_STRING(conn.serviceName, optarg); break; case 'C': SET_STRING(conn.acName, optarg); break; case 's': conn.synchronous = 1; break; case 'U': if (conn.hostUniq) { fprintf(stderr, "-U and -W are mutually-exclusive and may only be used once.\n"); exit(EXIT_FAILURE); } /* Allows for a 64-bit PID */ conn.hostUniq = malloc(17); if (!conn.hostUniq) { fprintf(stderr, "Out of memory.\n"); exit(EXIT_FAILURE); } sprintf(conn.hostUniq, "%lx", (unsigned long) getpid()); break; case 'W': if (conn.hostUniq) { fprintf(stderr, "-U and -W are mutually-exclusive and may only be used once.\n"); exit(EXIT_FAILURE); } if (strlen(optarg) > 64) { fprintf(stderr, "Maximum length of -W argument is 64 bytes.\n"); exit(EXIT_FAILURE); } SET_STRING(conn.hostUniq, optarg); break; #ifdef DEBUGGING_ENABLED case 'D': switchToRealID(); conn.debugFile = fopen(optarg, "w"); switchToEffectiveID(); if (!conn.debugFile) { fprintf(stderr, "Could not open %s: %s\n", optarg, strerror(errno)); exit(EXIT_FAILURE); } fprintf(conn.debugFile, "rp-pppoe-%s\n", RP_VERSION); fflush(conn.debugFile); break; #endif case 'T': optInactivityTimeout = (int) strtol(optarg, NULL, 10); if (optInactivityTimeout < 0) { optInactivityTimeout = 0; } break; case 'm': optClampMSS = (int) strtol(optarg, NULL, 10); if (optClampMSS < 536) { fprintf(stderr, "-m: %d is too low (min 536)\n", optClampMSS); exit(EXIT_FAILURE); } if (optClampMSS > 1452) { fprintf(stderr, "-m: %d is too high (max 1452)\n", optClampMSS); exit(EXIT_FAILURE); } break; case 'I': SET_STRING(conn.ifName, optarg); break; case 'V': printf("RP-PPPoE Version %s\n", RP_VERSION); exit(EXIT_SUCCESS); case 'A': conn.printACNames = 1; break; case 'h': usage(argv[0]); break; default: usage(argv[0]); } } /* Pick a default interface name */ if (!conn.ifName) { SET_STRING(conn.ifName, DEFAULT_IF); } if (!conn.printACNames) { #ifdef HAVE_N_HDLC if (conn.synchronous) { if (ioctl(0, TIOCSETD, &disc) < 0) { printErr("Unable to set line discipline to N_HDLC. Make sure your kernel supports the N_HDLC line discipline, or do not use the SYNCHRONOUS option. Quitting."); exit(EXIT_FAILURE); } else { syslog(LOG_INFO, "Changed pty line discipline to N_HDLC for synchronous mode"); } /* There is a bug in Linux's select which returns a descriptor * as readable if N_HDLC line discipline is on, even if * it isn't really readable. This return happens only when * select() times out. To avoid blocking forever in read(), * make descriptor 0 non-blocking */ flags = fcntl(0, F_GETFL); if (flags < 0) fatalSys("fcntl(F_GETFL)"); if (fcntl(0, F_SETFL, (long) flags | O_NONBLOCK) < 0) { fatalSys("fcntl(F_SETFL)"); } } #endif } if (optFloodDiscovery) { for (n=0; n < optFloodDiscovery; n++) { if (conn.printACNames) { fprintf(stderr, "Sending discovery flood %d\n", n+1); } conn.discoverySocket = openInterface(conn.ifName, Eth_PPPOE_Discovery, conn.myEth, NULL); discovery(&conn); conn.discoveryState = STATE_SENT_PADI; close(conn.discoverySocket); } exit(EXIT_SUCCESS); } /* Open session socket before discovery phase, to avoid losing session */ /* packets sent by peer just after PADS packet (noted on some Cisco */ /* server equipment). */ /* Opening this socket just before waitForPADS in the discovery() */ /* function would be more appropriate, but it would mess-up the code */ if (!optSkipSession) { conn.sessionSocket = openInterface(conn.ifName, Eth_PPPOE_Session, conn.myEth, NULL); } /* Skip discovery and don't open discovery socket? */ if (conn.skipDiscovery && conn.noDiscoverySocket) { conn.discoveryState = STATE_SESSION; } else { conn.discoverySocket = openInterface(conn.ifName, Eth_PPPOE_Discovery, conn.myEth, NULL); discovery(&conn); } if (optSkipSession) { printf("%u:%02x:%02x:%02x:%02x:%02x:%02x\n", ntohs(conn.session), conn.peerEth[0], conn.peerEth[1], conn.peerEth[2], conn.peerEth[3], conn.peerEth[4], conn.peerEth[5]); exit(EXIT_SUCCESS); } /* Set signal handlers: send PADT on HUP, TERM and INT */ signal(SIGTERM, sigPADT); signal(SIGINT, sigPADT); signal(SIGHUP, sigPADT); session(&conn); return 0; } /********************************************************************** *%FUNCTION: fatalSys *%ARGUMENTS: * str -- error message *%RETURNS: * Nothing *%DESCRIPTION: * Prints a message plus the errno value to stderr and syslog and exits. ***********************************************************************/ void fatalSys(char const *str) { printErr("%.256s: Session %d: %.256s", str, (int) ntohs(Connection->session), strerror(errno)); sendPADTf(Connection, "RP-PPPoE: System call error: %s", strerror(errno)); exit(EXIT_FAILURE); } /********************************************************************** *%FUNCTION: sysErr *%ARGUMENTS: * str -- error message *%RETURNS: * Nothing *%DESCRIPTION: * Prints a message plus the errno value to syslog. ***********************************************************************/ void sysErr(char const *str) { printErr("%.256s: %.256s", str, strerror(errno)); } /********************************************************************** *%FUNCTION: rp_fatal *%ARGUMENTS: * str -- error message *%RETURNS: * Nothing *%DESCRIPTION: * Prints a message to stderr and syslog and exits. ***********************************************************************/ void rp_fatal(char const *str) { printErr("%s", str); sendPADTf(Connection, "RP-PPPoE: Session %d: %.256s", (int) ntohs(Connection->session), str); exit(EXIT_FAILURE); } /********************************************************************** *%FUNCTION: asyncReadFromEth *%ARGUMENTS: * conn -- PPPoE connection info * sock -- Ethernet socket * clampMss -- if non-zero, do MSS-clamping *%RETURNS: * Nothing *%DESCRIPTION: * Reads a packet from the Ethernet interface and sends it to async PPP * device. ***********************************************************************/ void asyncReadFromEth(PPPoEConnection *conn, int sock, int clampMss) { PPPoEPacket packet; int len; int plen; int i; unsigned char pppBuf[4096]; unsigned char *ptr = pppBuf; unsigned char c; uint16_t fcs; unsigned char header[2] = {FRAME_ADDR, FRAME_CTRL}; unsigned char tail[2]; if (receivePacket(sock, &packet, &len) < 0) { return; } /* Check length */ if (ntohs(packet.length) + HDR_SIZE > len) { syslog(LOG_ERR, "Bogus PPPoE length field (%u)", (unsigned int) ntohs(packet.length)); return; } #ifdef DEBUGGING_ENABLED if (conn->debugFile) { dumpPacket(conn->debugFile, &packet, "RCVD"); fprintf(conn->debugFile, "\n"); fflush(conn->debugFile); } #endif /* Sanity check */ if (packet.code != CODE_SESS) { syslog(LOG_ERR, "Unexpected packet code %d", (int) packet.code); return; } if (PPPOE_VER(packet.vertype) != 1) { syslog(LOG_ERR, "Unexpected packet version %d", PPPOE_VER(packet.vertype)); return; } if (PPPOE_TYPE(packet.vertype) != 1) { syslog(LOG_ERR, "Unexpected packet type %d", PPPOE_TYPE(packet.vertype)); return; } if (memcmp(packet.ethHdr.h_dest, conn->myEth, ETH_ALEN)) { return; } if (memcmp(packet.ethHdr.h_source, conn->peerEth, ETH_ALEN)) { /* Not for us -- must be another session. This is not an error, so don't log anything. */ return; } if (packet.session != conn->session) { /* Not for us -- must be another session. This is not an error, so don't log anything. */ return; } plen = ntohs(packet.length); if (plen + HDR_SIZE > len) { syslog(LOG_ERR, "Bogus length field in session packet %d (%d)", (int) plen, (int) len); return; } /* Clamp MSS */ if (clampMss) { clampMSS(&packet, "incoming", clampMss); } /* Compute FCS */ fcs = pppFCS16(PPPINITFCS16, header, 2); fcs = pppFCS16(fcs, packet.payload, plen) ^ 0xffff; tail[0] = fcs & 0x00ff; tail[1] = (fcs >> 8) & 0x00ff; /* Build a buffer to send to PPP */ *ptr++ = FRAME_FLAG; *ptr++ = FRAME_ADDR; *ptr++ = FRAME_ESC; *ptr++ = FRAME_CTRL ^ FRAME_ENC; for (i=0; i len) { syslog(LOG_ERR, "Bogus PPPoE length field (%u)", (unsigned int) ntohs(packet.length)); return; } #ifdef DEBUGGING_ENABLED if (conn->debugFile) { dumpPacket(conn->debugFile, &packet, "RCVD"); fprintf(conn->debugFile, "\n"); fflush(conn->debugFile); } #endif /* Sanity check */ if (packet.code != CODE_SESS) { syslog(LOG_ERR, "Unexpected packet code %d", (int) packet.code); return; } if (PPPOE_VER(packet.vertype) != 1) { syslog(LOG_ERR, "Unexpected packet version %d", PPPOE_VER(packet.vertype)); return; } if (PPPOE_TYPE(packet.vertype) != 1) { syslog(LOG_ERR, "Unexpected packet type %d", PPPOE_TYPE(packet.vertype)); return; } if (memcmp(packet.ethHdr.h_dest, conn->myEth, ETH_ALEN)) { /* Not for us -- must be another session. This is not an error, so don't log anything. */ return; } if (memcmp(packet.ethHdr.h_source, conn->peerEth, ETH_ALEN)) { /* Not for us -- must be another session. This is not an error, so don't log anything. */ return; } if (packet.session != conn->session) { /* Not for us -- must be another session. This is not an error, so don't log anything. */ return; } plen = ntohs(packet.length); if (plen + HDR_SIZE > len) { syslog(LOG_ERR, "Bogus length field in session packet %d (%d)", (int) plen, (int) len); return; } /* Clamp MSS */ if (clampMss) { clampMSS(&packet, "incoming", clampMss); } /* Ship it out */ vec[0].iov_base = (void *) dummy; dummy[0] = FRAME_ADDR; dummy[1] = FRAME_CTRL; vec[0].iov_len = 2; vec[1].iov_base = (void *) packet.payload; vec[1].iov_len = plen; if (writev(1, vec, 2) < 0) { fatalSys("syncReadFromEth: write"); } }