Skip to content

Commit a3b8dca

Browse files
committed
Add Capsicum sandboxing for FreeBSD
Implement Capsicum capability mode for the main, resolver, and logger processes on FreeBSD, matching the existing pledge/unveil (OpenBSD) and seccomp-bpf (Linux) sandboxing. Each sandboxed process calls cap_enter() after initialization, restricting it to operations on already-open file descriptors. Per-fd rights are limited via cap_rights_limit() on IPC sockets. Resolver: eagerly initializes DoT SSL context (CA bundle loading) before cap_enter() since filesystem access is needed. c-ares channel is initialized before cap mode; socket/connect/sendto for DNS queries work on new fds which get full rights in capability mode. Logger: enters capability mode in the PRIVILEGES handler, after all initial sinks have been created. Pre-opens parent directories for each file sink and stores dirfd per ChildSink. Log rotation on SIGHUP uses openat() on the pre-opened dirfd. Syslog is pre-connected before cap_enter(). Main: checks config for AF_UNIX listeners, fallbacks, and backends; skips cap_enter() if any are found since connect()/bind() to Unix socket paths require VFS lookups forbidden in capability mode. Pre-opens config directory for SIGHUP reload via openat(), and temp directory for SIGUSR1 debug dumps. Sets logger_parent_fs_locked after cap_enter() so the parent does not attempt path-based opens during reload. Binder: not sandboxed with Capsicum because it must bind() AF_UNIX paths for Unix domain socket listeners. Build system detects cap_enter() and cap_rights_limit() on FreeBSD and defines HAVE_CAPSICUM. All capsicum code is guarded by __FreeBSD__ && HAVE_CAPSICUM ifdefs, compiling as no-ops elsewhere. Set SNIPROXY_DISABLE_CAPSICUM=1 to bypass for debugging.
1 parent c73bfbc commit a3b8dca

File tree

14 files changed

+488
-32
lines changed

14 files changed

+488
-32
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ Features
7272
spoofed sources
7373
+ **Privilege separation**: Separate processes for logging and DNS resolution
7474
+ **OpenBSD sandboxing**: pledge(2) and unveil(2) for minimal system access
75+
+ **FreeBSD sandboxing**: Capsicum capability mode with per-fd rights limiting
7576
+ **Input sanitization**: Hostname validation, control character removal
7677
+ **Comprehensive fuzzing**: Protocol fuzzers for TLS, DTLS, HTTP/2, XMPP,
7778
Minecraft, hostname, address, config, listener ACL, IPC crypto, and resolver
@@ -571,6 +572,19 @@ All paths are collected from the loaded configuration, so custom locations work
571572
as long as files/directories exist before launch. Helper processes are forked
572573
(not exec'd) and inherit the master key for IPC encryption.
573574

575+
### FreeBSD Sandboxing
576+
577+
On FreeBSD, SNIProxy uses Capsicum capability mode to restrict each process after initialization:
578+
579+
- **Resolver process**: DoT SSL context is eagerly initialized before entering capability mode (since CA bundle loading requires filesystem access). The IPC socket is limited to read/write/send/recv/event rights.
580+
- **Logger process**: Enters capability mode after privilege drop. Pre-opened directory fds allow log file rotation via `openat()` in capability mode. Syslog is pre-connected before `cap_enter()`. The IPC socket is limited to read/write/send/recv/event rights.
581+
- **Main process**: Config directory and temp directory are pre-opened before entering capability mode. Config reload uses `openat()` on the pre-opened directory fd. Debug dumps use `openat()` on the pre-opened temp directory fd.
582+
- **Binder process**: Not sandboxed with Capsicum because it must `bind()` AF_UNIX paths, which requires VFS lookups forbidden in capability mode.
583+
584+
The main process skips capability mode when any listener, fallback, or backend address is a Unix domain socket, since `connect()` and `bind()` to AF_UNIX paths require VFS lookups forbidden in capability mode. IP-only configurations (the common case) get full Capsicum protection. Adding new log file paths during SIGHUP reload is not supported in capability mode (existing log files can be reopened).
585+
586+
Set `SNIPROXY_DISABLE_CAPSICUM=1` in the environment to disable Capsicum sandboxing for debugging.
587+
574588
Performance Notes
575589
-----------------
576590

configure.ac

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,17 @@ case "$host_os" in
245245
esac
246246
AC_SUBST([LIBSECCOMP_LIBS])
247247

248+
# Check for Capsicum (FreeBSD only)
249+
case "$host_os" in
250+
freebsd*)
251+
AC_CHECK_HEADERS([sys/capsicum.h])
252+
AC_CHECK_FUNCS([cap_enter cap_rights_limit])
253+
AS_IF([test "x$ac_cv_func_cap_enter" = xyes -a "x$ac_cv_func_cap_rights_limit" = xyes], [
254+
AC_DEFINE([HAVE_CAPSICUM], 1, [Define to 1 if Capsicum is available])
255+
])
256+
;;
257+
esac
258+
248259
AC_CHECK_FUNCS([accept4 closefrom daemon setproctitle mkostemp explicit_bzero memset_s])
249260

250261
# Enable large file support (so we can log more than 2GB)

man/sniproxy.8

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,35 @@ or
9494
.B SIGINT\fR, \fBSIGTERM
9595
Shut down gracefully\&.
9696

97+
.SH SANDBOXING
98+
On OpenBSD,
99+
\fBsniproxy\fR
100+
uses
101+
\fBpledge\fR(2)
102+
and
103+
\fBunveil\fR(2)
104+
to restrict each process to the minimum set of system calls and
105+
filesystem paths required\&.
106+
.PP
107+
On FreeBSD,
108+
\fBsniproxy\fR
109+
uses Capsicum capability mode
110+
(\fBcap_enter\fR(2))
111+
and per-fd rights
112+
(\fBcap_rights_limit\fR(2))
113+
to confine each process after initialization\&.
114+
Directories needed for config reload and log rotation are pre-opened
115+
so that
116+
\fBopenat\fR(2)
117+
works inside capability mode\&.
118+
Set
119+
.B SNIPROXY_DISABLE_CAPSICUM
120+
in the environment to disable Capsicum sandboxing\&.
121+
.PP
122+
On Linux,
123+
\fBsniproxy\fR
124+
uses seccomp-bpf to restrict available system calls per process\&.
125+
97126
.SH "SEE ALSO"
98127
.PP
99128
\fBsniproxy.conf\fR(5)

src/Makefile.am

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ sniproxy_common_sources = address.c \
1414
binder.h \
1515
buffer.c \
1616
buffer.h \
17+
capsicum.c \
18+
capsicum.h \
1719
cfg_parser.c \
1820
cfg_parser.h \
1921
cfg_tokenizer.c \

src/binder.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,10 @@ binder_main(int sockfd) {
410410
}
411411
}
412412

413+
/* Capsicum is not used for the binder process because it must
414+
* call bind() on AF_UNIX paths, which requires VFS lookups
415+
* that are forbidden in capability mode. */
416+
413417
for (;;) {
414418
uint8_t *plain = NULL;
415419
size_t plain_len = 0;

src/capsicum.c

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright (c) 2025, Renaud Allard <renaud@allard.it>
3+
* All rights reserved.
4+
*
5+
* Redistribution and use in source and binary forms, with or without
6+
* modification, are permitted provided that the following conditions are met:
7+
*
8+
* 1. Redistributions of source code must retain the above copyright notice,
9+
* this list of conditions and the following disclaimer.
10+
* 2. Redistributions in binary form must reproduce the above copyright
11+
* notice, this list of conditions and the following disclaimer in the
12+
* documentation and/or other materials provided with the distribution.
13+
*
14+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
15+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17+
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
18+
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
19+
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
20+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
21+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
22+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
23+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
24+
* POSSIBILITY OF SUCH DAMAGE.
25+
*/
26+
#ifdef HAVE_CONFIG_H
27+
#include <config.h>
28+
#endif
29+
30+
#include "capsicum.h"
31+
32+
#if defined(__FreeBSD__) && defined(HAVE_CAPSICUM)
33+
34+
#include <sys/capsicum.h>
35+
#include <errno.h>
36+
#include <stdio.h>
37+
#include <stdlib.h>
38+
#include <string.h>
39+
40+
int
41+
capsicum_available(void) {
42+
const char *disable_env = getenv("SNIPROXY_DISABLE_CAPSICUM");
43+
if (disable_env != NULL && disable_env[0] != '\0')
44+
return 0;
45+
return 1;
46+
}
47+
48+
int
49+
capsicum_enter(void) {
50+
if (cap_enter() < 0) {
51+
if (errno == ENOSYS)
52+
return 0;
53+
return -1;
54+
}
55+
56+
return 0;
57+
}
58+
59+
#else /* !(__FreeBSD__ && HAVE_CAPSICUM) */
60+
61+
int
62+
capsicum_available(void) {
63+
return 0;
64+
}
65+
66+
int
67+
capsicum_enter(void) {
68+
return 0;
69+
}
70+
71+
#endif

src/capsicum.h

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright (c) 2025, Renaud Allard <renaud@allard.it>
3+
* All rights reserved.
4+
*
5+
* Redistribution and use in source and binary forms, with or without
6+
* modification, are permitted provided that the following conditions are met:
7+
*
8+
* 1. Redistributions of source code must retain the above copyright notice,
9+
* this list of conditions and the following disclaimer.
10+
* 2. Redistributions in binary form must reproduce the above copyright
11+
* notice, this list of conditions and the following disclaimer in the
12+
* documentation and/or other materials provided with the distribution.
13+
*
14+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
15+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17+
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
18+
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
19+
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
20+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
21+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
22+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
23+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
24+
* POSSIBILITY OF SUCH DAMAGE.
25+
*/
26+
#ifndef SNIPROXY_CAPSICUM_H
27+
#define SNIPROXY_CAPSICUM_H
28+
29+
int capsicum_available(void);
30+
int capsicum_enter(void);
31+
32+
#endif /* SNIPROXY_CAPSICUM_H */

src/config.c

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@
5353
#include "logger.h"
5454
#include "connection.h"
5555
#include "binder.h"
56+
#include <fcntl.h>
57+
#include <unistd.h>
58+
#if defined(__FreeBSD__) && defined(HAVE_CAPSICUM)
59+
#include <libgen.h>
60+
#endif
5661

5762

5863
struct LoggerBuilder {
@@ -78,6 +83,8 @@ enum GlobalACLPolicy {
7883

7984
static enum GlobalACLPolicy global_acl_policy = GLOBAL_ACL_POLICY_UNSET;
8085
static int allow_group_read = 0;
86+
static int config_dirfd = -1;
87+
static char *config_basename = NULL;
8188

8289
static int accept_username(struct Config *, const char *);
8390
static int accept_groupname(struct Config *, const char *);
@@ -518,11 +525,32 @@ init_config(const char *filename, struct ev_loop *loop, int fatal_on_perm_error)
518525

519526
global_acl_policy = GLOBAL_ACL_POLICY_UNSET;
520527

521-
FILE *file = fopen(config->filename, "r");
522-
if (file == NULL) {
523-
err("%s: unable to open configuration file: %s", __func__, config->filename);
524-
free_config(config, loop);
525-
return NULL;
528+
FILE *file;
529+
if (config_dirfd >= 0 && config_basename != NULL) {
530+
int cfd = openat(config_dirfd, config_basename,
531+
O_RDONLY | O_CLOEXEC);
532+
if (cfd < 0) {
533+
err("%s: unable to open configuration file: %s",
534+
__func__, config->filename);
535+
free_config(config, loop);
536+
return NULL;
537+
}
538+
file = fdopen(cfd, "r");
539+
if (file == NULL) {
540+
close(cfd);
541+
err("%s: unable to open configuration file: %s",
542+
__func__, config->filename);
543+
free_config(config, loop);
544+
return NULL;
545+
}
546+
} else {
547+
file = fopen(config->filename, "r");
548+
if (file == NULL) {
549+
err("%s: unable to open configuration file: %s",
550+
__func__, config->filename);
551+
free_config(config, loop);
552+
return NULL;
553+
}
526554
}
527555

528556
/* Check permissions on opened file to prevent TOCTOU.
@@ -1984,3 +2012,40 @@ int
19842012
config_get_allow_group_read(void) {
19852013
return allow_group_read;
19862014
}
2015+
2016+
#if defined(__FreeBSD__) && defined(HAVE_CAPSICUM)
2017+
int
2018+
config_prepare_capsicum(const char *filename) {
2019+
char *pathcopy, *dir, *base;
2020+
2021+
if (filename == NULL)
2022+
return -1;
2023+
2024+
pathcopy = strdup(filename);
2025+
if (pathcopy == NULL)
2026+
return -1;
2027+
dir = dirname(pathcopy);
2028+
2029+
config_dirfd = open(dir, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
2030+
free(pathcopy);
2031+
if (config_dirfd < 0)
2032+
return -1;
2033+
2034+
pathcopy = strdup(filename);
2035+
if (pathcopy == NULL) {
2036+
close(config_dirfd);
2037+
config_dirfd = -1;
2038+
return -1;
2039+
}
2040+
base = basename(pathcopy);
2041+
config_basename = strdup(base);
2042+
free(pathcopy);
2043+
if (config_basename == NULL) {
2044+
close(config_dirfd);
2045+
config_dirfd = -1;
2046+
return -1;
2047+
}
2048+
2049+
return 0;
2050+
}
2051+
#endif

src/config.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,5 +76,8 @@ void free_config(struct Config *, struct ev_loop *);
7676
void print_config(FILE *, struct Config *);
7777
void config_set_allow_group_read(int);
7878
int config_get_allow_group_read(void);
79+
#if defined(__FreeBSD__) && defined(HAVE_CAPSICUM)
80+
int config_prepare_capsicum(const char *);
81+
#endif
7982

8083
#endif

0 commit comments

Comments
 (0)