updated PF anchor check to not rely on listing the PF policy
[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   anchor_search_str[MAX_PF_ANCHOR_SEARCH_LEN] = {0};
95
96     /* Build our anchor search string
97     */
98     snprintf(anchor_search_str, MAX_PF_ANCHOR_SEARCH_LEN-1, "%s\n",
99         opts->fw_config->anchor);
100
101     zero_cmd_buffers();
102
103     snprintf(cmd_buf, CMD_BUFSIZE-1, "%s " PF_ANCHOR_CHECK_ARGS,
104         opts->fw_config->fw_command
105     );
106
107     res = run_extcmd(cmd_buf, cmd_out, STANDARD_CMD_OUT_BUFSIZE, 0);
108
109     if(!EXTCMD_IS_SUCCESS(res))
110     {
111         log_msg(LOG_ERR, "Error %i from cmd:'%s': %s", res, cmd_buf, cmd_out);
112         return 0;
113     }
114
115     /* Check to see if the anchor exists and is linked into the main policy
116     */
117
118     if(strstr(cmd_out, anchor_search_str) == NULL)
119         return 0;
120
121     return 1;
122 }
123
124 static void
125 delete_all_anchor_rules(const fko_srv_options_t *opts)
126 {
127     int res = 0;
128
129     zero_cmd_buffers();
130
131     snprintf(cmd_buf, CMD_BUFSIZE-1, "%s " PF_DEL_ALL_ANCHOR_RULES,
132         fwc.fw_command,
133         fwc.anchor
134     );
135
136     res = run_extcmd(cmd_buf, err_buf, CMD_BUFSIZE, 0);
137
138     /* Expect full success on this */
139     if(! EXTCMD_IS_SUCCESS(res))
140         log_msg(LOG_ERR, "Error %i from cmd:'%s': %s", res, cmd_buf, err_buf);
141
142     return;
143 }
144
145 void
146 fw_config_init(fko_srv_options_t *opts)
147 {
148     memset(&fwc, 0x0, sizeof(struct fw_config));
149
150     /* Set our firewall exe command path
151     */
152     strlcpy(fwc.fw_command, opts->config[CONF_FIREWALL_EXE], MAX_PATH_LEN);
153
154     /* Set the PF anchor name
155     */
156     strlcpy(fwc.anchor, opts->config[CONF_PF_ANCHOR_NAME], MAX_PF_ANCHOR_LEN);
157
158     /* Let us find it via our opts struct as well.
159     */
160     opts->fw_config = &fwc;
161
162     return;
163 }
164
165 void
166 fw_initialize(const fko_srv_options_t *opts)
167 {
168
169     if (! anchor_active(opts))
170     {
171         fprintf(stderr, "Warning: the fwknop anchor is not active in the pf policy\n");
172         exit(EXIT_FAILURE);
173     }
174
175     /* Delete any existing rules in the fwknop anchor
176     */
177     delete_all_anchor_rules(opts);
178
179     return;
180 }
181
182 int
183 fw_cleanup(const fko_srv_options_t *opts)
184 {
185     delete_all_anchor_rules(opts);
186     return(0);
187 }
188
189 /****************************************************************************/
190
191 /* Rule Processing - Create an access request...
192 */
193 int
194 process_spa_request(const fko_srv_options_t *opts, const acc_stanza_t *acc, spa_data_t *spadat)
195 {
196     char             new_rule[MAX_PF_NEW_RULE_LEN];
197     char             write_cmd[CMD_BUFSIZE];
198
199     FILE            *pfctl_fd = NULL;
200
201     acc_port_list_t *port_list = NULL;
202     acc_port_list_t *ple;
203
204     unsigned int    fst_proto;
205     unsigned int    fst_port;
206
207     int             res = 0;
208     time_t          now;
209     unsigned int    exp_ts;
210
211     /* Parse and expand our access message.
212     */
213     expand_acc_port_list(&port_list, spadat->spa_message_remain);
214
215     /* Start at the top of the proto-port list...
216     */
217     ple = port_list;
218
219     /* Remember the first proto/port combo in case we need them
220      * for NAT access requests.
221     */
222     fst_proto = ple->proto;
223     fst_port  = ple->port;
224
225     /* Set our expire time value.
226     */
227     time(&now);
228     exp_ts = now + spadat->fw_access_timeout;
229
230     /* For straight access requests, we currently support multiple proto/port
231      * request.
232     */
233     if(spadat->message_type == FKO_ACCESS_MSG
234       || spadat->message_type == FKO_CLIENT_TIMEOUT_ACCESS_MSG)
235     {
236         /* Create an access command for each proto/port for the source ip.
237         */
238         while(ple != NULL)
239         {
240             zero_cmd_buffers();
241
242             snprintf(cmd_buf, CMD_BUFSIZE-1, "%s " PF_LIST_ANCHOR_RULES_ARGS,
243                 opts->fw_config->fw_command,
244                 opts->fw_config->anchor
245             );
246
247             /* Cache the current anchor rule set
248             */
249             res = run_extcmd(cmd_buf, cmd_out, STANDARD_CMD_OUT_BUFSIZE, 0);
250
251             /* Build the new rule string
252             */
253             memset(new_rule, 0x0, MAX_PF_NEW_RULE_LEN);
254             snprintf(new_rule, MAX_PF_NEW_RULE_LEN-1, PF_ADD_RULE_ARGS "\n",
255                 ple->proto,
256                 spadat->use_src_ip,
257                 ple->port,
258                 exp_ts
259             );
260
261             if (strlen(cmd_out) + strlen(new_rule) < STANDARD_CMD_OUT_BUFSIZE)
262             {
263                 /* We add the rule to the running policy
264                 */
265                 strlcat(cmd_out, new_rule, STANDARD_CMD_OUT_BUFSIZE);
266
267                 memset(write_cmd, 0x0, CMD_BUFSIZE);
268
269                 snprintf(write_cmd, CMD_BUFSIZE-1, "%s " PF_WRITE_ANCHOR_RULES_ARGS,
270                     opts->fw_config->fw_command,
271                     opts->fw_config->anchor
272                 );
273
274                 if ((pfctl_fd = popen(write_cmd, "w")) == NULL)
275                 {
276                     log_msg(LOG_WARNING, "Could not execute command: %s",
277                         write_cmd);
278                     return(-1);
279                 }
280
281                 if (fwrite(cmd_out, strlen(cmd_out), 1, pfctl_fd) == 1)
282                 {
283                     log_msg(LOG_INFO, "Added Rule for %s, %s expires at %u",
284                         spadat->use_src_ip,
285                         spadat->spa_message_remain,
286                         exp_ts
287                     );
288
289                     fwc.active_rules++;
290
291                     /* Reset the next expected expire time for this chain if it
292                      * is warranted.
293                     */
294                     if(fwc.next_expire < now || exp_ts < fwc.next_expire)
295                         fwc.next_expire = exp_ts;
296                 }
297                 else
298                     log_msg(LOG_WARNING, "Could not write rule to pf anchor");
299
300                 pclose(pfctl_fd);
301             }
302             else
303             {
304                 /* We don't have enough room to add the new firewall rule,
305                  * so throw a warning and bail.  Once some of the existing
306                  * rules are expired the user will once again be able to gain
307                  * access.  Note that we don't expect to really ever hit this
308                  * limit because of STANDARD_CMD_OUT_BUFSIZE is quite a number
309                  * of anchor rules.
310                 */
311                 log_msg(LOG_WARNING, "Max anchor rules reached, try again later.");
312                 return 0;
313             }
314
315             ple = ple->next;
316         }
317
318     }
319     else
320     {
321         /* No other SPA request modes are supported yet.
322         */
323         if(spadat->message_type == FKO_LOCAL_NAT_ACCESS_MSG
324           || spadat->message_type == FKO_CLIENT_TIMEOUT_LOCAL_NAT_ACCESS_MSG)
325         {
326             log_msg(LOG_WARNING, "Local NAT requests are not currently supported.");
327         }
328         else if(spadat->message_type == FKO_NAT_ACCESS_MSG
329           || spadat->message_type == FKO_CLIENT_TIMEOUT_NAT_ACCESS_MSG)
330         {
331             log_msg(LOG_WARNING, "Forwarding/NAT requests are not currently supported.");
332         }
333
334         return(-1);
335     }
336
337     return(res);
338 }
339
340 /* Iterate over the configure firewall access chains and purge expired
341  * firewall rules.
342 */
343 void
344 check_firewall_rules(const fko_srv_options_t *opts)
345 {
346     char            exp_str[12];
347     char            anchor_rules_copy[STANDARD_CMD_OUT_BUFSIZE];
348     char            write_cmd[CMD_BUFSIZE];
349     char           *ndx, *tmp_mark, *tmp_ndx, *newline_tmp_ndx;
350
351     time_t          now, rule_exp, min_exp=0;
352     int             i=0, res=0, anchor_ndx=0, is_delete=0;
353
354     FILE            *pfctl_fd = NULL;
355
356     /* If we have not yet reached our expected next expire
357        time, continue.
358     */
359     if(fwc.next_expire == 0)
360         return;
361
362     time(&now);
363
364     if (fwc.next_expire > now)
365         return;
366
367     zero_cmd_buffers();
368
369     /* There should be a rule to delete.  Get the current list of
370      * rules and delete the ones that are expired.
371     */
372     snprintf(cmd_buf, CMD_BUFSIZE-1, "%s " PF_LIST_ANCHOR_RULES_ARGS,
373         opts->fw_config->fw_command,
374         opts->fw_config->anchor
375     );
376
377     res = run_extcmd(cmd_buf, cmd_out, STANDARD_CMD_OUT_BUFSIZE, 0);
378
379     if(!EXTCMD_IS_SUCCESS(res))
380     {
381         log_msg(LOG_ERR, "Error %i from cmd:'%s': %s", res, cmd_buf, cmd_out);
382         return;
383     }
384
385     /* Find the first _exp_ string (if any).
386     */
387     ndx = strstr(cmd_out, EXPIRE_COMMENT_PREFIX);
388
389     if(ndx == NULL)
390     {
391         /* we did not find an expected rule.
392         */
393         log_msg(LOG_ERR,
394             "Did not find expire comment in rules list %i.\n", i);
395
396         return;
397     }
398
399     memset(anchor_rules_copy, 0x0, STANDARD_CMD_OUT_BUFSIZE);
400
401     /* Walk the list and process rules as needed.
402     */
403     while (ndx != NULL)
404     {
405         /* Jump forward and extract the timestamp
406         */
407         ndx += strlen(EXPIRE_COMMENT_PREFIX);
408
409         /* remember this spot for when we look for the next
410          * rule.
411         */
412         tmp_mark = ndx;
413
414         strlcpy(exp_str, ndx, 11);
415         rule_exp = (time_t)atoll(exp_str);
416
417         if(rule_exp <= now)
418         {
419             /* We are going to delete this rule, and because we rebuild the
420              * PF anchor to include all rules that haven't expired, to delete
421              * this rule we just skip to the next one.
422             */
423             log_msg(LOG_INFO, "Deleting rule with expire time of %u.", rule_exp);
424
425             if (fwc.active_rules > 0)
426                 fwc.active_rules--;
427
428             is_delete = 1;
429         }
430         else
431         {
432             /* The rule has not expired, so copy it into the anchor string that
433              * lists current rules and will be used to feed
434              * 'pfctl -a <anchor> -f -'.
435             */
436
437             /* back up to the previous newline or the beginning of the rules
438              * output string.
439             */
440             tmp_ndx = ndx;
441             while(--tmp_ndx > cmd_out)
442             {
443                 if(*tmp_ndx == '\n')
444                     break;
445             }
446
447             if(*tmp_ndx == '\n')
448             {
449                 tmp_ndx++;
450             }
451
452             /* may sure the rule begins with the string "pass", and make sure
453              * it ends with a newline.  Bail if either test fails.
454             */
455             if (strlen(tmp_ndx) <= strlen("pass")
456                 || strncmp(tmp_ndx, "pass", strlen("pass")) != 0)
457             {
458                 break;
459             }
460
461             newline_tmp_ndx = tmp_ndx;
462             while (*newline_tmp_ndx != '\n' && *newline_tmp_ndx != '\0')
463             {
464                 newline_tmp_ndx++;
465             }
466
467             if (*newline_tmp_ndx != '\n')
468                 break;
469
470             /* copy the whole rule to the next newline (includes the expiration
471                time).
472             */
473             while (*tmp_ndx != '\n' && *tmp_ndx != '\0'
474                 && anchor_ndx < STANDARD_CMD_OUT_BUFSIZE)
475             {
476                 anchor_rules_copy[anchor_ndx] = *tmp_ndx;
477                 tmp_ndx++;
478                 anchor_ndx++;
479             }
480             anchor_rules_copy[anchor_ndx] = '\n';
481             anchor_ndx++;
482
483             /* Track the minimum future rule expire time.
484             */
485             if(rule_exp > now)
486                 min_exp = (min_exp < rule_exp) ? min_exp : rule_exp;
487         }
488
489         /* Push our tracking index forward beyond (just processed) _exp_
490          * string so we can continue to the next rule in the list.
491         */
492         ndx = strstr(tmp_mark, EXPIRE_COMMENT_PREFIX);
493
494     }
495
496     if (is_delete)
497     {
498         /* We re-instantiate the anchor rules with the new rules string that
499          * has the rule(s) deleted.  If there isn't at least one "pass" rule,
500          * then we just flush the anchor.
501         */
502
503         if (strlen(anchor_rules_copy) > strlen("pass")
504             && strncmp(anchor_rules_copy, "pass", strlen("pass")) == 0)
505         {
506             memset(write_cmd, 0x0, CMD_BUFSIZE);
507
508             snprintf(write_cmd, CMD_BUFSIZE-1, "%s " PF_WRITE_ANCHOR_RULES_ARGS,
509                 opts->fw_config->fw_command,
510                 opts->fw_config->anchor
511             );
512
513             if ((pfctl_fd = popen(write_cmd, "w")) == NULL)
514             {
515                 log_msg(LOG_WARNING, "Could not execute command: %s",
516                     write_cmd);
517                 return;
518             }
519
520             if (fwrite(anchor_rules_copy, strlen(anchor_rules_copy), 1, pfctl_fd) != 1)
521             {
522                 log_msg(LOG_WARNING, "Could not write rules to pf anchor");
523             }
524             pclose(pfctl_fd);
525         }
526         else
527         {
528             delete_all_anchor_rules(opts);
529         }
530
531     }
532
533     /* Set the next pending expire time accordingly. 0 if there are no
534      * more rules, or whatever the next expected (min_exp) time will be.
535     */
536     if(fwc.active_rules < 1)
537         fwc.next_expire = 0;
538     else if(min_exp)
539         fwc.next_expire = min_exp;
540
541     return;
542 }
543
544 #endif /* FIREWALL_PF */
545
546 /***EOF***/