Skip to content

Match forwarded-tcpip opens to registered forwards - #1148

Open
ejohnstown wants to merge 7 commits into
wolfSSL:masterfrom
ejohnstown:fixfwd
Open

Match forwarded-tcpip opens to registered forwards#1148
ejohnstown wants to merge 7 commits into
wolfSSL:masterfrom
ejohnstown:fixfwd

Conversation

@ejohnstown

Copy link
Copy Markdown
Contributor

RFC 4254 7.2 says a forwarded-tcpip open answers a forward the client asked for, but we accepted any of them. This tracks what wolfSSH_FwdRemoteSetup() registered and refuses opens naming anything else. Enforcement starts at the first registration, so a client that frames tcpip-forward itself is unaffected. Two wolfSSH_SendPacket() bugs found while testing it are fixed here too.

  • Per-session list of registered forwards. Wildcard binds match on port alone, port 0 now requires want-reply, and repeat setups of one bind share a registration.
  • A cancel stops matching as it goes out; a want-reply cancel stays registered until the peer answers, since a refusal leaves the listener up.
  • Replies carry no request id, so a per-session queue pairs them in send order. Registration is split around the send, since the highwater callback reports a rekey's errors as the send's.
  • WS_CBIO_ERR_ISR fell through to WS_SOCKET_ERROR_E, discarding framed output the peer never refused; retry instead, as ReceiveData() does. And WS_CBIO_ERR_GENERAL left the discarded packet counted in plainSz, so SendChannelData() flushed nothing and called it a success.
  • Tests in tests/regress.c cover matching, cancel, overlapping requests, send-order pairing, port 0, registration around the send, reentrancy from a callback, and both send paths.

The three affected API contracts are documented in wolfssh/ssh.h; no signatures changed.

Issue: ZD-22195

@ejohnstown
ejohnstown requested review from wolfSSL-Fenrir-bot and a lite review from Copilot August 6, 2026 23:15

@wolfSSL-Fenrir-bot wolfSSL-Fenrir-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.

Fenrir Automated Review — PR #1148

Scan targets checked: wolfssh-bugs, wolfssh-src

Findings: 2
2 finding(s) posted as inline comments (see file-level comments below)

This review was generated automatically by Fenrir. Findings are non-blocking.

Comment thread tests/regress.c
Comment thread src/internal.c

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

This PR tightens forwarded-tcpip channel-open handling to only accept opens that correspond to tcpip-forward registrations made via wolfSSH_FwdRemoteSetup() (per RFC 4254 §7.2), and fixes two wolfSSH_SendPacket() error-handling issues discovered during testing.

Changes:

  • Track per-session remote-forward registrations and enforce matching for inbound forwarded-tcpip opens (with wildcard + cancel/overlap semantics).
  • Introduce a send-order reply queue to pair REQUEST_SUCCESS/FAILURE with want-reply global requests/forward requests.
  • Fix wolfSSH_SendPacket() handling for WS_CBIO_ERR_ISR and WS_CBIO_ERR_GENERAL, and expand regression/API tests accordingly.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
wolfssh/ssh.h Documents updated remote-forward enforcement semantics and global-request reply-queue behavior.
wolfssh/internal.h Adds forward tracking structs, reply queue state, and a flush counter to disambiguate post-send errors.
src/ssh.c Integrates forward prepare/commit/discard around sends; enforces port-0 want-reply; queues want-reply global requests.
src/internal.c Implements forward tracking + reply pairing; enforces forwarded-tcpip matching; fixes wolfSSH_SendPacket() ISR/general error behavior.
tests/regress.c Adds extensive regression coverage for matching, cancellation semantics, reentrancy, send-order pairing, and send error paths.
tests/api.c Adds API bad-args coverage for port-0 without want-reply.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread wolfssh/internal.h Outdated
Comment thread src/internal.c Outdated
Comment thread src/internal.c
BuildDirectTcpipExtra() and BuildGlobalRequestFwdPacket() have callers in
several conditional blocks, and a build with none of them left the two
functions unused, which -Werror turns into a build failure.
ReadUint32() next to them already carries the attribute for the same
reason.

- --disable-server builds regress.c again.
A send's return code cannot tell a caller its request is on its way. The
highwater callback runs after the last byte goes out, so a rekey's
errors surface as the send's, and WS_WANT_WRITE leaves the packet framed
for the next flush.

- Count the flushes wolfSSH_SendPacket() completes.
- Compare that count across a send to tell those outcomes apart.
- SendGlobalRequest() and SendGlobalRequestFwd() carry the answer in an
  optional out-param.
- Both callers pass NULL, so nothing acts on it yet.

Issue: ZD-22195
RFC 4254 7.2 says a forwarded-tcpip open answers a forward the client
asked for, so refuse an open naming anything else. Enforcement starts at
the first wolfSSH_FwdRemoteSetup(), leaving a client that frames
tcpip-forward itself unaffected.

- Register each setup per session. A wildcard bind matches on port alone.
- Port 0 now requires want-reply, since only the reply names the port.
- Repeat setups of one bind share a registration, so one cancel undoes
  it.
- A cancel stops matching as it goes out, but a want-reply cancel stays
  registered until the peer answers: a refusal leaves the listener up.
- Replies carry no request id, so a per-session queue pairs them in send
  order. A want-reply wolfSSH_global_request() takes a slot as well.
- Registration is split around the send: allocate first, link once the
  request reached the wire.
- A request resolves its registration on commit, since sending runs
  application callbacks that can reenter the library.
- Tests cover matching, cancel, overlapping requests, send-order
  pairing, port 0, registration around the send, and reentrancy from a
  callback.
- They drive a client session, so they sit outside the server-only block
  in regress.c and run in a --disable-server build. The harness struct,
  its teardown, the channel-open-failure helpers and the forwarding
  callback moved out with them, shared with the server-side tests.

Contracts for wolfSSH_FwdRemoteSetup(), wolfSSH_FwdRemoteCancel() and
wolfSSH_global_request() are in wolfssh/ssh.h.

Issue: ZD-22195
A signal was turned into a fatal error, and a refused send left its
packet counted in the output buffer. Both fixes are in
wolfSSH_SendPacket(), so they cover every sender.

- WS_CBIO_ERR_ISR fell through to WS_SOCKET_ERROR_E. Nothing went out
  and the session is unharmed, so retry, as ReceiveData() already does.
- Callers that discard a packet on error, like the KEX and userauth
  sends, were throwing away framed output the peer never refused.
- On WS_CBIO_ERR_GENERAL the buffer was shrunk with the packet still
  counted in plainSz, so SendChannelData() flushed nothing and called it
  a success. Clear it with the packet it described.
- Tests pin the retry from a forwarding sender and a plain global
  request, and drive a channel send through a would-block and a refused
  flush.
A slot named its forward only once its request committed, so the scans
could not see a request mid-send. A cancel the peer confirms while a
fresh setup for the same bind is still going out then found nothing
standing for the forward and unlinked it, leaving the peer with a
listener the client refuses every open for.

- Name a slot by its bind until it commits.
- FwdReplyNames() answers for both FwdReplyHasSetup() and
  FwdReplyNewest().
- The bind is borrowed from the caller and dropped at commit, the
  lifetime WOLFSSH_FWD_PENDING already assumes for it.
- A pointer to the entry would not do: the send runs callbacks that can
  free and remake it, which is why the entry is re-resolved at commit.
- Test drives a confirmed cancel against a re-setup still in its send
  window.
Only the peer gives a slot back, by answering, so a peer that never
answers a want-reply global request lets the queue grow for the life of
the session, and the queue scans run once per forward on every inbound
forwarded-tcpip open.

- WOLFSSH_MAX_FWD_REPLIES bounds the queue.
- Refuse before the request is framed: one whose slot was never queued
  would mispair every later reply.
- Return WS_RESOURCE_E, not the WS_MEMORY_E that nothing failing to
  allocate would have yielded.
- Test fills the queue, then covers the refusal, that a refused setup
  registers nothing, and a slot coming back on an answer.
The forwarded-tcpip check is a behaviour change on a shipped API, so an
application that trips over it needs a way out that isn't abandoning
wolfSSH_FwdRemoteSetup(). wolfSSH_SetFwdRemoteMatch() relaxes the check
for the session.

- STRICT is the default and keeps the bind-plus-port rule.
- PORT compares the port alone, for a peer that rewrites the bind
  address it echoes back, which STRICT refuses every open from.
- OFF accepts any open, as wolfSSH did before the check existed.
- Tests cover each setting, and a refused one leaving the default in
  place.
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.

3 participants