/* ***************************************************************************** * * File: fw_util_pf.c * * Author: Damien S. Stuart, Michael Rash * * Purpose: Fwknop routines for managing pf firewall rules. * * Copyright 2011 Damien Stuart (dstuart@dstuart.org), * Michael Rash (mbr@cipherdyne.org) * * License (GNU Public License): * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * USA * ***************************************************************************** */ #include "fwknopd_common.h" #if FIREWALL_PF #include "fw_util.h" #include "utils.h" #include "log_msg.h" #include "extcmd.h" #include "access.h" static struct fw_config fwc; static char cmd_buf[CMD_BUFSIZE]; static char err_buf[CMD_BUFSIZE]; static char cmd_out[STANDARD_CMD_OUT_BUFSIZE]; static void zero_cmd_buffers(void) { memset(cmd_buf, 0x0, CMD_BUFSIZE); memset(err_buf, 0x0, CMD_BUFSIZE); memset(cmd_out, 0x0, STANDARD_CMD_OUT_BUFSIZE); } /* Print all firewall rules currently instantiated by the running fwknopd * daemon to stdout. */ int fw_dump_rules(const fko_srv_options_t * const opts) { int res, got_err = 0; printf("Listing fwknopd pf rules...\n"); zero_cmd_buffers(); /* Create the list command for active rules */ snprintf(cmd_buf, CMD_BUFSIZE-1, "%s " PF_LIST_ANCHOR_RULES_ARGS, opts->fw_config->fw_command, opts->fw_config->anchor ); printf("\nActive Rules in PF anchor '%s':\n", opts->fw_config->anchor); res = system(cmd_buf); /* Expect full success on this */ if(! EXTCMD_IS_SUCCESS(res)) { log_msg(LOG_ERR, "Error %i from cmd:'%s': %s", res, cmd_buf, err_buf); got_err++; } return(got_err); } /* Check to see if the fwknop anchor is linked into the main policy. If not, * any rules added/deleted by fwknopd will have no effect on real traffic. */ static int anchor_active(const fko_srv_options_t *opts) { int res = 0; char anchor_search_str[MAX_PF_ANCHOR_SEARCH_LEN] = {0}; /* Build our anchor search string */ snprintf(anchor_search_str, MAX_PF_ANCHOR_SEARCH_LEN-1, "%s\n", opts->fw_config->anchor); zero_cmd_buffers(); snprintf(cmd_buf, CMD_BUFSIZE-1, "%s " PF_ANCHOR_CHECK_ARGS, opts->fw_config->fw_command ); res = run_extcmd(cmd_buf, cmd_out, STANDARD_CMD_OUT_BUFSIZE, 0); if(!EXTCMD_IS_SUCCESS(res)) { log_msg(LOG_ERR, "Error %i from cmd:'%s': %s", res, cmd_buf, cmd_out); return 0; } /* Check to see if the anchor exists and is linked into the main policy */ if(strstr(cmd_out, anchor_search_str) == NULL) return 0; return 1; } static void delete_all_anchor_rules(const fko_srv_options_t *opts) { int res = 0; zero_cmd_buffers(); snprintf(cmd_buf, CMD_BUFSIZE-1, "%s " PF_DEL_ALL_ANCHOR_RULES, fwc.fw_command, fwc.anchor ); res = run_extcmd(cmd_buf, err_buf, CMD_BUFSIZE, 0); /* Expect full success on this */ if(! EXTCMD_IS_SUCCESS(res)) log_msg(LOG_ERR, "Error %i from cmd:'%s': %s", res, cmd_buf, err_buf); return; } void fw_config_init(fko_srv_options_t * const opts) { memset(&fwc, 0x0, sizeof(struct fw_config)); /* Set our firewall exe command path */ strlcpy(fwc.fw_command, opts->config[CONF_FIREWALL_EXE], sizeof(fwc.fw_command)); /* Set the PF anchor name */ strlcpy(fwc.anchor, opts->config[CONF_PF_ANCHOR_NAME], sizeof(fwc.anchor)); /* Let us find it via our opts struct as well. */ opts->fw_config = &fwc; return; } void fw_initialize(const fko_srv_options_t * const opts) { if (! anchor_active(opts)) { fprintf(stderr, "Warning: the fwknop anchor is not active in the pf policy\n"); exit(EXIT_FAILURE); } /* Delete any existing rules in the fwknop anchor */ delete_all_anchor_rules(opts); return; } int fw_cleanup(const fko_srv_options_t * const opts) { delete_all_anchor_rules(opts); return(0); } /****************************************************************************/ /* Rule Processing - Create an access request... */ int process_spa_request(const fko_srv_options_t * const opts, const acc_stanza_t * const acc, spa_data_t * const spadat) { char new_rule[MAX_PF_NEW_RULE_LEN] = {0}; char write_cmd[CMD_BUFSIZE] = {0}; FILE *pfctl_fd = NULL; acc_port_list_t *port_list = NULL; acc_port_list_t *ple; int res = 0; time_t now; unsigned int exp_ts; /* Parse and expand our access message. */ expand_acc_port_list(&port_list, spadat->spa_message_remain); /* Start at the top of the proto-port list... */ ple = port_list; /* Set our expire time value. */ time(&now); exp_ts = now + spadat->fw_access_timeout; /* For straight access requests, we currently support multiple proto/port * request. */ if(spadat->message_type == FKO_ACCESS_MSG || spadat->message_type == FKO_CLIENT_TIMEOUT_ACCESS_MSG) { /* Create an access command for each proto/port for the source ip. */ while(ple != NULL) { zero_cmd_buffers(); snprintf(cmd_buf, CMD_BUFSIZE-1, "%s " PF_LIST_ANCHOR_RULES_ARGS, opts->fw_config->fw_command, opts->fw_config->anchor ); /* Cache the current anchor rule set */ res = run_extcmd(cmd_buf, cmd_out, STANDARD_CMD_OUT_BUFSIZE, 0); /* Build the new rule string */ memset(new_rule, 0x0, MAX_PF_NEW_RULE_LEN); snprintf(new_rule, MAX_PF_NEW_RULE_LEN-1, PF_ADD_RULE_ARGS "\n", ple->proto, spadat->use_src_ip, ple->port, exp_ts ); if (strlen(cmd_out) + strlen(new_rule) < STANDARD_CMD_OUT_BUFSIZE) { /* We add the rule to the running policy */ strlcat(cmd_out, new_rule, STANDARD_CMD_OUT_BUFSIZE); memset(write_cmd, 0x0, CMD_BUFSIZE); snprintf(write_cmd, CMD_BUFSIZE-1, "%s " PF_WRITE_ANCHOR_RULES_ARGS, opts->fw_config->fw_command, opts->fw_config->anchor ); if ((pfctl_fd = popen(write_cmd, "w")) == NULL) { log_msg(LOG_WARNING, "Could not execute command: %s", write_cmd); free_acc_port_list(port_list); return(-1); } if (fwrite(cmd_out, strlen(cmd_out), 1, pfctl_fd) == 1) { log_msg(LOG_INFO, "Added Rule for %s, %s expires at %u", spadat->use_src_ip, spadat->spa_message_remain, exp_ts ); fwc.active_rules++; /* Reset the next expected expire time for this chain if it * is warranted. */ if(fwc.next_expire < now || exp_ts < fwc.next_expire) fwc.next_expire = exp_ts; } else log_msg(LOG_WARNING, "Could not write rule to pf anchor"); pclose(pfctl_fd); } else { /* We don't have enough room to add the new firewall rule, * so throw a warning and bail. Once some of the existing * rules are expired the user will once again be able to gain * access. Note that we don't expect to really ever hit this * limit because of STANDARD_CMD_OUT_BUFSIZE is quite a number * of anchor rules. */ log_msg(LOG_WARNING, "Max anchor rules reached, try again later."); free_acc_port_list(port_list); return 0; } ple = ple->next; } } else { /* No other SPA request modes are supported yet. */ if(spadat->message_type == FKO_LOCAL_NAT_ACCESS_MSG || spadat->message_type == FKO_CLIENT_TIMEOUT_LOCAL_NAT_ACCESS_MSG) { log_msg(LOG_WARNING, "Local NAT requests are not currently supported."); } else if(spadat->message_type == FKO_NAT_ACCESS_MSG || spadat->message_type == FKO_CLIENT_TIMEOUT_NAT_ACCESS_MSG) { log_msg(LOG_WARNING, "Forwarding/NAT requests are not currently supported."); } free_acc_port_list(port_list); return(-1); } free_acc_port_list(port_list); return(res); } /* Iterate over the configure firewall access chains and purge expired * firewall rules. */ void check_firewall_rules(const fko_srv_options_t * const opts) { char exp_str[12] = {0}; char anchor_rules_copy[STANDARD_CMD_OUT_BUFSIZE] = {0}; char write_cmd[CMD_BUFSIZE] = {0}; char *ndx, *tmp_mark, *tmp_ndx, *newline_tmp_ndx; time_t now, rule_exp, min_exp=0; int i=0, res=0, anchor_ndx=0, is_delete=0; FILE *pfctl_fd = NULL; /* If we have not yet reached our expected next expire time, continue. */ if(fwc.next_expire == 0) return; time(&now); if (fwc.next_expire > now) return; zero_cmd_buffers(); /* There should be a rule to delete. Get the current list of * rules and delete the ones that are expired. */ snprintf(cmd_buf, CMD_BUFSIZE-1, "%s " PF_LIST_ANCHOR_RULES_ARGS, opts->fw_config->fw_command, opts->fw_config->anchor ); res = run_extcmd(cmd_buf, cmd_out, STANDARD_CMD_OUT_BUFSIZE, 0); if(!EXTCMD_IS_SUCCESS(res)) { log_msg(LOG_ERR, "Error %i from cmd:'%s': %s", res, cmd_buf, cmd_out); return; } /* Find the first _exp_ string (if any). */ ndx = strstr(cmd_out, EXPIRE_COMMENT_PREFIX); if(ndx == NULL) { /* we did not find an expected rule. */ log_msg(LOG_ERR, "Did not find expire comment in rules list %i.\n", i); return; } memset(anchor_rules_copy, 0x0, STANDARD_CMD_OUT_BUFSIZE); /* Walk the list and process rules as needed. */ while (ndx != NULL) { /* Jump forward and extract the timestamp */ ndx += strlen(EXPIRE_COMMENT_PREFIX); /* remember this spot for when we look for the next * rule. */ tmp_mark = ndx; strlcpy(exp_str, ndx, sizeof(exp_str)); rule_exp = (time_t)atoll(exp_str); if(rule_exp <= now) { /* We are going to delete this rule, and because we rebuild the * PF anchor to include all rules that haven't expired, to delete * this rule we just skip to the next one. */ log_msg(LOG_INFO, "Deleting rule with expire time of %u.", rule_exp); if (fwc.active_rules > 0) fwc.active_rules--; is_delete = 1; } else { /* The rule has not expired, so copy it into the anchor string that * lists current rules and will be used to feed * 'pfctl -a -f -'. */ /* back up to the previous newline or the beginning of the rules * output string. */ tmp_ndx = ndx; while(--tmp_ndx > cmd_out) { if(*tmp_ndx == '\n') break; } if(*tmp_ndx == '\n') { tmp_ndx++; } /* may sure the rule begins with the string "pass", and make sure * it ends with a newline. Bail if either test fails. */ if (strlen(tmp_ndx) <= strlen("pass") || strncmp(tmp_ndx, "pass", strlen("pass")) != 0) { break; } newline_tmp_ndx = tmp_ndx; while (*newline_tmp_ndx != '\n' && *newline_tmp_ndx != '\0') { newline_tmp_ndx++; } if (*newline_tmp_ndx != '\n') break; /* copy the whole rule to the next newline (includes the expiration time). */ while (*tmp_ndx != '\n' && *tmp_ndx != '\0' && anchor_ndx < STANDARD_CMD_OUT_BUFSIZE) { anchor_rules_copy[anchor_ndx] = *tmp_ndx; tmp_ndx++; anchor_ndx++; } anchor_rules_copy[anchor_ndx] = '\n'; anchor_ndx++; /* Track the minimum future rule expire time. */ if(rule_exp > now) min_exp = (min_exp < rule_exp) ? min_exp : rule_exp; } /* Push our tracking index forward beyond (just processed) _exp_ * string so we can continue to the next rule in the list. */ ndx = strstr(tmp_mark, EXPIRE_COMMENT_PREFIX); } if (is_delete) { /* We re-instantiate the anchor rules with the new rules string that * has the rule(s) deleted. If there isn't at least one "pass" rule, * then we just flush the anchor. */ if (strlen(anchor_rules_copy) > strlen("pass") && strncmp(anchor_rules_copy, "pass", strlen("pass")) == 0) { memset(write_cmd, 0x0, CMD_BUFSIZE); snprintf(write_cmd, CMD_BUFSIZE-1, "%s " PF_WRITE_ANCHOR_RULES_ARGS, opts->fw_config->fw_command, opts->fw_config->anchor ); if ((pfctl_fd = popen(write_cmd, "w")) == NULL) { log_msg(LOG_WARNING, "Could not execute command: %s", write_cmd); return; } if (fwrite(anchor_rules_copy, strlen(anchor_rules_copy), 1, pfctl_fd) != 1) { log_msg(LOG_WARNING, "Could not write rules to pf anchor"); } pclose(pfctl_fd); } else { delete_all_anchor_rules(opts); } } /* Set the next pending expire time accordingly. 0 if there are no * more rules, or whatever the next expected (min_exp) time will be. */ if(fwc.active_rules < 1) fwc.next_expire = 0; else if(min_exp) fwc.next_expire = min_exp; return; } #endif /* FIREWALL_PF */ /***EOF***/