mirror of
https://github.com/r-smith/deceptifeed.git
synced 2025-11-02 13:13:37 +00:00
Compare commits
14 Commits
dc06d64b5b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0d8651c7f | ||
|
|
0def1728ee | ||
|
|
b7b8aa6110 | ||
|
|
fd02995f52 | ||
|
|
4ab8f2dfee | ||
|
|
cefc9952f0 | ||
|
|
5c91ae0e4f | ||
|
|
363c429a1e | ||
|
|
8c97e05f6f | ||
|
|
153191f6c5 | ||
|
|
c83ebcc342 | ||
|
|
a9dcc759f7 | ||
|
|
f9d7b767bc | ||
|
|
375da6eeac |
@@ -3,7 +3,7 @@ FROM golang:latest AS build-stage
|
||||
WORKDIR /build
|
||||
COPY . .
|
||||
RUN git update-index -q --refresh
|
||||
RUN make
|
||||
RUN make clean build
|
||||
|
||||
FROM alpine:latest
|
||||
RUN apk add --no-cache tzdata
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
<port>2222</port>
|
||||
<logEnabled>true</logEnabled>
|
||||
<sendToThreatFeed>true</sendToThreatFeed>
|
||||
<useProxyProtocol>false</useProxyProtocol>
|
||||
<keyPath>/opt/deceptifeed/certs/ssh-key.pem</keyPath>
|
||||
<banner>SSH-2.0-OpenSSH_9.6</banner>
|
||||
</server>
|
||||
@@ -38,6 +39,7 @@
|
||||
<port>8080</port>
|
||||
<logEnabled>true</logEnabled>
|
||||
<sendToThreatFeed>true</sendToThreatFeed>
|
||||
<sourceIpHeader></sourceIpHeader>
|
||||
<rules>
|
||||
<!-- Update the threat feed if any of the following rules match: -->
|
||||
<include target="path" negate="true">(?i)^(/|/index\.html|/favicon\.ico|/robots\.txt|/sitemap\.xml|/\.well-known/\w+\.txt)$</include>
|
||||
@@ -55,6 +57,7 @@
|
||||
<port>8443</port>
|
||||
<logEnabled>true</logEnabled>
|
||||
<sendToThreatFeed>true</sendToThreatFeed>
|
||||
<sourceIpHeader></sourceIpHeader>
|
||||
<certPath>/opt/deceptifeed/certs/https-cert.pem</certPath>
|
||||
<keyPath>/opt/deceptifeed/certs/https-key.pem</keyPath>
|
||||
<rules>
|
||||
@@ -75,6 +78,7 @@
|
||||
<port>2323</port>
|
||||
<logEnabled>true</logEnabled>
|
||||
<sendToThreatFeed>true</sendToThreatFeed>
|
||||
<useProxyProtocol>false</useProxyProtocol>
|
||||
<banner>\nUser Access Verification\n\n</banner>
|
||||
<prompts>
|
||||
<prompt log="username">Username: </prompt>
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
<port>2222</port>
|
||||
<logEnabled>true</logEnabled>
|
||||
<sendToThreatFeed>true</sendToThreatFeed>
|
||||
<useProxyProtocol>false</useProxyProtocol>
|
||||
<keyPath>key-ssh-private.pem</keyPath>
|
||||
<banner>SSH-2.0-OpenSSH_9.6</banner>
|
||||
</server>
|
||||
@@ -38,6 +39,7 @@
|
||||
<port>8080</port>
|
||||
<logEnabled>true</logEnabled>
|
||||
<sendToThreatFeed>true</sendToThreatFeed>
|
||||
<sourceIpHeader></sourceIpHeader>
|
||||
<rules>
|
||||
<!-- Update the threat feed if any of the following rules match: -->
|
||||
<include target="path" negate="true">(?i)^(/|/index\.html|/favicon\.ico|/robots\.txt|/sitemap\.xml|/\.well-known/\w+\.txt)$</include>
|
||||
@@ -55,6 +57,7 @@
|
||||
<port>8443</port>
|
||||
<logEnabled>true</logEnabled>
|
||||
<sendToThreatFeed>true</sendToThreatFeed>
|
||||
<sourceIpHeader></sourceIpHeader>
|
||||
<certPath>key-https-public.pem</certPath>
|
||||
<keyPath>key-https-private.pem</keyPath>
|
||||
<rules>
|
||||
@@ -75,6 +78,7 @@
|
||||
<port>2323</port>
|
||||
<logEnabled>true</logEnabled>
|
||||
<sendToThreatFeed>true</sendToThreatFeed>
|
||||
<useProxyProtocol>false</useProxyProtocol>
|
||||
<banner>\nUser Access Verification\n\n</banner>
|
||||
<prompts>
|
||||
<prompt log="username">Username: </prompt>
|
||||
|
||||
6
go.mod
6
go.mod
@@ -3,8 +3,8 @@ module github.com/r-smith/deceptifeed
|
||||
go 1.24
|
||||
|
||||
require (
|
||||
golang.org/x/crypto v0.37.0
|
||||
golang.org/x/net v0.39.0
|
||||
golang.org/x/crypto v0.38.0
|
||||
golang.org/x/net v0.40.0
|
||||
)
|
||||
|
||||
require golang.org/x/sys v0.32.0 // indirect
|
||||
require golang.org/x/sys v0.33.0 // indirect
|
||||
|
||||
16
go.sum
16
go.sum
@@ -1,8 +1,8 @@
|
||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
|
||||
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
|
||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
|
||||
@@ -104,6 +104,7 @@ type Server struct {
|
||||
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"`
|
||||
@@ -221,7 +222,6 @@ func validateRegexRules(rules Rules) error {
|
||||
func (c *Config) InitializeLoggers() error {
|
||||
const maxSize = 50
|
||||
c.Monitor = logmonitor.New()
|
||||
|
||||
openedLogFiles := make(map[string]*slog.Logger)
|
||||
|
||||
for i := range c.Servers {
|
||||
@@ -231,14 +231,14 @@ func (c *Config) InitializeLoggers() error {
|
||||
|
||||
logPath := c.Servers[i].LogPath
|
||||
|
||||
// If no log path is specified or if logging is disabled, discard logs.
|
||||
// If no log path is specified or logging is disabled, write to a log
|
||||
// monitor for live monitoring. No log data is written to disk.
|
||||
if len(logPath) == 0 || !c.Servers[i].LogEnabled {
|
||||
c.Servers[i].Logger = slog.New(slog.DiscardHandler)
|
||||
c.Servers[i].Logger = slog.New(slog.NewJSONHandler(c.Monitor, nil))
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this log path has already been opened. If so, reuse the
|
||||
// logger.
|
||||
// Reuse the logger if this log path has already been opened.
|
||||
if logger, exists := openedLogFiles[logPath]; exists {
|
||||
c.Servers[i].Logger = logger
|
||||
continue
|
||||
@@ -257,9 +257,15 @@ func (c *Config) InitializeLoggers() error {
|
||||
io.MultiWriter(file, c.Monitor),
|
||||
&slog.HandlerOptions{
|
||||
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
|
||||
// Remove 'message' and 'log level' fields from output.
|
||||
if a.Key == slog.MessageKey || a.Key == slog.LevelKey {
|
||||
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
|
||||
},
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
@@ -148,65 +149,93 @@ func listenHTTPS(cfg *config.Server, response *responseConfig) {
|
||||
// based on the honeypot configuration.
|
||||
func handleConnection(cfg *config.Server, customHeaders map[string]string, response *responseConfig) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Log details of the incoming HTTP request.
|
||||
dst_ip, dst_port := getLocalAddr(r)
|
||||
src_ip, _, _ := net.SplitHostPort(r.RemoteAddr)
|
||||
username, password, isAuth := r.BasicAuth()
|
||||
if isAuth {
|
||||
cfg.Logger.LogAttrs(context.Background(), slog.LevelInfo, "",
|
||||
slog.String("event_type", "http"),
|
||||
slog.String("source_ip", src_ip),
|
||||
slog.String("server_ip", dst_ip),
|
||||
slog.String("server_port", dst_port),
|
||||
slog.String("server_name", config.GetHostname()),
|
||||
slog.Group("event_details",
|
||||
slog.String("method", r.Method),
|
||||
slog.String("path", r.URL.Path),
|
||||
slog.String("query", r.URL.RawQuery),
|
||||
slog.String("user_agent", r.UserAgent()),
|
||||
slog.String("protocol", r.Proto),
|
||||
slog.String("host", r.Host),
|
||||
slog.Group("basic_auth",
|
||||
slog.String("username", username),
|
||||
slog.String("password", password),
|
||||
),
|
||||
slog.Any("headers", flattenHeaders(r.Header)),
|
||||
),
|
||||
// Record connection details.
|
||||
dstIP, dstPort := getLocalAddr(r)
|
||||
srcIP, _, _ := net.SplitHostPort(r.RemoteAddr)
|
||||
var remIP string
|
||||
var parsed bool
|
||||
var errMsg string
|
||||
|
||||
// If a custom source IP header is configured, set remIP to the remote
|
||||
// IP and extract the client IP from the header into srcIP.
|
||||
if len(cfg.SourceIPHeader) > 0 {
|
||||
// If the custom header is missing, invalid, contains multiple IPs,
|
||||
// or if there a multiple headers with the same name, parsing will
|
||||
// fail, and srcIP will fallback to the original connecting IP.
|
||||
remIP = srcIP
|
||||
header := r.Header[cfg.SourceIPHeader]
|
||||
switch len(header) {
|
||||
case 0:
|
||||
errMsg = "missing header " + cfg.SourceIPHeader
|
||||
case 1:
|
||||
v := header[0]
|
||||
if _, err := netip.ParseAddr(v); err != nil {
|
||||
if strings.Contains(v, ",") {
|
||||
errMsg = "multiple values in header " + cfg.SourceIPHeader
|
||||
} else {
|
||||
errMsg = "invalid IP in header " + cfg.SourceIPHeader
|
||||
}
|
||||
} else {
|
||||
parsed = true
|
||||
srcIP = v
|
||||
}
|
||||
default:
|
||||
errMsg = "multiple instances of header " + cfg.SourceIPHeader
|
||||
}
|
||||
}
|
||||
|
||||
// Log the connection details.
|
||||
logData := make([]slog.Attr, 0, 9)
|
||||
logData = append(logData,
|
||||
slog.String("event_type", "http"),
|
||||
slog.String("source_ip", srcIP),
|
||||
)
|
||||
if len(cfg.SourceIPHeader) > 0 {
|
||||
logData = append(logData,
|
||||
slog.Bool("source_ip_parsed", parsed),
|
||||
slog.String("source_ip_error", errMsg),
|
||||
slog.String("remote_ip", remIP),
|
||||
)
|
||||
} else {
|
||||
cfg.Logger.LogAttrs(context.Background(), slog.LevelInfo, "",
|
||||
slog.String("event_type", "http"),
|
||||
slog.String("source_ip", src_ip),
|
||||
slog.String("server_ip", dst_ip),
|
||||
slog.String("server_port", dst_port),
|
||||
slog.String("server_name", config.GetHostname()),
|
||||
slog.Group("event_details",
|
||||
slog.String("method", r.Method),
|
||||
slog.String("path", r.URL.Path),
|
||||
slog.String("query", r.URL.RawQuery),
|
||||
slog.String("user_agent", r.UserAgent()),
|
||||
slog.String("protocol", r.Proto),
|
||||
slog.String("host", r.Host),
|
||||
slog.Any("headers", flattenHeaders(r.Header)),
|
||||
}
|
||||
logData = append(logData,
|
||||
slog.String("server_ip", dstIP),
|
||||
slog.String("server_port", dstPort),
|
||||
slog.String("server_name", config.GetHostname()),
|
||||
)
|
||||
|
||||
// Log the HTTP request information.
|
||||
eventDetails := []any{
|
||||
slog.String("method", r.Method),
|
||||
slog.String("path", r.URL.Path),
|
||||
slog.String("query", r.URL.RawQuery),
|
||||
slog.String("user_agent", r.UserAgent()),
|
||||
slog.String("protocol", r.Proto),
|
||||
slog.String("host", r.Host),
|
||||
slog.Any("headers", flattenHeaders(r.Header)),
|
||||
}
|
||||
|
||||
// If the request includes a "basic" Authorization header, decode and
|
||||
// log the credentials.
|
||||
if username, password, ok := r.BasicAuth(); ok {
|
||||
eventDetails = append(eventDetails,
|
||||
slog.Group("basic_auth",
|
||||
slog.String("username", username),
|
||||
slog.String("password", password),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Print a simplified version of the request to the console.
|
||||
fmt.Printf("[HTTP] %s %s %s %s\n", src_ip, r.Method, r.URL.Path, r.URL.RawQuery)
|
||||
// Combine log data and write the final log entry.
|
||||
logData = append(logData, slog.Group("event_details", eventDetails...))
|
||||
cfg.Logger.LogAttrs(context.Background(), slog.LevelInfo, "", logData...)
|
||||
|
||||
// Update the threat feed with the source IP address from the request.
|
||||
// If the configuration specifies an HTTP header to be used for the
|
||||
// source IP, retrieve the header value and use it instead of the
|
||||
// connecting IP.
|
||||
// Print a simplified version of the request to the console.
|
||||
fmt.Printf("[HTTP] %s %s %s %s\n", srcIP, r.Method, r.URL.Path, r.URL.RawQuery)
|
||||
|
||||
// Update the threat feed with srcIP. If Proxy Protocol is enabled, srcIP
|
||||
// is taken from the proxy header. Otherwise, it's the connecting IP.
|
||||
if shouldUpdateThreatFeed(cfg, r) {
|
||||
src := src_ip
|
||||
if len(cfg.SourceIPHeader) > 0 {
|
||||
if header := r.Header.Get(cfg.SourceIPHeader); len(header) > 0 {
|
||||
src = header
|
||||
}
|
||||
}
|
||||
threatfeed.Update(src)
|
||||
threatfeed.Update(srcIP)
|
||||
}
|
||||
|
||||
// Apply optional custom HTTP response headers.
|
||||
|
||||
178
internal/proxyproto/proxyproto.go
Normal file
178
internal/proxyproto/proxyproto.go
Normal file
@@ -0,0 +1,178 @@
|
||||
package proxyproto
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// v1Signature is the byte representation of "PROXY ", which is the start of a
|
||||
// Proxy Protocol v1 header.
|
||||
var v1Signature = []byte{
|
||||
0x50, 0x52, 0x4F, 0x58, 0x59, 0x20,
|
||||
}
|
||||
|
||||
// v2Signature is a 12-byte constant which is the start of a Proxy Protocol v2
|
||||
// header.
|
||||
var v2Signature = []byte{
|
||||
0x0D, 0x0A, 0x0D, 0x0A,
|
||||
0x00, 0x0D, 0x0A, 0x51,
|
||||
0x55, 0x49, 0x54, 0x0A,
|
||||
}
|
||||
|
||||
// serverTimeout defines the duration after which connected clients are
|
||||
// automatically disconnected, set to 2 seconds.
|
||||
const serverTimeout = 2 * time.Second
|
||||
|
||||
// ReadHeader reads and parses a Proxy Protocol v1 or v2 header from conn. It
|
||||
// extracts and returns the client IP address from the header. It sets a
|
||||
// 2-second deadline on conn. If parsing fails, it returns an error. Callers
|
||||
// should reset the deadline after this function returns to extend the timeout.
|
||||
func ReadHeader(conn net.Conn) (string, error) {
|
||||
conn.SetDeadline(time.Now().Add(serverTimeout))
|
||||
|
||||
reader := bufio.NewReader(conn)
|
||||
peek, err := reader.Peek(12)
|
||||
if err != nil {
|
||||
return "", errors.New("failed to read proxy header data")
|
||||
}
|
||||
|
||||
var clientIP string
|
||||
|
||||
// Determine the Proxy Protocol version and parse accordingly.
|
||||
if bytes.Equal(peek, v2Signature) {
|
||||
// Proxy Protocol version 2.
|
||||
clientIP, err = parseVersion2(reader)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("proxy protocol v2: %w", err)
|
||||
}
|
||||
} else if bytes.HasPrefix(peek, v1Signature) {
|
||||
// Proxy Protocol version 1.
|
||||
clientIP, err = parseVersion1(reader)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("proxy protocol v1: %w", err)
|
||||
}
|
||||
} else {
|
||||
// Not a Proxy Protocol header.
|
||||
return "", errors.New("invalid or missing proxy protocol header")
|
||||
}
|
||||
|
||||
// Ensure the header data was provided by a private IP address.
|
||||
host, _, _ := net.SplitHostPort(conn.RemoteAddr().String())
|
||||
if ip, err := netip.ParseAddr(host); err != nil || (!ip.IsPrivate() && !ip.IsLoopback()) {
|
||||
return "", errors.New("proxy connection must originate from a private IP address")
|
||||
}
|
||||
|
||||
return clientIP, nil
|
||||
}
|
||||
|
||||
// parseVersion1 reads and parses a Proxy Protocol vesion 1 text header and
|
||||
// returns the extracted source IP address.
|
||||
func parseVersion1(r *bufio.Reader) (string, error) {
|
||||
// Proxy Protocol v1 ends with a CRLF (\r\n) and contains no more than 108
|
||||
// bytes (including the CRLF). Read up to the newline. The presence of a
|
||||
// carriage return before the newline is not validated.
|
||||
buf := make([]byte, 0, 108)
|
||||
for {
|
||||
b, err := r.ReadByte()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("can't read header: %w", err)
|
||||
}
|
||||
buf = append(buf, b)
|
||||
if b == '\n' {
|
||||
break
|
||||
}
|
||||
if len(buf) == 108 {
|
||||
return "", errors.New("invalid header")
|
||||
}
|
||||
}
|
||||
|
||||
// Split into space-delimited parts. When address information is provided,
|
||||
// this should be exactly 6 parts. Other formats are not supported.
|
||||
parts := strings.Fields(string(buf))
|
||||
if len(parts) != 6 {
|
||||
return "", errors.New("invalid or unsupported format")
|
||||
}
|
||||
|
||||
// Read the protocol part and validate the address part. Protocols other
|
||||
// than TCP4 and TCP6 are not supported by this implementation.
|
||||
switch parts[1] {
|
||||
case "TCP4":
|
||||
// Parse and validate as an IPv4 address.
|
||||
if ip, err := netip.ParseAddr(parts[2]); err != nil || !ip.Is4() || !ip.IsValid() {
|
||||
return "", errors.New("invalid ipv4 source address")
|
||||
}
|
||||
case "TCP6":
|
||||
// Parse and validate as an IPv6 address.
|
||||
if ip, err := netip.ParseAddr(parts[2]); err != nil || !ip.Is6() || !ip.IsValid() {
|
||||
return "", errors.New("invalid ipv6 source address")
|
||||
}
|
||||
default:
|
||||
return "", errors.New("invalid or unsupported proxied protocol")
|
||||
}
|
||||
|
||||
// Return the IP address part.
|
||||
return parts[2], nil
|
||||
}
|
||||
|
||||
// parseVersion2 reads and parses a Proxy Protocol vesion 2 binary header and
|
||||
// returns the extracted source IP address.
|
||||
func parseVersion2(r *bufio.Reader) (string, error) {
|
||||
// Read the first 16 bytes into a buffer. The first 12 bytes is the Proxy
|
||||
// Protocol v2 signature. Byte 13 is the protocol version and command. Byte
|
||||
// 14 is the transport protocol and address family. Bytes 15-16 is the
|
||||
// length of the address data.
|
||||
header := make([]byte, 16)
|
||||
if _, err := io.ReadFull(r, header); err != nil {
|
||||
return "", fmt.Errorf("can't read header: %w", err)
|
||||
}
|
||||
|
||||
// Byte 13 must be 0x21. The upper four bits represent the proxy protocol
|
||||
// version, which must be 0x2. The lower four bits specify the command -
|
||||
// 0x1 (PROXY) is the only supported command in this implementation.
|
||||
if header[12] != 0x21 {
|
||||
return "", errors.New("unsupported proxy command or version data")
|
||||
}
|
||||
|
||||
// Read bytes 15-16, which specify the length (in bytes) of the address
|
||||
// data in big-endian format. The address data includes source/destination
|
||||
// IPs and ports. Read the specified number of bytes into a buffer. The
|
||||
// length may indicate that additional bytes are part of the header beyond
|
||||
// the address data. These are Type-Length-Value (TLV) vectors, which are
|
||||
// read, but ignored by this implementation.
|
||||
addresses := make([]byte, binary.BigEndian.Uint16(header[14:16]))
|
||||
if _, err := io.ReadFull(r, addresses); err != nil {
|
||||
return "", fmt.Errorf("can't read address information: %w", err)
|
||||
}
|
||||
|
||||
// Byte 14 is the transport protocol and address family. Only TCP/UDP
|
||||
// over IPv4 and IPv6 are supported in this implementation.
|
||||
addrType := header[13]
|
||||
|
||||
// Extract, parse, validate, and return the source IP address.
|
||||
// TCP over IPv4 = 0x11, UDP over IPv4 = 0x12.
|
||||
if (addrType == 0x11 || addrType == 0x12) && len(addresses) >= 12 {
|
||||
ip, ok := netip.AddrFromSlice(addresses[0:4])
|
||||
if !ok || !ip.IsValid() {
|
||||
return "", errors.New("invalid ipv4 source address")
|
||||
}
|
||||
return ip.String(), nil
|
||||
}
|
||||
// TCP over IPv6 = 0x21, UDP over IPv6 = 0x22.
|
||||
if (addrType == 0x21 || addrType == 0x22) && len(addresses) >= 36 {
|
||||
ip, ok := netip.AddrFromSlice(addresses[0:16])
|
||||
if !ok || !ip.IsValid() {
|
||||
return "", errors.New("invalid ipv6 source address")
|
||||
}
|
||||
return ip.String(), nil
|
||||
}
|
||||
|
||||
return "", errors.New("unsupported transport protocol or address family")
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/r-smith/deceptifeed/internal/config"
|
||||
"github.com/r-smith/deceptifeed/internal/proxyproto"
|
||||
"github.com/r-smith/deceptifeed/internal/threatfeed"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
@@ -62,39 +63,6 @@ func Start(cfg *config.Server) {
|
||||
return nil, fmt.Errorf("permission denied")
|
||||
}
|
||||
|
||||
// Define the password authentication callback function.
|
||||
sshConfig.PasswordCallback = func(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {
|
||||
// Log the the username and password submitted by the client.
|
||||
dst_ip, dst_port, _ := net.SplitHostPort(conn.LocalAddr().String())
|
||||
src_ip, _, _ := net.SplitHostPort(conn.RemoteAddr().String())
|
||||
cfg.Logger.LogAttrs(context.Background(), slog.LevelInfo, "",
|
||||
slog.String("event_type", "ssh"),
|
||||
slog.String("source_ip", src_ip),
|
||||
slog.String("server_ip", dst_ip),
|
||||
slog.String("server_port", dst_port),
|
||||
slog.String("server_name", config.GetHostname()),
|
||||
slog.Group("event_details",
|
||||
slog.String("username", conn.User()),
|
||||
slog.String("password", string(password)),
|
||||
slog.String("ssh_client", string(conn.ClientVersion())),
|
||||
),
|
||||
)
|
||||
|
||||
// Print a simplified version of the request to the console.
|
||||
fmt.Printf("[SSH] %s Username: %q Password: %q\n", src_ip, conn.User(), string(password))
|
||||
|
||||
// Update the threat feed with the source IP address from the request.
|
||||
if cfg.SendToThreatFeed {
|
||||
threatfeed.Update(src_ip)
|
||||
}
|
||||
|
||||
// Insert fixed delay to mimic PAM.
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// Reject the authentication request.
|
||||
return nil, fmt.Errorf("invalid username or password")
|
||||
}
|
||||
|
||||
// Start the SSH server.
|
||||
listener, err := net.Listen("tcp", ":"+cfg.Port)
|
||||
if err != nil {
|
||||
@@ -110,21 +78,87 @@ func Start(cfg *config.Server) {
|
||||
continue
|
||||
}
|
||||
|
||||
go handleConnection(conn, sshConfig)
|
||||
go handleConnection(conn, sshConfig, cfg)
|
||||
}
|
||||
}
|
||||
|
||||
// handleConnection manages incoming SSH client connections. It performs the
|
||||
// handshake and handles authentication callbacks.
|
||||
func handleConnection(conn net.Conn, config *ssh.ServerConfig) {
|
||||
func handleConnection(conn net.Conn, sshConfig *ssh.ServerConfig, cfg *config.Server) {
|
||||
defer conn.Close()
|
||||
|
||||
// Record connection details.
|
||||
dstIP, dstPort, _ := net.SplitHostPort(conn.LocalAddr().String())
|
||||
srcIP, _, _ := net.SplitHostPort(conn.RemoteAddr().String())
|
||||
var remIP string
|
||||
var errMsg string
|
||||
var parsed bool
|
||||
|
||||
// If Proxy Protocol is enabled, set remIP to the remote IP and extract the
|
||||
// client IP from the proxy header into srcIP.
|
||||
if cfg.UseProxyProtocol {
|
||||
remIP = srcIP
|
||||
if clientIP, err := proxyproto.ReadHeader(conn); err != nil {
|
||||
errMsg = err.Error()
|
||||
} else {
|
||||
parsed = true
|
||||
srcIP = clientIP
|
||||
}
|
||||
}
|
||||
|
||||
// Set a connection deadline.
|
||||
_ = conn.SetDeadline(time.Now().Add(serverTimeout))
|
||||
|
||||
// Set the password authentication callback function. This function is
|
||||
// called after a successful SSH handshake. It logs the credentials,
|
||||
// updates the threat feed, then responds to the client that auth failed.
|
||||
sshConfig.PasswordCallback = func(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {
|
||||
// Log the authentication attempt.
|
||||
logData := make([]slog.Attr, 0, 9)
|
||||
logData = append(logData,
|
||||
slog.String("event_type", "ssh"),
|
||||
slog.String("source_ip", srcIP),
|
||||
)
|
||||
if cfg.UseProxyProtocol {
|
||||
logData = append(logData,
|
||||
slog.Bool("source_ip_parsed", parsed),
|
||||
slog.String("source_ip_error", errMsg),
|
||||
slog.String("remote_ip", remIP),
|
||||
)
|
||||
}
|
||||
logData = append(logData,
|
||||
slog.String("server_ip", dstIP),
|
||||
slog.String("server_port", dstPort),
|
||||
slog.String("server_name", config.GetHostname()),
|
||||
slog.Group("event_details",
|
||||
slog.String("username", conn.User()),
|
||||
slog.String("password", string(password)),
|
||||
slog.String("ssh_client", string(conn.ClientVersion())),
|
||||
),
|
||||
)
|
||||
cfg.Logger.LogAttrs(context.Background(), slog.LevelInfo, "", logData...)
|
||||
|
||||
// Print a simplified version of the request to the console.
|
||||
fmt.Printf("[SSH] %s Username: %q Password: %q\n", srcIP, conn.User(), string(password))
|
||||
|
||||
// Update the threat feed with srcIP. If Proxy Protocol is enabled,
|
||||
// srcIP is from the proxy header. Otherwise, it's the connecting IP.
|
||||
if cfg.SendToThreatFeed {
|
||||
threatfeed.Update(srcIP)
|
||||
}
|
||||
|
||||
// Insert a fixed delay between authentication attempts.
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// Reject the authentication request.
|
||||
return nil, fmt.Errorf("invalid username or password")
|
||||
}
|
||||
|
||||
// Perform handshake and authentication. Authentication callbacks are
|
||||
// defined in the SSH server configuration. Since authentication requests
|
||||
// are always rejected, this function will consistently return an error,
|
||||
// and no further connection handling is necessary.
|
||||
sshConn, _, _, err := ssh.NewServerConn(conn, config)
|
||||
sshConn, _, _, err := ssh.NewServerConn(conn, sshConfig)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/r-smith/deceptifeed/internal/config"
|
||||
"github.com/r-smith/deceptifeed/internal/proxyproto"
|
||||
"github.com/r-smith/deceptifeed/internal/threatfeed"
|
||||
)
|
||||
|
||||
@@ -31,6 +32,13 @@ func Start(cfg *config.Server) {
|
||||
}
|
||||
defer listener.Close()
|
||||
|
||||
// Replace occurrences of "\n" with "\r\n". The configuration file uses
|
||||
// "\n", but CRLF is expected for TCP protocols.
|
||||
cfg.Banner = strings.ReplaceAll(cfg.Banner, "\\n", "\r\n")
|
||||
for i := range cfg.Prompts {
|
||||
cfg.Prompts[i].Text = strings.ReplaceAll(cfg.Prompts[i].Text, "\\n", "\r\n")
|
||||
}
|
||||
|
||||
// Listen for and accept incoming connections.
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
@@ -48,21 +56,39 @@ func Start(cfg *config.Server) {
|
||||
// client interaction.
|
||||
func handleConnection(conn net.Conn, cfg *config.Server) {
|
||||
defer conn.Close()
|
||||
_ = conn.SetDeadline(time.Now().Add(serverTimeout))
|
||||
|
||||
// Print an optional banner. Replace any occurrences of the newline escape
|
||||
// sequence "\\n" with "\r\n" (carriage return, line feed), used by
|
||||
// protocols such as Telnet and SMTP.
|
||||
if len(cfg.Banner) > 0 {
|
||||
_, _ = conn.Write([]byte(strings.ReplaceAll(cfg.Banner, "\\n", "\r\n")))
|
||||
// Record connection details.
|
||||
dstIP, dstPort, _ := net.SplitHostPort(conn.LocalAddr().String())
|
||||
srcIP, _, _ := net.SplitHostPort(conn.RemoteAddr().String())
|
||||
var remIP string
|
||||
var parsed bool
|
||||
var errMsg string
|
||||
|
||||
// If Proxy Protocol is enabled, set remIP to the remote IP and extract the
|
||||
// client IP from the proxy header into srcIP.
|
||||
if cfg.UseProxyProtocol {
|
||||
remIP = srcIP
|
||||
if clientIP, err := proxyproto.ReadHeader(conn); err != nil {
|
||||
errMsg = err.Error()
|
||||
} else {
|
||||
parsed = true
|
||||
srcIP = clientIP
|
||||
}
|
||||
}
|
||||
|
||||
// Present the prompts from the server configuration to the connected
|
||||
// client and record their responses.
|
||||
// Set a connection deadline.
|
||||
_ = conn.SetDeadline(time.Now().Add(serverTimeout))
|
||||
|
||||
// Display initial banner to the client if configured.
|
||||
if len(cfg.Banner) > 0 {
|
||||
_, _ = conn.Write([]byte(cfg.Banner))
|
||||
}
|
||||
|
||||
// Display configured prompts to the client and record the responses.
|
||||
scanner := bufio.NewScanner(conn)
|
||||
responses := make(map[string]string)
|
||||
for i, prompt := range cfg.Prompts {
|
||||
_, _ = conn.Write([]byte(strings.ReplaceAll(prompt.Text, "\\n", "\r\n")))
|
||||
_, _ = conn.Write([]byte(prompt.Text))
|
||||
scanner.Scan()
|
||||
var key string
|
||||
// Each prompt includes an optional Log field that serves as the key
|
||||
@@ -70,7 +96,6 @@ func handleConnection(conn net.Conn, cfg *config.Server) {
|
||||
// the response will not be logged. If Log is omitted, the default key
|
||||
// "data00" is used, where "00" is the index plus one.
|
||||
if prompt.Log == "none" {
|
||||
// Skip logging for this entry.
|
||||
continue
|
||||
} else if len(prompt.Log) > 0 {
|
||||
key = prompt.Log
|
||||
@@ -80,8 +105,8 @@ func handleConnection(conn net.Conn, cfg *config.Server) {
|
||||
responses[key] = scanner.Text()
|
||||
}
|
||||
|
||||
// If no prompts are provided in the configuration, wait for the client to
|
||||
// send data then record the received input.
|
||||
// If no prompts are configured, wait for client input and record the
|
||||
// received data.
|
||||
if len(cfg.Prompts) == 0 {
|
||||
scanner.Scan()
|
||||
responses["data"] = scanner.Text()
|
||||
@@ -99,24 +124,34 @@ func handleConnection(conn net.Conn, cfg *config.Server) {
|
||||
return
|
||||
}
|
||||
|
||||
// Log the connection along with all responses received from the client.
|
||||
dst_ip, dst_port, _ := net.SplitHostPort(conn.LocalAddr().String())
|
||||
src_ip, _, _ := net.SplitHostPort(conn.RemoteAddr().String())
|
||||
cfg.Logger.LogAttrs(context.Background(), slog.LevelInfo, "",
|
||||
// Log the connection and all responses received from the client.
|
||||
logData := make([]slog.Attr, 0, 9)
|
||||
logData = append(logData,
|
||||
slog.String("event_type", "tcp"),
|
||||
slog.String("source_ip", src_ip),
|
||||
slog.String("server_ip", dst_ip),
|
||||
slog.String("server_port", dst_port),
|
||||
slog.String("source_ip", srcIP),
|
||||
)
|
||||
if cfg.UseProxyProtocol {
|
||||
logData = append(logData,
|
||||
slog.Bool("source_ip_parsed", parsed),
|
||||
slog.String("source_ip_error", errMsg),
|
||||
slog.String("remote_ip", remIP),
|
||||
)
|
||||
}
|
||||
logData = append(logData,
|
||||
slog.String("server_ip", dstIP),
|
||||
slog.String("server_port", dstPort),
|
||||
slog.String("server_name", config.GetHostname()),
|
||||
slog.Any("event_details", responses),
|
||||
)
|
||||
cfg.Logger.LogAttrs(context.Background(), slog.LevelInfo, "", logData...)
|
||||
|
||||
// Print a simplified version of the interaction to the console.
|
||||
fmt.Printf("[TCP] %s %q\n", src_ip, responsesToString(responses))
|
||||
fmt.Printf("[TCP] %s %q\n", srcIP, responsesToString(responses))
|
||||
|
||||
// Update the threat feed with the source IP address from the interaction.
|
||||
// Update the threat feed with srcIP. If Proxy Protocol is enabled, srcIP
|
||||
// is taken from the proxy header. Otherwise, it's the connecting IP.
|
||||
if cfg.SendToThreatFeed {
|
||||
threatfeed.Update(src_ip)
|
||||
threatfeed.Update(srcIP)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"encoding/csv"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -65,11 +65,8 @@ var (
|
||||
func Update(ip string) {
|
||||
// Check if the given IP string is a private address. The threat feed may
|
||||
// be configured to include or exclude private IPs.
|
||||
netIP := net.ParseIP(ip)
|
||||
if netIP == nil || netIP.IsLoopback() {
|
||||
return
|
||||
}
|
||||
if !cfg.ThreatFeed.IsPrivateIncluded && netIP.IsPrivate() {
|
||||
parsedIP, err := netip.ParseAddr(ip)
|
||||
if err != nil || parsedIP.IsLoopback() || (!cfg.ThreatFeed.IsPrivateIncluded && parsedIP.IsPrivate()) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,9 @@ package threatfeed
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"cmp"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
@@ -16,11 +15,11 @@ import (
|
||||
|
||||
// feedEntry represents an individual entry in the threat feed.
|
||||
type feedEntry struct {
|
||||
IP string `json:"ip"`
|
||||
IPBytes net.IP `json:"-"`
|
||||
Added time.Time `json:"added"`
|
||||
LastSeen time.Time `json:"last_seen"`
|
||||
Observations int `json:"observations"`
|
||||
IP string `json:"ip"`
|
||||
IPBytes netip.Addr `json:"-"`
|
||||
Added time.Time `json:"added"`
|
||||
LastSeen time.Time `json:"last_seen"`
|
||||
Observations int `json:"observations"`
|
||||
}
|
||||
|
||||
// feedEntries is a slice of feedEntry structs. It represents the threat feed
|
||||
@@ -86,13 +85,13 @@ loop:
|
||||
continue
|
||||
}
|
||||
|
||||
parsedIP := net.ParseIP(ip)
|
||||
if parsedIP == nil || (parsedIP.IsPrivate() && !cfg.ThreatFeed.IsPrivateIncluded) {
|
||||
parsedIP, err := netip.ParseAddr(ip)
|
||||
if err != nil || (parsedIP.IsPrivate() && !cfg.ThreatFeed.IsPrivateIncluded) {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, ipnet := range excludedCIDR {
|
||||
if ipnet.Contains(parsedIP) {
|
||||
for _, prefix := range excludedCIDR {
|
||||
if prefix.Contains(parsedIP) {
|
||||
continue loop
|
||||
}
|
||||
}
|
||||
@@ -120,9 +119,9 @@ loop:
|
||||
// should contain an IP address or CIDR. It returns a map of the unique IPs and
|
||||
// a slice of the CIDR ranges found in the file. The file may include comments
|
||||
// using "#". The "#" symbol on a line and everything after is ignored.
|
||||
func parseExcludeList(filepath string) (map[string]struct{}, []*net.IPNet, error) {
|
||||
func parseExcludeList(filepath string) (map[string]struct{}, []netip.Prefix, error) {
|
||||
if len(filepath) == 0 {
|
||||
return map[string]struct{}{}, []*net.IPNet{}, nil
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
f, err := os.Open(filepath)
|
||||
@@ -134,22 +133,24 @@ func parseExcludeList(filepath string) (map[string]struct{}, []*net.IPNet, error
|
||||
// `ips` stores individual IPs to exclude, and `cidr` stores CIDR networks
|
||||
// to exclude.
|
||||
ips := make(map[string]struct{})
|
||||
cidr := []*net.IPNet{}
|
||||
cidr := []netip.Prefix{}
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
line := scanner.Text()
|
||||
|
||||
// Remove comments from text.
|
||||
if i := strings.Index(line, "#"); i != -1 {
|
||||
line = strings.TrimSpace(line[:i])
|
||||
// Remove comments and trim.
|
||||
if i := strings.IndexByte(line, '#'); i != -1 {
|
||||
line = line[:i]
|
||||
}
|
||||
line = strings.TrimSpace(line)
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(line) > 0 {
|
||||
if _, ipnet, err := net.ParseCIDR(line); err == nil {
|
||||
cidr = append(cidr, ipnet)
|
||||
} else {
|
||||
ips[line] = struct{}{}
|
||||
}
|
||||
if prefix, err := netip.ParsePrefix(line); err == nil {
|
||||
cidr = append(cidr, prefix)
|
||||
} else {
|
||||
ips[line] = struct{}{}
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
@@ -164,13 +165,13 @@ func (f feedEntries) applySort(method sortMethod, direction sortDirection) {
|
||||
switch method {
|
||||
case byIP:
|
||||
slices.SortFunc(f, func(a, b feedEntry) int {
|
||||
return bytes.Compare(a.IPBytes, b.IPBytes)
|
||||
return a.IPBytes.Compare(b.IPBytes)
|
||||
})
|
||||
case byLastSeen:
|
||||
slices.SortFunc(f, func(a, b feedEntry) int {
|
||||
t := a.LastSeen.Compare(b.LastSeen)
|
||||
if t == 0 {
|
||||
return bytes.Compare(a.IPBytes, b.IPBytes)
|
||||
return a.IPBytes.Compare(b.IPBytes)
|
||||
}
|
||||
return t
|
||||
})
|
||||
@@ -178,7 +179,7 @@ func (f feedEntries) applySort(method sortMethod, direction sortDirection) {
|
||||
slices.SortFunc(f, func(a, b feedEntry) int {
|
||||
t := a.Added.Compare(b.Added)
|
||||
if t == 0 {
|
||||
return bytes.Compare(a.IPBytes, b.IPBytes)
|
||||
return a.IPBytes.Compare(b.IPBytes)
|
||||
}
|
||||
return t
|
||||
})
|
||||
@@ -186,7 +187,7 @@ func (f feedEntries) applySort(method sortMethod, direction sortDirection) {
|
||||
slices.SortFunc(f, func(a, b feedEntry) int {
|
||||
t := cmp.Compare(a.Observations, b.Observations)
|
||||
if t == 0 {
|
||||
return bytes.Compare(a.IPBytes, b.IPBytes)
|
||||
return a.IPBytes.Compare(b.IPBytes)
|
||||
}
|
||||
return t
|
||||
})
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/net/websocket"
|
||||
@@ -65,7 +66,7 @@ func handleWebSocket(ws *websocket.Conn) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if netIP := net.ParseIP(ip); !netIP.IsPrivate() && !netIP.IsLoopback() {
|
||||
if parsedIP, err := netip.ParseAddr(ip); err != nil || (!parsedIP.IsPrivate() && !parsedIP.IsLoopback()) {
|
||||
return
|
||||
}
|
||||
fmt.Println("[Threat Feed]", ip, "established WebSocket connection")
|
||||
|
||||
@@ -3,6 +3,7 @@ package threatfeed
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
)
|
||||
|
||||
// enforcePrivateIP is a middleware that restricts access to the HTTP server
|
||||
@@ -12,11 +13,11 @@ func enforcePrivateIP(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ip, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
http.Error(w, "Could not get IP", http.StatusInternalServerError)
|
||||
http.Error(w, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if netIP := net.ParseIP(ip); !netIP.IsPrivate() && !netIP.IsLoopback() {
|
||||
if parsedIP, err := netip.ParseAddr(ip); err != nil || (!parsedIP.IsPrivate() && !parsedIP.IsLoopback()) {
|
||||
http.Error(w, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
<tr><th>Log Path</th><td class="blue">{{if .LogPath}}{{.LogPath}}{{else}}<span class="gray">(not set)</span>{{end}}</td></tr>
|
||||
{{if eq .Type.String "https"}}<tr><th>Certificate</th><td class="blue">{{if .CertPath}}{{.CertPath}}{{else}}<span class="gray">(not set){{end}}</span></td></tr>{{end}}
|
||||
{{if or (eq .Type.String "https") (eq .Type.String "ssh")}}<tr><th>Private Key</th><td class="blue">{{if .KeyPath}}{{.KeyPath}}{{else}}<span class="gray">(not set){{end}}</span></td></tr>{{end}}
|
||||
{{if or (eq .Type.String "tcp") (eq .Type.String "ssh")}}<tr><th>Proxy Protocol</th><td>{{if .UseProxyProtocol}}<span class="green">Enabled{{else}}<span class="gray">Disabled{{end}}</span></td></tr>{{end}}
|
||||
{{if .HomePagePath}}<tr><th>Home Page</th><td class="blue">{{.HomePagePath}}</td></tr>{{end}}
|
||||
{{if .ErrorPagePath}}<tr><th>Error Page</th><td class="blue">{{.ErrorPagePath}}</td></tr>{{end}}
|
||||
{{if .Banner}}<tr><th>Banner</th><td class="magenta">{{.Banner}}</td></tr>{{end}}
|
||||
|
||||
@@ -57,14 +57,14 @@ func Start(cfg *config.Server) {
|
||||
// addresses, this may not correspond to the IP address that
|
||||
// received the UDP data. However, this limitation is acceptable as
|
||||
// the primary goal is to log the received data.
|
||||
_, dst_port, _ := net.SplitHostPort(conn.LocalAddr().String())
|
||||
src_ip, _, _ := net.SplitHostPort(remoteAddr.String())
|
||||
_, dstPort, _ := net.SplitHostPort(conn.LocalAddr().String())
|
||||
srcIP, _, _ := net.SplitHostPort(remoteAddr.String())
|
||||
cfg.Logger.LogAttrs(context.Background(), slog.LevelInfo, "",
|
||||
slog.String("event_type", "udp"),
|
||||
slog.String("source_ip", src_ip+" [unreliable]"),
|
||||
slog.String("source_ip", srcIP+" [unreliable]"),
|
||||
slog.String("source_reliability", "unreliable"),
|
||||
slog.String("server_ip", config.GetHostIP()),
|
||||
slog.String("server_port", dst_port),
|
||||
slog.String("server_port", dstPort),
|
||||
slog.String("server_name", config.GetHostname()),
|
||||
slog.Group("event_details",
|
||||
slog.String("data", string(buffer[:n])),
|
||||
@@ -72,7 +72,7 @@ func Start(cfg *config.Server) {
|
||||
)
|
||||
|
||||
// Print a simplified version of the interaction to the console.
|
||||
fmt.Printf("[UDP] %s Data: %q\n", src_ip, strings.TrimSpace(string(buffer[:n])))
|
||||
fmt.Printf("[UDP] %s Data: %q\n", srcIP, strings.TrimSpace(string(buffer[:n])))
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user