options.c revision 7c478bd95313f5f23a4c958a745db2134aa03244
/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the
* Common Development and Distribution License, Version 1.0 only
* (the "License"). You may not use this file except in compliance
* with the License.
*
* You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
* or http://www.opensolaris.org/os/licensing.
* See the License for the specific language governing permissions
* and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at usr/src/OPENSOLARIS.LICENSE.
* If applicable, add the following below this CDDL HEADER, with the
* fields enclosed by brackets "[]" replaced with your own identifying
* information: Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*/
/*
* PPPoE Server-mode daemon option parsing.
*
* Copyright 2000-2002 Sun Microsystems, Inc. All rights reserved.
* Use is subject to license terms.
*/
#pragma ident "%Z%%M% %I% %E% SMI"
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>
#include <ctype.h>
#include <string.h>
#include <sys/types.h>
#include <fcntl.h>
#include <pwd.h>
#include <grp.h>
#include <errno.h>
#include <netdb.h>
#include <stropts.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <net/if.h>
#include <netinet/in.h>
#include <netinet/if_ether.h>
#include <net/sppptun.h>
#include "common.h"
#include "logging.h"
#define MAX_KEYWORD 4096 /* Maximum token length */
#define MAX_NEST 32 /* Maximum ${$sub} nesting */
#define MAXARGS 256 /* Maximum number of pppd arguments */
/*
* Client filter entry. These are linked in *reverse* order so that
* the DAG created by file inclusion nesting works as expected. Since
* the administrator who wrote the configuration expects "first
* match," this means that tests against the filter list must actually
* use "last match."
*/
struct filter_entry {
struct filter_entry *fe_prev; /* Previous filter in list */
struct ether_addr fe_mac; /* MAC address */
struct ether_addr fe_mask; /* Mask for above address test */
uchar_t fe_isexcept; /* invert sense; exclude matching clients */
uchar_t fe_prevcopy; /* fe_prev points to copied list */
uchar_t fe_unused[2]; /* padding */
};
/*
* Note: I would like to make the strings and filters here const, but
* I can't because they have to be passed to free() during parsing. I
* could work around this with offsetof() or data copies, but it's not
* worth the effort.
*/
struct service_entry {
const char *se_name; /* Name of service */
struct filter_entry *se_flist; /* Pointer to list of client filters */
uint_t se_flags; /* SEF_* flags (below) */
int se_debug; /* Debug level (0=nodebug) */
char *se_server; /* Server (AC) name */
char *se_pppd; /* Options for pppd */
char *se_path; /* Path to pppd executable */
char *se_extra; /* Extra options */
char *se_log; /* Log file */
uid_t se_uid; /* User ID */
gid_t se_gid; /* Group ID */
};
#define SEF_WILD 0x00000001 /* Offer in wildcard reply */
#define SEF_NOWILD 0x00000002 /* Don't offer in wildcard */
#define SEF_CFLIST 0x00000004 /* se_flist copied from global */
#define SEF_CSERVER 0x00000008 /* se_server copied from global */
#define SEF_CPPPD 0x00000010 /* se_pppd copied from global */
#define SEF_CPATH 0x00000020 /* se_path copied from global */
#define SEF_CEXTRA 0x00000040 /* se_extra copied from global */
#define SEF_CLOG 0x00000080 /* se_log copied from global */
#define SEF_UIDSET 0x00000100 /* se_uid has been set */
#define SEF_GIDSET 0x00000200 /* se_gid has been set */
#define SEF_DEBUGCLR 0x00000400 /* do not add se_debug from global */
#define SEF_CDEV 0x00000800 /* copied devs (parse only) */
/*
* One of these is allocated per lower-level stream (device) that is
* referenced by the configuration files. The queries are received
* per device, and this structure allows us to find all of the
* services that correspond to that device.
*/
struct device_entry {
const char *de_name;
const struct service_entry **de_services;
int de_nservices;
};
/*
* This is the parsed configuration. While a new configuration is
* being read, this is kept around until the new configuration is
* ready, and then it is discarded in one operation. It has an array
* of device entries (as above) -- one per referenced lower stream --
* and a pointer to the allocated parser information. The latter is
* kept around because we reuse pointers rather than reallocating and
* copying the data. There are thus multiple aliases to the dynamic
* data, and the "owner" (for purposes of freeing the storage) is
* considered to be this 'junk' list.
*/
struct option_state {
const struct device_entry *os_devices;
int os_ndevices;
struct per_file *os_pfjunk; /* Kept for deallocation */
char **os_evjunk; /* ditto */
};
/*
* This is the root pointer to the current parsed options.
* This cannot be const because it's passed to free() when reparsing
* options.
*/
static struct option_state *cur_options;
/* Global settings for module-wide options. */
static struct service_entry glob_svc;
/*
* *******************************************************************
* Data structures generated during parsing.
*/
/* List of device names attached to one service */
struct device_list {
struct device_list *dl_next;
const char *dl_name; /* Name of one device */
};
/* Entry for a single defined service. */
struct service_list {
struct service_entry sl_entry; /* Parsed service data */
struct service_list *sl_next; /* Next service entry */
struct parse_state *sl_parse; /* Back pointer to state */
struct device_list *sl_dev; /* List of devices */
int sl_serial; /* Serial number (conflict resolve) */
};
#define SESERIAL(x) ((struct service_list *)&(x))->sl_serial
#define ISGLOBAL(x) ((x) == &(x)->sl_parse->ps_cfile->pf_global)
/*
* Structure allocated for each file opened. File nesting is chained
* in reverse order so that global option scoping works as expected.
*/
struct per_file {
struct per_file *pf_prev; /* Back chain */
struct service_list pf_global; /* Global (default) service context */
struct service_list *pf_svc; /* List of services */
struct service_list *pf_svc_last;
FILE *pf_input; /* File for input */
const char *pf_name; /* File name */
int pf_nsvc; /* Count of services */
};
/* State of parser */
enum key_state {
ksDefault, ksService, ksDevice, ksClient, ksClientE, ksServer,
ksPppd, ksFile, ksPath, ksExtra, ksLog, ksUser, ksGroup
};
/*
* Global parser state. There is one of these structures, and it
* exists only while actively parsing configuration files.
*/
struct parse_state {
enum key_state ps_state; /* Parser state */
int ps_serial; /* Service serial number */
struct per_file *ps_files; /* Parsed files */
struct per_file *ps_cfile; /* Current file */
struct service_list *ps_csvc; /* Current service */
struct device_list *ps_star; /* Wildcard device */
int ps_flags; /* PSF_* below */
char **ps_evlist; /* allocated environment variables */
int ps_evsize; /* max length; for realloc */
};
#define PSF_PERDEV 0x0001 /* In a per-device file */
#define PSF_SETLEVEL 0x0002 /* Set log level along the way */
/* Should be in a library somewhere. */
static char *
strsave(const char *str)
{
char *newstr;
if (str == NULL)
return (NULL);
newstr = (char *)malloc(strlen(str) + 1);
if (newstr != NULL)
(void) strcpy(newstr, str);
return (newstr);
}
/*
* Stop defining current service and revert to global definition.
* This resolves any implicit references to global options by copying
* ("inheriting") from the current global state.
*/
static void
close_service(struct service_list *slp)
{
struct parse_state *psp;
struct per_file *cfile;
struct service_entry *sep;
struct service_entry *sedefp;
struct filter_entry *fep;
assert(slp != NULL);
psp = slp->sl_parse;
cfile = psp->ps_cfile;
/* If no current file, then nothing to close. */
if (cfile == NULL)
return;
sep = &slp->sl_entry;
/*
* Fix up filter pointers to make DAG. First, locate
* the end of the filter list.
*/
if (sep->se_flags & SEF_CFLIST) {
sep->se_flist = fep = NULL;
} else {
for (fep = sep->se_flist; fep != NULL; fep = fep->fe_prev)
if (fep->fe_prev == NULL || fep->fe_prevcopy) {
fep->fe_prev = NULL;
break;
}
}
if (slp == &cfile->pf_global) {
/*
* If we're in a global context, then we're about to
* open a new service, so it's time to fix up the
* filter list so that it's usable as a reference.
* Loop through files from which we were included, and
* link up filters. Note: closure may occur more than
* once here.
*/
/* We don't inherit from ourselves. */
cfile = cfile->pf_prev;
while (cfile != NULL) {
if (fep == NULL) {
sep->se_flist = fep =
cfile->pf_global.sl_entry.se_flist;
sep->se_flags |= SEF_CFLIST;
} else if (fep->fe_prev == NULL) {
fep->fe_prev =
cfile->pf_global.sl_entry.se_flist;
fep->fe_prevcopy = 1;
}
cfile = cfile->pf_prev;
}
} else {
/*
* Loop through default options in current and all
* enclosing include files. Inherit options.
*/
logdbg("service %s ends", slp->sl_entry.se_name);
while (cfile != NULL) {
/* Inherit from global service options. */
if (slp->sl_dev == NULL) {
slp->sl_dev = cfile->pf_global.sl_dev;
sep->se_flags |= SEF_CDEV;
}
sedefp = &cfile->pf_global.sl_entry;
if (fep == NULL) {
sep->se_flist = fep = sedefp->se_flist;
sep->se_flags |= SEF_CFLIST;
} else if (fep->fe_prev == NULL) {
fep->fe_prev = sedefp->se_flist;
fep->fe_prevcopy = 1;
}
if (sep->se_server == NULL) {
sep->se_server = sedefp->se_server;
sep->se_flags |= SEF_CSERVER;
}
if (sep->se_pppd == NULL) {
sep->se_pppd = sedefp->se_pppd;
sep->se_flags |= SEF_CPPPD;
}
if (sep->se_path == NULL) {
sep->se_path = sedefp->se_path;
sep->se_flags |= SEF_CPATH;
}
if (sep->se_extra == NULL) {
sep->se_extra = sedefp->se_extra;
sep->se_flags |= SEF_CEXTRA;
}
if (sep->se_log == NULL) {
sep->se_log = sedefp->se_log;
sep->se_flags |= SEF_CLOG;
}
if (!(sep->se_flags & SEF_UIDSET) &&
(sedefp->se_flags & SEF_UIDSET)) {
sep->se_uid = sedefp->se_uid;
sep->se_flags |= SEF_UIDSET;
}
if (!(sep->se_flags & SEF_GIDSET) &&
(sedefp->se_flags & SEF_GIDSET)) {
sep->se_gid = sedefp->se_gid;
sep->se_flags |= SEF_GIDSET;
}
if (!(sep->se_flags & (SEF_WILD|SEF_NOWILD)))
sep->se_flags |= sedefp->se_flags &
(SEF_WILD|SEF_NOWILD);
if (!(sep->se_flags & SEF_DEBUGCLR)) {
sep->se_debug += sedefp->se_debug;
sep->se_flags |= sedefp->se_flags &
SEF_DEBUGCLR;
}
cfile = cfile->pf_prev;
}
}
/* Revert to global definitions. */
psp->ps_csvc = &psp->ps_cfile->pf_global;
}
/* Discard a dynamic device list */
static void
free_device_list(struct device_list *dlp)
{
struct device_list *dln;
while (dlp != NULL) {
dln = dlp->dl_next;
free(dlp);
dlp = dln;
}
}
/*
* Handle "service <name>" -- finish up previous service definition
* (if any) by copying from global state where necessary, and start
* defining new service.
*/
static int
set_service(struct service_list *slp, const char *str)
{
struct parse_state *psp;
struct per_file *cfile;
/* Finish current service */
close_service(slp);
/* Start new service */
psp = slp->sl_parse;
slp = (struct service_list *)calloc(sizeof (*slp) + strlen(str) + 1,
1);
if (slp == NULL) {
logerr("no memory for service \"%s\"", str);
return (-1);
}
/* Add to end of list */
cfile = psp->ps_cfile;
if (cfile->pf_svc_last == NULL)
cfile->pf_svc = slp;
else
cfile->pf_svc_last->sl_next = slp;
cfile->pf_svc_last = slp;
cfile->pf_nsvc++;
/* Fill in initial service entry */
slp->sl_entry.se_name = (const char *)(slp+1);
(void) strcpy((char *)(slp+1), str);
logdbg("service %s begins", slp->sl_entry.se_name);
slp->sl_serial = psp->ps_serial++;
slp->sl_parse = psp;
/* This is now the current service that we're defining. */
psp->ps_csvc = slp;
return (0);
}
/*
* Handle both "wildcard" and "nowildcard" options.
*/
static int
set_wildcard(struct service_list *slp, const char *str)
{
/* Allow global context to switch back and forth without error. */
if (!ISGLOBAL(slp) &&
(slp->sl_entry.se_flags & (SEF_WILD|SEF_NOWILD))) {
logdbg("%s: extra \"%s\" ignored",
slp->sl_parse->ps_cfile->pf_name, str);
return (0);
}
slp->sl_entry.se_flags =
(slp->sl_entry.se_flags & ~(SEF_WILD|SEF_NOWILD)) |
(*str == 'n' ? SEF_NOWILD : SEF_WILD);
return (0);
}
/*
* Handle "debug" option.
*/
/*ARGSUSED*/
static int
set_debug(struct service_list *slp, const char *str)
{
slp->sl_entry.se_debug++;
if (ISGLOBAL(slp) && (slp->sl_parse->ps_flags & PSF_SETLEVEL)) {
log_level = slp->sl_entry.se_debug;
}
return (0);
}
/*
* Handle "nodebug" option.
*/
/*ARGSUSED*/
static int
set_nodebug(struct service_list *slp, const char *str)
{
slp->sl_entry.se_flags |= SEF_DEBUGCLR;
slp->sl_entry.se_debug = 0;
if (ISGLOBAL(slp) && (slp->sl_parse->ps_flags & PSF_SETLEVEL)) {
log_level = slp->sl_entry.se_debug;
}
return (0);
}
/*
* Handle all plain string options; "server", "pppd", "path", "extra",
* and "log".
*/
static int
set_string(struct service_list *slp, const char *str)
{
char **cpp;
assert(!(slp->sl_entry.se_flags &
(SEF_CSERVER|SEF_CPPPD|SEF_CPATH|SEF_CEXTRA|SEF_CLOG)));
switch (slp->sl_parse->ps_state) {
case ksServer:
cpp = &slp->sl_entry.se_server;
break;
case ksPppd:
cpp = &slp->sl_entry.se_pppd;
break;
case ksPath:
cpp = &slp->sl_entry.se_path;
break;
case ksExtra:
cpp = &slp->sl_entry.se_extra;
break;
case ksLog:
cpp = &slp->sl_entry.se_log;
break;
default:
assert(0);
return (-1);
}
if (*cpp != NULL)
free(*cpp);
*cpp = strsave(str);
return (0);
}
/*
* Handle "file <name>" option. Close out current service (if any)
* and begin parsing from new file.
*/
static int
set_file(struct service_list *slp, const char *str)
{
FILE *fp;
struct per_file *pfp;
struct parse_state *psp;
close_service(slp);
if ((fp = fopen(str, "r")) == NULL) {
logwarn("%s: %s: %s", slp->sl_parse->ps_cfile->pf_name, str,
mystrerror(errno));
return (-1);
}
pfp = (struct per_file *)calloc(sizeof (*pfp) + strlen(str) + 1, 1);
if (pfp == NULL) {
logerr("no memory for parsing file %s", str);
(void) fclose(fp);
return (-1);
}
logdbg("config file %s open", str);
/* Fill in new file structure. */
pfp->pf_name = (const char *)(pfp+1);
(void) strcpy((char *)(pfp+1), str);
pfp->pf_input = fp;
psp = slp->sl_parse;
pfp->pf_prev = psp->ps_cfile;
psp->ps_cfile = pfp;
/* Start off in global context for this file. */
psp->ps_csvc = &pfp->pf_global;
pfp->pf_global.sl_parse = psp;
pfp->pf_global.sl_entry.se_name = "<global>";
return (0);
}
/*
* Handle "device <list>" option.
*/
static int
set_device(struct service_list *slp, const char *str)
{
struct parse_state *psp = slp->sl_parse;
struct device_list *dlp;
struct device_list *dln;
struct device_list **dlpp;
const char *cp;
int len;
/* Can't use this option in the per-device files. */
if (psp->ps_flags & PSF_PERDEV) {
logerr("\"device %s\" ignored in %s", str,
psp->ps_cfile->pf_name);
return (0);
}
if (strcmp(str, "*") == 0 || strcmp(str, "all") == 0) {
if (!(slp->sl_entry.se_flags & SEF_CDEV))
free_device_list(slp->sl_dev);
slp->sl_dev = psp->ps_star;
slp->sl_entry.se_flags |= SEF_CDEV;
} else {
dlpp = &dlp;
for (;;) {
while (isspace(*str) || *str == ',')
str++;
if (*str == '\0')
break;
cp = str;
while (*str != '\0' && !isspace(*str) && *str != ',')
str++;
len = str - cp;
if ((len == 1 && *cp == '*') ||
(len == 3 && strncmp(cp, "all", 3) == 0)) {
logerr("%s: cannot use %.*s in device list",
psp->ps_cfile->pf_name, len, cp);
continue;
}
dln = (struct device_list *)malloc(sizeof (*dln) +
len + 1);
if (dln == NULL) {
logerr("no memory for device name");
break;
}
dln->dl_name = (const char *)(dln + 1);
/* Cannot use strcpy because cp isn't terminated. */
(void) memcpy(dln + 1, cp, len);
((char *)(dln + 1))[len] = '\0';
logdbg("%s: device %s", psp->ps_cfile->pf_name,
dln->dl_name);
*dlpp = dln;
dlpp = &dln->dl_next;
}
*dlpp = NULL;
dlpp = &slp->sl_dev;
if (!(slp->sl_entry.se_flags & SEF_CDEV))
while (*dlpp != NULL)
dlpp = &(*dlpp)->dl_next;
*dlpp = dlp;
slp->sl_entry.se_flags &= ~SEF_CDEV;
}
return (0);
}
/*
* Handle <list> portion of "client [except] <list>" option. Attach
* to list of filters in reverse order.
*/
static int
set_client(struct service_list *slp, const char *str)
{
struct parse_state *psp = slp->sl_parse;
struct filter_entry *fep;
struct filter_entry *fen;
const char *cp;
int len;
char hbuf[MAXHOSTNAMELEN];
struct ether_addr ea;
struct ether_addr mask;
uchar_t *ucp;
uchar_t *mcp;
/* Head of list. */
fep = slp->sl_entry.se_flist;
for (;;) {
while (isspace(*str) || *str == ',')
str++;
if (*str == '\0')
break;
cp = str;
while (*str != '\0' && !isspace(*str) && *str != ',')
str++;
len = str - cp;
(void) memcpy(hbuf, cp, len);
hbuf[len] = '\0';
mcp = mask.ether_addr_octet;
mcp[0] = mcp[1] = mcp[2] = mcp[3] = mcp[4] = mcp[5] = 0xFF;
if (ether_hostton(hbuf, &ea) != 0) {
ucp = ea.ether_addr_octet;
while (cp < str) {
if (ucp >= ea.ether_addr_octet + sizeof (ea))
break;
if (*cp == '*') {
*mcp++ = *ucp++ = 0;
cp++;
} else {
if (!isxdigit(*cp))
break;
*ucp = hexdecode(*cp++);
if (cp < str && isxdigit(*cp)) {
*ucp = (*ucp << 4) |
hexdecode(*cp++);
}
ucp++;
*mcp++ = 0xFF;
}
if (cp < str) {
if (*cp != ':' || cp + 1 == str)
break;
cp++;
}
}
if (cp < str) {
logerr("%s: illegal Ethernet address %.*s",
psp->ps_cfile->pf_name, len, cp);
continue;
}
}
fen = (struct filter_entry *)malloc(sizeof (*fen));
if (fen == NULL) {
logerr("unable to allocate memory for filter");
break;
}
fen->fe_isexcept = psp->ps_state == ksClientE;
fen->fe_prevcopy = 0;
(void) memcpy(&fen->fe_mac, &ea, sizeof (fen->fe_mac));
(void) memcpy(&fen->fe_mask, &mask, sizeof (fen->fe_mask));
fen->fe_prev = fep;
fep = fen;
}
slp->sl_entry.se_flist = fep;
return (0);
}
/*
* Handle "user <name>" option.
*/
static int
set_user(struct service_list *slp, const char *str)
{
struct passwd *pw;
char *cp;
uid_t myuid, uid;
if ((pw = getpwnam(str)) == NULL) {
uid = (uid_t)strtol(str, &cp, 0);
if (str == cp || *cp != '\0') {
logerr("%s: bad user name \"%s\"",
slp->sl_parse->ps_cfile->pf_name, str);
return (0);
}
} else {
uid = pw->pw_uid;
}
slp->sl_entry.se_uid = uid;
myuid = getuid();
if (myuid != 0) {
if (myuid == uid)
return (0);
logdbg("%s: not root; ignoring attempt to set UID %d (%s)",
slp->sl_parse->ps_cfile->pf_name, uid, str);
return (0);
}
slp->sl_entry.se_flags |= SEF_UIDSET;
return (0);
}
/*
* Handle "group <name>" option.
*/
static int
set_group(struct service_list *slp, const char *str)
{
struct group *gr;
char *cp;
gid_t gid;
if ((gr = getgrnam(str)) == NULL) {
gid = (gid_t)strtol(str, &cp, 0);
if (str == cp || *cp != '\0') {
logerr("%s: bad group name \"%s\"",
slp->sl_parse->ps_cfile->pf_name, str);
return (0);
}
} else {
gid = gr->gr_gid;
}
slp->sl_entry.se_gid = gid;
if (getuid() != 0) {
logdbg("%s: not root; ignoring attempt to set GID %d (%s)",
slp->sl_parse->ps_cfile->pf_name, gid, str);
return (0);
}
slp->sl_entry.se_flags |= SEF_GIDSET;
return (0);
}
/*
* This state machine is used to parse the configuration files. The
* "kwe_in" is the state in which the keyword is recognized. The
* "kwe_out" is the state that the keyword produces.
*/
struct kw_entry {
const char *kwe_word;
enum key_state kwe_in;
enum key_state kwe_out;
int (*kwe_func)(struct service_list *slp, const char *str);
};
static const struct kw_entry key_list[] = {
{ "service", ksDefault, ksService, NULL },
{ "device", ksDefault, ksDevice, NULL },
{ "client", ksDefault, ksClient, NULL },
{ "except", ksClient, ksClientE, NULL },
{ "wildcard", ksDefault, ksDefault, set_wildcard },
{ "nowildcard", ksDefault, ksDefault, set_wildcard },
{ "server", ksDefault, ksServer, NULL },
{ "pppd", ksDefault, ksPppd, NULL },
{ "debug", ksDefault, ksDefault, set_debug },
{ "nodebug", ksDefault, ksDefault, set_nodebug },
{ "file", ksDefault, ksFile, NULL },
{ "path", ksDefault, ksPath, NULL },
{ "extra", ksDefault, ksExtra, NULL },
{ "log", ksDefault, ksLog, NULL },
{ "user", ksDefault, ksUser, NULL },
{ "group", ksDefault, ksGroup, NULL },
/* Wildcards only past this point. */
{ "", ksService, ksDefault, set_service },
{ "", ksDevice, ksDefault, set_device },
{ "", ksClient, ksDefault, set_client },
{ "", ksClientE, ksDefault, set_client },
{ "", ksServer, ksDefault, set_string },
{ "", ksPppd, ksDefault, set_string },
{ "", ksFile, ksDefault, set_file },
{ "", ksPath, ksDefault, set_string },
{ "", ksExtra, ksDefault, set_string },
{ "", ksLog, ksDefault, set_string },
{ "", ksUser, ksDefault, set_user },
{ "", ksGroup, ksDefault, set_group },
{ NULL, ksDefault, ksDefault, NULL }
};
/*
* Produce a string for the keyword that would have gotten us into the
* current state.
*/
static const char *
after_key(enum key_state kstate)
{
const struct kw_entry *kep;
for (kep = key_list; kep->kwe_word != NULL; kep++)
if (kep->kwe_out == kstate)
return (kep->kwe_word);
return ("nothing");
}
/*
* Handle end-of-file processing -- close service, close file, revert
* to global context in previous include file nest level.
*/
static void
file_end(struct parse_state *psp)
{
struct per_file *pfp;
/* Must not be in the middle of parsing a multi-word sequence now. */
if (psp->ps_state != ksDefault) {
logerr("%s ends with \"%s\"", psp->ps_cfile->pf_name,
after_key(psp->ps_state));
psp->ps_state = ksDefault;
}
close_service(psp->ps_csvc);
if ((pfp = psp->ps_cfile) != NULL) {
/* Put this file on the list of finished files. */
psp->ps_cfile = pfp->pf_prev;
pfp->pf_prev = psp->ps_files;
psp->ps_files = pfp;
if (pfp->pf_input != NULL) {
logdbg("file %s closed", pfp->pf_name);
(void) fclose(pfp->pf_input);
pfp->pf_input = NULL;
}
/* Back up to previous file, if any, and set global context. */
if ((pfp = psp->ps_cfile) != NULL)
psp->ps_csvc = &pfp->pf_global;
}
}
/*
* Dispatch a single keyword against the parser state machine or
* handle an environment variable assignment. The input is a string
* containing the single word to be dispatched.
*/
static int
dispatch_keyword(struct parse_state *psp, const char *keybuf)
{
const struct kw_entry *kep;
int retv;
char *cp;
char *env;
char **evlist;
int len;
retv = 0;
for (kep = key_list; kep->kwe_word != NULL; kep++) {
if (kep->kwe_in == psp->ps_state &&
(*kep->kwe_word == '\0' ||
strcasecmp(kep->kwe_word, keybuf) == 0)) {
if (kep->kwe_func != NULL)
retv = (*kep->kwe_func)(psp->ps_csvc, keybuf);
psp->ps_state = kep->kwe_out;
return (retv);
}
}
if (strchr(keybuf, '=') != NULL) {
if ((cp = strsave(keybuf)) == NULL) {
logerr("no memory to save %s", keybuf);
return (0);
}
len = (strchr(cp, '=') - cp) + 1;
if ((evlist = psp->ps_evlist) == NULL) {
psp->ps_evlist = evlist =
(char **)malloc(8 * sizeof (*evlist));
if (evlist == NULL) {
logerr("no memory for evlist");
free(cp);
return (0);
}
psp->ps_evsize = 8;
evlist[0] = evlist[1] = NULL;
} else {
while ((env = *evlist) != NULL) {
if (strncmp(cp, env, len) == 0)
break;
evlist++;
}
if (env == NULL &&
evlist-psp->ps_evlist >= psp->ps_evsize-1) {
evlist = (char **)realloc(psp->ps_evlist,
(psp->ps_evsize + 8) * sizeof (*evlist));
if (evlist == NULL) {
logerr("cannot realloc evlist to %d",
psp->ps_evsize + 8);
free(cp);
return (0);
}
psp->ps_evlist = evlist;
evlist += psp->ps_evsize - 1;
psp->ps_evsize += 8;
evlist[1] = NULL;
}
}
logdbg("setenv \"%s\"", cp);
if (*evlist != NULL)
free(*evlist);
*evlist = cp;
return (0);
}
logerr("%s: unknown keyword '%s'", psp->ps_cfile->pf_name, keybuf);
return (-1);
}
/*
* Modified version of standard getenv; looks in locally-stored
* environment first. This function exists because we need to be able
* to revert to the original environment during a reread (SIGHUP), and
* the putenv() function overwrites that environment.
*/
static char *
my_getenv(struct parse_state *psp, char *estr)
{
char **evlist, *ent;
int elen;
if (psp != NULL && (evlist = psp->ps_evlist) != NULL) {
elen = strlen(estr);
while ((ent = *evlist++) != NULL) {
if (strncmp(ent, estr, elen) == 0 &&
ent[elen] == '=')
return (ent + elen + 1);
}
}
return (getenv(estr));
}
/*
* Expand an environment variable at the end of current buffer and
* return pointer to next spot in buffer for character append. psp
* context may be null.
*/
static char *
env_replace(struct parse_state *psp, char *keybuf, char kwstate)
{
char *cpe;
char *cp;
if ((cp = strrchr(keybuf, kwstate)) != NULL) {
if ((cpe = my_getenv(psp, cp + 1)) != NULL) {
*cp = '\0';
(void) strncat(cp, cpe,
MAX_KEYWORD - (cp - keybuf) - 1);
keybuf[MAX_KEYWORD - 1] = '\0';
cp += strlen(cp);
} else {
logerr("unknown variable \"%s\"", cp + 1);
}
} else {
/* Should not occur. */
cp = keybuf + strlen(keybuf);
}
return (cp);
}
/*
* Given a character-at-a-time input function, get a delimited keyword
* from the input. This function handles the usual escape sequences,
* quoting, commenting, and environment variable expansion.
*
* The standard wordexp(3C) function isn't used here because the POSIX
* definition is hard to use, and the Solaris implementation is
* resource-intensive and insecure. The "hard-to-use" part is that
* wordexp expands only variables from the environment, and can't
* handle an environment overlay. Instead, the caller must use the
* feeble putenv/getenv interface, and rewinding to the initial
* environment without leaking storage is hard. The Solaris
* implementation invokes an undocumented extensions via
* fork/exec("/bin/ksh -\005 %s") for every invocation, and gathers
* the expanded result with pipe. This makes it slow to execute and
* exposes the string being expanded to users with access to "ps -f."
*
* psp may be null; it's used only for environment variable expansion.
* Input "flag" is 1 to ignore EOL, '#', and '$'; 0 for normal file parsing.
*
* Returns:
* 0 - keyword parsed.
* 1 - end of file; no keyword.
* 2 - end of file after this keyword.
*/
static int
getkeyword(struct parse_state *psp, char *keybuf, int keymax,
int (*nextchr)(void *), void *arg, int flag)
{
char varnest[MAX_NEST];
char *kbp;
char *vnp;
char chr;
int ichr;
char kwstate;
static const char escstr[] = "a\ab\bf\fn\nr\r";
const char *cp;
keymax--; /* Account for trailing NUL byte */
kwstate = '\0';
kbp = keybuf;
vnp = varnest;
for (;;) {
ichr = (*nextchr)(arg);
chr = (char)ichr;
tryagain:
switch (kwstate) {
case '\\': /* Start of unquoted escape sequence */
case '|': /* Start of escape sequence in double quotes */
case '~': /* Start of escape sequence in single quotes */
/* Convert the character if we can. */
if (chr == '\n')
chr = '\0';
else if (isalpha(chr) &&
(cp = strchr(escstr, chr)) != NULL)
chr = cp[1];
/* Revert to previous state */
switch (kwstate) {
case '\\':
kwstate = 'A';
break;
case '|':
kwstate = '"';
break;
case '~':
kwstate = '\'';
break;
}
break;
case '"': /* In double-quote string */
if (!flag && chr == '$') {
/* Handle variable expansion. */
kwstate = '%';
chr = '\0';
break;
}
/* FALLTHROUGH */
case '\'': /* In single-quote string */
if (chr == '\\') {
/* Handle start of escape sequence */
kwstate = kwstate == '"' ? '|' : '~';
chr = '\0';
break;
}
if (chr == kwstate) {
/* End of quoted string; revert to normal */
kwstate = 'A';
chr = '\0';
}
break;
case '$': /* Start of unquoted variable name */
case '%': /* Start of variable name in quoted string */
if (chr == '{') {
/* Variable name is bracketed. */
kwstate = chr =
kwstate == '$' ? '{' : '[';
break;
}
*kbp++ = kwstate = kwstate == '$' ? '+' : '*';
/* FALLTHROUGH */
case '+': /* Gathering unquoted variable name */
case '*': /* Gathering variable name in quoted string */
if (chr == '$' &&
vnp < varnest + sizeof (varnest)) {
*vnp++ = kwstate;
kwstate = '$';
chr = '\0';
break;
}
if (!isalnum(chr) && chr != '_' &&
chr != '.' && chr != '-') {
*kbp = '\0';
kbp = env_replace(psp, keybuf, kwstate);
if (vnp > varnest)
kwstate = *--vnp;
else
kwstate = kwstate == '+' ?
'A' : '"';
/* Go reinterpret in new context */
goto tryagain;
}
break;
case '{': /* Gathering bracketed, unquoted var name */
case '[': /* Gathering bracketed, quoted var name */
if (chr == '}') {
*kbp = '\0';
kbp = env_replace(psp, keybuf, kwstate);
kwstate = kwstate == '{' ? 'A' : '"';
chr = '\0';
}
break;
case '#': /* Comment before word state */
case '@': /* Comment after word state */
if (chr == '\n' || chr == '\r' || ichr == EOF) {
/* At end of line, revert to previous state */
kwstate = kwstate == '#' ? '\0' : ' ';
chr = '\0';
break;
}
chr = '\0';
break;
case '\0': /* Initial state; no word seen yet. */
if (ichr == EOF || isspace(chr)) {
chr = '\0'; /* Skip over leading spaces */
break;
}
if (chr == '#') {
kwstate = '#';
chr = '\0'; /* Skip over comments */
break;
}
/* Start of keyword seen. */
kwstate = 'A';
/* FALLTHROUGH */
default: /* Middle of keyword parsing. */
if (ichr == EOF)
break;
if (isspace(chr)) { /* Space terminates word */
kwstate = ' ';
break;
}
if (chr == '"' || chr == '\'' || chr == '\\') {
kwstate = chr; /* Begin quote or escape */
chr = '\0';
break;
}
if (flag) /* Allow ignore; for string reparse */
break;
if (chr == '#') { /* Comment terminates word */
kwstate = '@'; /* Must consume comment also */
chr = '\0';
break;
}
if (chr == '$') {
kwstate = '$'; /* Begin variable expansion */
chr = '\0';
}
break;
}
/*
* If we've reached a space at the end of the word,
* then we're done.
*/
if (ichr == EOF || kwstate == ' ')
break;
/*
* If there's a character to store and space
* available, then add it to the string
*/
if (chr != '\0' && kbp < keybuf + keymax)
*kbp++ = (char)chr;
}
*kbp = '\0';
if (ichr == EOF) {
return (kwstate == '\0' ? 1 : 2);
}
return (0);
}
/*
* Fetch words from current file until all files are closed. Handles
* include files.
*/
static void
parse_from_file(struct parse_state *psp)
{
char keybuf[MAX_KEYWORD];
int retv;
while (psp->ps_cfile != NULL && psp->ps_cfile->pf_input != NULL) {
retv = getkeyword(psp, keybuf, sizeof (keybuf),
(int (*)(void *))fgetc, (void *)psp->ps_cfile->pf_input,
0);
if (retv != 1)
(void) dispatch_keyword(psp, keybuf);
if (retv != 0)
file_end(psp);
}
}
/*
* Open and parse named file. This is for the predefined
* configuration files in /etc/ppp -- it's not an error if any of
* these are missing.
*/
static void
parse_file(struct parse_state *psp, const char *fname)
{
struct stat sb;
/* It's ok if any of these files are missing. */
if (stat(fname, &sb) == -1 && errno == ENOENT)
return;
if (set_file(psp->ps_csvc, fname) == 0)
parse_from_file(psp);
}
/*
* Dispatch keywords from command line. Handles any files included
* from there.
*/
static void
parse_arg_list(struct parse_state *psp, int argc, char **argv)
{
/* The first argument (program name) can be null. */
if (--argc <= 0)
return;
while (--argc >= 0) {
(void) dispatch_keyword(psp, *++argv);
if (psp->ps_cfile->pf_input != NULL)
parse_from_file(psp);
}
}
/* Count length of dynamic device list */
static int
count_devs(struct device_list *dlp)
{
int ndevs;
ndevs = 0;
for (; dlp != NULL; dlp = dlp->dl_next)
ndevs++;
return (ndevs);
}
/* Count number of devices named in entire file. */
static int
count_per_file(struct per_file *pfp)
{
struct service_list *slp;
int ndevs = 0;
for (; pfp != NULL; pfp = pfp->pf_prev) {
ndevs += count_devs(pfp->pf_global.sl_dev);
for (slp = pfp->pf_svc; slp != NULL; slp = slp->sl_next)
if (!(slp->sl_entry.se_flags & SEF_CDEV))
ndevs += count_devs(slp->sl_dev);
}
return (ndevs);
}
/* Write device names into linear array. */
static const char **
devs_to_list(struct device_list *dlp, const char **dnames)
{
for (; dlp != NULL; dlp = dlp->dl_next)
*dnames++ = dlp->dl_name;
return (dnames);
}
/* Write all device names from file into a linear array. */
static const char **
per_file_to_list(struct per_file *pfp, const char **dnames)
{
struct service_list *slp;
for (; pfp != NULL; pfp = pfp->pf_prev) {
dnames = devs_to_list(pfp->pf_global.sl_dev, dnames);
for (slp = pfp->pf_svc; slp != NULL; slp = slp->sl_next)
if (!(slp->sl_entry.se_flags & SEF_CDEV))
dnames = devs_to_list(slp->sl_dev, dnames);
}
return (dnames);
}
/* Compare device names; used with qsort */
static int
devcmp(const void *d1, const void *d2)
{
return (strcmp(*(const char **)d1, *(const char **)d2));
}
/*
* Get sorted list of unique device names among all defined and
* partially defined services in all files.
*/
static const char **
get_unique_devs(struct parse_state *psp)
{
int ndevs;
const char **dnames;
const char **dnp;
const char **dnf;
/*
* Count number of explicitly referenced devices among all
* services (including duplicates).
*/
ndevs = count_per_file(psp->ps_files);
ndevs += count_per_file(psp->ps_cfile);
if (ndevs <= 0) {
return (NULL);
}
/* Sort and trim out duplicate devices. */
dnames = (const char **)malloc((ndevs+1) * sizeof (const char *));
if (dnames == NULL) {
logerr("unable to allocate space for %d devices", ndevs + 1);
return (NULL);
}
dnp = per_file_to_list(psp->ps_files, dnames);
(void) per_file_to_list(psp->ps_cfile, dnp);
qsort(dnames, ndevs, sizeof (const char *), devcmp);
for (dnf = (dnp = dnames) + 1; dnf < dnames+ndevs; dnf++)
if (strcmp(*dnf, *dnp) != 0)
*++dnp = *dnf;
*++dnp = NULL;
/* Return array of pointers to names. */
return (dnames);
}
/*
* Convert data structures created by parsing process into data
* structures used by service dispatch. This gathers the unique
* device (lower stream) names and attaches the services available on
* each device to a list while triming duplicate services.
*/
static struct option_state *
organize_state(struct parse_state *psp)
{
struct per_file *pfp;
struct per_file *pftopp;
struct service_list *slp;
struct device_list *dlp;
int ndevs;
int nsvcs;
const char **dnames;
const char **dnp;
struct device_entry *dep;
struct option_state *osp;
struct service_entry **sepp;
struct service_entry **sebpp;
struct service_entry **se2pp;
/*
* Parsing is now done.
*/
close_service(psp->ps_csvc);
psp->ps_csvc = NULL;
if ((pfp = psp->ps_cfile) != NULL) {
pfp->pf_prev = psp->ps_files;
psp->ps_files = pfp;
psp->ps_cfile = NULL;
}
/* Link the services from all files together for easy referencing. */
pftopp = psp->ps_files;
for (pfp = pftopp->pf_prev; pfp != NULL; pfp = pfp->pf_prev)
if (pfp->pf_svc != NULL) {
if (pftopp->pf_svc_last == NULL)
pftopp->pf_svc = pfp->pf_svc;
else
pftopp->pf_svc_last->sl_next = pfp->pf_svc;
pftopp->pf_svc_last = pfp->pf_svc_last;
pfp->pf_svc = pfp->pf_svc_last = NULL;
}
/*
* Count up number of services per device, including
* duplicates but not including defaults.
*/
nsvcs = 0;
for (slp = psp->ps_files->pf_svc; slp != NULL; slp = slp->sl_next)
for (dlp = slp->sl_dev; dlp != NULL; dlp = dlp->dl_next)
nsvcs++;
/*
* Get the unique devices referenced by all services.
*/
dnames = get_unique_devs(psp);
if (dnames == NULL) {
logdbg("no devices referenced by any service");
return (NULL);
}
ndevs = 0;
for (dnp = dnames; *dnp != NULL; dnp++)
ndevs++;
/*
* Allocate room for main structure, device records, and
* per-device lists. Worst case is all devices having all
* services; that's why we allocate for nsvcs * ndevs.
*/
osp = (struct option_state *)malloc(sizeof (*osp) +
ndevs * sizeof (*dep) + nsvcs * ndevs * sizeof (*sepp));
if (osp == NULL) {
logerr("unable to allocate option state structure");
free(dnames);
return (NULL);
}
/* We're going to succeed now, so steal these over. */
osp->os_devices = dep = (struct device_entry *)(osp+1);
osp->os_pfjunk = psp->ps_files;
psp->ps_files = NULL;
osp->os_evjunk = psp->ps_evlist;
psp->ps_evlist = NULL;
/* Loop over devices, install services, remove duplicates. */
sepp = (struct service_entry **)(dep + ndevs);
for (dnp = dnames; *dnp != NULL; dnp++) {
dep->de_name = *dnp;
dep->de_services = (const struct service_entry **)sepp;
sebpp = sepp;
for (slp = osp->os_pfjunk->pf_svc; slp != NULL;
slp = slp->sl_next)
for (dlp = slp->sl_dev; dlp != NULL;
dlp = dlp->dl_next) {
if (dlp->dl_name == *dnp ||
strcmp(dlp->dl_name, *dnp) == 0) {
for (se2pp = sebpp; se2pp < sepp;
se2pp++)
if ((*se2pp)->se_name ==
slp->sl_entry.se_name ||
strcmp((*se2pp)->
se_name,
slp->sl_entry.
se_name) == 0)
break;
/*
* We retain a service if it's
* unique or if its serial
* number (position in the
* file) is greater than than
* any other.
*/
if (se2pp >= sepp)
*sepp++ = &slp->sl_entry;
else if (SESERIAL(**se2pp) <
SESERIAL(slp->sl_entry))
*se2pp = &slp->sl_entry;
}
}
/* Count up the services on this device. */
dep->de_nservices = (const struct service_entry **)sepp -
dep->de_services;
/* Ignore devices having no services at all. */
if (dep->de_nservices > 0)
dep++;
}
/* Count up the devices. */
osp->os_ndevices = dep - osp->os_devices;
/* Free the list of device names */
free(dnames);
return (osp);
}
/*
* Free storage unique to a given service. Pointers copied from other
* services are ignored.
*/
static void
free_service(struct service_list *slp)
{
struct filter_entry *fep;
struct filter_entry *fen;
if (!(slp->sl_entry.se_flags & SEF_CDEV))
free_device_list(slp->sl_dev);
if (!(slp->sl_entry.se_flags & SEF_CFLIST)) {
fep = slp->sl_entry.se_flist;
while (fep != NULL) {
fen = fep->fe_prevcopy ? NULL : fep->fe_prev;
free(fep);
fep = fen;
}
}
if (!(slp->sl_entry.se_flags & SEF_CPPPD) &&
slp->sl_entry.se_pppd != NULL)
free(slp->sl_entry.se_pppd);
if (!(slp->sl_entry.se_flags & SEF_CSERVER) &&
slp->sl_entry.se_server != NULL)
free(slp->sl_entry.se_server);
if (!(slp->sl_entry.se_flags & SEF_CPATH) &&
slp->sl_entry.se_path != NULL)
free(slp->sl_entry.se_path);
if (!(slp->sl_entry.se_flags & SEF_CEXTRA) &&
slp->sl_entry.se_extra != NULL)
free(slp->sl_entry.se_extra);
if (!(slp->sl_entry.se_flags & SEF_CLOG) &&
slp->sl_entry.se_log != NULL)
free(slp->sl_entry.se_log);
}
/*
* Free a linked list of services.
*/
static void
free_service_list(struct service_list *slp)
{
struct service_list *sln;
while (slp != NULL) {
free_service(slp);
sln = slp->sl_next;
free(slp);
slp = sln;
}
}
/*
* Free a linked list of files and all services in those files.
*/
static void
free_file_list(struct per_file *pfp)
{
struct per_file *pfn;
while (pfp != NULL) {
free_service(&pfp->pf_global);
free_service_list(pfp->pf_svc);
pfn = pfp->pf_prev;
free(pfp);
pfp = pfn;
}
}
/*
* Free an array of local environment variables.
*/
static void
free_env_list(char **evlist)
{
char **evp;
char *env;
if ((evp = evlist) != NULL) {
while ((env = *evp++) != NULL)
free(env);
free(evlist);
}
}
/*
* Add a new device (lower stream) to the list for which we're the
* PPPoE server.
*/
static void
add_new_dev(int tunfd, const char *dname)
{
union ppptun_name ptn;
(void) snprintf(ptn.ptn_name, sizeof (ptn.ptn_name), "%s:pppoed",
dname);
if (strioctl(tunfd, PPPTUN_SCTL, &ptn, sizeof (ptn), 0) < 0) {
logerr("PPPTUN_SCTL %s: %s", ptn.ptn_name, mystrerror(errno));
} else {
logdbg("added %s", ptn.ptn_name);
}
}
/*
* Remove an existing device (lower stream) from the list for which we
* were the PPPoE server.
*/
static void
rem_old_dev(int tunfd, const char *dname)
{
union ppptun_name ptn;
(void) snprintf(ptn.ptn_name, sizeof (ptn.ptn_name), "%s:pppoed",
dname);
if (strioctl(tunfd, PPPTUN_DCTL, &ptn, sizeof (ptn), 0) < 0) {
logerr("PPPTUN_DCTL %s: %s", ptn.ptn_name, mystrerror(errno));
} else {
logdbg("removed %s", ptn.ptn_name);
}
}
/*
* Get a list of all of the devices currently plumbed for PPPoE. This
* is used for supporting the "*" and "all" device aliases.
*/
static void
get_device_list(struct parse_state *psp, int tunfd)
{
struct device_list *dlp;
struct device_list **dlpp;
struct device_list *dlalt;
struct device_list **dl2pp;
struct device_list *dla;
int i;
union ppptun_name ptn;
char *cp;
/* First pass; just allocate space for all *:pppoe* devices */
dlpp = &psp->ps_star;
dl2pp = &dlalt;
for (i = 0; ; i++) {
ptn.ptn_index = i;
if (strioctl(tunfd, PPPTUN_GNNAME, &ptn, sizeof (ptn),
sizeof (ptn)) < 0) {
logerr("PPPTUN_GNNAME %d: %s", i, mystrerror(errno));
break;
}
if (ptn.ptn_name[0] == '\0')
break;
if ((cp = strchr(ptn.ptn_name, ':')) == NULL ||
strncmp(cp, ":pppoe", 6) != 0 ||
(cp[6] != '\0' && strcmp(cp+6, "d") != 0))
continue;
*cp = '\0';
dlp = (struct device_list *)malloc(sizeof (*dlp) +
strlen(ptn.ptn_name) + 1);
if (dlp == NULL)
break;
dlp->dl_name = (const char *)(dlp + 1);
(void) strcpy((char *)(dlp + 1), ptn.ptn_name);
if (cp[6] == '\0') {
*dlpp = dlp;
dlpp = &dlp->dl_next;
} else {
*dl2pp = dlp;
dl2pp = &dlp->dl_next;
}
}
*dlpp = NULL;
*dl2pp = NULL;
/* Second pass; eliminate improperly plumbed devices */
for (dlpp = &psp->ps_star; (dlp = *dlpp) != NULL; ) {
for (dla = dlalt; dla != NULL; dla = dla->dl_next)
if (strcmp(dla->dl_name, dlp->dl_name) == 0)
break;
if (dla == NULL) {
*dlpp = dlp->dl_next;
free(dlp);
} else {
dlpp = &dlp->dl_next;
}
}
free_device_list(dlalt);
/* Add in "*" so we can always handle dynamic plumbing. */
dlp = (struct device_list *)malloc(sizeof (*dlp) + 2);
if (dlp != NULL) {
dlp->dl_name = (const char *)(dlp + 1);
(void) strcpy((char *)(dlp + 1), "*");
dlp->dl_next = psp->ps_star;
psp->ps_star = dlp;
}
}
/*
* Set logging subsystem back to configured global default values.
*/
void
global_logging(void)
{
log_for_service(glob_svc.se_log, glob_svc.se_debug);
}
/*
* Handle SIGHUP -- reparse command line and all configuration files.
* When reparsing is complete, free old parsed data and replace with
* new.
*/
void
parse_options(int tunfd, int argc, char **argv)
{
struct parse_state pstate;
struct per_file *argpf;
struct option_state *newopt;
const char **dnames;
const char **dnp;
const struct device_entry *newdep, *newmax;
const struct device_entry *olddep, *oldmax;
int cmpval;
struct service_entry newglobsvc, *mainsvc;
/* Note that all per_file structures must be freeable */
argpf = (struct per_file *)calloc(sizeof (*argpf), 1);
if (argpf == NULL) {
return;
}
(void) memset(&pstate, '\0', sizeof (pstate));
pstate.ps_state = ksDefault;
pstate.ps_cfile = argpf;
pstate.ps_csvc = &argpf->pf_global;
argpf->pf_global.sl_parse = &pstate;
argpf->pf_name = "command line";
/* Default is 1 -- errors only */
argpf->pf_global.sl_entry.se_debug++;
argpf->pf_global.sl_entry.se_name = "<global>";
/* Get list of all devices */
get_device_list(&pstate, tunfd);
/* Parse options from command line and main configuration file. */
pstate.ps_flags |= PSF_SETLEVEL;
parse_arg_list(&pstate, argc, argv);
parse_file(&pstate, "/etc/ppp/pppoe");
pstate.ps_flags &= ~PSF_SETLEVEL;
/*
* At this point, global options from the main configuration
* file are pointed to by ps_files, and options from command
* line are in argpf. We need to pull three special options
* from these -- wildcard, debug, and log. Note that the main
* options file overrides the command line. This is
* intentional. The semantics are such that the system
* behaves as though the main configuration file were
* "included" from the command line, and thus options there
* override the command line. This may seem odd, but at least
* it's self-consistent.
*/
newglobsvc = argpf->pf_global.sl_entry;
if (pstate.ps_files != NULL) {
mainsvc = &pstate.ps_files->pf_global.sl_entry;
if (mainsvc->se_log != NULL)
newglobsvc.se_log = mainsvc->se_log;
if (mainsvc->se_flags & (SEF_WILD|SEF_NOWILD))
newglobsvc.se_flags =
(newglobsvc.se_flags & ~(SEF_WILD|SEF_NOWILD)) |
(mainsvc->se_flags & (SEF_WILD|SEF_NOWILD));
if (mainsvc->se_flags & SEF_DEBUGCLR)
newglobsvc.se_debug = 0;
newglobsvc.se_debug += mainsvc->se_debug;
}
glob_svc = newglobsvc;
global_logging();
/* Get the list of devices referenced by configuration above. */
dnames = get_unique_devs(&pstate);
if (dnames != NULL) {
/* Read per-device configuration files. */
pstate.ps_flags |= PSF_PERDEV;
for (dnp = dnames; *dnp != NULL; dnp++)
parse_file(&pstate, *dnp);
pstate.ps_flags &= ~PSF_PERDEV;
free(dnames);
}
file_end(&pstate);
/*
* Convert parsed data structures into per-device structures.
* (Invert the table.)
*/
newopt = organize_state(&pstate);
/* If we're going to free the file name, then stop logging there. */
if (newopt == NULL && glob_svc.se_log != NULL) {
glob_svc.se_log = NULL;
global_logging();
}
/*
* Unless an error has occurred, these pointers are normally
* all NULL. Nothing is freed until the file is re-read.
*/
free_file_list(pstate.ps_files);
free_file_list(pstate.ps_cfile);
free_device_list(pstate.ps_star);
free_env_list(pstate.ps_evlist);
/*
* Match up entries on device list. Detach devices no longer
* referenced. Attach ones now referenced. (The use of null
* pointers here may look fishy, but it actually works.
* NULL>=NULL is always true.)
*/
if (newopt != NULL) {
newdep = newopt->os_devices;
newmax = newdep + newopt->os_ndevices;
} else {
newdep = newmax = NULL;
}
if (cur_options != NULL) {
olddep = cur_options->os_devices;
oldmax = olddep + cur_options->os_ndevices;
} else {
olddep = oldmax = NULL;
}
while ((newdep != NULL && newdep < newmax) ||
(olddep != NULL && olddep < oldmax)) {
if (newdep < newmax) {
if (olddep >= oldmax) {
add_new_dev(tunfd, newdep->de_name);
newdep++;
} else {
cmpval = strcmp(newdep->de_name,
olddep->de_name);
if (cmpval < 0) {
/* Brand new device seen. */
add_new_dev(tunfd, newdep->de_name);
newdep++;
} else if (cmpval == 0) {
/* Existing device; skip it. */
newdep++;
olddep++;
}
/* No else clause -- removal is below */
}
}
if (olddep < oldmax) {
if (newdep >= newmax) {
rem_old_dev(tunfd, olddep->de_name);
olddep++;
} else {
cmpval = strcmp(newdep->de_name,
olddep->de_name);
if (cmpval > 0) {
/* Old device is gone */
rem_old_dev(tunfd, olddep->de_name);
olddep++;
} else if (cmpval == 0) {
/* Existing device; skip it. */
newdep++;
olddep++;
}
/* No else clause -- insert handled above */
}
}
}
/* Discard existing parsed data storage. */
if (cur_options != NULL) {
free_file_list(cur_options->os_pfjunk);
free_env_list(cur_options->os_evjunk);
free(cur_options);
}
/* Install new. */
cur_options = newopt;
}
/*
* Check if configured filters permit requesting client to use a given
* service. Note -- filters are stored in reverse order in order to
* make file-inclusion work as expected. Thus, the "first match"
* filter rule becomes "last match" here.
*/
static boolean_t
allow_service(const struct service_entry *sep, const ppptun_atype *pap)
{
const struct filter_entry *fep;
const struct filter_entry *lmatch;
boolean_t anynonexcept = B_FALSE;
const uchar_t *upt;
const uchar_t *macp;
const uchar_t *maskp;
int i;
lmatch = NULL;
for (fep = sep->se_flist; fep != NULL; fep = fep->fe_prev) {
anynonexcept |= !fep->fe_isexcept;
upt = pap->pta_pppoe.ptma_mac;
macp = fep->fe_mac.ether_addr_octet;
maskp = fep->fe_mask.ether_addr_octet;
for (i = sizeof (pap->pta_pppoe.ptma_mac); i > 0; i--)
if (((*macp++ ^ *upt++) & *maskp++) != 0)
break;
if (i <= 0)
lmatch = fep;
}
if (lmatch == NULL) {
/*
* Assume reject by default if any positive-match
* (non-except) filters are given. Otherwise, if
* there are no positive-match filters, then
* non-matching means accept by default.
*/
return (!anynonexcept);
}
return (!lmatch->fe_isexcept);
}
/*
* Locate available service(s) based on client request. Assumes that
* outp points to a buffer of at least size PPPOE_MSGMAX. Creates a
* PPPoE response message in outp. Returns count of matched services
* and (through *srvp) a pointer to the last (or only) service. If
* some error is found in the request, an error string is added and -1
* is returned; the caller should just send the message without
* alteration.
*/
int
locate_service(poep_t *poep, int plen, const char *iname, ppptun_atype *pap,
uint32_t *outp, void **srvp)
{
poep_t *opoe;
const uint8_t *tagp;
const char *cp;
int ttyp;
int tlen;
int nsvcs;
const struct device_entry *dep, *depe;
const struct device_entry *wdep;
const struct service_entry **sepp, **seppe;
const struct service_entry *sep;
char *str;
boolean_t ispadi;
ispadi = poep->poep_code == POECODE_PADI;
opoe = poe_mkheader(outp, ispadi ? POECODE_PADO : POECODE_PADS, 0);
*srvp = NULL;
if (cur_options == NULL)
return (0);
/* Search for named device (lower stream) in tables. */
dep = cur_options->os_devices;
depe = dep + cur_options->os_ndevices;
wdep = NULL;
if ((cp = strchr(iname, ':')) != NULL)
tlen = cp - iname;
else
tlen = strlen(iname);
for (; dep < depe; dep++)
if (strncmp(iname, dep->de_name, tlen) == 0 &&
dep->de_name[tlen] == '\0')
break;
else if (dep->de_name[0] == '*' && dep->de_name[1] == '\0')
wdep = dep;
if (dep >= depe)
dep = wdep;
/*
* Return if interface not found. Zero-service case can't
* occur, since devices with no services aren't included in
* the list, but the code is just being safe here.
*/
if (dep == NULL || dep->de_services == NULL || dep->de_nservices <= 0)
return (0);
/*
* Loop over tags in client message and process them.
* Services must be matched against our list. Host-Uniq and
* Relay-Session-Id must be copied to the reply. All others
* must be discarded.
*/
nsvcs = 0;
sepp = dep->de_services;
tagp = (const uint8_t *)(poep + 1);
while (poe_tagcheck(poep, plen, tagp)) {
ttyp = POET_GET_TYPE(tagp);
if (ttyp == POETT_END)
break;
tlen = POET_GET_LENG(tagp);
switch (ttyp) {
case POETT_SERVICE: /* Service-Name */
/*
* Allow only one. (Note that this test works
* because there's always at least one service
* per device; otherwise, the device is
* removed from the list.)
*/
if (sepp != dep->de_services) {
if (nsvcs != -1)
(void) poe_add_str(opoe, POETT_NAMERR,
"Too many Service-Name tags");
nsvcs = -1;
break;
}
seppe = sepp + dep->de_nservices;
/* Clients's requested service must appear in reply. */
if (tlen != 0 || (ispadi &&
!(glob_svc.se_flags & SEF_NOWILD)))
(void) poe_tag_copy(opoe, tagp);
if (tlen == 0) {
/*
* If config specifies "nowild" in a
* global context, then we don't
* respond to wildcard PADRs. The
* client must know the exact service
* name to get access.
*/
if (!ispadi && (glob_svc.se_flags & SEF_NOWILD))
sepp = seppe;
while (sepp < seppe) {
sep = *sepp++;
if ((ispadi || !(sep->se_flags &
SEF_NOWILD)) &&
allow_service(sep, pap)) {
nsvcs++;
*srvp = (void *)sep;
if (poep->poep_code ==
POECODE_PADR)
break;
if (sep->se_name[0] == '\0')
continue;
(void) poe_add_str(opoe,
POETT_SERVICE,
sep->se_name);
}
}
} else {
/* Requested specific service; find it. */
cp = (char *)POET_DATA(tagp);
while (sepp < seppe) {
sep = *sepp++;
if (strlen(sep->se_name) == tlen &&
strncasecmp(sep->se_name, cp,
tlen) == 0) {
if (allow_service(sep, pap)) {
nsvcs++;
*srvp = (void *)sep;
}
break;
}
}
}
/*
* Allow service definition to override
* AC-Name (concentrator [server] name) field.
*/
if (*srvp != NULL) {
sep = (const struct service_entry *)*srvp;
log_for_service(sep->se_log, sep->se_debug);
str = "Solaris PPPoE";
if (sep->se_server != NULL)
str = sep->se_server;
(void) poe_add_str(opoe, POETT_ACCESS, str);
}
break;
/* Ones we should discard */
case POETT_ACCESS: /* AC-Name */
case POETT_COOKIE: /* AC-Cookie */
case POETT_NAMERR: /* Service-Name-Error */
case POETT_SYSERR: /* AC-System-Error */
case POETT_GENERR: /* Generic-Error */
case POETT_HURL: /* Host-URL */
case POETT_MOTM: /* Message-Of-The-Minute */
case POETT_RTEADD: /* IP-Route-Add */
case POETT_VENDOR: /* Vendor-Specific */
case POETT_MULTI: /* Multicast-Capable */
default:
break;
/* Ones we should copy */
case POETT_UNIQ: /* Host-Uniq */
case POETT_RELAY: /* Relay-Session-Id */
(void) poe_tag_copy(opoe, tagp);
break;
}
tagp = POET_NEXT(tagp);
}
return (nsvcs);
}
/*
* Like fgetc, but reads from a string.
*/
static int
sgetc(void *arg)
{
char **cpp = (char **)arg;
if (**cpp == '\0')
return (EOF);
return (*(*cpp)++);
}
/*
* Given a service structure, launch pppd. Called by handle_input()
* in pppoed.c if locate_service() [above] finds exactly one service
* matching a PADR.
*/
int
launch_service(int tunfd, poep_t *poep, void *srvp, struct ppptun_control *ptc)
{
const struct service_entry *sep = (const struct service_entry *)srvp;
const char *path;
const char *extra;
const char *pppd;
const char *cp;
pid_t pidv;
int newtun;
struct ppptun_peer ptp;
union ppptun_name ptn;
const char *args[MAXARGS];
struct strbuf ctrl;
struct strbuf data;
const char **cpp;
char *sptr;
char *spv;
int slen;
int retv;
char keybuf[MAX_KEYWORD];
assert(sep != NULL);
/* Get tunnel driver connection for new PPP session. */
newtun = open(tunnam, O_RDWR);
if (newtun == -1)
goto syserr;
/* Set this session up for standard PPP and client's address. */
(void) memset(&ptp, '\0', sizeof (ptp));
ptp.ptp_style = PTS_PPPOE;
ptp.ptp_address = ptc->ptc_address;
if (strioctl(newtun, PPPTUN_SPEER, &ptp, sizeof (ptp), sizeof (ptp)) <
0)
goto syserr;
ptp.ptp_rsessid = ptp.ptp_lsessid;
if (strioctl(newtun, PPPTUN_SPEER, &ptp, sizeof (ptp), sizeof (ptp)) <
0)
goto syserr;
/* Attach the requested lower stream. */
cp = strchr(ptc->ptc_name, ':');
if (cp == NULL)
cp = ptc->ptc_name + strlen(ptc->ptc_name);
(void) snprintf(ptn.ptn_name, sizeof (ptn.ptn_name), "%.*s:pppoe",
cp-ptc->ptc_name, ptc->ptc_name);
if (strioctl(newtun, PPPTUN_SDATA, &ptn, sizeof (ptn), 0) < 0)
goto syserr;
(void) snprintf(ptn.ptn_name, sizeof (ptn.ptn_name), "%.*s:pppoed",
cp-ptc->ptc_name, ptc->ptc_name);
if (strioctl(newtun, PPPTUN_SCTL, &ptn, sizeof (ptn), 0) < 0)
goto syserr;
pidv = fork();
if (pidv == (pid_t)-1)
goto syserr;
if (pidv == (pid_t)0) {
/*
* Use syslog only in order to avoid mixing log messages
* in regular files.
*/
close_log_files();
if ((path = sep->se_path) == NULL)
path = "/usr/bin/pppd";
if ((extra = sep->se_extra) == NULL)
extra = "plugin pppoe.so directtty";
if ((pppd = sep->se_pppd) == NULL)
pppd = "";
/* Concatenate these. */
slen = strlen(path) + strlen(extra) + strlen(pppd) + 3;
if ((sptr = (char *)malloc(slen)) == NULL)
goto bail_out;
(void) strcpy(sptr, path);
(void) strcat(sptr, " ");
(void) strcat(sptr, extra);
(void) strcat(sptr, " ");
(void) strcat(sptr, pppd);
/* Parse out into arguments */
cpp = args;
spv = sptr;
while (cpp < args + MAXARGS - 1) {
retv = getkeyword(NULL, keybuf, sizeof (keybuf), sgetc,
(void *)&spv, 1);
if (retv != 1)
*cpp++ = strsave(keybuf);
if (retv != 0)
break;
}
*cpp = NULL;
if (cpp == args)
goto bail_out;
/*
* Fix tunnel device on stdin/stdout and error file on
* stderr.
*/
if (newtun != 0 && dup2(newtun, 0) < 0)
goto bail_out;
if (newtun != 1 && dup2(newtun, 1) < 0)
goto bail_out;
if (newtun > 1)
(void) close(newtun);
if (tunfd > 1)
(void) close(tunfd);
(void) close(2);
(void) open("/etc/ppp/pppoe-errors", O_WRONLY | O_APPEND |
O_CREAT, 0600);
/*
* Change GID first, for obvious reasons. Note that
* we log any problems to syslog, not the errors file.
* The errors file is intended for problems in the
* exec'd program.
*/
if ((sep->se_flags & SEF_GIDSET) &&
setgid(sep->se_gid) == -1) {
cp = mystrerror(errno);
reopen_log();
logerr("setgid(%d): %s", sep->se_gid, cp);
goto logged;
}
if ((sep->se_flags & SEF_UIDSET) &&
setuid(sep->se_uid) == -1) {
cp = mystrerror(errno);
reopen_log();
logerr("setuid(%d): %s", sep->se_uid, cp);
goto logged;
}
/* Run pppd */
path = args[0];
cp = strrchr(args[0], '/');
if (cp != NULL && cp[1] != '\0')
args[0] = cp+1;
errno = 0;
(void) execv(path, (char * const *)args);
newtun = 0;
/*
* Exec failure; attempt to log the problem and send a
* PADT to the client so that he knows the session
* went south.
*/
bail_out:
cp = mystrerror(errno);
reopen_log();
logerr("\"%s\": %s", (sptr == NULL ? path : sptr), cp);
logged:
poep = poe_mkheader(pkt_output, POECODE_PADT, ptp.ptp_lsessid);
poep->poep_session_id = htons(ptp.ptp_lsessid);
(void) poe_add_str(poep, POETT_SYSERR, cp);
(void) sleep(1);
ctrl.len = sizeof (*ptc);
ctrl.buf = (caddr_t)ptc;
data.len = poe_length(poep) + sizeof (*poep);
data.buf = (caddr_t)poep;
if (putmsg(newtun, &ctrl, &data, 0) < 0) {
logerr("putmsg %s: %s", ptc->ptc_name,
mystrerror(errno));
}
exit(1);
}
(void) close(newtun);
/* Give session ID to client in reply. */
poep->poep_session_id = htons(ptp.ptp_lsessid);
return (1);
syserr:
/* Peer doesn't know session ID yet; hope for the best. */
retv = errno;
if (newtun >= 0)
(void) close(newtun);
(void) poe_add_str(poep, POETT_SYSERR, mystrerror(retv));
return (0);
}
/*
* This is pretty awful -- it uses recursion to print a simple list.
* It's just for debug, though, and does a reasonable job of printing
* the filters in the right order.
*/
static void
print_filter_list(FILE *fp, struct filter_entry *fep)
{
if (fep->fe_prev != NULL)
print_filter_list(fp, fep->fe_prev);
(void) fprintf(fp, "\t\t MAC %s", ehost2(&fep->fe_mac));
(void) fprintf(fp, ", mask %s%s\n", ehost2(&fep->fe_mask),
(fep->fe_isexcept ? ", except" : ""));
}
/*
* Write summary of parsed configuration data to given file.
*/
void
dump_configuration(FILE *fp)
{
const struct device_entry *dep;
const struct service_entry *sep, **sepp;
struct per_file *pfp;
int i, j;
(void) fprintf(fp, "Will%s respond to wildcard queries.\n",
(glob_svc.se_flags & SEF_NOWILD) ? " not" : "");
(void) fprintf(fp,
"Global debug level %d, log to %s; current level %d\n",
glob_svc.se_debug,
((glob_svc.se_log == NULL || *glob_svc.se_log == '\0') ?
"syslog" : glob_svc.se_log),
log_level);
if (cur_options == NULL) {
(void) fprintf(fp, "No current configuration.\n");
return;
}
(void) fprintf(fp, "Current configuration:\n");
(void) fprintf(fp, " %d device(s):\n", cur_options->os_ndevices);
dep = cur_options->os_devices;
for (i = 0; i < cur_options->os_ndevices; i++, dep++) {
(void) fprintf(fp, "\t%s: %d service(s):\n",
dep->de_name, dep->de_nservices);
sepp = dep->de_services;
for (j = 0; j < dep->de_nservices; j++, sepp++) {
sep = *sepp;
(void) fprintf(fp, "\t %s: debug level %d",
sep->se_name, sep->se_debug);
if (sep->se_flags & SEF_UIDSET)
(void) fprintf(fp, ", UID %ld", sep->se_uid);
if (sep->se_flags & SEF_GIDSET)
(void) fprintf(fp, ", GID %ld", sep->se_gid);
if (sep->se_flags & SEF_WILD)
(void) fprintf(fp, ", wildcard");
else if (sep->se_flags & SEF_NOWILD)
(void) fprintf(fp, ", nowildcard");
else
(void) fprintf(fp, ", wildcard (default)");
(void) putc('\n', fp);
if (sep->se_server != NULL)
(void) fprintf(fp, "\t\tserver \"%s\"\n",
sep->se_server);
if (sep->se_pppd != NULL)
(void) fprintf(fp, "\t\tpppd \"%s\"\n",
sep->se_pppd);
if (sep->se_path != NULL)
(void) fprintf(fp, "\t\tpath \"%s\"\n",
sep->se_path);
if (sep->se_extra != NULL)
(void) fprintf(fp, "\t\textra \"%s\"\n",
sep->se_extra);
if (sep->se_log != NULL)
(void) fprintf(fp, "\t\tlog \"%s\"\n",
sep->se_log);
if (sep->se_flist != NULL) {
(void) fprintf(fp, "\t\tfilter list:\n");
print_filter_list(fp, sep->se_flist);
}
}
}
(void) fprintf(fp, "\nConfiguration read from:\n");
for (pfp = cur_options->os_pfjunk; pfp != NULL; pfp = pfp->pf_prev) {
(void) fprintf(fp, " %s: %d service(s)\n", pfp->pf_name,
pfp->pf_nsvc);
}
}