packageftpimport("bufio""context""crypto/tls""fmt""log/slog""net""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.HoneypotTypeFTPHoneypotLabel="FTP"DefaultConnectionTimeout=30*time.SecondServerBanner="220 FTP server ready\r\n")// Config holds the configuration for the FTP honeypot.typeConfigstruct{ListenAddrstringPorts[]uint16FTPSPorts[]uint16Certificate*tls.CertificateCertConfigtlscert.CertConfig}// ftpHoneypot implements the honeypot.Honeypot interface.typeftpHoneypotstruct{configConfiglogger*slog.LoggertlsConfig*tls.Config}typeftpSessionstruct{remoteHoststringremotePortuint16dstPortuint16userstringtlsEnabledbooltlsTypestring}// New creates a new FTP honeypot instance.funcNew(cfgConfig)honeypot.Honeypot{h:=&ftpHoneypot{config:cfg}ifcfg.Certificate!=nil{h.tlsConfig=&tls.Config{Certificates:[]tls.Certificate{*cfg.Certificate},MinVersion:tls.VersionTLS12,}}returnh}// Name returns the name of this honeypot.func(h*ftpHoneypot)Name()types.HoneypotType{returnHoneypotType}// Label returns the label of this honeypot.func(h*ftpHoneypot)Label()string{returnHoneypotLabel}// Start starts the FTP honeypot server.func(h*ftpHoneypot)Start(ctxcontext.Context,l*slog.Logger)error{h.logger=lifh.tlsConfig==nil&&len(h.config.FTPSPorts)>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.FTPSPorts{startServer(port,true)}wg.Wait()returnnil}func(h*ftpHoneypot)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*ftpHoneypot)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:=&ftpSession{remoteHost:rh,remotePort:rp,dstPort:dp,tlsEnabled:implicitTLS,}ifimplicitTLS{session.tlsType="ftps"}varreader*bufio.Readervarwriter*bufio.WriterifimplicitTLS{reader=bufio.NewReader(conn)writer=bufio.NewWriter(conn)}else{// For non-implicit TLS, we send the banner first before anything else// because FTP is a server-speaks-first protocol.writer=bufio.NewWriter(conn)h.writeLine(writer,ServerBanner)reader=bufio.NewReader(conn)}// If implicit TLS, we need to send the banner AFTER the handshake (which is already done by tls.Listen)ifimplicitTLS{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")parts:=strings.Fields(line)iflen(parts)==0{continue}cmd:=strings.ToUpper(parts[0])arg:=""iflen(parts)>1{arg=parts[1]}switchcmd{case"AUTH":ifstrings.ToUpper(arg)=="TLS"||strings.ToUpper(arg)=="SSL"{ifsession.tlsEnabled||h.tlsConfig==nil{h.writeLine(writer,"502 Command not implemented\r\n")continue}h.writeLine(writer,"234 Proceed with negotiation.\r\n")tlsConn:=tls.Server(conn,h.tlsConfig)iferr:=tlsConn.Handshake();err!=nil{return}conn=tlsConnreader=bufio.NewReader(conn)writer=bufio.NewWriter(conn)session.tlsEnabled=truesession.tlsType="auth-tls"}else{h.writeLine(writer,"504 Security mechanism not supported\r\n")}case"PBSZ":h.writeLine(writer,"200 PBSZ=0\r\n")case"PROT":ifstrings.ToUpper(arg)=="P"{h.writeLine(writer,"200 Private\r\n")}else{h.writeLine(writer,"200 Clear\r\n")}case"USER":session.user=argh.writeLine(writer,fmt.Sprintf("331 Password required for %s\r\n",arg))case"PASS":h.logAuthAttempt(session,session.user,arg)h.writeLine(writer,"530 Login incorrect\r\n")case"SYST":h.writeLine(writer,"215 UNIX Type: L8\r\n")case"PWD":h.writeLine(writer,"257 \"/\" is current directory\r\n")case"TYPE":h.writeLine(writer,"200 Type set to I\r\n")case"PASV":h.writeLine(writer,"227 Entering Passive Mode (127,0,0,1,0,21)\r\n")case"LIST":h.writeLine(writer,"150 Opening ASCII mode data connection for file list\r\n")h.writeLine(writer,"226 Transfer complete\r\n")case"QUIT":h.writeLine(writer,"221 Goodbye\r\n")returncase"FEAT":feat:="211-Features:\r\n MDTM\r\n REST STREAM\r\n SIZE\r\n"ifh.tlsConfig!=nil&&!session.tlsEnabled{feat+=" AUTH TLS\r\n AUTH SSL\r\n"}feat+="211 End\r\n"h.writeLine(writer,feat)case"OPTS":h.writeLine(writer,"200 OPTS command successful\r\n")default:h.writeLine(writer,"500 Unknown command\r\n")}}}func(h*ftpHoneypot)writeLine(w*bufio.Writer,sstring){w.WriteString(s)w.Flush()}func(h*ftpHoneypot)logAuthAttempt(s*ftpSession,user,passstring){fields:=map[string]interface{}{"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*ftpHoneypot)logTLSHandshake(s*ftpSession){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"},})}func(h*ftpHoneypot)GetScores(db*database.Database,intervalstring)honeypot.ScoreMap{// auth_attempt scores are handled in the honeypot.authAttemptScores functionreturnhoneypot.ScoreMap{}}func(h*ftpHoneypot)Ports()[]uint16{ports:=append([]uint16{},h.config.Ports...)ports=append(ports,h.config.FTPSPorts...)returnports}