217387e0f11c76af088a39022f9e4c6d50adf12f
[fwknop.git] / server / fw_util_pf.c
1 /*
2  *****************************************************************************
3  *
4  * File:    fw_util_pf.c
5  *
6  * Author:  Damien S. Stuart, Michael Rash
7  *
8  * Purpose: Fwknop routines for managing pf firewall rules.
9  *
10  * Copyright 2011 Damien Stuart (dstuart@dstuart.org),
11  *                Michael Rash (mbr@cipherdyne.org)
12  *
13  *  License (GNU Public License):
14  *
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.
19  *
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.
24  *
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
28  *  USA
29  *
30  *****************************************************************************
31 */
32 #include "fwknopd_common.h"
33
34 #if FIREWALL_PF
35
36 #include "fw_util.h"
37 #include "utils.h"
38 #include "log_msg.h"
39 #include "extcmd.h"
40 #include "access.h"
41
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];
46
47 static void
48 zero_cmd_buffers(void)
49 {
50     memset(cmd_buf, 0x0, CMD_BUFSIZE);
51     memset(err_buf, 0x0, CMD_BUFSIZE);
52     memset(cmd_out, 0x0, STANDARD_CMD_OUT_BUFSIZE);
53 }
54
55 /* Print all firewall rules currently instantiated by the running fwknopd
56  * daemon to stdout.
57 */
58 int
59 fw_dump_rules(const fko_srv_options_t *opts)
60 {
61     int     res, got_err = 0;
62
63     printf("Listing fwknopd pf rules...\n");
64
65     zero_cmd_buffers();
66
67     /* Create the list command for active rules
68     */
69     snprintf(cmd_buf, CMD_BUFSIZE-1, "%s " PF_LIST_ANCHOR_RULES_ARGS,
70         opts->fw_config->fw_command,
71         opts->fw_config->anchor
72     );
73
74     printf("\nActive Rules in PF anchor '%s':\n", opts->fw_config->anchor);
75     res = system(cmd_buf);
76
77     /* Expect full success on this */
78     if(! EXTCMD_IS_SUCCESS(res))
79     {
80         log_msg(LOG_ERR, "Error %i from cmd:'%s': %s", res, cmd_buf, err_buf);
81         got_err++;
82     }
83
84     return(got_err);
85 }
86
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.
89 */
90 static int
91 anchor_active(const fko_srv_options_t *opts)
92 {
93     int    res = 0;
94     char  *ndx = NULL;
95     char   anchor_search_str[MAX_PF_ANCHOR_SEARCH_LEN] = {0};
96
97     /* Build our anchor search string
98     */
99     snprintf(anchor_search_str, MAX_PF_ANCHOR_SEARCH_LEN-1, "%s%s\" ",
100         "anchor \"", opts->fw_config->anchor);
101
102     zero_cmd_buffers();
103
104     snprintf(cmd_buf, CMD_BUFSIZE-1, "%s " PF_LIST_ALL_RULES_ARGS,
105         opts->fw_config->fw_command
106     );
107
108     res = run_extcmd(cmd_buf, cmd_out, STANDARD_CMD_OUT_BUFSIZE, 0);
109
110     if(!EXTCMD_IS_SUCCESS(res))
111     {
112         log_msg(LOG_ERR, "Error %i from cmd:'%s': %s", res, cmd_buf, cmd_out);
113         return 0;
114     }
115
116     /* first check for the anchor at the very first rule position
117     */
118     if (strncmp(cmd_out, anchor_search_str, strlen(anchor_search_str)) != 0)
119     {
120         anchor_search_str[0] = '\0';
121
122         /* look for the anchor in the middle of the rule set, but make sure
123          * it appears only after a newline
124         */
125         snprintf(anchor_search_str, MAX_PF_ANCHOR_SEARCH_LEN-1, "%s%s\" ",
126             "\nanchor \"", opts->fw_config->anchor);
127
128         ndx = strstr(cmd_out, anchor_search_str);
129
130         if(ndx == NULL)
131             return 0;
132     }
133
134     return 1;
135 }
136
137 static void
138 delete_all_anchor_rules(const fko_srv_options_t *opts)
139 {
140     int res = 0;
141
142     zero_cmd_buffers();
143
144     snprintf(cmd_buf, CMD_BUFSIZE-1, "%s " PF_DEL_ALL_ANCHOR_RULES,
145         fwc.fw_command,
146         fwc.anchor
147     );
148
149     res = run_extcmd(cmd_buf, err_buf, CMD_BUFSIZE, 0);
150
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);
154
155     return;
156 }
157
158 void
159 fw_config_init(fko_srv_options_t *opts)
160 {
161     memset(&fwc, 0x0, sizeof(struct fw_config));
162
163     /* Set our firewall exe command path
164     */
165     strlcpy(fwc.fw_command, opts->config[CONF_FIREWALL_EXE], MAX_PATH_LEN);
166
167     /* Set the PF anchor name
168     */
169     strlcpy(fwc.anchor, opts->config[CONF_PF_ANCHOR_NAME], MAX_PF_ANCHOR_LEN);
170
171     /* Let us find it via our opts struct as well.
172     */
173     opts->fw_config = &fwc;
174
175     return;
176 }
177
178 void
179 fw_initialize(const fko_srv_options_t *opts)
180 {
181
182     if (! anchor_active(opts))
183     {
184         fprintf(stderr, "Warning: the fwknop anchor is not active in the pf policy\n");
185         exit(EXIT_FAILURE);
186     }
187
188     /* Delete any existing rules in the fwknop anchor
189     */
190     delete_all_anchor_rules(opts);
191
192     return;
193 }
194
195 int
196 fw_cleanup(const fko_srv_options_t *opts)
197 {
198     delete_all_anchor_rules(opts);
199     return(0);
200 }
201
202 /****************************************************************************/
203
204 /* Rule Processing - Create an access request...
205 */
206 int
207 process_spa_request(const fko_srv_options_t *opts, const acc_stanza_t *acc, spa_data_t *spadat)
208 {
209     char             new_rule[MAX_PF_NEW_RULE_LEN];
210     char             write_cmd[CMD_BUFSIZE];
211
212     FILE            *pfctl_fd = NULL;
213
214     acc_port_list_t *port_list = NULL;
215     acc_port_list_t *ple;
216
217     unsigned int    fst_proto;
218     unsigned int    fst_port;
219
220     int             res = 0;
221     time_t          now;
222     unsigned int    exp_ts;
223
224     /* Parse and expand our access message.
225     */
226     expand_acc_port_list(&port_list, spadat->spa_message_remain);
227
228     /* Start at the top of the proto-port list...
229     */
230     ple = port_list;
231
232     /* Remember the first proto/port combo in case we need them
233      * for NAT access requests.
234     */
235     fst_proto = ple->proto;
236     fst_port  = ple->port;
237
238     /* Set our expire time value.
239     */
240     time(&now);
241     exp_ts = now + spadat->fw_access_timeout;
242
243     /* For straight access requests, we currently support multiple proto/port
244      * request.
245     */
246     if(spadat->message_type == FKO_ACCESS_MSG
247       || spadat->message_type == FKO_CLIENT_TIMEOUT_ACCESS_MSG)
248     {
249         /* Create an access command for each proto/port for the source ip.
250         */
251         while(ple != NULL)
252         {
253             zero_cmd_buffers();
254
255             snprintf(cmd_buf, CMD_BUFSIZE-1, "%s " PF_LIST_ANCHOR_RULES_ARGS,
256                 opts->fw_config->fw_command,
257                 opts->fw_config->anchor
258             );
259
260             /* Cache the current anchor rule set
261             */
262             res = run_extcmd(cmd_buf, cmd_out, STANDARD_CMD_OUT_BUFSIZE, 0);
263
264             /* Build the new rule string
265             */
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",
268                 ple->proto,
269                 spadat->use_src_ip,
270                 ple->port,
271                 exp_ts
272             );
273
274             if (strlen(cmd_out) + strlen(new_rule) < STANDARD_CMD_OUT_BUFSIZE)
275             {
276                 /* We add the rule to the running policy
277                 */
278                 strlcat(cmd_out, new_rule, STANDARD_CMD_OUT_BUFSIZE);
279
280                 memset(write_cmd, 0x0, CMD_BUFSIZE);
281
282                 snprintf(write_cmd, CMD_BUFSIZE-1, "%s " PF_WRITE_ANCHOR_RULES_ARGS,
283                     opts->fw_config->fw_command,
284                     opts->fw_config->anchor
285                 );
286
287                 if ((pfctl_fd = popen(write_cmd, "w")) == NULL)
288                 {
289                     log_msg(LOG_WARNING, "Could not execute command: %s",
290                         write_cmd);
291                     return(-1);
292                 }
293
294                 if (fwrite(cmd_out, strlen(cmd_out), 1, pfctl_fd) == 1)
295                 {
296                     log_msg(LOG_INFO, "Added Rule for %s, %s expires at %u",
297                         spadat->use_src_ip,
298                         spadat->spa_message_remain,
299                         exp_ts
300                     );
301
302                     fwc.active_rules++;
303
304                     /* Reset the next expected expire time for this chain if it
305                      * is warranted.
306                     */
307                     if(fwc.next_expire < now || exp_ts < fwc.next_expire)
308                         fwc.next_expire = exp_ts;
309                 }
310                 else
311                     log_msg(LOG_WARNING, "Could not write rule to pf anchor");
312
313                 pclose(pfctl_fd);
314             }
315             else
316             {
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
322                  * of anchor rules.
323                 */
324                 log_msg(LOG_WARNING, "Max anchor rules reached, try again later.");
325                 return 0;
326             }
327
328             ple = ple->next;
329         }
330
331     }
332     else
333     {
334         /* No other SPA request modes are supported yet.
335         */
336         if(spadat->message_type == FKO_LOCAL_NAT_ACCESS_MSG
337           || spadat->message_type == FKO_CLIENT_TIMEOUT_LOCAL_NAT_ACCESS_MSG)
338         {
339             log_msg(LOG_WARNING, "Local NAT requests are not currently supported.");
340         }
341         else if(spadat->message_type == FKO_NAT_ACCESS_MSG
342           || spadat->message_type == FKO_CLIENT_TIMEOUT_NAT_ACCESS_MSG)
343         {
344             log_msg(LOG_WARNING, "Forwarding/NAT requests are not currently supported.");
345         }
346
347         return(-1);
348     }
349
350     return(res);
351 }
352
353 /* Iterate over the configure firewall access chains and purge expired
354  * firewall rules.
355 */
356 void
357 check_firewall_rules(const fko_srv_options_t *opts)
358 {
359     char            exp_str[12];
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;
363
364     time_t          now, rule_exp, min_exp=0;
365     int             i=0, res=0, anchor_ndx=0, is_delete=0;
366
367     FILE            *pfctl_fd = NULL;
368
369     /* If we have not yet reached our expected next expire
370        time, continue.
371     */
372     if(fwc.next_expire == 0)
373         return;
374
375     time(&now);
376
377     if (fwc.next_expire > now)
378         return;
379
380     zero_cmd_buffers();
381
382     /* There should be a rule to delete.  Get the current list of
383      * rules and delete the ones that are expired.
384     */
385     snprintf(cmd_buf, CMD_BUFSIZE-1, "%s " PF_LIST_ANCHOR_RULES_ARGS,
386         opts->fw_config->fw_command,
387         opts->fw_config->anchor
388     );
389
390     res = run_extcmd(cmd_buf, cmd_out, STANDARD_CMD_OUT_BUFSIZE, 0);
391
392     if(!EXTCMD_IS_SUCCESS(res))
393     {
394         log_msg(LOG_ERR, "Error %i from cmd:'%s': %s", res, cmd_buf, cmd_out);
395         return;
396     }
397
398     /* Find the first _exp_ string (if any).
399     */
400     ndx = strstr(cmd_out, EXPIRE_COMMENT_PREFIX);
401
402     if(ndx == NULL)
403     {
404         /* we did not find an expected rule.
405         */
406         log_msg(LOG_ERR,
407             "Did not find expire comment in rules list %i.\n", i);
408
409         return;
410     }
411
412     memset(anchor_rules_copy, 0x0, STANDARD_CMD_OUT_BUFSIZE);
413
414     /* Walk the list and process rules as needed.
415     */
416     while (ndx != NULL)
417     {
418         /* Jump forward and extract the timestamp
419         */
420         ndx += strlen(EXPIRE_COMMENT_PREFIX);
421
422         /* remember this spot for when we look for the next
423          * rule.
424         */
425         tmp_mark = ndx;
426
427         strlcpy(exp_str, ndx, 11);
428         rule_exp = (time_t)atoll(exp_str);
429
430         if(rule_exp <= now)
431         {
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.
435             */
436             log_msg(LOG_INFO, "Deleting rule with expire time of %u.", rule_exp);
437
438             if (fwc.active_rules > 0)
439                 fwc.active_rules--;
440
441             is_delete = 1;
442         }
443         else
444         {
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 -'.
448             */
449
450             /* back up to the previous newline or the beginning of the rules
451              * output string.
452             */
453             tmp_ndx = ndx;
454             while(--tmp_ndx > cmd_out)
455             {
456                 if(*tmp_ndx == '\n')
457                     break;
458             }
459
460             if(*tmp_ndx == '\n')
461             {
462                 tmp_ndx++;
463             }
464
465             /* may sure the rule begins with the string "pass", and make sure
466              * it ends with a newline.  Bail if either test fails.
467             */
468             if (strlen(tmp_ndx) <= strlen("pass")
469                 || strncmp(tmp_ndx, "pass", strlen("pass")) != 0)
470             {
471                 break;
472             }
473
474             newline_tmp_ndx = tmp_ndx;
475             while (*newline_tmp_ndx != '\n' && *newline_tmp_ndx != '\0')
476             {
477                 newline_tmp_ndx++;
478             }
479
480             if (*newline_tmp_ndx != '\n')
481                 break;
482
483             /* copy the whole rule to the next newline (includes the expiration
484                time).
485             */
486             while (*tmp_ndx != '\n' && *tmp_ndx != '\0'
487                 && anchor_ndx < STANDARD_CMD_OUT_BUFSIZE)
488             {
489                 anchor_rules_copy[anchor_ndx] = *tmp_ndx;
490                 tmp_ndx++;
491                 anchor_ndx++;
492             }
493             anchor_rules_copy[anchor_ndx] = '\n';
494             anchor_ndx++;
495
496             /* Track the minimum future rule expire time.
497             */
498             if(rule_exp > now)
499                 min_exp = (min_exp < rule_exp) ? min_exp : rule_exp;
500         }
501
502         /* Push our tracking index forward beyond (just processed) _exp_
503          * string so we can continue to the next rule in the list.
504         */
505         ndx = strstr(tmp_mark, EXPIRE_COMMENT_PREFIX);
506
507     }
508
509     if (is_delete)
510     {
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.
514         */
515
516         if (strlen(anchor_rules_copy) > strlen("pass")
517             && strncmp(anchor_rules_copy, "pass", strlen("pass")) == 0)
518         {
519             memset(write_cmd, 0x0, CMD_BUFSIZE);
520
521             snprintf(write_cmd, CMD_BUFSIZE-1, "%s " PF_WRITE_ANCHOR_RULES_ARGS,
522                 opts->fw_config->fw_command,
523                 opts->fw_config->anchor
524             );
525
526             if ((pfctl_fd = popen(write_cmd, "w")) == NULL)
527             {
528                 log_msg(LOG_WARNING, "Could not execute command: %s",
529                     write_cmd);
530                 return;
531             }
532
533             if (fwrite(anchor_rules_copy, strlen(anchor_rules_copy), 1, pfctl_fd) != 1)
534             {
535                 log_msg(LOG_WARNING, "Could not write rules to pf anchor");
536             }
537             pclose(pfctl_fd);
538         }
539         else
540         {
541             delete_all_anchor_rules(opts);
542         }
543
544     }
545
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.
548     */
549     if(fwc.active_rules < 1)
550         fwc.next_expire = 0;
551     else if(min_exp)
552         fwc.next_expire = min_exp;
553
554     return;
555 }
556
557 #endif /* FIREWALL_PF */
558
559 /***EOF***/