From 463c00e3f1b0a1282116138a7c61b2f8b300f8e4 Mon Sep 17 00:00:00 2001 From: Christian Roessner Date: Thu, 5 Dec 2024 16:33:00 +0100 Subject: [PATCH] Feat: Refactor backend server handling, unify connection methods Unified backend server handling by replacing separate IP and port with a single server struct across multiple modules. This refactor simplifies connection methods by integrating detailed server configurations, enabling improved handling of different protocols and connections, including enhanced TLS support and HAProxy v2 configurations. Signed-off-by: Christian Roessner --- server/config/features.go | 17 +- server/config/file.go | 2 +- server/definitions/const.go | 7 +- server/lua-plugins.d/filters/monitoring.lua | 24 +- server/lualib/backendresult.go | 3 +- server/lualib/connection.go | 70 +++-- server/lualib/connection_test.go | 120 -------- server/lualib/filter/filter.go | 112 ++++---- server/lualib/filter/filter_test.go | 8 +- server/main.go | 10 +- server/monitoring/connection.go | 302 ++++++++++++++++++-- 11 files changed, 399 insertions(+), 276 deletions(-) delete mode 100644 server/lualib/connection_test.go diff --git a/server/config/features.go b/server/config/features.go index 66e7ad7a..a55a1146 100644 --- a/server/config/features.go +++ b/server/config/features.go @@ -31,11 +31,15 @@ func (r *RelayDomainsSection) String() string { } type BackendServer struct { - Protocol string `mapstructure:"protocol"` - IP string `mapstructure:"ip"` - Port int `mapstructure:"port"` - TLS bool `mapstructure:"tls"` - HAProxyV2 bool `mapstructure:"haproxy_v2"` + Protocol string `mapstructure:"protocol"` + Host string `mapstructure:"host"` + RequestURI string `mapstructure:"request_uri"` + TestUsername string `mapstructure:"test_username"` + TestPassword string `mapstructure:"test_password"` + Port int `mapstructure:"port"` + TLS bool `mapstructure:"tls"` + TLSSkipVerify bool `mapstructure:"tls_skip_verify"` + HAProxyV2 bool `mapstructure:"haproxy_v2"` } func (n *BackendServer) String() string { @@ -43,7 +47,8 @@ func (n *BackendServer) String() string { return "BackendServer: " } - return fmt.Sprintf("BackendServers: {Protocol: %s, IP: %s, Port: %d}", n.Protocol, n.IP, n.Port) + return fmt.Sprintf("BackendServer: {Protocol: %s, Host: %s, RequestURI: %s, TestUsername: %s, TestPassword: , Port: %d, TLS: %t, TLSSkipVerify: %t, HAProxyV2: %t}", + n.Protocol, n.Host, n.RequestURI, n.TestUsername, n.Port, n.TLS, n.TLSSkipVerify, n.HAProxyV2) } type BackendServerMonitoring struct { diff --git a/server/config/file.go b/server/config/file.go index 0a41a309..61d335f3 100644 --- a/server/config/file.go +++ b/server/config/file.go @@ -145,7 +145,7 @@ func (f *File) GetBackendServerIP(protocol string) string { } if f.GetBackendServer(protocol) != nil { - return f.GetBackendServer(protocol).IP + return f.GetBackendServer(protocol).Host } return "" diff --git a/server/definitions/const.go b/server/definitions/const.go index bc070f20..1508be19 100644 --- a/server/definitions/const.go +++ b/server/definitions/const.go @@ -175,11 +175,8 @@ const ( // LogKeyLuaScripttimeout represents timeout setting for lua scripts LogKeyLuaScripttimeout = "lua_script_timeout" - // LogKeyBackendServerIP represents the IP address of the backend server. - LogKeyBackendServerIP = "backend_server_ip" - - // LogKeyBackendServerPort represents the port of the backend server. - LogKeyBackendServerPort = "backend_server_port" + // LogKeyBackendServer represents the IP address of the backend server. + LogKeyBackendServer = "backend_server" // NotAvailable is used when data for a particular field is not available. NotAvailable = "N/A" diff --git a/server/lua-plugins.d/filters/monitoring.lua b/server/lua-plugins.d/filters/monitoring.lua index 863aa6d8..dcbc43cd 100644 --- a/server/lua-plugins.d/filters/monitoring.lua +++ b/server/lua-plugins.d/filters/monitoring.lua @@ -151,14 +151,14 @@ function nauthilus_call_filter(request) if nauthilus_util.is_table(backend_servers) then num_of_bs = nauthilus_util.table_length(backend_servers) - local server_ip = "" - local new_server_ip = "" + local server_host = "" + local new_server_host = "" local session = get_dovecot_session() if session then local maybe_server = get_server_from_sessions(session) if maybe_server then - server_ip = maybe_server + server_host = maybe_server end end @@ -168,13 +168,13 @@ function nauthilus_call_filter(request) local b = nauthilus_backend_result.new() for _, server in ipairs(backend_servers) do - new_server_ip = server.ip + new_server_host = server.host - if server_ip == new_server_ip then - attributes["Proxy-Host"] = server_ip + if server_host == new_server_host then + attributes["Proxy-Host"] = server_host - add_session(session, server_ip) - nauthilus_builtin.custom_log_add(N .. "_backend_server_current", server_ip) + add_session(session, server_host) + nauthilus_builtin.custom_log_add(N .. "_backend_server_current", server_host) b:attributes(attributes) nauthilus_backend.apply_backend_result(b) @@ -183,13 +183,13 @@ function nauthilus_call_filter(request) end end - if server_ip ~= new_server_ip then + if server_host ~= new_server_host then -- Put your own logic here to select a proper server for the user. In this demo, the last server -- available is always used. - attributes["Proxy-Host"] = new_server_ip + attributes["Proxy-Host"] = new_server_host - add_session(session, new_server_ip) - nauthilus_builtin.custom_log_add(N .. "_backend_server_new", new_server_ip) + add_session(session, new_server_host) + nauthilus_builtin.custom_log_add(N .. "_backend_server_new", new_server_host) b:attributes(attributes) nauthilus_backend.apply_backend_result(b) diff --git a/server/lualib/backendresult.go b/server/lualib/backendresult.go index ce1b66be..1faac14f 100644 --- a/server/lualib/backendresult.go +++ b/server/lualib/backendresult.go @@ -249,7 +249,8 @@ func backendResultGetSetAttributes(L *lua.LState) int { backendResult := checkBackendResult(L) if L.GetTop() == 2 { - backendResult.Attributes = convert.LuaValueToGo(L.CheckTable(2)).(map[any]any) + attributes := convert.LuaValueToGo(L.CheckTable(2)).(map[any]any) + backendResult.Attributes = attributes return 0 } diff --git a/server/lualib/connection.go b/server/lualib/connection.go index a016ec55..df18909b 100644 --- a/server/lualib/connection.go +++ b/server/lualib/connection.go @@ -16,56 +16,50 @@ package lualib import ( + "github.com/croessner/nauthilus/server/config" "github.com/croessner/nauthilus/server/monitoring" lua "github.com/yuin/gopher-lua" ) -// CheckBackendConnection is a Lua function that checks the connection to a backend server. -// It receives the server IP address, port number, a boolean flag indicating whether the server runs with HAProxy V2 protocol, -// and a boolean flag indicating whether TLS should be used. -// The function calls the CheckBackendConnection method of the provided monitoring.Monitor instance -// and returns an error message if there is an error, or nil if the connection is successful. +// getNumberFromTable retrieves an integer value from a Lua table for the given key. If the key is missing or nil, returns 0. // -// Params: -// - monitor monitoring.Monitor : The monitoring.Monitor instance used to check the backend connection. +// Parameters: +// - table: The Lua table to search in. +// - key: The key to look for in the table. // // Returns: -// - int : The number of return values pushed to the Lua stack, always 1. -// -// Lua stack requirements: -// - 4 arguments are expected in the following order: -// 1. string : The server IP address. -// 2. int : The server port number. -// 3. boolean : Whether the server runs with HAProxy V2 protocol. -// 4. boolean : Whether TLS should be used. -// - The arguments should be of the expected types; otherwise, an error will be raised. -// - The function expects to have 1 return value on the stack - nil if the connection is successful, -// or a string with an error message if there is an error. -// -// Example: -// -// connection_error = check_backend_connection("192.168.0.1", 8080, false, true) -// if connection_error ~= nil then -// log("Connection failed: " .. connection_error) -// else -// log("Connection successful") -// end -// -// Note: The above example is in Lua language and should be executed in a Lua environment. +// - The integer value associated with the key, or 0 if the key is not present or value is nil. +func getNumberFromTable(table *lua.LTable, key string) int { + value := table.RawGet(lua.LString(key)) + + if value == nil { + return 0 + } + + return int(value.(lua.LNumber)) +} + +// CheckBackendConnection attempts to verify the connection to a backend server using the provided monitor. +// It extracts necessary configuration details such as protocol, IP address, port, and credentials from the given Lua table. +// The function then calls the monitor's CheckBackendConnection method to perform the actual connectivity check. +// If the connection check encounters an error, this error is pushed onto the Lua stack. +// If the connection check is successful, a nil value is pushed onto the Lua stack. +// It returns an integer indicating the number of results pushed onto the Lua stack. func CheckBackendConnection(monitor monitoring.Monitor) lua.LGFunction { return func(L *lua.LState) int { - if L.GetTop() != 4 { - L.RaiseError("Invalid number of arguments. Expected 4, got %d", L.GetTop()) + table := L.CheckTable(1) - return 0 - } + server := &config.BackendServer{} - server := L.CheckString(1) - port := L.CheckInt(2) - haproxyV2 := L.CheckBool(3) - tls := L.CheckBool(4) + server.Protocol = getStringFromTable(table, "protocol") + server.Host = getStringFromTable(table, "ip_address") + server.Port = getNumberFromTable(table, "port") + server.HAProxyV2 = getBoolFromTable(table, "haproxy_v2") + server.TLS = getBoolFromTable(table, "tls") + server.TestUsername = getStringFromTable(table, "test_username") + server.TestPassword = getStringFromTable(table, "test_password") - if err := monitor.CheckBackendConnection(server, port, haproxyV2, tls); err != nil { + if err := monitor.CheckBackendConnection(server); err != nil { L.Push(lua.LString(err.Error())) return 1 diff --git a/server/lualib/connection_test.go b/server/lualib/connection_test.go deleted file mode 100644 index d59502c0..00000000 --- a/server/lualib/connection_test.go +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright (C) 2024 Christian Rößner -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package lualib - -import ( - "errors" - "fmt" - "strings" - "testing" - - lua "github.com/yuin/gopher-lua" -) - -type mockMonitor struct { - fail bool -} - -func (m *mockMonitor) CheckBackendConnection(_ string, _ int, _ bool, _ bool) error { - if m.fail { - return errors.New("connection failed") - } - - return nil -} - -func TestCheckBackendConnection(t *testing.T) { - monitor := &mockMonitor{} - checkBackendConnection := CheckBackendConnection(monitor) - - testCases := []struct { - desc string - parameters []lua.LValue - expected lua.LValue - fail bool - }{ - { - desc: "successful connection", - parameters: []lua.LValue{ - lua.LString("127.0.0.1"), - lua.LNumber(143), - lua.LTrue, - lua.LFalse, - }, - expected: lua.LNil, - fail: false, - }, - { - desc: "failed connection", - parameters: []lua.LValue{ - lua.LString("127.0.0.1"), - lua.LNumber(143), - lua.LTrue, - lua.LFalse, - }, - expected: lua.LString("connection failed"), - fail: true, - }, - { - desc: "missing parameters", - parameters: []lua.LValue{lua.LString("127.0.0.1")}, - expected: lua.LString("Invalid number of arguments. Expected 4, got 1"), - fail: true, - }, - } - - for _, tc := range testCases { - t.Run(tc.desc, func(t *testing.T) { - defer func() { - if r := recover(); r != nil { - if tc.fail { - recoveredMessage := strings.Split(fmt.Sprintf("%v", r), "\n")[0] - expectedMessage := strings.Split(fmt.Sprintf("%v", tc.expected), "\n")[0] - - if strings.TrimSpace(recoveredMessage) == strings.TrimSpace(expectedMessage) { - return - } - - t.Errorf("Expected panic with message '%s', got '%s'", expectedMessage, recoveredMessage) - } else { - t.Errorf("Unexpected panic: %v", r) - } - } - }() - - L := lua.NewState() - - defer L.Close() - - for _, p := range tc.parameters { - L.Push(p) - } - - monitor.fail = tc.fail - - if r := checkBackendConnection(L); r != 1 { - t.Errorf("Expected return value to be 1, got %d", r) - } - - if !tc.fail { - gotResult := L.Get(-1) - if gotResult.Type() != tc.expected.Type() && gotResult.String() != tc.expected.String() { - t.Errorf("Expected lua return value of '%s', got '%s'", tc.expected.String(), L.Get(-1).String()) - } - } - }) - } -} diff --git a/server/lualib/filter/filter.go b/server/lualib/filter/filter.go index be35571c..1388b0d9 100644 --- a/server/lualib/filter/filter.go +++ b/server/lualib/filter/filter.go @@ -34,7 +34,7 @@ import ( "github.com/gin-gonic/gin" "github.com/go-kit/log/level" "github.com/spf13/viper" - lua "github.com/yuin/gopher-lua" + "github.com/yuin/gopher-lua" ) // httpClient is a pre-configured instance of http.Client with custom timeout and TLS settings for making HTTP requests. @@ -255,11 +255,15 @@ func NewLuaFilter(name string, scriptPath string) (*LuaFilter, error) { }, nil } +// Request represents a structure used for handling and processing requests within the system. type Request struct { + // BackendServers holds a list of backend server configurations that are used for handling requests. BackendServers []*config.BackendServer + // UsedBackendAddress indicates the specific backend server address selected for processing the current request. UsedBackendAddress *string + // UsedBackendPort represents the port of the backend server that was used for the current request execution. UsedBackendPort *int // Log is used to capture logging information. @@ -268,15 +272,38 @@ type Request struct { // Context includes context data from the caller. *lualib.Context + // CommonRequest represents a common request object with various properties used in different functionalities. *lualib.CommonRequest } +// LuaBackendServer represents a server configuration for a Lua script backend. type LuaBackendServer struct { - Protocol string - IP string - Port int + // 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 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: @@ -301,14 +328,22 @@ func indexMethod(L *lua.LState) int { switch field { case "protocol": L.Push(lua.LString(server.Protocol)) - case "ip": - L.Push(lua.LString(server.IP)) + case "host": + L.Push(lua.LString(server.Host)) case "port": L.Push(lua.LNumber(server.Port)) + case "request_url": + L.Push(lua.LString(server.RequestURL)) + case "test_username": + L.Push(lua.LString(server.TestUsername)) + case "test_password": + L.Push(lua.LString(server.TestPassword)) case "haproxy_v2": L.Push(lua.LBool(server.HAProxyV2)) case "tls": L.Push(lua.LBool(server.TLS)) + case "tls_skip_verify": + L.Push(lua.LBool(server.TLSSKipVerify)) default: return 0 // The field does not exist } @@ -316,15 +351,7 @@ func indexMethod(L *lua.LState) int { return 1 // Number of return values } -// getBackendServers is a higher-order function that returns a LGFunction. -// The returned LGFunction creates a new Lua table and populates it with userdata objects representing backend servers. -// Each userdata object has a metatable set, allowing Lua code to index the object and retrieve its properties. -// The userdata objects are created based on the provided backendServers slice. -// The userdata values are instances of the LuaBackendServer struct, with Protocol, IP, Port, and HAProxyV2 fields. -// The metatable of the userdata objects has __index method set to the indexMethod function. -// The indexMethod function retrieves the corresponding property value from the userdata object based on the requested field name. -// The userdata objects are added to the created Lua table. -// The created Lua table is pushed onto the Lua stack before returning from the LGFunction. +// getBackendServers creates a Lua function that returns a table of backend server configurations as userdata. func getBackendServers(backendServers []*config.BackendServer) lua.LGFunction { return func(L *lua.LState) int { servers := L.NewTable() @@ -343,11 +370,15 @@ func getBackendServers(backendServers []*config.BackendServer) lua.LGFunction { serverUserData := L.NewUserData() serverUserData.Value = &LuaBackendServer{ - Protocol: backendServer.Protocol, - IP: backendServer.IP, - Port: backendServer.Port, - HAProxyV2: backendServer.HAProxyV2, - TLS: backendServer.TLS, + 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, } L.SetMetatable(serverUserData, L.GetTypeMetatable(definitions.LuaBackendServerTypeName)) @@ -362,15 +393,7 @@ func getBackendServers(backendServers []*config.BackendServer) lua.LGFunction { } } -// selectBackendServer is a function that takes a server pointer (expected to be a string) and a port -// pointer (expected to be an integer) as parameters. It returns a Lua function. This Lua function -// wraps the functionality of checking the count of passed arguments and assigning the values of -// server and port based on Lua's stack. The Lua function throws an error if the count of passed -// arguments is not 2. If the argument count is correct, it gets the server and port values from the -// 1st and the 2nd positions in the Lua stack respectively, and assigns them to the server and port pointers. -// -// It's important to note that this function doesn't perform any kind of connection or communication -// with a server or port. It only assigns values based on Lua stack positions. +// selectBackendServer returns a Lua function that assigns a server address and port from Lua state arguments. func selectBackendServer(server **string, port **int) lua.LGFunction { return func(L *lua.LState) int { if L.GetTop() != 2 { @@ -389,16 +412,7 @@ func selectBackendServer(server **string, port **int) lua.LGFunction { } } -// applyBackendResult is a function that returns a Lua LGFunction. -// The returned function is used to assign the value of the backendResult to the LuaBackendResult -// extracted from the provided user data. If the user data does not contain a LuaBackendResult, -// the backendResult remains unchanged. -// -// Params: -// - backendResult: A double pointer to a LuaBackendResult -// -// Returns: -// - A Lua LGFunction that assigns the value of the userData to the backendResult +// applyBackendResult sets the backendResult pointer to the value from Lua userdata if it's of type LuaBackendResult. func applyBackendResult(backendResult **lualib.LuaBackendResult) lua.LGFunction { return func(L *lua.LState) int { userData := L.CheckUserData(1) @@ -413,22 +427,8 @@ func applyBackendResult(backendResult **lualib.LuaBackendResult) lua.LGFunction } } -// removeFromBackendResult is a function that creates and returns a Lua LGFunction. -// The LGFunction takes a Lua state as argument and modifies a slice (attributes) -// by appending values from a Lua table passed as argument to the LGFunction. -// The function returns 0, indicating no values are returned to Lua. -// If the attributes slice is nil, the function returns 0 immediately. -// The function extracts a Lua table from the Lua stack and iterates over its -// values. For each value, it appends its string representation to the attributes slice. -// Finally, the function returns 0 to Lua. -// -// Params: -// -// attributes *[]string : Pointer to a slice of strings to store the extracted attributes -// -// Returns: -// -// the LGFunction that takes a Lua state as argument and modifies the attributes slice +// removeFromBackendResult is a Lua function generator that populates a given slice with strings +// from a Lua table passed as an argument. If the attributes slice is nil, the function does nothing. func removeFromBackendResult(attributes *[]string) lua.LGFunction { return func(L *lua.LState) int { if attributes == nil { diff --git a/server/lualib/filter/filter_test.go b/server/lualib/filter/filter_test.go index fe7a815c..dbfc66b8 100644 --- a/server/lualib/filter/filter_test.go +++ b/server/lualib/filter/filter_test.go @@ -19,7 +19,7 @@ import ( "testing" "github.com/croessner/nauthilus/server/config" - lua "github.com/yuin/gopher-lua" + "github.com/yuin/gopher-lua" ) func TestGetBackendServers(t *testing.T) { @@ -38,7 +38,7 @@ func TestGetBackendServers(t *testing.T) { serversInput: []*config.BackendServer{ { Protocol: "http", - IP: "192.168.1.1", + Host: "192.168.1.1", Port: 8000, HAProxyV2: false, TLS: false, @@ -51,7 +51,7 @@ func TestGetBackendServers(t *testing.T) { serversInput: []*config.BackendServer{ { Protocol: "http", - IP: "192.168.1.1", + Host: "192.168.1.1", Port: 8000, HAProxyV2: false, TLS: false, @@ -59,7 +59,7 @@ func TestGetBackendServers(t *testing.T) { nil, { Protocol: "https", - IP: "192.168.1.2", + Host: "192.168.1.2", Port: 443, HAProxyV2: true, TLS: true, diff --git a/server/main.go b/server/main.go index c19bfe73..d252a18a 100644 --- a/server/main.go +++ b/server/main.go @@ -853,9 +853,7 @@ func logBackendServerError(server *config.BackendServer, err error) { level.Error(log.Logger).Log( definitions.LogKeyMsg, err, definitions.LogKeyMsg, "Server down", - definitions.LogKeyProtocol, server.Protocol, - definitions.LogKeyBackendServerIP, server.IP, - definitions.LogKeyBackendServerPort, server.Port, + definitions.LogKeyBackendServer, server, ) } @@ -869,9 +867,7 @@ func logBackendServerDebug(server *config.BackendServer) { util.DebugModule( definitions.DbgFeature, definitions.LogKeyMsg, "Server alive", - definitions.LogKeyProtocol, server.Protocol, - definitions.LogKeyBackendServerIP, server.IP, - definitions.LogKeyBackendServerPort, server.Port, + definitions.LogKeyBackendServer, server, ) } @@ -911,7 +907,7 @@ func loopBackendServersHealthCheck(servers []*config.BackendServer, oldBackendSe for _, server := range servers { go func(server *config.BackendServer) { - err := monitoring.NewMonitor().CheckBackendConnection(server.IP, server.Port, server.HAProxyV2, server.TLS) + err := monitoring.NewMonitor().CheckBackendConnection(server) backendServersLiveness.mu.Lock() diff --git a/server/monitoring/connection.go b/server/monitoring/connection.go index eac90dd8..71612c32 100644 --- a/server/monitoring/connection.go +++ b/server/monitoring/connection.go @@ -16,39 +16,36 @@ package monitoring import ( + "bufio" + "bytes" "crypto/tls" + "encoding/base64" "fmt" "net" + "net/textproto" "time" + "github.com/croessner/nauthilus/server/config" "github.com/croessner/nauthilus/server/definitions" "github.com/croessner/nauthilus/server/log" "github.com/go-kit/log/level" "github.com/pires/go-proxyproto" ) -// Monitor represents an interface used to check the backend connection of a server. -// It provides the method CheckBackendConnection to perform the check and returns an error if the connection fails. -// The CheckBackendConnection method takes the IP address, port number, whether the server runs with HAProxy V2 protocol, -// and whether TLS should be used as parameters. +// Monitor defines an interface for monitoring and checking backend server connections. +// It provides a method to verify connectivity using specified configurations. type Monitor interface { - // CheckBackendConnection checks the backend connection of a server based on the provided IP address, port number, whether the server runs with HAProxy V2 protocol, and whether TLS should be used. It returns an error if the connection fails. - CheckBackendConnection(ipAddress string, port int, haproxyv2 bool, useTLS bool) error + // CheckBackendConnection checks the backend connection of a server based on the provided Host address, port number, whether the server runs with HAProxy V2 protocol, and whether TLS should be used. It returns an error if the connection fails. + CheckBackendConnection(server *config.BackendServer) error } -// ConnMonitor represents a connection monitor that can be used to check the availability of a backend server -// by establishing a TCP connection with the specified IP address and port. -// It provides a method `CheckBackendConnection` to perform the check and returns an error if the connection -// cannot be established within the timeout period. This monitor does not retry the connection and closes -// the connection before returning. +// ConnMonitor is a struct that implements monitoring of backend server connections by checking their availability. type ConnMonitor struct{} -// CheckBackendConnection checks the availability of a backend server by trying to -// establish a TCP connection with the specified IP address and port. -// It returns an error if the connection cannot be established within the timeout period. -// The function does not retry the connection and closes the connection before returning. -func (ConnMonitor) CheckBackendConnection(ipAddress string, port int, haproxyv2 bool, useTLS bool) error { - return checkBackendConnection(ipAddress, port, haproxyv2, useTLS) +// CheckBackendConnection attempts to establish a connection to a backend server to verify its availability. +// It returns an error if the connection cannot be established, using the specified configuration parameters. +func (ConnMonitor) CheckBackendConnection(server *config.BackendServer) error { + return checkBackendConnection(server) } var _ Monitor = (*ConnMonitor)(nil) @@ -58,29 +55,30 @@ func NewMonitor() Monitor { return &ConnMonitor{} } -// checkBackendConnection checks the availability of a backend server by trying to establish a TCP connection with the specified IP address and port. -// It returns an error if the connection cannot be established within the timeout period. -// The function does not retry the connection and closes the connection before returning. -func checkBackendConnection(ipAddress string, port int, haproxyV2 bool, useTLS bool) error { +// checkBackendConnection attempts to establish a TCP connection to a specified backend server within a given timeout period. +// If the backend requires HAProxy v2 protocol, it sends the necessary headers. For secure connections, it performs a TLS handshake. +// Upon successful connection, it handles different protocols using the provided server settings. It returns an error if any step fails. +func checkBackendConnection(server *config.BackendServer) error { timeout := 5 * time.Second - conn, err := net.DialTimeout("tcp", net.JoinHostPort(ipAddress, fmt.Sprintf("%d", port)), timeout) + conn, err := net.DialTimeout("tcp", net.JoinHostPort(server.Host, fmt.Sprintf("%d", server.Port)), timeout) if err != nil { return err } defer conn.Close() - if haproxyV2 { - if err = checkHAproxyV2(conn, ipAddress, port); err != nil { + if server.HAProxyV2 { + if err = checkHAproxyV2(conn, server.Host, server.Port); err != nil { return err } } - if useTLS { + if server.TLS { // Securing the connection tlsConfig := &tls.Config{ - InsecureSkipVerify: true, + InsecureSkipVerify: server.TLSSkipVerify, + ServerName: server.Host, } tlsConn := tls.Client(conn, tlsConfig) @@ -95,9 +93,260 @@ func checkBackendConnection(ipAddress string, port int, haproxyV2 bool, useTLS b conn = net.Conn(tlsConn) } + handleProtocol(server, conn) + + return nil +} + +// 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) { + // Limited support only. Plain connections requireing StartTLS are not supported at the moment! + switch server.Protocol { + case "smtp": + checkSMTP(conn, server.TestUsername, server.TestPassword) + case "pop3": + checkPOP3(conn, server.TestUsername, server.TestPassword) + case "imap": + checkIMAP(conn, server.TestUsername, server.TestPassword) + case "http": + checkHTTP(conn, server.Host, server.RequestURI, server.TestUsername, server.TestPassword) + default: + level.Warn(log.Logger).Log(definitions.LogKeyMsg, "Unsupported protocol", "protocol", server.Protocol) + } +} + +// 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) { + reader := bufio.NewReader(conn) + tp := textproto.NewReader(reader) + + defer fmt.Fprintf(conn, "QUIT\r\n") + + _, err := tp.ReadLine() + if err != nil { + level.Error(log.Logger).Log("Error reading SMTP initial response", "error", err) + + return + } + + fmt.Fprintf(conn, "EHLO localhost\r\n") + for { + response, err := tp.ReadLine() + if err != nil { + level.Error(log.Logger).Log("Error reading SMTP EHLO response", "error", err) + + return + } + + if response[:3] != "250" { + if response[0] >= '4' { + level.Error(log.Logger).Log("EHLO command failed", "response", response) + + return + } + + break + } + } + + if username == "" || password == "" { + return + } + + fmt.Fprintf(conn, "AUTH LOGIN\r\n") + + _, err = tp.ReadLine() + if err != nil { + level.Error(log.Logger).Log("Error in SMTP AUTH LOGIN", "error", err) + + return + } + + usernameEnc := base64.StdEncoding.EncodeToString([]byte(username)) + + fmt.Fprintf(conn, "%s\r\n", usernameEnc) + + _, err = tp.ReadLine() + if err != nil { + level.Error(log.Logger).Log("Error sending SMTP username", "error", err) + + return + } + + passwordEnc := base64.StdEncoding.EncodeToString([]byte(password)) + + fmt.Fprintf(conn, "%s\r\n", passwordEnc) + + response, err := tp.ReadLine() + if err != nil || response[:3] != "235" { + level.Error(log.Logger).Log("SMTP AUTH LOGIN failed", "error", err, "response", response) + + return + } +} + +// 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) { + reader := bufio.NewReader(conn) + tp := textproto.NewReader(reader) + + defer fmt.Fprintf(conn, "QUIT\r\n") + + greeting, err := tp.ReadLine() + if err != nil { + level.Error(log.Logger).Log(fmt.Sprintf("Error reading POP3 greeting: %v\n", err)) + + return + } + + if !isOkResponsePOP3(greeting) { + level.Error(log.Logger).Log(fmt.Sprintf("POP3 greeting failed: %s\n", greeting)) + + return + } + + if username == "" || password == "" { + return + } + + fmt.Fprintf(conn, "USER %s\r\n", username) + + response, err := tp.ReadLine() + if err != nil { + level.Error(log.Logger).Log(fmt.Sprintf("Error reading POP3 response after USER command: %v\n", err)) + + return + } + + if !isOkResponsePOP3(response) { + level.Error(log.Logger).Log(fmt.Sprintf("POP3 USER command failed: %s\n", response)) + + return + } + + fmt.Fprintf(conn, "PASS %s\r\n", password) + + response, err = tp.ReadLine() + if err != nil { + level.Error(log.Logger).Log(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)) + } +} + +// isOkResponsePOP3 checks if the provided POP3 server response starts with "+OK". +func isOkResponsePOP3(response string) bool { + return bytes.HasPrefix([]byte(response), []byte("+OK")) +} + +// 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) { + reader := bufio.NewReader(conn) + tp := textproto.NewReader(reader) + + defer fmt.Fprintf(conn, "a2 LOGOUT\r\n") + + greeting, err := tp.ReadLine() + if err != nil { + level.Error(log.Logger).Log(definitions.LogKeyMsg, "Error reading IMAP greeting", "error", err) + return + } + + if !isOkResponseIMAP(greeting) { + level.Error(log.Logger).Log(definitions.LogKeyMsg, "IMAP greeting failed", "response", greeting) + + return + } + + if username == "" || password == "" { + return + } + + 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, "Error reading IMAP response", "error", err) + + return + } + + if !isOkResponseIMAP(response) { + level.Error(log.Logger).Log(definitions.LogKeyMsg, "IMAP login failed", "response", response) + } +} + +// isOkResponseIMAP checks if the given IMAP server response starts with "* OK" or "a1 OK". +func isOkResponseIMAP(response string) bool { + return bytes.HasPrefix([]byte(response), []byte("* OK")) || bytes.HasPrefix([]byte(response), []byte("a1 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. +func checkHTTP(conn net.Conn, hostname, requestURI, username, password string) error { + authHeader := "" + + if username != "" && password != "" { + auth := username + ":" + password + encoded := base64.StdEncoding.EncodeToString([]byte(auth)) + authHeader = "Authorization: Basic " + encoded + "\r\n" + } + + if requestURI == "" { + requestURI = "/" + } + + _, 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) + + return err + } + + reader := bufio.NewReader(conn) + tp := textproto.NewReader(reader) + + statusLine, err := tp.ReadLine() + if err != nil { + level.Error(log.Logger).Log(definitions.LogKeyMsg, "Error reading HTTP status line", "error", err) + + return err + } + + if !isOkResponseHTTP(statusLine) { + level.Error(log.Logger).Log(definitions.LogKeyMsg, "HTTP request failed", "response", 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) + + return err + } + return nil } +// isOkResponseHTTP checks if the given HTTP response starts with "HTTP/1.1 200", indicating a successful response. +func isOkResponseHTTP(response string) bool { + return response[:12] == "HTTP/1.1 200" +} + +// checkHAproxyV2 sends a HAProxy protocol v2 header to the given connection with specified Host address and port. +// It returns an error if writing the header to the connection fails. The error is also logged for diagnostics. func checkHAproxyV2(conn net.Conn, ipAddress string, port int) error { header := &proxyproto.Header{ Command: proxyproto.LOCAL, @@ -120,6 +369,7 @@ func checkHAproxyV2(conn net.Conn, ipAddress string, port int) error { return err } +// handleHAproxyV2Error logs an error related to HAProxy version 2 operations using the global Logger. func handleHAproxyV2Error(err error) { level.Error(log.Logger).Log( definitions.LogKeyInstance, definitions.InstanceName,