package app import ( "bufio" "context" "crypto/rand" "crypto/rsa" "crypto/tls" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "fmt" "io" "log" "math/big" "net" "net/http" "os" "path/filepath" "strconv" "strings" "sync" "time" "honeydany/app/dashboard" svcs "honeydany/app/services" "golang.org/x/crypto/ssh" ) // App holds runtime pieces type App struct { cfg Config logger *Logger threatIntel *ThreatIntel threatManager *dashboard.ThreatManager webTemplateManager *WebTemplateManager webServiceManager *WebServiceManager 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{} // restart channel restartCh chan 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 } // Handle POST requests (login attempts) if r.Method == http.MethodPost { if err := r.ParseForm(); err == nil { username := strings.TrimSpace(r.FormValue("username")) password := r.FormValue("password") if username != "" || password != "" { details["login_attempt"] = "true" details["username"] = username details["password"] = password bodySnippet = fmt.Sprintf("username=%s&password=%s", username, password) // Log the login attempt rec := Record{Timestamp: time.Now().UTC(), RemoteAddr: remoteIP(r.RemoteAddr), RemotePort: remotePort(r.RemoteAddr), Service: "https", Details: details, RawPayload: bodySnippet} a.logEvent(rec) // Redirect back with error (honeypot always rejects) redirectURL := fmt.Sprintf("%s?error=invalid", r.URL.Path) http.Redirect(w, r, redirectURL, http.StatusSeeOther) return } } } rec := Record{Timestamp: time.Now().UTC(), RemoteAddr: remoteIP(r.RemoteAddr), RemotePort: remotePort(r.RemoteAddr), Service: "https", Details: details, RawPayload: bodySnippet} a.logEvent(rec) // Check if template is configured if a.cfg.Web.HTTPSTemplateName != "" && a.webTemplateManager != nil { tmpl, err := a.webTemplateManager.LoadTemplate(a.cfg.Web.HTTPSTemplateName) if err == nil { // Render template with context data compatible with admin-login.html data := map[string]interface{}{ "ServiceName": "HTTPS Service", "LoginPath": r.URL.Path, "UseHTTPS": true, } w.Header().Set("Server", "nginx/1.18.0 (Ubuntu)") w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(200) if err := tmpl.Execute(w, data); err != nil { log.Printf("HTTPS template execution error: %v", err) _, _ = w.Write([]byte("Welcome (TLS)\n")) } return } else { log.Printf("HTTPS template load error: %v", err) } } // Default response if no template or template failed 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}} 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() } // 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() } // parseCreds attempts to extract a username and password from arbitrary SSH-like payloads. // This is heuristic-based and targets common patterns like: // - "login: root\npassword: toor" // - "user=root pass=toor" // - "username root password toor" func parseCreds(data []byte) (string, string) { sOrig := string(data) sLower := strings.ToLower(sOrig) // Try common keys in decreasing specificity userKeys := []string{"username", "user", "login"} passKeys := []string{"password", "passwd", "pass", "pwd"} 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) } } // 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 } // 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] } 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 } 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) // Initialize threat manager dbPath := "app.db" if cfg.LogPath != "" { dbPath = filepath.Join(filepath.Dir(cfg.LogPath), "app.db") } tm, err := dashboard.NewThreatManager(dbPath) if err != nil { log.Printf("Failed to initialize threat manager: %v", err) tm = nil // Continue without threat manager if it fails } // Initialize web template manager configPath := "config.json" // Default path, should be passed from main if LastConfigPath != "" { configPath = LastConfigPath } webTemplateManager := NewWebTemplateManager(configPath) // Initialize web service manager webServiceManager := NewWebServiceManager(webTemplateManager, func(record Record) { // Use the same logging function as other services if l != nil { _ = l.Log(record) } // Also process with threat intelligence if ti != nil && record.RemoteAddr != "" && !IsPrivateIP(record.RemoteAddr) { ti.RecordActivity(record) } // Process with threat manager if tm != nil && record.RemoteAddr != "" && !IsPrivateIP(record.RemoteAddr) { details := make(map[string]interface{}) for k, v := range record.Details { details[k] = v } tm.ProcessHoneypotRecord(record.Timestamp, record.RemoteAddr, record.RemotePort, record.Service, details, record.RawPayload) } }) // Root context for the App used for shutdown signalling ctx, cancel := context.WithCancel(context.Background()) a := &App{ cfg: cfg, logger: l, threatIntel: ti, threatManager: tm, webTemplateManager: webTemplateManager, webServiceManager: webServiceManager, ctx: ctx, cancel: cancel, conns: make(map[net.Conn]struct{}), restartCh: make(chan struct{}, 1), } // Start periodic threat analysis tasks if tm != nil { tm.RunPeriodicTasks(a.ctx) } 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 { a.wg.Add(1) go func() { defer a.wg.Done() a.startTCPService("ftp", a.cfg.Ports.FTP, svcs.NewFTPHandler(a.svcLogger())) }() } if a.cfg.Services.SMTP { a.wg.Add(1) go func() { defer a.wg.Done() a.startTCPService("smtp", a.cfg.Ports.SMTP, svcs.NewSMTPHandler(a.svcLogger())) }() } if a.cfg.Services.IMAP { a.wg.Add(1) go func() { defer a.wg.Done() a.startTCPService("imap", a.cfg.Ports.IMAP, svcs.NewIMAPHandler(a.svcLogger())) }() } if a.cfg.Services.Telnet { a.wg.Add(1) go func() { defer a.wg.Done() a.startTCPService("telnet", a.cfg.Ports.Telnet, svcs.NewTelnetHandler(a.svcLogger())) }() } if a.cfg.Services.MySQL { a.wg.Add(1) go func() { defer a.wg.Done() a.startTCPService("mysql", a.cfg.Ports.MySQL, svcs.NewMySQLHandler(a.svcLogger())) }() } if a.cfg.Services.PostgreSQL { a.wg.Add(1) go func() { defer a.wg.Done() a.startTCPService("postgresql", a.cfg.Ports.PostgreSQL, svcs.NewPostgreSQLHandler(a.svcLogger())) }() } if a.cfg.Services.MongoDB { a.wg.Add(1) go func() { defer a.wg.Done() a.startTCPService("mongodb", a.cfg.Ports.MongoDB, svcs.NewMongoDBHandler(a.svcLogger())) }() } if a.cfg.Services.RDP { a.wg.Add(1) go func() { defer a.wg.Done() a.startTCPService("rdp", a.cfg.Ports.RDP, svcs.NewRDPHandler(a.svcLogger())) }() } if a.cfg.Services.SMB { a.wg.Add(1) go func() { defer a.wg.Done() 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.NewVNCHandler(a.svcLogger())) }() } // start web dashboard if enabled if a.cfg.Web.Enabled { a.wg.Add(1) go func() { defer a.wg.Done() a.startWeb() }() } // Start custom web services (only generic services) for _, genericService := range a.cfg.WebServices.Generic { if genericService.Enabled { if err := a.webServiceManager.StartWebService(genericService); err != nil { log.Printf("Failed to start web service %s: %v", genericService.Name, err) } } } <-ctx.Done() a.Shutdown() return nil } func (a *App) Restart() { select { case a.restartCh <- struct{}{}: default: } } func (a *App) RestartChan() <-chan struct{} { return a.restartCh } 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() } // Close threat manager if a.threatManager != nil { _ = a.threatManager.Close() } _ = 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() } } // 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() } // removeConn removes a connection from tracking func (a *App) removeConn(c net.Conn) { 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() } } // 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) } // Process with threat manager for advanced analysis if a.threatManager != nil && r.RemoteAddr != "" && !IsPrivateIP(r.RemoteAddr) { // Convert map[string]string to map[string]interface{} details := make(map[string]interface{}) for k, v := range r.Details { details[k] = v } a.threatManager.ProcessHoneypotRecord(r.Timestamp, r.RemoteAddr, r.RemotePort, r.Service, details, r.RawPayload) } } // 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, }) } } // 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 } // Handle POST requests (login attempts) if r.Method == http.MethodPost { if err := r.ParseForm(); err == nil { username := strings.TrimSpace(r.FormValue("username")) password := r.FormValue("password") if username != "" || password != "" { details["login_attempt"] = "true" details["username"] = username details["password"] = password bodySnippet = fmt.Sprintf("username=%s&password=%s", username, password) // Log the login attempt rec := Record{Timestamp: time.Now().UTC(), RemoteAddr: remoteIP(r.RemoteAddr), RemotePort: remotePort(r.RemoteAddr), Service: "http", Details: details, RawPayload: bodySnippet} a.logEvent(rec) // Redirect back with error (honeypot always rejects) redirectURL := fmt.Sprintf("%s?error=invalid", r.URL.Path) http.Redirect(w, r, redirectURL, http.StatusSeeOther) return } } } rec := Record{Timestamp: time.Now().UTC(), RemoteAddr: remoteIP(r.RemoteAddr), RemotePort: remotePort(r.RemoteAddr), Service: "http", Details: details, RawPayload: bodySnippet} a.logEvent(rec) // Check if template is configured if a.cfg.Web.HTTPTemplateName != "" && a.webTemplateManager != nil { tmpl, err := a.webTemplateManager.LoadTemplate(a.cfg.Web.HTTPTemplateName) if err == nil { // Render template with context data compatible with admin-login.html data := map[string]interface{}{ "ServiceName": "HTTP Service", "LoginPath": r.URL.Path, "UseHTTPS": false, } w.Header().Set("Server", "Apache/2.4.41 (Ubuntu)") w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(200) if err := tmpl.Execute(w, data); err != nil { log.Printf("HTTP template execution error: %v", err) _, _ = w.Write([]byte("Welcome\n")) } return } else { log.Printf("HTTP template load error: %v", err) } } // Default response if no template or template failed 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") } // remote helpers func remoteIP(addr string) string { h, _, _ := net.SplitHostPort(addr) return h } func remotePort(addr string) string { _, 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) }() // 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 } }() 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) signer, err := a.getSSHSigner() if err != nil { log.Printf("[SSH] host key error: %v", err) return } 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) _ = 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() } // 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 } // 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) } // 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) } // 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 } // 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), }) } // 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}) } } 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}) } 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()}) }