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
8 changes: 5 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -377,12 +377,14 @@ if (UA2F_BUILD_TESTS)
target_link_libraries(ua2f_handler_test uci)
endif ()

# Proxy lifecycle tests exercise listener setup, worker selection, and
# startup failure paths without requiring transparent-routing privileges.
# Proxy tests cover lifecycle and real socket/epoll forwarding without
# transparent-routing privileges. The C bridge includes proxy.c to exercise
# private connection state without exposing test hooks in production headers.
add_executable(
ua2f_proxy_test
test/proxy_test.cc
src/proxy.c
test/proxy_transport_test.cc
test/proxy_transport.c
src/handler.c
src/util.c
src/cache.c
Expand Down
38 changes: 29 additions & 9 deletions src/proxy.c
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ struct proxy_connection {
uint32_t target_armed;
};

// Not registered in epoll, including a paused side with no useful events.
#define PROXY_EVENTS_UNSET UINT32_MAX

struct proxy_context {
Expand Down Expand Up @@ -373,18 +374,32 @@ static void proxy_buffer_compact(struct proxy_buffer *buf) {
static int epoll_set(int epoll_fd, int op, int fd, struct epoll_ref *ref, uint32_t events) {
struct epoll_event event;
memset(&event, 0, sizeof(event));
event.events = events | EPOLLERR | EPOLLHUP | EPOLLRDHUP;
event.events = events | EPOLLERR | EPOLLHUP;
if (events & EPOLLIN) {
event.events |= EPOLLRDHUP;
}
event.data.ptr = ref;
return epoll_ctl(epoll_fd, op, fd, &event);
}

// Re-arm an fd only when the desired interest mask differs from what is armed,
// avoiding an EPOLL_CTL_MOD syscall on every event in steady state.
static int epoll_rearm(int epoll_fd, int fd, struct epoll_ref *ref, uint32_t *armed, uint32_t desired) {
if (desired == 0) {
// HUP is reported even with an empty interest mask. Remove a blocked
// or fully drained side until the other side makes progress, rather
// than spinning on HUP while there is nowhere to forward its data.
if (*armed != PROXY_EVENTS_UNSET && epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL) != 0) {
return -1;
}
*armed = PROXY_EVENTS_UNSET;
return 0;
}
if (*armed == desired) {
return 0;
}
if (epoll_set(epoll_fd, EPOLL_CTL_MOD, fd, ref, desired) != 0) {
const int op = *armed == PROXY_EVENTS_UNSET ? EPOLL_CTL_ADD : EPOLL_CTL_MOD;
if (epoll_set(epoll_fd, op, fd, ref, desired) != 0) {
return -1;
}
*armed = desired;
Expand Down Expand Up @@ -802,7 +817,7 @@ static void handle_connection_event(struct epoll_ref *ref, uint32_t events) {
return;
}

if (events & EPOLLOUT) {
if (events & (EPOLLOUT | EPOLLHUP)) {
if (side == PROXY_SIDE_CLIENT) {
if (flush_buffer(conn->client_fd, &conn->target_to_client) != 0 || pump_splice_to_client(conn) != 0) {
connection_schedule_close(conn);
Expand All @@ -814,7 +829,10 @@ static void handle_connection_event(struct epoll_ref *ref, uint32_t events) {
}
}

if (events & (EPOLLIN | EPOLLRDHUP)) {
// HUP/RDHUP may arrive with unread bytes. Only recv/splice returning zero
// establishes EOF; a full buffer or pending pipe must resume after flushing.
const bool read_eof = side == PROXY_SIDE_CLIENT ? conn->client_eof : conn->target_eof;
if (!read_eof && (events & (EPOLLIN | EPOLLRDHUP | EPOLLHUP))) {
const int read_result =
side == PROXY_SIDE_TARGET ? transfer_target_to_client(conn) : read_into_buffer(conn, side);
if (read_result != 0) {
Expand All @@ -835,11 +853,13 @@ static void handle_connection_event(struct epoll_ref *ref, uint32_t events) {
}
}

if ((events & (EPOLLERR | EPOLLHUP)) != 0) {
if (side == PROXY_SIDE_CLIENT) {
conn->client_eof = true;
} else {
conn->target_eof = true;
if (events & EPOLLERR) {
const int fd = side == PROXY_SIDE_CLIENT ? conn->client_fd : conn->target_fd;
int error = 0;
socklen_t error_len = sizeof(error);
if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &error_len) != 0 || error != 0) {
connection_schedule_close(conn);
return;
}
}

Expand Down
86 changes: 86 additions & 0 deletions test/proxy_transport.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Compile the production proxy here for transport tests. This keeps its private
// event-loop API out of the installed headers, while testing the exact same C
// implementation (including lifecycle tests that call run_proxy).
#include "../src/proxy.c"
#include "proxy_transport.h"

struct proxy_test_connection {
struct proxy_context ctx;
struct proxy_connection *conn;
};

struct proxy_test_connection *proxy_test_create(int client_fd, int target_fd, bool splice_enabled, bool target_in_progress) {
struct proxy_test_connection *test = calloc(1, sizeof(*test));
if (test == NULL) {
close(client_fd);
close(target_fd);
return NULL;
}
test->ctx.epoll_fd = epoll_create1(EPOLL_CLOEXEC);
if (test->ctx.epoll_fd < 0 || !proxy_try_acquire_connection()) {
if (test->ctx.epoll_fd >= 0) {
close(test->ctx.epoll_fd);
}
close(client_fd);
close(target_fd);
free(test);
return NULL;
}
test->conn = create_connection(&test->ctx, client_fd, target_fd, AF_INET, target_in_progress);
if (test->conn == NULL) {
close_all_connections(&test->ctx);
close(test->ctx.epoll_fd);
free(test);
return NULL;
}
test->conn->rewrite_disabled = true;
if (!splice_enabled) {
test->conn->splice_enabled = false;
} else if (!test->conn->splice_enabled || fcntl(test->conn->splice_pipe[0], F_SETPIPE_SZ, 4096) < 0) {
proxy_test_destroy(test);
return NULL;
}
return test;
}

void proxy_test_destroy(struct proxy_test_connection *test) {
if (test != NULL) {
close_all_connections(&test->ctx);
close(test->ctx.epoll_fd);
free(test);
}
}

int proxy_test_step(struct proxy_test_connection *test, int timeout_ms) {
struct epoll_event events[16];
const int ready = epoll_wait(test->ctx.epoll_fd, events, 16, timeout_ms);
if (ready < 0) {
return -1;
}
for (int i = 0; i < ready; i++) {
handle_connection_event(events[i].data.ptr, events[i].events);
}
// Retain closing connections until test destruction so assertions can inspect
// EOF and pending-byte state after the descriptors have been closed.
return ready;
}

struct proxy_test_state proxy_test_snapshot(const struct proxy_test_connection *test) {
const struct proxy_connection *conn = test->conn;
return (struct proxy_test_state){
.client_eof = conn->client_eof,
.target_eof = conn->target_eof,
.client_write_shutdown = conn->client_write_shutdown,
.target_write_shutdown = conn->target_write_shutdown,
.closing = conn->closing,
.splice_enabled = conn->splice_enabled,
.target_connected = conn->target_connected,
.client_registered = !conn->closing && conn->client_armed != PROXY_EVENTS_UNSET,
.target_registered = !conn->closing && conn->target_armed != PROXY_EVENTS_UNSET,
.client_events = conn->client_armed,
.target_events = conn->target_armed,
.request_pending = conn->client_to_target.len - conn->client_to_target.off,
.response_pending = conn->target_to_client.len - conn->target_to_client.off,
.splice_pending = conn->splice_pending,
};
}
24 changes: 24 additions & 0 deletions test/proxy_transport.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#ifndef UA2F_PROXY_TRANSPORT_TEST_H
#define UA2F_PROXY_TRANSPORT_TEST_H

#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>

struct proxy_test_connection;
struct proxy_test_state {
bool client_eof, target_eof;
bool client_write_shutdown, target_write_shutdown;
bool closing, splice_enabled, target_connected;
bool client_registered, target_registered;
uint32_t client_events, target_events;
size_t request_pending, response_pending, splice_pending;
};

// Takes ownership of both proxy-side descriptors, including on failure.
struct proxy_test_connection *proxy_test_create(int client_fd, int target_fd, bool splice_enabled, bool target_in_progress);
void proxy_test_destroy(struct proxy_test_connection *test);
int proxy_test_step(struct proxy_test_connection *test, int timeout_ms);
struct proxy_test_state proxy_test_snapshot(const struct proxy_test_connection *test);

#endif
Loading
Loading