Skip to content

upstream#2

Open
kooksee wants to merge 59 commits into
pubgo:masterfrom
liudf0716:master
Open

upstream#2
kooksee wants to merge 59 commits into
pubgo:masterfrom
liudf0716:master

Conversation

@kooksee

@kooksee kooksee commented Jun 16, 2026

Copy link
Copy Markdown

No description provided.

liudf0716 added 30 commits June 5, 2026 17:53
- New visitor.c/visitor.h: local TCP listener that tunnels connections
  to named STCP/XTCP/SUDP proxy servers via frps
- INI section format: [stcp_visitor:name] with server_name, sk, bind_port
- State machine: connect → send NewVisitorConn → validate response → tunnel
- config.c: route visitor INI sections to parse_visitor_section()
- control.c: init visitors after login, handle TypeNewVisitorConnResp
- CMakeLists.txt: add visitor.c to build
- plugin = unix_domain_socket with plugin_unix_path config
- connect_unix_server(): non-blocking Unix socket connection via libevent
- setup_local_connection(): detects UDS plugin, connects to socket path
- Added sys/un.h for sockaddr_un
- Config example: test_uds.ini
- Works with any proxy type (tcp/http/https) backed by a local UDS service
When SIGHUP is received (e.g. via 'kill -HUP <pid>' or 'reload' command),
the client gracefully reloads its configuration without restarting:

- Signal handler sets a volatile flag (async-signal-safe)
- 500ms periodic timer in the event loop checks the flag
- On trigger: stops visitors, clears proxy tunnels, reloads INI,
  and re-registers proxies with frps over the existing control connection
- The frps connection itself is preserved (no re-login needed)

Usage:
  kill -HUP
  # or if running in foreground: kill -HUP <pid>

Changes:
- commandline.c/h: SIGHUP handler + reload flag + config file accessor
- control.c/h:   reload_xfrpc_config() + reload_check_timer_cb()
- visitor.c/h:   free_all_visitor_instances() for clean visitor shutdown
Adds configurable health checks for proxy services. When a local
service fails its health check, the proxy is temporarily unregistered
from frps and re-registered once the service recovers.

INI config per proxy section:
  [my_service]
  type = tcp
  local_ip = 127.0.0.1
  local_port = 8080
  remote_port = 80
  health_check_type = tcp          # tcp or http
  health_check_interval = 10       # seconds between checks (default 10)
  health_check_timeout = 3         # per-check timeout (default 3)
  health_check_max_failed = 1      # consecutive failures before down (default 1)
  health_check_url = /health       # HTTP path for http type (default /)

Implementation:
- health_check.c/h: libevent-based async TCP/HTTP checks with periodic timers
- control.c: integration with proxy registration lifecycle
  - Skips unhealthy proxies during start_proxy_services()
  - Re-registers proxy on recovery via health_check_result_cb
  - Stops health checks on disconnect and config reload
- config.c: parsing, defaults, dump, and cleanup for health_check_* fields
- client.h: health check config fields in proxy_service struct
Signed-off-by: liudf0716 <liudf0716@gmail.com>
…ndling

Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
…ional field support

Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
…atibility

Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
- Introduced `xtcp_visitor.c` and `xtcp_visitor.h` to implement the XTCP visitor functionality.
- Implemented the full NAT hole-punching flow including PreCheck, STUN discovery, and data relaying.
- Enhanced `visitor.c` to route XTCP visitors to the new handler.
- Added caching for STUN results to optimize subsequent connections.
- Implemented UDP socket management for hole-punching and data relay.
- Added necessary debug logging for better traceability during the XTCP process.

Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
…sitor

Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
- Add quic_transport.c/h: QUIC transport layer using ngtcp2 + wolfSSL/OpenSSL
- Add xtcp_client.c/h: client-side NAT hole-punch and P2P relay
- Support wolfSSL and OpenSSL crypto backends for ngtcp2
- Reuse STUN socket for hole-punch to preserve NAT mapping
- Add tunnel framing protocol with optional AES-128-CFB encryption
- Add reconnect logic with rate limiting for KeepTunnelOpen
- Add fallback_to visitor support for XTCP failure
- Fix ngtcp2 v1.24 API compatibility (callbacks, settings, transport params)
- Fix nathole.c format-truncation warnings
- Fix xtcp_visitor.c typedef/function name collisions

Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
…rapper

- CMakeLists.txt: add USE_WOLFSSL option (default ON), auto-fallback
  to OpenSSL when wolfSSL is not found. QUIC ngtcp2 crypto backend
  auto-detects and prefers the matching SSL library.
- Remove fastpbkdf2.c/h: was a trivial wrapper around PKCS5_PBKDF2_HMAC().
  crypto.c and nathole.c now call PKCS5_PBKDF2_HMAC() directly.
- Source code keeps using <openssl/*.h> includes (no #ifdef needed);
  on OpenWrt, wolfssl package provides compat headers at the standard path.

Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
- quic_client_transport.c/h: ngtcp2-based QUIC client that connects to
  frps over UDP/QUIC. Uses socketpair bridge so existing bufferevent-based
  code (msg.c, control.c, login.c) works unchanged.
- config: new 'protocol' option (tcp/quic) and 'quic_bind_port' for
  specifying frps's QUIC listening port.
- control.c: connect_server() and init_server_connection() now route
  through QUIC when protocol=quic. Work connections also use QUIC.
- CMakeLists.txt: quic_client_transport.c always compiled (has stubs
  when HAVE_NGTCP2 is not defined).

Usage:
  [common]
  protocol = quic
  quic_bind_port = 7000
  server_addr = 1.2.3.4
  server_port = 7000
Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
- Document wolfSSL as default TLS backend with OpenSSL fallback
- Add build options table (USE_WOLFSSL, ENABLE_QUIC, DEBUG)
- Add QUIC transport configuration section with frps and xfrpc examples
- Add TLS backend explanation section
- Update feature compatibility table with quic transport

Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
The ngtcp2 crypto backend (wolfssl/openssl) is now auto-detected based
on which library is actually installed, not tied to the general TLS
backend choice. This fixes the build when ngtcp2 was built with wolfssl
but USE_WOLFSSL=OFF is passed to cmake.

Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
Critical fixes:
- crypto.c: fix memory leak in encrypt_data/decrypt_data error paths
- crypto.c: add NULL check in init_main_decoder
- control.c: fix reconnect_timer event leak (store in main_ctl)
- control.c: fix event object leak in clear_main_control (add event_free)
- control.c: fix hash table dangling pointer (use del_proxy_client_by_stream_id)
- control.c: fix partial frame data hang (add set_cur_stream before break)

High/Medium fixes:
- config.c: add password validation in add_user_and_set_password
- config.c: replace exit(0) with exit(EXIT_FAILURE) on error paths
- config.c: fix SET_STRING_VALUE macro to free old value before strdup
- config.c: remove redundant field initialization in new_proxy_service
- config.c: free protocol field in free_common_config
- control.c: prefer system DNS with OpenWrt-aware resolv.conf search
- control.h: remove duplicate function declarations
- client.c: harden xdpi_engine SSH/HTTP/RDP protocol detection

Low fixes:
- client.c/xfrpc.c: remove duplicate #include directives
- tcpmux.c: replace GCC extensions with C11 stdatomic.h

Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
- control.c: fix init_main_control() reentry resource leak (use close_main_control)
- control.c: set main_ctl=NULL after free in TLS/DNS failure paths
- control.c: reset handle_tcp_mux static parser state on reconnect
- client.c: improve xdpi HTTP validation loop readability (i+5 < len)

Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
Fix critical issues preventing xfrpc from establishing working QUIC
connections with frps (quic-go based server):

1. TLS certificate loading for QUIC SSL_CTX:
   The QUIC client created its own SSL_CTX but never loaded CA, client
   cert, or private key from config. When frps requires mTLS
   (transport.tls.force=true), the handshake would succeed at the QUIC
   level but the server would immediately close the connection.
   Added tls_get_ca_file/cert_file/key_file() getters and load certs
   into the QUIC SSL_CTX using the same SSL library API (wolfSSL or
   OpenSSL) that created the context.

2. Open QUIC bidi stream before sending data:
   ngtcp2 requires streams to be explicitly opened via
   ngtcp2_conn_open_bidi_stream() before writev_stream can target them.
   Without this, ERR_STREAM_NOT_FOUND (-224) was returned and the
   connection entered ERR_DRAINING state immediately after handshake.

3. Disable tcp_mux for QUIC protocol:
   QUIC already provides native stream multiplexing. When tcp_mux=1
   (the default), the client sent tmux-framed messages on the QUIC
   stream, but frps expected raw frp protocol messages. This caused
   the server to fail parsing and close the connection. Now tcp_mux
   is automatically disabled when protocol=quic.

4. Handle BEV_EVENT_CONNECTED for QUIC connections:
   quic_connect_to_server() returns an already-connected bufferevent
   (backed by socketpair). libevent does NOT fire BEV_EVENT_CONNECTED
   for these, so login() and proxy registration were never triggered.
   Added manual handle_connection_success() call for QUIC in both
   start_base_connect() (control connection) and init_direct_client()
   (work connections).

5. QUIC work stream support:
   Instead of creating a new QUIC connection for each work connection
   (which caused reentrant event_base_loop and handshake timeouts),
   work connections now open additional bidi streams on the existing
   QUIC connection via quic_open_work_stream(). Data is relayed
   through per-stream socketpairs with proper stream ID routing in
   qc_recv_stream().

6. ERR_DRAINING handling:
   Added draining state checks in qc_write_udp() and qc_sp_read_cb()
   to prevent infinite error loops when the connection enters the
   draining state.

7. Port check in connect_server():
   connect_server() matched QUIC work connections by IP only, which
   incorrectly routed local service connections (e.g., 127.0.0.1:2222)
   through QUIC. Added port comparison to only match frps connections.

Remaining known issue: frps server panics with 'send on closed channel'
when QUIC control stream and work stream share the same connection.
This appears to be a server-side race condition in the QUIC control
lifecycle (stream 0 read EOF triggers control teardown while work
stream 4 registration is in progress). Requires frps-side fix.

Testing: verified with frps (quic-go) + TLS mTLS on localhost.
QUIC handshake completes, login succeeds, proxy registers, and
work stream opens. End-to-end data forwarding blocked by server panic.

Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
…op reentrancy

Problem:
  quic_connect_to_server() used a blocking while+event_base_loop()
  polling loop for the QUIC handshake. This caused 'reentrant
  invocation' warnings when called from the reconnect path
  (reconnect_timer_cb -> run_control -> quic_connect_to_server),
  since the timer callback runs inside an already-dispatching
  event loop. The reentrant calls could corrupt libevent state
  and cause unpredictable behavior.

Fix:
  Make the QUIC handshake fully asynchronous:
  - quic_connect_to_server() now returns immediately (int) after
    registering UDP read + handshake timer events on the base.
  - A new qc_handshake_timer_cb (10ms repeating) polls ngtcp2
    for handshake completion without calling event_base_loop().
  - When handshake completes, the timer opens stream 0 and invokes
    a quic_handshake_cb callback with the resulting bev.
  - control.c's quic_connect_done_cb handles bev setup + login
    trigger, called from the timer callback context.

Results:
  - Reentrant warnings: hundreds -> 0 (initial) / 2 (reconnect edge)
  - ERR_DRAINING infinite loop: eliminated (clean handling in timer)
  - Handshake succeeds reliably with retry
  - Login + proxy registration works end-to-end

Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
Root cause: The QUIC handshake timer (10ms repeating) called
qc_write_udp() between packet processing and handshake_confirmed
callback. ngtcp2 returned ERR_DRAINING from writev_stream() because
the handshake wasn't confirmed yet. The old code set draining=1
unconditionally, permanently killing the connection.

Fixes:
1. qc_write_udp: Don't set draining=1 on ERR_DRAINING — just return
   -1 and let callers decide. Transient ERR_DRAINING during handshake
   is not fatal.

2. qc_handshake_timer_cb: If draining=1 but handshake_confirmed=0,
   treat as transient — reset draining=0 and re-arm timer. Only treat
   as fatal when handshake is confirmed or done.

3. qc_handshake_timer_cb: Stop and free the handshake timer once
   handshake_done=1. Previously the timer kept firing every 10ms
   calling qc_write_udp() after the handshake completed, which could
   interfere with the established connection.

4. Add handshake_confirmed callback to ngtcp2 callbacks struct.
   This is required for the client to properly confirm the QUIC v1
   handshake with the server.

Verified: QUIC handshake now succeeds consistently (both TLS and mTLS).
Login + proxy registration works. Remaining frps RegisterWorkConn panic
is a server-side issue.

Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
Root cause: quic-go (frps) sends LoginResp as a STREAM frame on
stream 0 immediately after the QUIC handshake completes. If stream 0
is not registered in ngtcp2 by the time the STREAM frame arrives,
ngtcp2 silently drops it and recv_stream_data never fires.

The previous code opened stream 0 in the handshake timer callback,
which fires AFTER handshake_confirmed. But the server's STREAM frame
(LoginResp) arrives between handshake_completed and handshake_confirmed,
so it was always dropped.

Fix: Open bidi stream 0 in the handshake_completed callback, which
fires before the server sends LoginResp. This ensures stream 0 is
registered in ngtcp2 when the STREAM frame arrives.

Also:
- Initialize stream_id to -1 to detect early-open
- Skip duplicate stream open in timer callback
- Add debug logging for sp_read_cb and writev_stream
- Remove temporary fprintf debug statements

Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
liudf0716 added 13 commits June 14, 2026 15:39
… fix

Test infrastructure (test/e2e/):
- tcp_echo_server.py: Python TCP echo server for tunnel verification
- test_proxy.py: Automated test runner for tcp, mux, quic, tls scenarios
- configs/: frps + xfrpc configs for each scenario (different ports)
- certs/: self-signed TLS certs for the tls scenario

Results: tcp PASS, mux PASS, tls PASS, quic FAIL (known work stream FIN
issue between ngtcp2 and quic-go — needs packet-level investigation)

Code fixes:
- Add quic_work_stream_send_initial() to send NewWorkConn directly on
  QUIC wire, bypassing socketpair/event-loop (frps FIN'd stream before
  bev-flushed data arrived)
- send_msg_frp_server: for QUIC work streams, write directly to
  socketpair fd instead of bev buffering
- Add prepare_message() forward declaration
Root cause: sp_read_cb read ALL available data from the socketpair in
one call, but ngtcp2_conn_writev_stream could only fit ~1200 bytes into
a single QUIC packet. The unconsumed remainder was lost, causing data
truncation for payloads >1 QUIC packet (~1400 bytes).

Fix:
- Track pdatalen from ngtcp2_conn_writev_stream to know how many bytes
  were actually consumed
- Stash unconsumed data in out_buf
- On next sp_read_cb invocation, flush out_buf before reading new data
- Use event_active(read_ev) to re-trigger sp_read_cb when out_buf has
  remaining data after a partial send
- Clean up read_ev in FIN and stream_close handlers

Also:
- Add quic_interop_test.py for standalone ngtcp2↔quic-go echo testing
- Remove stale QUIC warning from test_proxy.py scenario description
- All 4 e2e scenarios now pass: tcp, mux, quic, tls
Remove debug logs that were added during QUIC interop debugging sessions:

quic_client_transport.c:
- Remove hex dump of received UDP packets and stream data
- Remove verbose per-packet state dumps (read_pkt, sp_read_cb, sp_writev)
- Remove entry/exit traces (qc_client_initial, recv_stream_data ENTER)
- Remove internal buffer state logging (control sp flush)
- Remove noisy per-write 'sending N bytes' logs

control.c:
- Remove hex dump of encrypted/decrypted message content ([ENC_MSG] IV, enc_data)
- Remove per-message tracing ([FRPS_MSG], [CTRL_WORK], [RECV_CB])
- Remove raw message content logging (Message content: ...)
- Remove login request length logging
- Remove [SEND_ENC] hex dump of outgoing encrypted messages
- Remove connection status change logging

crypto.c (SECURITY):
- Remove token/salt logging in encrypt_key (leaked auth credentials)
- Remove derived_key hex dump (leaked key material)
- Remove key/iv logging in encrypt_data and decrypt_data

Keep operational logs: connection events, errors, warnings, stream lifecycle,
heartbeat, handshake completion, QUIC transport state changes.

Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
QUIC transport maturity release:
- Full QUIC transport support (ngtcp2 + quic-go interop)
- Work stream data relay fix for large payloads
- IV-per-message protocol fix for frps compatibility
- Null-termination safety fixes (4 crash sites)
- Crypto context reset on reconnect
- Stream lifecycle management (FIN/close handling)
- E2E test suite (tcp/mux/quic/tls all pass)
- Debug logging cleanup (removed leaked credentials)

Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
…compile

On OpenWrt, both system OpenSSL (/usr/include/openssl/ssl.h) and
wolfSSL compat headers (/usr/include/wolfssl/openssl/ssl.h) exist.
Without the wolfssl/ prefix in the include path, tls.c picks up the
real OpenSSL header, so SSL_CTX_free is not macro-expanded to
wolfSSL_CTX_free — causing an unresolved symbol at link time.

Fix: include_directories(BEFORE /wolfssl) so
that <openssl/ssl.h> resolves to wolfSSL's compat header first.

Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
…tion

- Add ssl_compat.h conditional include wrapper for wolfSSL/OpenSSL
- Fix CMakeLists.txt to link both wolfSSL and OpenSSL libraries
- Rename DATA enum to TMUX_DATA to avoid wolfSSL oid_sum.h conflict
- Fix corrupted new_work_connection() #ifdef HAVE_NGTCP2 structure
- Fix IV protocol (iv_sent flag) for frps golib crypto interop
- Fix null-termination in msg_data_to_json() helper (4 call sites)
- Fix QUIC work stream FIN and partial write handling
- Remove excessive debug logging
- Bump version to 5.06.954

Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
wolfSSL's EVP compatibility layer is incomplete — it lacks
EVP_aes_128_cfb128 required by frp's AES-128-CFB encryption.
ssl_compat.h now always includes real OpenSSL headers since
CMakeLists.txt already links both wolfSSL and OpenSSL when
USE_WOLFSSL is enabled.

Bump version to 5.06.955.

Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
ngtcp2's OpenSSL crypto backend library is named libngtcp2_crypto_ossl,
not libngtcp2_crypto_openssl. This caused QUIC detection to fail in
OpenWrt cross-compilation when using OpenSSL as the ngtcp2 crypto backend.

Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
ngtcp2's OpenSSL crypto backend library is named libngtcp2_crypto_ossl
(not openssl). Update CMakeLists.txt to find the correct library name
and add USE_NGTCP2_OSSL define. Fix header includes and API calls in
quic_transport.c and quic_client_transport.c to use ossl naming.

Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
…ansport

- Add missing qc->base and qc->udp_read_ev initialization that was
  lost during previous edits
- Add ngtcp2_crypto_ossl_configure_client_session() call and
  SSL_set_app_data() for the ossl crypto backend
- Add missing openssl/rand.h include in quic_transport.c for RAND_bytes
- Replace deprecated ngtcp2_crypto_openssl_quic_method with
  ngtcp2_crypto_ossl_configure_{client,server}_session in quic_transport.c

Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
When both wolfSSL and OpenSSL are linked (QUIC enabled), SSL_CTX_new
resolves to wolfSSL's version because it appears first in the link
order. But libevent's bufferevent_openssl_socket_new uses real OpenSSL
types, causing TLS handshake failures. Put OpenSSL first so the real
SSL_CTX_new is used.

Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces major enhancements to the xfrpc client, including the integration of wolfSSL and ngtcp2 for QUIC transport support, NAT hole punching (XTCP) for P2P traversal, TCP/HTTP health checks, Unix Domain Socket (UDS) proxying, and SIGHUP-triggered configuration hot-reloading. The review identified several critical and high-severity issues across the new transport and proxy implementations: potential data truncation and protocol corruption due to unhandled partial writes or EAGAIN when writing directly to non-blocking sockets; a lack of upper-bound validation on incoming message lengths which could cause out-of-memory (OOM) conditions; unhandled partial writes in the QUIC client transport leading to high latency; leaked QUIC streams on socketpair closure; and a permanent leak of work stream slots. Additionally, potential buffer overflows from truncated Unix socket paths and unbounded memory growth in HTTP health checks were flagged.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread control.c
Comment on lines +2113 to +2116
ssize_t w = write(fd, req_msg, total_len);
if (w < 0)
debug(LOG_ERR, "QUIC work stream write: %s", strerror(errno));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Writing directly to a non-blocking socket using write without handling partial writes or EAGAIN can lead to silent data truncation and protocol corruption. If the socket buffer is full, write will return EAGAIN or write fewer bytes than requested, and the unwritten bytes will be silently discarded. Consider using a loop to handle partial writes or utilizing evbuffer / bufferevent APIs to manage the buffering safely.

Comment thread control.c
Comment on lines +960 to +964
int data_len = (int)msg_hton(msg->length);
char *json_str = malloc(data_len + 1);
if (!json_str) return;
memcpy(json_str, msg->data, data_len);
json_str[data_len] = '\0';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Casting msg->length (which is received from the remote server) to a signed int without any upper-bound validation can lead to integer overflow or out-of-memory (OOM) denial of service. If a malformed or malicious packet specifies a very large length, malloc might fail or, worse, lead to buffer overflows if signed/unsigned mismatches occur in subsequent operations. A sanity check should be added to ensure the length is within a reasonable limit (e.g., < 10MB).

	uint32_t data_len = msg_hton(msg->length);
	if (data_len > 10 * 1024 * 1024) {
		debug(LOG_ERR, "Received message length too large: %u", data_len);
		return;
	}
	char *json_str = malloc(data_len + 1);
	if (!json_str) return;
	memcpy(json_str, msg->data, data_len);
	json_str[data_len] = '\0';

Comment thread quic_client_transport.c
Comment on lines +104 to +109
size_t before = evbuffer_get_length(qc->quic_out_buf);
int n = evbuffer_write(qc->quic_out_buf, qc->sp_fd);
debug(LOG_DEBUG, "QUIC client: sp flush %d bytes (buf %zu -> %zu)",
n, before, evbuffer_get_length(qc->quic_out_buf));
if (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK)
debug(LOG_ERR, "QUIC client: sp write err: %s", strerror(errno));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

evbuffer_write can perform a partial write if the socket's write buffer is full. When this happens, the remaining data stays in qc->quic_out_buf. However, there is no write event registered on qc->sp_fd to resume flushing when the socket becomes writeable again. The remaining data will remain stuck until the next UDP read event triggers another flush, causing high latency or hangs.

Comment thread quic_client_transport.c
Comment on lines +491 to +492
ssize_t n = read(fd, buf, sizeof(buf));
if (n <= 0) { if (n < 0 && errno == EAGAIN) return; return; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If read returns 0 (EOF) or a negative value (excluding EAGAIN/EWOULDBLOCK), it indicates that the local side of the socketpair has closed. Currently, the callback simply returns without notifying the remote QUIC peer, leading to a leaked QUIC stream. The stream should be explicitly shut down using ngtcp2_conn_shutdown_stream.

Comment thread quic_client_transport.c
Comment on lines +956 to +964
if (qc->work_stream_count >= 64) {
debug(LOG_ERR, "QUIC: too many work streams");
close(sp[0]); close(sp[1]);
return NULL;
}
struct work_stream_relay *relay = &qc->work_streams[qc->work_stream_count++];
relay->stream_id = sid;
relay->sp_fd = sp[0];
relay->out_buf = evbuffer_new();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

When a work stream is closed in qc_stream_close, its resources are freed, but the slot in qc->work_streams is not removed, and qc->work_stream_count is never decremented. This creates a permanent leak of stream slots, meaning a single QUIC connection can only ever handle 64 work streams before failing with "QUIC: too many work streams". Reusing empty slots (where sp_fd == -1) resolves this limitation.

	struct work_stream_relay *relay = NULL;
	for (int i = 0; i < qc->work_stream_count; i++) {
		if (qc->work_streams[i].sp_fd < 0) {
			relay = &qc->work_streams[i];
			break;
		}
	}
	if (!relay) {
		if (qc->work_stream_count >= 64) {
			debug(LOG_ERR, "QUIC: too many work streams");
			close(sp[0]); close(sp[1]);
			return NULL;
		}
		relay = &qc->work_streams[qc->work_stream_count++];
	}
	relay->stream_id = sid;
	relay->sp_fd = sp[0];
	relay->out_buf = evbuffer_new();

Comment thread client.c
Comment on lines +294 to +297
struct sockaddr_un sun;
memset(&sun, 0, sizeof(sun));
sun.sun_family = AF_UNIX;
strncpy(sun.sun_path, unix_path, sizeof(sun.sun_path) - 1);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If unix_path exceeds sizeof(sun.sun_path) - 1 (typically 107 bytes on Linux), strncpy will truncate the path. Connecting to a truncated path will fail or connect to the wrong socket. A length check should be added to reject paths that are too long.

	if (strlen(unix_path) >= sizeof(sun.sun_path)) {
		debug(LOG_ERR, "Unix socket path too long: %s", unix_path);
		close(fd);
		return NULL;
	}
	struct sockaddr_un sun;
	memset(&sun, 0, sizeof(sun));
	sun.sun_family = AF_UNIX;
	strncpy(sun.sun_path, unix_path, sizeof(sun.sun_path) - 1);

Comment thread health_check.c
Comment on lines +377 to +383
struct evbuffer *input = bufferevent_get_input(bev);

/* Wait until we have the full status line (terminated by \r\n).
* In high-latency conditions the status line may arrive across
* multiple TCP segments; parsing prematurely would cause false
* health-check failures. */
struct evbuffer_ptr crlf = evbuffer_search(input, "\r\n", 2, NULL);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If the HTTP server sends a very long status line or keeps sending data without a \r\n, the input buffer will grow indefinitely, leading to high memory usage or OOM. A maximum limit (e.g., 4096 bytes) should be enforced on the input buffer length before failing the health check.

	struct evbuffer *input = bufferevent_get_input(bev);
	if (evbuffer_get_length(input) > 4096) {
		debug(LOG_WARNING, "Health check [%s]: HTTP status line too long", entry->proxy_name);
		hc_finish_check(entry, 0);
		return;
	}
	struct evbuffer_ptr crlf = evbuffer_search(input, "\r\n", 2, NULL);

liudf0716 added 13 commits June 16, 2026 22:09
Add native TOML parsing for frp-compatible configuration files.
xfrpc now supports both INI (.ini) and TOML (.toml) config formats,
auto-detected by file extension via -c option.

TOML features supported:
- Top-level key-value pairs (serverAddr, serverPort, etc.)
- Dotted keys for nested config (auth.token, transport.tls.enable)
- [[proxies]] array of tables for proxy definitions
- [[visitors]] array of tables for visitor definitions
- Inline arrays (customDomains = ["a", "b"])
- String, integer, and boolean value types
- Comments (# ...) and blank lines

Key mapping from frp TOML to xfrpc internal fields:
- serverAddr -> server_addr, serverPort -> server_port
- auth.token -> auth_token
- transport.tls.enable -> tls_enable
- proxies[].secretKey -> sk, proxies[].localIP -> local_ip
- visitors[].serverName -> server_name, visitors[].bindAddr -> bind_addr

New files:
- toml_parser.c/h: Lightweight TOML parser (no external dependencies)
- xfrpc.toml: STCP proxy + visitor example config
- xfrpc_min.toml: Minimal TCP proxy example
- xfrpc_full.toml: Full-featured example with HTTP, TCP, STCP

Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
The init script outputs serviceType for TCP/TCPMux proxies but the
TOML load_toml_proxies() function never read it, causing the field
to be silently ignored. Add toml_get for 'serviceType' mapped to
convert_service_type().

Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
Replace the hand-written toml_parser.c/h with a thin wrapper around
cktan/tomlc17 (TOML v1.1 compliant, C17, amalgamated).

- Vendor tomlc17.h + tomlc17.c under vendor/tomlc17/
- Rewrite toml_parser.c/h as a compatibility wrapper
  (xfrpc_toml_* functions with old-name macros for config.c)
- Update config.c: use void* for section handles, remove
  internal struct access
- Update CMakeLists.txt: add vendor/tomlc17/tomlc17.c

Benefits: full TOML v1.1 compliance, proper dotted-key support,
no hand-maintained parser code.

Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
The scratch buffer was replaced by thread-local storage in
xfrpc_toml_get(). Remove the dead field to save 1KB per doc.

Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
Implement AES-128-CFB encryption and Snappy compression for proxy work
connections, compatible with frp's use_encryption/use_compression protocol.

Encryption (AES-128-CFB):
- Key derived from auth.token via PBKDF2(salt='crypto', iter=64, SHA1)
- Random 16-byte IV prepended to first write
- Uses OpenSSL EVP API

Compression (Snappy):
- Applied before encryption on send, after decryption on recv
- Uses snappy-c library
- Conditionally compiled with HAS_SNAPPY

Data flow (client→server): compress → encrypt → send
Data flow (server→client): recv → decrypt → decompress

Files added:
- crypto_stream.c/h: AES-128-CFB + snappy wrapper functions

Files modified:
- client.h: add crypto/compression context fields to proxy_client
- client.c: initialize crypto from proxy_service, cleanup on free
- proxy_tcp.c: encrypt/decrypt data in c2s/s2c relay callbacks
- CMakeLists.txt: add crypto_stream.c, find snappy library

Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
When HAS_SNAPPY is not defined (e.g. OpenWrt), provide non-inline
stub implementations of snappy_ctx_new/free/compress/decompress in
crypto_stream.c so linking succeeds.

Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
zlib is already a dependency of xfrpc, available on all platforms
including OpenWrt. No need for external snappy library.

Wire format: [4-byte BE compressed_len][zlib compressed data]

Note: not compatible with frp's snappy compression format.
use_compression works between xfrpc instances only.

Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
Vendor snappy-c (Linux kernel version, ~1600 lines) into
vendor/snappy/ so use_compression produces snappy-framed output
compatible with frp's golang implementation.

No external snappy library needed - compiles from source on all
platforms including OpenWrt.

Replaces the previous zlib compression approach.

Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
Implement OAuth2 client_credentials grant for OIDC authentication,
compatible with frp's auth.oidc.* configuration.

New TOML config fields:
- auth.method = "oidc" (instead of "token")
- auth.oidc.clientID
- auth.oidc.clientSecret
- auth.oidc.audience
- auth.oidc.scope
- auth.oidc.tokenEndpointURL
- auth.oidc.trustedCaFile
- auth.oidc.insecureSkipVerify

Implementation:
- oidc_auth.c/h: HTTP(S) POST to token endpoint, JSON response parsing
- Uses raw sockets + OpenSSL (no external HTTP library dependency)
- msg.c: login message uses OIDC access_token as privilege_key
- config.c: TOML + INI parser support for all OIDC fields
- xfrpc.init: UCI → TOML generation for OIDC config

Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
Compatible with frp's requestHeaders.set.* and responseHeaders.set.*
TOML configuration. Headers are sent to frps in the NewProxy message
as 'headers' and 'response_headers' JSON maps.

- config.c: TOML parser reads requestHeaders.set/responseHeaders.set
  via new xfrpc_toml_get_table_pairs() wrapper
- config.c: INI parser reads request_headers/response_headers
- msg.c: JSON marshaling as 'headers'/'response_headers' maps
- toml_parser.c/h: new xfrpc_toml_get_table_pairs() for table iteration
- xfrpc.init: UCI options request_headers/response_headers
  (comma-separated key=value format)

Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
Read startTime and endTime from [[proxies]] TOML sections,
enabling time-based proxy management through TOML config files.

Previously only supported via INI config (config.c common_handler).

Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
Remove IOD (I/O Direct) proxy:
- Remove from valid_types[] in config.c
- Remove IOD validation in validate_proxy()
- Remove handle_iod() from init script

Remove FTP proxy:
- Remove proxy_ftp.c from build (CMakeLists.txt)
- Remove ftp_pasv struct and FTP callbacks from proxy.h
- Remove is_ftp_proxy() from client.c/h
- Remove FTP dispatch in setup_proxy_callbacks()
- Remove new_ftp_data_proxy_service() from config.c
- Remove get_ftp_data_proxy_name() from config.c/h
- Remove handle_ftp_configuration() from control.c
- Remove FTP-specific msg marshaling from msg.c
- Remove remote_data_port from proxy struct
- Remove ftp_cfg_proxy_name from proxy_service struct

These proxy types were unused and added complexity.

Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
Major changes since 5.06.956:
- TOML configuration support (frp-compatible) via tomlc17
- OIDC authentication (auth.oidc.*)
- AES-128-CFB use_encryption (frp-compatible)
- Snappy use_compression (frp-compatible, vendored)
- HTTP requestHeaders/responseHeaders support
- Time-based proxy management (startTime/endTime)
- serviceType XDPI classification
- Removed IOD and FTP proxy types
- All proxy fields exposed via OpenWrt UCI init script

Signed-off-by: Dengfeng Liu <liudf0716@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants