aboutsummaryrefslogtreecommitdiff
path: root/sys/security/mac_portacl/mac_portacl.c
blob: c5d48279917e8f88dad20652fc32965f297822be (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
/*-
 * Copyright (c) 2003-2004 Networks Associates Technology, Inc.
 * Copyright (c) 2006 SPARTA, Inc.
 * All rights reserved.
 *
 * This software was developed for the FreeBSD Project by Network
 * Associates Laboratories, the Security Research Division of Network
 * Associates, Inc. under DARPA/SPAWAR contract N66001-01-C-8035 ("CBOSS"),
 * as part of the DARPA CHATS research program.
 *
 * This software was enhanced by SPARTA ISSO under SPAWAR contract
 * N66001-04-C-6019 ("SEFOS").
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 *
 * $FreeBSD$
 */

/*
 * Developed by the TrustedBSD Project.
 *
 * Administratively limit access to local UDP/TCP ports for binding purposes.
 * Intended to be combined with net.inet.ip.portrange.reservedhigh to allow
 * specific uids and gids to bind specific ports for specific purposes,
 * while not opening the door to any user replacing an "official" service
 * while you're restarting it.  This only affects ports explicitly bound by
 * the user process (either for listen/outgoing socket for TCP, or send/
 * receive for UDP).  This module will not limit ports bound implicitly for
 * out-going connections where the process hasn't explicitly selected a port:
 * these are automatically selected by the IP stack.
 *
 * To use this module, security.mac.enforce_socket must be enabled, and you
 * will probably want to twiddle the net.inet sysctl listed above.  Then use
 * sysctl(8) to modify the rules string:
 *
 * # sysctl security.mac.portacl.rules="uid:425:tcp:80,uid:425:tcp:79"
 *
 * This ruleset, for example, permits uid 425 to bind TCP ports 80 (http) and
 * 79 (finger).  User names and group names can't be used directly because
 * the kernel only knows about uids and gids.
 */

#include <sys/param.h>
#include <sys/domain.h>
#include <sys/kernel.h>
#include <sys/lock.h>
#include <sys/malloc.h>
#include <sys/module.h>
#include <sys/mutex.h>
#include <sys/priv.h>
#include <sys/proc.h>
#include <sys/protosw.h>
#include <sys/queue.h>
#include <sys/systm.h>
#include <sys/sbuf.h>
#include <sys/socket.h>
#include <sys/socketvar.h>
#include <sys/sysctl.h>

#include <netinet/in.h>
#include <netinet/in_pcb.h>

#include <security/mac/mac_policy.h>

SYSCTL_DECL(_security_mac);

SYSCTL_NODE(_security_mac, OID_AUTO, portacl, CTLFLAG_RW, 0,
    "TrustedBSD mac_portacl policy controls");

static int	portacl_enabled = 1;
SYSCTL_INT(_security_mac_portacl, OID_AUTO, enabled, CTLFLAG_RW,
    &portacl_enabled, 0, "Enforce portacl policy");
TUNABLE_INT("security.mac.portacl.enabled", &portacl_enabled);

static int	portacl_suser_exempt = 1;
SYSCTL_INT(_security_mac_portacl, OID_AUTO, suser_exempt, CTLFLAG_RW,
    &portacl_suser_exempt, 0, "Privilege permits binding of any port");
TUNABLE_INT("security.mac.portacl.suser_exempt",
    &portacl_suser_exempt);

static int	portacl_autoport_exempt = 1;
SYSCTL_INT(_security_mac_portacl, OID_AUTO, autoport_exempt, CTLFLAG_RW,
    &portacl_autoport_exempt, 0, "Allow automatic allocation through "
    "binding port 0 if not IP_PORTRANGELOW");
TUNABLE_INT("security.mac.portacl.autoport_exempt",
    &portacl_autoport_exempt);

static int	portacl_port_high = 1023;
SYSCTL_INT(_security_mac_portacl, OID_AUTO, port_high, CTLFLAG_RW,
    &portacl_port_high, 0, "Highest port to enforce for");
TUNABLE_INT("security.mac.portacl.port_high", &portacl_port_high);

static MALLOC_DEFINE(M_PORTACL, "portacl_rule", "Rules for mac_portacl");

#define	MAC_RULE_STRING_LEN	1024

#define	RULE_GID	1
#define	RULE_UID	2
#define	RULE_PROTO_TCP	1
#define	RULE_PROTO_UDP	2
struct rule {
	id_t			r_id;
	int			r_idtype;
	u_int16_t		r_port;
	int			r_protocol;

	TAILQ_ENTRY(rule)	r_entries;
};

#define	GID_STRING	"gid"
#define	TCP_STRING	"tcp"
#define	UID_STRING	"uid"
#define	UDP_STRING	"udp"

/*
 * Text format for the rule string is that a rule consists of a
 * comma-separated list of elements.  Each element is in the form
 * idtype:id:protocol:portnumber, and constitutes granting of permission
 * for the specified binding.
 */

static struct mtx			rule_mtx;
static TAILQ_HEAD(rulehead, rule)	rule_head;
static char				rule_string[MAC_RULE_STRING_LEN];

static void
toast_rules(struct rulehead *head)
{
	struct rule *rule;

	while ((rule = TAILQ_FIRST(head)) != NULL) {
		TAILQ_REMOVE(head, rule, r_entries);
		free(rule, M_PORTACL);
	}
}

/*
 * Note that there is an inherent race condition in the unload of modules
 * and access via sysctl.
 */
static void
destroy(struct mac_policy_conf *mpc)
{

	mtx_destroy(&rule_mtx);
	toast_rules(&rule_head);
}

static void
init(struct mac_policy_conf *mpc)
{

	mtx_init(&rule_mtx, "rule_mtx", NULL, MTX_DEF);
	TAILQ_INIT(&rule_head);
}

/*
 * Note: parsing routines are destructive on the passed string.
 */
static int
parse_rule_element(char *element, struct rule **rule)
{
	char *idtype, *id, *protocol, *portnumber, *p;
	struct rule *new;
	int error;

	error = 0;
	new = malloc(sizeof(*new), M_PORTACL, M_ZERO | M_WAITOK);

	idtype = strsep(&element, ":");
	if (idtype == NULL) {
		error = EINVAL;
		goto out;
	}
	id = strsep(&element, ":");
	if (id == NULL) {
		error = EINVAL;
		goto out;
	}
	new->r_id = strtol(id, &p, 10);
	if (*p != '\0') {
		error = EINVAL;
		goto out;
	}
	if (strcmp(idtype, UID_STRING) == 0)
		new->r_idtype = RULE_UID;
	else if (strcmp(idtype, GID_STRING) == 0)
		new->r_idtype = RULE_GID;
	else {
		error = EINVAL;
		goto out;
	}
	protocol = strsep(&element, ":");
	if (protocol == NULL) {
		error = EINVAL;
		goto out;
	}
	if (strcmp(protocol, TCP_STRING) == 0)
		new->r_protocol = RULE_PROTO_TCP;
	else if (strcmp(protocol, UDP_STRING) == 0)
		new->r_protocol = RULE_PROTO_UDP;
	else {
		error = EINVAL;
		goto out;
	}
	portnumber = element;
	if (portnumber == NULL) {
		error = EINVAL;
		goto out;
	}
	new->r_port = strtol(portnumber, &p, 10);
	if (*p != '\0') {
		error = EINVAL;
		goto out;
	}

out:
	if (error != 0) {
		free(new, M_PORTACL);
		*rule = NULL;
	} else
		*rule = new;
	return (error);
}

static int
parse_rules(char *string, struct rulehead *head)
{
	struct rule *new;
	char *element;
	int error;

	error = 0;
	while ((element = strsep(&string, ",")) != NULL) {
		if (strlen(element) == 0)
			continue;
		error = parse_rule_element(element, &new);
		if (error)
			goto out;
		TAILQ_INSERT_TAIL(head, new, r_entries);
	}
out:
	if (error != 0)
		toast_rules(head);
	return (error);
}

/*
 * rule_printf() and rules_to_string() are unused currently because they rely
 * on sbufs with auto-extension, which may sleep while holding a mutex.
 * Instead, the non-canonical user-generated rule string is returned to the
 * user when the rules are queried, which is faster anyway.
 */
#if 0
static void
rule_printf(struct sbuf *sb, struct rule *rule)
{
	const char *idtype, *protocol;

	switch(rule->r_idtype) {
	case RULE_GID:
		idtype = GID_STRING;
		break;
	case RULE_UID:
		idtype = UID_STRING;
		break;
	default:
		panic("rule_printf: unknown idtype (%d)\n", rule->r_idtype);
	}

	switch (rule->r_protocol) {
	case RULE_PROTO_TCP:
		protocol = TCP_STRING;
		break;
	case RULE_PROTO_UDP:
		protocol = UDP_STRING;
		break;
	default:
		panic("rule_printf: unknown protocol (%d)\n",
		    rule->r_protocol);
	}
	sbuf_printf(sb, "%s:%jd:%s:%d", idtype, (intmax_t)rule->r_id,
	    protocol, rule->r_port);
}

static char *
rules_to_string(void)
{
	struct rule *rule;
	struct sbuf *sb;
	int needcomma;
	char *temp;

	sb = sbuf_new_auto();
	needcomma = 0;
	mtx_lock(&rule_mtx);
	for (rule = TAILQ_FIRST(&rule_head); rule != NULL;
	    rule = TAILQ_NEXT(rule, r_entries)) {
		if (!needcomma)
			needcomma = 1;
		else
			sbuf_printf(sb, ",");
		rule_printf(sb, rule);
	}
	mtx_unlock(&rule_mtx);
	sbuf_finish(sb);
	temp = strdup(sbuf_data(sb), M_PORTACL);
	sbuf_delete(sb);
	return (temp);
}
#endif

/*
 * Note: due to races, there is not a single serializable order
 * between parallel calls to the sysctl.
 */
static int
sysctl_rules(SYSCTL_HANDLER_ARGS)
{
	char *string, *copy_string, *new_string;
	struct rulehead head, save_head;
	int error;

	new_string = NULL;
	if (req->newptr != NULL) {
		new_string = malloc(MAC_RULE_STRING_LEN, M_PORTACL,
		    M_WAITOK | M_ZERO);
		mtx_lock(&rule_mtx);
		strcpy(new_string, rule_string);
		mtx_unlock(&rule_mtx);
		string = new_string;
	} else
		string = rule_string;

	error = sysctl_handle_string(oidp, string, MAC_RULE_STRING_LEN, req);
	if (error)
		goto out;

	if (req->newptr != NULL) {
		copy_string = strdup(string, M_PORTACL);
		TAILQ_INIT(&head);
		error = parse_rules(copy_string, &head);
		free(copy_string, M_PORTACL);
		if (error)
			goto out;

		TAILQ_INIT(&save_head);
		mtx_lock(&rule_mtx);
		TAILQ_CONCAT(&save_head, &rule_head, r_entries);
		TAILQ_CONCAT(&rule_head, &head, r_entries);
		strcpy(rule_string, string);
		mtx_unlock(&rule_mtx);
		toast_rules(&save_head);
	}
out:
	if (new_string != NULL)
		free(new_string, M_PORTACL);
	return (error);
}

SYSCTL_PROC(_security_mac_portacl, OID_AUTO, rules,
       CTLTYPE_STRING|CTLFLAG_RW, 0, 0, sysctl_rules, "A", "Rules");

static int
rules_check(struct ucred *cred, int family, int type, u_int16_t port)
{
	struct rule *rule;
	int error;

#if 0
	printf("Check requested for euid %d, family %d, type %d, port %d\n",
	    cred->cr_uid, family, type, port);
#endif

	if (port > portacl_port_high)
		return (0);

	error = EPERM;
	mtx_lock(&rule_mtx);
	for (rule = TAILQ_FIRST(&rule_head);
	    rule != NULL;
	    rule = TAILQ_NEXT(rule, r_entries)) {
		if (type == SOCK_DGRAM && rule->r_protocol != RULE_PROTO_UDP)
			continue;
		if (type == SOCK_STREAM && rule->r_protocol != RULE_PROTO_TCP)
			continue;
		if (port != rule->r_port)
			continue;
		if (rule->r_idtype == RULE_UID) {
			if (cred->cr_uid == rule->r_id) {
				error = 0;
				break;
			}
		} else if (rule->r_idtype == RULE_GID) {
			if (cred->cr_gid == rule->r_id) {
				error = 0;
				break;
			} else if (groupmember(rule->r_id, cred)) {
				error = 0;
				break;
			}
		} else
			panic("rules_check: unknown rule type %d",
			    rule->r_idtype);
	}
	mtx_unlock(&rule_mtx);

	if (error != 0 && portacl_suser_exempt != 0)
		error = priv_check_cred(cred, PRIV_NETINET_RESERVEDPORT, 0);

	return (error);
}

/*
 * Note, this only limits the ability to explicitly bind a port, it
 * doesn't limit implicitly bound ports for outgoing connections where
 * the source port is left up to the IP stack to determine automatically.
 */
static int
socket_check_bind(struct ucred *cred, struct socket *so,
    struct label *solabel, struct sockaddr *sa)
{
	struct sockaddr_in *sin;
	struct inpcb *inp;
	int family, type;
	u_int16_t port;

	/* Only run if we are enabled. */
	if (portacl_enabled == 0)
		return (0);

	/* Only interested in IPv4 and IPv6 sockets. */
	if (so->so_proto->pr_domain->dom_family != PF_INET &&
	    so->so_proto->pr_domain->dom_family != PF_INET6)
		return (0);

	/* Currently, we don't attempt to deal with SOCK_RAW, etc. */
	if (so->so_type != SOCK_DGRAM &&
	    so->so_type != SOCK_STREAM)
		return (0);

	/* Reject addresses we don't understand; fail closed. */
	if (sa->sa_family != AF_INET && sa->sa_family != AF_INET6)
		return (EINVAL);

	family = so->so_proto->pr_domain->dom_family;
	type = so->so_type;
	sin = (struct sockaddr_in *) sa;
	port = ntohs(sin->sin_port);

	/*
	 * Sockets are frequently bound with a specific IP address but a port
	 * number of '0' to request automatic port allocation.  This is often
	 * desirable as long as IP_PORTRANGELOW isn't set, which might permit
	 * automatic allocation of a "privileged" port.  The autoport exempt
	 * flag exempts port 0 allocation from rule checking as long as a low
	 * port isn't required.
	 */
	if (portacl_autoport_exempt && port == 0) {
		inp = sotoinpcb(so);
		if ((inp->inp_flags & INP_LOWPORT) == 0)
			return (0);
	}

	return (rules_check(cred, family, type, port));
}

static struct mac_policy_ops portacl_ops =
{
	.mpo_destroy = destroy,
	.mpo_init = init,
	.mpo_socket_check_bind = socket_check_bind,
};

MAC_POLICY_SET(&portacl_ops, mac_portacl, "TrustedBSD MAC/portacl",
    MPC_LOADTIME_FLAG_UNLOADOK, NULL);