src/usr.sbin/relayd/parse.y

3518 lines
78 KiB
Text

/* $OpenBSD: parse.y,v 1.256 2024/06/17 08:02:57 sashan Exp $ */
/*
* Copyright (c) 2007 - 2014 Reyk Floeter <reyk@openbsd.org>
* Copyright (c) 2008 Gilles Chehade <gilles@openbsd.org>
* Copyright (c) 2006 Pierre-Yves Ritschard <pyr@openbsd.org>
* Copyright (c) 2004, 2005 Esben Norby <norby@openbsd.org>
* Copyright (c) 2004 Ryan McBride <mcbride@openbsd.org>
* Copyright (c) 2002, 2003, 2004 Henning Brauer <henning@openbsd.org>
* Copyright (c) 2001 Markus Friedl. All rights reserved.
* Copyright (c) 2001 Daniel Hartmeier. All rights reserved.
* Copyright (c) 2001 Theo de Raadt. All rights reserved.
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
%{
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/queue.h>
#include <sys/ioctl.h>
#include <sys/time.h>
#include <sys/tree.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <net/if.h>
#include <net/pfvar.h>
#include <net/route.h>
#include <agentx.h>
#include <stdint.h>
#include <stdarg.h>
#include <stdio.h>
#include <unistd.h>
#include <ctype.h>
#include <err.h>
#include <endian.h>
#include <errno.h>
#include <limits.h>
#include <netdb.h>
#include <string.h>
#include <ifaddrs.h>
#include <syslog.h>
#include <md5.h>
#include "relayd.h"
#include "http.h"
TAILQ_HEAD(files, file) files = TAILQ_HEAD_INITIALIZER(files);
static struct file {
TAILQ_ENTRY(file) entry;
FILE *stream;
char *name;
size_t ungetpos;
size_t ungetsize;
u_char *ungetbuf;
int eof_reached;
int lineno;
int errors;
} *file, *topfile;
struct file *pushfile(const char *, int);
int popfile(void);
int check_file_secrecy(int, const char *);
int yyparse(void);
int yylex(void);
int yyerror(const char *, ...)
__attribute__((__format__ (printf, 1, 2)))
__attribute__((__nonnull__ (1)));
int kw_cmp(const void *, const void *);
int lookup(char *);
int igetc(void);
int lgetc(int);
void lungetc(int);
int findeol(void);
TAILQ_HEAD(symhead, sym) symhead = TAILQ_HEAD_INITIALIZER(symhead);
struct sym {
TAILQ_ENTRY(sym) entry;
int used;
int persist;
char *nam;
char *val;
};
int symset(const char *, const char *, int);
char *symget(const char *);
struct relayd *conf = NULL;
static int errors = 0;
static int loadcfg = 0;
objid_t last_rdr_id = 0;
objid_t last_table_id = 0;
objid_t last_host_id = 0;
objid_t last_relay_id = 0;
objid_t last_proto_id = 0;
objid_t last_rt_id = 0;
objid_t last_nr_id = 0;
static struct rdr *rdr = NULL;
static struct table *table = NULL;
static struct relay *rlay = NULL;
static struct host *hst = NULL;
struct relaylist relays;
static struct protocol *proto = NULL;
static struct relay_rule *rule = NULL;
static struct router *router = NULL;
static int label = 0;
static int tagged = 0;
static int tag = 0;
static in_port_t tableport = 0;
static int dstmode;
static enum key_type keytype = KEY_TYPE_NONE;
static enum direction dir = RELAY_DIR_ANY;
static char *rulefile = NULL;
static union hashkey *hashkey = NULL;
struct address *host_ip(const char *);
int host_dns(const char *, struct addresslist *,
int, struct portrange *, const char *, int);
int host_if(const char *, struct addresslist *,
int, struct portrange *, const char *, int);
int host(const char *, struct addresslist *,
int, struct portrange *, const char *, int);
void host_free(struct addresslist *);
struct table *table_inherit(struct table *);
int relay_id(struct relay *);
struct relay *relay_inherit(struct relay *, struct relay *);
int getservice(char *);
int is_if_in_group(const char *, const char *);
typedef struct {
union {
int64_t number;
char *string;
struct host *host;
struct timeval tv;
struct table *table;
struct portrange port;
struct {
union hashkey key;
int keyset;
} key;
enum direction dir;
struct {
struct sockaddr_storage ss;
int prefixlen;
char name[HOST_NAME_MAX+1];
} addr;
struct {
enum digest_type type;
char *digest;
} digest;
} v;
int lineno;
} YYSTYPE;
%}
%token AGENTX APPEND BACKLOG BACKUP BINARY BUFFER CA CACHE SET CHECK CIPHERS
%token CODE COOKIE DEMOTE DIGEST DISABLE ERROR EXPECT PASS BLOCK EXTERNAL
%token FILENAME FORWARD FROM HASH HEADER HEADERLEN HOST HTTP ICMP INCLUDE INET
%token INET6 INTERFACE INTERVAL IP KEYPAIR LABEL LISTEN VALUE LOADBALANCE LOG
%token LOOKUP METHOD MODE NAT NO DESTINATION NODELAY NOTHING ON PARENT PATH
%token PFTAG PORT PREFORK PRIORITY PROTO QUERYSTR REAL REDIRECT RELAY REMOVE
%token REQUEST RESPONSE RETRY QUICK RETURN ROUNDROBIN ROUTE SACK SCRIPT SEND
%token SESSION SOCKET SPLICE STICKYADDR STRIP STYLE TABLE TAG TAGGED TCP
%token TIMEOUT TLS TO ROUTER RTLABEL TRANSPARENT URL WITH TTL RTABLE
%token MATCH PARAMS RANDOM LEASTSTATES SRCHASH KEY CERTIFICATE PASSWORD ECDHE
%token EDH TICKETS CONNECTION CONNECTIONS CONTEXT ERRORS STATE CHANGES CHECKS
%token WEBSOCKETS PFLOG
%token <v.string> STRING
%token <v.number> NUMBER
%type <v.string> context hostname interface table value path
%type <v.number> http_type loglevel quick
%type <v.number> dstmode flag forwardmode retry
%type <v.number> opttls opttlsclient
%type <v.number> redirect_proto relay_proto match pflog
%type <v.number> action ruleaf key_option
%type <v.port> port
%type <v.host> host
%type <v.addr> address rulesrc ruledst addrprefix
%type <v.tv> timeout
%type <v.digest> digest optdigest
%type <v.table> tablespec
%type <v.dir> dir
%type <v.key> hashkey
%%
grammar : /* empty */
| grammar include '\n'
| grammar '\n'
| grammar varset '\n'
| grammar main '\n'
| grammar rdr '\n'
| grammar tabledef '\n'
| grammar relay '\n'
| grammar proto '\n'
| grammar router '\n'
| grammar error '\n' { file->errors++; }
;
include : INCLUDE STRING {
struct file *nfile;
if ((nfile = pushfile($2, 0)) == NULL) {
yyerror("failed to include file %s", $2);
free($2);
YYERROR;
}
free($2);
file = nfile;
lungetc('\n');
}
;
opttls : /*empty*/ { $$ = 0; }
| TLS { $$ = 1; }
;
opttlsclient : /*empty*/ { $$ = 0; }
| WITH TLS { $$ = 1; }
;
http_type : HTTP { $$ = 0; }
| STRING {
if (strcmp("https", $1) == 0) {
$$ = 1;
} else {
yyerror("invalid check type: %s", $1);
free($1);
YYERROR;
}
free($1);
}
;
hostname : /* empty */ {
$$ = strdup("");
if ($$ == NULL)
fatal("calloc");
}
| HOST STRING {
if (asprintf(&$$, "Host: %s\r\nConnection: close\r\n",
$2) == -1)
fatal("asprintf");
}
;
relay_proto : /* empty */ { $$ = RELAY_PROTO_TCP; }
| TCP { $$ = RELAY_PROTO_TCP; }
| HTTP { $$ = RELAY_PROTO_HTTP; }
| STRING {
if (strcmp("dns", $1) == 0) {
$$ = RELAY_PROTO_DNS;
} else {
yyerror("invalid protocol type: %s", $1);
free($1);
YYERROR;
}
free($1);
}
;
redirect_proto : /* empty */ { $$ = IPPROTO_TCP; }
| TCP { $$ = IPPROTO_TCP; }
| STRING {
struct protoent *p;
if ((p = getprotobyname($1)) == NULL) {
yyerror("invalid protocol: %s", $1);
free($1);
YYERROR;
}
free($1);
$$ = p->p_proto;
}
;
eflags_l : eflags comma eflags_l
| eflags
;
opteflags : /* nothing */
| eflags
;
eflags : STYLE STRING
{
if ((proto->style = strdup($2)) == NULL)
fatal("out of memory");
free($2);
}
;
port : PORT HTTP {
int p = 0;
$$.op = PF_OP_EQ;
if ((p = getservice("http")) == -1)
YYERROR;
$$.val[0] = p;
$$.val[1] = 0;
}
| PORT STRING {
char *a, *b;
int p[2];
p[0] = p[1] = 0;
a = $2;
b = strchr($2, ':');
if (b == NULL)
$$.op = PF_OP_EQ;
else {
*b++ = '\0';
if ((p[1] = getservice(b)) == -1) {
free($2);
YYERROR;
}
$$.op = PF_OP_RRG;
}
if ((p[0] = getservice(a)) == -1) {
free($2);
YYERROR;
}
$$.val[0] = p[0];
$$.val[1] = p[1];
free($2);
}
| PORT NUMBER {
if ($2 <= 0 || $2 > (int)USHRT_MAX) {
yyerror("invalid port: %lld", $2);
YYERROR;
}
$$.val[0] = htons($2);
$$.op = PF_OP_EQ;
}
;
varset : STRING '=' STRING {
char *s = $1;
while (*s++) {
if (isspace((unsigned char)*s)) {
yyerror("macro name cannot contain "
"whitespace");
free($1);
free($3);
YYERROR;
}
}
if (symset($1, $3, 0) == -1)
fatal("cannot store variable");
free($1);
free($3);
}
;
sendbuf : NOTHING {
table->sendbuf = NULL;
}
| STRING {
table->sendbuf = strdup($1);
if (table->sendbuf == NULL)
fatal("out of memory");
free($1);
}
;
sendbinbuf : NOTHING {
table->sendbinbuf = NULL;
}
| STRING {
if (strlen($1) == 0) {
yyerror("empty binary send data");
free($1);
YYERROR;
}
table->sendbuf = strdup($1);
if (table->sendbuf == NULL)
fatal("out of memory");
table->sendbinbuf = string2binary($1);
if (table->sendbinbuf == NULL)
fatal("failed in binary send data");
free($1);
}
;
main : INTERVAL NUMBER {
if ((conf->sc_conf.interval.tv_sec = $2) < 0) {
yyerror("invalid interval: %lld", $2);
YYERROR;
}
}
| LOG loglevel {
conf->sc_conf.opts |= $2;
}
| TIMEOUT timeout {
bcopy(&$2, &conf->sc_conf.timeout,
sizeof(struct timeval));
}
| PREFORK NUMBER {
if ($2 <= 0 || $2 > PROC_MAX_INSTANCES) {
yyerror("invalid number of preforked "
"relays: %lld", $2);
YYERROR;
}
conf->sc_conf.prefork_relay = $2;
}
| AGENTX context path {
conf->sc_conf.flags |= F_AGENTX;
if ($2 != NULL) {
if (strlcpy(conf->sc_conf.agentx_context, $2,
sizeof(conf->sc_conf.agentx_context)) >=
sizeof(conf->sc_conf.agentx_context)) {
yyerror("agentx context too long");
free($2);
free($3);
YYERROR;
}
free($2);
} else
conf->sc_conf.agentx_context[0] = '\0';
if ($3 != NULL) {
if (strlcpy(conf->sc_conf.agentx_path, $3,
sizeof(conf->sc_conf.agentx_path)) >=
sizeof(conf->sc_conf.agentx_path)) {
yyerror("agentx path too long");
free($3);
YYERROR;
}
free($3);
} else
(void)strlcpy(conf->sc_conf.agentx_path,
AGENTX_MASTER_PATH,
sizeof(conf->sc_conf.agentx_path));
}
| SOCKET STRING {
conf->sc_ps->ps_csock.cs_name = $2;
}
;
path : /* nothing */ { $$ = NULL; }
| PATH STRING { $$ = $2; }
context : /* nothing */ { $$ = NULL; }
| CONTEXT STRING { $$ = $2; }
loglevel : STATE CHANGES { $$ = RELAYD_OPT_LOGUPDATE; }
| HOST CHECKS { $$ = RELAYD_OPT_LOGHOSTCHECK; }
| CONNECTION { $$ = (RELAYD_OPT_LOGCON |
RELAYD_OPT_LOGCONERR); }
| CONNECTION ERRORS { $$ = RELAYD_OPT_LOGCONERR; }
;
rdr : REDIRECT STRING {
struct rdr *srv;
conf->sc_conf.flags |= F_NEEDPF;
if (!loadcfg) {
free($2);
YYACCEPT;
}
TAILQ_FOREACH(srv, conf->sc_rdrs, entry)
if (!strcmp(srv->conf.name, $2))
break;
if (srv != NULL) {
yyerror("redirection %s defined twice", $2);
free($2);
YYERROR;
}
if ((srv = calloc(1, sizeof (*srv))) == NULL)
fatal("out of memory");
if (strlcpy(srv->conf.name, $2,
sizeof(srv->conf.name)) >=
sizeof(srv->conf.name)) {
yyerror("redirection name truncated");
free($2);
free(srv);
YYERROR;
}
free($2);
srv->conf.id = ++last_rdr_id;
srv->conf.timeout.tv_sec = RELAY_TIMEOUT;
if (last_rdr_id == INT_MAX) {
yyerror("too many redirections defined");
free(srv);
YYERROR;
}
rdr = srv;
} '{' optnl rdropts_l '}' {
if (rdr->table == NULL) {
yyerror("redirection %s has no table",
rdr->conf.name);
YYERROR;
}
if (TAILQ_EMPTY(&rdr->virts)) {
yyerror("redirection %s has no virtual ip",
rdr->conf.name);
YYERROR;
}
conf->sc_rdrcount++;
if (rdr->backup == NULL) {
rdr->conf.backup_id =
conf->sc_empty_table.conf.id;
rdr->backup = &conf->sc_empty_table;
} else if (rdr->backup->conf.port !=
rdr->table->conf.port) {
yyerror("redirection %s uses two different "
"ports for its table and backup table",
rdr->conf.name);
YYERROR;
}
if (!(rdr->conf.flags & F_DISABLE))
rdr->conf.flags |= F_ADD;
TAILQ_INSERT_TAIL(conf->sc_rdrs, rdr, entry);
tableport = 0;
rdr = NULL;
}
;
rdropts_l : rdropts_l rdroptsl nl
| rdroptsl optnl
;
rdroptsl : forwardmode TO tablespec interface {
if (hashkey != NULL) {
memcpy(&rdr->conf.key,
hashkey, sizeof(rdr->conf.key));
rdr->conf.flags |= F_HASHKEY;
free(hashkey);
hashkey = NULL;
}
switch ($1) {
case FWD_NORMAL:
if ($4 == NULL)
break;
yyerror("superfluous interface");
free($4);
YYERROR;
case FWD_ROUTE:
if ($4 != NULL)
break;
yyerror("missing interface to route to");
free($4);
YYERROR;
case FWD_TRANS:
yyerror("no transparent forward here");
if ($4 != NULL)
free($4);
YYERROR;
}
if ($4 != NULL) {
if (strlcpy($3->conf.ifname, $4,
sizeof($3->conf.ifname)) >=
sizeof($3->conf.ifname)) {
yyerror("interface name truncated");
free($4);
YYERROR;
}
free($4);
}
if ($3->conf.check == CHECK_NOCHECK) {
yyerror("table %s has no check", $3->conf.name);
purge_table(conf, conf->sc_tables, $3);
YYERROR;
}
if (rdr->backup) {
yyerror("only one backup table is allowed");
purge_table(conf, conf->sc_tables, $3);
YYERROR;
}
if (rdr->table) {
rdr->backup = $3;
rdr->conf.backup_id = $3->conf.id;
if (dstmode != rdr->conf.mode) {
yyerror("backup table for %s with "
"different mode", rdr->conf.name);
YYERROR;
}
} else {
rdr->table = $3;
rdr->conf.table_id = $3->conf.id;
rdr->conf.mode = dstmode;
}
$3->conf.fwdmode = $1;
$3->conf.rdrid = rdr->conf.id;
$3->conf.flags |= F_USED;
}
| LISTEN ON STRING redirect_proto port interface pflog {
if (host($3, &rdr->virts,
SRV_MAX_VIRTS, &$5, $6, $4) <= 0) {
yyerror("invalid virtual ip: %s", $3);
free($3);
free($6);
YYERROR;
}
free($3);
free($6);
if (rdr->conf.port == 0)
rdr->conf.port = $5.val[0];
tableport = rdr->conf.port;
if ($7)
rdr->conf.flags |= F_PFLOG;
}
| DISABLE { rdr->conf.flags |= F_DISABLE; }
| STICKYADDR { rdr->conf.flags |= F_STICKY; }
| match PFTAG STRING {
conf->sc_conf.flags |= F_NEEDPF;
if (strlcpy(rdr->conf.tag, $3,
sizeof(rdr->conf.tag)) >=
sizeof(rdr->conf.tag)) {
yyerror("redirection tag name truncated");
free($3);
YYERROR;
}
if ($1)
rdr->conf.flags |= F_MATCH;
free($3);
}
| SESSION TIMEOUT NUMBER {
if ((rdr->conf.timeout.tv_sec = $3) < 0) {
yyerror("invalid timeout: %lld", $3);
YYERROR;
}
if (rdr->conf.timeout.tv_sec > INT_MAX) {
yyerror("timeout too large: %lld", $3);
YYERROR;
}
}
| include
;
match : /* empty */ { $$ = 0; }
| MATCH { $$ = 1; }
;
pflog : /* empty */ { $$ = 0; }
| PFLOG { $$ = 1; }
;
forwardmode : FORWARD { $$ = FWD_NORMAL; }
| ROUTE { $$ = FWD_ROUTE; }
| TRANSPARENT FORWARD { $$ = FWD_TRANS; }
;
table : '<' STRING '>' {
if (strlen($2) >= TABLE_NAME_SIZE) {
yyerror("invalid table name");
free($2);
YYERROR;
}
$$ = $2;
}
;
tabledef : TABLE table {
struct table *tb;
if (!loadcfg) {
free($2);
YYACCEPT;
}
TAILQ_FOREACH(tb, conf->sc_tables, entry)
if (!strcmp(tb->conf.name, $2))
break;
if (tb != NULL) {
yyerror("table %s defined twice", $2);
free($2);
YYERROR;
}
if ((tb = calloc(1, sizeof (*tb))) == NULL)
fatal("out of memory");
if (strlcpy(tb->conf.name, $2,
sizeof(tb->conf.name)) >= sizeof(tb->conf.name)) {
yyerror("table name truncated");
free($2);
YYERROR;
}
free($2);
tb->conf.id = 0; /* will be set later */
bcopy(&conf->sc_conf.timeout, &tb->conf.timeout,
sizeof(struct timeval));
TAILQ_INIT(&tb->hosts);
table = tb;
dstmode = RELAY_DSTMODE_DEFAULT;
} tabledefopts_l {
if (TAILQ_EMPTY(&table->hosts)) {
yyerror("table %s has no hosts",
table->conf.name);
YYERROR;
}
conf->sc_tablecount++;
TAILQ_INSERT_TAIL(conf->sc_tables, table, entry);
}
;
tabledefopts_l : tabledefopts_l tabledefopts
| tabledefopts
;
tabledefopts : DISABLE { table->conf.flags |= F_DISABLE; }
| '{' optnl tablelist_l '}'
;
tablelist_l : tablelist comma tablelist_l
| tablelist optnl
;
tablelist : host {
$1->conf.tableid = table->conf.id;
$1->tablename = table->conf.name;
TAILQ_INSERT_TAIL(&table->hosts, $1, entry);
}
| include
;
tablespec : table {
struct table *tb;
if ((tb = calloc(1, sizeof (*tb))) == NULL)
fatal("out of memory");
if (strlcpy(tb->conf.name, $1,
sizeof(tb->conf.name)) >= sizeof(tb->conf.name)) {
yyerror("table name truncated");
free($1);
YYERROR;
}
free($1);
table = tb;
dstmode = RELAY_DSTMODE_DEFAULT;
hashkey = NULL;
} tableopts_l {
struct table *tb;
if (table->conf.port == 0)
table->conf.port = tableport;
else
table->conf.flags |= F_PORT;
if ((tb = table_inherit(table)) == NULL)
YYERROR;
$$ = tb;
}
;
tableopts_l : tableopts tableopts_l
| tableopts
;
tableopts : CHECK tablecheck
| port {
if ($1.op != PF_OP_EQ) {
yyerror("invalid port");
YYERROR;
}
table->conf.port = $1.val[0];
}
| TIMEOUT timeout {
bcopy(&$2, &table->conf.timeout,
sizeof(struct timeval));
}
| DEMOTE STRING {
table->conf.flags |= F_DEMOTE;
if (strlcpy(table->conf.demote_group, $2,
sizeof(table->conf.demote_group))
>= sizeof(table->conf.demote_group)) {
yyerror("yyparse: demote group name too long");
free($2);
YYERROR;
}
free($2);
if (carp_demote_init(table->conf.demote_group, 1)
== -1) {
yyerror("yyparse: error initializing group "
"'%s'", table->conf.demote_group);
YYERROR;
}
}
| INTERVAL NUMBER {
if ($2 < conf->sc_conf.interval.tv_sec ||
$2 % conf->sc_conf.interval.tv_sec) {
yyerror("table interval must be "
"divisible by global interval");
YYERROR;
}
table->conf.skip_cnt =
($2 / conf->sc_conf.interval.tv_sec) - 1;
}
| MODE dstmode hashkey {
switch ($2) {
case RELAY_DSTMODE_LOADBALANCE:
case RELAY_DSTMODE_HASH:
case RELAY_DSTMODE_SRCHASH:
if (hashkey != NULL) {
yyerror("key already specified");
free(hashkey);
YYERROR;
}
if ((hashkey = calloc(1,
sizeof(*hashkey))) == NULL)
fatal("out of memory");
memcpy(hashkey, &$3.key, sizeof(*hashkey));
break;
default:
if ($3.keyset) {
yyerror("key not supported by mode");
YYERROR;
}
hashkey = NULL;
break;
}
switch ($2) {
case RELAY_DSTMODE_LOADBALANCE:
case RELAY_DSTMODE_HASH:
if (rdr != NULL) {
yyerror("mode not supported "
"for redirections");
YYERROR;
}
/* FALLTHROUGH */
case RELAY_DSTMODE_RANDOM:
case RELAY_DSTMODE_ROUNDROBIN:
case RELAY_DSTMODE_SRCHASH:
dstmode = $2;
break;
case RELAY_DSTMODE_LEASTSTATES:
if (rdr == NULL) {
yyerror("mode not supported "
"for relays");
YYERROR;
}
dstmode = $2;
break;
}
}
;
/* should be in sync with sbin/pfctl/parse.y's hashkey */
hashkey : /* empty */ {
$$.keyset = 0;
$$.key.data[0] = arc4random();
$$.key.data[1] = arc4random();
$$.key.data[2] = arc4random();
$$.key.data[3] = arc4random();
}
| STRING {
/* manual key configuration */
$$.keyset = 1;
if (!strncmp($1, "0x", 2)) {
if (strlen($1) != 34) {
free($1);
yyerror("hex key must be 128 bits "
"(32 hex digits) long");
YYERROR;
}
if (sscanf($1, "0x%8x%8x%8x%8x",
&$$.key.data[0], &$$.key.data[1],
&$$.key.data[2], &$$.key.data[3]) != 4) {
free($1);
yyerror("invalid hex key");
YYERROR;
}
} else {
MD5_CTX context;
MD5Init(&context);
MD5Update(&context, (unsigned char *)$1,
strlen($1));
MD5Final((unsigned char *)$$.key.data,
&context);
HTONL($$.key.data[0]);
HTONL($$.key.data[1]);
HTONL($$.key.data[2]);
HTONL($$.key.data[3]);
}
free($1);
}
;
tablecheck : ICMP { table->conf.check = CHECK_ICMP; }
| TCP { table->conf.check = CHECK_TCP; }
| TLS {
table->conf.check = CHECK_TCP;
conf->sc_conf.flags |= F_TLS;
table->conf.flags |= F_TLS;
}
| http_type STRING hostname CODE NUMBER {
if ($1) {
conf->sc_conf.flags |= F_TLS;
table->conf.flags |= F_TLS;
}
table->conf.check = CHECK_HTTP_CODE;
if ((table->conf.retcode = $5) <= 0) {
yyerror("invalid HTTP code: %lld", $5);
free($2);
free($3);
YYERROR;
}
if (asprintf(&table->sendbuf,
"HEAD %s HTTP/1.%c\r\n%s\r\n",
$2, strlen($3) ? '1' : '0', $3) == -1)
fatal("asprintf");
free($2);
free($3);
if (table->sendbuf == NULL)
fatal("out of memory");
}
| http_type STRING hostname digest {
if ($1) {
conf->sc_conf.flags |= F_TLS;
table->conf.flags |= F_TLS;
}
table->conf.check = CHECK_HTTP_DIGEST;
if (asprintf(&table->sendbuf,
"GET %s HTTP/1.%c\r\n%s\r\n",
$2, strlen($3) ? '1' : '0', $3) == -1)
fatal("asprintf");
free($2);
free($3);
if (table->sendbuf == NULL)
fatal("out of memory");
if (strlcpy(table->conf.digest, $4.digest,
sizeof(table->conf.digest)) >=
sizeof(table->conf.digest)) {
yyerror("digest truncated");
free($4.digest);
YYERROR;
}
table->conf.digest_type = $4.type;
free($4.digest);
}
| SEND sendbuf EXPECT STRING opttls {
table->conf.check = CHECK_SEND_EXPECT;
if ($5) {
conf->sc_conf.flags |= F_TLS;
table->conf.flags |= F_TLS;
}
if (strlcpy(table->conf.exbuf, $4,
sizeof(table->conf.exbuf))
>= sizeof(table->conf.exbuf)) {
yyerror("yyparse: expect buffer truncated");
free($4);
YYERROR;
}
translate_string(table->conf.exbuf);
free($4);
}
| BINARY SEND sendbinbuf EXPECT STRING opttls {
table->conf.check = CHECK_BINSEND_EXPECT;
if ($6) {
conf->sc_conf.flags |= F_TLS;
table->conf.flags |= F_TLS;
}
if (strlen($5) == 0) {
yyerror("empty binary expect data");
free($5);
YYERROR;
}
if (strlcpy(table->conf.exbuf, $5,
sizeof(table->conf.exbuf))
>= sizeof(table->conf.exbuf)) {
yyerror("expect buffer truncated");
free($5);
YYERROR;
}
struct ibuf *ibuf = string2binary($5);
if (ibuf == NULL) {
yyerror("failed in binary expect data buffer");
ibuf_free(ibuf);
free($5);
YYERROR;
}
memcpy(table->conf.exbinbuf, ibuf_data(ibuf),
ibuf_size(ibuf));
ibuf_free(ibuf);
free($5);
}
| SCRIPT STRING {
table->conf.check = CHECK_SCRIPT;
if (strlcpy(table->conf.path, $2,
sizeof(table->conf.path)) >=
sizeof(table->conf.path)) {
yyerror("script path truncated");
free($2);
YYERROR;
}
conf->sc_conf.flags |= F_SCRIPT;
free($2);
}
;
digest : DIGEST STRING
{
switch (strlen($2)) {
case 40:
$$.type = DIGEST_SHA1;
break;
case 32:
$$.type = DIGEST_MD5;
break;
default:
yyerror("invalid http digest");
free($2);
YYERROR;
}
$$.digest = $2;
}
;
optdigest : digest {
$$.digest = $1.digest;
$$.type = $1.type;
}
| STRING {
$$.digest = $1;
$$.type = DIGEST_NONE;
}
;
proto : relay_proto PROTO STRING {
struct protocol *p;
if (!loadcfg) {
free($3);
YYACCEPT;
}
if (strcmp($3, "default") == 0) {
p = &conf->sc_proto_default;
} else {
TAILQ_FOREACH(p, conf->sc_protos, entry)
if (!strcmp(p->name, $3))
break;
}
if (p != NULL) {
yyerror("protocol %s defined twice", $3);
free($3);
YYERROR;
}
if ((p = calloc(1, sizeof (*p))) == NULL)
fatal("out of memory");
if (strlcpy(p->name, $3, sizeof(p->name)) >=
sizeof(p->name)) {
yyerror("protocol name truncated");
free($3);
free(p);
YYERROR;
}
free($3);
p->id = ++last_proto_id;
p->type = $1;
p->tcpflags = TCPFLAG_DEFAULT;
p->tlsflags = TLSFLAG_DEFAULT;
p->tcpbacklog = RELAY_BACKLOG;
p->httpheaderlen = RELAY_DEFHEADERLENGTH;
TAILQ_INIT(&p->rules);
TAILQ_INIT(&p->tlscerts);
(void)strlcpy(p->tlsciphers, TLSCIPHERS_DEFAULT,
sizeof(p->tlsciphers));
(void)strlcpy(p->tlsecdhecurves, TLSECDHECURVES_DEFAULT,
sizeof(p->tlsecdhecurves));
(void)strlcpy(p->tlsdhparams, TLSDHPARAM_DEFAULT,
sizeof(p->tlsdhparams));
if (last_proto_id == INT_MAX) {
yyerror("too many protocols defined");
free(p);
YYERROR;
}
proto = p;
} protopts_n {
conf->sc_protocount++;
if ((proto->tlsflags & TLSFLAG_VERSION) == 0) {
yyerror("invalid TLS protocol");
YYERROR;
}
TAILQ_INSERT_TAIL(conf->sc_protos, proto, entry);
}
;
protopts_n : /* empty */
| '{' '}'
| '{' optnl protopts_l '}'
;
protopts_l : protopts_l protoptsl nl
| protoptsl optnl
;
protoptsl : TLS {
if (!(proto->type == RELAY_PROTO_TCP ||
proto->type == RELAY_PROTO_HTTP)) {
yyerror("can set tls options only for "
"tcp or http protocols");
YYERROR;
}
} tlsflags
| TLS {
if (!(proto->type == RELAY_PROTO_TCP ||
proto->type == RELAY_PROTO_HTTP)) {
yyerror("can set tls options only for "
"tcp or http protocols");
YYERROR;
}
} '{' tlsflags_l '}'
| TCP {
if (!(proto->type == RELAY_PROTO_TCP ||
proto->type == RELAY_PROTO_HTTP)) {
yyerror("can set tcp options only for "
"tcp or http protocols");
YYERROR;
}
} tcpflags
| TCP {
if (!(proto->type == RELAY_PROTO_TCP ||
proto->type == RELAY_PROTO_HTTP)) {
yyerror("can set tcp options only for "
"tcp or http protocols");
YYERROR;
}
} '{' tcpflags_l '}'
| HTTP {
if (proto->type != RELAY_PROTO_HTTP) {
yyerror("can set http options only for "
"http protocol");
YYERROR;
}
} httpflags
| HTTP {
if (proto->type != RELAY_PROTO_HTTP) {
yyerror("can set http options only for "
"http protocol");
YYERROR;
}
} '{' httpflags_l '}'
| RETURN ERROR opteflags { proto->flags |= F_RETURN; }
| RETURN ERROR '{' eflags_l '}' { proto->flags |= F_RETURN; }
| filterrule
| include
;
httpflags_l : httpflags comma httpflags_l
| httpflags
;
httpflags : HEADERLEN NUMBER {
if ($2 < 0 || $2 > RELAY_MAXHEADERLENGTH) {
yyerror("invalid headerlen: %lld", $2);
YYERROR;
}
proto->httpheaderlen = $2;
}
| WEBSOCKETS { proto->httpflags |= HTTPFLAG_WEBSOCKETS; }
| NO WEBSOCKETS { proto->httpflags &= ~HTTPFLAG_WEBSOCKETS; }
;
tcpflags_l : tcpflags comma tcpflags_l
| tcpflags
;
tcpflags : SACK { proto->tcpflags |= TCPFLAG_SACK; }
| NO SACK { proto->tcpflags |= TCPFLAG_NSACK; }
| NODELAY { proto->tcpflags |= TCPFLAG_NODELAY; }
| NO NODELAY { proto->tcpflags |= TCPFLAG_NNODELAY; }
| SPLICE { /* default */ }
| NO SPLICE { proto->tcpflags |= TCPFLAG_NSPLICE; }
| BACKLOG NUMBER {
if ($2 < 0 || $2 > RELAY_MAX_BACKLOG) {
yyerror("invalid backlog: %lld", $2);
YYERROR;
}
proto->tcpbacklog = $2;
}
| SOCKET BUFFER NUMBER {
proto->tcpflags |= TCPFLAG_BUFSIZ;
if ((proto->tcpbufsiz = $3) < 0) {
yyerror("invalid socket buffer size: %lld", $3);
YYERROR;
}
}
| IP STRING NUMBER {
if ($3 < 0) {
yyerror("invalid ttl: %lld", $3);
free($2);
YYERROR;
}
if (strcasecmp("ttl", $2) == 0) {
proto->tcpflags |= TCPFLAG_IPTTL;
proto->tcpipttl = $3;
} else if (strcasecmp("minttl", $2) == 0) {
proto->tcpflags |= TCPFLAG_IPMINTTL;
proto->tcpipminttl = $3;
} else {
yyerror("invalid TCP/IP flag: %s", $2);
free($2);
YYERROR;
}
free($2);
}
;
tlsflags_l : tlsflags comma tlsflags_l
| tlsflags
;
tlsflags : SESSION TICKETS { proto->tickets = 1; }
| NO SESSION TICKETS { proto->tickets = 0; }
| CIPHERS STRING {
if (strlcpy(proto->tlsciphers, $2,
sizeof(proto->tlsciphers)) >=
sizeof(proto->tlsciphers)) {
yyerror("tlsciphers truncated");
free($2);
YYERROR;
}
free($2);
}
| NO EDH {
(void)strlcpy(proto->tlsdhparams, "none",
sizeof(proto->tlsdhparams));
}
| EDH {
(void)strlcpy(proto->tlsdhparams, "auto",
sizeof(proto->tlsdhparams));
}
| EDH PARAMS STRING {
struct tls_config *tls_cfg;
if ((tls_cfg = tls_config_new()) == NULL) {
yyerror("tls_config_new failed");
free($3);
YYERROR;
}
if (tls_config_set_dheparams(tls_cfg, $3) != 0) {
yyerror("tls edh params %s: %s", $3,
tls_config_error(tls_cfg));
tls_config_free(tls_cfg);
free($3);
YYERROR;
}
tls_config_free(tls_cfg);
if (strlcpy(proto->tlsdhparams, $3,
sizeof(proto->tlsdhparams)) >=
sizeof(proto->tlsdhparams)) {
yyerror("tls edh truncated");
free($3);
YYERROR;
}
free($3);
}
| ECDHE STRING {
struct tls_config *tls_cfg;
if ((tls_cfg = tls_config_new()) == NULL) {
yyerror("tls_config_new failed");
free($2);
YYERROR;
}
if (tls_config_set_ecdhecurves(tls_cfg, $2) != 0) {
yyerror("tls ecdhe %s: %s", $2,
tls_config_error(tls_cfg));
tls_config_free(tls_cfg);
free($2);
YYERROR;
}
tls_config_free(tls_cfg);
if (strlcpy(proto->tlsecdhecurves, $2,
sizeof(proto->tlsecdhecurves)) >=
sizeof(proto->tlsecdhecurves)) {
yyerror("tls ecdhe curves truncated");
free($2);
YYERROR;
}
free($2);
}
| CA FILENAME STRING {
if (strlcpy(proto->tlsca, $3,
sizeof(proto->tlsca)) >=
sizeof(proto->tlsca)) {
yyerror("tlsca truncated");
free($3);
YYERROR;
}
free($3);
}
| CA KEY STRING PASSWORD STRING {
if (strlcpy(proto->tlscakey, $3,
sizeof(proto->tlscakey)) >=
sizeof(proto->tlscakey)) {
yyerror("tlscakey truncated");
free($3);
free($5);
YYERROR;
}
if ((proto->tlscapass = strdup($5)) == NULL) {
yyerror("tlscapass");
free($3);
free($5);
YYERROR;
}
free($3);
free($5);
}
| CA CERTIFICATE STRING {
if (strlcpy(proto->tlscacert, $3,
sizeof(proto->tlscacert)) >=
sizeof(proto->tlscacert)) {
yyerror("tlscacert truncated");
free($3);
YYERROR;
}
free($3);
}
| KEYPAIR STRING {
struct keyname *name;
if (strlen($2) >= PATH_MAX) {
yyerror("keypair name too long");
free($2);
YYERROR;
}
if ((name = calloc(1, sizeof(*name))) == NULL) {
yyerror("calloc");
free($2);
YYERROR;
}
name->name = $2;
TAILQ_INSERT_TAIL(&proto->tlscerts, name, entry);
}
| NO flag { proto->tlsflags &= ~($2); }
| flag { proto->tlsflags |= $1; }
;
flag : STRING {
if (strcmp("sslv3", $1) == 0)
$$ = TLSFLAG_SSLV3;
else if (strcmp("tlsv1", $1) == 0)
$$ = TLSFLAG_TLSV1;
else if (strcmp("tlsv1.0", $1) == 0)
$$ = TLSFLAG_TLSV1_0;
else if (strcmp("tlsv1.1", $1) == 0)
$$ = TLSFLAG_TLSV1_1;
else if (strcmp("tlsv1.2", $1) == 0)
$$ = TLSFLAG_TLSV1_2;
else if (strcmp("tlsv1.3", $1) == 0)
$$ = TLSFLAG_TLSV1_3;
else if (strcmp("cipher-server-preference", $1) == 0)
$$ = TLSFLAG_CIPHER_SERVER_PREF;
else if (strcmp("client-renegotiation", $1) == 0)
$$ = TLSFLAG_CLIENT_RENEG;
else {
yyerror("invalid TLS flag: %s", $1);
free($1);
YYERROR;
}
free($1);
}
;
filterrule : action dir quick ruleaf rulesrc ruledst {
if ((rule = calloc(1, sizeof(*rule))) == NULL)
fatal("out of memory");
rule->rule_action = $1;
rule->rule_proto = proto->type;
rule->rule_dir = $2;
rule->rule_flags |= $3;
rule->rule_af = $4;
rule->rule_src.addr = $5.ss;
rule->rule_src.addr_mask = $5.prefixlen;
rule->rule_dst.addr = $6.ss;
rule->rule_dst.addr_mask = $6.prefixlen;
if (RELAY_AF_NEQ(rule->rule_af,
rule->rule_src.addr.ss_family) ||
RELAY_AF_NEQ(rule->rule_af,
rule->rule_dst.addr.ss_family) ||
RELAY_AF_NEQ(rule->rule_src.addr.ss_family,
rule->rule_dst.addr.ss_family)) {
yyerror("address family mismatch");
YYERROR;
}
rulefile = NULL;
} ruleopts_l {
if (rule_add(proto, rule, rulefile) == -1) {
if (rulefile == NULL) {
yyerror("failed to load rule");
} else {
yyerror("failed to load rules from %s",
rulefile);
free(rulefile);
}
rule_free(rule);
free(rule);
YYERROR;
}
if (rulefile)
free(rulefile);
rulefile = NULL;
rule = NULL;
keytype = KEY_TYPE_NONE;
}
;
action : PASS { $$ = RULE_ACTION_PASS; }
| BLOCK { $$ = RULE_ACTION_BLOCK; }
| MATCH { $$ = RULE_ACTION_MATCH; }
;
dir : /* empty */ {
$$ = dir = RELAY_DIR_REQUEST;
}
| REQUEST {
$$ = dir = RELAY_DIR_REQUEST;
}
| RESPONSE {
$$ = dir = RELAY_DIR_RESPONSE;
}
;
quick : /* empty */ { $$ = 0; }
| QUICK { $$ = RULE_FLAG_QUICK; }
;
ruleaf : /* empty */ { $$ = AF_UNSPEC; }
| INET6 { $$ = AF_INET6; }
| INET { $$ = AF_INET; }
;
rulesrc : /* empty */ {
memset(&$$, 0, sizeof($$));
}
| FROM addrprefix {
$$ = $2;
}
;
ruledst : /* empty */ {
memset(&$$, 0, sizeof($$));
}
| TO addrprefix {
$$ = $2;
}
;
ruleopts_l : /* empty */
| ruleopts_t
;
ruleopts_t : ruleopts ruleopts_t
| ruleopts
;
ruleopts : METHOD STRING {
u_int id;
if ((id = relay_httpmethod_byname($2)) ==
HTTP_METHOD_NONE) {
yyerror("unknown HTTP method currently not "
"supported");
free($2);
YYERROR;
}
rule->rule_method = id;
free($2);
}
| COOKIE key_option STRING value {
keytype = KEY_TYPE_COOKIE;
rule->rule_kv[keytype].kv_key = strdup($3);
rule->rule_kv[keytype].kv_option = $2;
rule->rule_kv[keytype].kv_value = (($4 != NULL) ?
strdup($4) : strdup("*"));
if (rule->rule_kv[keytype].kv_key == NULL ||
rule->rule_kv[keytype].kv_value == NULL)
fatal("out of memory");
free($3);
if ($4)
free($4);
rule->rule_kv[keytype].kv_type = keytype;
}
| COOKIE key_option {
keytype = KEY_TYPE_COOKIE;
rule->rule_kv[keytype].kv_option = $2;
rule->rule_kv[keytype].kv_type = keytype;
}
| HEADER key_option STRING value {
keytype = KEY_TYPE_HEADER;
memset(&rule->rule_kv[keytype], 0,
sizeof(rule->rule_kv[keytype]));
rule->rule_kv[keytype].kv_option = $2;
rule->rule_kv[keytype].kv_key = strdup($3);
rule->rule_kv[keytype].kv_value = (($4 != NULL) ?
strdup($4) : strdup("*"));
if (rule->rule_kv[keytype].kv_key == NULL ||
rule->rule_kv[keytype].kv_value == NULL)
fatal("out of memory");
free($3);
if ($4)
free($4);
rule->rule_kv[keytype].kv_type = keytype;
}
| HEADER key_option {
keytype = KEY_TYPE_HEADER;
rule->rule_kv[keytype].kv_option = $2;
rule->rule_kv[keytype].kv_type = keytype;
}
| PATH key_option STRING value {
keytype = KEY_TYPE_PATH;
rule->rule_kv[keytype].kv_option = $2;
rule->rule_kv[keytype].kv_key = strdup($3);
rule->rule_kv[keytype].kv_value = (($4 != NULL) ?
strdup($4) : strdup("*"));
if (rule->rule_kv[keytype].kv_key == NULL ||
rule->rule_kv[keytype].kv_value == NULL)
fatal("out of memory");
free($3);
if ($4)
free($4);
rule->rule_kv[keytype].kv_type = keytype;
}
| PATH key_option {
keytype = KEY_TYPE_PATH;
rule->rule_kv[keytype].kv_option = $2;
rule->rule_kv[keytype].kv_type = keytype;
}
| PATH STRIP NUMBER {
char *strip = NULL;
if ($3 < 0 || $3 > INT_MAX) {
yyerror("invalid strip number");
YYERROR;
}
if (asprintf(&strip, "%lld", $3) <= 0)
fatal("can't parse strip");
keytype = KEY_TYPE_PATH;
rule->rule_kv[keytype].kv_option = KEY_OPTION_STRIP;
rule->rule_kv[keytype].kv_value = strip;
rule->rule_kv[keytype].kv_type = keytype;
}
| QUERYSTR key_option STRING value {
switch ($2) {
case KEY_OPTION_APPEND:
case KEY_OPTION_SET:
case KEY_OPTION_REMOVE:
yyerror("combining query type and the given "
"option is not supported");
free($3);
if ($4)
free($4);
YYERROR;
break;
}
keytype = KEY_TYPE_QUERY;
rule->rule_kv[keytype].kv_option = $2;
rule->rule_kv[keytype].kv_key = strdup($3);
rule->rule_kv[keytype].kv_value = (($4 != NULL) ?
strdup($4) : strdup("*"));
if (rule->rule_kv[keytype].kv_key == NULL ||
rule->rule_kv[keytype].kv_value == NULL)
fatal("out of memory");
free($3);
if ($4)
free($4);
rule->rule_kv[keytype].kv_type = keytype;
}
| QUERYSTR key_option {
switch ($2) {
case KEY_OPTION_APPEND:
case KEY_OPTION_SET:
case KEY_OPTION_REMOVE:
yyerror("combining query type and the given "
"option is not supported");
YYERROR;
break;
}
keytype = KEY_TYPE_QUERY;
rule->rule_kv[keytype].kv_option = $2;
rule->rule_kv[keytype].kv_type = keytype;
}
| URL key_option optdigest value {
switch ($2) {
case KEY_OPTION_APPEND:
case KEY_OPTION_SET:
case KEY_OPTION_REMOVE:
yyerror("combining url type and the given "
"option is not supported");
free($3.digest);
free($4);
YYERROR;
break;
}
keytype = KEY_TYPE_URL;
rule->rule_kv[keytype].kv_option = $2;
rule->rule_kv[keytype].kv_key = strdup($3.digest);
rule->rule_kv[keytype].kv_digest = $3.type;
rule->rule_kv[keytype].kv_value = (($4 != NULL) ?
strdup($4) : strdup("*"));
if (rule->rule_kv[keytype].kv_key == NULL ||
rule->rule_kv[keytype].kv_value == NULL)
fatal("out of memory");
free($3.digest);
if ($4)
free($4);
rule->rule_kv[keytype].kv_type = keytype;
}
| URL key_option {
switch ($2) {
case KEY_OPTION_APPEND:
case KEY_OPTION_SET:
case KEY_OPTION_REMOVE:
yyerror("combining url type and the given "
"option is not supported");
YYERROR;
break;
}
keytype = KEY_TYPE_URL;
rule->rule_kv[keytype].kv_option = $2;
rule->rule_kv[keytype].kv_type = keytype;
}
| FORWARD TO table {
if (table_findbyname(conf, $3) == NULL) {
yyerror("undefined forward table");
free($3);
YYERROR;
}
if (strlcpy(rule->rule_tablename, $3,
sizeof(rule->rule_tablename)) >=
sizeof(rule->rule_tablename)) {
yyerror("invalid forward table name");
free($3);
YYERROR;
}
free($3);
}
| TAG STRING {
tag = tag_name2id($2);
if (rule->rule_tag) {
yyerror("tag already defined");
free($2);
rule_free(rule);
free(rule);
YYERROR;
}
if (tag == 0) {
yyerror("invalid tag");
free($2);
rule_free(rule);
free(rule);
YYERROR;
}
rule->rule_tag = tag;
if (strlcpy(rule->rule_tagname, $2,
sizeof(rule->rule_tagname)) >=
sizeof(rule->rule_tagname)) {
yyerror("tag truncated");
free($2);
rule_free(rule);
free(rule);
YYERROR;
}
free($2);
}
| NO TAG {
if (tag == 0) {
yyerror("no tag defined");
YYERROR;
}
rule->rule_tag = -1;
memset(rule->rule_tagname, 0,
sizeof(rule->rule_tagname));
}
| TAGGED STRING {
tagged = tag_name2id($2);
if (rule->rule_tagged) {
yyerror("tagged already defined");
free($2);
rule_free(rule);
free(rule);
YYERROR;
}
if (tagged == 0) {
yyerror("invalid tag");
free($2);
rule_free(rule);
free(rule);
YYERROR;
}
rule->rule_tagged = tagged;
if (strlcpy(rule->rule_taggedname, $2,
sizeof(rule->rule_taggedname)) >=
sizeof(rule->rule_taggedname)) {
yyerror("tagged truncated");
free($2);
rule_free(rule);
free(rule);
YYERROR;
}
free($2);
}
| LABEL STRING {
label = label_name2id($2);
if (rule->rule_label) {
yyerror("label already defined");
free($2);
rule_free(rule);
free(rule);
YYERROR;
}
if (label == 0) {
yyerror("invalid label");
free($2);
rule_free(rule);
free(rule);
YYERROR;
}
rule->rule_label = label;
if (strlcpy(rule->rule_labelname, $2,
sizeof(rule->rule_labelname)) >=
sizeof(rule->rule_labelname)) {
yyerror("label truncated");
free($2);
rule_free(rule);
free(rule);
YYERROR;
}
free($2);
}
| NO LABEL {
if (label == 0) {
yyerror("no label defined");
YYERROR;
}
rule->rule_label = -1;
memset(rule->rule_labelname, 0,
sizeof(rule->rule_labelname));
}
| FILENAME STRING value {
if (rulefile != NULL) {
yyerror("only one file per rule supported");
free($2);
free($3);
rule_free(rule);
free(rule);
YYERROR;
}
if ($3) {
if ((rule->rule_kv[keytype].kv_value =
strdup($3)) == NULL)
fatal("out of memory");
free($3);
} else
rule->rule_kv[keytype].kv_value = NULL;
rulefile = $2;
}
;
value : /* empty */ { $$ = NULL; }
| VALUE STRING { $$ = $2; }
;
key_option : /* empty */ { $$ = KEY_OPTION_NONE; }
| APPEND { $$ = KEY_OPTION_APPEND; }
| SET { $$ = KEY_OPTION_SET; }
| REMOVE { $$ = KEY_OPTION_REMOVE; }
| HASH { $$ = KEY_OPTION_HASH; }
| LOG { $$ = KEY_OPTION_LOG; }
;
relay : RELAY STRING {
struct relay *r;
if (!loadcfg) {
free($2);
YYACCEPT;
}
if ((r = calloc(1, sizeof (*r))) == NULL)
fatal("out of memory");
TAILQ_INIT(&relays);
if (strlcpy(r->rl_conf.name, $2,
sizeof(r->rl_conf.name)) >=
sizeof(r->rl_conf.name)) {
yyerror("relay name truncated");
free($2);
free(r);
YYERROR;
}
free($2);
if (relay_id(r) == -1) {
yyerror("too many relays defined");
free(r);
YYERROR;
}
r->rl_conf.timeout.tv_sec = RELAY_TIMEOUT;
r->rl_proto = NULL;
r->rl_conf.proto = EMPTY_ID;
r->rl_conf.dstretry = 0;
r->rl_tls_ca_fd = -1;
r->rl_tls_cacert_fd = -1;
TAILQ_INIT(&r->rl_tables);
if (last_relay_id == INT_MAX) {
yyerror("too many relays defined");
free(r);
YYERROR;
}
dstmode = RELAY_DSTMODE_DEFAULT;
rlay = r;
} '{' optnl relayopts_l '}' {
struct relay *r;
struct relay_config *rlconf = &rlay->rl_conf;
struct keyname *name;
if (relay_findbyname(conf, rlconf->name) != NULL ||
relay_findbyaddr(conf, rlconf) != NULL) {
yyerror("relay %s or listener defined twice",
rlconf->name);
YYERROR;
}
if (rlay->rl_conf.ss.ss_family == AF_UNSPEC) {
yyerror("relay %s has no listener",
rlay->rl_conf.name);
YYERROR;
}
if ((rlay->rl_conf.flags & (F_NATLOOK|F_DIVERT)) ==
(F_NATLOOK|F_DIVERT)) {
yyerror("relay %s with conflicting nat lookup "
"and peer options", rlay->rl_conf.name);
YYERROR;
}
if ((rlay->rl_conf.flags & (F_NATLOOK|F_DIVERT)) == 0 &&
rlay->rl_conf.dstss.ss_family == AF_UNSPEC &&
TAILQ_EMPTY(&rlay->rl_tables)) {
yyerror("relay %s has no target, rdr, "
"or table", rlay->rl_conf.name);
YYERROR;
}
if (rlay->rl_conf.proto == EMPTY_ID) {
rlay->rl_proto = &conf->sc_proto_default;
rlay->rl_conf.proto = conf->sc_proto_default.id;
}
if (TAILQ_EMPTY(&rlay->rl_proto->tlscerts) &&
relay_load_certfiles(conf, rlay, NULL) == -1) {
yyerror("cannot load certificates for relay %s",
rlay->rl_conf.name);
YYERROR;
}
TAILQ_FOREACH(name, &rlay->rl_proto->tlscerts, entry) {
if (relay_load_certfiles(conf,
rlay, name->name) == -1) {
yyerror("cannot load keypair %s"
" for relay %s", name->name,
rlay->rl_conf.name);
YYERROR;
}
}
conf->sc_relaycount++;
SPLAY_INIT(&rlay->rl_sessions);
TAILQ_INSERT_TAIL(conf->sc_relays, rlay, rl_entry);
tableport = 0;
while ((r = TAILQ_FIRST(&relays)) != NULL) {
TAILQ_REMOVE(&relays, r, rl_entry);
if (relay_inherit(rlay, r) == NULL) {
YYERROR;
}
}
rlay = NULL;
}
;
relayopts_l : relayopts_l relayoptsl nl
| relayoptsl optnl
;
relayoptsl : LISTEN ON STRING port opttls {
struct addresslist al;
struct address *h;
struct relay *r;
if (rlay->rl_conf.ss.ss_family != AF_UNSPEC) {
if ((r = calloc(1, sizeof (*r))) == NULL)
fatal("out of memory");
TAILQ_INSERT_TAIL(&relays, r, rl_entry);
} else
r = rlay;
if ($4.op != PF_OP_EQ) {
yyerror("invalid port");
free($3);
YYERROR;
}
TAILQ_INIT(&al);
if (host($3, &al, 1, &$4, NULL, -1) <= 0) {
yyerror("invalid listen ip: %s", $3);
free($3);
YYERROR;
}
free($3);
h = TAILQ_FIRST(&al);
bcopy(&h->ss, &r->rl_conf.ss, sizeof(r->rl_conf.ss));
r->rl_conf.port = h->port.val[0];
if ($5) {
r->rl_conf.flags |= F_TLS;
conf->sc_conf.flags |= F_TLS;
}
tableport = h->port.val[0];
host_free(&al);
}
| forwardmode opttlsclient TO forwardspec dstaf {
rlay->rl_conf.fwdmode = $1;
if ($1 == FWD_ROUTE) {
yyerror("no route for relays");
YYERROR;
}
if ($2) {
rlay->rl_conf.flags |= F_TLSCLIENT;
conf->sc_conf.flags |= F_TLSCLIENT;
}
}
| SESSION TIMEOUT NUMBER {
if ((rlay->rl_conf.timeout.tv_sec = $3) < 0) {
yyerror("invalid timeout: %lld", $3);
YYERROR;
}
if (rlay->rl_conf.timeout.tv_sec > INT_MAX) {
yyerror("timeout too large: %lld", $3);
YYERROR;
}
}
| PROTO STRING {
struct protocol *p;
if (rlay->rl_conf.proto != EMPTY_ID) {
yyerror("more than one protocol specified");
YYERROR;
}
TAILQ_FOREACH(p, conf->sc_protos, entry)
if (!strcmp(p->name, $2))
break;
if (p == NULL) {
yyerror("no such protocol: %s", $2);
free($2);
YYERROR;
}
p->flags |= F_USED;
rlay->rl_conf.proto = p->id;
rlay->rl_proto = p;
free($2);
}
| DISABLE { rlay->rl_conf.flags |= F_DISABLE; }
| include
;
forwardspec : STRING port retry {
struct addresslist al;
struct address *h;
if (rlay->rl_conf.dstss.ss_family != AF_UNSPEC) {
yyerror("relay %s target or redirection "
"already specified", rlay->rl_conf.name);
free($1);
YYERROR;
}
if ($2.op != PF_OP_EQ) {
yyerror("invalid port");
free($1);
YYERROR;
}
TAILQ_INIT(&al);
if (host($1, &al, 1, &$2, NULL, -1) <= 0) {
yyerror("invalid forward ip: %s", $1);
free($1);
YYERROR;
}
free($1);
h = TAILQ_FIRST(&al);
bcopy(&h->ss, &rlay->rl_conf.dstss,
sizeof(rlay->rl_conf.dstss));
rlay->rl_conf.dstport = h->port.val[0];
rlay->rl_conf.dstretry = $3;
host_free(&al);
}
| NAT LOOKUP retry {
conf->sc_conf.flags |= F_NEEDPF;
rlay->rl_conf.flags |= F_NATLOOK;
rlay->rl_conf.dstretry = $3;
}
| DESTINATION retry {
conf->sc_conf.flags |= F_NEEDPF;
rlay->rl_conf.flags |= F_DIVERT;
rlay->rl_conf.dstretry = $2;
}
| tablespec {
struct relay_table *rlt;
if ((rlt = calloc(1, sizeof(*rlt))) == NULL) {
yyerror("failed to allocate table reference");
YYERROR;
}
rlt->rlt_table = $1;
rlt->rlt_table->conf.flags |= F_USED;
rlt->rlt_mode = dstmode;
rlt->rlt_flags = F_USED;
if (!TAILQ_EMPTY(&rlay->rl_tables))
rlt->rlt_flags |= F_BACKUP;
if (hashkey != NULL &&
(rlay->rl_conf.flags & F_HASHKEY) == 0) {
memcpy(&rlay->rl_conf.hashkey,
hashkey, sizeof(rlay->rl_conf.hashkey));
rlay->rl_conf.flags |= F_HASHKEY;
}
free(hashkey);
hashkey = NULL;
TAILQ_INSERT_TAIL(&rlay->rl_tables, rlt, rlt_entry);
}
;
dstmode : /* empty */ { $$ = RELAY_DSTMODE_DEFAULT; }
| LOADBALANCE { $$ = RELAY_DSTMODE_LOADBALANCE; }
| ROUNDROBIN { $$ = RELAY_DSTMODE_ROUNDROBIN; }
| HASH { $$ = RELAY_DSTMODE_HASH; }
| LEASTSTATES { $$ = RELAY_DSTMODE_LEASTSTATES; }
| SRCHASH { $$ = RELAY_DSTMODE_SRCHASH; }
| RANDOM { $$ = RELAY_DSTMODE_RANDOM; }
;
router : ROUTER STRING {
struct router *rt = NULL;
if (!loadcfg) {
free($2);
YYACCEPT;
}
conf->sc_conf.flags |= F_NEEDRT;
TAILQ_FOREACH(rt, conf->sc_rts, rt_entry)
if (!strcmp(rt->rt_conf.name, $2))
break;
if (rt != NULL) {
yyerror("router %s defined twice", $2);
free($2);
YYERROR;
}
if ((rt = calloc(1, sizeof (*rt))) == NULL)
fatal("out of memory");
if (strlcpy(rt->rt_conf.name, $2,
sizeof(rt->rt_conf.name)) >=
sizeof(rt->rt_conf.name)) {
yyerror("router name truncated");
free(rt);
YYERROR;
}
free($2);
rt->rt_conf.id = ++last_rt_id;
if (last_rt_id == INT_MAX) {
yyerror("too many routers defined");
free(rt);
YYERROR;
}
TAILQ_INIT(&rt->rt_netroutes);
router = rt;
tableport = -1;
} '{' optnl routeopts_l '}' {
if (!router->rt_conf.nroutes) {
yyerror("router %s without routes",
router->rt_conf.name);
free(router);
router = NULL;
YYERROR;
}
conf->sc_routercount++;
TAILQ_INSERT_TAIL(conf->sc_rts, router, rt_entry);
router = NULL;
tableport = 0;
}
;
routeopts_l : routeopts_l routeoptsl nl
| routeoptsl optnl
;
routeoptsl : ROUTE addrprefix {
struct netroute *nr;
if (router->rt_conf.af == AF_UNSPEC)
router->rt_conf.af = $2.ss.ss_family;
else if (router->rt_conf.af != $2.ss.ss_family) {
yyerror("router %s address family mismatch",
router->rt_conf.name);
YYERROR;
}
if ((nr = calloc(1, sizeof(*nr))) == NULL)
fatal("out of memory");
nr->nr_conf.id = ++last_nr_id;
if (last_nr_id == INT_MAX) {
yyerror("too many routes defined");
free(nr);
YYERROR;
}
nr->nr_conf.prefixlen = $2.prefixlen;
nr->nr_conf.routerid = router->rt_conf.id;
nr->nr_router = router;
bcopy(&$2.ss, &nr->nr_conf.ss, sizeof($2.ss));
router->rt_conf.nroutes++;
conf->sc_routecount++;
TAILQ_INSERT_TAIL(&router->rt_netroutes, nr, nr_entry);
TAILQ_INSERT_TAIL(conf->sc_routes, nr, nr_route);
}
| FORWARD TO tablespec {
free(hashkey);
hashkey = NULL;
if (router->rt_gwtable) {
yyerror("router %s table already specified",
router->rt_conf.name);
purge_table(conf, conf->sc_tables, $3);
YYERROR;
}
router->rt_gwtable = $3;
router->rt_gwtable->conf.flags |= F_USED;
router->rt_conf.gwtable = $3->conf.id;
router->rt_conf.gwport = $3->conf.port;
}
| RTABLE NUMBER {
if (router->rt_conf.rtable) {
yyerror("router %s rtable already specified",
router->rt_conf.name);
YYERROR;
}
if ($2 < 0 || $2 > RT_TABLEID_MAX) {
yyerror("invalid rtable id %lld", $2);
YYERROR;
}
router->rt_conf.rtable = $2;
}
| RTLABEL STRING {
if (strlcpy(router->rt_conf.label, $2,
sizeof(router->rt_conf.label)) >=
sizeof(router->rt_conf.label)) {
yyerror("route label truncated");
free($2);
YYERROR;
}
free($2);
}
| DISABLE { rlay->rl_conf.flags |= F_DISABLE; }
| include
;
dstaf : /* empty */ {
rlay->rl_conf.dstaf.ss_family = AF_UNSPEC;
}
| INET {
rlay->rl_conf.dstaf.ss_family = AF_INET;
}
| INET6 STRING {
struct sockaddr_in6 *sin6;
sin6 = (struct sockaddr_in6 *)&rlay->rl_conf.dstaf;
if (inet_pton(AF_INET6, $2, &sin6->sin6_addr) == -1) {
yyerror("invalid ipv6 address %s", $2);
free($2);
YYERROR;
}
free($2);
sin6->sin6_family = AF_INET6;
sin6->sin6_len = sizeof(*sin6);
}
;
interface : /* empty */ { $$ = NULL; }
| INTERFACE STRING { $$ = $2; }
;
host : address {
if ((hst = calloc(1, sizeof(*(hst)))) == NULL)
fatal("out of memory");
if (strlcpy(hst->conf.name, $1.name,
sizeof(hst->conf.name)) >= sizeof(hst->conf.name)) {
yyerror("host name truncated");
free(hst);
YYERROR;
}
bcopy(&$1.ss, &hst->conf.ss, sizeof($1.ss));
hst->conf.id = 0; /* will be set later */
SLIST_INIT(&hst->children);
} opthostflags {
$$ = hst;
hst = NULL;
}
;
opthostflags : /* empty */
| hostflags_l
;
hostflags_l : hostflags hostflags_l
| hostflags
;
hostflags : RETRY NUMBER {
if (hst->conf.retry) {
yyerror("retry value already set");
YYERROR;
}
if ($2 < 0) {
yyerror("invalid retry value: %lld\n", $2);
YYERROR;
}
hst->conf.retry = $2;
}
| PARENT NUMBER {
if (hst->conf.parentid) {
yyerror("parent value already set");
YYERROR;
}
if ($2 < 0) {
yyerror("invalid parent value: %lld\n", $2);
YYERROR;
}
hst->conf.parentid = $2;
}
| PRIORITY NUMBER {
if (hst->conf.priority) {
yyerror("priority already set");
YYERROR;
}
if ($2 < 0 || $2 > RTP_MAX) {
yyerror("invalid priority value: %lld\n", $2);
YYERROR;
}
hst->conf.priority = $2;
}
| IP TTL NUMBER {
if (hst->conf.ttl) {
yyerror("ttl value already set");
YYERROR;
}
if ($3 < 0) {
yyerror("invalid ttl value: %lld\n", $3);
YYERROR;
}
hst->conf.ttl = $3;
}
;
address : STRING {
struct address *h;
struct addresslist al;
if (strlcpy($$.name, $1,
sizeof($$.name)) >= sizeof($$.name)) {
yyerror("host name truncated");
free($1);
YYERROR;
}
TAILQ_INIT(&al);
if (host($1, &al, 1, NULL, NULL, -1) <= 0) {
yyerror("invalid host %s", $1);
free($1);
YYERROR;
}
free($1);
h = TAILQ_FIRST(&al);
memcpy(&$$.ss, &h->ss, sizeof($$.ss));
host_free(&al);
}
;
addrprefix : address '/' NUMBER {
$$ = $1;
if (($$.ss.ss_family == AF_INET &&
($3 > 32 || $3 < 0)) ||
($$.ss.ss_family == AF_INET6 &&
($3 > 128 || $3 < 0))) {
yyerror("invalid prefixlen %lld", $3);
YYERROR;
}
$$.prefixlen = $3;
}
| address {
$$ = $1;
if ($$.ss.ss_family == AF_INET)
$$.prefixlen = 32;
else if ($$.ss.ss_family == AF_INET6)
$$.prefixlen = 128;
}
;
retry : /* empty */ { $$ = 0; }
| RETRY NUMBER {
if (($$ = $2) < 0) {
yyerror("invalid retry value: %lld\n", $2);
YYERROR;
}
}
;
timeout : NUMBER
{
if ($1 < 0) {
yyerror("invalid timeout: %lld\n", $1);
YYERROR;
}
$$.tv_sec = $1 / 1000;
$$.tv_usec = ($1 % 1000) * 1000;
}
;
comma : ','
| nl
| /* empty */
;
optnl : '\n' optnl
|
;
nl : '\n' optnl
;
%%
struct keywords {
const char *k_name;
int k_val;
};
int
yyerror(const char *fmt, ...)
{
va_list ap;
char *msg;
file->errors++;
va_start(ap, fmt);
if (vasprintf(&msg, fmt, ap) == -1)
fatalx("yyerror vasprintf");
va_end(ap);
logit(LOG_CRIT, "%s:%d: %s", file->name, yylval.lineno, msg);
free(msg);
return (0);
}
int
kw_cmp(const void *k, const void *e)
{
return (strcmp(k, ((const struct keywords *)e)->k_name));
}
int
lookup(char *s)
{
/* this has to be sorted always */
static const struct keywords keywords[] = {
{ "agentx", AGENTX },
{ "append", APPEND },
{ "backlog", BACKLOG },
{ "backup", BACKUP },
{ "binary", BINARY },
{ "block", BLOCK },
{ "buffer", BUFFER },
{ "ca", CA },
{ "cache", CACHE },
{ "cert", CERTIFICATE },
{ "changes", CHANGES },
{ "check", CHECK },
{ "checks", CHECKS },
{ "ciphers", CIPHERS },
{ "code", CODE },
{ "connection", CONNECTION },
{ "context", CONTEXT },
{ "cookie", COOKIE },
{ "demote", DEMOTE },
{ "destination", DESTINATION },
{ "digest", DIGEST },
{ "disable", DISABLE },
{ "ecdhe", ECDHE },
{ "edh", EDH },
{ "error", ERROR },
{ "errors", ERRORS },
{ "expect", EXPECT },
{ "external", EXTERNAL },
{ "file", FILENAME },
{ "forward", FORWARD },
{ "from", FROM },
{ "hash", HASH },
{ "header", HEADER },
{ "headerlen", HEADERLEN },
{ "host", HOST },
{ "http", HTTP },
{ "icmp", ICMP },
{ "include", INCLUDE },
{ "inet", INET },
{ "inet6", INET6 },
{ "interface", INTERFACE },
{ "interval", INTERVAL },
{ "ip", IP },
{ "key", KEY },
{ "keypair", KEYPAIR },
{ "label", LABEL },
{ "least-states", LEASTSTATES },
{ "listen", LISTEN },
{ "loadbalance", LOADBALANCE },
{ "log", LOG },
{ "lookup", LOOKUP },
{ "match", MATCH },
{ "method", METHOD },
{ "mode", MODE },
{ "nat", NAT },
{ "no", NO },
{ "nodelay", NODELAY },
{ "nothing", NOTHING },
{ "on", ON },
{ "params", PARAMS },
{ "parent", PARENT },
{ "pass", PASS },
{ "password", PASSWORD },
{ "path", PATH },
{ "pflog", PFLOG },
{ "pftag", PFTAG },
{ "port", PORT },
{ "prefork", PREFORK },
{ "priority", PRIORITY },
{ "protocol", PROTO },
{ "query", QUERYSTR },
{ "quick", QUICK },
{ "random", RANDOM },
{ "real", REAL },
{ "redirect", REDIRECT },
{ "relay", RELAY },
{ "remove", REMOVE },
{ "request", REQUEST },
{ "response", RESPONSE },
{ "retry", RETRY },
{ "return", RETURN },
{ "roundrobin", ROUNDROBIN },
{ "route", ROUTE },
{ "router", ROUTER },
{ "rtable", RTABLE },
{ "rtlabel", RTLABEL },
{ "sack", SACK },
{ "script", SCRIPT },
{ "send", SEND },
{ "session", SESSION },
{ "set", SET },
{ "socket", SOCKET },
{ "source-hash", SRCHASH },
{ "splice", SPLICE },
{ "state", STATE },
{ "sticky-address", STICKYADDR },
{ "strip", STRIP },
{ "style", STYLE },
{ "table", TABLE },
{ "tag", TAG },
{ "tagged", TAGGED },
{ "tcp", TCP },
{ "tickets", TICKETS },
{ "timeout", TIMEOUT },
{ "tls", TLS },
{ "to", TO },
{ "transparent", TRANSPARENT },
{ "ttl", TTL },
{ "url", URL },
{ "value", VALUE },
{ "websockets", WEBSOCKETS },
{ "with", WITH }
};
const struct keywords *p;
p = bsearch(s, keywords, sizeof(keywords)/sizeof(keywords[0]),
sizeof(keywords[0]), kw_cmp);
if (p)
return (p->k_val);
else
return (STRING);
}
#define START_EXPAND 1
#define DONE_EXPAND 2
static int expanding;
int
igetc(void)
{
int c;
while (1) {
if (file->ungetpos > 0)
c = file->ungetbuf[--file->ungetpos];
else c = getc(file->stream);
if (c == START_EXPAND)
expanding = 1;
else if (c == DONE_EXPAND)
expanding = 0;
else
break;
}
return (c);
}
int
lgetc(int quotec)
{
int c, next;
if (quotec) {
if ((c = igetc()) == EOF) {
yyerror("reached end of file while parsing "
"quoted string");
if (file == topfile || popfile() == EOF)
return (EOF);
return (quotec);
}
return (c);
}
while ((c = igetc()) == '\\') {
next = igetc();
if (next != '\n') {
c = next;
break;
}
yylval.lineno = file->lineno;
file->lineno++;
}
if (c == EOF) {
/*
* Fake EOL when hit EOF for the first time. This gets line
* count right if last line in included file is syntactically
* invalid and has no newline.
*/
if (file->eof_reached == 0) {
file->eof_reached = 1;
return ('\n');
}
while (c == EOF) {
if (file == topfile || popfile() == EOF)
return (EOF);
c = igetc();
}
}
return (c);
}
void
lungetc(int c)
{
if (c == EOF)
return;
if (file->ungetpos >= file->ungetsize) {
void *p = reallocarray(file->ungetbuf, file->ungetsize, 2);
if (p == NULL)
err(1, "%s", __func__);
file->ungetbuf = p;
file->ungetsize *= 2;
}
file->ungetbuf[file->ungetpos++] = c;
}
int
findeol(void)
{
int c;
/* skip to either EOF or the first real EOL */
while (1) {
c = lgetc(0);
if (c == '\n') {
file->lineno++;
break;
}
if (c == EOF)
break;
}
return (ERROR);
}
int
yylex(void)
{
char buf[8096];
char *p, *val;
int quotec, next, c;
int token;
top:
p = buf;
while ((c = lgetc(0)) == ' ' || c == '\t')
; /* nothing */
yylval.lineno = file->lineno;
if (c == '#')
while ((c = lgetc(0)) != '\n' && c != EOF)
; /* nothing */
if (c == '$' && !expanding) {
while (1) {
if ((c = lgetc(0)) == EOF)
return (0);
if (p + 1 >= buf + sizeof(buf) - 1) {
yyerror("string too long");
return (findeol());
}
if (isalnum(c) || c == '_') {
*p++ = c;
continue;
}
*p = '\0';
lungetc(c);
break;
}
val = symget(buf);
if (val == NULL) {
yyerror("macro '%s' not defined", buf);
return (findeol());
}
p = val + strlen(val) - 1;
lungetc(DONE_EXPAND);
while (p >= val) {
lungetc((unsigned char)*p);
p--;
}
lungetc(START_EXPAND);
goto top;
}
switch (c) {
case '\'':
case '"':
quotec = c;
while (1) {
if ((c = lgetc(quotec)) == EOF)
return (0);
if (c == '\n') {
file->lineno++;
continue;
} else if (c == '\\') {
if ((next = lgetc(quotec)) == EOF)
return (0);
if (next == quotec || next == ' ' ||
next == '\t')
c = next;
else if (next == '\n') {
file->lineno++;
continue;
} else
lungetc(next);
} else if (c == quotec) {
*p = '\0';
break;
} else if (c == '\0') {
yyerror("syntax error");
return (findeol());
}
if (p + 1 >= buf + sizeof(buf) - 1) {
yyerror("string too long");
return (findeol());
}
*p++ = c;
}
yylval.v.string = strdup(buf);
if (yylval.v.string == NULL)
err(1, "%s", __func__);
return (STRING);
}
#define allowed_to_end_number(x) \
(isspace(x) || x == ')' || x ==',' || x == '/' || x == '}' || x == '=')
if (c == '-' || isdigit(c)) {
do {
*p++ = c;
if ((size_t)(p-buf) >= sizeof(buf)) {
yyerror("string too long");
return (findeol());
}
} while ((c = lgetc(0)) != EOF && isdigit(c));
lungetc(c);
if (p == buf + 1 && buf[0] == '-')
goto nodigits;
if (c == EOF || allowed_to_end_number(c)) {
const char *errstr = NULL;
*p = '\0';
yylval.v.number = strtonum(buf, LLONG_MIN,
LLONG_MAX, &errstr);
if (errstr) {
yyerror("\"%s\" invalid number: %s",
buf, errstr);
return (findeol());
}
return (NUMBER);
} else {
nodigits:
while (p > buf + 1)
lungetc((unsigned char)*--p);
c = (unsigned char)*--p;
if (c == '-')
return (c);
}
}
#define allowed_in_string(x) \
(isalnum(x) || (ispunct(x) && x != '(' && x != ')' && \
x != '{' && x != '}' && x != '<' && x != '>' && \
x != '!' && x != '=' && x != '#' && \
x != ',' && x != '/'))
if (isalnum(c) || c == ':' || c == '_') {
do {
*p++ = c;
if ((size_t)(p-buf) >= sizeof(buf)) {
yyerror("string too long");
return (findeol());
}
} while ((c = lgetc(0)) != EOF && (allowed_in_string(c)));
lungetc(c);
*p = '\0';
if ((token = lookup(buf)) == STRING)
if ((yylval.v.string = strdup(buf)) == NULL)
err(1, "%s", __func__);
return (token);
}
if (c == '\n') {
yylval.lineno = file->lineno;
file->lineno++;
}
if (c == EOF)
return (0);
return (c);
}
int
check_file_secrecy(int fd, const char *fname)
{
struct stat st;
if (fstat(fd, &st)) {
log_warn("cannot stat %s", fname);
return (-1);
}
if (st.st_uid != 0 && st.st_uid != getuid()) {
log_warnx("%s: owner not root or current user", fname);
return (-1);
}
if (st.st_mode & (S_IWGRP | S_IXGRP | S_IRWXO)) {
log_warnx("%s: group writable or world read/writable", fname);
return (-1);
}
return (0);
}
struct file *
pushfile(const char *name, int secret)
{
struct file *nfile;
if ((nfile = calloc(1, sizeof(struct file))) == NULL) {
log_warn("%s", __func__);
return (NULL);
}
if ((nfile->name = strdup(name)) == NULL) {
log_warn("%s", __func__);
free(nfile);
return (NULL);
}
if ((nfile->stream = fopen(nfile->name, "r")) == NULL) {
log_warn("%s: %s", __func__, nfile->name);
free(nfile->name);
free(nfile);
return (NULL);
} else if (secret &&
check_file_secrecy(fileno(nfile->stream), nfile->name)) {
fclose(nfile->stream);
free(nfile->name);
free(nfile);
return (NULL);
}
nfile->lineno = TAILQ_EMPTY(&files) ? 1 : 0;
nfile->ungetsize = 16;
nfile->ungetbuf = malloc(nfile->ungetsize);
if (nfile->ungetbuf == NULL) {
log_warn("%s", __func__);
fclose(nfile->stream);
free(nfile->name);
free(nfile);
return (NULL);
}
TAILQ_INSERT_TAIL(&files, nfile, entry);
return (nfile);
}
int
popfile(void)
{
struct file *prev;
if ((prev = TAILQ_PREV(file, files, entry)) != NULL)
prev->errors += file->errors;
TAILQ_REMOVE(&files, file, entry);
fclose(file->stream);
free(file->name);
free(file->ungetbuf);
free(file);
file = prev;
return (file ? 0 : EOF);
}
int
parse_config(const char *filename, struct relayd *x_conf)
{
struct sym *sym, *next;
conf = x_conf;
if (config_init(conf) == -1) {
log_warn("%s: cannot initialize configuration", __func__);
return (-1);
}
errors = 0;
if ((file = pushfile(filename, 0)) == NULL)
return (-1);
topfile = file;
setservent(1);
yyparse();
errors = file->errors;
while (popfile() != EOF)
;
endservent();
endprotoent();
/* Free macros */
TAILQ_FOREACH_SAFE(sym, &symhead, entry, next) {
if (!sym->persist) {
free(sym->nam);
free(sym->val);
TAILQ_REMOVE(&symhead, sym, entry);
free(sym);
}
}
return (errors ? -1 : 0);
}
int
load_config(const char *filename, struct relayd *x_conf)
{
struct sym *sym, *next;
struct table *nexttb;
struct host *h, *ph;
struct relay_table *rlt;
conf = x_conf;
conf->sc_conf.flags = 0;
loadcfg = 1;
errors = 0;
last_host_id = last_table_id = last_rdr_id = last_proto_id =
last_relay_id = last_rt_id = last_nr_id = 0;
rdr = NULL;
table = NULL;
rlay = NULL;
proto = NULL;
router = NULL;
if ((file = pushfile(filename, 0)) == NULL)
return (-1);
topfile = file;
setservent(1);
yyparse();
errors = file->errors;
while (popfile() != EOF)
;
endservent();
endprotoent();
/* Free macros and check which have not been used. */
for (sym = TAILQ_FIRST(&symhead); sym != NULL; sym = next) {
next = TAILQ_NEXT(sym, entry);
if ((conf->sc_conf.opts & RELAYD_OPT_VERBOSE) && !sym->used)
fprintf(stderr, "warning: macro '%s' not "
"used\n", sym->nam);
if (!sym->persist) {
free(sym->nam);
free(sym->val);
TAILQ_REMOVE(&symhead, sym, entry);
free(sym);
}
}
if (TAILQ_EMPTY(conf->sc_rdrs) &&
TAILQ_EMPTY(conf->sc_relays) &&
TAILQ_EMPTY(conf->sc_rts)) {
log_warnx("no actions, nothing to do");
errors++;
}
/* Cleanup relay list to inherit */
while ((rlay = TAILQ_FIRST(&relays)) != NULL) {
TAILQ_REMOVE(&relays, rlay, rl_entry);
while ((rlt = TAILQ_FIRST(&rlay->rl_tables))) {
TAILQ_REMOVE(&rlay->rl_tables, rlt, rlt_entry);
free(rlt);
}
free(rlay);
}
if (timercmp(&conf->sc_conf.timeout, &conf->sc_conf.interval, >=)) {
log_warnx("global timeout exceeds interval");
errors++;
}
/* Verify that every table is used */
for (table = TAILQ_FIRST(conf->sc_tables); table != NULL;
table = nexttb) {
nexttb = TAILQ_NEXT(table, entry);
if (table->conf.port == 0) {
TAILQ_REMOVE(conf->sc_tables, table, entry);
while ((h = TAILQ_FIRST(&table->hosts)) != NULL) {
TAILQ_REMOVE(&table->hosts, h, entry);
free(h);
}
if (table->sendbuf != NULL)
free(table->sendbuf);
if (table->sendbinbuf != NULL)
ibuf_free(table->sendbinbuf);
free(table);
continue;
}
TAILQ_FOREACH(h, &table->hosts, entry) {
if (h->conf.parentid) {
ph = host_find(conf, h->conf.parentid);
/* Validate the parent id */
if (h->conf.id == h->conf.parentid ||
ph == NULL || ph->conf.parentid)
ph = NULL;
if (ph == NULL) {
log_warnx("host parent id %d invalid",
h->conf.parentid);
errors++;
} else
SLIST_INSERT_HEAD(&ph->children,
h, child);
}
}
if (!(table->conf.flags & F_USED)) {
log_warnx("unused table: %s", table->conf.name);
errors++;
}
if (timercmp(&table->conf.timeout,
&conf->sc_conf.interval, >=)) {
log_warnx("table timeout exceeds interval: %s",
table->conf.name);
errors++;
}
}
/* Verify that every non-default protocol is used */
TAILQ_FOREACH(proto, conf->sc_protos, entry) {
if (!(proto->flags & F_USED)) {
log_warnx("unused protocol: %s", proto->name);
}
}
return (errors ? -1 : 0);
}
int
symset(const char *nam, const char *val, int persist)
{
struct sym *sym;
TAILQ_FOREACH(sym, &symhead, entry) {
if (strcmp(nam, sym->nam) == 0)
break;
}
if (sym != NULL) {
if (sym->persist == 1)
return (0);
else {
free(sym->nam);
free(sym->val);
TAILQ_REMOVE(&symhead, sym, entry);
free(sym);
}
}
if ((sym = calloc(1, sizeof(*sym))) == NULL)
return (-1);
sym->nam = strdup(nam);
if (sym->nam == NULL) {
free(sym);
return (-1);
}
sym->val = strdup(val);
if (sym->val == NULL) {
free(sym->nam);
free(sym);
return (-1);
}
sym->used = 0;
sym->persist = persist;
TAILQ_INSERT_TAIL(&symhead, sym, entry);
return (0);
}
int
cmdline_symset(char *s)
{
char *sym, *val;
int ret;
if ((val = strrchr(s, '=')) == NULL)
return (-1);
sym = strndup(s, val - s);
if (sym == NULL)
errx(1, "%s: strndup", __func__);
ret = symset(sym, val + 1, 1);
free(sym);
return (ret);
}
char *
symget(const char *nam)
{
struct sym *sym;
TAILQ_FOREACH(sym, &symhead, entry) {
if (strcmp(nam, sym->nam) == 0) {
sym->used = 1;
return (sym->val);
}
}
return (NULL);
}
struct address *
host_ip(const char *s)
{
struct addrinfo hints, *res;
struct address *h = NULL;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_DGRAM; /*dummy*/
hints.ai_flags = AI_NUMERICHOST;
if (getaddrinfo(s, "0", &hints, &res) == 0) {
if (res->ai_family == AF_INET ||
res->ai_family == AF_INET6) {
if ((h = calloc(1, sizeof(*h))) == NULL)
fatal(NULL);
memcpy(&h->ss, res->ai_addr, res->ai_addrlen);
}
freeaddrinfo(res);
}
return (h);
}
int
host_dns(const char *s, struct addresslist *al, int max,
struct portrange *port, const char *ifname, int ipproto)
{
struct addrinfo hints, *res0, *res;
int error, cnt = 0;
struct address *h;
if ((cnt = host_if(s, al, max, port, ifname, ipproto)) != 0)
return (cnt);
bzero(&hints, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_DGRAM; /* DUMMY */
hints.ai_flags = AI_ADDRCONFIG;
error = getaddrinfo(s, NULL, &hints, &res0);
if (error == EAI_AGAIN || error == EAI_NODATA || error == EAI_NONAME)
return (0);
if (error) {
log_warnx("%s: could not parse \"%s\": %s", __func__, s,
gai_strerror(error));
return (-1);
}
for (res = res0; res && cnt < max; res = res->ai_next) {
if (res->ai_family != AF_INET &&
res->ai_family != AF_INET6)
continue;
if ((h = calloc(1, sizeof(*h))) == NULL)
fatal(__func__);
if (port != NULL)
bcopy(port, &h->port, sizeof(h->port));
if (ifname != NULL) {
if (strlcpy(h->ifname, ifname, sizeof(h->ifname)) >=
sizeof(h->ifname))
log_warnx("%s: interface name truncated",
__func__);
freeaddrinfo(res0);
free(h);
return (-1);
}
if (ipproto != -1)
h->ipproto = ipproto;
memcpy(&h->ss, res->ai_addr, res->ai_addrlen);
TAILQ_INSERT_HEAD(al, h, entry);
cnt++;
}
if (cnt == max && res) {
log_warnx("%s: %s resolves to more than %d hosts", __func__,
s, max);
}
freeaddrinfo(res0);
return (cnt);
}
int
host_if(const char *s, struct addresslist *al, int max,
struct portrange *port, const char *ifname, int ipproto)
{
struct ifaddrs *ifap, *p;
struct sockaddr_in *sain;
struct sockaddr_in6 *sin6;
struct address *h;
int cnt = 0, af;
if (getifaddrs(&ifap) == -1)
fatal("getifaddrs");
/* First search for IPv4 addresses */
af = AF_INET;
nextaf:
for (p = ifap; p != NULL && cnt < max; p = p->ifa_next) {
if (p->ifa_addr == NULL ||
p->ifa_addr->sa_family != af ||
(strcmp(s, p->ifa_name) != 0 &&
!is_if_in_group(p->ifa_name, s)))
continue;
if ((h = calloc(1, sizeof(*h))) == NULL)
fatal("calloc");
if (port != NULL)
bcopy(port, &h->port, sizeof(h->port));
if (ifname != NULL) {
if (strlcpy(h->ifname, ifname, sizeof(h->ifname)) >=
sizeof(h->ifname))
log_warnx("%s: interface name truncated",
__func__);
freeifaddrs(ifap);
free(h);
return (-1);
}
if (ipproto != -1)
h->ipproto = ipproto;
h->ss.ss_family = af;
if (af == AF_INET) {
sain = (struct sockaddr_in *)&h->ss;
sain->sin_len = sizeof(struct sockaddr_in);
sain->sin_addr.s_addr = ((struct sockaddr_in *)
p->ifa_addr)->sin_addr.s_addr;
} else {
sin6 = (struct sockaddr_in6 *)&h->ss;
sin6->sin6_len = sizeof(struct sockaddr_in6);
memcpy(&sin6->sin6_addr, &((struct sockaddr_in6 *)
p->ifa_addr)->sin6_addr, sizeof(struct in6_addr));
sin6->sin6_scope_id = ((struct sockaddr_in6 *)
p->ifa_addr)->sin6_scope_id;
}
TAILQ_INSERT_HEAD(al, h, entry);
cnt++;
}
if (af == AF_INET) {
/* Next search for IPv6 addresses */
af = AF_INET6;
goto nextaf;
}
if (cnt > max) {
log_warnx("%s: %s resolves to more than %d hosts", __func__,
s, max);
}
freeifaddrs(ifap);
return (cnt);
}
int
host(const char *s, struct addresslist *al, int max,
struct portrange *port, const char *ifname, int ipproto)
{
struct address *h;
if ((h = host_ip(s)) == NULL)
return (host_dns(s, al, max, port, ifname, ipproto));
if (port != NULL)
bcopy(port, &h->port, sizeof(h->port));
if (ifname != NULL) {
if (strlcpy(h->ifname, ifname, sizeof(h->ifname)) >=
sizeof(h->ifname)) {
log_warnx("%s: interface name truncated",
__func__);
free(h);
return (-1);
}
}
if (ipproto != -1)
h->ipproto = ipproto;
TAILQ_INSERT_HEAD(al, h, entry);
return (1);
}
void
host_free(struct addresslist *al)
{
struct address *h;
while ((h = TAILQ_FIRST(al)) != NULL) {
TAILQ_REMOVE(al, h, entry);
free(h);
}
}
struct table *
table_inherit(struct table *tb)
{
char pname[TABLE_NAME_SIZE + 6];
struct host *h, *dsth;
struct table *dsttb, *oldtb;
/* Get the table or table template */
if ((dsttb = table_findbyname(conf, tb->conf.name)) == NULL) {
yyerror("unknown table %s", tb->conf.name);
goto fail;
}
if (dsttb->conf.port != 0)
fatal("invalid table"); /* should not happen */
if (tb->conf.port == 0) {
yyerror("invalid port");
goto fail;
}
/* Check if a matching table already exists */
if (snprintf(pname, sizeof(pname), "%s:%u",
tb->conf.name, ntohs(tb->conf.port)) >= (int)sizeof(pname)) {
yyerror("invalid table name");
goto fail;
}
if (strlcpy(tb->conf.name, pname, sizeof(tb->conf.name)) >=
sizeof(tb->conf.name)) {
yyerror("invalid table mame");
goto fail;
}
if ((oldtb = table_findbyconf(conf, tb)) != NULL) {
purge_table(conf, NULL, tb);
return (oldtb);
}
/* Create a new table */
tb->conf.id = ++last_table_id;
if (last_table_id == INT_MAX) {
yyerror("too many tables defined");
goto fail;
}
tb->conf.flags |= dsttb->conf.flags;
/* Inherit global table options */
if (tb->conf.timeout.tv_sec == 0 && tb->conf.timeout.tv_usec == 0)
bcopy(&dsttb->conf.timeout, &tb->conf.timeout,
sizeof(struct timeval));
/* Copy the associated hosts */
TAILQ_INIT(&tb->hosts);
TAILQ_FOREACH(dsth, &dsttb->hosts, entry) {
if ((h = (struct host *)
calloc(1, sizeof (*h))) == NULL)
fatal("out of memory");
bcopy(dsth, h, sizeof(*h));
h->conf.id = ++last_host_id;
if (last_host_id == INT_MAX) {
yyerror("too many hosts defined");
free(h);
goto fail;
}
h->conf.tableid = tb->conf.id;
h->tablename = tb->conf.name;
SLIST_INIT(&h->children);
TAILQ_INSERT_TAIL(&tb->hosts, h, entry);
TAILQ_INSERT_TAIL(&conf->sc_hosts, h, globalentry);
}
conf->sc_tablecount++;
TAILQ_INSERT_TAIL(conf->sc_tables, tb, entry);
return (tb);
fail:
purge_table(conf, NULL, tb);
return (NULL);
}
int
relay_id(struct relay *rl)
{
rl->rl_conf.id = ++last_relay_id;
if (last_relay_id == INT_MAX)
return (-1);
return (0);
}
struct relay *
relay_inherit(struct relay *ra, struct relay *rb)
{
struct relay_config rc;
struct relay_table *rta, *rtb;
bcopy(&rb->rl_conf, &rc, sizeof(rc));
bcopy(ra, rb, sizeof(*rb));
bcopy(&rc.ss, &rb->rl_conf.ss, sizeof(rb->rl_conf.ss));
rb->rl_conf.port = rc.port;
rb->rl_conf.flags =
(ra->rl_conf.flags & ~F_TLS) | (rc.flags & F_TLS);
if (!(rb->rl_conf.flags & F_TLS)) {
rb->rl_tls_cacert_fd = -1;
rb->rl_tls_ca_fd = -1;
}
TAILQ_INIT(&rb->rl_tables);
if (relay_id(rb) == -1) {
yyerror("too many relays defined");
goto err;
}
if (snprintf(rb->rl_conf.name, sizeof(rb->rl_conf.name), "%s%u:%u",
ra->rl_conf.name, rb->rl_conf.id, ntohs(rc.port)) >=
(int)sizeof(rb->rl_conf.name)) {
yyerror("invalid relay name");
goto err;
}
if (relay_findbyname(conf, rb->rl_conf.name) != NULL ||
relay_findbyaddr(conf, &rb->rl_conf) != NULL) {
yyerror("relay %s or listener defined twice",
rb->rl_conf.name);
goto err;
}
if (relay_load_certfiles(conf, rb, NULL) == -1) {
yyerror("cannot load certificates for relay %s",
rb->rl_conf.name);
goto err;
}
TAILQ_FOREACH(rta, &ra->rl_tables, rlt_entry) {
if ((rtb = calloc(1, sizeof(*rtb))) == NULL) {
yyerror("cannot allocate relay table");
goto err;
}
rtb->rlt_table = rta->rlt_table;
rtb->rlt_mode = rta->rlt_mode;
rtb->rlt_flags = rta->rlt_flags;
TAILQ_INSERT_TAIL(&rb->rl_tables, rtb, rlt_entry);
}
conf->sc_relaycount++;
SPLAY_INIT(&rlay->rl_sessions);
TAILQ_INSERT_TAIL(conf->sc_relays, rb, rl_entry);
return (rb);
err:
while ((rtb = TAILQ_FIRST(&rb->rl_tables))) {
TAILQ_REMOVE(&rb->rl_tables, rtb, rlt_entry);
free(rtb);
}
free(rb);
return (NULL);
}
int
getservice(char *n)
{
struct servent *s;
const char *errstr;
long long llval;
llval = strtonum(n, 0, UINT16_MAX, &errstr);
if (errstr) {
s = getservbyname(n, "tcp");
if (s == NULL)
s = getservbyname(n, "udp");
if (s == NULL) {
yyerror("unknown port %s", n);
return (-1);
}
return (s->s_port);
}
return (htons((u_short)llval));
}
int
is_if_in_group(const char *ifname, const char *groupname)
{
unsigned int len;
struct ifgroupreq ifgr;
struct ifg_req *ifg;
int s;
int ret = 0;
if ((s = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
err(1, "socket");
memset(&ifgr, 0, sizeof(ifgr));
if (strlcpy(ifgr.ifgr_name, ifname, IFNAMSIZ) >= IFNAMSIZ)
err(1, "IFNAMSIZ");
if (ioctl(s, SIOCGIFGROUP, (caddr_t)&ifgr) == -1) {
if (errno == EINVAL || errno == ENOTTY)
goto end;
err(1, "SIOCGIFGROUP");
}
len = ifgr.ifgr_len;
ifgr.ifgr_groups = calloc(len / sizeof(struct ifg_req),
sizeof(struct ifg_req));
if (ifgr.ifgr_groups == NULL)
err(1, "getifgroups");
if (ioctl(s, SIOCGIFGROUP, (caddr_t)&ifgr) == -1)
err(1, "SIOCGIFGROUP");
ifg = ifgr.ifgr_groups;
for (; ifg && len >= sizeof(struct ifg_req); ifg++) {
len -= sizeof(struct ifg_req);
if (strcmp(ifg->ifgrq_group, groupname) == 0) {
ret = 1;
break;
}
}
free(ifgr.ifgr_groups);
end:
close(s);
return (ret);
}