2 *****************************************************************************
6 * Author: Damien S. Stuart, Michael Rash
8 * Purpose: Fwknop routines for managing pf firewall rules.
10 * Copyright 2011 Damien Stuart (dstuart@dstuart.org),
11 * Michael Rash (mbr@cipherdyne.org)
13 * License (GNU Public License):
15 * This program is free software; you can redistribute it and/or
16 * modify it under the terms of the GNU General Public License
17 * as published by the Free Software Foundation; either version 2
18 * of the License, or (at your option) any later version.
20 * This program is distributed in the hope that it will be useful,
21 * but WITHOUT ANY WARRANTY; without even the implied warranty of
22 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 * GNU General Public License for more details.
25 * You should have received a copy of the GNU General Public License
26 * along with this program; if not, write to the Free Software
27 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
30 *****************************************************************************
32 #include "fwknopd_common.h"
42 static struct fw_config fwc;
43 static char cmd_buf[CMD_BUFSIZE];
44 static char err_buf[CMD_BUFSIZE];
45 static char cmd_out[STANDARD_CMD_OUT_BUFSIZE];
48 zero_cmd_buffers(void)
50 memset(cmd_buf, 0x0, CMD_BUFSIZE);
51 memset(err_buf, 0x0, CMD_BUFSIZE);
52 memset(cmd_out, 0x0, STANDARD_CMD_OUT_BUFSIZE);
55 /* Print all firewall rules currently instantiated by the running fwknopd
59 fw_dump_rules(const fko_srv_options_t *opts)
63 printf("Listing fwknopd pf rules...\n");
67 /* Create the list command for active rules
69 snprintf(cmd_buf, CMD_BUFSIZE-1, "%s " PF_LIST_ANCHOR_RULES_ARGS,
70 opts->fw_config->fw_command,
71 opts->fw_config->anchor
74 printf("\nActive Rules in PF anchor '%s':\n", opts->fw_config->anchor);
75 res = system(cmd_buf);
77 /* Expect full success on this */
78 if(! EXTCMD_IS_SUCCESS(res))
80 log_msg(LOG_ERR, "Error %i from cmd:'%s': %s", res, cmd_buf, err_buf);
87 /* Check to see if the fwknop anchor is linked into the main policy. If not,
88 * any rules added/deleted by fwknopd will have no effect on real traffic.
91 anchor_active(const fko_srv_options_t *opts)
95 char anchor_search_str[MAX_PF_ANCHOR_SEARCH_LEN] = {0};
97 /* Build our anchor search string
99 snprintf(anchor_search_str, MAX_PF_ANCHOR_SEARCH_LEN-1, "%s%s\" ",
100 "anchor \"", opts->fw_config->anchor);
104 snprintf(cmd_buf, CMD_BUFSIZE-1, "%s " PF_LIST_ALL_RULES_ARGS,
105 opts->fw_config->fw_command
108 res = run_extcmd(cmd_buf, cmd_out, STANDARD_CMD_OUT_BUFSIZE, 0);
110 if(!EXTCMD_IS_SUCCESS(res))
112 log_msg(LOG_ERR, "Error %i from cmd:'%s': %s", res, cmd_buf, cmd_out);
116 /* first check for the anchor at the very first rule position
118 if (strncmp(cmd_out, anchor_search_str, strlen(anchor_search_str)) != 0)
120 anchor_search_str[0] = '\0';
122 /* look for the anchor in the middle of the rule set, but make sure
123 * it appears only after a newline
125 snprintf(anchor_search_str, MAX_PF_ANCHOR_SEARCH_LEN-1, "%s%s\" ",
126 "\nanchor \"", opts->fw_config->anchor);
128 ndx = strstr(cmd_out, anchor_search_str);
138 delete_all_anchor_rules(const fko_srv_options_t *opts)
144 snprintf(cmd_buf, CMD_BUFSIZE-1, "%s " PF_DEL_ALL_ANCHOR_RULES,
149 res = run_extcmd(cmd_buf, err_buf, CMD_BUFSIZE, 0);
151 /* Expect full success on this */
152 if(! EXTCMD_IS_SUCCESS(res))
153 log_msg(LOG_ERR, "Error %i from cmd:'%s': %s", res, cmd_buf, err_buf);
159 fw_config_init(fko_srv_options_t *opts)
161 memset(&fwc, 0x0, sizeof(struct fw_config));
163 /* Set our firewall exe command path
165 strlcpy(fwc.fw_command, opts->config[CONF_FIREWALL_EXE], MAX_PATH_LEN);
167 /* Set the PF anchor name
169 strlcpy(fwc.anchor, opts->config[CONF_PF_ANCHOR_NAME], MAX_PF_ANCHOR_LEN);
171 /* Let us find it via our opts struct as well.
173 opts->fw_config = &fwc;
179 fw_initialize(const fko_srv_options_t *opts)
182 if (! anchor_active(opts))
184 fprintf(stderr, "Warning: the fwknop anchor is not active in the pf policy\n");
188 /* Delete any existing rules in the fwknop anchor
190 delete_all_anchor_rules(opts);
196 fw_cleanup(const fko_srv_options_t *opts)
198 delete_all_anchor_rules(opts);
202 /****************************************************************************/
204 /* Rule Processing - Create an access request...
207 process_spa_request(const fko_srv_options_t *opts, const acc_stanza_t *acc, spa_data_t *spadat)
209 char new_rule[MAX_PF_NEW_RULE_LEN];
210 char write_cmd[CMD_BUFSIZE];
212 FILE *pfctl_fd = NULL;
214 acc_port_list_t *port_list = NULL;
215 acc_port_list_t *ple;
217 unsigned int fst_proto;
218 unsigned int fst_port;
224 /* Parse and expand our access message.
226 expand_acc_port_list(&port_list, spadat->spa_message_remain);
228 /* Start at the top of the proto-port list...
232 /* Remember the first proto/port combo in case we need them
233 * for NAT access requests.
235 fst_proto = ple->proto;
236 fst_port = ple->port;
238 /* Set our expire time value.
241 exp_ts = now + spadat->fw_access_timeout;
243 /* For straight access requests, we currently support multiple proto/port
246 if(spadat->message_type == FKO_ACCESS_MSG
247 || spadat->message_type == FKO_CLIENT_TIMEOUT_ACCESS_MSG)
249 /* Create an access command for each proto/port for the source ip.
255 snprintf(cmd_buf, CMD_BUFSIZE-1, "%s " PF_LIST_ANCHOR_RULES_ARGS,
256 opts->fw_config->fw_command,
257 opts->fw_config->anchor
260 /* Cache the current anchor rule set
262 res = run_extcmd(cmd_buf, cmd_out, STANDARD_CMD_OUT_BUFSIZE, 0);
264 /* Build the new rule string
266 memset(new_rule, 0x0, MAX_PF_NEW_RULE_LEN);
267 snprintf(new_rule, MAX_PF_NEW_RULE_LEN-1, PF_ADD_RULE_ARGS "\n",
274 if (strlen(cmd_out) + strlen(new_rule) < STANDARD_CMD_OUT_BUFSIZE)
276 /* We add the rule to the running policy
278 strlcat(cmd_out, new_rule, STANDARD_CMD_OUT_BUFSIZE);
280 memset(write_cmd, 0x0, CMD_BUFSIZE);
282 snprintf(write_cmd, CMD_BUFSIZE-1, "%s " PF_WRITE_ANCHOR_RULES_ARGS,
283 opts->fw_config->fw_command,
284 opts->fw_config->anchor
287 if ((pfctl_fd = popen(write_cmd, "w")) == NULL)
289 log_msg(LOG_WARNING, "Could not execute command: %s",
294 if (fwrite(cmd_out, strlen(cmd_out), 1, pfctl_fd) == 1)
296 log_msg(LOG_INFO, "Added Rule for %s, %s expires at %u",
298 spadat->spa_message_remain,
304 /* Reset the next expected expire time for this chain if it
307 if(fwc.next_expire < now || exp_ts < fwc.next_expire)
308 fwc.next_expire = exp_ts;
311 log_msg(LOG_WARNING, "Could not write rule to pf anchor");
317 /* We don't have enough room to add the new firewall rule,
318 * so throw a warning and bail. Once some of the existing
319 * rules are expired the user will once again be able to gain
320 * access. Note that we don't expect to really ever hit this
321 * limit because of STANDARD_CMD_OUT_BUFSIZE is quite a number
324 log_msg(LOG_WARNING, "Max anchor rules reached, try again later.");
334 /* No other SPA request modes are supported yet.
336 if(spadat->message_type == FKO_LOCAL_NAT_ACCESS_MSG
337 || spadat->message_type == FKO_CLIENT_TIMEOUT_LOCAL_NAT_ACCESS_MSG)
339 log_msg(LOG_WARNING, "Local NAT requests are not currently supported.");
341 else if(spadat->message_type == FKO_NAT_ACCESS_MSG
342 || spadat->message_type == FKO_CLIENT_TIMEOUT_NAT_ACCESS_MSG)
344 log_msg(LOG_WARNING, "Forwarding/NAT requests are not currently supported.");
353 /* Iterate over the configure firewall access chains and purge expired
357 check_firewall_rules(const fko_srv_options_t *opts)
360 char anchor_rules_copy[STANDARD_CMD_OUT_BUFSIZE];
361 char write_cmd[CMD_BUFSIZE];
362 char *ndx, *tmp_mark, *tmp_ndx, *newline_tmp_ndx;
364 time_t now, rule_exp, min_exp=0;
365 int i=0, res=0, anchor_ndx=0, is_delete=0;
367 FILE *pfctl_fd = NULL;
369 /* If we have not yet reached our expected next expire
372 if(fwc.next_expire == 0)
377 if (fwc.next_expire > now)
382 /* There should be a rule to delete. Get the current list of
383 * rules and delete the ones that are expired.
385 snprintf(cmd_buf, CMD_BUFSIZE-1, "%s " PF_LIST_ANCHOR_RULES_ARGS,
386 opts->fw_config->fw_command,
387 opts->fw_config->anchor
390 res = run_extcmd(cmd_buf, cmd_out, STANDARD_CMD_OUT_BUFSIZE, 0);
392 if(!EXTCMD_IS_SUCCESS(res))
394 log_msg(LOG_ERR, "Error %i from cmd:'%s': %s", res, cmd_buf, cmd_out);
398 /* Find the first _exp_ string (if any).
400 ndx = strstr(cmd_out, EXPIRE_COMMENT_PREFIX);
404 /* we did not find an expected rule.
407 "Did not find expire comment in rules list %i.\n", i);
412 memset(anchor_rules_copy, 0x0, STANDARD_CMD_OUT_BUFSIZE);
414 /* Walk the list and process rules as needed.
418 /* Jump forward and extract the timestamp
420 ndx += strlen(EXPIRE_COMMENT_PREFIX);
422 /* remember this spot for when we look for the next
427 strlcpy(exp_str, ndx, 11);
428 rule_exp = (time_t)atoll(exp_str);
432 /* We are going to delete this rule, and because we rebuild the
433 * PF anchor to include all rules that haven't expired, to delete
434 * this rule we just skip to the next one.
436 log_msg(LOG_INFO, "Deleting rule with expire time of %u.", rule_exp);
438 if (fwc.active_rules > 0)
445 /* The rule has not expired, so copy it into the anchor string that
446 * lists current rules and will be used to feed
447 * 'pfctl -a <anchor> -f -'.
450 /* back up to the previous newline or the beginning of the rules
454 while(--tmp_ndx > cmd_out)
465 /* may sure the rule begins with the string "pass", and make sure
466 * it ends with a newline. Bail if either test fails.
468 if (strlen(tmp_ndx) <= strlen("pass")
469 || strncmp(tmp_ndx, "pass", strlen("pass")) != 0)
474 newline_tmp_ndx = tmp_ndx;
475 while (*newline_tmp_ndx != '\n' && *newline_tmp_ndx != '\0')
480 if (*newline_tmp_ndx != '\n')
483 /* copy the whole rule to the next newline (includes the expiration
486 while (*tmp_ndx != '\n' && *tmp_ndx != '\0'
487 && anchor_ndx < STANDARD_CMD_OUT_BUFSIZE)
489 anchor_rules_copy[anchor_ndx] = *tmp_ndx;
493 anchor_rules_copy[anchor_ndx] = '\n';
496 /* Track the minimum future rule expire time.
499 min_exp = (min_exp < rule_exp) ? min_exp : rule_exp;
502 /* Push our tracking index forward beyond (just processed) _exp_
503 * string so we can continue to the next rule in the list.
505 ndx = strstr(tmp_mark, EXPIRE_COMMENT_PREFIX);
511 /* We re-instantiate the anchor rules with the new rules string that
512 * has the rule(s) deleted. If there isn't at least one "pass" rule,
513 * then we just flush the anchor.
516 if (strlen(anchor_rules_copy) > strlen("pass")
517 && strncmp(anchor_rules_copy, "pass", strlen("pass")) == 0)
519 memset(write_cmd, 0x0, CMD_BUFSIZE);
521 snprintf(write_cmd, CMD_BUFSIZE-1, "%s " PF_WRITE_ANCHOR_RULES_ARGS,
522 opts->fw_config->fw_command,
523 opts->fw_config->anchor
526 if ((pfctl_fd = popen(write_cmd, "w")) == NULL)
528 log_msg(LOG_WARNING, "Could not execute command: %s",
533 if (fwrite(anchor_rules_copy, strlen(anchor_rules_copy), 1, pfctl_fd) != 1)
535 log_msg(LOG_WARNING, "Could not write rules to pf anchor");
541 delete_all_anchor_rules(opts);
546 /* Set the next pending expire time accordingly. 0 if there are no
547 * more rules, or whatever the next expected (min_exp) time will be.
549 if(fwc.active_rules < 1)
552 fwc.next_expire = min_exp;
557 #endif /* FIREWALL_PF */