Some refactoring allowing better use as a library

This commit is contained in:
Travis Burtrum 2023-05-18 20:30:10 -04:00
parent e8b218e316
commit 5ae25e8aba
20 changed files with 1324 additions and 89 deletions

5
Cargo.lock generated
View File

@ -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",

View File

@ -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

View File

@ -0,0 +1,467 @@
-- mod_net_proxy.lua
-- Copyright (C) 2018 Pascal Mathis <mail@pascalmathis.com>
--
-- 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+<length> 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 <nil>, 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;
});

View File

@ -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);

View File

@ -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

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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<Vec<_>> = 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) {

View File

@ -152,7 +152,7 @@ async fn open_incoming(
is_c2s: bool,
in_filter: &mut StanzaFilter,
) -> Result<(ReadHalf<tokio::net::TcpStream>, WriteHalf<tokio::net::TcpStream>)> {
let target = if is_c2s {
let target: Option<SocketAddr> = 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?;

View File

@ -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<Self>;
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<Self> {
Ok(read_half.reunite(write_half)?)
}
fn split(self) -> (Self::ReadHalf, Self::WriteHalf) {
self.into_split()
}
}
impl<T: AsyncRead + AsyncWrite + Unpin + Send> Split for BufStream<T> {
type ReadHalf = tokio::io::ReadHalf<BufStream<T>>;
type WriteHalf = tokio::io::WriteHalf<BufStream<T>>;
fn combine(read_half: Self::ReadHalf, write_half: Self::WriteHalf) -> Result<Self> {
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<bool> {
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<bool> {
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<T: AsyncRead + AsyncWrite + Unpin + Send> Peek for BufStream<T> {
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<T: AsyncRead + Unpin + Send> Peek for BufReader<T> {
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<u8>, 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<rustls::sign::CertifiedKey> {
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<rustls::sign:
.next()
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid key"))?;
let tls_certs = certs(&mut BufReader::new(File::open(tls_cert)?))
let tls_certs = certs(&mut io::BufReader::new(File::open(tls_cert)?))
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid cert"))
.map(|mut certs| certs.drain(..).map(Certificate).collect())?;

View File

@ -1,8 +1,5 @@
use crate::{
common::{certs_key::CertsKey, ALPN_XMPP_CLIENT, ALPN_XMPP_SERVER},
verify::XmppServerCertVerifier,
};
use rustls::ClientConfig;
use crate::common::{certs_key::CertsKey, ALPN_XMPP_CLIENT, ALPN_XMPP_SERVER};
use rustls::{client::ServerCertVerifier, ClientConfig};
use std::sync::Arc;
use tokio_rustls::TlsConnector;
@ -13,16 +10,13 @@ pub struct OutgoingConfig {
}
impl OutgoingConfig {
pub fn with_custom_certificate_verifier(&self, is_c2s: bool, cert_verifier: XmppServerCertVerifier) -> OutgoingVerifierConfig {
pub fn with_custom_certificate_verifier(&self, is_c2s: bool, cert_verifier: Arc<dyn ServerCertVerifier>) -> 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();

View File

@ -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);

View File

@ -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<std::io::Result<()>> {
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<Result<usize, Error>> {
Pin::new(&mut self.send).poll_write(cx, buf)
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Error>> {
Pin::new(&mut self.send).poll_flush(cx)
}
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Error>> {
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<Self> {
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)
}
}

View File

@ -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() {

View File

@ -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<S: AsyncRead + AsyncWrite + Unpin + Send + Sync + Peek + Split + 'static>(
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");
}
}
<S as Split>::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<tokio::net::TcpStream> = 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

View File

@ -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<webpki::EndEntityCert<'a>, 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<CertChainAndRoots<'a, 'b>, Error> {
fn prepare<'a>(end_entity: &'a Certificate, intermediates: &'a [Certificate]) -> Result<CertChainAndRoots<'a>, 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<ServerCertVerified, Error> {
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<u8> = 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() {