Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions misc/dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Dprotobuf
endforeach
endif
endmacro
EPOLLIN
fdata
FETCHCONTENT
fexecve
Expand Down
2 changes: 2 additions & 0 deletions misc/iwyu_mappings.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
- symbol: ["pthread_mutex_t", "private", "<sys/types.h>", "public"]
- symbol: ["pthread_t", "private", "<sys/types.h>", "public"]
- symbol: ["PR_SET_PDEATHSIG", "private", "<sys/prctl.h>", "public"]
- symbol: ["EINTR", "private", "<errno.h>", "public"]
- symbol: ["CLOCK_MONOTONIC", "private", "<time.h>", "public"]
112 changes: 101 additions & 11 deletions src/tunnel.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include "tunnel.h"
#include "secure-tunnel.h"
#include "tunnel_notification_parser.h"
#include <errno.h>
#include <fcntl.h>
#include <gg/buffer.h>
#include <gg/cleanup.h>
Expand All @@ -11,9 +12,12 @@
#include <gg/vector.h>
#include <pthread.h>
#include <signal.h>
#include <sys/epoll.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
Expand Down Expand Up @@ -65,12 +69,74 @@ static int prepare_localproxy_fd(void) {
return fd;
}

static void wait_for_child(
int pidfd, int epoll_fd, pid_t pid, int timeout_seconds
) {
struct timespec start;
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &start);
int timeout_ms = timeout_seconds * 1000;
int ret;
struct epoll_event event;

do {
ret = epoll_wait(epoll_fd, &event, 1, timeout_ms);
if (ret < 0 && errno == EINTR) {
clock_gettime(CLOCK_MONOTONIC, &now);
int elapsed_ms
= (int) (((now.tv_sec - start.tv_sec) * 1000)
+ ((now.tv_nsec - start.tv_nsec) / 1000000));
timeout_ms = (timeout_seconds * 1000) - elapsed_ms;
if (timeout_ms <= 0) {
ret = 0;
break;
}
}
} while (ret < 0 && errno == EINTR);

close(epoll_fd);
int status;

if (ret < 0) {
GG_LOGE("epoll_wait failed, errno: %d", errno);
// Kill entire process group to clean up localproxy and any children.
killpg(pid, SIGKILL);
} else if (ret == 0) {
GG_LOGW(
"Tunnel timeout (%d seconds) reached, killing localproxy",
timeout_seconds
);
// Kill entire process group to clean up localproxy and any children.
killpg(pid, SIGKILL);
} else {
close(pidfd);
waitpid(pid, &status, 0);
if (WIFEXITED(status) && WEXITSTATUS(status) == 0) {
GG_LOGI("Tunnel completed successfully");
} else {
GG_LOGW("Tunnel exited with status: %d", status);
}
return;
}

close(pidfd);
waitpid(pid, &status, 0);
}

static void execute_localproxy(
int localproxy_fd, const char *const *args, const char *access_token
int localproxy_fd,
const char *const *args,
const char *access_token,
int timeout_seconds
) {
// Fork and execute localproxy
pid_t pid = fork();
if (pid == 0) {
// Child process block

// Create new process group so we can kill all children
setpgid(0, 0);

// Child process: kill localproxy if parent dies
prctl(PR_SET_PDEATHSIG, SIGTERM);

Expand All @@ -90,17 +156,36 @@ static void execute_localproxy(
_exit(1);

} else if (pid > 0) {
// Parent process: wait for completion
int status;
waitpid(pid, &status, 0);
// Parent process block

// Get a file descriptor for the child process to use with epoll.
int pidfd = (int) syscall(SYS_pidfd_open, pid, 0);
int status = 0;

if (pidfd == -1) {
GG_LOGE("pidfd_open failed");
// Use killpg to signal the entire process group, ensuring any
// grandchildren spawned by localproxy are also killed.
killpg(pid, SIGKILL);
// Reap the child to prevent zombie process.
waitpid(pid, &status, 0);
return;
}

if (WIFEXITED(status) && WEXITSTATUS(status) == 0) {
GG_LOGI("Tunnel completed successfully");
} else {
GG_LOGW("Tunnel exited with status: %d", status);
int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
if (epoll_fd == -1) {
GG_LOGE("epoll_create1 failed");
// Kill entire process group.
killpg(pid, SIGKILL);
close(pidfd);
// Reap the child to prevent zombie process.
waitpid(pid, &status, 0);
return;
}
} else {
GG_LOGE("Failed to fork process");

struct epoll_event ev = { .events = EPOLLIN, .data.fd = pidfd };
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, pidfd, &ev);
wait_for_child(pidfd, epoll_fd, pid, timeout_seconds);
}
}

Expand Down Expand Up @@ -138,7 +223,12 @@ static void *tunnel_worker(void *arg) {
"Using localproxy for service: %s on port %u", ctx->service, ctx->port
);

execute_localproxy(localproxy_fd, args, ctx->access_token);
execute_localproxy(
localproxy_fd,
args,
ctx->access_token,
tunnel_config->tunnel_timeout_seconds
);

return NULL;
}
Expand Down
73 changes: 54 additions & 19 deletions test/unit/test_localproxy_failure.c
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ void test_nonexistent_binary_cleanup(void);
void test_nonexecutable_binary_cleanup(void);
void test_crashing_binary_cleanup(void);
void test_max_tunnel_slots_enforced(void);
void test_tunnel_timeout_kills_localproxy(void);

static int initial_fd_count;

Expand Down Expand Up @@ -135,26 +136,48 @@ void test_nonexecutable_binary_cleanup(void) {
TEST_ASSERT_EQUAL_INT(initial_fd_count, count_open_fds());
}

// Test crashing localproxy binary
void test_crashing_binary_cleanup(void) {
// Create temp directory with crashing localproxy script
// Test that 21st tunnel is rejected when 20 slots are occupied
void test_max_tunnel_slots_enforced(void) {
SecureTunnelConfig *config
= make_config_with_max("/nonexistent", MAX_TUNNEL_SLOTS + 1);
uint8_t arena_mem[1024];

// Manually occupy all 20 slots
pthread_mutex_lock(&tunnel_mutex);
active_tunnels = MAX_TUNNEL_SLOTS;
tunnel_slots_mask = 0xFFFFF; // All 20 bits set
pthread_mutex_unlock(&tunnel_mutex);

GgMap notification
= mock_create_tunnel_notification(arena_mem, sizeof(arena_mem));
GgError ret = handle_tunnel_notification(notification, config);

TEST_ASSERT_EQUAL(GG_ERR_NOMEM, ret);
TEST_ASSERT_EQUAL_INT(MAX_TUNNEL_SLOTS, active_tunnels);
}

// Test that tunnel timeout kills a long-running localproxy
void test_tunnel_timeout_kills_localproxy(void) {
// Create a localproxy script that sleeps and logs its PID
mkdir(TEST_DIR, 0755);
FILE *f = fopen(TEST_DIR "/localproxy", "w");
TEST_ASSERT_NOT_NULL(f);
fprintf(f, "#!/bin/sh\nkill -SEGV $$\n");
fprintf(f, "#!/bin/sh\necho $$ > /tmp/localproxy.pid\nsleep 3600\n");
fclose(f);
chmod(TEST_DIR "/localproxy", 0755);

SecureTunnelConfig *config = make_config(TEST_DIR);
config->tunnel_timeout_seconds = 5;

uint8_t arena_mem[1024];
GgMap notification
= mock_create_tunnel_notification(arena_mem, sizeof(arena_mem));

GgError ret = handle_tunnel_notification(notification, config);
TEST_ASSERT_EQUAL(GG_ERR_OK, ret);

// Wait for worker thread to complete by polling active_tunnels
for (int i = 0; i < 50 && active_tunnels > 0; i++) {
// Wait for timeout to trigger
for (int i = 0; i < 70 && active_tunnels > 0; i++) {
usleep(100000); // 100ms
}

Expand All @@ -163,24 +186,35 @@ void test_crashing_binary_cleanup(void) {
TEST_ASSERT_EQUAL_INT(initial_fd_count, count_open_fds());
}

// Test that 21st tunnel is rejected when 20 slots are occupied
void test_max_tunnel_slots_enforced(void) {
SecureTunnelConfig *config
= make_config_with_max("/nonexistent", MAX_TUNNEL_SLOTS + 1);
uint8_t arena_mem[1024];

// Manually occupy all 20 slots
pthread_mutex_lock(&tunnel_mutex);
active_tunnels = MAX_TUNNEL_SLOTS;
tunnel_slots_mask = 0xFFFFF; // All 20 bits set
pthread_mutex_unlock(&tunnel_mutex);
// Test crashing localproxy binary
void test_crashing_binary_cleanup(void) {
// Create temp directory with crashing localproxy script
mkdir(TEST_DIR, 0755);
FILE *f = fopen(TEST_DIR "/localproxy", "w");
TEST_ASSERT_NOT_NULL(f);
fprintf(f, "#!/bin/sh\nkill -SEGV $$\n");
fclose(f);
chmod(TEST_DIR "/localproxy", 0755);

SecureTunnelConfig *config = make_config(TEST_DIR);
uint8_t arena_mem[1024];
GgMap notification
= mock_create_tunnel_notification(arena_mem, sizeof(arena_mem));

GgError ret = handle_tunnel_notification(notification, config);
TEST_ASSERT_EQUAL(GG_ERR_OK, ret);

TEST_ASSERT_EQUAL(GG_ERR_NOMEM, ret);
TEST_ASSERT_EQUAL_INT(MAX_TUNNEL_SLOTS, active_tunnels);
// Wait for timeout to trigger
for (int i = 0; i < 40 && active_tunnels > 0; i++) {
usleep(100000); // 100ms
}

TEST_ASSERT_EQUAL_INT(0, active_tunnels);
TEST_ASSERT_EQUAL_UINT32(0, tunnel_slots_mask);
TEST_ASSERT_EQUAL_INT(initial_fd_count, count_open_fds());

unlink(TEST_DIR "/localproxy");
rmdir(TEST_DIR);
}

int main(void) {
Expand All @@ -189,5 +223,6 @@ int main(void) {
RUN_TEST(test_nonexecutable_binary_cleanup);
RUN_TEST(test_crashing_binary_cleanup);
RUN_TEST(test_max_tunnel_slots_enforced);
RUN_TEST(test_tunnel_timeout_kills_localproxy);
return UNITY_END();
}