From d86f9e5ea2ffa277b111ebd73cea9f47e89043f4 Mon Sep 17 00:00:00 2001 From: Christian Roessner Date: Thu, 5 Dec 2024 17:20:20 +0100 Subject: [PATCH 1/7] Fix: Fix logging Fix various log strings in checkSMTP, checkIMAP, checkPOP3 and checkHTTP. Signed-off-by: Christian Roessner --- server/monitoring/connection.go | 40 ++++++++++++++++----------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/server/monitoring/connection.go b/server/monitoring/connection.go index 71612c32..dba9c737 100644 --- a/server/monitoring/connection.go +++ b/server/monitoring/connection.go @@ -127,7 +127,7 @@ func checkSMTP(conn net.Conn, username string, password string) { _, err := tp.ReadLine() if err != nil { - level.Error(log.Logger).Log("Error reading SMTP initial response", "error", err) + level.Error(log.Logger).Log(definitions.LogKeyMsg, fmt.Sprintf("Error reading SMTP initial response, error: %s", err)) return } @@ -136,14 +136,14 @@ func checkSMTP(conn net.Conn, username string, password string) { for { response, err := tp.ReadLine() if err != nil { - level.Error(log.Logger).Log("Error reading SMTP EHLO response", "error", err) + level.Error(log.Logger).Log(definitions.LogKeyMsg, fmt.Sprintf("Error reading SMTP EHLO response, error: %v", err)) return } if response[:3] != "250" { if response[0] >= '4' { - level.Error(log.Logger).Log("EHLO command failed", "response", response) + level.Error(log.Logger).Log(definitions.LogKeyMsg, fmt.Sprintf("EHLO command failed, response: %s", response)) return } @@ -160,7 +160,7 @@ func checkSMTP(conn net.Conn, username string, password string) { _, err = tp.ReadLine() if err != nil { - level.Error(log.Logger).Log("Error in SMTP AUTH LOGIN", "error", err) + level.Error(log.Logger).Log(definitions.LogKeyMsg, fmt.Sprintf("Error in SMTP AUTH LOGIN, error: %v", err)) return } @@ -171,7 +171,7 @@ func checkSMTP(conn net.Conn, username string, password string) { _, err = tp.ReadLine() if err != nil { - level.Error(log.Logger).Log("Error sending SMTP username", "error", err) + level.Error(log.Logger).Log(definitions.LogKeyMsg, fmt.Sprintf("Error sending SMTP username, error: %v", err)) return } @@ -182,7 +182,7 @@ func checkSMTP(conn net.Conn, username string, password string) { response, err := tp.ReadLine() if err != nil || response[:3] != "235" { - level.Error(log.Logger).Log("SMTP AUTH LOGIN failed", "error", err, "response", response) + level.Error(log.Logger).Log(definitions.LogKeyMsg, fmt.Sprintf("SMTP AUTH LOGIN failed, error: %v", err), "response", response) return } @@ -198,13 +198,13 @@ func checkPOP3(conn net.Conn, username string, password string) { greeting, err := tp.ReadLine() if err != nil { - level.Error(log.Logger).Log(fmt.Sprintf("Error reading POP3 greeting: %v\n", err)) + level.Error(log.Logger).Log(definitions.LogKeyMsg, fmt.Sprintf("Error reading POP3 greeting, error: %v", err)) return } if !isOkResponsePOP3(greeting) { - level.Error(log.Logger).Log(fmt.Sprintf("POP3 greeting failed: %s\n", greeting)) + level.Error(log.Logger).Log(definitions.LogKeyMsg, fmt.Sprintf("POP3 greeting failed, error: %s", greeting)) return } @@ -217,13 +217,13 @@ func checkPOP3(conn net.Conn, username string, password string) { response, err := tp.ReadLine() if err != nil { - level.Error(log.Logger).Log(fmt.Sprintf("Error reading POP3 response after USER command: %v\n", err)) + level.Error(log.Logger).Log(definitions.LogKeyMsg, fmt.Sprintf("Error reading POP3 response after USER command, error %v", err)) return } if !isOkResponsePOP3(response) { - level.Error(log.Logger).Log(fmt.Sprintf("POP3 USER command failed: %s\n", response)) + level.Error(log.Logger).Log(definitions.LogKeyMsg, fmt.Sprintf("POP3 USER command failed, error: %s", response)) return } @@ -232,13 +232,13 @@ func checkPOP3(conn net.Conn, username string, password string) { response, err = tp.ReadLine() if err != nil { - level.Error(log.Logger).Log(fmt.Sprintf("Error reading POP3 response after PASS command: %v\n", err)) + level.Error(log.Logger).Log(definitions.LogKeyMsg, fmt.Sprintf("Error reading POP3 response after PASS command: %v\n", err)) return } if !isOkResponsePOP3(response) { - level.Error(log.Logger).Log(fmt.Sprintf("POP3 login failed: %s\n", response)) + level.Error(log.Logger).Log(definitions.LogKeyMsg, fmt.Sprintf("POP3 login failed: %s\n", response)) } } @@ -258,12 +258,12 @@ func checkIMAP(conn net.Conn, username string, password string) { greeting, err := tp.ReadLine() if err != nil { - level.Error(log.Logger).Log(definitions.LogKeyMsg, "Error reading IMAP greeting", "error", err) + level.Error(log.Logger).Log(definitions.LogKeyMsg, fmt.Sprintf("Error reading IMAP greeting, error: %v", err)) return } if !isOkResponseIMAP(greeting) { - level.Error(log.Logger).Log(definitions.LogKeyMsg, "IMAP greeting failed", "response", greeting) + level.Error(log.Logger).Log(definitions.LogKeyMsg, fmt.Sprintf("IMAP greeting failed, response: %s", greeting)) return } @@ -276,13 +276,13 @@ func checkIMAP(conn net.Conn, username string, password string) { response, err := tp.ReadLine() if err != nil { - level.Error(log.Logger).Log(definitions.LogKeyMsg, "Error reading IMAP response", "error", err) + level.Error(log.Logger).Log(definitions.LogKeyMsg, fmt.Sprintf("Error reading IMAP response, error: %v", err)) return } if !isOkResponseIMAP(response) { - level.Error(log.Logger).Log(definitions.LogKeyMsg, "IMAP login failed", "response", response) + level.Error(log.Logger).Log(definitions.LogKeyMsg, fmt.Sprintf("IMAP login failed, response: %s", response)) } } @@ -309,7 +309,7 @@ func checkHTTP(conn net.Conn, hostname, requestURI, username, password string) e _, err := fmt.Fprintf(conn, "GET %s HTTP/1.1\r\nHost: %s\r\n%sUser-Agent: Nauthilus\r\nAccept: */*\r\n\r\n", requestURI, hostname, authHeader) if err != nil { - level.Error(log.Logger).Log(definitions.LogKeyMsg, "Error sending HTTP request", "error", err) + level.Error(log.Logger).Log(definitions.LogKeyMsg, fmt.Sprintf("Error sending HTTP request, error: %v", err)) return err } @@ -319,20 +319,20 @@ func checkHTTP(conn net.Conn, hostname, requestURI, username, password string) e statusLine, err := tp.ReadLine() if err != nil { - level.Error(log.Logger).Log(definitions.LogKeyMsg, "Error reading HTTP status line", "error", err) + level.Error(log.Logger).Log(definitions.LogKeyMsg, fmt.Sprintf("Error reading HTTP status line, error: %v", err)) return err } if !isOkResponseHTTP(statusLine) { - level.Error(log.Logger).Log(definitions.LogKeyMsg, "HTTP request failed", "response", statusLine) + level.Error(log.Logger).Log(definitions.LogKeyMsg, fmt.Sprintf("HTTP request failed, response: %s", statusLine)) return fmt.Errorf("HTTP request failed: %s", statusLine) } _, err = tp.ReadMIMEHeader() if err != nil { - level.Error(log.Logger).Log(definitions.LogKeyMsg, "Error reading HTTP headers", "error", err) + level.Error(log.Logger).Log(definitions.LogKeyMsg, fmt.Sprintf("Error reading HTTP headers, error: %v", err)) return err } From bbf1ebb498a08dfe300362dc49a6c9dd1c2040e4 Mon Sep 17 00:00:00 2001 From: Christian Roessner Date: Fri, 6 Dec 2024 09:25:08 +0100 Subject: [PATCH 2/7] Fix: Refactor error handling in connection protocols Revised error handling and logging across various network connection protocols, including SMTP, POP3, IMAP, and HTTP for better clarity and error propagation. Set read and write timeouts for network connections to enhance reliability. These changes improve code maintainability and error transparency, reducing reliance on external logging and ensuring errors are properly returned and handled. Signed-off-by: Christian Roessner --- server/monitoring/connection.go | 111 +++++++++++++------------------- 1 file changed, 46 insertions(+), 65 deletions(-) diff --git a/server/monitoring/connection.go b/server/monitoring/connection.go index dba9c737..ffe9ec66 100644 --- a/server/monitoring/connection.go +++ b/server/monitoring/connection.go @@ -20,6 +20,7 @@ import ( "bytes" "crypto/tls" "encoding/base64" + "errors" "fmt" "net" "net/textproto" @@ -66,6 +67,9 @@ func checkBackendConnection(server *config.BackendServer) error { return err } + conn.SetReadDeadline(time.Now().Add(5 * time.Second)) + conn.SetWriteDeadline(time.Now().Add(5 * time.Second)) + defer conn.Close() if server.HAProxyV2 { @@ -101,51 +105,51 @@ func checkBackendConnection(server *config.BackendServer) error { // handleProtocol processes authentication for a test user over a network connection based on the specified protocol. // Supported protocols include SMTP, POP3, IMAP, and HTTP. If an unsupported protocol is specified, a warning is logged. // This function currently does not support plain connections requiring StartTLS. -func handleProtocol(server *config.BackendServer, conn net.Conn) { +func handleProtocol(server *config.BackendServer, conn net.Conn) (err error) { // Limited support only. Plain connections requireing StartTLS are not supported at the moment! switch server.Protocol { case "smtp": - checkSMTP(conn, server.TestUsername, server.TestPassword) + err = checkSMTP(conn, server.TestUsername, server.TestPassword) case "pop3": - checkPOP3(conn, server.TestUsername, server.TestPassword) + err = checkPOP3(conn, server.TestUsername, server.TestPassword) case "imap": - checkIMAP(conn, server.TestUsername, server.TestPassword) + err = checkIMAP(conn, server.TestUsername, server.TestPassword) case "http": - checkHTTP(conn, server.Host, server.RequestURI, server.TestUsername, server.TestPassword) + err = checkHTTP(conn, server.Host, server.RequestURI, server.TestUsername, server.TestPassword) default: - level.Warn(log.Logger).Log(definitions.LogKeyMsg, "Unsupported protocol", "protocol", server.Protocol) + err = errors.New("unsupported protocol") } + + return err } // checkSMTP performs SMTP authentication using the provided username and password over a given network connection. // It sends EHLO and AUTH LOGIN commands to the SMTP server, encodes credentials in base64, and logs errors if authentication fails. -func checkSMTP(conn net.Conn, username string, password string) { +func checkSMTP(conn net.Conn, username string, password string) error { reader := bufio.NewReader(conn) tp := textproto.NewReader(reader) defer fmt.Fprintf(conn, "QUIT\r\n") - _, err := tp.ReadLine() + greeting, err := tp.ReadLine() if err != nil { - level.Error(log.Logger).Log(definitions.LogKeyMsg, fmt.Sprintf("Error reading SMTP initial response, error: %s", err)) + return err + } - return + if greeting[:4] != "220 " { + return fmt.Errorf("SMTP greeting failed, response: %s", greeting) } - fmt.Fprintf(conn, "EHLO localhost\r\n") + fmt.Fprintf(conn, "EHLO localhost.localdomain\r\n") for { response, err := tp.ReadLine() if err != nil { - level.Error(log.Logger).Log(definitions.LogKeyMsg, fmt.Sprintf("Error reading SMTP EHLO response, error: %v", err)) - - return + return err } - if response[:3] != "250" { + if response[:4] != "250 " { if response[0] >= '4' { - level.Error(log.Logger).Log(definitions.LogKeyMsg, fmt.Sprintf("EHLO command failed, response: %s", response)) - - return + return fmt.Errorf("SMTP EHLO failed, response: %s", response) } break @@ -153,16 +157,14 @@ func checkSMTP(conn net.Conn, username string, password string) { } if username == "" || password == "" { - return + return nil } fmt.Fprintf(conn, "AUTH LOGIN\r\n") _, err = tp.ReadLine() if err != nil { - level.Error(log.Logger).Log(definitions.LogKeyMsg, fmt.Sprintf("Error in SMTP AUTH LOGIN, error: %v", err)) - - return + return err } usernameEnc := base64.StdEncoding.EncodeToString([]byte(username)) @@ -171,9 +173,7 @@ func checkSMTP(conn net.Conn, username string, password string) { _, err = tp.ReadLine() if err != nil { - level.Error(log.Logger).Log(definitions.LogKeyMsg, fmt.Sprintf("Error sending SMTP username, error: %v", err)) - - return + return err } passwordEnc := base64.StdEncoding.EncodeToString([]byte(password)) @@ -182,15 +182,15 @@ func checkSMTP(conn net.Conn, username string, password string) { response, err := tp.ReadLine() if err != nil || response[:3] != "235" { - level.Error(log.Logger).Log(definitions.LogKeyMsg, fmt.Sprintf("SMTP AUTH LOGIN failed, error: %v", err), "response", response) - - return + return fmt.Errorf("SMTP AUTH LOGIN failed: %s", response) } + + return nil } // checkPOP3 performs POP3 authentication using the provided username and password over a given network connection. // It sends USER and PASS commands to the POP3 server, validates responses, and logs errors if authentication fails. -func checkPOP3(conn net.Conn, username string, password string) { +func checkPOP3(conn net.Conn, username string, password string) error { reader := bufio.NewReader(conn) tp := textproto.NewReader(reader) @@ -198,48 +198,40 @@ func checkPOP3(conn net.Conn, username string, password string) { greeting, err := tp.ReadLine() if err != nil { - level.Error(log.Logger).Log(definitions.LogKeyMsg, fmt.Sprintf("Error reading POP3 greeting, error: %v", err)) - - return + return err } if !isOkResponsePOP3(greeting) { - level.Error(log.Logger).Log(definitions.LogKeyMsg, fmt.Sprintf("POP3 greeting failed, error: %s", greeting)) - - return + return err } if username == "" || password == "" { - return + return nil } fmt.Fprintf(conn, "USER %s\r\n", username) response, err := tp.ReadLine() if err != nil { - level.Error(log.Logger).Log(definitions.LogKeyMsg, fmt.Sprintf("Error reading POP3 response after USER command, error %v", err)) - - return + return err } if !isOkResponsePOP3(response) { - level.Error(log.Logger).Log(definitions.LogKeyMsg, fmt.Sprintf("POP3 USER command failed, error: %s", response)) - - return + return fmt.Errorf("POP3 USER command failed: %s", response) } fmt.Fprintf(conn, "PASS %s\r\n", password) response, err = tp.ReadLine() if err != nil { - level.Error(log.Logger).Log(definitions.LogKeyMsg, fmt.Sprintf("Error reading POP3 response after PASS command: %v\n", err)) - - return + return fmt.Errorf("POP3 PASS command failed: %s", response) } if !isOkResponsePOP3(response) { - level.Error(log.Logger).Log(definitions.LogKeyMsg, fmt.Sprintf("POP3 login failed: %s\n", response)) + return fmt.Errorf("POP3 PASS command failed: %s", response) } + + return nil } // isOkResponsePOP3 checks if the provided POP3 server response starts with "+OK". @@ -250,7 +242,7 @@ func isOkResponsePOP3(response string) bool { // checkIMAP authenticates to an IMAP server using provided username and password over an existing network connection. // It sends an IMAP LOGIN command and checks if the response indicates a successful login. // Errors in reading responses or unsuccessful logins are logged for diagnostic purposes. -func checkIMAP(conn net.Conn, username string, password string) { +func checkIMAP(conn net.Conn, username string, password string) error { reader := bufio.NewReader(conn) tp := textproto.NewReader(reader) @@ -258,32 +250,29 @@ func checkIMAP(conn net.Conn, username string, password string) { greeting, err := tp.ReadLine() if err != nil { - level.Error(log.Logger).Log(definitions.LogKeyMsg, fmt.Sprintf("Error reading IMAP greeting, error: %v", err)) - return + return err } if !isOkResponseIMAP(greeting) { - level.Error(log.Logger).Log(definitions.LogKeyMsg, fmt.Sprintf("IMAP greeting failed, response: %s", greeting)) - - return + return fmt.Errorf("IMAP greeting failed: %s", greeting) } if username == "" || password == "" { - return + return nil } fmt.Fprintf(conn, "a1 LOGIN %s %s\r\n", username, password) response, err := tp.ReadLine() if err != nil { - level.Error(log.Logger).Log(definitions.LogKeyMsg, fmt.Sprintf("Error reading IMAP response, error: %v", err)) - - return + return err } if !isOkResponseIMAP(response) { - level.Error(log.Logger).Log(definitions.LogKeyMsg, fmt.Sprintf("IMAP login failed, response: %s", response)) + return fmt.Errorf("IMAP LOGIN command failed: %s", response) } + + return nil } // isOkResponseIMAP checks if the given IMAP server response starts with "* OK" or "a1 OK". @@ -309,8 +298,6 @@ func checkHTTP(conn net.Conn, hostname, requestURI, username, password string) e _, err := fmt.Fprintf(conn, "GET %s HTTP/1.1\r\nHost: %s\r\n%sUser-Agent: Nauthilus\r\nAccept: */*\r\n\r\n", requestURI, hostname, authHeader) if err != nil { - level.Error(log.Logger).Log(definitions.LogKeyMsg, fmt.Sprintf("Error sending HTTP request, error: %v", err)) - return err } @@ -319,21 +306,15 @@ func checkHTTP(conn net.Conn, hostname, requestURI, username, password string) e statusLine, err := tp.ReadLine() if err != nil { - level.Error(log.Logger).Log(definitions.LogKeyMsg, fmt.Sprintf("Error reading HTTP status line, error: %v", err)) - return err } if !isOkResponseHTTP(statusLine) { - level.Error(log.Logger).Log(definitions.LogKeyMsg, fmt.Sprintf("HTTP request failed, response: %s", statusLine)) - return fmt.Errorf("HTTP request failed: %s", statusLine) } _, err = tp.ReadMIMEHeader() if err != nil { - level.Error(log.Logger).Log(definitions.LogKeyMsg, fmt.Sprintf("Error reading HTTP headers, error: %v", err)) - return err } From 35fc8d056639125b5df4f5838ad4a360a6eb3b71 Mon Sep 17 00:00:00 2001 From: Christian Roessner Date: Fri, 6 Dec 2024 09:36:19 +0100 Subject: [PATCH 3/7] Fix: Remove redundant methods GetBackendServerIP and GetBackendServerPort The methods GetBackendServerIP and GetBackendServerPort have been removed from the File struct in server/config/file.go. This change cleans up the code by eliminating unused methods, improving maintainability and readability. Signed-off-by: Christian Roessner --- server/config/file.go | 50 ------------------------------------------- 1 file changed, 50 deletions(-) diff --git a/server/config/file.go b/server/config/file.go index 61d335f3..2113218f 100644 --- a/server/config/file.go +++ b/server/config/file.go @@ -116,56 +116,6 @@ func (f *File) GetBackendServer(protocol string) *BackendServer { return nil } -// GetBackendServerIP is a method for the File struct which -// attempts to get the IP address of a backend server -// for a specified protocol. The method first calls -// GetBackendServer with the given protocol and checks -// if it returns a non-nil value. If the value is not nil, -// it retrieves the IP attribute of the backend server. -// If the returned value is nil, indicating that there is -// no backend server for the given protocol, the method -// returns an empty string. -// -// Parameters: -// -// protocol: A string that specifies the protocol for -// which the backend server's IP address -// is to be retrieved. This could be "http", -// "https", etc. -// -// Returns: -// -// A string representing the IP address of the backend -// server for the given protocol. If there is no backend -// server for the specified protocol, the method returns -// an empty string. -func (f *File) GetBackendServerIP(protocol string) string { - if f == nil { - return "" - } - - if f.GetBackendServer(protocol) != nil { - return f.GetBackendServer(protocol).Host - } - - return "" -} - -// GetBackendServerPort checks the specific protocol's backend server in the File structure. -// If the server exists, it returns the port of the server. -// If the server does not exist, it returns 0. -func (f *File) GetBackendServerPort(protocol string) int { - if f == nil { - return 0 - } - - if f.GetBackendServer(protocol) != nil { - return f.GetBackendServer(protocol).Port - } - - return 0 -} - /* * LDAP Config */ From 281191abbafa5a10f0bb3577a743042d951b2b5f Mon Sep 17 00:00:00 2001 From: Christian Roessner Date: Fri, 6 Dec 2024 10:22:25 +0100 Subject: [PATCH 4/7] Fix: Refactor error logging and enhance server authentication Consolidated logging of server errors with formatted messages for clarity. Improved server connection handling by adding a deep check to enable more thorough protocol handling, and ensured proper response checks for SMTP authentication. Refactored Lua backend server structure for better integration with the existing backend server configuration. Signed-off-by: Christian Roessner --- server/config/features.go | 1 + server/lualib/connection.go | 3 ++ server/lualib/filter/filter.go | 55 +++++---------------------------- server/main.go | 3 +- server/monitoring/connection.go | 35 ++++++++++++++++----- 5 files changed, 40 insertions(+), 57 deletions(-) diff --git a/server/config/features.go b/server/config/features.go index a55a1146..2060f40c 100644 --- a/server/config/features.go +++ b/server/config/features.go @@ -33,6 +33,7 @@ func (r *RelayDomainsSection) String() string { type BackendServer struct { Protocol string `mapstructure:"protocol"` Host string `mapstructure:"host"` + DeepCheck bool `mapstructure:"deep_check"` RequestURI string `mapstructure:"request_uri"` TestUsername string `mapstructure:"test_username"` TestPassword string `mapstructure:"test_password"` diff --git a/server/lualib/connection.go b/server/lualib/connection.go index df18909b..ff2c86af 100644 --- a/server/lualib/connection.go +++ b/server/lualib/connection.go @@ -56,8 +56,11 @@ func CheckBackendConnection(monitor monitoring.Monitor) lua.LGFunction { server.Port = getNumberFromTable(table, "port") server.HAProxyV2 = getBoolFromTable(table, "haproxy_v2") server.TLS = getBoolFromTable(table, "tls") + server.TLSSkipVerify = getBoolFromTable(table, "tls_skip_verify") server.TestUsername = getStringFromTable(table, "test_username") server.TestPassword = getStringFromTable(table, "test_password") + server.RequestURI = getStringFromTable(table, "request_uri") + server.DeepCheck = getBoolFromTable(table, "deep_check") if err := monitor.CheckBackendConnection(server); err != nil { L.Push(lua.LString(err.Error())) diff --git a/server/lualib/filter/filter.go b/server/lualib/filter/filter.go index 1388b0d9..af239e8e 100644 --- a/server/lualib/filter/filter.go +++ b/server/lualib/filter/filter.go @@ -276,39 +276,9 @@ type Request struct { *lualib.CommonRequest } -// LuaBackendServer represents a server configuration for a Lua script backend. -type LuaBackendServer struct { - // Protocol specifies the communication protocol (e.g., HTTP, HTTPS) used by the server. - Protocol string - - // Host specifies the hostname or IP address of the server used in the backend configuration. - Host string - - // RequestURL represents the request URL path used by the Lua backend server. - RequestURL string - - // TestUsername is a placeholder used for testing purposes, representing the username required for server authentication. - TestUsername string - - // TestPassword is a placeholder used for testing purposes, representing the password required for server authentication. - TestPassword string - - // Port represents the network port number used by the server for communication. - Port int - - // HAProxyV2 indicates whether HAProxy version 2 protocol is enabled for the backend server configuration. - HAProxyV2 bool - - // TLS indicates whether Transport Layer Security (TLS) should be enabled for the server connection. - TLS bool - - // TLSSKipVerify indicates whether to skip verification of the server's TLS certificate. - TLSSKipVerify bool -} - // The userData constellation method: -func newLuaBackendServer(userData *lua.LUserData) *LuaBackendServer { - if v, ok := userData.Value.(*LuaBackendServer); ok { +func newLuaBackendServer(userData *lua.LUserData) *config.BackendServer { + if v, ok := userData.Value.(*config.BackendServer); ok { return v } @@ -332,8 +302,8 @@ func indexMethod(L *lua.LState) int { L.Push(lua.LString(server.Host)) case "port": L.Push(lua.LNumber(server.Port)) - case "request_url": - L.Push(lua.LString(server.RequestURL)) + case "request_uri": + L.Push(lua.LString(server.RequestURI)) case "test_username": L.Push(lua.LString(server.TestUsername)) case "test_password": @@ -343,7 +313,9 @@ func indexMethod(L *lua.LState) int { case "tls": L.Push(lua.LBool(server.TLS)) case "tls_skip_verify": - L.Push(lua.LBool(server.TLSSKipVerify)) + L.Push(lua.LBool(server.TLSSkipVerify)) + case "deep_check": + L.Push(lua.LBool(server.DeepCheck)) default: return 0 // The field does not exist } @@ -368,18 +340,7 @@ func getBackendServers(backendServers []*config.BackendServer) lua.LGFunction { // Create an userdata and set its metatable serverUserData := L.NewUserData() - - serverUserData.Value = &LuaBackendServer{ - Protocol: backendServer.Protocol, - Host: backendServer.Host, - RequestURL: backendServer.RequestURI, - TestUsername: backendServer.TestUsername, - TestPassword: backendServer.TestPassword, - Port: backendServer.Port, - HAProxyV2: backendServer.HAProxyV2, - TLS: backendServer.TLS, - TLSSKipVerify: backendServer.TLSSkipVerify, - } + serverUserData.Value = backendServer L.SetMetatable(serverUserData, L.GetTypeMetatable(definitions.LuaBackendServerTypeName)) diff --git a/server/main.go b/server/main.go index d252a18a..d8ff1fb9 100644 --- a/server/main.go +++ b/server/main.go @@ -851,8 +851,7 @@ func startStatsLoop(ctx context.Context, ticker *time.Ticker) error { // err: the error that has occurred func logBackendServerError(server *config.BackendServer, err error) { level.Error(log.Logger).Log( - definitions.LogKeyMsg, err, - definitions.LogKeyMsg, "Server down", + definitions.LogKeyMsg, fmt.Sprintf("Server down: %v", err), definitions.LogKeyBackendServer, server, ) } diff --git a/server/monitoring/connection.go b/server/monitoring/connection.go index ffe9ec66..e58a45c1 100644 --- a/server/monitoring/connection.go +++ b/server/monitoring/connection.go @@ -97,9 +97,11 @@ func checkBackendConnection(server *config.BackendServer) error { conn = net.Conn(tlsConn) } - handleProtocol(server, conn) + if server.DeepCheck { + err = handleProtocol(server, conn) + } - return nil + return err } // handleProtocol processes authentication for a test user over a network connection based on the specified protocol. @@ -136,13 +138,18 @@ func checkSMTP(conn net.Conn, username string, password string) error { return err } + // We asume submission endpoints here! Not using postfix-postscreen multi-line features! if greeting[:4] != "220 " { return fmt.Errorf("SMTP greeting failed, response: %s", greeting) } + // Normally submission must not validate FQDN or DNS resolution for MUAs fmt.Fprintf(conn, "EHLO localhost.localdomain\r\n") + + response := "" + for { - response, err := tp.ReadLine() + response, err = tp.ReadLine() if err != nil { return err } @@ -151,7 +158,7 @@ func checkSMTP(conn net.Conn, username string, password string) error { if response[0] >= '4' { return fmt.Errorf("SMTP EHLO failed, response: %s", response) } - + } else { break } } @@ -162,26 +169,38 @@ func checkSMTP(conn net.Conn, username string, password string) error { fmt.Fprintf(conn, "AUTH LOGIN\r\n") - _, err = tp.ReadLine() + response, err = tp.ReadLine() if err != nil { return err } + if response[:3] != "334" { + return fmt.Errorf("SMTP AUTH LOGIN failed: %s", response) + } + usernameEnc := base64.StdEncoding.EncodeToString([]byte(username)) fmt.Fprintf(conn, "%s\r\n", usernameEnc) - _, err = tp.ReadLine() + response, err = tp.ReadLine() if err != nil { return err } + if response[:3] != "334" { + return fmt.Errorf("SMTP AUTH LOGIN failed: %s", response) + } + passwordEnc := base64.StdEncoding.EncodeToString([]byte(password)) fmt.Fprintf(conn, "%s\r\n", passwordEnc) - response, err := tp.ReadLine() - if err != nil || response[:3] != "235" { + response, err = tp.ReadLine() + if err != nil { + return err + } + + if response[:3] != "235" { return fmt.Errorf("SMTP AUTH LOGIN failed: %s", response) } From 3a64deb31c9319f40d69ea242c421d68a76b90ae Mon Sep 17 00:00:00 2001 From: Christian Roessner Date: Fri, 6 Dec 2024 10:34:09 +0100 Subject: [PATCH 5/7] Feat: Add LMTP protocol support in connection handling Enhance the handleProtocol function to support LMTP alongside SMTP by adding a check for LMTP in the switch statement. Modify the checkSMTP function to differentiate between SMTP and LMTP by using the appropriate command (EHLO for SMTP, LHLO for LMTP). This update ensures better protocol handling and extends the server's capability to authenticate connections using the LMTP protocol. Signed-off-by: Christian Roessner --- server/monitoring/connection.go | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/server/monitoring/connection.go b/server/monitoring/connection.go index e58a45c1..3653b4dc 100644 --- a/server/monitoring/connection.go +++ b/server/monitoring/connection.go @@ -24,6 +24,7 @@ import ( "fmt" "net" "net/textproto" + "strings" "time" "github.com/croessner/nauthilus/server/config" @@ -109,9 +110,9 @@ func checkBackendConnection(server *config.BackendServer) error { // This function currently does not support plain connections requiring StartTLS. func handleProtocol(server *config.BackendServer, conn net.Conn) (err error) { // Limited support only. Plain connections requireing StartTLS are not supported at the moment! - switch server.Protocol { - case "smtp": - err = checkSMTP(conn, server.TestUsername, server.TestPassword) + switch strings.ToLower(server.Protocol) { + case "smtp", "lmtp": + err = checkSMTP(conn, server.Protocol, server.TestUsername, server.TestPassword) case "pop3": err = checkPOP3(conn, server.TestUsername, server.TestPassword) case "imap": @@ -127,9 +128,10 @@ func handleProtocol(server *config.BackendServer, conn net.Conn) (err error) { // checkSMTP performs SMTP authentication using the provided username and password over a given network connection. // It sends EHLO and AUTH LOGIN commands to the SMTP server, encodes credentials in base64, and logs errors if authentication fails. -func checkSMTP(conn net.Conn, username string, password string) error { +func checkSMTP(conn net.Conn, protocol string, username string, password string) error { reader := bufio.NewReader(conn) tp := textproto.NewReader(reader) + protocol = strings.ToLower(protocol) defer fmt.Fprintf(conn, "QUIT\r\n") @@ -140,11 +142,16 @@ func checkSMTP(conn net.Conn, username string, password string) error { // We asume submission endpoints here! Not using postfix-postscreen multi-line features! if greeting[:4] != "220 " { - return fmt.Errorf("SMTP greeting failed, response: %s", greeting) + return fmt.Errorf("S/LMTP greeting failed, response: %s", greeting) + } + + cmd := "EHLO" + if protocol == "lmtp" { + cmd = "LHLO" } // Normally submission must not validate FQDN or DNS resolution for MUAs - fmt.Fprintf(conn, "EHLO localhost.localdomain\r\n") + fmt.Fprintf(conn, fmt.Sprintf("%s localhost.localdomain\r\n", cmd)) response := "" @@ -156,14 +163,14 @@ func checkSMTP(conn net.Conn, username string, password string) error { if response[:4] != "250 " { if response[0] >= '4' { - return fmt.Errorf("SMTP EHLO failed, response: %s", response) + return fmt.Errorf("L/SMTP EHLO/LHLO failed, response: %s", response) } } else { break } } - if username == "" || password == "" { + if protocol == "lmtp" || username == "" || password == "" { return nil } From 94880b3b2c9f8d1f0bf459b48996ba1f92fd72b0 Mon Sep 17 00:00:00 2001 From: Christian Roessner Date: Fri, 6 Dec 2024 11:33:08 +0100 Subject: [PATCH 6/7] Feat: Add TLS check for network connections This update introduces a verification to ensure that network connections are using TLS before proceeding with authentication. If a connection is not secured with TLS, an error is returned to reinforce security. Additionally, a new error type, ErrMissingTLS, is introduced to handle non-TLS connection scenarios. Signed-off-by: Christian Roessner --- server/errors/errors.go | 6 ++ server/monitoring/connection.go | 108 +++++++++++++++++++++++++++++++- 2 files changed, 112 insertions(+), 2 deletions(-) diff --git a/server/errors/errors.go b/server/errors/errors.go index 05896745..02b94dd3 100644 --- a/server/errors/errors.go +++ b/server/errors/errors.go @@ -221,3 +221,9 @@ var ( var ( ErrInvalidRange = errors.New("invalid range") ) + +// connection. + +var ( + ErrMissingTLS = errors.New("missing TLS connection") +) diff --git a/server/monitoring/connection.go b/server/monitoring/connection.go index 3653b4dc..1a36fe97 100644 --- a/server/monitoring/connection.go +++ b/server/monitoring/connection.go @@ -20,7 +20,7 @@ import ( "bytes" "crypto/tls" "encoding/base64" - "errors" + stderrors "errors" "fmt" "net" "net/textproto" @@ -29,6 +29,7 @@ import ( "github.com/croessner/nauthilus/server/config" "github.com/croessner/nauthilus/server/definitions" + "github.com/croessner/nauthilus/server/errors" "github.com/croessner/nauthilus/server/log" "github.com/go-kit/log/level" "github.com/pires/go-proxyproto" @@ -117,15 +118,24 @@ func handleProtocol(server *config.BackendServer, conn net.Conn) (err error) { err = checkPOP3(conn, server.TestUsername, server.TestPassword) case "imap": err = checkIMAP(conn, server.TestUsername, server.TestPassword) + case "sieve": + err = checkSieve(conn, server.Host, server.TestUsername, server.TestPassword, server.TLSSkipVerify) case "http": err = checkHTTP(conn, server.Host, server.RequestURI, server.TestUsername, server.TestPassword) default: - err = errors.New("unsupported protocol") + err = stderrors.New("unsupported protocol") } return err } +// isTLSConnection determines if the provided network connection is a TLS connection. +func isTLSConnection(conn net.Conn) bool { + _, isTLS := conn.(*tls.Conn) + + return isTLS +} + // checkSMTP performs SMTP authentication using the provided username and password over a given network connection. // It sends EHLO and AUTH LOGIN commands to the SMTP server, encodes credentials in base64, and logs errors if authentication fails. func checkSMTP(conn net.Conn, protocol string, username string, password string) error { @@ -174,6 +184,10 @@ func checkSMTP(conn net.Conn, protocol string, username string, password string) return nil } + if !isTLSConnection(conn) { + return errors.ErrMissingTLS + } + fmt.Fprintf(conn, "AUTH LOGIN\r\n") response, err = tp.ReadLine() @@ -235,6 +249,10 @@ func checkPOP3(conn net.Conn, username string, password string) error { return nil } + if !isTLSConnection(conn) { + return errors.ErrMissingTLS + } + fmt.Fprintf(conn, "USER %s\r\n", username) response, err := tp.ReadLine() @@ -287,6 +305,10 @@ func checkIMAP(conn net.Conn, username string, password string) error { return nil } + if !isTLSConnection(conn) { + return errors.ErrMissingTLS + } + fmt.Fprintf(conn, "a1 LOGIN %s %s\r\n", username, password) response, err := tp.ReadLine() @@ -306,6 +328,84 @@ func isOkResponseIMAP(response string) bool { return bytes.HasPrefix([]byte(response), []byte("* OK")) || bytes.HasPrefix([]byte(response), []byte("a1 OK")) } +// checkSieve authenticates with a Sieve server using an existing network connection. +// It sends an AUTHENTICATE PLAIN command with encoded username and password. +func checkSieve(conn net.Conn, hostname, username, password string, tlsSkipVerify bool) error { + reader := bufio.NewReader(conn) + tp := textproto.NewReader(reader) + + defer fmt.Fprintf(conn, "LOGOUT\r\n") + + // Read server greeting + greeting, err := tp.ReadLine() + if err != nil { + return err + } + + if !isOkResponseSieve(greeting) { + //goland:noinspection GoErrorStringFormat + return fmt.Errorf("Sieve greeting failed: %s", greeting) + } + + // Send STARTTLS command + fmt.Fprintf(conn, "STARTTLS\r\n") + + // Read STARTTLS response + response, err := tp.ReadLine() + if err != nil { + return err + } + + if !isOkResponseSieve(response) { + return fmt.Errorf("STARTTLS command failed: %s", response) + } + + // Upgrade to TLS connection + tlsConfig := &tls.Config{ + InsecureSkipVerify: tlsSkipVerify, + ServerName: hostname, + } + + tlsConn := tls.Client(conn, tlsConfig) + if err = tlsConn.Handshake(); err != nil { + return fmt.Errorf("TLS handshake failed: %s", err) + } + + if username == "" || password == "" { + return nil + } + + if !isTLSConnection(conn) { + return errors.ErrMissingTLS + } + + // Switch to TLS-wrapped connection for further communication + conn = tlsConn + reader = bufio.NewReader(conn) + tp = textproto.NewReader(reader) + + // Authenticate using PLAIN mechanism + authString := fmt.Sprintf("\x00%s\x00%s", username, password) + fmt.Fprintf(conn, "AUTHENTICATE \"PLAIN\" {%d+}\r\n%s", len(authString), authString) + + response, err = tp.ReadLine() + if err != nil { + return err + } + + if !isOkResponseSieve(response) { + //goland:noinspection GoErrorStringFormat + return fmt.Errorf("Sieve AUTHENTICATE command failed: %s", response) + } + + return nil +} + +// isOkResponseSieve checks if the Sieve server response starts with "OK", indicating a successful operation. +func isOkResponseSieve(response string) bool { + return response[:2] == "OK" +} + // checkHTTP performs an HTTP GET request with Basic Authentication using a given username and password. // It encodes the credentials, sends the request over a provided connection, and checks the response for success. // Errors related to request sending or response handling are logged and returned. @@ -313,6 +413,10 @@ func checkHTTP(conn net.Conn, hostname, requestURI, username, password string) e authHeader := "" if username != "" && password != "" { + if !isTLSConnection(conn) { + return errors.ErrMissingTLS + } + auth := username + ":" + password encoded := base64.StdEncoding.EncodeToString([]byte(auth)) authHeader = "Authorization: Basic " + encoded + "\r\n" From 143b813e9ef23037cd7453ddfec6291c2fac4fc2 Mon Sep 17 00:00:00 2001 From: Christian Roessner Date: Fri, 6 Dec 2024 12:31:22 +0100 Subject: [PATCH 7/7] Fix: Enhance TLS handling and simplify response checks Added TLS minimum version enforcement to improve security. Refactored the sieve response parsing to centralize logic and streamline error handling, improving maintainability and reducing redundancy. Signed-off-by: Christian Roessner --- server/monitoring/connection.go | 48 ++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/server/monitoring/connection.go b/server/monitoring/connection.go index 1a36fe97..69a6eb83 100644 --- a/server/monitoring/connection.go +++ b/server/monitoring/connection.go @@ -85,6 +85,7 @@ func checkBackendConnection(server *config.BackendServer) error { tlsConfig := &tls.Config{ InsecureSkipVerify: server.TLSSkipVerify, ServerName: server.Host, + MinVersion: tls.VersionTLS12, } tlsConn := tls.Client(conn, tlsConfig) @@ -336,27 +337,21 @@ func checkSieve(conn net.Conn, hostname, username, password string, tlsSkipVerif defer fmt.Fprintf(conn, "LOGOUT\r\n") - // Read server greeting - greeting, err := tp.ReadLine() - if err != nil { + // Wait for initial greeting + if response, err := isOkResponseSieve(tp); err != nil { return err - } - - if !isOkResponseSieve(greeting) { + } else if response != "OK" { //goland:noinspection GoErrorStringFormat - return fmt.Errorf("Sieve greeting failed: %s", greeting) + return fmt.Errorf("Sieve greeting failed: %s", response) } // Send STARTTLS command fmt.Fprintf(conn, "STARTTLS\r\n") // Read STARTTLS response - response, err := tp.ReadLine() - if err != nil { + if response, err := isOkResponseSieve(tp); err != nil { return err - } - - if !isOkResponseSieve(response) { + } else if response != "OK" { return fmt.Errorf("STARTTLS command failed: %s", response) } @@ -364,10 +359,11 @@ func checkSieve(conn net.Conn, hostname, username, password string, tlsSkipVerif tlsConfig := &tls.Config{ InsecureSkipVerify: tlsSkipVerify, ServerName: hostname, + MinVersion: tls.VersionTLS12, } tlsConn := tls.Client(conn, tlsConfig) - if err = tlsConn.Handshake(); err != nil { + if err := tlsConn.Handshake(); err != nil { return fmt.Errorf("TLS handshake failed: %s", err) } @@ -375,7 +371,7 @@ func checkSieve(conn net.Conn, hostname, username, password string, tlsSkipVerif return nil } - if !isTLSConnection(conn) { + if !isTLSConnection(tlsConn) { return errors.ErrMissingTLS } @@ -388,12 +384,9 @@ func checkSieve(conn net.Conn, hostname, username, password string, tlsSkipVerif authString := fmt.Sprintf("\x00%s\x00%s", username, password) fmt.Fprintf(conn, "AUTHENTICATE \"PLAIN\" {%d+}\r\n%s", len(authString), authString) - response, err = tp.ReadLine() - if err != nil { + if response, err := isOkResponseSieve(tp); err != nil { return err - } - - if !isOkResponseSieve(response) { + } else if response != "OK" { //goland:noinspection GoErrorStringFormat return fmt.Errorf("Sieve AUTHENTICATE command failed: %s", response) } @@ -402,8 +395,21 @@ func checkSieve(conn net.Conn, hostname, username, password string, tlsSkipVerif } // isOkResponseSieve checks if the Sieve server response starts with "OK", indicating a successful operation. -func isOkResponseSieve(response string) bool { - return response[:2] == "OK" +func isOkResponseSieve(tp *textproto.Reader) (response string, err error) { + for { + response, err = tp.ReadLine() + if err != nil { + return "", err + } + + if response[:2] == "OK" { + return response[:2], nil + } + + if response[:2] == "NO" { + return response, nil + } + } } // checkHTTP performs an HTTP GET request with Basic Authentication using a given username and password.