packagetelnetimport("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.HoneypotTypeTelnetHoneypotLabel="Telnet"DefaultConnectionTimeout=3*time.MinuteShutdownTimeout=3*time.Second// Maximum time to wait for server shutdown// Telnet protocol constantsIAC=255// Interpret As CommandDONT=254DO=253WONT=252WILL=251SB=250// Subnegotiation BeginSE=240// Subnegotiation EndECHO=1// Echo option// NEW-ENVIRON option codes (RFC 1572)NEW_ENVIRON=39IS=0SEND=1INFO=2VAR=0VALUE=1ESC=2USERVAR=3)// Config holds the configuration for the Telnet honeypot.typeConfigstruct{ListenAddrstringPorts[]uint16}// telnetHoneypot implements the honeypot.Honeypot interface.typetelnetHoneypotstruct{configConfiglogger*slog.Logger}// New creates a new Telnet honeypot instance.funcNew(cfgConfig)honeypot.Honeypot{return&telnetHoneypot{config:cfg,}}// Name returns the name of this honeypot.func(h*telnetHoneypot)Name()types.HoneypotType{returnHoneypotType}// Label returns the label of this honeypot.func(h*telnetHoneypot)Label()string{returnHoneypotLabel}// Start starts the Telnet honeypot server.func(h*telnetHoneypot)Start(ctxcontext.Context,l*slog.Logger)error{h.logger=lvarwgsync.WaitGroupfor_,port:=rangeh.config.Ports{ifport==0{continue}wg.Add(1)gofunc(puint16){deferwg.Done()h.startTelnetServer(ctx,p)}(port)}wg.Wait()logger.LogInfo(h.logger,HoneypotType,"honeypot shutdown complete",nil)returnnil}// startTelnetServer starts a Telnet server on the specified port.func(h*telnetHoneypot)startTelnetServer(ctxcontext.Context,portuint16){listenAddr:=utils.BuildAddress(h.config.ListenAddr,port)listener,err:=net.Listen("tcp",listenAddr)iferr!=nil{logger.LogError(h.logger,HoneypotType,"listen_failed",err,[]any{"addr",listenAddr,})return}deferlistener.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(ctxcontext.Context,listenernet.Listener){gofunc(){<-ctx.Done()iferr:=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(ctxcontext.Context,listenernet.Listener){for{conn,err:=listener.Accept()iferr!=nil{ifctx.Err()!=nil{break}logger.LogError(h.logger,HoneypotType,"accept_failed",err,nil)continue}goh.handleConn(ctx,conn)}}// handleConn handles a new Telnet connection.func(h*telnetHoneypot)handleConn(ctxcontext.Context,connnet.Conn){deferconn.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 portpeek,_:=reader.Peek(1)iflen(peek)>0&&peek[0]==0x16{h.logTLSHandshake(remoteHost,remotePort,dstPort)return}// Filter telnet negotiation commands before reading inputfilteredReader:=h.newTelnetReader(reader,remoteHost,remotePort,dstPort)// Initiate handshake: send DO NEW_ENVIRON to ask client for env varsh.sendNegotiation(writer,DO,NEW_ENVIRON)// Also send SB NEW_ENVIRON SEND IAC SE to actually request themwriter.Write([]byte{IAC,SB,NEW_ENVIRON,SEND,IAC,SE})writer.Flush()varloggedbooldeferfunc(){// Log the environment even if no credentials were received, as long as we have some dataif!logged&&len(filteredReader.envVars)>0{h.logAuthAttempt(remoteHost,remotePort,dstPort,"","",filteredReader.envVars)}}()// Prompt for usernameusername,err:=h.promptAndRead(writer,filteredReader,"login: ",false)iferr!=nil{return}// Prompt for password (with echo disabled)password,err:=h.promptAndRead(writer,filteredReader,"Password:",true)iferr!=nil{return}// Log the credentials and environment variableslogged=trueh.logAuthAttempt(remoteHost,remotePort,dstPort,username,password,filteredReader.envVars)// Send error message and closefmt.Fprintf(writer,"\r\nAccess denied.\r\n")writer.Flush()}// telnetReader wraps a bufio.Reader to filter out telnet protocol commandstypetelnetReaderstruct{h*telnetHoneypotreader*bufio.Readerbuf[]byteremoteHoststringremotePortuint16dstPortuint16envVarsmap[string]string}// newTelnetReader creates a new telnet reader that filters IAC commandsfunc(h*telnetHoneypot)newTelnetReader(reader*bufio.Reader,remoteHoststring,remotePort,dstPortuint16)*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 commandsfunc(tr*telnetReader)ReadByte()(byte,error){for{b,err:=tr.reader.ReadByte()iferr!=nil{return0,err}// Handle IAC (Interpret As Command)ifb==IAC{err:=tr.handleIAC()iferr!=nil{return0,err}continue// Skip the IAC and continue reading}returnb,nil}}// handleIAC processes telnet IAC commandsfunc(tr*telnetReader)handleIAC()error{cmd,err:=tr.reader.ReadByte()iferr!=nil{returnerr}switchcmd{caseWILL,WONT,DO,DONT:// These commands have an option byte following_,err:=tr.reader.ReadByte()iferr!=nil{returnerr}caseSB:// Subnegotiation: read until SEvarsbData[]bytefor{b,err:=tr.reader.ReadByte()iferr!=nil{returnerr}ifb==IAC{next,err:=tr.reader.ReadByte()iferr!=nil{returnerr}ifnext==SE{break}// If it's not SE, it might be an escaped IAC (IAC IAC)ifnext==IAC{sbData=append(sbData,IAC)continue}// Otherwise it's some other IAC command inside SB, which is weird but let's just consumecontinue}sbData=append(sbData,b)}iflen(sbData)>0&&sbData[0]==NEW_ENVIRON{tr.handleNewEnviron(sbData[1:])}// For other commands, just consume them (they don't have parameters)}returnnil}// handleNewEnviron parses NEW-ENVIRON subnegotiation data for CVE-2026-24061 detection.func(tr*telnetReader)handleNewEnviron(data[]byte){iflen(data)==0{return}// We are looking for the client sending environment variables (IS or INFO)ifdata[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:=1fori<len(data){ifdata[i]!=VAR&&data[i]!=USERVAR{i++continue}i++// skip VAR/USERVAR// Read variable namevarvarName[]bytefori<len(data)&&data[i]!=VALUE&&data[i]!=VAR&&data[i]!=USERVAR{varName=append(varName,data[i])i++}varNameStr:=string(varName)ifi<len(data)&&data[i]==VALUE{i++// skip VALUE// Read valuevarvalue[]bytefori<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 valuetr.envVars[varNameStr]=""}}}// sendNegotiation sends a standard 3-byte Telnet negotiation command.func(h*telnetHoneypot)sendNegotiation(writer*bufio.Writer,cmd,optionbyte)error{_,err:=writer.Write([]byte{IAC,cmd,option})iferr!=nil{returnerr}returnwriter.Flush()}// ReadString reads until the delimiter, filtering telnet commandsfunc(tr*telnetReader)ReadString(delimbyte)(string,error){tr.buf=tr.buf[:0]for{b,err:=tr.ReadByte()iferr!=nil{return"",err}ifb==delim{returnstring(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,promptstring,noEchobool)(string,error){// Disable echo if requested (tell client we will echo, so it shouldn't echo locally)ifnoEcho{iferr:=h.sendEchoCommand(writer,WILL,ECHO);err!=nil{return"",err}}// Send promptif_,err:=writer.WriteString(prompt);err!=nil{// Try to re-enable echo even on errorifnoEcho{h.sendEchoCommand(writer,WONT,ECHO)}return"",err}iferr:=writer.Flush();err!=nil{ifnoEcho{h.sendEchoCommand(writer,WONT,ECHO)}return"",err}varlinestringvarerrerrorifnoEcho{// Read character by character to handle \r\n properly when echo is disabledvarinput[]bytefor{b,readErr:=reader.ReadByte()ifreadErr!=nil{// Re-enable echo before returning errorh.sendEchoCommand(writer,WONT,ECHO)return"",readErr}// Handle both \r and \n as line terminatorsifb=='\r'{// Try to read \n if it followsnextByte,nextErr:=reader.ReadByte()ifnextErr==nil&&nextByte=='\n'{// Got \r\n, donebreak}// Got \r without \n, still donebreak}ifb=='\n'{// Got \n, donebreak}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 upline=strings.TrimSpace(string(input))}else{// Read line (telnet clients send \r\n, we'll handle both)line,err=reader.ReadString('\n')iferr!=nil{return"",err}// Clean up the line: remove \r, \n, and trim spacesline=strings.TrimRight(line,"\r\n")line=strings.TrimSpace(line)}returnline,nil}// sendEchoCommand sends a telnet echo control command.func(h*telnetHoneypot)sendEchoCommand(writer*bufio.Writer,cmdbyte,optionbyte)error{_,err:=writer.Write([]byte{IAC,cmd,option})iferr!=nil{returnerr}returnwriter.Flush()}// logAuthAttempt logs an authentication attempt with the provided credentials.func(h*telnetHoneypot)logAuthAttempt(remoteHoststring,remotePortuint16,dstPortuint16,username,passwordstring,envVarsmap[string]string){// Check for HTTP requestsifstrings.HasPrefix(username,"GET")||strings.HasPrefix(password,"Host"){return}// Check for HTTP requestsifstrings.HasPrefix(username,"GET")||strings.HasPrefix(password,"Host"){return}fields:=map[string]interface{}{"username":username,"password":password,}iflen(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(remoteHoststring,remotePort,dstPortuint16){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,intervalstring)honeypot.ScoreMap{// auth_attempt scores are handled in the honeypot.authAttemptScores functionreturnhoneypot.ScoreMap{}}func(h*telnetHoneypot)Ports()[]uint16{returnh.config.Ports}