fix(session): resolve race conditions (#151)
## Summary Comprehensive fixes for race conditions, concurrency bugs, and a critical session handling bug across the session package and its child packages. ## Changes ### Bug Fix: waitForTCPIPForward fails on non-tcpip-forward global requests - Changed `waitForTCPIPForward()` from a single select to a for loop that drains non-tcpip-forward requests gracefully - Non-tcpip-forward requests now receive a false reply and the function continues waiting for the actual forward request - The 500ms timeout resets after each request to handle slow clients with multiple early requests - Updated and added tests to cover the new behavior: Wrong_Request_Type_Then_Timeout, Multiple_Non-Forward_Requests_Then_Success, Timeout_After_Non-Forward_Requests ### Race Condition Fixes - Added sync.RWMutex to the forwarder struct to protect concurrent field access - Fixed data race in forwarder setters (`SetType()`, `SetForwardedPort()`, `SetListener()`) by adding `Lock()`/`Unlock()` protection - Fixed data race in forwarder getters (`TunnelType()`, `ForwardedPort()`, `Listener()`) by adding `RLock()`/`RUnlock()` protection - Added sync.RWMutex to the slug struct to protect concurrent field access - Fixed data race in `slug.Set()` by adding `Lock()`/`Unlock()` protection - Fixed data race in `slug.String()` by adding `RLock()`/`RUnlock()` protection ### Concurrency Safety Improvements - Changed `OpenForwardedChannel()` to use `f.ForwardedPort()` getter instead of direct field access - Fixed `Close()` in forwarder to use `f.Listener()` getter consistently instead of accessing `f.listener` directly - Fixed data race in `HandleTCPForward()` where the goroutine running `tcpServer.Serve(listener)` wrote to the outer err variable from the enclosing function scope - Changed err = `tcpServer.Serve(listener)` to `if err := tcpServer.Serve(listener)` so the goroutine uses a local variable Reviewed-on: #151 Co-authored-by: Bagas <bagas@fossy.my.id> Co-committed-by: Bagas <bagas@fossy.my.id>
This commit was merged in pull request #151.
This commit is contained in:
@@ -807,7 +807,7 @@ func (m *mockNewChanFail) Accept() (ssh.Channel, <-chan *ssh.Request, error) {
|
||||
}
|
||||
|
||||
func TestWaitForTCPIPForward_EdgeCases(t *testing.T) {
|
||||
t.Run("Wrong Request Type", func(t *testing.T) {
|
||||
t.Run("Wrong Request Type Then Timeout", func(t *testing.T) {
|
||||
_, sReqs, _, cConn, cleanup := setupSSH(t)
|
||||
defer cleanup()
|
||||
|
||||
@@ -817,10 +817,65 @@ func TestWaitForTCPIPForward_EdgeCases(t *testing.T) {
|
||||
_, _, _ = cConn.SendRequest("not-tcpip-forward", true, nil)
|
||||
}()
|
||||
|
||||
start := time.Now()
|
||||
req := s.waitForTCPIPForward()
|
||||
elapsed := time.Since(start)
|
||||
|
||||
if req != nil {
|
||||
t.Error("expected nil request")
|
||||
}
|
||||
if elapsed < 400*time.Millisecond {
|
||||
t.Errorf("expected timeout ~500ms, got %v", elapsed)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Multiple Non-Forward Requests Then Success", func(t *testing.T) {
|
||||
_, sReqs, _, cConn, cleanup := setupSSH(t)
|
||||
defer cleanup()
|
||||
|
||||
s := &session{initialReq: sReqs}
|
||||
|
||||
go func() {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
_, _, _ = cConn.SendRequest("keepalive@openssh.com", false, nil)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
_, _, _ = cConn.SendRequest("hostkeys-00@openssh.com", false, nil)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
_, _, _ = cConn.SendRequest("tcpip-forward", true, nil)
|
||||
}()
|
||||
|
||||
req := s.waitForTCPIPForward()
|
||||
if req == nil {
|
||||
t.Error("expected tcpip-forward request, got nil")
|
||||
}
|
||||
if req != nil && req.Type != "tcpip-forward" {
|
||||
t.Errorf("expected tcpip-forward, got %s", req.Type)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Timeout After Non-Forward Requests", func(t *testing.T) {
|
||||
_, sReqs, _, cConn, cleanup := setupSSH(t)
|
||||
defer cleanup()
|
||||
|
||||
s := &session{initialReq: sReqs}
|
||||
|
||||
go func() {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
_, _, _ = cConn.SendRequest("keepalive@openssh.com", false, nil)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
_, _, _ = cConn.SendRequest("hostkeys-00@openssh.com", false, nil)
|
||||
}()
|
||||
|
||||
start := time.Now()
|
||||
req := s.waitForTCPIPForward()
|
||||
elapsed := time.Since(start)
|
||||
|
||||
if req != nil {
|
||||
t.Error("expected nil request after timeout")
|
||||
}
|
||||
if elapsed < 400*time.Millisecond {
|
||||
t.Errorf("expected timeout ~500ms after last request, got %v", elapsed)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Channel Closed", func(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user