Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions docs/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,19 @@ sshgate correlate --db ./sshgate.db --log /var/log/auth.log <fingerprint>
Use `--log /var/log/secure` on distributions that write SSH authentication
events there. Use `--window 5m` to widen the matching window.

When `sshd` only listens behind `sshgate`, it logs every client as the gate
(for example `Accepted publickey for alice from 127.0.0.1 port 54321`), so
source IPs do not match. Each connection forwarded to the backend logs a
`CONNECTED` line with the local address of the backend socket:

```text
[203.0.113.7] CONNECTED <fingerprint> backend=127.0.0.1:22 local=127.0.0.1:54321
```

The `local` port is the port `sshd` reports, so joining the two lines on host,
port, and a short time window recovers the client IP and fingerprint for each
authenticated session.

## Troubleshooting

- **Permission denied opening the database:** the service user needs write
Expand Down
3 changes: 3 additions & 0 deletions proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,9 @@ func handleConn(client net.Conn, route gateproxy.Route, st *store.Store, allowUn
return
}
defer closeConnection(backend, clientIP, "backend")
// The backend sees sshgate as the peer, so sshd logs "from 127.0.0.1 port
// N". Logging the local address lets that line be joined to the client IP.
log.Printf("[%s] CONNECTED %s backend=%s local=%s", clientIP, kex.fingerprint.Hash, route.Backend, backend.LocalAddr())

_ = backend.SetDeadline(time.Now().Add(handshakeTimeout))
if _, err := backend.Write(clientID.bytes); err != nil {
Expand Down
95 changes: 95 additions & 0 deletions proxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import (
"bufio"
"bytes"
"io"
"log"
"net"
"path/filepath"
"strings"
"sync"
"testing"
"time"

Expand Down Expand Up @@ -169,3 +172,95 @@ func TestApprovedClientChecksBannerAndPreservesBufferedTraffic(t *testing.T) {
}
}
}

type lockedBuffer struct {
mu sync.Mutex
buf bytes.Buffer
}

func (b *lockedBuffer) Write(p []byte) (int, error) {
b.mu.Lock()
defer b.mu.Unlock()
return b.buf.Write(p)
}

func (b *lockedBuffer) String() string {
b.mu.Lock()
defer b.mu.Unlock()
return b.buf.String()
}

func TestConnectedLogsBackendLocalAddress(t *testing.T) {
logs := &lockedBuffer{}
prev := log.Writer()
log.SetOutput(logs)
t.Cleanup(func() { log.SetOutput(prev) })

ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer func() { _ = ln.Close() }()
st, err := NewStore(filepath.Join(t.TempDir(), "gate.db"))
if err != nil {
t.Fatal(err)
}
defer func() { _ = st.Close() }()
packet := kexInitPacket()
kex, err := readSSHKexInit(bufio.NewReader(bytes.NewReader(packet)), "SSH-2.0-test")
if err != nil {
t.Fatal(err)
}
if err := st.UpsertStatus(kex.fingerprint.Hash, StatusApproved, ""); err != nil {
t.Fatal(err)
}

// The backend's view of its peer is what sshd would log as "port N".
backendPeer := make(chan string, 1)
go func() {
c, err := ln.Accept()
if err != nil {
backendPeer <- ""
return
}
defer func() { _ = c.Close() }()
backendPeer <- c.RemoteAddr().String()
_ = c.SetDeadline(time.Now().Add(3 * time.Second))
r := bufio.NewReader(c)
if _, err := r.ReadString('\n'); err != nil {
return
}
if _, err := c.Write([]byte("SSH-2.0-test-server\r\n")); err != nil {
return
}
_, _ = io.Copy(io.Discard, r)
}()

client, peer := net.Pipe()
done := make(chan struct{})
go func() {
defer close(done)
handleConn(client, gateproxy.Route{Backend: ln.Addr().String()}, st, false, nil, testBanner("SSH-2.0-test-server"), nil)
}()
_ = peer.SetDeadline(time.Now().Add(3 * time.Second))
if _, err := peer.Write([]byte("SSH-2.0-test\r\n")); err != nil {
t.Fatal(err)
}
if _, err := bufio.NewReader(peer).ReadString('\n'); err != nil {
t.Fatal(err)
}
if _, err := peer.Write(packet); err != nil {
t.Fatal(err)
}
peerAddr := <-backendPeer
_ = peer.Close()
<-done

if peerAddr == "" {
t.Fatal("backend never accepted a connection")
}
want := "CONNECTED " + kex.fingerprint.Hash + " backend=" + ln.Addr().String() + " local=" + peerAddr
if !strings.Contains(logs.String(), want) {
t.Fatalf("log missing %q\ngot:\n%s", want, logs.String())
}
}
Loading