/***********************************************************************
*
* 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 <syslog.h>
#include <getopt.h>

#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>

#include <sys/time.h>
#include <sys/uio.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <fcntl.h>

#include <signal.h>

#ifdef HAVE_N_HDLC
#ifndef N_HDLC
#include <pty.h>
#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<plen; i++) {
	c = packet.payload[i];
	if (c == FRAME_FLAG || c == FRAME_ADDR || c == FRAME_ESC || c < 0x20) {
	    *ptr++ = FRAME_ESC;
	    *ptr++ = c ^ FRAME_ENC;
	} else {
	    *ptr++ = c;
	}
    }
    for (i=0; i<2; i++) {
	c = tail[i];
	if (c == FRAME_FLAG || c == FRAME_ADDR || c == FRAME_ESC || c < 0x20) {
	    *ptr++ = FRAME_ESC;
	    *ptr++ = c ^ FRAME_ENC;
	} else {
	    *ptr++ = c;
	}
    }
    *ptr++ = FRAME_FLAG;

    /* Ship it out */
    if (write(1, pppBuf, (ptr-pppBuf)) < 0) {
	fatalSys("asyncReadFromEth: write");
    }
}

/**********************************************************************
*%FUNCTION: syncReadFromEth
*%ARGUMENTS:
* conn -- PPPoE connection info
* sock -- Ethernet socket
* clampMss -- if true, clamp MSS.
*%RETURNS:
* Nothing
*%DESCRIPTION:
* Reads a packet from the Ethernet interface and sends it to sync PPP
* device.
***********************************************************************/
void
syncReadFromEth(PPPoEConnection *conn, int sock, int clampMss)
{
    PPPoEPacket packet;
    int len;
    int plen;
    struct iovec vec[2];
    unsigned char dummy[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)) {
	/* 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");
    }
}
