33b1bdd079
Depending on the C library, but not necessarily in all the functions we use, statx() might be used instead of stat(), getdents() instead of getdents64(), readlinkat() instead of readlink(), openat() instead of open(). On aarch64, it's clone() and not fork(), and dup3() instead of dup2() -- just allow the existing alternative instead of dealing with per-arch selections. Since glibc commit 9a7565403758 ("posix: Consolidate fork implementation"), we need to allow set_robust_list() for fork()/clone(), even in a single-threaded context. On some architectures, epoll_pwait() is provided instead of epoll_wait(), but never both. Same with newfstat() and fstat(), sigreturn() and rt_sigreturn(), getdents64() and getdents(), readlink() and readlinkat(), unlink() and unlinkat(), whereas pipe() might not be available, but pipe2() always is, exclusively or not. Seen on Fedora 34: newfstatat() is used on top of fstat(). syslog() is an actual system call on some glibc/arch combinations, instead of a connect()/send() implementation. On ppc64 and ppc64le, _llseek(), recv(), send() and getuid() are used. For ppc64 only: ugetrlimit() for the getrlimit() implementation, plus sigreturn() and fcntl64(). On s390x, additionally, we need to allow socketcall() (on top of socket()), and sigreturn() also for passt (not just for pasta). Signed-off-by: Stefano Brivio <sbrivio@redhat.com>
400 lines
9.4 KiB
C
400 lines
9.4 KiB
C
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
/* PASST - Plug A Simple Socket Transport
|
|
* for qemu/UNIX domain socket mode
|
|
*
|
|
* PASTA - Pack A Subtle Tap Abstraction
|
|
* for network namespace/tap device mode
|
|
*
|
|
* passt.c - Daemon implementation
|
|
*
|
|
* Copyright (c) 2020-2021 Red Hat GmbH
|
|
* Author: Stefano Brivio <sbrivio@redhat.com>
|
|
*
|
|
* Grab Ethernet frames from AF_UNIX socket (in "passt" mode) or tap device (in
|
|
* "pasta" mode), build SOCK_DGRAM/SOCK_STREAM sockets for each 5-tuple from
|
|
* TCP, UDP packets, perform connection tracking and forward them. Forward
|
|
* packets received on sockets back to the UNIX domain socket (typically, a
|
|
* socket virtio_net file descriptor from qemu) or to the tap device (typically,
|
|
* created in a separate network namespace).
|
|
*/
|
|
|
|
#include <sched.h>
|
|
#include <stdio.h>
|
|
#include <sys/epoll.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/types.h>
|
|
#include <sys/stat.h>
|
|
#include <dirent.h>
|
|
#include <fcntl.h>
|
|
#include <sys/mman.h>
|
|
#include <sys/resource.h>
|
|
#include <sys/uio.h>
|
|
#include <sys/wait.h>
|
|
#include <netinet/ip.h>
|
|
#include <net/ethernet.h>
|
|
#include <stdlib.h>
|
|
#include <unistd.h>
|
|
#include <net/if.h>
|
|
#include <netdb.h>
|
|
#include <string.h>
|
|
#include <errno.h>
|
|
#include <time.h>
|
|
#include <syslog.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/prctl.h>
|
|
#include <stddef.h>
|
|
#include <pwd.h>
|
|
#include <grp.h>
|
|
#include <netinet/udp.h>
|
|
#include <netinet/tcp.h>
|
|
#include <netinet/if_ether.h>
|
|
|
|
#include <linux/seccomp.h>
|
|
#include <linux/audit.h>
|
|
#include <linux/filter.h>
|
|
#include <linux/capability.h>
|
|
#include <linux/icmpv6.h>
|
|
|
|
#include "util.h"
|
|
#include "seccomp.h"
|
|
#include "passt.h"
|
|
#include "dhcp.h"
|
|
#include "dhcpv6.h"
|
|
#include "icmp.h"
|
|
#include "tcp.h"
|
|
#include "udp.h"
|
|
#include "pcap.h"
|
|
#include "tap.h"
|
|
#include "conf.h"
|
|
#include "pasta.h"
|
|
|
|
#define EPOLL_EVENTS 8
|
|
|
|
#define __TIMER_INTERVAL MIN(TCP_TIMER_INTERVAL, UDP_TIMER_INTERVAL)
|
|
#define TIMER_INTERVAL MIN(__TIMER_INTERVAL, ICMP_TIMER_INTERVAL)
|
|
|
|
char pkt_buf[PKT_BUF_BYTES] __attribute__ ((aligned(PAGE_SIZE)));
|
|
|
|
char *ip_proto_str[IPPROTO_SCTP + 1] = {
|
|
[IPPROTO_ICMP] = "ICMP",
|
|
[IPPROTO_TCP] = "TCP",
|
|
[IPPROTO_UDP] = "UDP",
|
|
[IPPROTO_ICMPV6] = "ICMPV6",
|
|
[IPPROTO_SCTP] = "SCTP",
|
|
};
|
|
|
|
/**
|
|
* sock_handler() - Event handler for L4 sockets
|
|
* @c: Execution context
|
|
* @ref: epoll reference
|
|
* @events: epoll events
|
|
* @now: Current timestamp
|
|
*/
|
|
static void sock_handler(struct ctx *c, union epoll_ref ref, uint32_t events,
|
|
struct timespec *now)
|
|
{
|
|
debug("%s: %s packet from socket %i (events: 0x%08x)",
|
|
c->mode == MODE_PASST ? "passt" : "pasta",
|
|
IP_PROTO_STR(ref.r.proto), ref.r.s, events);
|
|
|
|
if (!c->no_tcp && ref.r.proto == IPPROTO_TCP)
|
|
tcp_sock_handler( c, ref, events, now);
|
|
else if (!c->no_udp && ref.r.proto == IPPROTO_UDP)
|
|
udp_sock_handler( c, ref, events, now);
|
|
else if (!c->no_icmp &&
|
|
(ref.r.proto == IPPROTO_ICMP || ref.r.proto == IPPROTO_ICMPV6))
|
|
icmp_sock_handler(c, ref, events, now);
|
|
}
|
|
|
|
/**
|
|
* post_handler() - Run periodic and deferred tasks for L4 protocol handlers
|
|
* @c: Execution context
|
|
* @now: Current timestamp
|
|
*/
|
|
static void post_handler(struct ctx *c, struct timespec *now)
|
|
{
|
|
#define CALL_PROTO_HANDLER(c, now, lc, uc) \
|
|
do { \
|
|
extern void \
|
|
lc ## _defer_handler (struct ctx *c) \
|
|
__attribute__ ((weak)); \
|
|
\
|
|
if (!c->no_ ## lc) { \
|
|
if (lc ## _defer_handler) \
|
|
lc ## _defer_handler(c); \
|
|
\
|
|
if (timespec_diff_ms((now), &c->lc.timer_run) \
|
|
>= uc ## _TIMER_INTERVAL) { \
|
|
lc ## _timer(c, now); \
|
|
c->lc.timer_run = *now; \
|
|
} \
|
|
} \
|
|
} while (0)
|
|
|
|
CALL_PROTO_HANDLER(c, now, tcp, TCP);
|
|
CALL_PROTO_HANDLER(c, now, udp, UDP);
|
|
CALL_PROTO_HANDLER(c, now, icmp, ICMP);
|
|
|
|
#undef CALL_PROTO_HANDLER
|
|
}
|
|
|
|
/**
|
|
* timer_init() - Set initial timestamp for timer runs to current time
|
|
* @c: Execution context
|
|
* @now: Current timestamp
|
|
*/
|
|
static void timer_init(struct ctx *c, struct timespec *now)
|
|
{
|
|
c->tcp.timer_run = c->udp.timer_run = c->icmp.timer_run = *now;
|
|
}
|
|
|
|
/**
|
|
* proto_update_l2_buf() - Update scatter-gather L2 buffers in protocol handlers
|
|
* @eth_d: Ethernet destination address, NULL if unchanged
|
|
* @eth_s: Ethernet source address, NULL if unchanged
|
|
* @ip_da: Pointer to IPv4 destination address, NULL if unchanged
|
|
*/
|
|
void proto_update_l2_buf(unsigned char *eth_d, unsigned char *eth_s,
|
|
uint32_t *ip_da)
|
|
{
|
|
tcp_update_l2_buf(eth_d, eth_s, ip_da);
|
|
udp_update_l2_buf(eth_d, eth_s, ip_da);
|
|
}
|
|
|
|
/**
|
|
* seccomp() - Set up seccomp filters depending on mode, won't return on failure
|
|
* @c: Execution context
|
|
*/
|
|
static void seccomp(struct ctx *c)
|
|
{
|
|
struct sock_fprog prog;
|
|
|
|
if (c->mode == MODE_PASST) {
|
|
prog.len = (unsigned short)ARRAY_SIZE(filter_passt);
|
|
prog.filter = filter_passt;
|
|
} else {
|
|
prog.len = (unsigned short)ARRAY_SIZE(filter_pasta);
|
|
prog.filter = filter_pasta;
|
|
}
|
|
|
|
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) ||
|
|
prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog)) {
|
|
perror("prctl");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* check_root() - Warn if root in init, exit if we can't drop to nobody
|
|
*/
|
|
static void check_root(void)
|
|
{
|
|
struct passwd *pw;
|
|
char buf[BUFSIZ];
|
|
int fd;
|
|
|
|
if (getuid() && geteuid())
|
|
return;
|
|
|
|
if ((fd = open("/proc/self/uid_map", O_RDONLY)) < 0)
|
|
return;
|
|
|
|
if (read(fd, buf, BUFSIZ) > 0 &&
|
|
strcmp(buf, " 0 0 4294967295")) {
|
|
close(fd);
|
|
return;
|
|
}
|
|
|
|
close(fd);
|
|
|
|
fprintf(stderr, "Don't run this as root. Changing to nobody...\n");
|
|
#ifndef GLIBC_NO_STATIC_NSS
|
|
pw = getpwnam("nobody");
|
|
if (!pw) {
|
|
perror("getpwnam");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
if (!initgroups(pw->pw_name, pw->pw_gid) &&
|
|
!setgid(pw->pw_gid) && !setuid(pw->pw_uid))
|
|
return;
|
|
#else
|
|
(void)pw;
|
|
#endif
|
|
|
|
fprintf(stderr, "Can't change to user/group nobody, exiting");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
/**
|
|
* drop_caps() - Drop capabilities we might have except for CAP_NET_BIND_SERVICE
|
|
*/
|
|
static void drop_caps(void)
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; i < 64; i++) {
|
|
if (i == CAP_NET_BIND_SERVICE)
|
|
continue;
|
|
|
|
prctl(PR_CAPBSET_DROP, i, 0, 0, 0);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* pid_file() - Write own PID to file, if configured
|
|
* @c: Execution context
|
|
*/
|
|
static void pid_file(struct ctx *c) {
|
|
char pid_buf[12];
|
|
int pid_fd, n;
|
|
|
|
if (!*c->pid_file)
|
|
return;
|
|
|
|
pid_fd = open(c->pid_file, O_CREAT | O_WRONLY, S_IRUSR | S_IWUSR);
|
|
if (pid_fd < 0)
|
|
return;
|
|
|
|
n = snprintf(pid_buf, sizeof(pid_buf), "%i\n", getpid());
|
|
|
|
if (write(pid_fd, pid_buf, n) < 0) {
|
|
perror("PID file write");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
close(pid_fd);
|
|
}
|
|
|
|
/**
|
|
* main() - Entry point and main loop
|
|
* @argc: Argument count
|
|
* @argv: Options, plus optional target PID for pasta mode
|
|
*
|
|
* Return: non-zero on failure
|
|
*
|
|
* #syscalls read write open|openat close fork|clone dup2|dup3 ioctl writev
|
|
* #syscalls socket bind connect getsockopt setsockopt recvfrom sendto shutdown
|
|
* #syscalls accept4 accept listen set_robust_list getrlimit setrlimit
|
|
* #syscalls openat fcntl lseek clone setsid exit exit_group getpid chdir
|
|
* #syscalls epoll_ctl epoll_create1 epoll_wait|epoll_pwait epoll_pwait
|
|
* #syscalls prlimit64 clock_gettime fstat|newfstat newfstatat syslog
|
|
* #syscalls ppc64le:_llseek ppc64le:recv ppc64le:send ppc64le:getuid
|
|
* #syscalls ppc64:_llseek ppc64:recv ppc64:send ppc64:getuid ppc64:ugetrlimit
|
|
* #syscalls s390x:socketcall s390x:sigreturn
|
|
* #syscalls:pasta rt_sigreturn|sigreturn ppc64:sigreturn ppc64:fcntl64
|
|
*/
|
|
int main(int argc, char **argv)
|
|
{
|
|
struct epoll_event events[EPOLL_EVENTS];
|
|
struct ctx c = { 0 };
|
|
struct rlimit limit;
|
|
struct timespec now;
|
|
char *log_name;
|
|
int nfds, i;
|
|
|
|
#ifndef PASST_LEGACY_NO_OPTIONS
|
|
check_root();
|
|
#endif
|
|
drop_caps();
|
|
|
|
if (strstr(argv[0], "pasta") || strstr(argv[0], "passt4netns")) {
|
|
struct sigaction sa;
|
|
|
|
sigemptyset(&sa.sa_mask);
|
|
sa.sa_flags = 0;
|
|
sa.sa_handler = pasta_child_handler;
|
|
sigaction(SIGCHLD, &sa, NULL);
|
|
signal(SIGPIPE, SIG_IGN);
|
|
|
|
c.mode = MODE_PASTA;
|
|
log_name = "pasta";
|
|
} else {
|
|
c.mode = MODE_PASST;
|
|
log_name = "passt";
|
|
}
|
|
|
|
if (madvise(pkt_buf, TAP_BUF_BYTES, MADV_HUGEPAGE))
|
|
perror("madvise");
|
|
|
|
__openlog(log_name, 0, LOG_DAEMON);
|
|
|
|
__setlogmask(LOG_MASK(LOG_EMERG));
|
|
|
|
conf(&c, argc, argv);
|
|
|
|
seccomp(&c);
|
|
|
|
if (!c.debug && (c.stderr || isatty(fileno(stdout))))
|
|
__openlog(log_name, LOG_PERROR, LOG_DAEMON);
|
|
|
|
c.epollfd = epoll_create1(0);
|
|
if (c.epollfd == -1) {
|
|
perror("epoll_create1");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
if (getrlimit(RLIMIT_NOFILE, &limit)) {
|
|
perror("getrlimit");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
limit.rlim_cur = limit.rlim_max;
|
|
if (setrlimit(RLIMIT_NOFILE, &limit)) {
|
|
perror("setrlimit");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
sock_probe_mem(&c);
|
|
|
|
tap_sock_init(&c);
|
|
|
|
clock_gettime(CLOCK_MONOTONIC, &now);
|
|
|
|
if ((!c.no_udp && udp_sock_init(&c, &now)) ||
|
|
(!c.no_tcp && tcp_sock_init(&c, &now)))
|
|
exit(EXIT_FAILURE);
|
|
|
|
proto_update_l2_buf(c.mac_guest, c.mac, &c.addr4);
|
|
|
|
if (c.v4 && !c.no_dhcp)
|
|
dhcp_init();
|
|
|
|
if (c.v6 && !c.no_dhcpv6)
|
|
dhcpv6_init(&c);
|
|
|
|
if (c.debug)
|
|
__setlogmask(LOG_UPTO(LOG_DEBUG));
|
|
else if (c.quiet)
|
|
__setlogmask(LOG_UPTO(LOG_ERR));
|
|
else
|
|
__setlogmask(LOG_UPTO(LOG_INFO));
|
|
|
|
if (!c.foreground && daemon(0, 0)) {
|
|
perror("daemon");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
pid_file(&c);
|
|
|
|
timer_init(&c, &now);
|
|
loop:
|
|
nfds = epoll_wait(c.epollfd, events, EPOLL_EVENTS, TIMER_INTERVAL);
|
|
if (nfds == -1 && errno != EINTR) {
|
|
perror("epoll_wait");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
clock_gettime(CLOCK_MONOTONIC, &now);
|
|
|
|
for (i = 0; i < nfds; i++) {
|
|
union epoll_ref ref = *((union epoll_ref *)&events[i].data.u64);
|
|
|
|
if (events[i].data.fd == c.fd_tap)
|
|
tap_handler(&c, events[i].events, &now);
|
|
else
|
|
sock_handler(&c, ref, events[i].events, &now);
|
|
}
|
|
|
|
post_handler(&c, &now);
|
|
|
|
goto loop;
|
|
}
|