internal/honeypot/telnet/telnet.go

package telnet

import (
	"bufio"
	"context"
	"fmt"
	"log/slog"
	"net"
	"strings"
	"sync"
	"time"

	"honeypot/internal/database"
	"honeypot/internal/honeypot"
	"honeypot/internal/logger"
	"honeypot/internal/types"
	"honeypot/internal/utils"
)

const (
	HoneypotType             = types.HoneypotTypeTelnet
	HoneypotLabel            = "Telnet"
	DefaultConnectionTimeout = 3 * time.Minute
	ShutdownTimeout          = 3 * time.Second // Maximum time to wait for server shutdown

	// Telnet protocol constants
	IAC  = 255 // Interpret As Command
	DONT = 254
	DO   = 253
	WONT = 252
	WILL = 251
	SB   = 250 // Subnegotiation Begin
	SE   = 240 // Subnegotiation End
	ECHO = 1   // Echo option

	// NEW-ENVIRON option codes (RFC 1572)
	NEW_ENVIRON = 39
	IS          = 0
	SEND        = 1
	INFO        = 2
	VAR         = 0
	VALUE       = 1
	ESC         = 2
	USERVAR     = 3
)

// Config holds the configuration for the Telnet honeypot.
type Config struct {
	ListenAddr string
	Ports      []uint16
}

// telnetHoneypot implements the honeypot.Honeypot interface.
type telnetHoneypot struct {
	config Config
	logger *slog.Logger
}

// New creates a new Telnet honeypot instance.
func New(cfg Config) honeypot.Honeypot {
	return &telnetHoneypot{
		config: cfg,
	}
}

// Name returns the name of this honeypot.
func (h *telnetHoneypot) Name() types.HoneypotType {
	return HoneypotType
}

// Label returns the label of this honeypot.
func (h *telnetHoneypot) Label() string {
	return HoneypotLabel
}

// Start starts the Telnet honeypot server.
func (h *telnetHoneypot) Start(ctx context.Context, l *slog.Logger) error {
	h.logger = l

	var wg sync.WaitGroup

	for _, port := range h.config.Ports {
		if port == 0 {
			continue
		}

		wg.Add(1)
		go func(p uint16) {
			defer wg.Done()
			h.startTelnetServer(ctx, p)
		}(port)
	}

	wg.Wait()
	logger.LogInfo(h.logger, HoneypotType, "honeypot shutdown complete", nil)
	return nil
}

// startTelnetServer starts a Telnet server on the specified port.
func (h *telnetHoneypot) startTelnetServer(ctx context.Context, port uint16) {
	listenAddr := utils.BuildAddress(h.config.ListenAddr, port)
	listener, err := net.Listen("tcp", listenAddr)
	if err != nil {
		logger.LogError(h.logger, HoneypotType, "listen_failed", err, []any{
			"addr", listenAddr,
		})
		return
	}
	defer listener.Close()

	logger.LogInfo(h.logger, HoneypotType, "honeypot listening", []any{
		"port", port,
	})

	h.setupGracefulShutdown(ctx, listener)
	h.acceptConnections(ctx, listener)

	logger.LogInfo(h.logger, HoneypotType, "honeypot shutdown complete", []any{
		"port", port,
	})
}

// setupGracefulShutdown handles graceful shutdown on context cancellation.
func (h *telnetHoneypot) setupGracefulShutdown(ctx context.Context, listener net.Listener) {
	go func() {
		<-ctx.Done()
		if err := listener.Close(); err != nil {
			logger.LogError(h.logger, HoneypotType, "listener_close_error", err, nil)
		}
	}()
}

// acceptConnections accepts incoming connections and handles them.
func (h *telnetHoneypot) acceptConnections(ctx context.Context, listener net.Listener) {
	for {
		conn, err := listener.Accept()
		if err != nil {
			if ctx.Err() != nil {
				break
			}
			logger.LogError(h.logger, HoneypotType, "accept_failed", err, nil)
			continue
		}

		go h.handleConn(ctx, conn)
	}
}

// handleConn handles a new Telnet connection.
func (h *telnetHoneypot) handleConn(ctx context.Context, conn net.Conn) {
	defer conn.Close()

	conn.SetDeadline(time.Now().Add(DefaultConnectionTimeout))

	remoteHost, remotePort := utils.SplitAddr(conn.RemoteAddr().String(), h.logger)
	_, dstPort := utils.SplitAddr(conn.LocalAddr().String(), h.logger)

	reader := bufio.NewReader(conn)
	writer := bufio.NewWriter(conn)

	// Detect TLS handshake on non-TLS port
	peek, _ := reader.Peek(1)
	if len(peek) > 0 && peek[0] == 0x16 {
		h.logTLSHandshake(remoteHost, remotePort, dstPort)
		return
	}

	// Filter telnet negotiation commands before reading input
	filteredReader := h.newTelnetReader(reader, remoteHost, remotePort, dstPort)

	// Initiate handshake: send DO NEW_ENVIRON to ask client for env vars
	h.sendNegotiation(writer, DO, NEW_ENVIRON)
	// Also send SB NEW_ENVIRON SEND IAC SE to actually request them
	writer.Write([]byte{IAC, SB, NEW_ENVIRON, SEND, IAC, SE})
	writer.Flush()

	var logged bool
	defer func() {
		// Log the environment even if no credentials were received, as long as we have some data
		if !logged && len(filteredReader.envVars) > 0 {
			h.logAuthAttempt(remoteHost, remotePort, dstPort, "", "", filteredReader.envVars)
		}
	}()

	// Prompt for username
	username, err := h.promptAndRead(writer, filteredReader, "login: ", false)
	if err != nil {
		return
	}

	// Prompt for password (with echo disabled)
	password, err := h.promptAndRead(writer, filteredReader, "Password:", true)
	if err != nil {
		return
	}

	// Log the credentials and environment variables
	logged = true
	h.logAuthAttempt(remoteHost, remotePort, dstPort, username, password, filteredReader.envVars)

	// Send error message and close
	fmt.Fprintf(writer, "\r\nAccess denied.\r\n")
	writer.Flush()
}

// telnetReader wraps a bufio.Reader to filter out telnet protocol commands
type telnetReader struct {
	h          *telnetHoneypot
	reader     *bufio.Reader
	buf        []byte
	remoteHost string
	remotePort uint16
	dstPort    uint16
	envVars    map[string]string
}

// newTelnetReader creates a new telnet reader that filters IAC commands
func (h *telnetHoneypot) newTelnetReader(reader *bufio.Reader, remoteHost string, remotePort, dstPort uint16) *telnetReader {
	return &telnetReader{
		h:          h,
		reader:     reader,
		buf:        make([]byte, 0, 256),
		remoteHost: remoteHost,
		remotePort: remotePort,
		dstPort:    dstPort,
		envVars:    make(map[string]string),
	}
}

// ReadByte reads a byte while filtering telnet protocol commands
func (tr *telnetReader) ReadByte() (byte, error) {
	for {
		b, err := tr.reader.ReadByte()
		if err != nil {
			return 0, err
		}

		// Handle IAC (Interpret As Command)
		if b == IAC {
			err := tr.handleIAC()
			if err != nil {
				return 0, err
			}
			continue // Skip the IAC and continue reading
		}

		return b, nil
	}
}

// handleIAC processes telnet IAC commands
func (tr *telnetReader) handleIAC() error {
	cmd, err := tr.reader.ReadByte()
	if err != nil {
		return err
	}

	switch cmd {
	case WILL, WONT, DO, DONT:
		// These commands have an option byte following
		_, err := tr.reader.ReadByte()
		if err != nil {
			return err
		}
	case SB:
		// Subnegotiation: read until SE
		var sbData []byte
		for {
			b, err := tr.reader.ReadByte()
			if err != nil {
				return err
			}
			if b == IAC {
				next, err := tr.reader.ReadByte()
				if err != nil {
					return err
				}
				if next == SE {
					break
				}
				// If it's not SE, it might be an escaped IAC (IAC IAC)
				if next == IAC {
					sbData = append(sbData, IAC)
					continue
				}
				// Otherwise it's some other IAC command inside SB, which is weird but let's just consume
				continue
			}
			sbData = append(sbData, b)
		}

		if len(sbData) > 0 && sbData[0] == NEW_ENVIRON {
			tr.handleNewEnviron(sbData[1:])
		}
		// For other commands, just consume them (they don't have parameters)
	}

	return nil
}

// handleNewEnviron parses NEW-ENVIRON subnegotiation data for CVE-2026-24061 detection.
func (tr *telnetReader) handleNewEnviron(data []byte) {
	if len(data) == 0 {
		return
	}

	// We are looking for the client sending environment variables (IS or INFO)
	if data[0] != IS && data[0] != INFO {
		return
	}

	// RFC 1572: IS [VAR type value [VAR type value] ...]
	// We want to find the "USER" variable and check its value.
	i := 1
	for i < len(data) {
		if data[i] != VAR && data[i] != USERVAR {
			i++
			continue
		}
		i++ // skip VAR/USERVAR

		// Read variable name
		var varName []byte
		for i < len(data) && data[i] != VALUE && data[i] != VAR && data[i] != USERVAR {
			varName = append(varName, data[i])
			i++
		}

		varNameStr := string(varName)

		if i < len(data) && data[i] == VALUE {
			i++ // skip VALUE
			// Read value
			var value []byte
			for i < len(data) && data[i] != VAR && data[i] != USERVAR {
				value = append(value, data[i])
				i++
			}

			valStr := string(value)
			tr.envVars[varNameStr] = valStr

		} else {
			// Variable with no value
			tr.envVars[varNameStr] = ""
		}
	}
}

// sendNegotiation sends a standard 3-byte Telnet negotiation command.
func (h *telnetHoneypot) sendNegotiation(writer *bufio.Writer, cmd, option byte) error {
	_, err := writer.Write([]byte{IAC, cmd, option})
	if err != nil {
		return err
	}
	return writer.Flush()
}

// ReadString reads until the delimiter, filtering telnet commands
func (tr *telnetReader) ReadString(delim byte) (string, error) {
	tr.buf = tr.buf[:0]
	for {
		b, err := tr.ReadByte()
		if err != nil {
			return "", err
		}

		if b == delim {
			return string(tr.buf), nil
		}

		tr.buf = append(tr.buf, b)
	}
}

// promptAndRead sends a prompt and reads a line from the connection.
// If noEcho is true, echo is disabled for password input.
func (h *telnetHoneypot) promptAndRead(writer *bufio.Writer, reader *telnetReader, prompt string, noEcho bool) (string, error) {
	// Disable echo if requested (tell client we will echo, so it shouldn't echo locally)
	if noEcho {
		if err := h.sendEchoCommand(writer, WILL, ECHO); err != nil {
			return "", err
		}
	}

	// Send prompt
	if _, err := writer.WriteString(prompt); err != nil {
		// Try to re-enable echo even on error
		if noEcho {
			h.sendEchoCommand(writer, WONT, ECHO)
		}
		return "", err
	}
	if err := writer.Flush(); err != nil {
		if noEcho {
			h.sendEchoCommand(writer, WONT, ECHO)
		}
		return "", err
	}

	var line string
	var err error

	if noEcho {
		// Read character by character to handle \r\n properly when echo is disabled
		var input []byte
		for {
			b, readErr := reader.ReadByte()
			if readErr != nil {
				// Re-enable echo before returning error
				h.sendEchoCommand(writer, WONT, ECHO)
				return "", readErr
			}

			// Handle both \r and \n as line terminators
			if b == '\r' {
				// Try to read \n if it follows
				nextByte, nextErr := reader.ReadByte()
				if nextErr == nil && nextByte == '\n' {
					// Got \r\n, done
					break
				}
				// Got \r without \n, still done
				break
			}
			if b == '\n' {
				// Got \n, done
				break
			}

			input = append(input, b)
		}

		// Re-enable echo (tell client we won't echo, so it should echo locally)
		h.sendEchoCommand(writer, WONT, ECHO)

		// Convert to string and clean up
		line = strings.TrimSpace(string(input))
	} else {
		// Read line (telnet clients send \r\n, we'll handle both)
		line, err = reader.ReadString('\n')
		if err != nil {
			return "", err
		}

		// Clean up the line: remove \r, \n, and trim spaces
		line = strings.TrimRight(line, "\r\n")
		line = strings.TrimSpace(line)
	}

	return line, nil
}

// sendEchoCommand sends a telnet echo control command.
func (h *telnetHoneypot) sendEchoCommand(writer *bufio.Writer, cmd byte, option byte) error {
	_, err := writer.Write([]byte{IAC, cmd, option})
	if err != nil {
		return err
	}
	return writer.Flush()
}

// logAuthAttempt logs an authentication attempt with the provided credentials.
func (h *telnetHoneypot) logAuthAttempt(remoteHost string, remotePort uint16, dstPort uint16, username, password string, envVars map[string]string) {

	// Check for HTTP requests
	if strings.HasPrefix(username, "GET") || strings.HasPrefix(password, "Host") {
		return
	}

	// Check for HTTP requests
	if strings.HasPrefix(username, "GET") || strings.HasPrefix(password, "Host") {
		return
	}

	fields := map[string]interface{}{
		"username": username,
		"password": password,
	}

	if len(envVars) > 0 {
		fields["env_vars"] = envVars
	}

	logger.LogEvent(h.logger, types.LogEvent{
		Type:       HoneypotType,
		Event:      types.EventAuthAttempt,
		RemoteAddr: remoteHost,
		RemotePort: remotePort,
		DstPort:    dstPort,
		Fields:     fields,
	})
}

func (h *telnetHoneypot) logTLSHandshake(remoteHost string, remotePort, dstPort uint16) {
	logger.LogEvent(h.logger, types.LogEvent{
		Type:       HoneypotType,
		Event:      types.EventTLSHandshake,
		RemoteAddr: remoteHost,
		RemotePort: remotePort,
		DstPort:    dstPort,
		Fields:     map[string]interface{}{"message": "TLS handshake attempt on non-TLS port"},
	})
}

func (h *telnetHoneypot) GetScores(db *database.Database, interval string) honeypot.ScoreMap {
	// auth_attempt scores are handled in the honeypot.authAttemptScores function
	return honeypot.ScoreMap{}
}

func (h *telnetHoneypot) Ports() []uint16 {
	return h.config.Ports
}