diff --git a/app/config.go b/app/config.go index 41660c9..c6951e0 100644 --- a/app/config.go +++ b/app/config.go @@ -24,20 +24,15 @@ type Config struct { SSH bool `json:"ssh"` FTP bool `json:"ftp"` SMTP bool `json:"smtp"` - POP3 bool `json:"pop3"` IMAP bool `json:"imap"` Telnet bool `json:"telnet"` MySQL bool `json:"mysql"` PostgreSQL bool `json:"postgresql"` - Redis bool `json:"redis"` MongoDB bool `json:"mongodb"` RDP bool `json:"rdp"` SMB bool `json:"smb"` SIP bool `json:"sip"` VNC bool `json:"vnc"` - DNS bool `json:"dns"` - SNMP bool `json:"snmp"` - LDAP bool `json:"ldap"` Generic []int `json:"generic"` } `json:"services"` @@ -47,20 +42,15 @@ type Config struct { SSH int `json:"ssh"` FTP int `json:"ftp"` SMTP int `json:"smtp"` - POP3 int `json:"pop3"` IMAP int `json:"imap"` Telnet int `json:"telnet"` MySQL int `json:"mysql"` PostgreSQL int `json:"postgresql"` - Redis int `json:"redis"` MongoDB int `json:"mongodb"` RDP int `json:"rdp"` SMB int `json:"smb"` SIP int `json:"sip"` VNC int `json:"vnc"` - DNS int `json:"dns"` - SNMP int `json:"snmp"` - LDAP int `json:"ldap"` } `json:"ports"` // Certificates allows overriding default certificate/key locations. @@ -70,8 +60,8 @@ type Config struct { SSHHostKeyPath string `json:"ssh_host_key_path"` // TLSCertPath and TLSKeyPath are used by TLS-capable services if provided. // If empty, a self-signed certificate will be generated and stored next to LogPath. - TLSCertPath string `json:"tls_cert_path"` - TLSKeyPath string `json:"tls_key_path"` + TLSCertPath string `json:"tls_cert_path"` + TLSKeyPath string `json:"tls_key_path"` } `json:"certificates"` } @@ -120,17 +110,12 @@ func defaultConfig() Config { c.Services.Telnet = true c.Services.MySQL = false c.Services.PostgreSQL = false - c.Services.Redis = false c.Services.MongoDB = false - c.Services.POP3 = false c.Services.IMAP = false c.Services.RDP = false c.Services.SMB = false c.Services.SIP = false c.Services.VNC = false - c.Services.DNS = false - c.Services.SNMP = false - c.Services.LDAP = false c.Services.Generic = []int{} // Standard ports @@ -139,20 +124,15 @@ func defaultConfig() Config { c.Ports.SSH = 2222 c.Ports.FTP = 2121 c.Ports.SMTP = 2525 - c.Ports.POP3 = 1110 c.Ports.IMAP = 1143 c.Ports.Telnet = 2323 c.Ports.MySQL = 3306 c.Ports.PostgreSQL = 5432 - c.Ports.Redis = 6379 c.Ports.MongoDB = 27017 c.Ports.RDP = 3389 c.Ports.SMB = 4450 c.Ports.SIP = 5060 c.Ports.VNC = 5900 - c.Ports.DNS = 5353 - c.Ports.SNMP = 1161 - c.Ports.LDAP = 3890 return c } diff --git a/app/services.go b/app/services.go index fc990c3..736ddc4 100644 --- a/app/services.go +++ b/app/services.go @@ -2,13 +2,15 @@ package app import ( "bufio" + "context" "crypto/rand" "crypto/rsa" "crypto/tls" "crypto/x509" "crypto/x509/pkix" - "context" + "encoding/pem" "fmt" + svcs "honeydany/app/services" "io" "log" "math/big" @@ -20,86 +22,89 @@ import ( "strings" "sync" "time" - "encoding/pem" + "golang.org/x/crypto/ssh" - svcs "honeydany/app/services" ) // App holds runtime pieces type App struct { - cfg Config - logger *Logger - threatIntel *ThreatIntel - ctx context.Context - cancel context.CancelFunc - wg sync.WaitGroup - // keep references to servers for graceful shutdown - httpSrvs []*http.Server - sshSigner ssh.Signer - // track TCP listeners for graceful shutdown - mu sync.Mutex - listeners []net.Listener - conns map[net.Conn]struct{} + cfg Config + logger *Logger + threatIntel *ThreatIntel + ctx context.Context + cancel context.CancelFunc + wg sync.WaitGroup + // keep references to servers for graceful shutdown + httpSrvs []*http.Server + sshSigner ssh.Signer + // track TCP listeners for graceful shutdown + mu sync.Mutex + listeners []net.Listener + conns map[net.Conn]struct{} } // HTTPS honeypot using self-signed or configured certificate func (a *App) startHTTPS(port int) { - addr := fmt.Sprintf(":%d", port) - mux := http.NewServeMux() - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - var bodySnippet string - if r.Body != nil { - b, _ := io.ReadAll(io.LimitReader(r.Body, 1024*64)) - bodySnippet = string(b) - } - details := map[string]string{"method": r.Method, "url": r.URL.String(), "proto": r.Proto} - if ua := r.Header.Get("User-Agent"); ua != "" { details["user_agent"] = ua } - if sni := r.TLS; sni != nil && sni.ServerName != "" { details["sni_server_name"] = sni.ServerName } - rec := Record{Timestamp: time.Now().UTC(), RemoteAddr: remoteIP(r.RemoteAddr), RemotePort: remotePort(r.RemoteAddr), Service: "https", Details: details, RawPayload: bodySnippet} - a.logEvent(rec) + addr := fmt.Sprintf(":%d", port) + mux := http.NewServeMux() + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + var bodySnippet string + if r.Body != nil { + b, _ := io.ReadAll(io.LimitReader(r.Body, 1024*64)) + bodySnippet = string(b) + } + details := map[string]string{"method": r.Method, "url": r.URL.String(), "proto": r.Proto} + if ua := r.Header.Get("User-Agent"); ua != "" { + details["user_agent"] = ua + } + if sni := r.TLS; sni != nil && sni.ServerName != "" { + details["sni_server_name"] = sni.ServerName + } + rec := Record{Timestamp: time.Now().UTC(), RemoteAddr: remoteIP(r.RemoteAddr), RemotePort: remotePort(r.RemoteAddr), Service: "https", Details: details, RawPayload: bodySnippet} + a.logEvent(rec) - w.Header().Set("Server", "nginx/1.18.0 (Ubuntu)") - w.Header().Set("Content-Type", "text/plain") - w.WriteHeader(200) - _, _ = w.Write([]byte("Welcome (TLS)\n")) - }) + w.Header().Set("Server", "nginx/1.18.0 (Ubuntu)") + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(200) + _, _ = w.Write([]byte("Welcome (TLS)\n")) + }) - // Load or create certificate - cert, _, _, err := a.GetOrCreateTLSCertificate() - if err != nil { - log.Printf("HTTPS certificate error: %v", err) - return - } - tlsCfg := &tls.Config{Certificates: []tls.Certificate{cert}} + // Load or create certificate + cert, _, _, err := a.GetOrCreateTLSCertificate() + if err != nil { + log.Printf("HTTPS certificate error: %v", err) + return + } + tlsCfg := &tls.Config{Certificates: []tls.Certificate{cert}} - srv := &http.Server{Addr: addr, Handler: mux, TLSConfig: tlsCfg} - a.addHTTPServer(srv) - log.Printf("HTTPS listening on %s", addr) - ln, err := net.Listen("tcp", addr) - if err != nil { - log.Printf("https listen failed on %s: %v", addr, err) - return - } - // Wrap with TLS and serve - tlsLn := tls.NewListener(ln, tlsCfg) - if err := srv.Serve(tlsLn); err != nil && err != http.ErrServerClosed { - log.Printf("https server error: %v", err) - } - log.Printf("https stopped") + srv := &http.Server{Addr: addr, Handler: mux, TLSConfig: tlsCfg} + a.addHTTPServer(srv) + log.Printf("HTTPS listening on %s", addr) + ln, err := net.Listen("tcp", addr) + if err != nil { + log.Printf("https listen failed on %s: %v", addr, err) + return + } + // Wrap with TLS and serve + tlsLn := tls.NewListener(ln, tlsCfg) + if err := srv.Serve(tlsLn); err != nil && err != http.ErrServerClosed { + log.Printf("https server error: %v", err) + } + log.Printf("https stopped") } // addHTTPServer registers an HTTP server for graceful shutdown func (a *App) addHTTPServer(s *http.Server) { - a.mu.Lock() - a.httpSrvs = append(a.httpSrvs, s) - a.mu.Unlock() + a.mu.Lock() + a.httpSrvs = append(a.httpSrvs, s) + a.mu.Unlock() } // addListener registers a listener for later shutdown func (a *App) addListener(l net.Listener) { - a.mu.Lock() - a.listeners = append(a.listeners, l) - a.mu.Unlock() + a.mu.Lock() + a.listeners = append(a.listeners, l) + a.mu.Unlock() } // parseCreds attempts to extract a username and password from arbitrary SSH-like payloads. @@ -108,125 +113,125 @@ func (a *App) addListener(l net.Listener) { // - "user=root pass=toor" // - "username root password toor" func parseCreds(data []byte) (string, string) { - sOrig := string(data) - sLower := strings.ToLower(sOrig) + sOrig := string(data) + sLower := strings.ToLower(sOrig) - // Try common keys in decreasing specificity - userKeys := []string{"username", "user", "login"} - passKeys := []string{"password", "passwd", "pass", "pwd"} + // Try common keys in decreasing specificity + userKeys := []string{"username", "user", "login"} + passKeys := []string{"password", "passwd", "pass", "pwd"} - var user string - var pass string + var user string + var pass string - for _, k := range userKeys { - if user == "" { - user = extractAfter(sLower, sOrig, k) - } - } - for _, k := range passKeys { - if pass == "" { - pass = extractAfter(sLower, sOrig, k) - } - } + for _, k := range userKeys { + if user == "" { + user = extractAfter(sLower, sOrig, k) + } + } + for _, k := range passKeys { + if pass == "" { + pass = extractAfter(sLower, sOrig, k) + } + } - // Handle a very common two-line prompt format - if user == "" && strings.Contains(sLower, "login:") { - user = extractAfter(sLower, sOrig, "login:") - } - if pass == "" && strings.Contains(sLower, "password:") { - pass = extractAfter(sLower, sOrig, "password:") - } + // Handle a very common two-line prompt format + if user == "" && strings.Contains(sLower, "login:") { + user = extractAfter(sLower, sOrig, "login:") + } + if pass == "" && strings.Contains(sLower, "password:") { + pass = extractAfter(sLower, sOrig, "password:") + } - // Trim quotes and whitespace - user = trimQuotes(strings.TrimSpace(user)) - pass = trimQuotes(strings.TrimSpace(pass)) - return user, pass + // Trim quotes and whitespace + user = trimQuotes(strings.TrimSpace(user)) + pass = trimQuotes(strings.TrimSpace(pass)) + return user, pass } // extractAfter finds the token in the lowercased string, then returns the next word-like token // from the original-cased string. func extractAfter(sLower, sOrig, tokenLower string) string { - idx := strings.Index(sLower, tokenLower) - if idx == -1 { - return "" - } - i := idx + len(tokenLower) - // Skip separators - for i < len(sLower) { - c := sLower[i] - if c == ' ' || c == '\t' || c == ':' || c == '=' || c == '-' { // common separators - i++ - continue - } - break - } - // Capture until whitespace or line break - j := i - for j < len(sLower) { - c := sLower[j] - if c == ' ' || c == '\t' || c == '\n' || c == '\r' { // stop at separators - break - } - j++ - } - if i >= len(sOrig) || j > len(sOrig) || i >= j { - return "" - } - return sOrig[i:j] + idx := strings.Index(sLower, tokenLower) + if idx == -1 { + return "" + } + i := idx + len(tokenLower) + // Skip separators + for i < len(sLower) { + c := sLower[i] + if c == ' ' || c == '\t' || c == ':' || c == '=' || c == '-' { // common separators + i++ + continue + } + break + } + // Capture until whitespace or line break + j := i + for j < len(sLower) { + c := sLower[j] + if c == ' ' || c == '\t' || c == '\n' || c == '\r' { // stop at separators + break + } + j++ + } + if i >= len(sOrig) || j > len(sOrig) || i >= j { + return "" + } + return sOrig[i:j] } func trimQuotes(s string) string { - if len(s) >= 2 { - if (s[0] == '\'' && s[len(s)-1] == '\'') || (s[0] == '"' && s[len(s)-1] == '"') { - return s[1 : len(s)-1] - } - } - return s + if len(s) >= 2 { + if (s[0] == '\'' && s[len(s)-1] == '\'') || (s[0] == '"' && s[len(s)-1] == '"') { + return s[1 : len(s)-1] + } + } + return s } func NewApp(cfg Config) (*App, error) { - l, err := NewLogger(cfg) - if err != nil { - return nil, err - } - // Initialize threat intelligence system - threatIntelPath := "threat_intel.json" - if cfg.LogPath != "" { - threatIntelPath = filepath.Join(filepath.Dir(cfg.LogPath), "threat_intel.json") - } - ti := NewThreatIntel(threatIntelPath, true) + l, err := NewLogger(cfg) + if err != nil { + return nil, err + } + // Initialize threat intelligence system + threatIntelPath := "threat_intel.json" + if cfg.LogPath != "" { + threatIntelPath = filepath.Join(filepath.Dir(cfg.LogPath), "threat_intel.json") + } + ti := NewThreatIntel(threatIntelPath, true) - // Root context for the App used for shutdown signalling - ctx, cancel := context.WithCancel(context.Background()) - a := &App{cfg: cfg, logger: l, threatIntel: ti, ctx: ctx, cancel: cancel, conns: make(map[net.Conn]struct{})} - return a, nil + // Root context for the App used for shutdown signalling + ctx, cancel := context.WithCancel(context.Background()) + a := &App{cfg: cfg, logger: l, threatIntel: ti, ctx: ctx, cancel: cancel, conns: make(map[net.Conn]struct{})} + return a, nil } func (a *App) Run(ctx context.Context) error { - // start services according to cfg - if a.cfg.Services.HTTP { - a.wg.Add(1) - go func() { - defer a.wg.Done() - a.startHTTP(a.cfg.Ports.HTTP) - }() - } - if a.cfg.Services.HTTPS { - a.wg.Add(1) - go func() { - defer a.wg.Done() - a.startHTTPS(a.cfg.Ports.HTTPS) - }() - } - if a.cfg.Services.SSH { - a.wg.Add(1) - go func() { - defer a.wg.Done() - handler := svcs.NewSSHHandler(a.svcLogger(), a.getSSHSigner) - a.startTCPService("ssh", a.cfg.Ports.SSH, handler) - }() - } - if a.cfg.Services.FTP { + // start services according to cfg + if a.cfg.Services.HTTP { + a.wg.Add(1) + go func() { + defer a.wg.Done() + a.startHTTP(a.cfg.Ports.HTTP) + }() + } + if a.cfg.Services.HTTPS { + a.wg.Add(1) + go func() { + defer a.wg.Done() + a.startHTTPS(a.cfg.Ports.HTTPS) + }() + } + if a.cfg.Services.SSH { + a.wg.Add(1) + go func() { + defer a.wg.Done() + handler := svcs.NewSSHHandler(a.svcLogger(), a.getSSHSigner) + a.startTCPService("ssh", a.cfg.Ports.SSH, handler) + }() + } + if a.cfg.Services.FTP { a.wg.Add(1) go func() { defer a.wg.Done() @@ -240,13 +245,6 @@ func (a *App) Run(ctx context.Context) error { a.startTCPService("smtp", a.cfg.Ports.SMTP, svcs.NewSMTPHandler(a.svcLogger())) }() } - if a.cfg.Services.POP3 { - a.wg.Add(1) - go func() { - defer a.wg.Done() - a.startTCPService("pop3", a.cfg.Ports.POP3, svcs.NewPOP3Handler(a.svcLogger())) - }() - } if a.cfg.Services.IMAP { a.wg.Add(1) go func() { @@ -275,13 +273,6 @@ func (a *App) Run(ctx context.Context) error { a.startTCPService("postgresql", a.cfg.Ports.PostgreSQL, svcs.NewPostgreSQLHandler(a.svcLogger())) }() } - if a.cfg.Services.Redis { - a.wg.Add(1) - go func() { - defer a.wg.Done() - a.startTCPService("redis", a.cfg.Ports.Redis, svcs.NewRedisHandler(a.svcLogger())) - }() - } if a.cfg.Services.MongoDB { a.wg.Add(1) go func() { @@ -300,14 +291,14 @@ func (a *App) Run(ctx context.Context) error { a.wg.Add(1) go func() { defer a.wg.Done() - a.startTCPService("smb", a.cfg.Ports.SMB, svcs.NewGenericBannerHandler("SMB-Server-1.0", a.svcLogger())) + a.startTCPService("smb", a.cfg.Ports.SMB, svcs.NewSMBHandler(a.svcLogger())) }() } if a.cfg.Services.VNC { a.wg.Add(1) go func() { defer a.wg.Done() - a.startTCPService("vnc", a.cfg.Ports.VNC, svcs.NewGenericBannerHandler("RFB 003.008", a.svcLogger())) + a.startTCPService("vnc", a.cfg.Ports.VNC, svcs.NewVNCHandler(a.svcLogger())) }() } if a.cfg.Services.SIP { @@ -317,27 +308,6 @@ func (a *App) Run(ctx context.Context) error { a.startTCPService("sip", a.cfg.Ports.SIP, svcs.NewSIPHandler(a.svcLogger())) }() } - if a.cfg.Services.DNS { - a.wg.Add(1) - go func() { - defer a.wg.Done() - a.startTCPService("dns", a.cfg.Ports.DNS, svcs.NewDNSHandler(a.svcLogger())) - }() - } - if a.cfg.Services.SNMP { - a.wg.Add(1) - go func() { - defer a.wg.Done() - a.startTCPService("snmp", a.cfg.Ports.SNMP, svcs.NewSNMPHandler(a.svcLogger())) - }() - } - if a.cfg.Services.LDAP { - a.wg.Add(1) - go func() { - defer a.wg.Done() - a.startTCPService("ldap", a.cfg.Ports.LDAP, svcs.NewLDAPHandler(a.svcLogger())) - }() - } for _, p := range a.cfg.Services.Generic { port := p a.wg.Add(1) @@ -362,433 +332,433 @@ func (a *App) Run(ctx context.Context) error { } func (a *App) Shutdown() { - a.cancel() - // attempt to close all http servers if running - a.mu.Lock() - srvs := a.httpSrvs - a.httpSrvs = nil - a.mu.Unlock() - for _, s := range srvs { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - _ = s.Shutdown(ctx) - cancel() - } - // close all TCP listeners to unblock Accept loops - a.closeAllListeners() - // close all tracked connections to unblock handlers - a.closeAllConns() - a.wg.Wait() - - // Save threat intelligence data before shutdown - if a.threatIntel != nil { - _ = a.threatIntel.Save() - } - - _ = a.logger.Close() + a.cancel() + // attempt to close all http servers if running + a.mu.Lock() + srvs := a.httpSrvs + a.httpSrvs = nil + a.mu.Unlock() + for _, s := range srvs { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + _ = s.Shutdown(ctx) + cancel() + } + // close all TCP listeners to unblock Accept loops + a.closeAllListeners() + // close all tracked connections to unblock handlers + a.closeAllConns() + a.wg.Wait() + + // Save threat intelligence data before shutdown + if a.threatIntel != nil { + _ = a.threatIntel.Save() + } + + _ = a.logger.Close() } // closeAllListeners closes all tracked listeners to unblock Accept() func (a *App) closeAllListeners() { - a.mu.Lock() - ls := a.listeners - a.listeners = nil - a.mu.Unlock() - for _, l := range ls { - _ = l.Close() - } + a.mu.Lock() + ls := a.listeners + a.listeners = nil + a.mu.Unlock() + for _, l := range ls { + _ = l.Close() + } } // addConn tracks an active connection for shutdown func (a *App) addConn(c net.Conn) { - a.mu.Lock() - if a.conns == nil { - a.conns = make(map[net.Conn]struct{}) - } - a.conns[c] = struct{}{} - a.mu.Unlock() + a.mu.Lock() + if a.conns == nil { + a.conns = make(map[net.Conn]struct{}) + } + a.conns[c] = struct{}{} + a.mu.Unlock() } // removeConn removes a connection from tracking func (a *App) removeConn(c net.Conn) { - a.mu.Lock() - delete(a.conns, c) - a.mu.Unlock() + a.mu.Lock() + delete(a.conns, c) + a.mu.Unlock() } // closeAllConns closes all tracked connections to unblock handlers func (a *App) closeAllConns() { - a.mu.Lock() - conns := make([]net.Conn, 0, len(a.conns)) - for c := range a.conns { - conns = append(conns, c) - } - a.conns = make(map[net.Conn]struct{}) - a.mu.Unlock() - for _, c := range conns { - _ = c.Close() - } + a.mu.Lock() + conns := make([]net.Conn, 0, len(a.conns)) + for c := range a.conns { + conns = append(conns, c) + } + a.conns = make(map[net.Conn]struct{}) + a.mu.Unlock() + for _, c := range conns { + _ = c.Close() + } } // helpers for logging func (a *App) logEvent(r Record) { - if a.logger != nil { - _ = a.logger.Log(r) - } - - // Record threat intelligence if IP is not private - if a.threatIntel != nil && r.RemoteAddr != "" && !IsPrivateIP(r.RemoteAddr) { - a.threatIntel.RecordActivity(r) - } + if a.logger != nil { + _ = a.logger.Log(r) + } + + // Record threat intelligence if IP is not private + if a.threatIntel != nil && r.RemoteAddr != "" && !IsPrivateIP(r.RemoteAddr) { + a.threatIntel.RecordActivity(r) + } } // svcLogger adapts services.Record to the app Record and logs it func (a *App) svcLogger() func(svcs.Record) { - return func(sr svcs.Record) { - a.logEvent(Record{ - Timestamp: sr.Timestamp, - RemoteAddr: sr.RemoteAddr, - RemotePort: sr.RemotePort, - Service: sr.Service, - Details: sr.Details, - RawPayload: sr.RawPayload, - }) - } + return func(sr svcs.Record) { + a.logEvent(Record{ + Timestamp: sr.Timestamp, + RemoteAddr: sr.RemoteAddr, + RemotePort: sr.RemotePort, + Service: sr.Service, + Details: sr.Details, + RawPayload: sr.RawPayload, + }) + } } // HTTP honeypot func (a *App) startHTTP(port int) { - addr := fmt.Sprintf(":%d", port) - mux := http.NewServeMux() - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - // read bounded body - var bodySnippet string - if r.Body != nil { - b, _ := io.ReadAll(io.LimitReader(r.Body, 1024*64)) - bodySnippet = string(b) - } - details := map[string]string{"method": r.Method, "url": r.URL.String(), "proto": r.Proto} - if ua := r.Header.Get("User-Agent"); ua != "" { - details["user_agent"] = ua - } - if auth := r.Header.Get("Authorization"); auth != "" { - details["authorization"] = auth - } - rec := Record{Timestamp: time.Now().UTC(), RemoteAddr: remoteIP(r.RemoteAddr), RemotePort: remotePort(r.RemoteAddr), Service: "http", Details: details, RawPayload: bodySnippet} - a.logEvent(rec) + addr := fmt.Sprintf(":%d", port) + mux := http.NewServeMux() + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + // read bounded body + var bodySnippet string + if r.Body != nil { + b, _ := io.ReadAll(io.LimitReader(r.Body, 1024*64)) + bodySnippet = string(b) + } + details := map[string]string{"method": r.Method, "url": r.URL.String(), "proto": r.Proto} + if ua := r.Header.Get("User-Agent"); ua != "" { + details["user_agent"] = ua + } + if auth := r.Header.Get("Authorization"); auth != "" { + details["authorization"] = auth + } + rec := Record{Timestamp: time.Now().UTC(), RemoteAddr: remoteIP(r.RemoteAddr), RemotePort: remotePort(r.RemoteAddr), Service: "http", Details: details, RawPayload: bodySnippet} + a.logEvent(rec) - w.Header().Set("Server", "Apache/2.4.41 (Ubuntu)") - w.Header().Set("Content-Type", "text/plain") - w.WriteHeader(200) - _, _ = w.Write([]byte("Welcome\n")) - }) + w.Header().Set("Server", "Apache/2.4.41 (Ubuntu)") + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(200) + _, _ = w.Write([]byte("Welcome\n")) + }) - srv := &http.Server{Addr: addr, Handler: mux} - a.addHTTPServer(srv) - log.Printf("HTTP listening on %s", addr) - if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { - log.Printf("http server error: %v", err) - } - log.Printf("http stopped") + srv := &http.Server{Addr: addr, Handler: mux} + a.addHTTPServer(srv) + log.Printf("HTTP listening on %s", addr) + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Printf("http server error: %v", err) + } + log.Printf("http stopped") } // remote helpers func remoteIP(addr string) string { - h, _, _ := net.SplitHostPort(addr) - return h + h, _, _ := net.SplitHostPort(addr) + return h } func remotePort(addr string) string { - _, p, _ := net.SplitHostPort(addr) - return p + _, p, _ := net.SplitHostPort(addr) + return p } // Generic TCP listener starter func (a *App) startTCPService(name string, port int, handler func(net.Conn)) { - addr := fmt.Sprintf(":%d", port) - ln, err := net.Listen("tcp", addr) - if err != nil { - log.Printf("%s listen failed on %s: %v", name, addr, err) - return - } - log.Printf("%s listening on %s", name, addr) - a.addListener(ln) - defer func() { - ln.Close() - log.Printf("%s stopped", name) - }() + addr := fmt.Sprintf(":%d", port) + ln, err := net.Listen("tcp", addr) + if err != nil { + log.Printf("%s listen failed on %s: %v", name, addr, err) + return + } + log.Printf("%s listening on %s", name, addr) + a.addListener(ln) + defer func() { + ln.Close() + log.Printf("%s stopped", name) + }() - // Use a channel to coordinate shutdown - acceptCh := make(chan net.Conn) - errCh := make(chan error) - - go func() { - for { - conn, err := ln.Accept() - if err != nil { - errCh <- err - return - } - acceptCh <- conn - } - }() + // Use a channel to coordinate shutdown + acceptCh := make(chan net.Conn) + errCh := make(chan error) - for { - select { - case <-a.ctx.Done(): - return - case conn := <-acceptCh: - go func(c net.Conn) { - // track and ensure cleanup - a.addConn(c) - defer func() { - a.removeConn(c) - _ = c.Close() - }() - // Check if context is cancelled before processing - select { - case <-a.ctx.Done(): - return - default: - handler(c) - } - }(conn) - case err := <-errCh: - select { - case <-a.ctx.Done(): - return - default: - log.Printf("%s accept err: %v", name, err) - return // Exit on persistent errors - } - } - } + go func() { + for { + conn, err := ln.Accept() + if err != nil { + errCh <- err + return + } + acceptCh <- conn + } + }() + + for { + select { + case <-a.ctx.Done(): + return + case conn := <-acceptCh: + go func(c net.Conn) { + // track and ensure cleanup + a.addConn(c) + defer func() { + a.removeConn(c) + _ = c.Close() + }() + // Check if context is cancelled before processing + select { + case <-a.ctx.Done(): + return + default: + handler(c) + } + }(conn) + case err := <-errCh: + select { + case <-a.ctx.Done(): + return + default: + log.Printf("%s accept err: %v", name, err) + return // Exit on persistent errors + } + } + } } // sshHandler implements a real SSH server handshake and logs password auth attempts func (a *App) sshHandler(conn net.Conn) { - defer conn.Close() - remote := conn.RemoteAddr().String() - sessionID := fmt.Sprintf("%x", time.Now().UnixNano()) - start := time.Now() - log.Printf("[SSH] New connection from %s (session: %s)", remote, sessionID) + defer conn.Close() + remote := conn.RemoteAddr().String() + sessionID := fmt.Sprintf("%x", time.Now().UnixNano()) + start := time.Now() + log.Printf("[SSH] New connection from %s (session: %s)", remote, sessionID) - signer, err := a.getSSHSigner() - if err != nil { - log.Printf("[SSH] host key error: %v", err) - return - } + signer, err := a.getSSHSigner() + if err != nil { + log.Printf("[SSH] host key error: %v", err) + return + } - var authAttempts int - var lastUser, lastPass string + var authAttempts int + var lastUser, lastPass string - cfg := &ssh.ServerConfig{ - NoClientAuth: false, - ServerVersion: "SSH-2.0-OpenSSH_7.9p1 Ubuntu-10", - PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) { - authAttempts++ - lastUser = c.User() - lastPass = string(pass) - a.logSSHEvent(sessionID, remote, "auth_attempt", map[string]string{ - "attempt": strconv.Itoa(authAttempts), - "username": lastUser, - "password": lastPass, - "client": string(c.ClientVersion()), - }) - return nil, fmt.Errorf("permission denied") - }, - } - cfg.AddHostKey(signer) + cfg := &ssh.ServerConfig{ + NoClientAuth: false, + ServerVersion: "SSH-2.0-OpenSSH_7.9p1 Ubuntu-10", + PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) { + authAttempts++ + lastUser = c.User() + lastPass = string(pass) + a.logSSHEvent(sessionID, remote, "auth_attempt", map[string]string{ + "attempt": strconv.Itoa(authAttempts), + "username": lastUser, + "password": lastPass, + "client": string(c.ClientVersion()), + }) + return nil, fmt.Errorf("permission denied") + }, + } + cfg.AddHostKey(signer) - _ = conn.SetDeadline(time.Now().Add(2 * time.Minute)) + _ = conn.SetDeadline(time.Now().Add(2 * time.Minute)) - sc, chans, reqs, err := ssh.NewServerConn(conn, cfg) - if err != nil { - a.logSSHEvent(sessionID, remote, "session_end", map[string]string{ - "auth_attempts": strconv.Itoa(authAttempts), - "duration_sec": fmt.Sprintf("%.2f", time.Since(start).Seconds()), - "last_username": lastUser, - "last_password": lastPass, - "error": err.Error(), - }) - return - } - go ssh.DiscardRequests(reqs) - for ch := range chans { - _ = ch.Reject(ssh.Prohibited, "not allowed") - } - _ = sc.Close() + sc, chans, reqs, err := ssh.NewServerConn(conn, cfg) + if err != nil { + a.logSSHEvent(sessionID, remote, "session_end", map[string]string{ + "auth_attempts": strconv.Itoa(authAttempts), + "duration_sec": fmt.Sprintf("%.2f", time.Since(start).Seconds()), + "last_username": lastUser, + "last_password": lastPass, + "error": err.Error(), + }) + return + } + go ssh.DiscardRequests(reqs) + for ch := range chans { + _ = ch.Reject(ssh.Prohibited, "not allowed") + } + _ = sc.Close() } // getSSHSigner returns or generates an RSA host key signer func (a *App) getSSHSigner() (ssh.Signer, error) { - if a.sshSigner != nil { - return a.sshSigner, nil - } - // Determine host key path: config override or alongside LogPath - var hostKeyPath string - if a.cfg.Certificates.SSHHostKeyPath != "" { - hostKeyPath = a.cfg.Certificates.SSHHostKeyPath - } else { - baseDir := a.dataDir() - hostKeyPath = filepath.Join(baseDir, "ssh_host_key.pem") - } - signer, err := a.loadOrCreateSSHHostKey(hostKeyPath) - if err != nil { - return nil, err - } - a.sshSigner = signer - return signer, nil + if a.sshSigner != nil { + return a.sshSigner, nil + } + // Determine host key path: config override or alongside LogPath + var hostKeyPath string + if a.cfg.Certificates.SSHHostKeyPath != "" { + hostKeyPath = a.cfg.Certificates.SSHHostKeyPath + } else { + baseDir := a.dataDir() + hostKeyPath = filepath.Join(baseDir, "ssh_host_key.pem") + } + signer, err := a.loadOrCreateSSHHostKey(hostKeyPath) + if err != nil { + return nil, err + } + a.sshSigner = signer + return signer, nil } // dataDir returns the directory where persistent data should live (same dir as LogPath) func (a *App) dataDir() string { - if a.cfg.LogPath == "" { - return "." - } - return filepath.Dir(a.cfg.LogPath) + if a.cfg.LogPath == "" { + return "." + } + return filepath.Dir(a.cfg.LogPath) } // loadOrCreateSSHHostKey loads an RSA private key from PEM or generates and saves one func (a *App) loadOrCreateSSHHostKey(path string) (ssh.Signer, error) { - if b, err := os.ReadFile(path); err == nil { - // Try parse PEM private key - block, _ := pem.Decode(b) - if block != nil && strings.Contains(block.Type, "PRIVATE KEY") { - if key, err := x509.ParsePKCS1PrivateKey(block.Bytes); err == nil { - return ssh.NewSignerFromKey(key) - } - if k2, err := x509.ParsePKCS8PrivateKey(block.Bytes); err == nil { - return ssh.NewSignerFromKey(k2) - } - } - // If parse failed, fall through to generate new and overwrite - } - // Generate new RSA key and persist to PEM - key, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - return nil, err - } - if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { - return nil, err - } - pemBlock := &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)} - if err := os.WriteFile(path, pem.EncodeToMemory(pemBlock), 0600); err != nil { - return nil, err - } - return ssh.NewSignerFromKey(key) + if b, err := os.ReadFile(path); err == nil { + // Try parse PEM private key + block, _ := pem.Decode(b) + if block != nil && strings.Contains(block.Type, "PRIVATE KEY") { + if key, err := x509.ParsePKCS1PrivateKey(block.Bytes); err == nil { + return ssh.NewSignerFromKey(key) + } + if k2, err := x509.ParsePKCS8PrivateKey(block.Bytes); err == nil { + return ssh.NewSignerFromKey(k2) + } + } + // If parse failed, fall through to generate new and overwrite + } + // Generate new RSA key and persist to PEM + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, err + } + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + return nil, err + } + pemBlock := &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)} + if err := os.WriteFile(path, pem.EncodeToMemory(pemBlock), 0600); err != nil { + return nil, err + } + return ssh.NewSignerFromKey(key) } // GetOrCreateTLSCertificate loads a TLS cert/key if provided or generates a self-signed pair // in the data directory (next to LogPath) and returns the tls.Certificate and the file paths used. func (a *App) GetOrCreateTLSCertificate() (tls.Certificate, string, string, error) { - // If custom cert/key provided, try load them - if a.cfg.Certificates.TLSCertPath != "" && a.cfg.Certificates.TLSKeyPath != "" { - cert, err := tls.LoadX509KeyPair(a.cfg.Certificates.TLSCertPath, a.cfg.Certificates.TLSKeyPath) - return cert, a.cfg.Certificates.TLSCertPath, a.cfg.Certificates.TLSKeyPath, err - } - // Else generate or reuse self-signed in data dir - dir := a.dataDir() - certPath := filepath.Join(dir, "tls_cert.pem") - keyPath := filepath.Join(dir, "tls_key.pem") - if _, err := os.Stat(certPath); err == nil { - if _, err2 := os.Stat(keyPath); err2 == nil { - cert, err3 := tls.LoadX509KeyPair(certPath, keyPath) - return cert, certPath, keyPath, err3 - } - } - // create self-signed cert - if err := os.MkdirAll(dir, 0755); err != nil { - return tls.Certificate{}, "", "", err - } - priv, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - return tls.Certificate{}, "", "", err - } - serial, _ := rand.Int(rand.Reader, big.NewInt(1<<62)) - tmpl := x509.Certificate{ - SerialNumber: serial, - Subject: pkix.Name{CommonName: "honeypot.local"}, - NotBefore: time.Now().Add(-time.Hour), - NotAfter: time.Now().AddDate(5, 0, 0), - KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, - BasicConstraintsValid: true, - } - der, err := x509.CreateCertificate(rand.Reader, &tmpl, &tmpl, &priv.PublicKey, priv) - if err != nil { - return tls.Certificate{}, "", "", err - } - certOut := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}) - keyOut := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}) - if err := os.WriteFile(certPath, certOut, 0600); err != nil { - return tls.Certificate{}, "", "", err - } - if err := os.WriteFile(keyPath, keyOut, 0600); err != nil { - return tls.Certificate{}, "", "", err - } - cert, err := tls.X509KeyPair(certOut, keyOut) - return cert, certPath, keyPath, err + // If custom cert/key provided, try load them + if a.cfg.Certificates.TLSCertPath != "" && a.cfg.Certificates.TLSKeyPath != "" { + cert, err := tls.LoadX509KeyPair(a.cfg.Certificates.TLSCertPath, a.cfg.Certificates.TLSKeyPath) + return cert, a.cfg.Certificates.TLSCertPath, a.cfg.Certificates.TLSKeyPath, err + } + // Else generate or reuse self-signed in data dir + dir := a.dataDir() + certPath := filepath.Join(dir, "tls_cert.pem") + keyPath := filepath.Join(dir, "tls_key.pem") + if _, err := os.Stat(certPath); err == nil { + if _, err2 := os.Stat(keyPath); err2 == nil { + cert, err3 := tls.LoadX509KeyPair(certPath, keyPath) + return cert, certPath, keyPath, err3 + } + } + // create self-signed cert + if err := os.MkdirAll(dir, 0755); err != nil { + return tls.Certificate{}, "", "", err + } + priv, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return tls.Certificate{}, "", "", err + } + serial, _ := rand.Int(rand.Reader, big.NewInt(1<<62)) + tmpl := x509.Certificate{ + SerialNumber: serial, + Subject: pkix.Name{CommonName: "honeypot.local"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().AddDate(5, 0, 0), + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + } + der, err := x509.CreateCertificate(rand.Reader, &tmpl, &tmpl, &priv.PublicKey, priv) + if err != nil { + return tls.Certificate{}, "", "", err + } + certOut := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}) + keyOut := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}) + if err := os.WriteFile(certPath, certOut, 0600); err != nil { + return tls.Certificate{}, "", "", err + } + if err := os.WriteFile(keyPath, keyOut, 0600); err != nil { + return tls.Certificate{}, "", "", err + } + cert, err := tls.X509KeyPair(certOut, keyOut) + return cert, certPath, keyPath, err } // logSSHEvent is a helper to log SSH-specific events with consistent metadata func (a *App) logSSHEvent(sessionID, remote, eventType string, details map[string]string) { - if details == nil { - details = make(map[string]string) - } - details["session_id"] = sessionID - a.logEvent(Record{ - Timestamp: time.Now().UTC(), - RemoteAddr: remoteIP(remote), - RemotePort: remotePort(remote), - Service: "ssh", - Details: details, - RawPayload: fmt.Sprintf("%s: %v", eventType, details), - }) + if details == nil { + details = make(map[string]string) + } + details["session_id"] = sessionID + a.logEvent(Record{ + Timestamp: time.Now().UTC(), + RemoteAddr: remoteIP(remote), + RemotePort: remotePort(remote), + Service: "ssh", + Details: details, + RawPayload: fmt.Sprintf("%s: %v", eventType, details), + }) } // genericBannerHandler returns a handler that writes a banner then reads data func (a *App) genericBannerHandler(banner string) func(net.Conn) { - return func(c net.Conn) { - defer c.Close() - remote := c.RemoteAddr().String() - log.Printf("conn to %s from %s", banner, remote) - _, _ = c.Write([]byte(banner + "\r\n")) - c.SetDeadline(time.Now().Add(10 * time.Second)) - buf := make([]byte, 4096) - n, _ := c.Read(buf) - payload := strings.TrimSpace(string(buf[:n])) - a.logEvent(Record{Timestamp: time.Now().UTC(), RemoteAddr: remoteIP(remote), RemotePort: remotePort(remote), Service: banner, Details: map[string]string{"read_bytes": strconv.Itoa(n)}, RawPayload: payload}) - } + return func(c net.Conn) { + defer c.Close() + remote := c.RemoteAddr().String() + log.Printf("conn to %s from %s", banner, remote) + _, _ = c.Write([]byte(banner + "\r\n")) + c.SetDeadline(time.Now().Add(10 * time.Second)) + buf := make([]byte, 4096) + n, _ := c.Read(buf) + payload := strings.TrimSpace(string(buf[:n])) + a.logEvent(Record{Timestamp: time.Now().UTC(), RemoteAddr: remoteIP(remote), RemotePort: remotePort(remote), Service: banner, Details: map[string]string{"read_bytes": strconv.Itoa(n)}, RawPayload: payload}) + } } func (a *App) sipHandler(c net.Conn) { - defer c.Close() - remote := c.RemoteAddr().String() - log.Printf("sip conn from %s", remote) - c.SetDeadline(time.Now().Add(8 * time.Second)) - r := bufio.NewReader(c) - line, _ := r.ReadString('\n') - if line == "" { - return - } - a.logEvent(Record{Timestamp: time.Now().UTC(), RemoteAddr: remoteIP(remote), RemotePort: remotePort(remote), Service: "sip", Details: map[string]string{"first_line": strings.TrimSpace(line)}, RawPayload: line}) + defer c.Close() + remote := c.RemoteAddr().String() + log.Printf("sip conn from %s", remote) + c.SetDeadline(time.Now().Add(8 * time.Second)) + r := bufio.NewReader(c) + line, _ := r.ReadString('\n') + if line == "" { + return + } + a.logEvent(Record{Timestamp: time.Now().UTC(), RemoteAddr: remoteIP(remote), RemotePort: remotePort(remote), Service: "sip", Details: map[string]string{"first_line": strings.TrimSpace(line)}, RawPayload: line}) } func (a *App) genericEchoHandler(c net.Conn) { - defer c.Close() - remote := c.RemoteAddr().String() - log.Printf("generic conn from %s", remote) - c.SetDeadline(time.Now().Add(10 * time.Second)) - _, _ = c.Write([]byte("220 Welcome to service\r\n")) - r := bufio.NewReader(c) - var b strings.Builder - for i := 0; i < 10; i++ { - line, err := r.ReadString('\n') - if err != nil { - break - } - b.WriteString(line) - _, _ = c.Write([]byte("ACK\r\n")) - } - a.logEvent(Record{Timestamp: time.Now().UTC(), RemoteAddr: remoteIP(remote), RemotePort: remotePort(remote), Service: "generic", Details: map[string]string{"lines": strconv.Itoa(strings.Count(b.String(), "\n"))}, RawPayload: b.String()}) + defer c.Close() + remote := c.RemoteAddr().String() + log.Printf("generic conn from %s", remote) + c.SetDeadline(time.Now().Add(10 * time.Second)) + _, _ = c.Write([]byte("220 Welcome to service\r\n")) + r := bufio.NewReader(c) + var b strings.Builder + for i := 0; i < 10; i++ { + line, err := r.ReadString('\n') + if err != nil { + break + } + b.WriteString(line) + _, _ = c.Write([]byte("ACK\r\n")) + } + a.logEvent(Record{Timestamp: time.Now().UTC(), RemoteAddr: remoteIP(remote), RemotePort: remotePort(remote), Service: "generic", Details: map[string]string{"lines": strconv.Itoa(strings.Count(b.String(), "\n"))}, RawPayload: b.String()}) } diff --git a/app/services/dns.go b/app/services/dns.go deleted file mode 100644 index 2e24a5b..0000000 --- a/app/services/dns.go +++ /dev/null @@ -1,21 +0,0 @@ -package services - -import ( - "net" - "strconv" - "time" -) - -func NewDNSHandler(log LoggerFunc) Handler { - return func(conn net.Conn) { - defer conn.Close() - remote := conn.RemoteAddr().String() - conn.SetDeadline(time.Now().Add(10 * time.Second)) - buf := make([]byte, 512) - n, err := conn.Read(buf) - if err != nil { return } - if n > 12 { - log(Record{Timestamp: Now(), RemoteAddr: remoteIP(remote), RemotePort: remotePort(remote), Service: "dns", Details: map[string]string{"event":"query_received","query_size":strconv.Itoa(n)}}) - } - } -} diff --git a/app/services/imap.go b/app/services/imap.go index ffc1fb5..3ce2f76 100644 --- a/app/services/imap.go +++ b/app/services/imap.go @@ -14,7 +14,6 @@ func NewIMAPHandler(log LoggerFunc) Handler { _, _ = conn.Write([]byte("* OK IMAP4rev1 Service Ready\r\n")) conn.SetDeadline(time.Now().Add(2 * time.Minute)) scanner := bufio.NewScanner(conn) - authed := false selected := false mailbox := "INBOX" for scanner.Scan() { @@ -31,7 +30,6 @@ func NewIMAPHandler(log LoggerFunc) Handler { pass := strings.Trim(parts[3], "\"") log(Record{Timestamp: Now(), RemoteAddr: remoteIP(remote), RemotePort: remotePort(remote), Service: "imap", Details: map[string]string{"event":"login_attempt","username":user,"password":pass}}) } - authed = true // pretend success to keep interaction _, _ = conn.Write([]byte(tag + " OK LOGIN completed\r\n")) case "CAPABILITY": _, _ = conn.Write([]byte("* CAPABILITY IMAP4rev1 AUTH=PLAIN IDLE\r\n")) @@ -44,7 +42,8 @@ func NewIMAPHandler(log LoggerFunc) Handler { // fake mailbox with 2 messages _, _ = conn.Write([]byte("* 2 EXISTS\r\n")) _, _ = conn.Write([]byte("* OK [UIDVALIDITY 1] UIDs valid\r\n")) - _, _ = conn.Write([]byte(tag + " OK [READ-WRITE] SELECT completed\r\n")) + _, _ = conn.Write([]byte(tag + " OK [READ-WRITE] SELECT completed (" + mailbox + ")\r\n")) + log(Record{Timestamp: Now(), RemoteAddr: remoteIP(remote), RemotePort: remotePort(remote), Service: "imap", Details: map[string]string{"event":"select","mailbox":mailbox}}) case "FETCH": if !selected { _, _ = conn.Write([]byte(tag + " BAD No mailbox selected\r\n")); continue } // minimal fake fetch diff --git a/app/services/ldap.go b/app/services/ldap.go deleted file mode 100644 index 86c59d3..0000000 --- a/app/services/ldap.go +++ /dev/null @@ -1,22 +0,0 @@ -package services - -import ( - "net" - "strconv" - "time" -) - -func NewLDAPHandler(log LoggerFunc) Handler { - return func(conn net.Conn) { - defer conn.Close() - remote := conn.RemoteAddr().String() - conn.SetDeadline(time.Now().Add(30 * time.Second)) - buf := make([]byte, 1024) - n, err := conn.Read(buf) - if err != nil { return } - if n > 0 { - log(Record{Timestamp: Now(), RemoteAddr: remoteIP(remote), RemotePort: remotePort(remote), Service: "ldap", Details: map[string]string{"event":"bind_attempt","request_size":strconv.Itoa(n)}}) - } - _, _ = conn.Write([]byte{0x30,0x0c,0x02,0x01,0x01,0x61,0x07,0x0a,0x01,0x31,0x04,0x00,0x04,0x00}) - } -} diff --git a/app/services/mongodb.go b/app/services/mongodb.go index 829ccca..2d9984c 100644 --- a/app/services/mongodb.go +++ b/app/services/mongodb.go @@ -1,22 +1,202 @@ package services import ( - "net" - "strconv" - "time" + "encoding/binary" + "net" + "strconv" + "strings" + "time" ) func NewMongoDBHandler(log LoggerFunc) Handler { - return func(conn net.Conn) { - defer conn.Close() - remote := conn.RemoteAddr().String() - conn.SetDeadline(time.Now().Add(30 * time.Second)) - buf := make([]byte, 1024) - n, err := conn.Read(buf) - if err != nil { return } - if n >= 16 { - log(Record{Timestamp: Now(), RemoteAddr: remoteIP(remote), RemotePort: remotePort(remote), Service: "mongodb", Details: map[string]string{"event":"protocol_attempt","bytes_received":strconv.Itoa(n)}}) - } - _, _ = conn.Write([]byte("connection refused")) - } + return func(conn net.Conn) { + defer conn.Close() + remote := conn.RemoteAddr().String() + conn.SetDeadline(time.Now().Add(30 * time.Second)) + buf := make([]byte, 4096) + n, err := conn.Read(buf) + if err != nil { return } + details := map[string]string{"event":"protocol_attempt","bytes_received":strconv.Itoa(n)} + if n >= 16 { + msgLen := int32(binary.LittleEndian.Uint32(buf[0:4])) + // reqID := int32(binary.LittleEndian.Uint32(buf[4:8])) + // respTo := int32(binary.LittleEndian.Uint32(buf[8:12])) + opCode := int32(binary.LittleEndian.Uint32(buf[12:16])) + details["opcode"] = strconv.Itoa(int(opCode)) + details["msg_len"] = strconv.Itoa(int(msgLen)) + if opCode == 2004 { // OP_QUERY + // flags (4) + cstring ns starting at offset 20 + // skip flags + i := 20 + // extract cstring namespace + end := i + for end < n && buf[end] != 0 { end++ } + ns := string(buf[i:end]) + details["namespace"] = ns + } else if opCode == 2013 { // OP_MSG (modern commands like hello) + // Structure: flags (4) + sections... + payload := buf[16:n] + if len(payload) >= 5 { + // first section + kind := payload[4] + if kind == 0 && len(payload) > 5 { // body BSON document + doc := payload[5:] + out := map[string]string{} + // Flatten with prefix, cap strings to 64, cap total fields to 32 + parseBSONFlat(doc, 64, "", out, 32) + // Extract common command indicators + for _, k := range []string{"hello","isMaster","ismaster","saslStart","saslContinue","client","mechanism"} { + if v, ok := out[k]; ok { + details["op_msg_"+k] = v + } + } + if _, ok := out["hello"]; ok { details["op_msg_hint"] = "hello" } + if _, ok := out["isMaster"]; ok { details["op_msg_hint"] = "isMaster" } + if _, ok := out["ismaster"]; ok { details["op_msg_hint"] = "ismaster" } + if _, ok := out["saslStart"]; ok { details["op_msg_hint"] = "saslStart" } + } + } + } + } + log(Record{Timestamp: Now(), RemoteAddr: remoteIP(remote), RemotePort: remotePort(remote), Service: "mongodb", Details: details}) + // Send a minimal isMaster/hello-like JSON to keep client talking + reply := `{"ok":1,"ismaster":true,"maxWireVersion":13,"minWireVersion":0,"helloOk":true}` + _, _ = conn.Write([]byte(reply)) + } +} + +// parseBSONFlat flattens a BSON document or array into out with key prefixing. +// - maxStr: maximum string length to record +// - prefix: current key prefix (e.g., "client.") +// - out: destination map +// - maxFields: stop after this many fields to avoid floods +func parseBSONFlat(b []byte, maxStr int, prefix string, out map[string]string, maxFields int) { + if len(out) >= maxFields || len(b) < 5 { return } + // First 4 bytes are int32 length; be lenient but keep bounds + i := 4 + for i < len(b) && len(out) < maxFields { + t := b[i] + if t == 0x00 { // EOO + break + } + i++ + // cstring key + ks := i + for i < len(b) && b[i] != 0 { i++ } + if i >= len(b) { return } + key := string(b[ks:i]) + if prefix != "" { key = prefix + key } + i++ // skip NUL + switch t { + case 0x01: // double + if i+8 > len(b) { return } + out[key] = "" + i += 8 + case 0x02: // string + if i+4 > len(b) { return } + sl := int(int32(binary.LittleEndian.Uint32(b[i : i+4]))) + i += 4 + if sl <= 0 || i+sl > len(b) { return } + val := string(b[i : i+sl-1]) // exclude trailing NUL + if maxStr > 0 && len(val) > maxStr { val = val[:maxStr] } + out[key] = val + i += sl + case 0x03: // embedded document + if i+4 > len(b) { return } + // read declared length to bound sub-doc + subLen := int(int32(binary.LittleEndian.Uint32(b[i : i+4]))) + end := i + subLen + if subLen <= 4 || end > len(b) { return } + parseBSONFlat(b[i:end], maxStr, key+".", out, maxFields) + i = end + case 0x04: // array + if i+4 > len(b) { return } + subLen := int(int32(binary.LittleEndian.Uint32(b[i : i+4]))) + end := i + subLen + if subLen <= 4 || end > len(b) { return } + parseBSONFlat(b[i:end], maxStr, key+".", out, maxFields) + i = end + case 0x08: // bool + if i >= len(b) { return } + if b[i] == 0x01 { out[key] = "true" } else { out[key] = "false" } + i++ + case 0x10: // int32 + if i+4 > len(b) { return } + v := int32(binary.LittleEndian.Uint32(b[i : i+4])) + out[key] = strconv.FormatInt(int64(v), 10) + i += 4 + case 0x12: // int64 + if i+8 > len(b) { return } + out[key] = "" + i += 8 + default: + // skip unknown types conservatively: stop parsing to avoid desync + return + } + } +} + +// containsAny reports whether s contains any of the substrings in list. +func containsAny(s string, list []string) bool { + ls := strings.ToLower(s) + for _, t := range list { + if t == "" { continue } + if strings.Contains(ls, strings.ToLower(t)) { + return true + } + } + return false +} + +// parseBSONStrings extracts top-level string/boolean/int32/double markers from a BSON document buffer. +// It is intentionally minimal and bounded for honeypot logging. +func parseBSONStrings(b []byte, maxStr int) map[string]string { + out := map[string]string{} + if len(b) < 5 { + return out + } + i := 4 // skip doc length + for i < len(b) { + t := b[i] + if t == 0x00 { // terminator + break + } + i++ + // read cstring key + ks := i + for i < len(b) && b[i] != 0 { i++ } + if i >= len(b) { + break + } + key := string(b[ks:i]) + i++ // skip NUL + switch t { + case 0x02: // string + if i+4 > len(b) { return out } + sl := int(int32(binary.LittleEndian.Uint32(b[i : i+4]))) + i += 4 + if sl <= 0 || i+sl > len(b) { return out } + val := string(b[i : i+sl-1]) // exclude trailing NUL + if maxStr > 0 && len(val) > maxStr { val = val[:maxStr] } + out[key] = val + i += sl + case 0x08: // boolean + if i >= len(b) { return out } + if b[i] == 0x01 { out[key] = "true" } else { out[key] = "false" } + i++ + case 0x10: // int32 + if i+4 > len(b) { return out } + v := int32(binary.LittleEndian.Uint32(b[i : i+4])) + out[key] = strconv.FormatInt(int64(v), 10) + i += 4 + case 0x01: // double + if i+8 > len(b) { return out } + out[key] = "" + i += 8 + default: + // Stop on types we don't parse to avoid desync + return out + } + } + return out } diff --git a/app/services/pop3.go b/app/services/pop3.go deleted file mode 100644 index c16962b..0000000 --- a/app/services/pop3.go +++ /dev/null @@ -1,39 +0,0 @@ -package services - -import ( - "bufio" - "net" - "strings" - "time" -) - -func NewPOP3Handler(log LoggerFunc) Handler { - return func(conn net.Conn) { - defer conn.Close() - remote := conn.RemoteAddr().String() - _, _ = conn.Write([]byte("+OK POP3 server ready\r\n")) - conn.SetDeadline(time.Now().Add(2 * time.Minute)) - scanner := bufio.NewScanner(conn) - var username string - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - if line == "" { continue } - parts := strings.SplitN(line, " ", 2) - cmd := strings.ToUpper(parts[0]) - arg := ""; if len(parts)>1 { arg = parts[1] } - switch cmd { - case "USER": - username = arg - log(Record{Timestamp: Now(), RemoteAddr: remoteIP(remote), RemotePort: remotePort(remote), Service: "pop3", Details: map[string]string{"event":"username_attempt","username":username}}) - _, _ = conn.Write([]byte("+OK\r\n")) - case "PASS": - log(Record{Timestamp: Now(), RemoteAddr: remoteIP(remote), RemotePort: remotePort(remote), Service: "pop3", Details: map[string]string{"event":"password_attempt","username":username,"password":arg}}) - _, _ = conn.Write([]byte("-ERR Authentication failed\r\n")) - case "QUIT": - _, _ = conn.Write([]byte("+OK Bye\r\n")); return - default: - _, _ = conn.Write([]byte("-ERR Unknown command\r\n")) - } - } - } -} diff --git a/app/services/rdp.go b/app/services/rdp.go index 060b062..f5d63f2 100644 --- a/app/services/rdp.go +++ b/app/services/rdp.go @@ -1,22 +1,35 @@ package services import ( - "net" - "strconv" - "time" + "encoding/binary" + "net" + "strconv" + "time" ) func NewRDPHandler(log LoggerFunc) Handler { - return func(conn net.Conn) { - defer conn.Close() - remote := conn.RemoteAddr().String() - conn.SetDeadline(time.Now().Add(30 * time.Second)) - buf := make([]byte, 1024) - n, err := conn.Read(buf) - if err != nil { return } - if n > 0 { - log(Record{Timestamp: Now(), RemoteAddr: remoteIP(remote), RemotePort: remotePort(remote), Service: "rdp", Details: map[string]string{"event":"protocol_attempt","bytes_received":strconv.Itoa(n)}}) - } - _, _ = conn.Write([]byte{0x03,0x00,0x00,0x0b,0x02,0xf0,0x80,0x04,0x01,0x00,0x01}) - } + return func(conn net.Conn) { + defer conn.Close() + remote := conn.RemoteAddr().String() + conn.SetDeadline(time.Now().Add(30 * time.Second)) + buf := make([]byte, 2048) + n, err := conn.Read(buf) + if err != nil { return } + det := map[string]string{"event":"protocol_attempt","bytes_received":strconv.Itoa(n)} + if n >= 4 { + // TPKT Header: 0x03 0x00 length(2) + if buf[0] == 0x03 && buf[1] == 0x00 { + tpktLen := int(binary.BigEndian.Uint16(buf[2:4])) + det["tpkt_len"] = strconv.Itoa(tpktLen) + if n >= 7 { + // Basic X.224 header follows; first byte of X.224 should be length + det["x224_len"] = strconv.Itoa(int(buf[4])) + det["x224_type"] = strconv.Itoa(int(buf[5])) // likely 0xE0 for CR TPDU + } + } + } + log(Record{Timestamp: Now(), RemoteAddr: remoteIP(remote), RemotePort: remotePort(remote), Service: "rdp", Details: det}) + // Send short failure/abort PDU to conclude early but cleanly + _, _ = conn.Write([]byte{0x03,0x00,0x00,0x0b,0x02,0xf0,0x80,0x04,0x01,0x00,0x01}) + } } diff --git a/app/services/redis.go b/app/services/redis.go deleted file mode 100644 index 3fa2b7b..0000000 --- a/app/services/redis.go +++ /dev/null @@ -1,34 +0,0 @@ -package services - -import ( - "bufio" - "net" - "strings" - "time" -) - -func NewRedisHandler(log LoggerFunc) Handler { - return func(conn net.Conn) { - defer conn.Close() - remote := conn.RemoteAddr().String() - conn.SetDeadline(time.Now().Add(2 * time.Minute)) - scanner := bufio.NewScanner(conn) - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - if line == "" { continue } - if strings.ToUpper(line) == "PING" { - _, _ = conn.Write([]byte("+PONG\r\n")) - continue - } - if strings.ToUpper(line) == "AUTH" { - if scanner.Scan() { - password := strings.TrimSpace(scanner.Text()) - log(Record{Timestamp: Now(), RemoteAddr: remoteIP(remote), RemotePort: remotePort(remote), Service: "redis", Details: map[string]string{"event":"auth_attempt","password":password}}) - } - _, _ = conn.Write([]byte("-ERR invalid password\r\n")) - continue - } - _, _ = conn.Write([]byte("-ERR unknown command\r\n")) - } - } -} diff --git a/app/services/smb.go b/app/services/smb.go new file mode 100644 index 0000000..a14df22 --- /dev/null +++ b/app/services/smb.go @@ -0,0 +1,122 @@ +package services + +import ( + "bytes" + "encoding/binary" + "net" + "strconv" + "strings" + "time" +) + +// NewSMBHandler logs incoming SMB negotiate/session setup attempts and returns a minimal response. +// It is NOT a full SMB implementation; it's enough to keep scanners interacting and capture tokens. +func NewSMBHandler(log LoggerFunc) Handler { + return func(conn net.Conn) { + defer conn.Close() + remote := conn.RemoteAddr().String() + conn.SetDeadline(time.Now().Add(15 * time.Second)) + buf := make([]byte, 2048) + n, _ := conn.Read(buf) + if n <= 0 { + return + } + first := buf[:n] + det := map[string]string{"event":"smb_probe","bytes_received":strconv.Itoa(n)} + // rough fingerprint if NTLMSSP is present + if bytes.Contains(first, []byte("NTLMSSP")) { + det["ntlmssp"] = "present" + if u, d, w, lmLen, ntLen := parseNTLMSSP(first); u != "" || d != "" || w != "" { + if u != "" { det["user"] = u } + if d != "" { det["domain"] = d } + if w != "" { det["workstation"] = w } + if lmLen > 0 { det["lm_resp_len"] = strconv.Itoa(lmLen) } + if ntLen > 0 { det["nt_resp_len"] = strconv.Itoa(ntLen) } + } + } + // Try to guess dialect by sniffing strings + fstr := strings.ToUpper(string(first)) + if strings.Contains(fstr, "SMB 2.1") || strings.Contains(fstr, "SMB2") { + det["dialect"] = "smb2" + } else if strings.Contains(fstr, "SMB 3") { + det["dialect"] = "smb3" + } else { + det["dialect"] = "unknown" + } + log(Record{Timestamp: Now(), RemoteAddr: remoteIP(remote), RemotePort: remotePort(remote), Service: "smb", Details: det}) + + // Send a minimal SMB2 NEGOTIATE failure-like response (not a valid implementation, just a banner) + resp := []byte{ + 0x00, 0x00, 0x00, 0x3F, // NetBIOS length header (len following) + 0xFE, 0x53, 0x4D, 0x42, // SMB2 magic + 0x40, 0x00, 0x00, 0x00, // header fields + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x24, 0x00, // StructureSize + 0x00, 0x00, // SecurityMode + 0x01, 0x00, // DialectRevision (SMB 2.1) + 0x00, 0x00, // NegotiateContextCount + 0x00, 0x00, 0x00, 0x00, // ServerGuid part (fake) + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // Capabilities & MaxTrans/Read/WriteSize (fake zeros) + } + _, _ = conn.Write(resp) + } +} + +// parseNTLMSSP tries to extract fields from an NTLMSSP message embedded in SMB payloads. +// It supports Type 1, 2, and 3 and focuses on Type 3 credential info. +func parseNTLMSSP(p []byte) (username, domain, workstation string, lmLen, ntLen int) { + sig := []byte("NTLMSSP\x00") + i := bytes.Index(p, sig) + if i < 0 || i+12 >= len(p) { return "","","",0,0 } + // Message type at offset i+8 (LE uint32) + if i+12 > len(p) { return } + mtype := binary.LittleEndian.Uint32(p[i+8 : i+12]) + switch mtype { + case 1: // Negotiate - no creds + return "","","",0,0 + case 2: // Challenge - no creds to extract + return "","","",0,0 + case 3: // Authenticate - contains domain, user, workstation + // Offsets are relative to start of NTLMSSP message + base := i + // Helper to read fields: len(2), maxLen(2), offset(4) + readField := func(off int) (l, o int) { + if base+off+8 > len(p) { return 0, 0 } + l = int(binary.LittleEndian.Uint16(p[base+off : base+off+2])) + o = int(binary.LittleEndian.Uint32(p[base+off+4 : base+off+8])) + return + } + // Per spec Type 3 layout: + // 0x14 LMResp, 0x1C NTResp, 0x24 DomainName, 0x2C UserName, 0x34 Workstation, 0x3C EncryptedRandomSessionKey + lmLen, _ = readField(0x14) + ntLen, _ = readField(0x1C) + domLen, domOff := readField(0x24) + usrLen, usrOff := readField(0x2C) + wsLen, wsOff := readField(0x34) + // Extract UTF-16LE strings safely + username = readUTF16LE(p, usrOff, usrLen) + domain = readUTF16LE(p, domOff, domLen) + workstation = readUTF16LE(p, wsOff, wsLen) + return + default: + return "","","",0,0 + } +} + +func readUTF16LE(p []byte, off, length int) string { + if off <= 0 || length <= 1 || off+length > len(p) { return "" } + // best-effort: convert UTF-16LE to UTF-8 by dropping high bytes if ASCII + // for simplicity; full decoding not necessary for logging + b := make([]byte, 0, length/2) + for j := 0; j+1 < length && off+j+1 < len(p); j += 2 { + b = append(b, p[off+j]) + } + s := strings.TrimSpace(string(b)) + return s +} diff --git a/app/services/snmp.go b/app/services/snmp.go deleted file mode 100644 index e32ee68..0000000 --- a/app/services/snmp.go +++ /dev/null @@ -1,21 +0,0 @@ -package services - -import ( - "net" - "strconv" - "time" -) - -func NewSNMPHandler(log LoggerFunc) Handler { - return func(conn net.Conn) { - defer conn.Close() - remote := conn.RemoteAddr().String() - conn.SetDeadline(time.Now().Add(10 * time.Second)) - buf := make([]byte, 1024) - n, err := conn.Read(buf) - if err != nil { return } - if n > 0 { - log(Record{Timestamp: Now(), RemoteAddr: remoteIP(remote), RemotePort: remotePort(remote), Service: "snmp", Details: map[string]string{"event":"request_received","request_size":strconv.Itoa(n)}}) - } - } -} diff --git a/app/services/telnet.go b/app/services/telnet.go index b02f3e8..da13008 100644 --- a/app/services/telnet.go +++ b/app/services/telnet.go @@ -16,8 +16,17 @@ func NewTelnetHandler(log LoggerFunc) Handler { scanner := bufio.NewScanner(conn) var username string expectingPassword := false + inShell := false for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) + if inShell { + // fake shell: log the command and respond with minimal output + cmd := line + if cmd == "exit" || cmd == "logout" { _, _ = conn.Write([]byte("logout\r\n")); return } + log(Record{Timestamp: Now(), RemoteAddr: remoteIP(remote), RemotePort: remotePort(remote), Service: "telnet", Details: map[string]string{"event":"shell_cmd","cmd":cmd}}) + _, _ = conn.Write([]byte("sh: " + cmd + ": command not found\r\n$ ")) + continue + } if !expectingPassword { username = line log(Record{Timestamp: Now(), RemoteAddr: remoteIP(remote), RemotePort: remotePort(remote), Service: "telnet", Details: map[string]string{"event":"username_attempt","username":username}}) @@ -26,8 +35,9 @@ func NewTelnetHandler(log LoggerFunc) Handler { } else { password := line log(Record{Timestamp: Now(), RemoteAddr: remoteIP(remote), RemotePort: remotePort(remote), Service: "telnet", Details: map[string]string{"event":"password_attempt","username":username,"password":password}}) - _, _ = conn.Write([]byte("\r\nLogin incorrect\r\nlogin: ")) - expectingPassword = false + // drop into a fake shell prompt to encourage interaction + _, _ = conn.Write([]byte("\r\nWelcome to Ubuntu 20.04.3 LTS (GNU/Linux 5.4.0-42-generic x86_64)\r\n$ ")) + inShell = true } } } diff --git a/app/services/vnc.go b/app/services/vnc.go new file mode 100644 index 0000000..cd35ef9 --- /dev/null +++ b/app/services/vnc.go @@ -0,0 +1,118 @@ +package services + +import ( + "bufio" + "encoding/binary" + "net" + "strings" + "time" +) + +// NewVNCHandler implements a minimal RFB handshake and then fails auth to keep interaction realistic. +func NewVNCHandler(log LoggerFunc) Handler { + return func(conn net.Conn) { + remote := conn.RemoteAddr().String() + conn.SetDeadline(time.Now().Add(20 * time.Second)) + + // Send server protocol version banner + serverVer := "RFB 003.008\n" + _, _ = conn.Write([]byte(serverVer)) + + r := bufio.NewReader(conn) + clientVer, _ := r.ReadString('\n') + clientVer = strings.TrimSpace(clientVer) + if clientVer == "" { + return + } + log(Record{Timestamp: Now(), RemoteAddr: remoteIP(remote), RemotePort: remotePort(remote), Service: "vnc", Details: map[string]string{"event":"version","client":clientVer}}) + + // Offer two security types: None (1) and VNC Authentication (2) + _, _ = conn.Write([]byte{2, 1, 2}) + + // Read client's selected security type (1 byte) + b, _ := r.ReadByte() + // If client chose None (1), send SecurityResult OK and minimal ServerInit + if b == 1 { + // OK result + _, _ = conn.Write([]byte{0, 0, 0, 0}) + // ClientInit (read but ignore) + _, _ = r.ReadByte() + // Send ServerInit: framebuffer width/height, pixel format, name length + name + // Width=800, Height=600 + srv := make([]byte, 0, 24) + srv = append(srv, 0x03, 0x20) // width 800 + srv = append(srv, 0x02, 0x58) // height 600 + // Pixel format (16 bytes): 32bpp, 24 depth, big endian=0, trueColor=1, max red/green/blue, shifts + srv = append(srv, 32, 24, 0, 1, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 16, 8, 0) + // pad 3 bytes + srv = append(srv, 0, 0, 0) + // Name length and name + name := []byte("Ubuntu VNC") + nameLen := []byte{0, 0, byte(len(name) >> 8), byte(len(name) & 0xff)} + // Actually RFB uses 32-bit name length big-endian + nameLen = []byte{0, 0, 0, byte(len(name))} + srv = append(srv, nameLen...) + srv = append(srv, name...) + _, _ = conn.Write(srv) + // Enter a simple loop to keep client engaged: respond to FramebufferUpdateRequest (type=3) + for { + // Read message type + mt, err := r.ReadByte() + if err != nil { return } + switch mt { + case 0: // SetPixelFormat: skip 3 pad + 16 bytes + skip := make([]byte, 19) + if _, err := r.Read(skip); err != nil { return } + case 2: // SetEncodings: 1 pad + int16 num + list + pad, _ := r.ReadByte(); _ = pad + Hdr := make([]byte, 2) + if _, err := r.Read(Hdr); err != nil { return } + nEnc := int(binary.BigEndian.Uint16(Hdr)) + encs := make([]byte, nEnc*4) + if _, err := r.Read(encs); err != nil { return } + case 3: // FramebufferUpdateRequest + req := make([]byte, 9) + if _, err := r.Read(req); err != nil { return } + // incremental := req[0] + // x,y,w,h (big-endian) + // Respond with one small RAW rectangle 64x32 at 0,0 + reply := make([]byte, 0, 4+12+64*32*4) + // message-type=0 (FramebufferUpdate) + pad + reply = append(reply, 0, 0) + // number-of-rectangles = 1 + reply = append(reply, 0, 1) + // x=0,y=0,w=64,h=32 + rb := make([]byte, 12) + // x,y + binary.BigEndian.PutUint16(rb[0:2], 0) + binary.BigEndian.PutUint16(rb[2:4], 0) + // w,h + binary.BigEndian.PutUint16(rb[4:6], 64) + binary.BigEndian.PutUint16(rb[6:8], 32) + // encoding RAW = 0 + binary.BigEndian.PutUint32(rb[8:12], 0) + reply = append(reply, rb...) + // pixel data (32bpp). Simple grey fill (e.g., 0x202020ff little-endian if needed) + px := make([]byte, 64*32*4) + for i := 0; i < len(px); i += 4 { + // BGRA for many viewers when little endian flag=0; we keep it neutral + px[i+0] = 0x20 + px[i+1] = 0x20 + px[i+2] = 0x20 + px[i+3] = 0xFF + } + reply = append(reply, px...) + if _, err := conn.Write(reply); err != nil { return } + default: + // Unknown or unhandled; try to keep reading, but bail on error + } + } + return + } + // Otherwise (e.g., VNC Auth), send failure + _, _ = conn.Write([]byte{0, 0, 0, 1}) + reason := []byte("Authentication failed") + msg := append([]byte{0, 0, 0, byte(len(reason))}, reason...) + _, _ = conn.Write(msg) + } +}