Skip to content

Commit b212757

Browse files
committed
Add local timeout for localproxy
1 parent 49cd197 commit b212757

File tree

4 files changed

+158
-30
lines changed

4 files changed

+158
-30
lines changed

misc/dictionary.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Dprotobuf
1414
endforeach
1515
endif
1616
endmacro
17+
EPOLLIN
1718
fdata
1819
FETCHCONTENT
1920
fexecve

misc/iwyu_mappings.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
- symbol: ["pthread_mutex_t", "private", "<sys/types.h>", "public"]
22
- symbol: ["pthread_t", "private", "<sys/types.h>", "public"]
33
- symbol: ["PR_SET_PDEATHSIG", "private", "<sys/prctl.h>", "public"]
4+
- symbol: ["EINTR", "private", "<errno.h>", "public"]
5+
- symbol: ["CLOCK_MONOTONIC", "private", "<time.h>", "public"]

src/tunnel.c

Lines changed: 101 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#include "tunnel.h"
33
#include "secure-tunnel.h"
44
#include "tunnel_notification_parser.h"
5+
#include <errno.h>
56
#include <fcntl.h>
67
#include <gg/buffer.h>
78
#include <gg/cleanup.h>
@@ -11,9 +12,12 @@
1112
#include <gg/vector.h>
1213
#include <pthread.h>
1314
#include <signal.h>
15+
#include <sys/epoll.h>
1416
#include <sys/prctl.h>
17+
#include <sys/syscall.h>
1518
#include <sys/types.h>
1619
#include <sys/wait.h>
20+
#include <time.h>
1721
#include <unistd.h>
1822
#include <stdio.h>
1923
#include <stdlib.h>
@@ -65,12 +69,74 @@ static int prepare_localproxy_fd(void) {
6569
return fd;
6670
}
6771

72+
static void wait_for_child(
73+
int pidfd, int epoll_fd, pid_t pid, int timeout_seconds
74+
) {
75+
struct timespec start;
76+
struct timespec now;
77+
clock_gettime(CLOCK_MONOTONIC, &start);
78+
int timeout_ms = timeout_seconds * 1000;
79+
int ret;
80+
struct epoll_event event;
81+
82+
do {
83+
ret = epoll_wait(epoll_fd, &event, 1, timeout_ms);
84+
if (ret < 0 && errno == EINTR) {
85+
clock_gettime(CLOCK_MONOTONIC, &now);
86+
int elapsed_ms
87+
= (int) (((now.tv_sec - start.tv_sec) * 1000)
88+
+ ((now.tv_nsec - start.tv_nsec) / 1000000));
89+
timeout_ms = (timeout_seconds * 1000) - elapsed_ms;
90+
if (timeout_ms <= 0) {
91+
ret = 0;
92+
break;
93+
}
94+
}
95+
} while (ret < 0 && errno == EINTR);
96+
97+
close(epoll_fd);
98+
int status;
99+
100+
if (ret < 0) {
101+
GG_LOGE("epoll_wait failed, errno: %d", errno);
102+
// Kill entire process group to clean up localproxy and any children.
103+
killpg(pid, SIGKILL);
104+
} else if (ret == 0) {
105+
GG_LOGW(
106+
"Tunnel timeout (%d seconds) reached, killing localproxy",
107+
timeout_seconds
108+
);
109+
// Kill entire process group to clean up localproxy and any children.
110+
killpg(pid, SIGKILL);
111+
} else {
112+
close(pidfd);
113+
waitpid(pid, &status, 0);
114+
if (WIFEXITED(status) && WEXITSTATUS(status) == 0) {
115+
GG_LOGI("Tunnel completed successfully");
116+
} else {
117+
GG_LOGW("Tunnel exited with status: %d", status);
118+
}
119+
return;
120+
}
121+
122+
close(pidfd);
123+
waitpid(pid, &status, 0);
124+
}
125+
68126
static void execute_localproxy(
69-
int localproxy_fd, const char *const *args, const char *access_token
127+
int localproxy_fd,
128+
const char *const *args,
129+
const char *access_token,
130+
int timeout_seconds
70131
) {
71132
// Fork and execute localproxy
72133
pid_t pid = fork();
73134
if (pid == 0) {
135+
// Child process block
136+
137+
// Create new process group so we can kill all children
138+
setpgid(0, 0);
139+
74140
// Child process: kill localproxy if parent dies
75141
prctl(PR_SET_PDEATHSIG, SIGTERM);
76142

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

92158
} else if (pid > 0) {
93-
// Parent process: wait for completion
94-
int status;
95-
waitpid(pid, &status, 0);
159+
// Parent process block
160+
161+
// Get a file descriptor for the child process to use with epoll.
162+
int pidfd = (int) syscall(SYS_pidfd_open, pid, 0);
163+
int status = 0;
164+
165+
if (pidfd == -1) {
166+
GG_LOGE("pidfd_open failed");
167+
// Use killpg to signal the entire process group, ensuring any
168+
// grandchildren spawned by localproxy are also killed.
169+
killpg(pid, SIGKILL);
170+
// Reap the child to prevent zombie process.
171+
waitpid(pid, &status, 0);
172+
return;
173+
}
96174

97-
if (WIFEXITED(status) && WEXITSTATUS(status) == 0) {
98-
GG_LOGI("Tunnel completed successfully");
99-
} else {
100-
GG_LOGW("Tunnel exited with status: %d", status);
175+
int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
176+
if (epoll_fd == -1) {
177+
GG_LOGE("epoll_create1 failed");
178+
// Kill entire process group.
179+
killpg(pid, SIGKILL);
180+
close(pidfd);
181+
// Reap the child to prevent zombie process.
182+
waitpid(pid, &status, 0);
183+
return;
101184
}
102-
} else {
103-
GG_LOGE("Failed to fork process");
185+
186+
struct epoll_event ev = { .events = EPOLLIN, .data.fd = pidfd };
187+
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, pidfd, &ev);
188+
wait_for_child(pidfd, epoll_fd, pid, timeout_seconds);
104189
}
105190
}
106191

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

141-
execute_localproxy(localproxy_fd, args, ctx->access_token);
226+
execute_localproxy(
227+
localproxy_fd,
228+
args,
229+
ctx->access_token,
230+
tunnel_config->tunnel_timeout_seconds
231+
);
142232

143233
return NULL;
144234
}

test/unit/test_localproxy_failure.c

Lines changed: 54 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ void test_nonexistent_binary_cleanup(void);
2323
void test_nonexecutable_binary_cleanup(void);
2424
void test_crashing_binary_cleanup(void);
2525
void test_max_tunnel_slots_enforced(void);
26+
void test_tunnel_timeout_kills_localproxy(void);
2627

2728
static int initial_fd_count;
2829

@@ -135,26 +136,48 @@ void test_nonexecutable_binary_cleanup(void) {
135136
TEST_ASSERT_EQUAL_INT(initial_fd_count, count_open_fds());
136137
}
137138

138-
// Test crashing localproxy binary
139-
void test_crashing_binary_cleanup(void) {
140-
// Create temp directory with crashing localproxy script
139+
// Test that 21st tunnel is rejected when 20 slots are occupied
140+
void test_max_tunnel_slots_enforced(void) {
141+
SecureTunnelConfig *config
142+
= make_config_with_max("/nonexistent", MAX_TUNNEL_SLOTS + 1);
143+
uint8_t arena_mem[1024];
144+
145+
// Manually occupy all 20 slots
146+
pthread_mutex_lock(&tunnel_mutex);
147+
active_tunnels = MAX_TUNNEL_SLOTS;
148+
tunnel_slots_mask = 0xFFFFF; // All 20 bits set
149+
pthread_mutex_unlock(&tunnel_mutex);
150+
151+
GgMap notification
152+
= mock_create_tunnel_notification(arena_mem, sizeof(arena_mem));
153+
GgError ret = handle_tunnel_notification(notification, config);
154+
155+
TEST_ASSERT_EQUAL(GG_ERR_NOMEM, ret);
156+
TEST_ASSERT_EQUAL_INT(MAX_TUNNEL_SLOTS, active_tunnels);
157+
}
158+
159+
// Test that tunnel timeout kills a long-running localproxy
160+
void test_tunnel_timeout_kills_localproxy(void) {
161+
// Create a localproxy script that sleeps and logs its PID
141162
mkdir(TEST_DIR, 0755);
142163
FILE *f = fopen(TEST_DIR "/localproxy", "w");
143164
TEST_ASSERT_NOT_NULL(f);
144-
fprintf(f, "#!/bin/sh\nkill -SEGV $$\n");
165+
fprintf(f, "#!/bin/sh\necho $$ > /tmp/localproxy.pid\nsleep 3600\n");
145166
fclose(f);
146167
chmod(TEST_DIR "/localproxy", 0755);
147168

148169
SecureTunnelConfig *config = make_config(TEST_DIR);
170+
config->tunnel_timeout_seconds = 5;
171+
149172
uint8_t arena_mem[1024];
150173
GgMap notification
151174
= mock_create_tunnel_notification(arena_mem, sizeof(arena_mem));
152175

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

156-
// Wait for worker thread to complete by polling active_tunnels
157-
for (int i = 0; i < 50 && active_tunnels > 0; i++) {
179+
// Wait for timeout to trigger
180+
for (int i = 0; i < 70 && active_tunnels > 0; i++) {
158181
usleep(100000); // 100ms
159182
}
160183

@@ -163,24 +186,35 @@ void test_crashing_binary_cleanup(void) {
163186
TEST_ASSERT_EQUAL_INT(initial_fd_count, count_open_fds());
164187
}
165188

166-
// Test that 21st tunnel is rejected when 20 slots are occupied
167-
void test_max_tunnel_slots_enforced(void) {
168-
SecureTunnelConfig *config
169-
= make_config_with_max("/nonexistent", MAX_TUNNEL_SLOTS + 1);
170-
uint8_t arena_mem[1024];
171-
172-
// Manually occupy all 20 slots
173-
pthread_mutex_lock(&tunnel_mutex);
174-
active_tunnels = MAX_TUNNEL_SLOTS;
175-
tunnel_slots_mask = 0xFFFFF; // All 20 bits set
176-
pthread_mutex_unlock(&tunnel_mutex);
189+
// Test crashing localproxy binary
190+
void test_crashing_binary_cleanup(void) {
191+
// Create temp directory with crashing localproxy script
192+
mkdir(TEST_DIR, 0755);
193+
FILE *f = fopen(TEST_DIR "/localproxy", "w");
194+
TEST_ASSERT_NOT_NULL(f);
195+
fprintf(f, "#!/bin/sh\nkill -SEGV $$\n");
196+
fclose(f);
197+
chmod(TEST_DIR "/localproxy", 0755);
177198

199+
SecureTunnelConfig *config = make_config(TEST_DIR);
200+
uint8_t arena_mem[1024];
178201
GgMap notification
179202
= mock_create_tunnel_notification(arena_mem, sizeof(arena_mem));
203+
180204
GgError ret = handle_tunnel_notification(notification, config);
205+
TEST_ASSERT_EQUAL(GG_ERR_OK, ret);
181206

182-
TEST_ASSERT_EQUAL(GG_ERR_NOMEM, ret);
183-
TEST_ASSERT_EQUAL_INT(MAX_TUNNEL_SLOTS, active_tunnels);
207+
// Wait for timeout to trigger
208+
for (int i = 0; i < 40 && active_tunnels > 0; i++) {
209+
usleep(100000); // 100ms
210+
}
211+
212+
TEST_ASSERT_EQUAL_INT(0, active_tunnels);
213+
TEST_ASSERT_EQUAL_UINT32(0, tunnel_slots_mask);
214+
TEST_ASSERT_EQUAL_INT(initial_fd_count, count_open_fds());
215+
216+
unlink(TEST_DIR "/localproxy");
217+
rmdir(TEST_DIR);
184218
}
185219

186220
int main(void) {
@@ -189,5 +223,6 @@ int main(void) {
189223
RUN_TEST(test_nonexecutable_binary_cleanup);
190224
RUN_TEST(test_crashing_binary_cleanup);
191225
RUN_TEST(test_max_tunnel_slots_enforced);
226+
RUN_TEST(test_tunnel_timeout_kills_localproxy);
192227
return UNITY_END();
193228
}

0 commit comments

Comments
 (0)