133 lines
5.4 KiB
Go
133 lines
5.4 KiB
Go
package services
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"fmt"
|
|
"net"
|
|
"strconv"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/ssh"
|
|
)
|
|
|
|
// NewSSHHandler returns a TCP handler that performs SSH handshake and logs auth attempts.
|
|
// getSigner is used to obtain a host key signer (so the app can reuse/generate one).
|
|
func NewSSHHandler(log LoggerFunc, getSigner func() (ssh.Signer, error)) Handler {
|
|
return func(conn net.Conn) {
|
|
defer conn.Close()
|
|
remote := conn.RemoteAddr().String()
|
|
sessionID := fmt.Sprintf("%x", time.Now().UnixNano())
|
|
start := time.Now()
|
|
|
|
signer, err := getSigner()
|
|
if err != nil {
|
|
log(Record{Timestamp: Now(), RemoteAddr: remoteIP(remote), RemotePort: remotePort(remote), Service: "ssh", Details: map[string]string{"error": "hostkey"}, RawPayload: err.Error()})
|
|
return
|
|
}
|
|
|
|
var authAttempts int
|
|
var lastUser, lastPass string
|
|
|
|
cfg := &ssh.ServerConfig{
|
|
NoClientAuth: false,
|
|
ServerVersion: "SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.1", // Updated version string
|
|
MaxAuthTries: 6, // Allow more attempts like real SSH (default is usually 6)
|
|
PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
|
|
authAttempts++
|
|
|
|
lastUser = c.User()
|
|
lastPass = string(pass)
|
|
|
|
log(Record{Timestamp: Now(), RemoteAddr: remoteIP(remote), RemotePort: remotePort(remote), Service: "ssh", Details: map[string]string{
|
|
"event": "auth_attempt",
|
|
"attempt": strconv.Itoa(authAttempts),
|
|
"username": lastUser,
|
|
"password": lastPass,
|
|
"client": string(c.ClientVersion()),
|
|
"session_id": sessionID,
|
|
}})
|
|
|
|
// Small delay to simulate real SSH behavior
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
return nil, fmt.Errorf("permission denied")
|
|
},
|
|
KeyboardInteractiveCallback: func(c ssh.ConnMetadata, challenge ssh.KeyboardInteractiveChallenge) (*ssh.Permissions, error) {
|
|
// Log keyboard interactive attempts
|
|
log(Record{Timestamp: Now(), RemoteAddr: remoteIP(remote), RemotePort: remotePort(remote), Service: "ssh", Details: map[string]string{
|
|
"event": "keyboard_interactive_attempt",
|
|
"username": c.User(),
|
|
"client": string(c.ClientVersion()),
|
|
"session_id": sessionID,
|
|
}})
|
|
return nil, fmt.Errorf("permission denied")
|
|
},
|
|
PublicKeyCallback: func(c ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) {
|
|
// Log public key attempts
|
|
log(Record{Timestamp: Now(), RemoteAddr: remoteIP(remote), RemotePort: remotePort(remote), Service: "ssh", Details: map[string]string{
|
|
"event": "pubkey_attempt",
|
|
"username": c.User(),
|
|
"key_type": pubKey.Type(),
|
|
"key_fingerprint": ssh.FingerprintSHA256(pubKey),
|
|
"client": string(c.ClientVersion()),
|
|
"session_id": sessionID,
|
|
}})
|
|
return nil, fmt.Errorf("permission denied")
|
|
},
|
|
}
|
|
cfg.AddHostKey(signer)
|
|
|
|
// Set stricter timeout
|
|
_ = conn.SetDeadline(time.Now().Add(90 * time.Second))
|
|
|
|
sc, chans, reqs, err := ssh.NewServerConn(conn, cfg)
|
|
if err != nil {
|
|
log(Record{Timestamp: Now(), RemoteAddr: remoteIP(remote), RemotePort: remotePort(remote), Service: "ssh", Details: map[string]string{
|
|
"event": "session_end",
|
|
"session_id": sessionID,
|
|
"auth_attempts": strconv.Itoa(authAttempts),
|
|
"duration_sec": fmt.Sprintf("%.2f", time.Since(start).Seconds()),
|
|
"last_username": lastUser,
|
|
"last_password": lastPass,
|
|
"error": err.Error(),
|
|
}})
|
|
return
|
|
}
|
|
|
|
// Handle requests and channels with logging
|
|
go func() {
|
|
for req := range reqs {
|
|
log(Record{Timestamp: Now(), RemoteAddr: remoteIP(remote), RemotePort: remotePort(remote), Service: "ssh", Details: map[string]string{
|
|
"event": "global_request",
|
|
"request_type": req.Type,
|
|
"want_reply": fmt.Sprintf("%v", req.WantReply),
|
|
"session_id": sessionID,
|
|
}})
|
|
if req.WantReply {
|
|
req.Reply(false, nil)
|
|
}
|
|
}
|
|
}()
|
|
|
|
for ch := range chans {
|
|
log(Record{Timestamp: Now(), RemoteAddr: remoteIP(remote), RemotePort: remotePort(remote), Service: "ssh", Details: map[string]string{
|
|
"event": "channel_request",
|
|
"channel_type": ch.ChannelType(),
|
|
"session_id": sessionID,
|
|
}})
|
|
_ = ch.Reject(ssh.Prohibited, "not allowed")
|
|
}
|
|
_ = sc.Close()
|
|
}
|
|
}
|
|
|
|
// DefaultSigner provides a simple RSA signer if caller doesn't have one.
|
|
func DefaultSigner() (ssh.Signer, error) {
|
|
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return ssh.NewSignerFromKey(key)
|
|
}
|