packagesmtpimport("bufio""bytes""context""crypto/tls""encoding/base64""fmt""log/slog""net""regexp""strings""sync""time""honeypot/internal/database""honeypot/internal/honeypot""honeypot/internal/logger"tlscert"honeypot/internal/tls""honeypot/internal/types""honeypot/internal/utils")const(HoneypotType=types.HoneypotTypeSMTPHoneypotLabel="SMTP"DefaultConnectionTimeout=30*time.SecondMaxDataSize=500*1024// 500 KBServerBanner="220 localhost ESMTP ready\r\n")typesmtpStateintconst(stateNewsmtpState=iotastateGreetedstateMailstateRcptstateData)typeConfigstruct{ListenAddrstringPorts[]uint16SMTPSPorts[]uint16Certificate*tls.CertificateCertConfigtlscert.CertConfig}typesmtpHoneypotstruct{configConfiglogger*slog.LoggertlsConfig*tls.Config}typesmtpSessionstruct{remoteHoststringremotePortuint16dstPortuint16statesmtpStatecommands[]stringmailFromstringrcptTo[]stringdata[]bytetlsEnabledbooltlsTypestring}// New creates a new SMTP honeypot instance.funcNew(cfgConfig)honeypot.Honeypot{logger.RegisterTopNField(string(HoneypotType),"auth_mechanism")h:=&smtpHoneypot{config:cfg}ifcfg.Certificate!=nil{h.tlsConfig=&tls.Config{Certificates:[]tls.Certificate{*cfg.Certificate},MinVersion:tls.VersionTLS12,}}returnh}func(h*smtpHoneypot)Name()types.HoneypotType{returnHoneypotType}// Label returns the label of this honeypot.func(h*smtpHoneypot)Label()string{returnHoneypotLabel}func(h*smtpHoneypot)Start(ctxcontext.Context,l*slog.Logger)error{h.logger=lifh.tlsConfig==nil&&(len(h.config.Ports)>0||len(h.config.SMTPSPorts)>0){ifcert,err:=tlscert.GenerateSelfSignedCert(h.config.ListenAddr,h.config.CertConfig,h.logger);err==nil{h.tlsConfig=&tls.Config{Certificates:[]tls.Certificate{*cert},MinVersion:tls.VersionTLS12,}}}varwgsync.WaitGroupstartServer:=func(portuint16,useTLSbool){ifport==0{return}wg.Add(1)gofunc(){deferwg.Done()h.listenAndServe(ctx,port,useTLS)}()}for_,port:=rangeh.config.Ports{startServer(port,false)}for_,port:=rangeh.config.SMTPSPorts{startServer(port,true)}wg.Wait()returnnil}func(h*smtpHoneypot)listenAndServe(ctxcontext.Context,portuint16,useTLSbool){addr:=utils.BuildAddress(h.config.ListenAddr,port)varlnnet.ListenervarerrerrorifuseTLS{ifh.tlsConfig==nil{return}ln,err=tls.Listen("tcp",addr,h.tlsConfig)}else{ln,err=net.Listen("tcp",addr)}iferr!=nil{logger.LogError(h.logger,HoneypotType,"listen_failed",err,[]any{"addr",addr})return}deferln.Close()gofunc(){<-ctx.Done()ln.Close()}()logger.LogInfo(h.logger,HoneypotType,"honeypot listening",[]any{"port",port,"tls",useTLS,})for{conn,err:=ln.Accept()iferr!=nil{ifctx.Err()!=nil{return}continue}goh.handleSession(conn,useTLS)}}func(h*smtpHoneypot)handleSession(connnet.Conn,implicitTLSbool){deferconn.Close()conn.SetDeadline(time.Now().Add(DefaultConnectionTimeout))rh,rp:=utils.SplitAddr(conn.RemoteAddr().String(),h.logger)_,dp:=utils.SplitAddr(conn.LocalAddr().String(),h.logger)session:=&smtpSession{remoteHost:rh,remotePort:rp,dstPort:dp,state:stateNew,tlsEnabled:implicitTLS,}ifimplicitTLS{session.tlsType="smtps"}varreader*bufio.Readervarwriter*bufio.WriterifimplicitTLS{reader=bufio.NewReader(conn)writer=bufio.NewWriter(conn)}else{// SMTP is server-speaks-first. Banner MUST be sent before reading anything.writer=bufio.NewWriter(conn)h.writeLine(writer,ServerBanner)reader=bufio.NewReader(conn)}// For implicit TLS, the banner comes after the handshakeifimplicitTLS{h.writeLine(writer,ServerBanner)}for{// Detect TLS handshake on non-TLS portif!session.tlsEnabled{peek,_:=reader.Peek(1)iflen(peek)>0&&peek[0]==0x16{h.logTLSHandshake(session)return}}line,err:=reader.ReadString('\n')iferr!=nil{break}line=strings.TrimRight(line,"\r\n")session.commands=append(session.commands,line)parts:=strings.Fields(line)iflen(parts)==0{continue}cmd:=strings.ToUpper(parts[0])switchcmd{case"HELO","EHLO":session.state=stateGreetedifcmd=="HELO"{h.writeLine(writer,"250 localhost\r\n")}else{writer.WriteString("250-localhost\r\n")ifh.tlsConfig!=nil&&!session.tlsEnabled{writer.WriteString("250-STARTTLS\r\n")}writer.WriteString("250 AUTH LOGIN PLAIN\r\n")writer.Flush()}case"STARTTLS":ifsession.tlsEnabled||h.tlsConfig==nil{h.writeLine(writer,"454 TLS not available\r\n")continue}h.writeLine(writer,"220 Ready to start TLS\r\n")tlsConn:=tls.Server(conn,h.tlsConfig)iferr:=tlsConn.Handshake();err!=nil{break}conn=tlsConnreader=bufio.NewReader(conn)writer=bufio.NewWriter(conn)session.tlsEnabled=truesession.tlsType="starttls"session.state=stateNewcase"AUTH":h.handleAUTH(reader,writer,session,parts)case"MAIL":ifsession.state<stateGreeted{h.writeLine(writer,"503 Bad sequence of commands\r\n")continue}session.mailFrom=h.extractEmail(line)session.state=stateMailh.writeLine(writer,"250 OK\r\n")case"RCPT":ifsession.state<stateMail{h.writeLine(writer,"503 Bad sequence of commands\r\n")continue}session.rcptTo=append(session.rcptTo,h.extractEmail(line))session.state=stateRcpth.writeLine(writer,"250 OK\r\n")case"DATA":ifsession.state!=stateRcpt{h.writeLine(writer,"503 Bad sequence of commands\r\n")continue}h.handleDATA(reader,writer,session)case"RSET":session.state=stateGreetedsession.mailFrom,session.rcptTo,session.data="",nil,nilh.writeLine(writer,"250 OK\r\n")case"NOOP":h.writeLine(writer,"250 OK\r\n")case"QUIT":h.writeLine(writer,"221 Bye\r\n")h.logSession(session)returndefault:h.writeLine(writer,"500 Command unrecognized\r\n")}}h.logSession(session)}func(h*smtpHoneypot)writeLine(w*bufio.Writer,sstring){w.WriteString(s)w.Flush()}func(h*smtpHoneypot)handleAUTH(r*bufio.Reader,w*bufio.Writer,s*smtpSession,parts[]string){iflen(parts)<2{h.writeLine(w,"501 Syntax error\r\n")return}mech:=strings.ToUpper(parts[1])varuser,passstringswitchmech{case"PLAIN":varencodedstringiflen(parts)>=3{encoded=parts[2]}else{h.writeLine(w,"334 \r\n")line,_:=r.ReadString('\n')encoded=strings.TrimSpace(line)}user,pass=h.decodeAuthPlain(encoded)case"LOGIN":h.writeLine(w,"334 VXNlcm5hbWU6\r\n")// Username:ifline,err:=r.ReadString('\n');err==nil{s.commands=append(s.commands,strings.TrimRight(line,"\r\n"))uDec,_:=base64.StdEncoding.DecodeString(strings.TrimSpace(line))user=string(uDec)}h.writeLine(w,"334 UGFzc3dvcmQ6\r\n")// Password:ifline,err:=r.ReadString('\n');err==nil{s.commands=append(s.commands,strings.TrimRight(line,"\r\n"))pDec,_:=base64.StdEncoding.DecodeString(strings.TrimSpace(line))pass=string(pDec)}default:h.writeLine(w,"504 Unrecognized authentication type\r\n")return}h.writeLine(w,"535 Authentication failed\r\n")h.logAuthAttempt(s,mech,user,pass)}func(h*smtpHoneypot)decodeAuthPlain(encodedstring)(string,string){decoded,err:=base64.StdEncoding.DecodeString(encoded)iferr!=nil{return"",""}parts:=bytes.Split(decoded,[]byte{0})iflen(parts)!=3{return"",""}returnstring(parts[1]),string(parts[2])}func(h*smtpHoneypot)handleDATA(r*bufio.Reader,w*bufio.Writer,s*smtpSession){h.writeLine(w,"354 End data with <CRLF>.<CRLF>\r\n")varbufbytes.Bufferfor{l,err:=r.ReadBytes('\n')iferr!=nil{break}ifstring(bytes.TrimSpace(l))=="."{break}ifbuf.Len()+len(l)<=MaxDataSize{buf.Write(l)}}s.data=buf.Bytes()s.state=stateDatah.writeLine(w,"250 OK\r\n")}func(h*smtpHoneypot)logAuthAttempt(s*smtpSession,mech,user,passstring){fields:=map[string]interface{}{"auth_mechanism":mech,"username":user,"password":pass,}ifs.tlsEnabled{fields["tls"]=s.tlsType}logger.LogEvent(h.logger,types.LogEvent{Type:HoneypotType,Event:types.EventAuthAttempt,RemoteAddr:s.remoteHost,RemotePort:s.remotePort,DstPort:s.dstPort,Fields:fields,})}func(h*smtpHoneypot)logSession(s*smtpSession){iflen(s.commands)==0{return}fields:=map[string]interface{}{"commands":s.commands,}ifs.tlsEnabled{fields["tls"]=s.tlsType}ifs.mailFrom!=""{fields["from"]=s.mailFrom}iflen(s.rcptTo)>0{fields["to"]=s.rcptTo}iflen(s.data)>0{fields["data_size"]=len(s.data)fields["data"]=string(s.data)}logger.LogEvent(h.logger,types.LogEvent{Type:HoneypotType,Event:types.EventRequest,RemoteAddr:s.remoteHost,RemotePort:s.remotePort,DstPort:s.dstPort,Fields:fields,})}func(h*smtpHoneypot)logTLSHandshake(s*smtpSession){logger.LogEvent(h.logger,types.LogEvent{Type:HoneypotType,Event:types.EventTLSHandshake,RemoteAddr:s.remoteHost,RemotePort:s.remotePort,DstPort:s.dstPort,Fields:map[string]interface{}{"message":"TLS handshake attempt on non-TLS port"},})}varemailRegex=regexp.MustCompile(`<([^>]+)>`)func(h*smtpHoneypot)extractEmail(linestring)string{ifm:=emailRegex.FindStringSubmatch(line);len(m)>1{returnm[1]}return""}func(h*smtpHoneypot)GetScores(db*database.Database,intervalstring)honeypot.ScoreMap{// get scores for addresses with a set to addressrows,err:=db.DB.Query(fmt.Sprintf(` SELECT remote_addr, COUNT(*) as auth_count
FROM honeypot_events
WHERE type = 'smtp'
AND fields.to IS NOT NULL
AND time >= now() - INTERVAL %s
GROUP BY remote_addr
ORDER BY auth_count DESC
`,interval))iferr!=nil{returnhoneypot.ScoreMap{}}deferrows.Close()scores:=honeypot.ScoreMap{}forrows.Next(){varipstringvarauthCountuinterr:=rows.Scan(&ip,&authCount)iferr!=nil{returnhoneypot.ScoreMap{}}scores[ip]=honeypot.Score{Score:100*authCount,Tags:[]types.Tag{types.TagAuthAttempt}}}returnscores}func(h*smtpHoneypot)Ports()[]uint16{ports:=append([]uint16{},h.config.Ports...)ports=append(ports,h.config.SMTPSPorts...)returnports}