diff --git a/Cargo.lock b/Cargo.lock index 97671c7..1935e4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,9 +52,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.56" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96cf8829f67d2eab0b2dfa42c5d0ef737e0724e4a82b01b3e292456202b19716" +checksum = "1cd7fce9ba8c3c042128ce72d8b2ddbf3a05747efb67ea0313c635e10bda47a2" dependencies = [ "proc-macro2", "quote", @@ -2007,6 +2007,7 @@ name = "xmpp-proxy" version = "1.0.0" dependencies = [ "anyhow", + "async-trait", "data-encoding", "die", "env_logger", diff --git a/Cargo.toml b/Cargo.toml index dcdf76f..f3d8283 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ anyhow = "1.0" tokio = { version = "1.9", features = ["net", "rt", "rt-multi-thread", "macros", "io-util", "signal"] } ring = "0.16" data-encoding = "2.3" +async-trait = "0.1.64" # logging deps diff --git a/contrib/prosody-modules/mod_net_proxy.lua b/contrib/prosody-modules/mod_net_proxy.lua new file mode 100644 index 0000000..d4b7d2b --- /dev/null +++ b/contrib/prosody-modules/mod_net_proxy.lua @@ -0,0 +1,467 @@ +-- mod_net_proxy.lua +-- Copyright (C) 2018 Pascal Mathis +-- +-- Implementation of PROXY protocol versions 1 and 2 +-- Specifications: https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt + +module:set_global(); + +-- Imports +local softreq = require "util.dependencies".softreq; +local bit = assert(softreq "bit" or softreq "bit32" or softreq "util.bitcompat", "No bit module found. See https://prosody.im/doc/depends#bitop"); +local hex = require "util.hex"; +local ip = require "util.ip"; +local net = require "util.net"; +local set = require "util.set"; +local portmanager = require "core.portmanager"; + +-- Backwards Compatibility +local function net_ntop_bc(input) + if input:len() == 4 then + return string.format("%d.%d.%d.%d", input:byte(1, 4)); + elseif input:len() == 16 then + local octets = { nil, nil, nil, nil, nil, nil, nil, nil }; + + -- Convert received bytes into IPv6 address and skip leading zeroes for each group + for index = 1, 8 do + local high, low = input:byte(index * 2 - 1, index * 2); + octets[index] = string.format("%x", high * 256 + low); + end + local address = table.concat(octets, ":", 1, 8); + + -- Search for the longest sequence of zeroes + local token; + local length = (address:match("^0:[0:]+()") or 1) - 1; + for s in address:gmatch(":0:[0:]+") do + if length < #s then + length, token = #s, s; + end + end + + -- Return the shortened IPv6 address + return address:gsub(token or "^0:[0:]+", "::", 1); + end +end + +local net_ntop = net.ntop or net_ntop_bc + +-- Utility Functions +local function _table_invert(input) + local output = {}; + for key, value in pairs(input) do + output[value] = key; + end + return output; +end + +-- Constants +local ADDR_FAMILY = { UNSPEC = 0x0, INET = 0x1, INET6 = 0x2, UNIX = 0x3 }; +local ADDR_FAMILY_STR = _table_invert(ADDR_FAMILY); +local TRANSPORT = { UNSPEC = 0x0, STREAM = 0x1, DGRAM = 0x2 }; +local TRANSPORT_STR = _table_invert(TRANSPORT); + +local PROTO_MAX_HEADER_LENGTH = 256; +local PROTO_HANDLERS = { + PROXYv1 = { signature = hex.from("50524F5859"), callback = nil }, + PROXYv2 = { signature = hex.from("0D0A0D0A000D0A515549540A"), callback = nil } +}; +local PROTO_HANDLER_STATUS = { SUCCESS = 0, POSTPONE = 1, FAILURE = 2 }; + +-- Configuration Variables +local config_mappings = module:get_option("proxy_port_mappings", {}); +local config_ports = module:get_option_set("proxy_ports", {}); +local config_trusted_proxies = module:get_option_set("proxy_trusted_proxies", {"127.0.0.1", "::1"}); + +-- Persistent In-Memory Storage +local sessions = {}; +local mappings = {}; +local trusted_networks = set.new(); + +-- Proxy Data Methods +local proxy_data_mt = {}; proxy_data_mt.__index = proxy_data_mt; + +function proxy_data_mt:describe() + return string.format("proto=%s/%s src=%s:%d dst=%s:%d", + self:addr_family_str(), self:transport_str(), self:src_addr(), self:src_port(), self:dst_addr(), self:dst_port()); +end + +function proxy_data_mt:addr_family_str() + return ADDR_FAMILY_STR[self._addr_family] or ADDR_FAMILY_STR[ADDR_FAMILY.UNSPEC]; +end + +function proxy_data_mt:transport_str() + return TRANSPORT_STR[self._transport] or TRANSPORT_STR[TRANSPORT.UNSPEC]; +end + +function proxy_data_mt:version() + return self._version; +end + +function proxy_data_mt:addr_family() + return self._addr_family; +end + +function proxy_data_mt:transport() + return self._transport; +end + +function proxy_data_mt:src_addr() + return self._src_addr; +end + +function proxy_data_mt:src_port() + return self._src_port; +end + +function proxy_data_mt:dst_addr() + return self._dst_addr; +end + +function proxy_data_mt:dst_port() + return self._dst_port; +end + +-- Protocol Handler Functions +PROTO_HANDLERS["PROXYv1"].callback = function(conn, session) + local addr_family_mappings = { TCP4 = ADDR_FAMILY.INET, TCP6 = ADDR_FAMILY.INET6 }; + + -- Postpone processing if CRLF (PROXYv1 header terminator) does not exist within buffer + if session.buffer:find("\r\n") == nil then + return PROTO_HANDLER_STATUS.POSTPONE, nil; + end + + -- Declare header pattern and match current buffer against pattern + local header_pattern = "^PROXY (%S+) (%S+) (%S+) (%d+) (%d+)\r\n"; + local addr_family, src_addr, dst_addr, src_port, dst_port = session.buffer:match(header_pattern); + src_port, dst_port = tonumber(src_port), tonumber(dst_port); + + -- Ensure that header was successfully parsed and contains a valid address family + if addr_family == nil or src_addr == nil or dst_addr == nil or src_port == nil or dst_port == nil then + module:log("warn", "Received unparseable PROXYv1 header from %s", conn:ip()); + return PROTO_HANDLER_STATUS.FAILURE, nil; + end + if addr_family_mappings[addr_family] == nil then + module:log("warn", "Received invalid PROXYv1 address family from %s: %s", conn:ip(), addr_family); + return PROTO_HANDLER_STATUS.FAILURE, nil; + end + + -- Ensure that received source and destination ports are within 1 and 65535 (0xFFFF) + if src_port <= 0 or src_port >= 0xFFFF then + module:log("warn", "Received invalid PROXYv1 source port from %s: %d", conn:ip(), src_port); + return PROTO_HANDLER_STATUS.FAILURE, nil; + end + if dst_port <= 0 or dst_port >= 0xFFFF then + module:log("warn", "Received invalid PROXYv1 destination port from %s: %d", conn:ip(), dst_port); + return PROTO_HANDLER_STATUS.FAILURE, nil; + end + + -- Ensure that received source and destination address can be parsed + local _, err = ip.new_ip(src_addr); + if err ~= nil then + module:log("warn", "Received unparseable PROXYv1 source address from %s: %s", conn:ip(), src_addr); + return PROTO_HANDLER_STATUS.FAILURE, nil; + end + _, err = ip.new_ip(dst_addr); + if err ~= nil then + module:log("warn", "Received unparseable PROXYv1 destination address from %s: %s", conn:ip(), dst_addr); + return PROTO_HANDLER_STATUS.FAILURE, nil; + end + + -- Strip parsed header from session buffer and build proxy data + session.buffer = session.buffer:gsub(header_pattern, ""); + + local proxy_data = { + _version = 1, + _addr_family = addr_family, _transport = TRANSPORT.STREAM, + _src_addr = src_addr, _src_port = src_port, + _dst_addr = dst_addr, _dst_port = dst_port + }; + setmetatable(proxy_data, proxy_data_mt); + + -- Return successful response with gathered proxy data + return PROTO_HANDLER_STATUS.SUCCESS, proxy_data; +end + +PROTO_HANDLERS["PROXYv2"].callback = function(conn, session) + -- Postpone processing if less than 16 bytes are available + if #session.buffer < 16 then + return PROTO_HANDLER_STATUS.POSTPONE, nil; + end + + -- Parse first 16 bytes of protocol header + local version = bit.rshift(bit.band(session.buffer:byte(13), 0xF0), 4); + local command = bit.band(session.buffer:byte(13), 0x0F); + local addr_family = bit.rshift(bit.band(session.buffer:byte(14), 0xF0), 4); + local transport = bit.band(session.buffer:byte(14), 0x0F); + local length = bit.bor(session.buffer:byte(16), bit.lshift(session.buffer:byte(15), 8)); + + -- Postpone processing if less than 16+ bytes are available + if #session.buffer < 16 + length then + return PROTO_HANDLER_STATUS.POSTPONE, nil; + end + + -- Ensure that version number is correct + if version ~= 0x2 then + module:log("warn", "Received unsupported PROXYv2 version from %s: %d", conn:ip(), version); + return PROTO_HANDLER_STATUS.FAILURE, nil; + end + + local payload = session.buffer:sub(17); + if command == 0x0 then + -- Gather source/destination addresses and ports from local socket + local src_addr, src_port = conn:socket():getpeername(); + local dst_addr, dst_port = conn:socket():getsockname(); + + -- Build proxy data based on real connection information + local proxy_data = { + _version = version, + _addr_family = addr_family, _transport = transport, + _src_addr = src_addr, _src_port = src_port, + _dst_addr = dst_addr, _dst_port = dst_port + }; + setmetatable(proxy_data, proxy_data_mt); + + -- Return successful response with gathered proxy data + return PROTO_HANDLER_STATUS.SUCCESS, proxy_data; + elseif command == 0x1 then + local offset = 1; + local src_addr, src_port, dst_addr, dst_port; + + -- Verify transport protocol is either STREAM or DGRAM + if transport ~= TRANSPORT.STREAM and transport ~= TRANSPORT.DGRAM then + module:log("warn", "Received unsupported PROXYv2 transport from %s: 0x%02X", conn:ip(), transport); + return PROTO_HANDLER_STATUS.FAILURE, nil; + end + + -- Parse source and destination addresses + if addr_family == ADDR_FAMILY.INET then + src_addr = net_ntop(payload:sub(offset, offset + 3)); offset = offset + 4; + dst_addr = net_ntop(payload:sub(offset, offset + 3)); offset = offset + 4; + elseif addr_family == ADDR_FAMILY.INET6 then + src_addr = net_ntop(payload:sub(offset, offset + 15)); offset = offset + 16; + dst_addr = net_ntop(payload:sub(offset, offset + 15)); offset = offset + 16; + elseif addr_family == ADDR_FAMILY.UNIX then + src_addr = payload:sub(offset, offset + 107); offset = offset + 108; + dst_addr = payload:sub(offset, offset + 107); offset = offset + 108; + end + + -- Parse source and destination ports + if addr_family == ADDR_FAMILY.INET or addr_family == ADDR_FAMILY.INET6 then + src_port = bit.bor(payload:byte(offset + 1), bit.lshift(payload:byte(offset), 8)); offset = offset + 2; + -- luacheck: ignore 311 + dst_port = bit.bor(payload:byte(offset + 1), bit.lshift(payload:byte(offset), 8)); offset = offset + 2; + end + + -- Strip parsed header from session buffer and build proxy data + session.buffer = session.buffer:sub(17 + length); + + local proxy_data = { + _version = version, + _addr_family = addr_family, _transport = transport, + _src_addr = src_addr, _src_port = src_port, + _dst_addr = dst_addr, _dst_port = dst_port + }; + setmetatable(proxy_data, proxy_data_mt); + + -- Return successful response with gathered proxy data + return PROTO_HANDLER_STATUS.SUCCESS, proxy_data; + else + module:log("warn", "Received unsupported PROXYv2 command from %s: 0x%02X", conn:ip(), command); + return PROTO_HANDLER_STATUS.FAILURE, nil; + end +end + +-- Wrap an existing connection with the provided proxy data. This will override several methods of the 'conn' object to +-- return the proxied source instead of the source which initiated the TCP connection. Afterwards, the listener of the +-- connection gets set according to the globally defined port<>service mappings and the methods 'onconnect' and +-- 'onincoming' are being called manually with the current session buffer. +local function wrap_proxy_connection(conn, session, proxy_data) + -- Override and add functions of 'conn' object when source information has been collected + conn.proxyip, conn.proxyport = conn.ip, conn.port; + if proxy_data:src_addr() ~= nil and proxy_data:src_port() ~= nil then + conn.ip = function() + return proxy_data:src_addr(); + end + conn.port = function() + return proxy_data:src_port(); + end + conn.clientport = conn.port; + end + + -- Attempt to find service by processing port<>service mappings + local mapping = mappings[tonumber(conn:serverport())]; + if mapping == nil then + conn:close(); + module:log("warn", "Connection %s@%s terminated: Could not find mapping for port %d", + conn:ip(), conn:proxyip(), conn:serverport()); + return; + end + + if mapping.service == nil then + local service = portmanager.get_service(mapping.service_name); + + if service ~= nil then + mapping.service = service; + else + conn:close(); + module:log("warn", "Connection %s@%s terminated: Could not process mapping for unknown service %s", + conn:ip(), conn:proxyip(), mapping.service_name); + return; + end + end + + -- Pass connection to actual service listener and simulate onconnect/onincoming callbacks + local service_listener = mapping.service.listener; + + module:log("info", "Passing proxied connection %s:%d to service %s", conn:ip(), conn:port(), mapping.service_name); + conn:setlistener(service_listener); + if service_listener.onconnect then + service_listener.onconnect(conn); + end + return service_listener.onincoming(conn, session.buffer); +end + +local function is_trusted_proxy(conn) + -- If no trusted proxies were configured, trust any incoming connection + -- While this may seem insecure, the module defaults to only trusting 127.0.0.1 and ::1 + if trusted_networks:empty() then + return true; + end + + -- Iterate through all trusted proxies and check for match against connected IP address + local conn_ip = ip.new_ip(conn:ip()); + for trusted_network in trusted_networks:items() do + if ip.match(trusted_network.ip, conn_ip, trusted_network.cidr) then + return true; + end + end + + -- Connection does not match any trusted proxy + return false; +end + +-- Network Listener Methods +local listener = {}; + +function listener.onconnect(conn) + -- Silently drop connections with an IP address of , which can happen when the socket was closed before the + -- responsible net.server backend was able to grab the IP address of the connecting client. + if conn:ip() == nil then + conn:close(); + return; + end + + -- Check if connection is coming from a trusted proxy + if not is_trusted_proxy(conn) then + conn:close(); + module:log("warn", "Dropped connection from untrusted proxy: %s", conn:ip()); + return; + end + + -- Initialize session variables + sessions[conn] = { + handler = nil; + buffer = nil; + }; +end + +function listener.onincoming(conn, data) + -- Abort processing if no data has been received + if not data then + return; + end + + -- Lookup session for connection and append received data to buffer + local session = sessions[conn]; + session.buffer = session.buffer and session.buffer .. data or data; + + -- Attempt to determine protocol handler if not done previously + if session.handler == nil then + -- Match current session buffer against all known protocol signatures to determine protocol handler + for handler_name, handler in pairs(PROTO_HANDLERS) do + if session.buffer:find("^" .. handler.signature) ~= nil then + session.handler = handler.callback; + module:log("debug", "Detected %s connection from %s:%d", handler_name, conn:ip(), conn:port()); + break; + end + end + + -- Decide between waiting for a complete header signature or terminating the connection when no handler has been found + if session.handler == nil then + -- Terminate connection if buffer size has exceeded tolerable maximum size + if #session.buffer > PROTO_MAX_HEADER_LENGTH then + conn:close(); + module:log("warn", "Connection %s:%d terminated: No valid PROXY header within %d bytes", + conn:ip(), conn:port(), PROTO_MAX_HEADER_LENGTH); + end + + -- Skip further processing without a valid protocol handler + module:log("debug", "No valid header signature detected from %s:%d, waiting for more data...", + conn:ip(), conn:port()); + return; + end + end + + -- Execute proxy protocol handler and process response + local response, proxy_data = session.handler(conn, session); + if response == PROTO_HANDLER_STATUS.SUCCESS then + module:log("info", "Received PROXY header from %s: %s", conn:ip(), proxy_data:describe()); + return wrap_proxy_connection(conn, session, proxy_data); + elseif response == PROTO_HANDLER_STATUS.POSTPONE then + module:log("debug", "Postponed parsing of incomplete PROXY header received from %s", conn:ip()); + return; + elseif response == PROTO_HANDLER_STATUS.FAILURE then + conn:close(); + module:log("warn", "Connection %s terminated: Could not process PROXY header from client, " + + "see previous log messages.", conn:ip()); + return; + else + -- This code should be never reached, but is included for completeness + conn:close(); + module:log("warn", "Connection terminated: Received invalid protocol handler response with code %d", response); + return; + end +end + +function listener.ondisconnect(conn) + sessions[conn] = nil; +end + +listener.ondetach = listener.ondisconnect; + +-- Parse trusted proxies which can either contain single hosts or networks +if not config_trusted_proxies:empty() then + for trusted_proxy in config_trusted_proxies:items() do + local network = {}; + network.ip, network.cidr = ip.parse_cidr(trusted_proxy); + trusted_networks:add(network); + end +else + module:log("warn", "No trusted proxies configured, all connections will be accepted - this might be dangerous"); +end + +-- Process all configured port mappings and generate a list of mapped ports +local mapped_ports = {}; +for port, mapping in pairs(config_mappings) do + port = tonumber(port); + table.insert(mapped_ports, port); + mappings[port] = { + service_name = mapping, + service = nil, + }; +end + +-- Log error message when user manually specifies ports without configuring the necessary port mappings +if not config_ports:empty() then + local missing_ports = config_ports - set.new(mapped_ports); + if not missing_ports:empty() then + module:log("error", "Missing port<>service mappings for these ports: %s", tostring(missing_ports)); + end +end + +-- Register the previously declared network listener +module:provides("net", { + name = "proxy"; + listener = listener; + default_ports = mapped_ports; +}); diff --git a/contrib/prosody-modules/mod_secure_interfaces.lua b/contrib/prosody-modules/mod_secure_interfaces.lua index b7a8cb8..73f1358 100644 --- a/contrib/prosody-modules/mod_secure_interfaces.lua +++ b/contrib/prosody-modules/mod_secure_interfaces.lua @@ -20,8 +20,20 @@ end module:hook("stream-features", function (event) mark_secure(event, "c2s_unauthed"); -end, 2500); +end, 25000); module:hook("s2s-stream-features", function (event) mark_secure(event, "s2sin_unauthed"); -end, 2500); +end, 25000); + +-- todo: is this the best place to do this hook? +-- this hook marks incoming s2s as secure so we offer SASL EXTERNAL on it +module:hook("s2s-stream-features", function(event) + local session, features = event.origin, event.features; + if session.type == "s2sin_unauthed" then + module:log("debug", "marking hook session.type '%s' secure with validated cert!", session.type); + session.secure = true; + session.cert_chain_status = "valid"; + session.cert_identity_status = "valid"; + end +end, 3000); diff --git a/integration/28-s2s-srv-record-starttls-no-outgoing/example.org.zone b/integration/28-s2s-srv-record-starttls-no-outgoing/example.org.zone new file mode 100644 index 0000000..4ffcdd7 --- /dev/null +++ b/integration/28-s2s-srv-record-starttls-no-outgoing/example.org.zone @@ -0,0 +1,23 @@ +$TTL 300 +; example.org +@ IN SOA ns1.example.org. postmaster.example.org. ( + 2018111111 ; Serial + 28800 ; Refresh + 1800 ; Retry + 604800 ; Expire - 1 week + 86400 ) ; Negative Cache TTL + IN NS ns1 +ns1 IN A 192.5.0.10 +server1 IN A 192.5.0.20 +server2 IN A 192.5.0.30 +xp1 IN A 192.5.0.40 +xp2 IN A 192.5.0.50 +xp3 IN A 192.5.0.60 + +one IN CNAME xp1 +two IN CNAME xp2 +_xmpp-server._tcp.one IN SRV 5 1 52269 xp1 +_xmpp-server._tcp.two IN SRV 5 1 52269 xp2 + +scansion.one IN CNAME xp1 +scansion.two IN CNAME xp2 diff --git a/integration/28-s2s-srv-record-starttls-no-outgoing/prosody1.cfg.lua b/integration/28-s2s-srv-record-starttls-no-outgoing/prosody1.cfg.lua new file mode 100644 index 0000000..cb8fcdf --- /dev/null +++ b/integration/28-s2s-srv-record-starttls-no-outgoing/prosody1.cfg.lua @@ -0,0 +1,248 @@ +--Important for systemd +-- daemonize is important for systemd. if you set this to false the systemd startup will freeze. +daemonize = false +run_as_root = true + +pidfile = "/run/prosody/prosody.pid" + +plugin_paths = { "/opt/xmpp-proxy/prosody-modules", "/opt/prosody-modules" } + +-- Prosody Example Configuration File +-- +-- Information on configuring Prosody can be found on our +-- website at https://prosody.im/doc/configure +-- +-- Tip: You can check that the syntax of this file is correct +-- when you have finished by running this command: +-- prosodyctl check config +-- If there are any errors, it will let you know what and where +-- they are, otherwise it will keep quiet. +-- +-- The only thing left to do is rename this file to remove the .dist ending, and fill in the +-- blanks. Good luck, and happy Jabbering! + + +---------- Server-wide settings ---------- +-- Settings in this section apply to the whole server and are the default settings +-- for any virtual hosts + +-- This is a (by default, empty) list of accounts that are admins +-- for the server. Note that you must create the accounts separately +-- (see https://prosody.im/doc/creating_accounts for info) +-- Example: admins = { "user1@example.com", "user2@example.net" } +admins = { } + +-- Enable use of libevent for better performance under high load +-- For more information see: https://prosody.im/doc/libevent +--use_libevent = true + +-- Prosody will always look in its source directory for modules, but +-- this option allows you to specify additional locations where Prosody +-- will look for modules first. For community modules, see https://modules.prosody.im/ +--plugin_paths = {} + +-- This is the list of modules Prosody will load on startup. +-- It looks for mod_modulename.lua in the plugins folder, so make sure that exists too. +-- Documentation for bundled modules can be found at: https://prosody.im/doc/modules +modules_enabled = { + + -- Generally required + "roster"; -- Allow users to have a roster. Recommended ;) + "saslauth"; -- Authentication for clients and servers. Recommended if you want to log in. + "tls"; -- Add support for secure TLS on c2s/s2s connections + --"dialback"; -- s2s dialback support + "disco"; -- Service discovery + + -- Not essential, but recommended + "carbons"; -- Keep multiple clients in sync + "pep"; -- Enables users to publish their avatar, mood, activity, playing music and more + "private"; -- Private XML storage (for room bookmarks, etc.) + "blocklist"; -- Allow users to block communications with other users + "vcard4"; -- User profiles (stored in PEP) + "vcard_legacy"; -- Conversion between legacy vCard and PEP Avatar, vcard + "limits"; -- Enable bandwidth limiting for XMPP connections + + -- Nice to have + "version"; -- Replies to server version requests + "uptime"; -- Report how long server has been running + "time"; -- Let others know the time here on this server + "ping"; -- Replies to XMPP pings with pongs + "register"; -- Allow users to register on this server using a client and change passwords + --"mam"; -- Store messages in an archive and allow users to access it + --"csi_simple"; -- Simple Mobile optimizations + + -- Admin interfaces + "admin_adhoc"; -- Allows administration via an XMPP client that supports ad-hoc commands + --"admin_telnet"; -- Opens telnet console interface on localhost port 5582 + + -- HTTP modules + --"bosh"; -- Enable BOSH clients, aka "Jabber over HTTP" + --"websocket"; -- XMPP over WebSockets + --"http_files"; -- Serve static files from a directory over HTTP + + -- Other specific functionality + --"groups"; -- Shared roster support + --"server_contact_info"; -- Publish contact information for this service + --"announce"; -- Send announcement to all online users + --"welcome"; -- Welcome users who register accounts + --"watchregistrations"; -- Alert admins of registrations + --"motd"; -- Send a message to users when they log in + --"legacyauth"; -- Legacy authentication. Only used by some old clients and bots. + --"proxy65"; -- Enables a file transfer proxy service which clients behind NAT can use + "net_proxy"; + --"s2s_outgoing_proxy"; + "secure_interfaces"; +} + +-- These modules are auto-loaded, but should you want +-- to disable them then uncomment them here: +modules_disabled = { + -- "offline"; -- Store offline messages + -- "c2s"; -- Handle client connections + -- "s2s"; -- Handle server-to-server connections + -- "posix"; -- POSIX functionality, sends server to background, enables syslog, etc. +} + +-- Disable account creation by default, for security +-- For more information see https://prosody.im/doc/creating_accounts +allow_registration = false + +c2s_require_encryption = false +allow_unencrypted_plain_auth = true + +s2s_require_encryption = true +s2s_secure_auth = true + +-- handle PROXY protocol on these ports +proxy_port_mappings = { + [15222] = "c2s", + [15269] = "s2s" +} + +--[[ + Specifies a list of trusted hosts or networks which may use the PROXY protocol + If not specified, it will default to: 127.0.0.1, ::1 (local connections only) + An empty table ({}) can be configured to allow connections from any source. + Please read the module documentation about potential security impact. +]]-- +proxy_trusted_proxies = { + "192.5.0.40" +} + +secure_interfaces = { + "192.5.0.40" +} + +-- don't listen on any normal c2s/s2s ports (xmpp-proxy listens on these now) +-- you might need to comment these out further down in your config file if you set them +c2s_ports = {} +legacy_ssl_ports = {} +-- you MUST have at least one s2s_ports defined if you want outgoing S2S to work, don't ask.. +s2s_ports = {15268} + + +-- Some servers have invalid or self-signed certificates. You can list +-- remote domains here that will not be required to authenticate using +-- certificates. They will be authenticated using DNS instead, even +-- when s2s_secure_auth is enabled. + +--s2s_insecure_domains = { "insecure.example" } + +-- Even if you disable s2s_secure_auth, you can still require valid +-- certificates for some domains by specifying a list here. + +--s2s_secure_domains = { "jabber.org" } + +-- Enable rate limits for incoming client and server connections + +limits = { + c2s = { + rate = "10kb/s"; + }; + s2sin = { + rate = "30kb/s"; + }; +} + +-- Select the authentication backend to use. The 'internal' providers +-- use Prosody's configured data storage to store the authentication data. + +authentication = "internal_hashed" + +-- Select the storage backend to use. By default Prosody uses flat files +-- in its configured data directory, but it also supports more backends +-- through modules. An "sql" backend is included by default, but requires +-- additional dependencies. See https://prosody.im/doc/storage for more info. + +--storage = "sql" -- Default is "internal" + +-- For the "sql" backend, you can uncomment *one* of the below to configure: +--sql = { driver = "SQLite3", database = "prosody.sqlite" } -- Default. 'database' is the filename. +--sql = { driver = "MySQL", database = "prosody", username = "prosody", password = "secret", host = "localhost" } +--sql = { driver = "PostgreSQL", database = "prosody", username = "prosody", password = "secret", host = "localhost" } + + +-- Archiving configuration +-- If mod_mam is enabled, Prosody will store a copy of every message. This +-- is used to synchronize conversations between multiple clients, even if +-- they are offline. This setting controls how long Prosody will keep +-- messages in the archive before removing them. + +archive_expires_after = "1w" -- Remove archived messages after 1 week + +-- You can also configure messages to be stored in-memory only. For more +-- archiving options, see https://prosody.im/doc/modules/mod_mam + +-- Logging configuration +-- For advanced logging see https://prosody.im/doc/logging +log = { + -- info = "prosody.log"; -- Change 'info' to 'debug' for verbose logging + -- error = "prosody.err"; + --info = "*syslog"; -- Uncomment this for logging to syslog + debug = "*console"; -- Log to the console, useful for debugging with daemonize=false +} + +-- Uncomment to enable statistics +-- For more info see https://prosody.im/doc/statistics +-- statistics = "internal" + +-- Certificates +-- Every virtual host and component needs a certificate so that clients and +-- servers can securely verify its identity. Prosody will automatically load +-- certificates/keys from the directory specified here. +-- For more information, including how to use 'prosodyctl' to auto-import certificates +-- (from e.g. Let's Encrypt) see https://prosody.im/doc/certificates + +-- Location of directory to find certificates in (relative to main config file): +certificates = "certs" + +-- HTTPS currently only supports a single certificate, specify it here: +--https_certificate = "/etc/prosody/certs/localhost.crt" + +----------- Virtual hosts ----------- +-- You need to add a VirtualHost entry for each domain you wish Prosody to serve. +-- Settings under each VirtualHost entry apply *only* to that host. + +VirtualHost "one.example.org" + +--VirtualHost "example.com" +-- certificate = "/path/to/example.crt" + +------ Components ------ +-- You can specify components to add hosts that provide special services, +-- like multi-user conferences, and transports. +-- For more information on components, see https://prosody.im/doc/components + +---Set up a MUC (multi-user chat) room server on conference.example.com: +--Component "conference.example.com" "muc" +--- Store MUC messages in an archive and allow users to access it +--modules_enabled = { "muc_mam" } + +---Set up an external component (default component port is 5347) +-- +-- External components allow adding various services, such as gateways/ +-- transports to other networks like ICQ, MSN and Yahoo. For more info +-- see: https://prosody.im/doc/components#adding_an_external_component +-- +--Component "gateway.example.com" +-- component_secret = "password" diff --git a/integration/28-s2s-srv-record-starttls-no-outgoing/prosody2.cfg.lua b/integration/28-s2s-srv-record-starttls-no-outgoing/prosody2.cfg.lua new file mode 100644 index 0000000..6b4547f --- /dev/null +++ b/integration/28-s2s-srv-record-starttls-no-outgoing/prosody2.cfg.lua @@ -0,0 +1,247 @@ +--Important for systemd +-- daemonize is important for systemd. if you set this to false the systemd startup will freeze. +daemonize = false +run_as_root = true + +pidfile = "/run/prosody/prosody.pid" + +plugin_paths = { "/opt/xmpp-proxy/prosody-modules", "/opt/prosody-modules" } + +-- Prosody Example Configuration File +-- +-- Information on configuring Prosody can be found on our +-- website at https://prosody.im/doc/configure +-- +-- Tip: You can check that the syntax of this file is correct +-- when you have finished by running this command: +-- prosodyctl check config +-- If there are any errors, it will let you know what and where +-- they are, otherwise it will keep quiet. +-- +-- The only thing left to do is rename this file to remove the .dist ending, and fill in the +-- blanks. Good luck, and happy Jabbering! + + +---------- Server-wide settings ---------- +-- Settings in this section apply to the whole server and are the default settings +-- for any virtual hosts + +-- This is a (by default, empty) list of accounts that are admins +-- for the server. Note that you must create the accounts separately +-- (see https://prosody.im/doc/creating_accounts for info) +-- Example: admins = { "user1@example.com", "user2@example.net" } +admins = { } + +-- Enable use of libevent for better performance under high load +-- For more information see: https://prosody.im/doc/libevent +--use_libevent = true + +-- Prosody will always look in its source directory for modules, but +-- this option allows you to specify additional locations where Prosody +-- will look for modules first. For community modules, see https://modules.prosody.im/ +--plugin_paths = {} + +-- This is the list of modules Prosody will load on startup. +-- It looks for mod_modulename.lua in the plugins folder, so make sure that exists too. +-- Documentation for bundled modules can be found at: https://prosody.im/doc/modules +modules_enabled = { + + -- Generally required + "roster"; -- Allow users to have a roster. Recommended ;) + "saslauth"; -- Authentication for clients and servers. Recommended if you want to log in. + "tls"; -- Add support for secure TLS on c2s/s2s connections + --"dialback"; -- s2s dialback support + "disco"; -- Service discovery + + -- Not essential, but recommended + "carbons"; -- Keep multiple clients in sync + "pep"; -- Enables users to publish their avatar, mood, activity, playing music and more + "private"; -- Private XML storage (for room bookmarks, etc.) + "blocklist"; -- Allow users to block communications with other users + "vcard4"; -- User profiles (stored in PEP) + "vcard_legacy"; -- Conversion between legacy vCard and PEP Avatar, vcard + "limits"; -- Enable bandwidth limiting for XMPP connections + + -- Nice to have + "version"; -- Replies to server version requests + "uptime"; -- Report how long server has been running + "time"; -- Let others know the time here on this server + "ping"; -- Replies to XMPP pings with pongs + "register"; -- Allow users to register on this server using a client and change passwords + --"mam"; -- Store messages in an archive and allow users to access it + --"csi_simple"; -- Simple Mobile optimizations + + -- Admin interfaces + "admin_adhoc"; -- Allows administration via an XMPP client that supports ad-hoc commands + --"admin_telnet"; -- Opens telnet console interface on localhost port 5582 + + -- HTTP modules + --"bosh"; -- Enable BOSH clients, aka "Jabber over HTTP" + --"websocket"; -- XMPP over WebSockets + --"http_files"; -- Serve static files from a directory over HTTP + + -- Other specific functionality + --"groups"; -- Shared roster support + --"server_contact_info"; -- Publish contact information for this service + --"announce"; -- Send announcement to all online users + --"welcome"; -- Welcome users who register accounts + --"watchregistrations"; -- Alert admins of registrations + --"motd"; -- Send a message to users when they log in + --"legacyauth"; -- Legacy authentication. Only used by some old clients and bots. + --"proxy65"; -- Enables a file transfer proxy service which clients behind NAT can use + "net_proxy"; + --"s2s_outgoing_proxy"; + "secure_interfaces"; +} + +-- These modules are auto-loaded, but should you want +-- to disable them then uncomment them here: +modules_disabled = { + -- "offline"; -- Store offline messages + -- "c2s"; -- Handle client connections + -- "s2s"; -- Handle server-to-server connections + -- "posix"; -- POSIX functionality, sends server to background, enables syslog, etc. +} + +-- Disable account creation by default, for security +-- For more information see https://prosody.im/doc/creating_accounts +allow_registration = false + +c2s_require_encryption = false +allow_unencrypted_plain_auth = true + +s2s_require_encryption = true +s2s_secure_auth = true + +-- handle PROXY protocol on these ports +proxy_port_mappings = { + [15222] = "c2s", + [15269] = "s2s" +} + +--[[ + Specifies a list of trusted hosts or networks which may use the PROXY protocol + If not specified, it will default to: 127.0.0.1, ::1 (local connections only) + An empty table ({}) can be configured to allow connections from any source. + Please read the module documentation about potential security impact. +]]-- +proxy_trusted_proxies = { + "192.5.0.50" +} + +secure_interfaces = { + "192.5.0.50" +} + +-- don't listen on any normal c2s/s2s ports (xmpp-proxy listens on these now) +-- you might need to comment these out further down in your config file if you set them +c2s_ports = {} +legacy_ssl_ports = {} +-- you MUST have at least one s2s_ports defined if you want outgoing S2S to work, don't ask.. +s2s_ports = {15268} + +-- Some servers have invalid or self-signed certificates. You can list +-- remote domains here that will not be required to authenticate using +-- certificates. They will be authenticated using DNS instead, even +-- when s2s_secure_auth is enabled. + +--s2s_insecure_domains = { "insecure.example" } + +-- Even if you disable s2s_secure_auth, you can still require valid +-- certificates for some domains by specifying a list here. + +--s2s_secure_domains = { "jabber.org" } + +-- Enable rate limits for incoming client and server connections + +limits = { + c2s = { + rate = "10kb/s"; + }; + s2sin = { + rate = "30kb/s"; + }; +} + +-- Select the authentication backend to use. The 'internal' providers +-- use Prosody's configured data storage to store the authentication data. + +authentication = "internal_hashed" + +-- Select the storage backend to use. By default Prosody uses flat files +-- in its configured data directory, but it also supports more backends +-- through modules. An "sql" backend is included by default, but requires +-- additional dependencies. See https://prosody.im/doc/storage for more info. + +--storage = "sql" -- Default is "internal" + +-- For the "sql" backend, you can uncomment *one* of the below to configure: +--sql = { driver = "SQLite3", database = "prosody.sqlite" } -- Default. 'database' is the filename. +--sql = { driver = "MySQL", database = "prosody", username = "prosody", password = "secret", host = "localhost" } +--sql = { driver = "PostgreSQL", database = "prosody", username = "prosody", password = "secret", host = "localhost" } + + +-- Archiving configuration +-- If mod_mam is enabled, Prosody will store a copy of every message. This +-- is used to synchronize conversations between multiple clients, even if +-- they are offline. This setting controls how long Prosody will keep +-- messages in the archive before removing them. + +archive_expires_after = "1w" -- Remove archived messages after 1 week + +-- You can also configure messages to be stored in-memory only. For more +-- archiving options, see https://prosody.im/doc/modules/mod_mam + +-- Logging configuration +-- For advanced logging see https://prosody.im/doc/logging +log = { + -- info = "prosody.log"; -- Change 'info' to 'debug' for verbose logging + -- error = "prosody.err"; + --info = "*syslog"; -- Uncomment this for logging to syslog + debug = "*console"; -- Log to the console, useful for debugging with daemonize=false +} + +-- Uncomment to enable statistics +-- For more info see https://prosody.im/doc/statistics +-- statistics = "internal" + +-- Certificates +-- Every virtual host and component needs a certificate so that clients and +-- servers can securely verify its identity. Prosody will automatically load +-- certificates/keys from the directory specified here. +-- For more information, including how to use 'prosodyctl' to auto-import certificates +-- (from e.g. Let's Encrypt) see https://prosody.im/doc/certificates + +-- Location of directory to find certificates in (relative to main config file): +certificates = "certs" + +-- HTTPS currently only supports a single certificate, specify it here: +--https_certificate = "/etc/prosody/certs/localhost.crt" + +----------- Virtual hosts ----------- +-- You need to add a VirtualHost entry for each domain you wish Prosody to serve. +-- Settings under each VirtualHost entry apply *only* to that host. + +VirtualHost "two.example.org" + +--VirtualHost "example.com" +-- certificate = "/path/to/example.crt" + +------ Components ------ +-- You can specify components to add hosts that provide special services, +-- like multi-user conferences, and transports. +-- For more information on components, see https://prosody.im/doc/components + +---Set up a MUC (multi-user chat) room server on conference.example.com: +--Component "conference.example.com" "muc" +--- Store MUC messages in an archive and allow users to access it +--modules_enabled = { "muc_mam" } + +---Set up an external component (default component port is 5347) +-- +-- External components allow adding various services, such as gateways/ +-- transports to other networks like ICQ, MSN and Yahoo. For more info +-- see: https://prosody.im/doc/components#adding_an_external_component +-- +--Component "gateway.example.com" +-- component_secret = "password" diff --git a/integration/28-s2s-srv-record-starttls-no-outgoing/xmpp-proxy1.toml b/integration/28-s2s-srv-record-starttls-no-outgoing/xmpp-proxy1.toml new file mode 100644 index 0000000..7f91c66 --- /dev/null +++ b/integration/28-s2s-srv-record-starttls-no-outgoing/xmpp-proxy1.toml @@ -0,0 +1,44 @@ + +# interfaces to listen for reverse proxy STARTTLS/Direct TLS XMPP connections on, should be open to the internet +incoming_listen = [ "0.0.0.0:5222", "0.0.0.0:52269" ] +# interfaces to listen for reverse proxy QUIC XMPP connections on, should be open to the internet +quic_listen = [ ] +# interfaces to listen for reverse proxy TLS WebSocket (wss) XMPP connections on, should be open to the internet +websocket_listen = [ ] +# interfaces to listen for outgoing proxy TCP XMPP connections on, should be localhost +outgoing_listen = [ ] + +# these ports shouldn't do any TLS, but should assume any connection from xmpp-proxy is secure +# prosody module: https://modules.prosody.im/mod_secure_interfaces.html + +# c2s port backend XMPP server listens on +c2s_target = "192.5.0.20:15222" + +# s2s port backend XMPP server listens on +s2s_target = "192.5.0.20:15269" + +# send PROXYv1 header to backend XMPP server +# https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt +# prosody module: https://modules.prosody.im/mod_net_proxy.html +# ejabberd config: https://docs.ejabberd.im/admin/configuration/listen-options/#use-proxy-protocol +proxy = true + +# limit incoming stanzas to this many bytes, default to ejabberd's default +# https://github.com/processone/ejabberd/blob/master/ejabberd.yml.example#L32 +# xmpp-proxy will use this many bytes + 16k per connection +max_stanza_size_bytes = 262_144 + +# TLS key/certificate valid for all your XMPP domains, PEM format +# included systemd unit can only read files from /etc/xmpp-proxy/ so put them in there +tls_key = "/etc/prosody/certs/one.example.org.key" +tls_cert = "/etc/prosody/certs/one.example.org.crt" + +# configure logging, defaults are commented +# can also set env variables XMPP_PROXY_LOG_LEVEL and/or XMPP_PROXY_LOG_STYLE, but values in this file override them +# many options, trace is XML-console-level, refer to: https://docs.rs/env_logger/0.8.3/env_logger/#enabling-logging +#log_level = "info" +# for development/debugging: +log_level = "info,xmpp_proxy=trace" + +# one of auto, always, never, refer to: https://docs.rs/env_logger/0.8.3/env_logger/#disabling-colors +#log_style = "never" diff --git a/integration/28-s2s-srv-record-starttls-no-outgoing/xmpp-proxy2.toml b/integration/28-s2s-srv-record-starttls-no-outgoing/xmpp-proxy2.toml new file mode 100644 index 0000000..e7e7392 --- /dev/null +++ b/integration/28-s2s-srv-record-starttls-no-outgoing/xmpp-proxy2.toml @@ -0,0 +1,44 @@ + +# interfaces to listen for reverse proxy STARTTLS/Direct TLS XMPP connections on, should be open to the internet +incoming_listen = [ "0.0.0.0:5222", "0.0.0.0:52269" ] +# interfaces to listen for reverse proxy QUIC XMPP connections on, should be open to the internet +quic_listen = [ ] +# interfaces to listen for reverse proxy TLS WebSocket (wss) XMPP connections on, should be open to the internet +websocket_listen = [ ] +# interfaces to listen for outgoing proxy TCP XMPP connections on, should be localhost +outgoing_listen = [ ] + +# these ports shouldn't do any TLS, but should assume any connection from xmpp-proxy is secure +# prosody module: https://modules.prosody.im/mod_secure_interfaces.html + +# c2s port backend XMPP server listens on +c2s_target = "192.5.0.30:15222" + +# s2s port backend XMPP server listens on +s2s_target = "192.5.0.30:15269" + +# send PROXYv1 header to backend XMPP server +# https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt +# prosody module: https://modules.prosody.im/mod_net_proxy.html +# ejabberd config: https://docs.ejabberd.im/admin/configuration/listen-options/#use-proxy-protocol +proxy = true + +# limit incoming stanzas to this many bytes, default to ejabberd's default +# https://github.com/processone/ejabberd/blob/master/ejabberd.yml.example#L32 +# xmpp-proxy will use this many bytes + 16k per connection +max_stanza_size_bytes = 262_144 + +# TLS key/certificate valid for all your XMPP domains, PEM format +# included systemd unit can only read files from /etc/xmpp-proxy/ so put them in there +tls_key = "/etc/prosody/certs/two.example.org.key" +tls_cert = "/etc/prosody/certs/two.example.org.crt" + +# configure logging, defaults are commented +# can also set env variables XMPP_PROXY_LOG_LEVEL and/or XMPP_PROXY_LOG_STYLE, but values in this file override them +# many options, trace is XML-console-level, refer to: https://docs.rs/env_logger/0.8.3/env_logger/#enabling-logging +#log_level = "info" +# for development/debugging: +log_level = "info,xmpp_proxy=trace" + +# one of auto, always, never, refer to: https://docs.rs/env_logger/0.8.3/env_logger/#disabling-colors +#log_style = "never" diff --git a/integration/Dockerfile b/integration/Dockerfile index 3675c7e..05098f1 100644 --- a/integration/Dockerfile +++ b/integration/Dockerfile @@ -18,9 +18,9 @@ RUN pacman -S --noconfirm --disable-download-timeout --needed rust cargo git mer hg clone 'https://hg.prosody.im/prosody-modules/' /build/prosody-modules && rm -rf /build/prosody-modules/.hg && \ git clone https://aur.archlinux.org/scansion-hg.git /build/scansion-hg && \ git clone https://aur.archlinux.org/lua52-cjson.git /build/lua52-cjson && \ - chown -R git: /build/ && ls -lah /build/ && \ - cd /build/lua52-cjson && su -m -s /bin/bash git makepkg && pacman -U --noconfirm --needed lua52-cjson-*.pkg.tar* && \ - cd /build/scansion-hg && su -m -s /bin/bash git makepkg + chown -R nobody: /build/ && ls -lah /build/ && \ + cd /build/lua52-cjson && su -m -s /bin/bash nobody makepkg && pacman -U --noconfirm --needed lua52-cjson-*.pkg.tar* && \ + cd /build/scansion-hg && su -m -s /bin/bash nobody makepkg COPY ./Cargo.* /build/ COPY ./src/ /build/src/ @@ -66,6 +66,8 @@ COPY ./integration/00-no-tls/prosody1.cfg.lua /etc/prosody/prosody.cfg.lua COPY ./contrib/prosody-modules /opt/xmpp-proxy/prosody-modules COPY ./integration/*.scs /scansion/ +RUN mkdir /run/prosody/ + ARG ECDSA=0 RUN if [ $ECDSA -ne 0 ]; then rm -rf /etc/prosody/certs && ln -sf /etc/certs/ecdsa /etc/prosody/certs; fi diff --git a/integration/test.sh b/integration/test.sh index 0dd6444..93a4b90 100755 --- a/integration/test.sh +++ b/integration/test.sh @@ -117,7 +117,7 @@ run_test() { sed -i "s/192\.5\.0\./$ipv4.$num./g" * # start the dns server - run_container "$network_name" $num -d -v ./example.org.zone:/var/named/example.org.zone:ro 10 dns named -g -u named -d 99 + run_container "$network_name" $num -d -v ./example.org.zone:/var/named/example.org.zone:ro 10 dns named -g -d 99 # start the prosody servers if required [ -f ./prosody1.cfg.lua ] && run_container "$network_name" $num -d -v ./prosody1.cfg.lua:/etc/prosody/prosody.cfg.lua:ro 20 server1 prosody && podman exec $num-server1 prosodyctl register romeo one.example.org pass && podman exec $num-server1 prosodyctl register juliet two.example.org pass diff --git a/src/common/ca_roots.rs b/src/common/ca_roots.rs index b16ae8a..76e0265 100644 --- a/src/common/ca_roots.rs +++ b/src/common/ca_roots.rs @@ -9,7 +9,7 @@ lazy_static::lazy_static! { pub static ref TLS_SERVER_ROOTS: TlsServerTrustAnchors<'static> = { // we need these to stick around for 'static, this is only called once so no problem let certs = Box::leak(Box::new(rustls_native_certs::load_native_certs().expect("could not load platform certs"))); - let root_cert_store = Box::leak(Box::new(Vec::new())); + let root_cert_store: &mut Box> = Box::leak(Box::default()); for cert in certs { // some system CAs are invalid, ignore those if let Ok(ta) = TrustAnchor::try_from_cert_der(&cert.0) { diff --git a/src/common/incoming.rs b/src/common/incoming.rs index ade8dfb..211b66e 100644 --- a/src/common/incoming.rs +++ b/src/common/incoming.rs @@ -152,7 +152,7 @@ async fn open_incoming( is_c2s: bool, in_filter: &mut StanzaFilter, ) -> Result<(ReadHalf, WriteHalf)> { - let target = if is_c2s { + let target: Option = if is_c2s { #[cfg(not(feature = "c2s-incoming"))] bail!("incoming c2s connection but lacking compile-time support"); #[cfg(feature = "c2s-incoming")] @@ -162,8 +162,8 @@ async fn open_incoming( bail!("incoming s2s connection but lacking compile-time support"); #[cfg(feature = "s2s-incoming")] config.s2s_target - } - .ok_or_else(|| anyhow!("incoming connection but `{}_target` not defined", c2s(is_c2s)))?; + }; + let target = target.ok_or_else(|| anyhow!("incoming connection but `{}_target` not defined", c2s(is_c2s)))?; client_addr.set_to_addr(target); let out_stream = tokio::net::TcpStream::connect(target).await?; diff --git a/src/common/mod.rs b/src/common/mod.rs index 33e8e18..f14ec8e 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -5,13 +5,18 @@ use crate::{ stanzafilter::StanzaFilter, }; use anyhow::{bail, Result}; +use async_trait::async_trait; use log::{info, trace}; #[cfg(feature = "rustls")] use rustls::{ sign::{RsaSigningKey, SigningKey}, Certificate, PrivateKey, }; -use std::{fs::File, io, io::BufReader, sync::Arc}; +use std::{fs::File, io, sync::Arc}; +use tokio::{ + io::{AsyncRead, AsyncWrite, BufReader, BufStream}, + net::TcpStream, +}; #[cfg(feature = "incoming")] pub mod incoming; @@ -41,31 +46,128 @@ pub fn c2s(is_c2s: bool) -> &'static str { } } -pub async fn peek_bytes<'a>(stream: &tokio::net::TcpStream, p: &'a mut [u8]) -> anyhow::Result<&'a [u8]> { - // sooo... I don't think peek here can be used for > 1 byte without this timer craziness... can it? - let len = p.len(); - // wait up to 10 seconds until len bytes have been read - use std::time::{Duration, Instant}; - let duration = Duration::from_secs(10); - let now = Instant::now(); - loop { - let n = stream.peek(p).await?; - if n == len { - break; // success - } - if n == 0 { - bail!("not enough bytes"); - } - if Instant::now() - now > duration { - bail!("less than {} bytes in 10 seconds, closed connection?", len); +pub trait Split: Sized { + type ReadHalf: AsyncRead + Unpin; + type WriteHalf: AsyncWrite + Unpin; + + fn combine(read_half: Self::ReadHalf, write_half: Self::WriteHalf) -> Result; + + fn split(self) -> (Self::ReadHalf, Self::WriteHalf); +} + +impl Split for TcpStream { + type ReadHalf = tokio::net::tcp::OwnedReadHalf; + type WriteHalf = tokio::net::tcp::OwnedWriteHalf; + + fn combine(read_half: Self::ReadHalf, write_half: Self::WriteHalf) -> Result { + Ok(read_half.reunite(write_half)?) + } + + fn split(self) -> (Self::ReadHalf, Self::WriteHalf) { + self.into_split() + } +} + +impl Split for BufStream { + type ReadHalf = tokio::io::ReadHalf>; + type WriteHalf = tokio::io::WriteHalf>; + + fn combine(read_half: Self::ReadHalf, write_half: Self::WriteHalf) -> Result { + if read_half.is_pair_of(&write_half) { + Ok(read_half.unsplit(write_half)) + } else { + bail!("non-matching read/write half") } } - Ok(p) + fn split(self) -> (Self::ReadHalf, Self::WriteHalf) { + tokio::io::split(self) + } } -pub async fn first_bytes_match(stream: &tokio::net::TcpStream, p: &mut [u8], matcher: fn(&[u8]) -> bool) -> anyhow::Result { - Ok(matcher(peek_bytes(stream, p).await?)) +#[async_trait] +pub trait Peek { + async fn peek_bytes<'a>(&mut self, p: &'a mut [u8]) -> anyhow::Result<&'a [u8]>; + + async fn first_bytes_match<'a>(&mut self, p: &'a mut [u8], matcher: fn(&'a [u8]) -> bool) -> anyhow::Result { + Ok(matcher(self.peek_bytes(p).await?)) + } +} + +#[async_trait] +impl Peek for TcpStream { + async fn peek_bytes<'a>(&mut self, p: &'a mut [u8]) -> anyhow::Result<&'a [u8]> { + // sooo... I don't think peek here can be used for > 1 byte without this timer craziness... can it? + let len = p.len(); + // wait up to 10 seconds until len bytes have been read + use std::time::{Duration, Instant}; + let duration = Duration::from_secs(10); + let now = Instant::now(); + loop { + let n = self.peek(p).await?; + if n == len { + return Ok(p); // success + } + if n == 0 { + bail!("not enough bytes"); + } + if Instant::now() - now > duration { + bail!("less than {} bytes in 10 seconds, closed connection?", len); + } + } + } +} + +#[async_trait] +impl Peek for BufStream { + async fn peek_bytes<'a>(&mut self, p: &'a mut [u8]) -> anyhow::Result<&'a [u8]> { + // sooo... I don't think peek here can be used for > 1 byte without this timer craziness... can it? + let len = p.len(); + // wait up to 10 seconds until len bytes have been read + use std::time::{Duration, Instant}; + use tokio::io::AsyncBufReadExt; + let duration = Duration::from_secs(10); + let now = Instant::now(); + loop { + let buf = self.fill_buf().await?; + if buf.len() >= len { + p.copy_from_slice(&buf[0..len]); + return Ok(p); // success + } + if buf.is_empty() { + bail!("not enough bytes"); + } + if Instant::now() - now > duration { + bail!("less than {} bytes in 10 seconds, closed connection?", len); + } + } + } +} + +#[async_trait] +impl Peek for BufReader { + async fn peek_bytes<'a>(&mut self, p: &'a mut [u8]) -> anyhow::Result<&'a [u8]> { + // sooo... I don't think peek here can be used for > 1 byte without this timer craziness... can it? + let len = p.len(); + // wait up to 10 seconds until len bytes have been read + use std::time::{Duration, Instant}; + use tokio::io::AsyncBufReadExt; + let duration = Duration::from_secs(10); + let now = Instant::now(); + loop { + let buf = self.fill_buf().await?; + if buf.len() >= len { + p.copy_from_slice(&buf[0..len]); + return Ok(p); // success + } + if buf.is_empty() { + bail!("not enough bytes"); + } + if Instant::now() - now > duration { + bail!("less than {} bytes in 10 seconds, closed connection?", len); + } + } + } } pub async fn stream_preamble(in_rd: &mut StanzaRead, in_wr: &mut StanzaWrite, client_addr: &'_ str, in_filter: &mut StanzaFilter) -> Result<(Vec, bool)> { @@ -130,7 +232,7 @@ pub async fn shuffle_rd_wr_filter_only( pub fn read_certified_key(tls_key: &str, tls_cert: &str) -> Result { use rustls_pemfile::{certs, read_all, Item}; - let tls_key = read_all(&mut BufReader::new(File::open(tls_key)?)) + let tls_key = read_all(&mut io::BufReader::new(File::open(tls_key)?)) .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid key"))? .into_iter() .flat_map(|item| match item { @@ -142,7 +244,7 @@ pub fn read_certified_key(tls_key: &str, tls_cert: &str) -> Result OutgoingVerifierConfig { + pub fn with_custom_certificate_verifier(&self, is_c2s: bool, cert_verifier: Arc) -> OutgoingVerifierConfig { let config = match is_c2s { false => ClientConfig::builder() .with_safe_defaults() - .with_custom_certificate_verifier(Arc::new(cert_verifier)) + .with_custom_certificate_verifier(cert_verifier) .with_client_cert_resolver(self.certs_key.clone()), - _ => ClientConfig::builder() - .with_safe_defaults() - .with_custom_certificate_verifier(Arc::new(cert_verifier)) - .with_no_client_auth(), + _ => ClientConfig::builder().with_safe_defaults().with_custom_certificate_verifier(cert_verifier).with_no_client_auth(), }; let mut config_alpn = config.clone(); diff --git a/src/outgoing.rs b/src/outgoing.rs index 125669e..1b8e7f8 100644 --- a/src/outgoing.rs +++ b/src/outgoing.rs @@ -1,5 +1,5 @@ use crate::{ - common::{first_bytes_match, outgoing::OutgoingConfig, shuffle_rd_wr_filter_only, stream_preamble}, + common::{outgoing::OutgoingConfig, shuffle_rd_wr_filter_only, stream_preamble, Peek}, context::Context, in_out::{StanzaRead, StanzaWrite}, slicesubsequence::SliceSubsequence, @@ -10,13 +10,13 @@ use anyhow::Result; use log::{error, info}; use tokio::{net::TcpListener, task::JoinHandle}; -async fn handle_outgoing_connection(stream: tokio::net::TcpStream, client_addr: &mut Context<'_>, config: OutgoingConfig) -> Result<()> { +async fn handle_outgoing_connection(mut stream: tokio::net::TcpStream, client_addr: &mut Context<'_>, config: OutgoingConfig) -> Result<()> { info!("{} connected", client_addr.log_from()); let mut in_filter = StanzaFilter::new(config.max_stanza_size_bytes); #[cfg(feature = "websocket")] - let (mut in_rd, mut in_wr) = if first_bytes_match(&stream, &mut in_filter.buf[0..3], |p| p == b"GET").await? { + let (mut in_rd, mut in_wr) = if stream.first_bytes_match(&mut in_filter.buf[0..3], |p| p == b"GET").await? { crate::websocket::incoming_websocket_connection(Box::new(stream), config.max_stanza_size_bytes).await? } else { let (in_rd, in_wr) = tokio::io::split(stream); diff --git a/src/quic/mod.rs b/src/quic/mod.rs index 82d4c6a..3a7bba1 100644 --- a/src/quic/mod.rs +++ b/src/quic/mod.rs @@ -1,5 +1,57 @@ +use crate::common::Split; +use anyhow::bail; +use quinn::{RecvStream, SendStream}; +use std::{ + io::Error, + pin::Pin, + task::{Context, Poll}, +}; +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; + #[cfg(feature = "incoming")] pub mod incoming; #[cfg(feature = "outgoing")] pub mod outgoing; + +pub struct QuicStream { + pub send: SendStream, + pub recv: RecvStream, +} + +impl AsyncRead for QuicStream { + fn poll_read(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>) -> Poll> { + Pin::new(&mut self.recv).poll_read(cx, buf) + } +} + +impl AsyncWrite for QuicStream { + fn poll_write(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll> { + Pin::new(&mut self.send).poll_write(cx, buf) + } + + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.send).poll_flush(cx) + } + + fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.send).poll_shutdown(cx) + } +} + +impl Split for QuicStream { + type ReadHalf = RecvStream; + type WriteHalf = SendStream; + + fn combine(recv: Self::ReadHalf, send: Self::WriteHalf) -> anyhow::Result { + if recv.id() != send.id() { + bail!("ids do not match") + } else { + Ok(Self { recv, send }) + } + } + + fn split(self) -> (Self::ReadHalf, Self::WriteHalf) { + (self.recv, self.send) + } +} diff --git a/src/srv.rs b/src/srv.rs index 3b32e0f..de70c30 100644 --- a/src/srv.rs +++ b/src/srv.rs @@ -20,6 +20,7 @@ use std::{ cmp::Ordering, convert::TryFrom, net::{IpAddr, SocketAddr}, + sync::Arc, }; use tokio_rustls::webpki::{DnsName, DnsNameRef}; #[cfg(feature = "websocket")] @@ -463,7 +464,7 @@ pub async fn srv_connect( bail!("outgoing s2s connection but s2s-outgoing disabled at compile-time"); } let (srvs, cert_verifier) = get_xmpp_connections(domain, is_c2s).await?; - let config = config.with_custom_certificate_verifier(is_c2s, cert_verifier); + let config = config.with_custom_certificate_verifier(is_c2s, Arc::new(cert_verifier)); for srv in srvs { let connect = srv.connect(domain, stream_open, in_filter, client_addr, config.clone()).await; if connect.is_err() { diff --git a/src/tls/incoming.rs b/src/tls/incoming.rs index 98e5f56..1432424 100644 --- a/src/tls/incoming.rs +++ b/src/tls/incoming.rs @@ -1,8 +1,7 @@ use crate::{ common::{ - first_bytes_match, incoming::{shuffle_rd_wr_filter, CloneableConfig, ServerCerts}, - to_str, IN_BUFFER_SIZE, + to_str, Peek, Split, IN_BUFFER_SIZE, }, context::Context, in_out::{StanzaRead, StanzaWrite}, @@ -14,7 +13,7 @@ use log::{error, info, trace}; use rustls::{ServerConfig, ServerConnection}; use std::{net::SocketAddr, sync::Arc}; use tokio::{ - io::{AsyncBufReadExt, AsyncWriteExt, BufStream}, + io::{AsyncRead, AsyncWrite, AsyncWriteExt, BufStream}, net::TcpListener, task::JoinHandle, }; @@ -41,7 +40,13 @@ pub fn spawn_tls_listener(listener: TcpListener, config: CloneableConfig, accept }) } -async fn handle_tls_connection(mut stream: tokio::net::TcpStream, client_addr: &mut Context<'_>, local_addr: SocketAddr, config: CloneableConfig, acceptor: TlsAcceptor) -> Result<()> { +pub async fn handle_tls_connection( + mut stream: S, + client_addr: &mut Context<'_>, + local_addr: SocketAddr, + config: CloneableConfig, + acceptor: TlsAcceptor, +) -> Result<()> { info!("{} connected", client_addr.log_from()); let mut in_filter = StanzaFilter::new(config.max_stanza_size_bytes); @@ -52,16 +57,19 @@ async fn handle_tls_connection(mut stream: tokio::net::TcpStream, client_addr: & * * could just check the leading 0x16 is TLS, it would *probably* be ok ? */ - let direct_tls = first_bytes_match(&stream, &mut in_filter.buf[0..3], |p| p[0] == 0x16 && p[1] == 0x03 && p[2] <= 0x03).await?; + let direct_tls = stream.first_bytes_match(&mut in_filter.buf[0..3], |p| p[0] == 0x16 && p[1] == 0x03 && p[2] <= 0x03).await?; client_addr.set_proto(if direct_tls { "directtls-in" } else { "starttls-in" }); info!("{} direct_tls sniffed", client_addr.log_from()); // starttls - if !direct_tls { + let stream = if !direct_tls { let mut proceed_sent = false; let (in_rd, mut in_wr) = stream.split(); + // todo: more efficient version for TCP: + //let (in_rd, mut in_wr) = stream.split(); + // we naively read 1 byte at a time, which buffering significantly speeds up let in_rd = tokio::io::BufReader::with_capacity(IN_BUFFER_SIZE, in_rd); let mut in_rd = StanzaReader(in_rd); @@ -109,7 +117,10 @@ async fn handle_tls_connection(mut stream: tokio::net::TcpStream, client_addr: & if !proceed_sent { bail!("stream ended before open"); } - } + ::combine(in_rd.0.into_inner(), in_wr)? + } else { + stream + }; let stream = acceptor.accept(stream).await?; let (_, server_connection) = stream.get_ref(); @@ -138,30 +149,9 @@ async fn handle_tls_connection(mut stream: tokio::net::TcpStream, client_addr: & #[cfg(feature = "websocket")] { - let stream: tokio_rustls::TlsStream = stream.into(); - let mut stream = BufStream::with_capacity(IN_BUFFER_SIZE, 0, stream); - let websocket = { - // wait up to 10 seconds until 3 bytes have been read - use std::time::{Duration, Instant}; - let duration = Duration::from_secs(10); - let now = Instant::now(); - let mut buf = stream.fill_buf().await?; - loop { - if buf.len() >= 3 { - break; // success - } - if buf.is_empty() { - bail!("not enough bytes"); - } - if Instant::now() - now > duration { - bail!("less than 3 bytes in 10 seconds, closed connection?"); - } - buf = stream.fill_buf().await?; - } - buf[..3] == b"GET"[..] - }; + let websocket = stream.first_bytes_match(&mut in_filter.buf[0..3], |b| b == b"GET").await?; if websocket { crate::websocket::incoming::handle_websocket_connection(Box::new(stream), config, server_certs, local_addr, client_addr, in_filter).await diff --git a/src/verify.rs b/src/verify.rs index 1488a47..bb17e03 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -2,7 +2,7 @@ use crate::{ common::ca_roots::TLS_SERVER_ROOTS, srv::{digest, Posh}, }; -use log::debug; +use log::{debug, trace}; use ring::digest::SHA256; use rustls::{ client::{ServerCertVerified, ServerCertVerifier}, @@ -41,6 +41,16 @@ pub fn pki_error(error: webpki::Error) -> Error { } } +pub fn verify_is_valid_tls_server_cert<'a>(end_entity: &'a Certificate, intermediates: &'a [Certificate], now: SystemTime) -> Result, Error> { + // from WebPkiVerifier, validates CA trusted cert + let (cert, chain) = prepare(end_entity, intermediates)?; + let webpki_now = webpki::Time::try_from(now).map_err(|_| Error::FailedToGetCurrentTime)?; + + cert.verify_is_valid_tls_server_cert(SUPPORTED_SIG_ALGS, &TLS_SERVER_ROOTS, &chain, webpki_now).map_err(pki_error)?; + + Ok(cert) +} + pub struct AllowAnonymousOrAnyCert; impl ClientCertVerifier for AllowAnonymousOrAnyCert { @@ -62,9 +72,9 @@ impl ClientCertVerifier for AllowAnonymousOrAnyCert { } } -type CertChainAndRoots<'a, 'b> = (webpki::EndEntityCert<'a>, Vec<&'a [u8]>); +type CertChainAndRoots<'a> = (webpki::EndEntityCert<'a>, Vec<&'a [u8]>); -fn prepare<'a, 'b>(end_entity: &'a Certificate, intermediates: &'a [Certificate]) -> Result, Error> { +fn prepare<'a>(end_entity: &'a Certificate, intermediates: &'a [Certificate]) -> Result, Error> { // EE cert must appear first. let cert = webpki::EndEntityCert::try_from(end_entity.0.as_ref()).map_err(pki_error)?; @@ -88,8 +98,8 @@ impl XmppServerCertVerifier { pub fn verify_cert(&self, end_entity: &Certificate, intermediates: &[Certificate], now: SystemTime) -> Result { if !self.sha256_pinnedpubkeys.is_empty() { let cert = webpki::TrustAnchor::try_from_cert_der(end_entity.0.as_ref()).map_err(pki_error)?; - println!("spki.len(): {}", cert.spki.len()); - println!("spki: {:?}", cert.spki); + trace!("spki.len(): {}", cert.spki.len()); + trace!("spki: {:?}", cert.spki); // todo: what is wrong with webpki? it returns *almost* the right answer but missing these leading bytes: // guess I'll open an issue... (I assume this is some type of algorithm identifying header or something) let mut pubkey: Vec = vec![48, 130, 1, 34]; @@ -111,11 +121,8 @@ impl XmppServerCertVerifier { debug!("posh failed for {:?}", self.names.first()); } } - // from WebPkiVerifier, validates CA trusted cert - let (cert, chain) = prepare(end_entity, intermediates)?; - let webpki_now = webpki::Time::try_from(now).map_err(|_| Error::FailedToGetCurrentTime)?; - - cert.verify_is_valid_tls_server_cert(SUPPORTED_SIG_ALGS, &TLS_SERVER_ROOTS, &chain, webpki_now).map_err(pki_error)?; + // validates CA trusted cert + let cert = verify_is_valid_tls_server_cert(end_entity, intermediates, now)?; for name in &self.names { if cert.verify_is_valid_for_dns_name(name.as_ref()).is_ok() {