Files
deceptifeed/internal/config/config.go
Ryan Smith 8c97e05f6f feat: add proxy protocol configuration setting
This change adds a configuration option to enable Proxy Protocol (not yet implemented). Planned for TCP and SSH honeypot types. Use `<useProxyProtocol>true</useProxyProtocol>` in the XML config.
2025-05-15 16:01:23 -07:00

296 lines
8.7 KiB
Go

package config
import (
"encoding/xml"
"fmt"
"io"
"log/slog"
"os"
"path/filepath"
"regexp"
"github.com/r-smith/deceptifeed/internal/logmonitor"
"github.com/r-smith/deceptifeed/internal/logrotate"
)
// Version stores Deceptifeed's version number. This variable is set at build
// time using the `-X` option with `-ldflags` and is assigned the latest Git
// tag. Refer to the Makefile in the project root for details on how it's set.
var Version = "undefined"
// This block of constants defines the default application settings when no
// configuration file is provided.
const (
DefaultEnableHTTP = true
DefaultEnableHTTPS = true
DefaultEnableSSH = true
DefaultEnableThreatFeed = true
DefaultPortHTTP = "8080"
DefaultPortHTTPS = "8443"
DefaultPortSSH = "2222"
DefaultPortThreatFeed = "9000"
DefaultThreatExpiryHours = 336
DefaultThreatDatabasePath = "deceptifeed-database.csv"
DefaultThreatIncludePrivate = true
DefaultLogPath = "deceptifeed-log.txt"
DefaultHomePagePath = ""
DefaultCertPathHTTPS = "deceptifeed-https.crt"
DefaultKeyPathHTTPS = "deceptifeed-https.key"
DefaultKeyPathSSH = "deceptifeed-ssh.key"
DefaultBannerSSH = "SSH-2.0-OpenSSH_9.6"
)
// ServerType represents the different types of honeypot servers that can be
// deployed. Each type has its own specific handlers and behavior.
type ServerType int
const (
HTTP ServerType = iota
HTTPS
SSH
TCP
UDP
)
// String returns a string represenation of ServerType.
func (t ServerType) String() string {
return [...]string{"http", "https", "ssh", "tcp", "udp"}[t]
}
// UnmarshalXMLAttr unmarshals the XML 'type' attribute from 'server' elements
// into a ServerType.
//
// Example XML snippet:
// <server type="http"><enabled>true</enabled></server>
func (t *ServerType) UnmarshalXMLAttr(attr xml.Attr) error {
switch attr.Value {
case "http":
*t = HTTP
case "https":
*t = HTTPS
case "ssh":
*t = SSH
case "tcp":
*t = TCP
case "udp":
*t = UDP
default:
return fmt.Errorf("invalid server type: %s", attr.Value)
}
return nil
}
// Config holds the configuration settings for the application. It contains the
// logger, settings for managing a threat feed, and the collection of honeypot
// servers that are configured to run.
type Config struct {
LogPath string `xml:"defaultLogPath"`
Servers []Server `xml:"honeypotServers>server"`
ThreatFeed ThreatFeed `xml:"threatFeed"`
FilePath string `xml:"-"`
Monitor *logmonitor.Monitor `xml:"-"`
}
// Server represents a honeypot server with its relevant settings.
type Server struct {
Type ServerType `xml:"type,attr"`
Enabled bool `xml:"enabled"`
Port string `xml:"port"`
CertPath string `xml:"certPath"`
KeyPath string `xml:"keyPath"`
HomePagePath string `xml:"homePagePath"`
ErrorPagePath string `xml:"errorPagePath"`
Banner string `xml:"banner"`
Headers []string `xml:"headers>header"`
Prompts []Prompt `xml:"prompts>prompt"`
SendToThreatFeed bool `xml:"sendToThreatFeed"`
UseProxyProtocol bool `xml:"useProxyProtocol"`
Rules Rules `xml:"rules"`
SourceIPHeader string `xml:"sourceIpHeader"`
LogPath string `xml:"logPath"`
LogEnabled bool `xml:"logEnabled"`
LogFile *logrotate.File `xml:"-"`
Logger *slog.Logger `xml:"-"`
}
type Rules struct {
Include []Rule `xml:"include"`
Exclude []Rule `xml:"exclude"`
}
type Rule struct {
Target string `xml:"target,attr"`
Pattern string `xml:",chardata"`
Negate bool `xml:"negate,attr"`
}
// Prompt represents a text prompt that can be displayed to connecting clients
// when using the TCP-type honeypot server. Each prompt waits for input and
// logs the response. A Server can include multiple prompts which are displayed
// one at a time. The optional Log field gives a description when logging the
// response.
type Prompt struct {
Text string `xml:",chardata"`
Log string `xml:"log,attr"`
}
// ThreatFeed represents an optional HTTP server that serves a list of IP
// addresses observed interacting with your honeypot servers. This server
// outputs data in a format compatible with most enterprise firewalls, which
// can be configured to automatically block communication with IP addresses
// appearing in the threat feed.
type ThreatFeed struct {
Enabled bool `xml:"enabled"`
Port string `xml:"port"`
DatabasePath string `xml:"databasePath"`
ExpiryHours int `xml:"threatExpiryHours"`
IsPrivateIncluded bool `xml:"includePrivateIPs"`
ExcludeListPath string `xml:"excludeListPath"`
EnableTLS bool `xml:"enableTLS"`
CertPath string `xml:"certPath"`
KeyPath string `xml:"keyPath"`
}
// Load reads an optional XML configuration file and unmarshals its contents
// into a Config struct. Any errors encountered opening or decoding the file
// are returned. When decoding is successful, the populated Config struct is
// returned.
func Load(filename string) (*Config, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()
var config Config
absPath, err := filepath.Abs(filename)
if err != nil {
config.FilePath = filename
} else {
config.FilePath = absPath
}
xmlBytes, _ := io.ReadAll(file)
err = xml.Unmarshal(xmlBytes, &config)
if err != nil {
return nil, err
}
for i := range config.Servers {
// Use the global log path if the server log path is not specified.
if len(config.Servers[i].LogPath) == 0 {
config.Servers[i].LogPath = config.LogPath
}
// Validate regex rules.
if err := validateRegexRules(config.Servers[i].Rules); err != nil {
return nil, err
}
// Use the default SSH banner if no banner is specified.
if config.Servers[i].Type == SSH && len(config.Servers[i].Banner) == 0 {
config.Servers[i].Banner = DefaultBannerSSH
}
// Explicitly disable threat feed for UDP honeypots.
if config.Servers[i].Type == UDP {
config.Servers[i].SendToThreatFeed = false
}
}
return &config, nil
}
// validateRegexRules checks the validity of regex patterns in the rules.
func validateRegexRules(rules Rules) error {
for _, rule := range rules.Include {
if _, err := regexp.Compile(rule.Pattern); err != nil {
return fmt.Errorf("invalid regex pattern: %s", rule.Pattern)
}
}
for _, rule := range rules.Exclude {
if _, err := regexp.Compile(rule.Pattern); err != nil {
return fmt.Errorf("invalid regex pattern: %s", rule.Pattern)
}
}
return nil
}
// InitializeLoggers creates structured loggers for each server. It opens log
// files using the server's specified log path, defaulting to the global log
// path if none is provided.
func (c *Config) InitializeLoggers() error {
const maxSize = 50
c.Monitor = logmonitor.New()
openedLogFiles := make(map[string]*slog.Logger)
for i := range c.Servers {
if !c.Servers[i].Enabled {
continue
}
logPath := c.Servers[i].LogPath
// If no log path is specified or if logging is disabled, discard logs.
if len(logPath) == 0 || !c.Servers[i].LogEnabled {
c.Servers[i].Logger = slog.New(slog.DiscardHandler)
continue
}
// Check if this log path has already been opened. If so, reuse the
// logger.
if logger, exists := openedLogFiles[logPath]; exists {
c.Servers[i].Logger = logger
continue
}
// Open the specified log file.
file, err := logrotate.OpenFile(logPath, maxSize)
if err != nil {
return err
}
// Create a JSON logger with two writers: one writes to disk using file
// rotation, the other writes to a channel for live monitoring.
logger := slog.New(
slog.NewJSONHandler(
io.MultiWriter(file, c.Monitor),
&slog.HandlerOptions{
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
switch a.Key {
case slog.MessageKey, slog.LevelKey:
// Remove default 'message' and 'log level' fields.
return slog.Attr{}
case "source_ip_error":
// Remove 'source_ip_error' field if it's empty.
if len(a.Value.String()) == 0 {
return slog.Attr{}
}
}
return a
},
},
),
)
c.Servers[i].Logger = logger
c.Servers[i].LogFile = file
// Store the logger for reuse.
openedLogFiles[logPath] = logger
}
return nil
}
// CloseLogFiles closes all open log file handles for the servers. This
// function should be called when the application is shutting down.
func (c *Config) CloseLogFiles() {
for i := range c.Servers {
if c.Servers[i].LogFile != nil {
_ = c.Servers[i].LogFile.Close()
}
}
}