1
0
mirror of https://github.com/openbsd/src.git synced 2024-12-22 07:27:59 -08:00
openbsd-src/usr.sbin/relayd/parse.y
tb 92388deed9 relayd: add support for client certificates
This feature has been requested many times over the years. Various patches
were provided by Asherah Connor, Rivo Nurges, Markus Läll and maybe others.
These patches always stalled for various reasons.

From Sören Tempel, mostly based on Asherah's latest patch.

ok florian tb
2024-10-28 19:56:18 +00:00

3533 lines
78 KiB
Plaintext

/* $OpenBSD: parse.y,v 1.258 2024/10/28 19:56:18 tb 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 CLIENT
%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: %s", $2);
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: %s",
$3);
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);
}
| CLIENT CA STRING {
if (strlcpy(proto->tlsclientca, $3,
sizeof(proto->tlsclientca)) >=
sizeof(proto->tlsclientca)) {
yyerror("tlsclientca truncated");
free($3);
YYERROR;
}
free($3);
}
| 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;
r->rl_tls_client_ca_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 },
{ "client", CLIENT },
{ "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;
rb->rl_tls_client_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);
}