Skip to content

Add CloseWrite() method to rwcConn in cnet/conn_rwc.go to support proper TCP connection closure in SOCKS5 mode.#548

Open
threatexpert wants to merge 1 commit into
jpillora:masterfrom
threatexpert:master
Open

Add CloseWrite() method to rwcConn in cnet/conn_rwc.go to support proper TCP connection closure in SOCKS5 mode.#548
threatexpert wants to merge 1 commit into
jpillora:masterfrom
threatexpert:master

Conversation

@threatexpert

Copy link
Copy Markdown

I encountered an issue while using Chisel in SOCKS5 mode, where the following connection flow caused unexpected behavior:

tcpclient ---use-socks5--> chisel-client --> chisel-server --> tcpserver
When the tcpserver actively closes the TCP connection, the chisel-server is notified correctly. However, the chisel-client does not receive the closure notification, which I believe is a bug.
If the tcpclient actively closes the TCP connection, the chisel-server successfully closes the connection with tcpserver, and everything works as expected.
Root Cause: Upon investigation, I found that when a channel object is passed through the go-socks5 process, the conversion to NewRWCConn(stream) prevents the CloseWrite method from being called on the connection.

In go-socks5/request.go, the following code is responsible for handling the connection closure:

type closeWriter interface {
    CloseWrite() error
}

// proxy is used to shuffle data from src to destination, and sends errors down a dedicated channel
func proxy(dst io.Writer, src io.Reader, errCh chan error) {
    _, err := io.Copy(dst, src)
    if tcpConn, ok := dst.(closeWriter); ok {
        tcpConn.CloseWrite()
    }
    errCh <- err
}

Here, the dst object needs to implement the closeWriter interface. When tcpserver actively closes the TCP connection, the chisel-server attempts to close the channel. However, since the object net.Conn from NewRWCConn does not implement CloseWrite, the connection closure does not propagate properly.

Solution: I believe adding a CloseWrite() method to cnet/conn_rwc.go would resolve this issue. The method would attempt to call CloseWrite on the underlying ReadWriteCloser object, if it supports it.

The proposed method is:

type closeWriter interface {
    CloseWrite() error
}

func (c *rwcConn) CloseWrite() error {
    if cw, ok := c.ReadWriteCloser.(closeWriter); ok {
        return cw.CloseWrite()
    }
    return nil
}

This change ensures that the rwcConn type can correctly close connections when necessary, especially when the connection involves a channel object that supports the CloseWrite() method. This will fix the issue where chisel-client fails to receive the connection closure notification when tcpserver closes the connection.

jpillora added a commit that referenced this pull request Jul 16, 2026
- cio.Pipe now propagates half-closes: clean EOF in one direction
  CloseWrites the destination while the other direction keeps
  flowing; copy errors still tear down both ends, and Pipe closes
  both conns before returning (no caller changes needed)
- rwcConn delegates CloseWrite to the underlying stream so the
  SOCKS path half-closes through ssh channels (from PR #548)
- exit side dials the target before accepting the ssh channel and
  rejects it on failure (ssh.ConnectionFailed), so inbound clients
  see a prompt close instead of a live conn to a dead target
- dial is context-bound (tunnel teardown cancels it) with a 30s
  default timeout, tunable via CHISEL_DIAL_TIMEOUT
- adapted from PRs #536/#548/#538; dropped #538's accept-loop
  serialization and error-text-into-stream reset behavior
- e2e: half-close echo (fails on old Pipe, verified), channel
  rejection for dead targets, prompt local-conn close; unit test
  for rwcConn CloseWrite delegation

Closes md task 19

Co-Authored-By: Claude Fable 5 <noreply@anthropic.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.

1 participant