mirror of
https://github.com/okhsunrog/vpnhide.git
synced 2026-07-25 08:53:52 +00:00
362 lines
8.2 KiB
C
362 lines
8.2 KiB
C
// SPDX-License-Identifier: MIT
|
|
/*
|
|
* State-aware SO_BINDTODEVICE / SO_BINDTOIFINDEX probe for the QEMU harness.
|
|
*
|
|
* The actor runs as uid 5555 and issues setsockopt through an inline arm64
|
|
* `svc`, bypassing libc. The socket itself is created by the root parent before
|
|
* fork, so after the actor exits a separate non-target observer (uid 5556) can
|
|
* inspect the very same open file description. This catches the dangerous
|
|
* failure mode where a hook returns ENODEV after the kernel has already changed
|
|
* sk_bound_dev_if; checking errno alone cannot distinguish that from a real
|
|
* pre-mutation denial.
|
|
*
|
|
* Output is deliberately line-oriented for init.sh / init-kpm.sh:
|
|
*
|
|
* BIND_NAME_RAW_ERRNO=0
|
|
* BIND_NAME_RAW_STATE=1
|
|
*
|
|
* errno is positive (0 on success). STATE is 0 for unbound, 1 for the
|
|
* expected interface, 2 for a different interface, or a negative errno if the
|
|
* observation itself failed.
|
|
*/
|
|
|
|
#include <errno.h>
|
|
#include <linux/if.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/syscall.h>
|
|
#include <sys/types.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
|
|
#ifndef SO_BINDTODEVICE
|
|
#define SO_BINDTODEVICE 25
|
|
#endif
|
|
|
|
#ifndef SO_BINDTOIFINDEX
|
|
#define SO_BINDTOIFINDEX 62
|
|
#endif
|
|
|
|
#ifndef __NR_setsockopt
|
|
#define __NR_setsockopt 208
|
|
#endif
|
|
|
|
#ifndef __NR_getsockopt
|
|
#define __NR_getsockopt 209
|
|
#endif
|
|
|
|
#define ACTOR_UID 5555
|
|
#define OBSERVER_UID 5556
|
|
|
|
struct call_result {
|
|
int ret;
|
|
int err;
|
|
};
|
|
|
|
static long raw_syscall5(long nr, long a0, long a1, long a2, long a3, long a4)
|
|
{
|
|
#if defined(__aarch64__)
|
|
register long x0 __asm__("x0") = a0;
|
|
register long x1 __asm__("x1") = a1;
|
|
register long x2 __asm__("x2") = a2;
|
|
register long x3 __asm__("x3") = a3;
|
|
register long x4 __asm__("x4") = a4;
|
|
register long x8 __asm__("x8") = nr;
|
|
|
|
__asm__ volatile("svc #0"
|
|
: "+r"(x0)
|
|
: "r"(x1), "r"(x2), "r"(x3), "r"(x4), "r"(x8)
|
|
: "memory", "cc");
|
|
return x0;
|
|
#else
|
|
#error "bind-probe is intentionally arm64-only"
|
|
#endif
|
|
}
|
|
|
|
static long raw_setsockopt(int fd, int level, int optname, const void *optval,
|
|
socklen_t optlen)
|
|
{
|
|
return raw_syscall5(__NR_setsockopt, fd, level, optname, (long)optval,
|
|
optlen);
|
|
}
|
|
|
|
static long raw_getsockopt(int fd, int level, int optname, void *optval,
|
|
socklen_t *optlen)
|
|
{
|
|
return raw_syscall5(__NR_getsockopt, fd, level, optname, (long)optval,
|
|
(long)optlen);
|
|
}
|
|
|
|
static int write_full(int fd, const void *buf, size_t len)
|
|
{
|
|
const char *p = buf;
|
|
|
|
while (len) {
|
|
ssize_t n = write(fd, p, len);
|
|
if (n < 0) {
|
|
if (errno == EINTR)
|
|
continue;
|
|
return -1;
|
|
}
|
|
p += n;
|
|
len -= (size_t)n;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int read_full(int fd, void *buf, size_t len)
|
|
{
|
|
char *p = buf;
|
|
|
|
while (len) {
|
|
ssize_t n = read(fd, p, len);
|
|
if (n < 0) {
|
|
if (errno == EINTR)
|
|
continue;
|
|
return -1;
|
|
}
|
|
if (n == 0)
|
|
return -1;
|
|
p += n;
|
|
len -= (size_t)n;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static struct call_result actor_setsockopt(int fd, int optname,
|
|
const void *optval, socklen_t optlen)
|
|
{
|
|
int pipefd[2];
|
|
pid_t pid;
|
|
struct call_result result = { .ret = -1, .err = EIO };
|
|
int status;
|
|
|
|
if (pipe(pipefd) != 0)
|
|
return result;
|
|
pid = fork();
|
|
if (pid < 0) {
|
|
close(pipefd[0]);
|
|
close(pipefd[1]);
|
|
return result;
|
|
}
|
|
if (pid == 0) {
|
|
long rc;
|
|
|
|
close(pipefd[0]);
|
|
if (setuid(ACTOR_UID) != 0) {
|
|
result.ret = -1;
|
|
result.err = errno;
|
|
} else {
|
|
rc = raw_setsockopt(fd, SOL_SOCKET, optname, optval,
|
|
optlen);
|
|
if (rc < 0) {
|
|
result.ret = -1;
|
|
result.err = (int)-rc;
|
|
} else {
|
|
result.ret = (int)rc;
|
|
result.err = 0;
|
|
}
|
|
}
|
|
(void)write_full(pipefd[1], &result, sizeof(result));
|
|
close(pipefd[1]);
|
|
_exit(0);
|
|
}
|
|
|
|
close(pipefd[1]);
|
|
if (read_full(pipefd[0], &result, sizeof(result)) != 0) {
|
|
result.ret = -1;
|
|
result.err = EIO;
|
|
}
|
|
close(pipefd[0]);
|
|
while (waitpid(pid, &status, 0) < 0 && errno == EINTR)
|
|
;
|
|
return result;
|
|
}
|
|
|
|
static int observer_name_state(int fd, const char *expected)
|
|
{
|
|
int pipefd[2];
|
|
pid_t pid;
|
|
int state = -EIO;
|
|
int status;
|
|
|
|
if (pipe(pipefd) != 0)
|
|
return state;
|
|
pid = fork();
|
|
if (pid < 0) {
|
|
close(pipefd[0]);
|
|
close(pipefd[1]);
|
|
return state;
|
|
}
|
|
if (pid == 0) {
|
|
char name[IFNAMSIZ] = { 0 };
|
|
socklen_t len = sizeof(name);
|
|
long rc;
|
|
|
|
close(pipefd[0]);
|
|
if (setuid(OBSERVER_UID) != 0) {
|
|
state = -errno;
|
|
} else {
|
|
rc = raw_getsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE,
|
|
name, &len);
|
|
if (rc < 0)
|
|
state = (int)rc;
|
|
else if (len == 0 || name[0] == '\0')
|
|
state = 0;
|
|
else if (strcmp(name, expected) == 0)
|
|
state = 1;
|
|
else
|
|
state = 2;
|
|
}
|
|
(void)write_full(pipefd[1], &state, sizeof(state));
|
|
close(pipefd[1]);
|
|
_exit(0);
|
|
}
|
|
|
|
close(pipefd[1]);
|
|
if (read_full(pipefd[0], &state, sizeof(state)) != 0)
|
|
state = -EIO;
|
|
close(pipefd[0]);
|
|
while (waitpid(pid, &status, 0) < 0 && errno == EINTR)
|
|
;
|
|
return state;
|
|
}
|
|
|
|
static int observer_index_state(int fd, int expected)
|
|
{
|
|
int pipefd[2];
|
|
pid_t pid;
|
|
int state = -EIO;
|
|
int status;
|
|
|
|
if (pipe(pipefd) != 0)
|
|
return state;
|
|
pid = fork();
|
|
if (pid < 0) {
|
|
close(pipefd[0]);
|
|
close(pipefd[1]);
|
|
return state;
|
|
}
|
|
if (pid == 0) {
|
|
int ifindex = -1;
|
|
socklen_t len = sizeof(ifindex);
|
|
long rc;
|
|
|
|
close(pipefd[0]);
|
|
if (setuid(OBSERVER_UID) != 0) {
|
|
state = -errno;
|
|
} else {
|
|
rc = raw_getsockopt(fd, SOL_SOCKET, SO_BINDTOIFINDEX,
|
|
&ifindex, &len);
|
|
if (rc < 0)
|
|
state = (int)rc;
|
|
else if (ifindex == 0)
|
|
state = 0;
|
|
else if (ifindex == expected)
|
|
state = 1;
|
|
else
|
|
state = 2;
|
|
}
|
|
(void)write_full(pipefd[1], &state, sizeof(state));
|
|
close(pipefd[1]);
|
|
_exit(0);
|
|
}
|
|
|
|
close(pipefd[1]);
|
|
if (read_full(pipefd[0], &state, sizeof(state)) != 0)
|
|
state = -EIO;
|
|
close(pipefd[0]);
|
|
while (waitpid(pid, &status, 0) < 0 && errno == EINTR)
|
|
;
|
|
return state;
|
|
}
|
|
|
|
static void run_name_case(const char *label, const char *ifname,
|
|
socklen_t optlen)
|
|
{
|
|
int fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
|
|
struct call_result result;
|
|
int state;
|
|
|
|
if (fd < 0) {
|
|
printf("%s_ERRNO=%d\n%s_STATE=%d\n", label, errno, label,
|
|
-errno);
|
|
return;
|
|
}
|
|
result = actor_setsockopt(fd, SO_BINDTODEVICE, ifname, optlen);
|
|
state = observer_name_state(fd, ifname);
|
|
printf("%s_ERRNO=%d\n%s_STATE=%d\n", label, result.err, label, state);
|
|
close(fd);
|
|
}
|
|
|
|
static void run_index_case(const char *label, int ifindex)
|
|
{
|
|
int fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
|
|
struct call_result result;
|
|
int state;
|
|
|
|
if (fd < 0) {
|
|
printf("%s_ERRNO=%d\n%s_STATE=%d\n", label, errno, label,
|
|
-errno);
|
|
return;
|
|
}
|
|
result = actor_setsockopt(fd, SO_BINDTOIFINDEX, &ifindex,
|
|
sizeof(ifindex));
|
|
state = observer_index_state(fd, ifindex);
|
|
printf("%s_ERRNO=%d\n%s_STATE=%d\n", label, result.err, label, state);
|
|
close(fd);
|
|
}
|
|
|
|
static void run_bad_pointer_case(const char *label, const char *expected)
|
|
{
|
|
const void *bad = (const void *)(uintptr_t)0xfffffffffffff000ULL;
|
|
int fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
|
|
struct call_result result;
|
|
int state;
|
|
|
|
if (fd < 0) {
|
|
printf("%s_ERRNO=%d\n%s_STATE=%d\n", label, errno, label,
|
|
-errno);
|
|
return;
|
|
}
|
|
result = actor_setsockopt(fd, SO_BINDTODEVICE, bad, 4);
|
|
state = observer_name_state(fd, expected);
|
|
printf("%s_ERRNO=%d\n%s_STATE=%d\n", label, result.err, label, state);
|
|
close(fd);
|
|
}
|
|
|
|
int main(int argc, char **argv)
|
|
{
|
|
const char *vpn_name;
|
|
int vpn_ifindex;
|
|
|
|
if (argc != 3) {
|
|
fprintf(stderr, "usage: %s <vpn-ifname> <vpn-ifindex>\n",
|
|
argv[0]);
|
|
return 2;
|
|
}
|
|
vpn_name = argv[1];
|
|
vpn_ifindex = atoi(argv[2]);
|
|
if (vpn_ifindex <= 0) {
|
|
fprintf(stderr, "invalid vpn ifindex: %s\n", argv[2]);
|
|
return 2;
|
|
}
|
|
|
|
/* Linux accepts both lengths. Exercise both so a hook cannot accidentally
|
|
* depend on callers including a trailing NUL. */
|
|
run_name_case("BIND_NAME_RAW", vpn_name, (socklen_t)strlen(vpn_name));
|
|
run_name_case("BIND_NAME_NUL", vpn_name,
|
|
(socklen_t)strlen(vpn_name) + 1);
|
|
/* A userspace pointer whose numeric value is in the kernel range must be
|
|
* rejected through the uaccess path, never dereferenced by a hook. */
|
|
run_bad_pointer_case("BIND_BADPTR", vpn_name);
|
|
/* sock_setbindtodevice treats the unsigned syscall length as int and must
|
|
* reject this before a hook classifies the otherwise valid VPN name. */
|
|
run_name_case("BIND_BADLEN", vpn_name, (socklen_t)-1);
|
|
run_index_case("BIND_INDEX", vpn_ifindex);
|
|
run_name_case("BIND_KEEP", "eth0", 4);
|
|
return 0;
|
|
}
|