8000 nstun: bound FIN flow idle lifetime and correct FIN retransmissions by botegalucas · Pull Request #383 · google/nsjail · GitHub
[go: up one dir, main page]

Skip to content
64 changes: 51 additions & 13 deletions nstun/tcp.cc
10BC8
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "tcp.h"

#include <algorithm>
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
Expand Down Expand Up @@ -311,6 +312,20 @@ void push_to_guest(Context* ctx, TcpFlow* flow) {
}
}

/* After host-side EOF, retain writable events when necessary but switch to
* edge-triggered HUP reporting: level-triggered HUP can otherwise spin the
* epoll loop, repeatedly refreshing the flow's idle timestamp. */
static uint32_t tcp_host_events(const TcpFlow* flow, bool want_out) {
uint32_t events = EPOLLERR | EPOLLHUP;
if (flow->host_eof) {
events |= EPOLLET;
} else if (!flow->epoll_in_disabled) {
events |= EPOLLIN;
}
if (want_out) events |= EPOLLOUT;
return events;
}

/* Returns true if the flow was destroyed (caller must not use flow afterward) */
bool flush_to_host(Context* ctx, TcpFlow* flow) {
if (flow->rx_sent_offset >= flow->rx_buffer.size()) {
Expand All @@ -330,7 +345,7 @@ bool flush_to_host(Context* ctx, TcpFlow* flow) {
/* We made progress, remove EPOLLOUT if empty */
if (flow->rx_buffer.empty() && flow->epoll_out_registered) {
struct epoll_event ev = {
.events = EPOLLIN | EPOLLERR | EPOLLHUP, .data = {.fd = flow->host_fd}};
.events = tcp_host_events(flow, false), .data = {.fd = flow->host_fd}};
epoll_ctl(ctx->epoll_fd, EPOLL_CTL_MOD, flow->host_fd, &ev);
flow->epoll_out_registered = false;
}
Expand All @@ -341,7 +356,7 @@ bool flush_to_host(Context* ctx, TcpFlow* flow) {
} else if (written < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
/* Apply backpressure, register EPOLLOUT */
if (!flow->epoll_out_registered) {
struct epoll_event ev = {.events = EPOLLIN | EPOLLOUT | EPOLLERR | EPOLLHUP,
struct epoll_event ev = {.events = tcp_host_events(flow, true),
.data = {.fd = flow->host_fd}};
epoll_ctl(ctx->epoll_fd, EPOLL_CTL_MOD, flow->host_fd, &ev);
flow->epoll_out_registered = true;
Expand Down Expand Up @@ -729,7 +744,7 @@ static void tcp_process_data(Context* ctx, TcpFlow* flow, const tcp_hdr* tcp,
}

push_to_guest(ctx, flow);
} else if (acked_bytes == 0 && data_len == 0 &&
} else if (acked_bytes == 0 && data_len == 0 && !flow->fin_sent &&
!(tcp->flags & (NSTUN_TCP_FLAG_FIN | NSTUN_TCP_FLAG_SYN |
NSTUN_TCP_FLAG_RST))) {
/* Duplicate ACK -> Fast Retransmit */
Expand All @@ -741,7 +756,7 @@ static void tcp_process_data(Context* ctx, TcpFlow* flow, const tcp_hdr* tcp,
if (flow->state == TcpState::FIN_WAIT_1 && ack == flow->seq_to_guest) {
flow->state = TcpState::FIN_WAIT_2;
}
} else if (data_len == 0 &&
} else if (data_len == 0 && !flow->fin_sent &&
!(tcp->flags &
(NSTUN_TCP_FLAG_FIN | NSTUN_TCP_FLAG_SYN | NSTUN_TCP_FLAG_RST))) {
flow->seq_to_guest = flow->ack_from_guest;
Expand Down Expand Up @@ -998,8 +1013,7 @@ void handle_host_tcp_data_eof(Context* ctx, TcpFlow* flow, int fd) {
flow->host_eof = true;
if (!flow->epoll_in_disabled) {
struct epoll_event ev = {
.events = EPOLLERR | EPOLLHUP |
(flow->epoll_out_registered ? (uint32_t)EPOLLOUT : 0),
.events = tcp_host_events(flow, flow->epoll_out_registered),
.data = {.fd = fd}};
if (epoll_ctl(ctx->epoll_fd, EPOLL_CTL_MOD, fd, &ev) == -1) {
PLOG_E("epoll_ctl(EPOLL_CTL_MOD) failed in eof");
Expand All @@ -1014,7 +1028,10 @@ void handle_host_tcp_data_eof(Context* ctx, TcpFlow* flow, int fd) {

void handle_host_tcp(Context* ctx, TcpFlow* flow, uint32_t events) {
int fd = flow->host_fd;
flow->last_active = time(NULL);
/* A stale HUP is not peer activity and must not extend the idle timeout. */
if (!flow->host_eof && (events & (EPOLLIN | EPOLLOUT))) {
flow->last_active = time(NULL);
}

LOG_D("handle_host_tcp fd=%d, events=0x%x, state=%d", fd, events, (int)flow->state);

Expand Down Expand Up @@ -1060,9 +1077,15 @@ void handle_host_tcp(Context* ctx, TcpFlow* flow, uint32_t events) {
return;
}
if (!flow->host_eof) {
/* Treat HUP as EOF from host */
/* Treat HUP as EOF from host; suppress repeated level HUP wakeups. */
flow->host_eof = true;
flow->epoll_in_disabled = true;
struct epoll_event ev = {
.events = tcp_host_events(flow, flow->epoll_out_registered),
.data = {.fd = fd}};
if (epoll_ctl(ctx->epoll_fd, EPOLL_CTL_MOD, fd, &ev) == -1) {
PLOG_E("epoll_ctl(EPOLL_CTL_MOD) failed on HUP");
}
push_to_guest(ctx, flow);
if (ctx->flows_by_fd.find(fd) == ctx->flows_by_fd.end()) {
return;
Expand Down Expand Up @@ -1398,11 +1421,24 @@ void TcpFlow::handle_host_event(Context* ctx, int fd, uint32_t events) {
}

void TcpFlow::periodic_check(Context* ctx, time_t now) {
if (this->seq_to_guest > this->ack_from_guest && (now - this->last_active >= 2)) {
/* Retransmissions must not reset last_active: that tracks peer activity and
* drives the idle timeout. Keep the RTO clock separate. */
time_t last_send = std::max(this->last_active, this->last_retransmit);
if ((int32_t)(this->seq_to_guest - this->ack_from_guest) > 0 &&
(now - last_send >= 2)) {
LOG_D("TCP RTO triggered for flow (fd=%d)", this->host_fd);
this->seq_to_guest = this->ack_from_guest;
push_to_guest(ctx, this);
this->last_active = now;
if (this->fin_sent && !this->fin_acked) {
/* FIN consumes one sequence number. Replay the same FIN sequence,
* without moving SND.NXT back or consuming a second number. */
const uint32_t next_seq = this->seq_to_guest;
this->seq_to_guest = next_seq - 1;
tcp_send_packet(ctx, this, NSTUN_TCP_FLAG_FIN | NSTUN_TCP_FLAG_ACK);
this->seq_to_guest = next_seq;
} else {
this->seq_to_guest = this->ack_from_guest;
push_to_guest(ctx, this);
}
this->last_retransmit = now;
}
}

Expand All @@ -1416,7 +1452,9 @@ bool TcpFlow::is_stale(time_t now) const {
timeout = TCP_TIMEOUT_CLOSING;
} else if (this->state == TcpState::CLOSE_WAIT) {
timeout = TCP_TIMEOUT_FIN;
} else if (this->state == TcpState::FIN_WAIT_1 || this->state == TcpState::FIN_WAIT_2) {
} else if (this->state == TcpState::FIN_WAIT_1 || this->state == TcpState::FIN_WAIT_2 ||
this->fin_sent) {
/* FIN has been sent even if the legacy state remains ESTABLISHED. */
timeout = TCP_TIMEOUT_FIN;
}

Expand Down
1 change: 1 addition & 0 deletions nstun/tcp.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ struct TcpFlow : public Flow {
bool epoll_out_registered = false;
bool epoll_in_disabled = false;
bool inbound = false; /* true if flow is HOST_TO_GUEST */
time_t last_retransmit = 0; /* RTO clock; does not extend idle lifetime */
~TcpFlow() override {
if (host_fd != -1) ::close(host_fd);
}
Expand Down
184 changes: 184 additions & 0 deletions tests/nstun_tcp_fin_lifecycle_test.py
10BC8 CD87
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
#!/usr/bin/env python3
"""Regression checks for TCP FIN handling using actual extracted timer method bodies.
These checks mock networking and do not substitute for the nsjail integration suite.
"""
from pathlib import Path
import argparse
import subprocess
import tempfile

def extract(text, signature):
start = text.index(signature)
opening = text.index('{', start)
depth = 0
for i in range(opening, len(text)):
if text[i] == '{':
depth += 1
elif text[i] == '}':
depth -= 1
if depth == 0:
return text[start:i + 1]
raise RuntimeError("Unbalanced braces in " + signature)

def main():
parser = argparse.ArgumentParser()
parser.add_argument("source", type=Path, nargs="?", default=Path("nstun/tcp.cc"))
args = parser.parse_args()
source = args.source.read_text()
assert 'acked_bytes == 0 && data_len == 0 && !flow->fin_sent' in source
assert 'data_len == 0 && !flow->fin_sent' in source
assert 'if (!flow->host_eof && (events & (EPOLLIN | EPOLLOUT)))' in source
assert source.count('tcp_host_events(flow,') >= 3
events_helper = extract(source, 'static uint32_t tcp_host_events(const TcpFlow* flow, bool want_out) {')
periodic = extract(source, 'void TcpFlow::periodic_check(Context* ctx, time_t now) {')
stale = extract(source, 'bool TcpFlow::is_stale(time_t now) const {')
cpp = r'''#include <algorithm>
#include <cassert>
#include <cstdint>
#include <cstdio>
#include <ctime>
#include <iostream>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <unistd.h>

static constexpr time_t TCP_TIMEOUT_ESTABLISHED = 3600;
static constexpr time_t TCP_TIMEOUT_CONNECTING = 10;
static constexpr time_t TCP_TIMEOUT_FIN = 60;
static constexpr time_t TCP_TIMEOUT_CLOSING = 5;
constexpr uint8_t NSTUN_TCP_FLAG_FIN = 0x01;
constexpr uint8_t NSTUN_TCP_FLAG_ACK = 0x10;
#define LOG_D(...) ((void)0)

namespace nstun {
enum class TcpState {SYN_SENT,SOCKS5_INIT,SOCKS5_CONNECTING,HTTP_CONNECT_WAIT,ESTABLISHED,FIN_WAIT_1,FIN_WAIT_2,CLOSING,TIME_WAIT,CLOSE_WAIT};
struct Context {int sends=0;uint8_t flags=0;uint32_t sequence=0;int pushes=0;};
struct TcpFlow {
TcpState state=TcpState::ESTABLISHED;
bool fin_sent=false,fin_acked=false;
bool host_eof=false,epoll_in_disabled=false;
time_t last_active=0,last_retransmit=0;
uint32_t seq_to_guest=1001,ack_from_guest=1000;
int host_fd=10;
void periodic_check(Context*,time_t);
bool is_stale(time_t) const;
};
void push_to_guest(Context* c,TcpFlow*) { c->pushes++; }
void tcp_send_packet(Context* c,TcpFlow* f,uint8_t flags) {
c->sends++; c->flags=flags; c->sequence=f->seq_to_guest;
}
'''
cpp += events_helper + '\n' + periodic + '\n' + stale + '\n'
cpp += r'''
} // namespace nstun
using namespace nstun;
int main() {
const time_t now=5000;
TcpFlow normal;
normal.last_active=now-3599;
assert(!normal.is_stale(now));
normal.last_active=now-3601;
assert(normal.is_stale(now));

TcpFlow fin;
fin.last_active=now-59;fin.fin_sent=true;
assert(!fin.is_stale(now));
fin.last_active=now-61;
assert(fin.is_stale(now));

TcpFlow wait;
wait.state=TcpState::FIN_WAIT_1;wait.last_active=now-61;
assert(wait.is_stale(now));
wait.state=TcpState::TIME_WAIT;wait.last_active=now-6;
assert(wait.is_stale(now));
wait.state=TcpState::SYN_SENT;wait.last_active=now-11;
assert(wait.is_stale(now));

TcpFlow pending;
pending.fin_sent=true;pending.last_active=now-10;
pending.last_retransmit=now-3;
Context context;
pending.periodic_check(&context,now);
assert(context.sends==1);
assert(context.flags==(NSTUN_TCP_FLAG_FIN|NSTUN_TCP_FLAG_ACK));
assert(context.sequence==1000);
assert(pending.seq_to_guest==1001);
assert(pending.last_active==now-10);
assert(pending.last_retransmit==now);
pending.periodic_check(&context,now+1);
assert(context.sends==1);
pending.periodic_check(&context,now+2);
assert(context.sends==2);
pending.last_active=now-61;
assert(pending.is_stale(now));

TcpFlow data;
data.last_active=now-3;
Context datactx;
data.periodic_check(&datactx,now);
assert(datactx.pushes==1 && datactx.sends==0);
assert(data.last_active==now-3 && data.last_retransmit==now);

TcpFlow wrap;
wrap.fin_sent=true;wrap.last_active=now-10;
wrap.seq_to_guest=0;wrap.ack_from_guest=UINT32_MAX;
Context wrapctx;
wrap.periodic_check(&wrapctx,now);
assert(wrapctx.sends==1);
assert(wrapctx.sequence==UINT32_MAX);
assert(wrap.seq_to_guest==0);

TcpFlow already_ack;
already_ack.fin_sent=true;already_ack.fin_acked=true;
already_ack.seq_to_guest=1001;already_ack.ack_from_guest=1001;
already_ack.last_active=now-10;
Context ackctx;
already_ack.periodic_check(&ackctx,now);
assert(ackctx.sends==0 && ackctx.pushes==0);

// Test real Linux readiness semantics using the actual nstun event mask helper.
TcpFlow epflow;
assert(tcp_host_events(&epflow,false)==(EPOLLERR|EPOLLHUP|EPOLLIN));
epflow.epoll_in_disabled=true;
assert(tcp_host_events(&epflow,true)==(EPOLLERR|EPOLLHUP|EPOLLOUT));
epflow.host_eof=true;
assert(tcp_host_events(&epflow,false)==(EPOLLERR|EPOLLHUP|EPOLLET));

int sock[2];
assert(socketpair(AF_UNIX,SOCK_STREAM|SOCK_NONBLOCK,0,sock)==0);
int epollfd=epoll_create1(EPOLL_CLOEXEC);
assert(epollfd>=0);
epoll_event ev={};
ev.events=EPOLLIN|EPOLLERR|EPOLLHUP;
ev.data.fd=sock[0];
assert(epoll_ctl(epollfd,EPOLL_CTL_ADD,sock[0],&ev)==0);
close(sock[1]);
char scratch=0;
assert(recv(sock[0],&scratch,1,0)==0);
ev.events=tcp_host_events(&epflow,false);
assert(epoll_ctl(epollfd,EPOLL_CTL_MOD,sock[0],&ev)==0);
epoll_event ready={};
int count=epoll_wait(epollfd,&ready,1,100);
assert(count==1 && (ready.events & EPOLLHUP)!=0);
// EPOLLET prevents a permanently readable HUP from spinning the event loop.
for(int i=0;i<3;i++) {
count=epoll_wait(epollfd,&ready,1,15);
assert(count==0);
}
close(sock[0]);
close(epollfd);

std::cout << "PASS: extracted production TCP timers and Linux EOF/HUP regression checks\n";
}
'''
with tempfile.TemporaryDirectory() as tmp:
filename=Path(tmp)/"tcp_fin_test.cc"
filename.write_text(cpp)
binary=Path(tmp)/"tcp_fin_test"
subprocess.run(["g++","-std=c++20","-O1","-Wall","-Wextra","-Werror",
"-Wno-unused-parameter","-fsanitize=address,undefined",
"-fno-omit-frame-pointer",str(filename),"-o",str(binary)],check=True)
subprocess.run([str(binary)],check=True)

if __name__=="__main__":
main()
Loading
0