Introduce a ndhc-sockd daemon that separates out the remaining elevated

capabilities from the ndhc master process.

Privsep is now complete.  The only notable improvement from before is that
exploitation of ndhc would only allow an attacker to open raw sockets,
bind sockets to ports < port 1024, and create broadcast sockets on the
interface that ndhc is performing dhcp on rather than on all interfaces.

However, this seems like a worthwhile change; note that it was already
impossible for an attacker to sniff packets on any interfaces (as that
requires CAP_NET_ADMIN, which was always separated to ifch).
This commit is contained in:
Nicholas J. Kain 2014-04-04 04:12:25 -04:00
parent 65c3cd4fd9
commit 09d6f7dfb8
8 changed files with 725 additions and 247 deletions

View File

@ -29,15 +29,10 @@
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/if_ether.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/time.h>
#include <linux/if_packet.h>
#include <linux/filter.h>
#include <fcntl.h>
#include <errno.h>
#include "nk/log.h"
#include "nk/io.h"
@ -48,6 +43,7 @@
#include "ifchange.h"
#include "options.h"
#include "leasefile.h"
#include "sockd.h"
#define ARP_MSG_SIZE 0x2a
#define ARP_RETRANS_DELAY 5000 // ms
@ -206,44 +202,23 @@ static void arp_set_bpf_defense(struct client_state_t *cs, int fd)
sizeof sfp_arp) != -1;
}
static int get_arp_socket(void)
{
return request_sockd_fd("a", 1, NULL);
}
static int arp_open_fd(struct client_state_t *cs)
{
if (cs->arpFd != -1)
return 0;
int fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ARP));
if (fd == -1) {
cs->arpFd = get_arp_socket();
if (cs->arpFd == -1) {
log_error("arp: Failed to create socket: %s", strerror(errno));
goto out;
return -1;
}
int opt = 1;
if (setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &opt, sizeof opt) == -1) {
log_error("arp: Failed to set broadcast: %s", strerror(errno));
goto out_fd;
}
if (fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK) == -1) {
log_error("arp: Failed to set non-blocking: %s", strerror(errno));
goto out_fd;
}
struct sockaddr_ll saddr = {
.sll_family = AF_PACKET,
.sll_protocol = htons(ETH_P_ARP),
.sll_ifindex = client_config.ifindex,
};
if (bind(fd, (struct sockaddr *)&saddr, sizeof(struct sockaddr_ll)) < 0) {
log_error("arp: bind failed: %s", strerror(errno));
goto out_fd;
}
cs->arpFd = fd;
epoll_add(cs->epollFd, fd);
epoll_add(cs->epollFd, cs->arpFd);
arpreply_clear();
return 0;
out_fd:
close(fd);
out:
return -1;
}
static void arp_switch_state(struct client_state_t *cs, arp_state_t state)

View File

@ -31,18 +31,10 @@
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <net/if.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <fcntl.h>
#include <features.h>
#include <netpacket/packet.h>
#include <net/ethernet.h>
#include <linux/filter.h>
#include <time.h>
#include <errno.h>
#include "nk/log.h"
#include "nk/io.h"
@ -54,6 +46,7 @@
#include "ifchange.h"
#include "sys.h"
#include "options.h"
#include "sockd.h"
typedef enum {
LM_NONE = 0,
@ -61,73 +54,33 @@ typedef enum {
LM_RAW
} listen_mode_t;
// Returns fd of new udp socket bound on success, or -1 on failure.
static int create_udp_socket(uint32_t ip, uint16_t port, char *iface)
static int get_udp_unicast_socket(void)
{
int fd;
if ((fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0) {
log_error("%s: (%s) socket failed: %s",
client_config.interface, __func__, strerror(errno));
goto out;
}
int opt = 1;
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof opt) == -1) {
log_error("%s: (%s) Set reuse addr failed: %s",
client_config.interface, __func__, strerror(errno));
goto out_fd;
}
if (setsockopt(fd, SOL_SOCKET, SO_DONTROUTE, &opt, sizeof opt) == -1) {
log_error("%s: (%s) Set don't route failed: %s",
client_config.interface, __func__, strerror(errno));
goto out_fd;
}
struct ifreq ifr;
memset(&ifr, 0, sizeof ifr);
ssize_t sl = snprintf(ifr.ifr_name, sizeof ifr.ifr_name, "%s", iface);
if (sl < 0 || (size_t)sl >= sizeof ifr.ifr_name) {
log_error("%s: (%s) Set interface name failed.",
client_config.interface, __func__);
goto out_fd;
}
if (setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, &ifr, sizeof ifr) < 0) {
log_error("%s: (%s) Set bind to device failed: %s",
client_config.interface, __func__, strerror(errno));
goto out_fd;
}
if (fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK) == -1) {
log_error("%s: (%s) Set non-blocking failed: %s",
client_config.interface, __func__, strerror(errno));
goto out_fd;
}
struct sockaddr_in sa = {
.sin_family = AF_INET,
.sin_port = htons(port),
.sin_addr.s_addr = ip,
};
if (bind(fd, (struct sockaddr *)&sa, sizeof sa) == -1)
goto out_fd;
return fd;
out_fd:
close(fd);
out:
return -1;
return request_sockd_fd("u", 1, NULL);
}
// Returns fd of new listen socket bound to 0.0.0.0:@68 on interface @inf
// on success, or -1 on failure.
static int create_udp_listen_socket(char *inf)
static int get_raw_broadcast_socket(void)
{
int fd = create_udp_socket(INADDR_ANY, DHCP_CLIENT_PORT, inf);
if (fd == -1)
return -1;
int opt = 1;
if (setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &opt, sizeof opt) == -1) {
log_error("%s: (%s) Set broadcast failed: %s",
client_config.interface, __func__, strerror(errno));
close(fd);
return -1;
return request_sockd_fd("s", 1, NULL);
}
static int get_udp_listen_socket(struct client_state_t *cs)
{
char buf[32];
buf[0] = 'u';
memcpy(buf + 1, &cs->clientAddr, sizeof cs->clientAddr);
return request_sockd_fd(buf, 1 + sizeof cs->clientAddr, NULL);
}
static int get_raw_listen_socket(struct client_state_t *cs)
{
char resp;
int fd = request_sockd_fd("L", 1, &resp);
switch (resp) {
case 'L': cs->using_dhcp_bpf = 1; break;
case 'l': cs->using_dhcp_bpf = 0; break;
default: suicide("%s: (%s) expected l or L sockd reply but got %c",
client_config.interface, __func__, resp);
}
return fd;
}
@ -136,8 +89,7 @@ static int create_udp_listen_socket(char *inf)
static int send_dhcp_cooked(struct client_state_t *cs, struct dhcpmsg *payload)
{
int ret = -1;
int fd = create_udp_socket(cs->clientAddr, DHCP_CLIENT_PORT,
client_config.interface);
int fd = get_udp_unicast_socket();
if (fd == -1)
goto out;
@ -324,110 +276,11 @@ static int get_raw_packet(struct client_state_t *cs, struct dhcpmsg *payload)
return l;
}
static int create_raw_socket(struct client_state_t *cs, struct sockaddr_ll *sa,
const struct sock_fprog *filter_prog)
{
int fd;
if ((fd = socket(AF_PACKET, SOCK_DGRAM, htons(ETH_P_IP))) < 0) {
log_error("create_raw_socket: socket failed: %s", strerror(errno));
goto out;
}
if (cs) {
if (filter_prog && (setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER,
filter_prog, sizeof *filter_prog) != -1))
cs->using_dhcp_bpf = 1;
else
cs->using_dhcp_bpf = 0;
}
int opt = 1;
if (setsockopt(fd, SOL_SOCKET, SO_DONTROUTE, &opt, sizeof opt) == -1) {
log_error("create_raw_socket: Failed to set don't route: %s",
strerror(errno));
goto out_fd;
}
if (fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK) == -1) {
log_error("create_raw_socket: Set non-blocking failed: %s",
strerror(errno));
goto out_fd;
}
if (bind(fd, (struct sockaddr *)sa, sizeof *sa) < 0) {
log_error("create_raw_socket: bind failed: %s", strerror(errno));
goto out_fd;
}
return fd;
out_fd:
close(fd);
out:
return -1;
}
static int create_raw_listen_socket(struct client_state_t *cs, int ifindex)
{
static const struct sock_filter sf_dhcp[] = {
// Verify that the packet has a valid IPv4 version nibble and
// that no IP options are defined.
BPF_STMT(BPF_LD + BPF_B + BPF_ABS, 0),
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, 0x45, 1, 0),
BPF_STMT(BPF_RET + BPF_K, 0),
// Verify that the IP header has a protocol number indicating UDP.
BPF_STMT(BPF_LD + BPF_B + BPF_ABS, 9),
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, IPPROTO_UDP, 1, 0),
BPF_STMT(BPF_RET + BPF_K, 0),
// Make certain that the packet is not a fragment. All bits in
// the flag and fragment offset field must be set to zero except
// for the Evil and DF bits (0,1).
BPF_STMT(BPF_LD + BPF_H + BPF_ABS, 6),
BPF_JUMP(BPF_JMP + BPF_JSET + BPF_K, 0x3fff, 0, 1),
BPF_STMT(BPF_RET + BPF_K, 0),
// Packet is UDP. Advance X past the IP header.
BPF_STMT(BPF_LDX + BPF_B + BPF_MSH, 0),
// Verify that the UDP client and server ports match that of the
// IANA-assigned DHCP ports.
BPF_STMT(BPF_LD + BPF_W + BPF_IND, 0),
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K,
(DHCP_SERVER_PORT << 16) + DHCP_CLIENT_PORT, 1, 0),
BPF_STMT(BPF_RET + BPF_K, 0),
// Get the UDP length field and store it in X.
BPF_STMT(BPF_LD + BPF_H + BPF_IND, 4),
BPF_STMT(BPF_MISC + BPF_TAX, 0),
// Get the IPv4 length field and store it in A and M[0].
BPF_STMT(BPF_LD + BPF_H + BPF_ABS, 2),
BPF_STMT(BPF_ST, 0),
// Verify that UDP length = IP length - IP header size
BPF_STMT(BPF_ALU + BPF_SUB + BPF_K, 20),
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_X, 0, 1, 0),
BPF_STMT(BPF_RET + BPF_K, 0),
// Pass the number of octets that are specified in the IPv4 header.
BPF_STMT(BPF_LD + BPF_MEM, 0),
BPF_STMT(BPF_RET + BPF_A, 0),
};
static const struct sock_fprog sfp_dhcp = {
.len = sizeof sf_dhcp / sizeof sf_dhcp[0],
.filter = (struct sock_filter *)sf_dhcp,
};
struct sockaddr_ll sa = {
.sll_family = AF_PACKET,
.sll_protocol = htons(ETH_P_IP),
.sll_ifindex = ifindex,
};
return create_raw_socket(cs, &sa, &sfp_dhcp);
}
// Broadcast a DHCP message using a raw socket.
static int send_dhcp_raw(struct dhcpmsg *payload)
{
int ret = -1;
struct sockaddr_ll da = {
.sll_family = AF_PACKET,
.sll_protocol = htons(ETH_P_IP),
.sll_pkttype = PACKET_BROADCAST,
.sll_ifindex = client_config.ifindex,
.sll_halen = 6,
};
memcpy(da.sll_addr, "\xff\xff\xff\xff\xff\xff", 6);
int fd = create_raw_socket(NULL, &da, NULL);
int fd = get_raw_broadcast_socket();
if (fd == -1)
return ret;
@ -459,19 +312,25 @@ static int send_dhcp_raw(struct dhcpmsg *payload)
.version = IPVERSION,
.ttl = IPDEFTTL,
},
.udp = {
.source = htons(DHCP_CLIENT_PORT),
.dest = htons(DHCP_SERVER_PORT),
.len = htons(ud_len),
.check = 0,
},
.data = *payload,
};
iudmsg.udp.source = htons(DHCP_CLIENT_PORT);
iudmsg.udp.dest = htons(DHCP_SERVER_PORT);
iudmsg.udp.len = htons(ud_len);
iudmsg.udp.check = 0;
uint16_t udpcs = net_checksum(&iudmsg.udp, ud_len);
uint16_t phcs = net_checksum(&ph, sizeof ph);
iudmsg.udp.check = net_checksum_add(udpcs, phcs);
iudmsg.ip.check = net_checksum(&iudmsg.ip, sizeof iudmsg.ip);
struct sockaddr_ll da = {
.sll_family = AF_PACKET,
.sll_protocol = htons(ETH_P_IP),
.sll_pkttype = PACKET_BROADCAST,
.sll_ifindex = client_config.ifindex,
.sll_halen = 6,
};
memcpy(da.sll_addr, "\xff\xff\xff\xff\xff\xff", 6);
ret = safe_sendto(fd, (const char *)&iudmsg, iud_len, 0,
(struct sockaddr *)&da, sizeof da);
if (ret == -1)
@ -490,11 +349,11 @@ static void change_listen_mode(struct client_state_t *cs, int new_mode)
close(cs->listenFd);
cs->listenFd = -1;
}
if (new_mode == LM_NONE)
return;
cs->listenFd = new_mode == LM_RAW ?
create_raw_listen_socket(cs, client_config.ifindex) :
create_udp_listen_socket(client_config.interface);
switch (cs->listenMode) {
default: return;
case LM_RAW: cs->listenFd = get_raw_listen_socket(cs); break;
case LM_COOKED: cs->listenFd = get_udp_listen_socket(cs); break;
}
if (cs->listenFd < 0)
suicide("%s: FATAL: Couldn't listen on socket: %s",
client_config.interface, strerror(errno));

View File

@ -70,6 +70,7 @@
#include "ifset.h"
#include "ifchd.h"
#include "duiaid.h"
#include "sockd.h"
struct client_state_t cs = {
.ifchWorking = 0,
@ -341,22 +342,86 @@ int pToNdhcW;
int pToIfchR;
int pToIfchW;
static void create_ipc_pipes(void) {
int psToNdhcR;
int psToNdhcW;
int pToSockdR;
int pToSockdW;
static void create_ifch_ipc_pipes(void) {
int niPipe[2];
int inPipe[2];
if (pipe2(niPipe, O_NONBLOCK))
if (pipe2(niPipe, 0))
suicide("FATAL - can't create ndhc -> ndhc-ifch pipe: %s",
strerror(errno));
if (fcntl(niPipe[0], F_SETFL, fcntl(niPipe[0], F_GETFL) | O_NONBLOCK) < 0)
suicide("FATAL - failed to set ndhc -> ndhc-ifch read-side nonblocking: %s",
strerror(errno));
pToNdhcR = niPipe[0];
pToNdhcW = niPipe[1];
if (pipe2(inPipe, O_NONBLOCK))
if (pipe2(inPipe, 0))
suicide("FATAL - can't create ndhc-ifch -> ndhc pipe: %s",
strerror(errno));
if (fcntl(inPipe[0], F_SETFL, fcntl(inPipe[0], F_GETFL) | O_NONBLOCK) < 0)
suicide("FATAL - failed to set ndhc-ifch -> ndhc read-side nonblocking: %s",
strerror(errno));
pToIfchR = inPipe[0];
pToIfchW = inPipe[1];
}
static void create_sockd_ipc_pipes(void) {
int nsPipe[2];
int snPipe[2];
if (pipe2(nsPipe, 0))
suicide("FATAL - can't create ndhc -> ndhc-sockd pipe: %s",
strerror(errno));
psToNdhcR = nsPipe[0];
psToNdhcW = nsPipe[1];
if (pipe2(snPipe, 0))
suicide("FATAL - can't create ndhc-sockd -> ndhc pipe: %s",
strerror(errno));
if (fcntl(snPipe[0], F_SETFL, fcntl(snPipe[0], F_GETFL) | O_NONBLOCK) < 0)
suicide("FATAL - failed to set ndhc-sockd -> ndhc read-side nonblocking: %s",
strerror(errno));
pToSockdR = snPipe[0];
pToSockdW = snPipe[1];
}
static void spawn_ifch(void)
{
create_ifch_ipc_pipes();
pid_t ifch_pid = fork();
if (ifch_pid == 0) {
close(pToNdhcR);
close(pToIfchW);
// Don't share the RNG state with the master process.
nk_random_u32_init(&cs.rnd32_state);
ifch_main();
} else if (ifch_pid > 0) {
close(pToIfchR);
close(pToNdhcW);
} else
suicide("failed to fork ndhc-ifch: %s", strerror(errno));
}
static void spawn_sockd(void)
{
create_sockd_ipc_pipes();
pid_t sockd_pid = fork();
if (sockd_pid == 0) {
close(psToNdhcR);
close(pToSockdW);
// Don't share the RNG state with the master process.
nk_random_u32_init(&cs.rnd32_state);
sockd_main();
} else if (sockd_pid > 0) {
close(pToSockdR);
close(psToNdhcW);
} else
suicide("failed to fork ndhc-sockd: %s", strerror(errno));
}
static void ndhc_main(void) {
prctl(PR_SET_NAME, "ndhc: master");
log_line("ndhc client " NDHC_VERSION " started on interface [%s].",
@ -376,8 +441,6 @@ static void ndhc_main(void) {
nk_set_chroot(chroot_dir);
memset(chroot_dir, '\0', sizeof chroot_dir);
nk_set_capability("cap_net_bind_service,cap_net_broadcast,cap_net_raw=ep");
nk_set_uidgid(ndhc_uid, ndhc_gid);
if (cs.ifsPrevState != IFS_UP)
@ -402,7 +465,7 @@ void background(void)
write_pid(pidfile);
}
int main(int argc, char **argv)
static void parse_program_options(int argc, char *argv[])
{
static const struct option arg_options[] = {
{"clientid", required_argument, 0, 'c'},
@ -417,6 +480,7 @@ int main(int argc, char **argv)
{"vendorid", required_argument, 0, 'V'},
{"user", required_argument, 0, 'u'},
{"ifch-user", required_argument, 0, 'U'},
{"sockd-user", required_argument, 0, 'D'},
{"chroot", required_argument, 0, 'C'},
{"state-dir", required_argument, 0, 's'},
{"seccomp-enforce", no_argument, 0, 'S'},
@ -435,7 +499,7 @@ int main(int argc, char **argv)
while (1) {
int c;
c = getopt_long(argc, argv, "c:bp:P:h:i:nqr:V:u:U:C:s:Sdw:W:m:M:t:R:Hv?",
c = getopt_long(argc, argv, "c:bp:P:h:i:nqr:V:u:U:D:C:s:Sdw:W:m:M:t:R:Hv?",
arg_options, NULL);
if (c == -1) break;
@ -479,6 +543,10 @@ int main(int argc, char **argv)
if (nk_uidgidbyname(optarg, &ifch_uid, &ifch_gid))
suicide("invalid ifch user '%s' specified", optarg);
break;
case 'D':
if (nk_uidgidbyname(optarg, &sockd_uid, &sockd_gid))
suicide("invalid sockd user '%s' specified", optarg);
break;
case 'C':
copy_cmdarg(chroot_dir, optarg, sizeof chroot_dir, "chroot");
break;
@ -567,6 +635,11 @@ int main(int argc, char **argv)
show_usage();
}
}
}
int main(int argc, char *argv[])
{
parse_program_options(argc, argv);
nk_random_u32_init(&cs.rnd32_state);
@ -582,28 +655,14 @@ int main(int argc, char **argv)
get_clientid(&cs, &client_config);
switch (perform_ifup()) {
case 1:
cs.ifsPrevState = IFS_UP;
case 0:
break;
default:
suicide("failed to set the interface to up state");
case 1: cs.ifsPrevState = IFS_UP;
case 0: break;
default: suicide("failed to set the interface to up state");
}
create_ipc_pipes();
pid_t ifch_pid = fork();
if (ifch_pid == 0) {
close(pToNdhcR);
close(pToIfchW);
// Don't share the RNG state with the master process.
nk_random_u32_init(&cs.rnd32_state);
ifch_main();
} else if (ifch_pid > 0) {
close(pToIfchR);
close(pToNdhcW);
ndhc_main();
} else
suicide("failed to fork ndhc-ifch: %s", strerror(errno));
spawn_ifch();
spawn_sockd();
ndhc_main();
exit(EXIT_SUCCESS);
}

View File

@ -71,6 +71,10 @@ extern int pToIfchR;
extern int pToIfchW;
extern int pToNdhcR;
extern int pToNdhcW;
extern int psToNdhcR;
extern int psToNdhcW;
extern int pToSockdR;
extern int pToSockdW;
extern char state_dir[PATH_MAX];
extern char chroot_dir[PATH_MAX];
extern char resolv_conf_d[PATH_MAX];

View File

@ -173,3 +173,63 @@ int enforce_seccomp_ifch(void)
return 0;
}
int enforce_seccomp_sockd(void)
{
#ifdef ENABLE_SECCOMP_FILTER
if (!seccomp_enforce)
return 0;
struct sock_filter filter[] = {
VALIDATE_ARCHITECTURE,
EXAMINE_SYSCALL,
ALLOW_SYSCALL(epoll_wait),
ALLOW_SYSCALL(epoll_ctl),
ALLOW_SYSCALL(read),
ALLOW_SYSCALL(write),
ALLOW_SYSCALL(close),
#if defined(__x86_64__) || (defined(__arm__) && defined(__ARM_EABI__))
ALLOW_SYSCALL(sendto), // used for glibc syslog routines
ALLOW_SYSCALL(recvmsg),
ALLOW_SYSCALL(socket),
ALLOW_SYSCALL(setsockopt),
ALLOW_SYSCALL(getsockname),
ALLOW_SYSCALL(connect),
ALLOW_SYSCALL(bind),
ALLOW_SYSCALL(socketpair),
#elif defined(__i386__)
ALLOW_SYSCALL(socketcall),
ALLOW_SYSCALL(fcntl64),
#else
#error Target platform does not support seccomp-filter.
#endif
ALLOW_SYSCALL(fcntl),
ALLOW_SYSCALL(open),
// Allowed by vDSO
ALLOW_SYSCALL(getcpu),
ALLOW_SYSCALL(time),
ALLOW_SYSCALL(gettimeofday),
ALLOW_SYSCALL(clock_gettime),
ALLOW_SYSCALL(rt_sigreturn),
#ifdef __NR_sigreturn
ALLOW_SYSCALL(sigreturn),
#endif
ALLOW_SYSCALL(exit_group),
ALLOW_SYSCALL(exit),
KILL_PROCESS,
};
struct sock_fprog prog = {
.len = (unsigned short)(sizeof filter / sizeof filter[0]),
.filter = filter,
};
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0))
return -1;
if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog))
return -1;
log_line("ndhc-sockd seccomp filter installed. Please disable seccomp if you encounter problems.");
#endif
return 0;
}

View File

@ -34,5 +34,6 @@ extern bool seccomp_enforce;
int enforce_seccomp_ndhc(void);
int enforce_seccomp_ifch(void);
int enforce_seccomp_sockd(void);
#endif /* NJK_NDHC_SECCOMP_H_ */

511
ndhc/sockd.c Normal file
View File

@ -0,0 +1,511 @@
/* sockd.c - privsep socket creation daemon
*
* Copyright (c) 2014 Nicholas J. Kain <njkain at gmail dot com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* - 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 COPYRIGHT HOLDERS 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 COPYRIGHT HOLDER 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.
*/
#include <stdbool.h>
#include <stdint.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/signalfd.h>
#include <sys/epoll.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/prctl.h>
#include <arpa/inet.h>
#include <netinet/ip.h>
#include <netpacket/packet.h>
#include <net/ethernet.h>
#include <linux/filter.h>
#include <pwd.h>
#include <grp.h>
#include "nk/log.h"
#include "nk/io.h"
#include "nk/privilege.h"
#include "sockd.h"
#include "ndhc-defines.h"
#include "ndhc.h"
#include "dhcp.h"
#include "sys.h"
#include "seccomp.h"
static int epollfd, signalFd;
/* Slots are for signalFd and the ndhc -> ifchd pipe. */
static struct epoll_event events[2];
uid_t sockd_uid = 0;
gid_t sockd_gid = 0;
// Interface to make requests of sockd. Called from ndhc process.
int request_sockd_fd(char *buf, size_t buflen, char *response)
{
if (!buflen)
return -1;
ssize_t r = safe_write(pToSockdW, buf, buflen);
if (r < 0 || (size_t)r != buflen)
suicide("%s: (%s) write failed: %d", client_config.interface,
__func__, r);
char data[MAX_BUF], control[MAX_BUF];
struct iovec iov = {
.iov_base = data,
.iov_len = sizeof data - 1,
};
struct msghdr msg = {
.msg_iov = &iov,
.msg_iovlen = 1,
.msg_control = control,
.msg_controllen = sizeof control
};
retry:
r = recvmsg(psToNdhcR, &msg, 0);
if (r == 0) {
suicide("%s: (%s) recvmsg received EOF", client_config.interface,
__func__);
} else if (r < 0) {
if (errno == EINTR)
goto retry;
suicide("%s: (%s) recvmsg failed: %s", client_config.interface,
__func__, strerror(errno));
}
data[iov.iov_len] = '\0';
char repc = data[0];
for (struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); cmsg;
cmsg = CMSG_NXTHDR(&msg, cmsg)) {
if (cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SCM_RIGHTS) {
if (response)
*response = repc;
else if (repc != buf[0])
suicide("%s: (%s) expected %c sockd reply but got %c",
client_config.interface, __func__, buf[0], repc);
int *fd = (int *)CMSG_DATA(cmsg);
return *fd;
}
}
suicide("%s: (%s) sockd reply did not include a fd",
client_config.interface, __func__);
}
static int create_arp_socket(void)
{
int fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ARP));
if (fd == -1) {
log_error("%s: (%s) socket failed: %s", client_config.interface,
__func__, strerror(errno));
goto out;
}
int opt = 1;
if (setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &opt, sizeof opt) == -1) {
log_error("%s: (%s) setsockopt failed: %s", client_config.interface,
__func__, strerror(errno));
goto out_fd;
}
if (fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK) == -1) {
log_error("%s: (%s) fcntl failed: %s", client_config.interface,
__func__, strerror(errno));
goto out_fd;
}
struct sockaddr_ll saddr = {
.sll_family = AF_PACKET,
.sll_protocol = htons(ETH_P_ARP),
.sll_ifindex = client_config.ifindex,
};
if (bind(fd, (struct sockaddr *)&saddr, sizeof(struct sockaddr_ll)) < 0) {
log_error("%s: (%s) bind failed: %s", client_config.interface,
__func__, strerror(errno));
goto out_fd;
}
return fd;
out_fd:
close(fd);
out:
return -1;
}
// Returns fd of new udp socket bound on success, or -1 on failure.
static int create_udp_socket(uint32_t ip, uint16_t port, char *iface)
{
int fd;
if ((fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0) {
log_error("%s: (%s) socket failed: %s",
client_config.interface, __func__, strerror(errno));
goto out;
}
int opt = 1;
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof opt) == -1) {
log_error("%s: (%s) Set reuse addr failed: %s",
client_config.interface, __func__, strerror(errno));
goto out_fd;
}
if (setsockopt(fd, SOL_SOCKET, SO_DONTROUTE, &opt, sizeof opt) == -1) {
log_error("%s: (%s) Set don't route failed: %s",
client_config.interface, __func__, strerror(errno));
goto out_fd;
}
struct ifreq ifr;
memset(&ifr, 0, sizeof ifr);
ssize_t sl = snprintf(ifr.ifr_name, sizeof ifr.ifr_name, "%s", iface);
if (sl < 0 || (size_t)sl >= sizeof ifr.ifr_name) {
log_error("%s: (%s) Set interface name failed.",
client_config.interface, __func__);
goto out_fd;
}
if (setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, &ifr, sizeof ifr) < 0) {
log_error("%s: (%s) Set bind to device failed: %s",
client_config.interface, __func__, strerror(errno));
goto out_fd;
}
if (fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK) == -1) {
log_error("%s: (%s) Set non-blocking failed: %s",
client_config.interface, __func__, strerror(errno));
goto out_fd;
}
struct sockaddr_in sa = {
.sin_family = AF_INET,
.sin_port = htons(port),
.sin_addr.s_addr = ip,
};
if (bind(fd, (struct sockaddr *)&sa, sizeof sa) == -1)
goto out_fd;
return fd;
out_fd:
close(fd);
out:
return -1;
}
static int create_raw_socket(struct sockaddr_ll *sa, bool *using_bpf,
const struct sock_fprog *filter_prog)
{
int fd;
if ((fd = socket(AF_PACKET, SOCK_DGRAM, htons(ETH_P_IP))) < 0) {
log_error("create_raw_socket: socket failed: %s", strerror(errno));
goto out;
}
if (filter_prog) {
int r = setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, filter_prog,
sizeof *filter_prog);
if (using_bpf)
*using_bpf = !r;
}
int opt = 1;
if (setsockopt(fd, SOL_SOCKET, SO_DONTROUTE, &opt, sizeof opt) == -1) {
log_error("create_raw_socket: Failed to set don't route: %s",
strerror(errno));
goto out_fd;
}
if (fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK) == -1) {
log_error("create_raw_socket: Set non-blocking failed: %s",
strerror(errno));
goto out_fd;
}
if (bind(fd, (struct sockaddr *)sa, sizeof *sa) < 0) {
log_error("create_raw_socket: bind failed: %s", strerror(errno));
goto out_fd;
}
return fd;
out_fd:
close(fd);
out:
return -1;
}
// Returns fd of new listen socket bound to 0.0.0.0:@68 on interface @inf
// on success, or -1 on failure.
static int create_udp_listen_socket(void)
{
int fd = create_udp_socket(INADDR_ANY, DHCP_CLIENT_PORT,
client_config.interface);
if (fd == -1)
return -1;
int opt = 1;
if (setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &opt, sizeof opt) == -1) {
log_error("%s: (%s) Set broadcast failed: %s",
client_config.interface, __func__, strerror(errno));
close(fd);
return -1;
}
return fd;
}
static int create_udp_send_socket(uint32_t client_addr)
{
return create_udp_socket(client_addr, DHCP_CLIENT_PORT,
client_config.interface);
}
static int create_raw_listen_socket(bool *using_bpf)
{
static const struct sock_filter sf_dhcp[] = {
// Verify that the packet has a valid IPv4 version nibble and
// that no IP options are defined.
BPF_STMT(BPF_LD + BPF_B + BPF_ABS, 0),
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, 0x45, 1, 0),
BPF_STMT(BPF_RET + BPF_K, 0),
// Verify that the IP header has a protocol number indicating UDP.
BPF_STMT(BPF_LD + BPF_B + BPF_ABS, 9),
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, IPPROTO_UDP, 1, 0),
BPF_STMT(BPF_RET + BPF_K, 0),
// Make certain that the packet is not a fragment. All bits in
// the flag and fragment offset field must be set to zero except
// for the Evil and DF bits (0,1).
BPF_STMT(BPF_LD + BPF_H + BPF_ABS, 6),
BPF_JUMP(BPF_JMP + BPF_JSET + BPF_K, 0x3fff, 0, 1),
BPF_STMT(BPF_RET + BPF_K, 0),
// Packet is UDP. Advance X past the IP header.
BPF_STMT(BPF_LDX + BPF_B + BPF_MSH, 0),
// Verify that the UDP client and server ports match that of the
// IANA-assigned DHCP ports.
BPF_STMT(BPF_LD + BPF_W + BPF_IND, 0),
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K,
(DHCP_SERVER_PORT << 16) + DHCP_CLIENT_PORT, 1, 0),
BPF_STMT(BPF_RET + BPF_K, 0),
// Get the UDP length field and store it in X.
BPF_STMT(BPF_LD + BPF_H + BPF_IND, 4),
BPF_STMT(BPF_MISC + BPF_TAX, 0),
// Get the IPv4 length field and store it in A and M[0].
BPF_STMT(BPF_LD + BPF_H + BPF_ABS, 2),
BPF_STMT(BPF_ST, 0),
// Verify that UDP length = IP length - IP header size
BPF_STMT(BPF_ALU + BPF_SUB + BPF_K, 20),
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_X, 0, 1, 0),
BPF_STMT(BPF_RET + BPF_K, 0),
// Pass the number of octets that are specified in the IPv4 header.
BPF_STMT(BPF_LD + BPF_MEM, 0),
BPF_STMT(BPF_RET + BPF_A, 0),
};
static const struct sock_fprog sfp_dhcp = {
.len = sizeof sf_dhcp / sizeof sf_dhcp[0],
.filter = (struct sock_filter *)sf_dhcp,
};
struct sockaddr_ll sa = {
.sll_family = AF_PACKET,
.sll_protocol = htons(ETH_P_IP),
.sll_ifindex = client_config.ifindex,
};
return create_raw_socket(&sa, using_bpf, &sfp_dhcp);
}
static int create_raw_broadcast_socket(void)
{
struct sockaddr_ll da = {
.sll_family = AF_PACKET,
.sll_protocol = htons(ETH_P_IP),
.sll_pkttype = PACKET_BROADCAST,
.sll_ifindex = client_config.ifindex,
.sll_halen = 6,
};
memcpy(da.sll_addr, "\xff\xff\xff\xff\xff\xff", 6);
return create_raw_socket(&da, NULL, NULL);
}
// XXX: Can share with ifch
static void setup_signals_sockd(void)
{
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGPIPE);
sigaddset(&mask, SIGUSR1);
sigaddset(&mask, SIGUSR2);
sigaddset(&mask, SIGTSTP);
sigaddset(&mask, SIGTTIN);
sigaddset(&mask, SIGCHLD);
sigaddset(&mask, SIGHUP);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGTERM);
if (sigprocmask(SIG_BLOCK, &mask, NULL) < 0)
suicide("sigprocmask failed");
signalFd = signalfd(-1, &mask, SFD_NONBLOCK);
if (signalFd < 0)
suicide("signalfd failed");
}
// XXX: Can share with ifch
static void signal_dispatch(void)
{
int t;
size_t off = 0;
struct signalfd_siginfo si = {0};
again:
t = read(signalFd, (char *)&si + off, sizeof si - off);
if (t < 0) {
if (t == EAGAIN || t == EWOULDBLOCK || t == EINTR)
goto again;
else
suicide("signalfd read error");
}
if (off + (unsigned)t < sizeof si)
off += t;
switch (si.ssi_signo) {
case SIGINT:
case SIGTERM:
exit(EXIT_SUCCESS);
break;
case SIGPIPE:
log_line("ndhc-sockd: IPC pipe closed. Exiting.");
exit(EXIT_SUCCESS);
break;
default:
break;
}
}
static void xfer_fd(int fd, char cmd)
{
char control[sizeof(struct cmsghdr) + 10];
struct iovec iov = {
.iov_base = &cmd,
.iov_len = 1,
};
struct msghdr msg = {
.msg_iov = &iov,
.msg_iovlen = 1,
.msg_control = control,
.msg_controllen = sizeof control,
};
struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
cmsg->cmsg_len = CMSG_LEN(sizeof fd);
int *cmsg_fd = (int *)CMSG_DATA(cmsg);
*cmsg_fd = fd;
msg.msg_controllen = cmsg->cmsg_len;
retry:
if (sendmsg(psToNdhcW, &msg, 0) < 0) {
if (errno == EINTR)
goto retry;
suicide("%s: (%s) sendmsg failed: %s", client_config.interface,
__func__, strerror(errno));
}
}
static size_t execute_sockd(char *buf, size_t buflen)
{
if (!buflen)
return 0;
char c = buf[0];
switch (c) {
case 'L': {
bool using_bpf;
int fd = create_raw_listen_socket(&using_bpf);
xfer_fd(fd, using_bpf ? 'L' : 'l');
return 1;
}
case 'U': xfer_fd(create_udp_listen_socket(), 'U'); return 1;
case 'a': xfer_fd(create_arp_socket(), 'a'); return 1;
case 's': xfer_fd(create_raw_broadcast_socket(), 's'); return 1;
case 'u': {
uint32_t client_addr;
if (buflen < 1 + sizeof client_addr)
return 0;
memcpy(&client_addr, buf + 1, sizeof client_addr);
xfer_fd(create_udp_send_socket(client_addr), 'u');
return 5;
}
default: suicide("%s: (%s) received invalid commands: '%c'",
client_config.interface, __func__, c);
}
}
static void process_client_pipe(void)
{
static char buf[MAX_BUF];
static size_t buflen;
if (buflen == MAX_BUF)
suicide("%s: (%s) receive buffer exhausted", client_config.interface,
__func__);
int r = safe_read(pToSockdR, buf + buflen, sizeof buf - buflen);
if (r == 0) {
// Remote end hung up.
exit(EXIT_SUCCESS);
} else if (r < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK)
return;
suicide("%s: (%s) error reading from ndhc -> sockd pipe: %s",
client_config.interface, __func__, strerror(errno));
}
buflen += (size_t)r;
buflen -= execute_sockd(buf, buflen);
}
static void do_sockd_work(void)
{
epollfd = epoll_create1(0);
if (epollfd == -1)
suicide("epoll_create1 failed");
if (enforce_seccomp_sockd())
log_line("sockd seccomp filter cannot be installed");
epoll_add(epollfd, pToSockdR);
epoll_add(epollfd, signalFd);
for (;;) {
int r = epoll_wait(epollfd, events, 2, -1);
if (r == -1) {
if (errno == EINTR)
continue;
else
suicide("epoll_wait failed");
}
for (int i = 0; i < r; ++i) {
int fd = events[i].data.fd;
if (fd == pToSockdR)
process_client_pipe();
else if (fd == signalFd)
signal_dispatch();
else
suicide("sockd: unexpected fd while performing epoll");
}
}
}
void sockd_main(void)
{
prctl(PR_SET_NAME, "ndhc: sockd");
umask(077);
setup_signals_sockd();
nk_set_chroot(chroot_dir);
memset(chroot_dir, 0, sizeof chroot_dir);
nk_set_capability("cap_net_bind_service,cap_net_broadcast,cap_net_raw=ep");
nk_set_uidgid(sockd_uid, sockd_gid);
do_sockd_work();
}

9
ndhc/sockd.h Normal file
View File

@ -0,0 +1,9 @@
#ifndef NDHC_SOCKD_H_
#define NDHC_SOCKD_H_
extern uid_t sockd_uid;
extern gid_t sockd_gid;
int request_sockd_fd(char *buf, size_t buflen, char *response);
void sockd_main(void);
#endif /* NDHC_SOCKD_H_ */