Implement future host-meta.json proposal

This commit is contained in:
Travis Burtrum 2022-03-24 22:49:50 -04:00
parent 5f05332e37
commit 4f5938e0ce
18 changed files with 520 additions and 836 deletions

1
Cargo.lock generated
View File

@ -1645,5 +1645,6 @@ dependencies = [
"tokio-tungstenite",
"toml",
"trust-dns-resolver",
"untrusted",
"webpki-roots",
]

View File

@ -29,6 +29,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"
untrusted = "0.7"
# logging deps
log = "0.4"

View File

@ -33,9 +33,6 @@
"priority": 10,
"weight": 50,
"sni": "example.org",
"alpn": [
"xmpp-client"
],
"ech": "eG1wcC1jbGllbnQ="
},
{
@ -48,9 +45,6 @@
"priority": 5,
"weight": 50,
"sni": "example.org",
"alpn": [
"xmpp-client"
],
"ech": "eG1wcC1jbGllbnQ="
},
{
@ -63,11 +57,6 @@
"priority": 15,
"weight": 50,
"sni": "example.org",
"alpn": [
"h2",
"http/1.1",
"h3"
],
"ech": "eG1wcC1jbGllbnQ="
},
{
@ -80,9 +69,6 @@
"priority": 10,
"weight": 50,
"sni": "example.org",
"alpn": [
"xmpp-server"
],
"ech": "eG1wcC1jbGllbnQ="
},
{
@ -95,9 +81,6 @@
"priority": 5,
"weight": 50,
"sni": "example.org",
"alpn": [
"xmpp-server"
],
"ech": "eG1wcC1jbGllbnQ="
}
]

View File

@ -70,9 +70,6 @@
"priority": 10,
"weight": 50,
"sni": "example.org",
"alpn": [
"xmpp-client"
],
"ech": "eG1wcC1jbGllbnQ="
},
{
@ -85,9 +82,6 @@
"priority": 5,
"weight": 50,
"sni": "example.org",
"alpn": [
"xmpp-client"
],
"ech": "eG1wcC1jbGllbnQ="
},
{
@ -117,9 +111,6 @@
"priority": 10,
"weight": 50,
"sni": "example.org",
"alpn": [
"xmpp-server"
],
"ech": "eG1wcC1jbGllbnQ="
},
{
@ -132,9 +123,6 @@
"priority": 5,
"weight": 50,
"sni": "example.org",
"alpn": [
"xmpp-server"
],
"ech": "eG1wcC1jbGllbnQ="
},
{

View File

@ -1,23 +0,0 @@
$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
web1 IN A 192.5.0.70
web2 IN A 192.5.0.80
one IN CNAME web1
two IN CNAME web2
scansion.one IN CNAME xp3
scansion.two IN CNAME xp3

View File

@ -1,25 +0,0 @@
daemon off;
worker_processes 1;
error_log stderr;
events {
worker_connections 32;
}
http {
access_log /dev/stdout;
server {
listen 443 ssl;
server_name one.example.org;
ssl_certificate /etc/prosody/certs/one.example.org.crt;
ssl_certificate_key /etc/prosody/certs/one.example.org.key;
location = /.well-known/host-meta {
default_type application/xrd+xml;
return 200 '<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link rel="urn:xmpp:alt-connections:s2s-websocket" href="wss://xp1.example.org:5281/xmpp-websocket"/><Link rel="urn:xmpp:alt-connections:websocket" href="wss://xp1.example.org:5281/xmpp-websocket"/></XRD>';
}
}
}

View File

@ -1,25 +0,0 @@
daemon off;
worker_processes 1;
error_log stderr;
events {
worker_connections 32;
}
http {
access_log /dev/stdout;
server {
listen 443 ssl;
server_name two.example.org;
ssl_certificate /etc/prosody/certs/two.example.org.crt;
ssl_certificate_key /etc/prosody/certs/two.example.org.key;
location = /.well-known/host-meta {
default_type application/xrd+xml;
return 200 '<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link rel="urn:xmpp:alt-connections:s2s-websocket" href="wss://xp2.example.org:5281/xmpp-websocket"/><Link rel="urn:xmpp:alt-connections:websocket" href="wss://xp2.example.org:5281/xmpp-websocket"/></XRD>';
}
}
}

View File

@ -1,251 +0,0 @@
--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";
}
-- 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
-- we don't need prosody doing any encryption, xmpp-proxy does this now
-- these are likely set to true somewhere in your file, find them, make them false
-- you can also remove all certificates from your config
s2s_require_encryption = false
s2s_secure_auth = false
-- xmpp-proxy outgoing is listening on this port, make all outgoing s2s connections directly to here
s2s_outgoing_proxy = { "192.5.0.40", 15270 }
-- 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"
}
-- 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}
-- Force clients to use encrypted connections? This option will
-- prevent clients from authenticating unless they are using encryption.
c2s_require_encryption = false
allow_unencrypted_plain_auth = true
-- 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

@ -1,251 +0,0 @@
--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";
}
-- 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
-- we don't need prosody doing any encryption, xmpp-proxy does this now
-- these are likely set to true somewhere in your file, find them, make them false
-- you can also remove all certificates from your config
s2s_require_encryption = false
s2s_secure_auth = false
-- xmpp-proxy outgoing is listening on this port, make all outgoing s2s connections directly to here
s2s_outgoing_proxy = { "192.5.0.50", 15270 }
-- 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"
}
-- 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}
-- Force clients to use encrypted connections? This option will
-- prevent clients from authenticating unless they are using encryption.
c2s_require_encryption = false
allow_unencrypted_plain_auth = true
-- 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

@ -1,42 +0,0 @@
# interfaces to listen for reverse proxy STARTTLS/Direct TLS XMPP connections on, should be open to the internet
incoming_listen = [ "0.0.0.0:5281" ]
# interfaces to listen for reverse proxy QUIC XMPP connections on, should be open to the internet
quic_listen = [ ]
# interfaces to listen for outgoing proxy TCP XMPP connections on, should be localhost
outgoing_listen = [ "0.0.0.0:15270" ]
# 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/xp1.example.org.key"
tls_cert = "/etc/prosody/certs/xp1.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

@ -1,42 +0,0 @@
# interfaces to listen for reverse proxy STARTTLS/Direct TLS XMPP connections on, should be open to the internet
incoming_listen = [ "0.0.0.0:5281" ]
# interfaces to listen for reverse proxy QUIC XMPP connections on, should be open to the internet
quic_listen = [ ]
# interfaces to listen for outgoing proxy TCP XMPP connections on, should be localhost
outgoing_listen = [ "0.0.0.0:15270" ]
# 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/xp2.example.org.key"
tls_cert = "/etc/prosody/certs/xp2.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

@ -1,44 +0,0 @@
# interfaces to listen for reverse proxy STARTTLS/Direct TLS XMPP connections on, should be open to the internet
incoming_listen = [ ]
# 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 = [ "0.0.0.0:5222" ]
# 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 = "127.0.0.1:15222"
# s2s port backend XMPP server listens on
s2s_target = "127.0.0.1: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/certs/rsa/one.example.org.key"
tls_cert = "/etc/certs/rsa/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

@ -18,7 +18,24 @@ http {
location = /.well-known/host-meta.json {
default_type application/json;
return 200 '{"links":[{"rel":"urn:xmpp:alt-connections:s2s-websocket","href":"wss://xp1.example.org:5281/xmpp-websocket"}, {"rel":"urn:xmpp:alt-connections:websocket","href":"wss://xp1.example.org:5281/xmpp-websocket"}]}';
return 200 '{
"links": [
{
"rel": "urn:xmpp:alt-connections:s2s-websocket",
"href": "wss://xp1.example.org:5281/xmpp-websocket",
"ips": [
"192.5.0.40"
],
"priority": 15,
"weight": 50,
"sni": "xp1.example.org"
},
{
"rel": "urn:xmpp:alt-connections:websocket",
"href": "wss://xp1.example.org:5281/xmpp-websocket"
}
]
}';
}
}

View File

@ -18,7 +18,24 @@ http {
location = /.well-known/host-meta.json {
default_type application/json;
return 200 '{"links":[{"rel":"urn:xmpp:alt-connections:s2s-websocket","href":"wss://xp2.example.org:5281/xmpp-websocket"}, {"rel":"urn:xmpp:alt-connections:websocket","href":"wss://xp2.example.org:5281/xmpp-websocket"}]}';
return 200 '{
"links": [
{
"rel": "urn:xmpp:alt-connections:s2s-websocket",
"href": "wss://xp2.example.org:5281/xmpp-websocket",
"ips": [
"192.5.0.50"
],
"priority": 15,
"weight": 50,
"sni": "xp2.example.org"
},
{
"rel": "urn:xmpp:alt-connections:websocket",
"href": "wss://xp2.example.org:5281/xmpp-websocket"
}
]
}';
}
}

View File

@ -39,19 +39,19 @@ RUN pacman -S --noconfirm --disable-download-timeout --needed bind nginx prosody
pacman -U --noconfirm --needed /tmp/*.pkg.tar* && rm -f /tmp/*.pkg.tar* && \
mkdir -p /opt/xmpp-proxy/prosody-modules/ /opt/prosody-modules/ /scansion && mkcert -install && \
mkdir -p /etc/certs/ecdsa && cd /etc/certs/ecdsa && \
mkcert -ecdsa -cert-file one.example.org.crt -key-file one.example.org.key one.example.org && \
mkcert -ecdsa -cert-file two.example.org.crt -key-file two.example.org.key two.example.org && \
mkcert -ecdsa -cert-file xp1.example.org.crt -key-file xp1.example.org.key xp1.example.org && \
mkcert -ecdsa -cert-file xp2.example.org.crt -key-file xp2.example.org.key xp2.example.org && \
mkcert -ecdsa -cert-file wildcard.crt -key-file wildcard.key '*.example.org' && \
mkcert -ecdsa -client -cert-file one.example.org.crt -key-file one.example.org.key one.example.org && \
mkcert -ecdsa -client -cert-file two.example.org.crt -key-file two.example.org.key two.example.org && \
mkcert -ecdsa -client -cert-file xp1.example.org.crt -key-file xp1.example.org.key xp1.example.org && \
mkcert -ecdsa -client -cert-file xp2.example.org.crt -key-file xp2.example.org.key xp2.example.org && \
mkcert -ecdsa -client -cert-file wildcard.crt -key-file wildcard.key '*.example.org' && \
cp wildcard.crt legacy_ssl.crt && cp wildcard.key legacy_ssl.key && \
cp wildcard.crt https.crt && cp wildcard.key https.key && \
mkdir -p /etc/certs/rsa && cd /etc/certs/rsa && \
mkcert -cert-file one.example.org.crt -key-file one.example.org.key one.example.org && \
mkcert -cert-file two.example.org.crt -key-file two.example.org.key two.example.org && \
mkcert -cert-file xp1.example.org.crt -key-file xp1.example.org.key xp1.example.org && \
mkcert -cert-file xp2.example.org.crt -key-file xp2.example.org.key xp2.example.org && \
mkcert -cert-file wildcard.crt -key-file wildcard.key '*.example.org' && \
mkcert -client -cert-file one.example.org.crt -key-file one.example.org.key one.example.org && \
mkcert -client -cert-file two.example.org.crt -key-file two.example.org.key two.example.org && \
mkcert -client -cert-file xp1.example.org.crt -key-file xp1.example.org.key xp1.example.org && \
mkcert -client -cert-file xp2.example.org.crt -key-file xp2.example.org.key xp2.example.org && \
mkcert -client -cert-file wildcard.crt -key-file wildcard.key '*.example.org' && \
cp wildcard.crt legacy_ssl.crt && cp wildcard.key legacy_ssl.key && \
cp wildcard.crt https.crt && cp wildcard.key https.key && \
chmod -R 777 /etc/certs/ && rm -rf /etc/prosody/certs && ln -sf /etc/certs/rsa /etc/prosody/certs

View File

@ -1,7 +1,8 @@
#![allow(clippy::upper_case_acronyms)]
use std::cmp::Ordering;
use std::convert::TryFrom;
use std::net::SocketAddr;
use std::net::{IpAddr, SocketAddr};
use data_encoding::BASE64;
use ring::digest::{Algorithm, Context as DigestContext, SHA256, SHA512};
@ -27,24 +28,128 @@ fn make_resolver() -> TokioAsyncResolver {
TokioAsyncResolver::tokio(config, options).unwrap()
}
#[derive(Clone, Debug, PartialEq)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum XmppConnectionType {
StartTLS,
DirectTLS,
#[cfg(feature = "quic")]
QUIC,
#[cfg(feature = "websocket")]
WebSocket(Uri, String, bool),
WebSocket(Uri, String),
}
impl XmppConnectionType {
fn idx(&self) -> u8 {
match self {
XmppConnectionType::QUIC => 0,
XmppConnectionType::DirectTLS => 1,
XmppConnectionType::StartTLS => 2,
XmppConnectionType::WebSocket(_, _) => 3,
}
}
}
impl Ord for XmppConnectionType {
fn cmp(&self, other: &Self) -> Ordering {
let cmp = self.idx().cmp(&other.idx());
if cmp != Ordering::Equal {
return cmp;
}
// so they are the same type, but WebSocket is a special case...
match (self, other) {
(XmppConnectionType::WebSocket(self_uri, self_origin), XmppConnectionType::WebSocket(other_uri, other_origin)) => {
let cmp = self_uri.to_string().cmp(&other_uri.to_string());
if cmp != Ordering::Equal {
return cmp;
}
self_origin.cmp(other_origin)
}
(_, _) => Ordering::Equal,
}
}
}
impl PartialOrd for XmppConnectionType {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[derive(Debug)]
pub struct XmppConnection {
conn_type: XmppConnectionType,
priority: u16,
#[allow(dead_code)]
weight: u16, // todo: use weight
port: u16,
target: String,
secure: bool,
ips: Vec<IpAddr>,
#[allow(dead_code)]
ech: Option<String>,
}
impl PartialEq for XmppConnection {
fn eq(&self, other: &Self) -> bool {
self.conn_type == other.conn_type && self.port == other.port && self.target == other.target
}
}
impl Ord for XmppConnection {
fn cmp(&self, other: &Self) -> Ordering {
// this should put equal things next to each other, but things we want to keep further to the left
let cmp = self.conn_type.cmp(&other.conn_type);
if cmp != Ordering::Equal {
return cmp;
}
let cmp = self.port.cmp(&other.port);
if cmp != Ordering::Equal {
return cmp;
}
let cmp = self.target.cmp(&other.target);
if cmp != Ordering::Equal {
return cmp;
}
// end of equality checks, now preferences:
// backwards on purpose, so secure is earlier in the list
let cmp = other.secure.cmp(&self.secure);
if cmp != Ordering::Equal {
return cmp;
}
// lowest priority preferred
let cmp = self.priority.cmp(&other.priority);
if cmp != Ordering::Equal {
return cmp;
}
// highest weight preferred
other.weight.cmp(&self.priority)
}
}
impl Eq for XmppConnection {}
impl PartialOrd for XmppConnection {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
fn sort_dedup(ret: &mut Vec<XmppConnection>) {
ret.sort();
ret.dedup();
// now sort by priority
ret.sort_by(|a, b| {
let cmp = a.priority.cmp(&b.priority);
if cmp != Ordering::Equal {
return cmp;
}
// prioritize "better" protocols todo: we *could* prioritize these first before priority...
let cmp = a.conn_type.idx().cmp(&b.conn_type.idx());
if cmp != Ordering::Equal {
return cmp;
}
// higher weight first todo: still not ideal
b.weight.cmp(&a.weight)
});
}
impl XmppConnection {
@ -57,12 +162,17 @@ impl XmppConnection {
config: OutgoingVerifierConfig,
) -> Result<(StanzaWrite, StanzaRead, SocketAddr, &'static str)> {
debug!("{} attempting connection to SRV: {:?}", client_addr.log_from(), self);
// todo: need to set options to Ipv4AndIpv6
let ips = RESOLVER.lookup_ip(self.target.clone()).await?;
// todo: for DNSSEC we need to optionally allow target in addition to domain, but what for SNI
let domain = if self.secure { &self.target } else { domain };
//let ips = RESOLVER.lookup_ip(self.target.clone()).await?;
let ips = if self.ips.is_empty() {
RESOLVER.lookup_ip(self.target.clone()).await?.iter().collect()
} else {
self.ips.clone() // todo: avoid clone?
};
for ip in ips.iter() {
let to_addr = SocketAddr::new(ip, self.port);
let to_addr = SocketAddr::new(*ip, self.port);
debug!("{} trying ip {}", client_addr.log_from(), to_addr);
// todo: for DNSSEC we need to optionally allow target in addition to domain, but what for SNI
match self.conn_type {
XmppConnectionType::StartTLS => match crate::starttls_connect(to_addr, domain, stream_open, in_filter, config.clone()).await {
Ok((wr, rd)) => return Ok((wr, rd, to_addr, "starttls-out")),
@ -79,12 +189,10 @@ impl XmppConnection {
},
#[cfg(feature = "websocket")]
// todo: when websocket is found via DNS, we need to validate cert against domain, *not* target, this is a security problem with XEP-0156, we are doing it the secure but likely unexpected way here for now
XmppConnectionType::WebSocket(ref url, ref origin, ref secure) => {
match crate::websocket_connect(to_addr, if *secure { &self.target } else { domain }, url, origin, config.clone()).await {
Ok((wr, rd)) => return Ok((wr, rd, to_addr, "websocket-out")),
Err(e) => error!("websocket connection failed to IP {} from TXT {}, error: {}", to_addr, url, e),
}
}
XmppConnectionType::WebSocket(ref url, ref origin) => match crate::websocket_connect(to_addr, domain, url, origin, config.clone()).await {
Ok((wr, rd)) => return Ok((wr, rd, to_addr, "websocket-out")),
Err(e) => error!("websocket connection failed to IP {} from TXT {}, error: {}", to_addr, url, e),
},
}
}
bail!("cannot connect to any IPs for SRV: {}", self.target)
@ -101,6 +209,9 @@ fn collect_srvs(ret: &mut Vec<XmppConnection>, srv_records: std::result::Result<
weight: srv.weight(),
port: srv.port(),
target: srv.target().to_ascii(),
secure: false, // todo: support dnssec here, and if true, look up TLSA
ips: Vec::new(),
ech: None,
});
}
}
@ -135,16 +246,19 @@ fn wss_to_srv(url: &str, secure: bool) -> Option<XmppConnection> {
443
};
Some(XmppConnection {
conn_type: XmppConnectionType::WebSocket(url, origin, secure),
conn_type: XmppConnectionType::WebSocket(url, origin),
priority: u16::MAX,
weight: 0,
port,
target,
secure,
ips: Vec::new(),
ech: None,
})
}
#[cfg(feature = "websocket")]
fn collect_txts(ret: &mut Vec<XmppConnection>, secure_urls: Vec<String>, txt_records: std::result::Result<TxtLookup, ResolveError>, is_c2s: bool) {
fn collect_txts(ret: &mut Vec<XmppConnection>, txt_records: std::result::Result<TxtLookup, ResolveError>, is_c2s: bool) {
if let Ok(txt_records) = txt_records {
for txt in txt_records.iter() {
for txt in txt.iter() {
@ -152,8 +266,8 @@ fn collect_txts(ret: &mut Vec<XmppConnection>, secure_urls: Vec<String>, txt_rec
if txt.starts_with(if is_c2s { b"_xmpp-client-websocket=wss://" } else { b"_xmpp-server-websocket=wss://" }) {
// 23 is the length of "_xmpp-client-websocket=" and "_xmpp-server-websocket="
if let Ok(url) = String::from_utf8(txt[23..].to_vec()) {
if !secure_urls.contains(&url) {
if let Some(srv) = wss_to_srv(&url, false) {
if let Some(srv) = wss_to_srv(&url, false) {
if !ret.contains(&srv) {
ret.push(srv);
}
}
@ -168,16 +282,11 @@ fn collect_txts(ret: &mut Vec<XmppConnection>, secure_urls: Vec<String>, txt_rec
pub async fn get_xmpp_connections(domain: &str, is_c2s: bool) -> Result<(Vec<XmppConnection>, XmppServerCertVerifier)> {
let mut valid_tls_cert_server_names: Vec<DnsName> = vec![DnsNameRef::try_from_ascii_str(domain)?.to_owned()];
let (starttls, direct_tls, quic, websocket_txt, websocket_rel) = if is_c2s {
("_xmpp-client._tcp", "_xmpps-client._tcp", "_xmppq-client._udp", "_xmppconnect", "urn:xmpp:alt-connections:websocket")
let mut sha256_pinnedpubkeys = Vec::new();
let (starttls, direct_tls, quic, websocket_txt) = if is_c2s {
("_xmpp-client._tcp", "_xmpps-client._tcp", "_xmppq-client._udp", "_xmppconnect")
} else {
(
"_xmpp-server._tcp",
"_xmpps-server._tcp",
"_xmppq-server._udp",
"_xmppconnect-server",
"urn:xmpp:alt-connections:s2s-websocket",
)
("_xmpp-server._tcp", "_xmpps-server._tcp", "_xmppq-server._udp", "_xmppconnect-server")
};
let starttls = format!("{}.{}.", starttls, domain).into_name()?;
@ -187,6 +296,8 @@ pub async fn get_xmpp_connections(domain: &str, is_c2s: bool) -> Result<(Vec<Xmp
#[cfg(feature = "websocket")]
let websocket_txt = format!("{}.{}.", websocket_txt, domain).into_name()?;
let mut ret = Vec::new();
// this lets them run concurrently but not in parallel, could spawn parallel tasks but... worth it ?
// todo: don't look up websocket or quic records when they are disabled
let (
@ -205,46 +316,34 @@ pub async fn get_xmpp_connections(domain: &str, is_c2s: bool) -> Result<(Vec<Xmp
RESOLVER.srv_lookup(quic),
//#[cfg(feature = "websocket")]
RESOLVER.txt_lookup(websocket_txt),
collect_host_meta(domain, websocket_rel),
collect_host_meta(&mut ret, &mut sha256_pinnedpubkeys, domain, is_c2s),
collect_posh(domain),
);
let mut ret = Vec::new();
collect_srvs(&mut ret, starttls, XmppConnectionType::StartTLS);
collect_srvs(&mut ret, direct_tls, XmppConnectionType::DirectTLS);
#[cfg(feature = "quic")]
collect_srvs(&mut ret, quic, XmppConnectionType::QUIC);
#[cfg(feature = "websocket")]
{
let urls = websocket_host.unwrap_or_default();
for url in &urls {
if let Some(url) = wss_to_srv(url, true) {
ret.push(url);
}
}
collect_txts(&mut ret, urls, websocket_txt, is_c2s);
if let Ok(Some(_ttl)) = websocket_host {
// todo: cache for ttl
} else {
// ignore everything else if new host-meta format
#[cfg(feature = "websocket")]
collect_txts(&mut ret, websocket_txt, is_c2s);
collect_srvs(&mut ret, starttls, XmppConnectionType::StartTLS);
collect_srvs(&mut ret, direct_tls, XmppConnectionType::DirectTLS);
#[cfg(feature = "quic")]
collect_srvs(&mut ret, quic, XmppConnectionType::QUIC);
}
ret.sort_by(|a, b| a.priority.cmp(&b.priority));
// todo: do something with weight
#[allow(clippy::single_match)]
sort_dedup(&mut ret);
for srv in &ret {
match srv.conn_type {
#[cfg(feature = "websocket")]
XmppConnectionType::WebSocket(_, _, ref secure) => {
if *secure {
if let Ok(target) = DnsNameRef::try_from_ascii_str(srv.target.as_str()) {
let target = target.to_owned();
if !valid_tls_cert_server_names.contains(&target) {
valid_tls_cert_server_names.push(target);
}
}
if srv.secure {
if let Ok(target) = DnsNameRef::try_from_ascii_str(srv.target.as_str()) {
let target = target.to_owned();
if !valid_tls_cert_server_names.contains(&target) {
valid_tls_cert_server_names.push(target);
}
}
_ => {}
}
}
let cert_verifier = XmppServerCertVerifier::new(valid_tls_cert_server_names, posh.ok());
let cert_verifier = XmppServerCertVerifier::new(valid_tls_cert_server_names, posh.ok(), sha256_pinnedpubkeys);
if ret.is_empty() {
// default starttls ports
@ -254,6 +353,9 @@ pub async fn get_xmpp_connections(domain: &str, is_c2s: bool) -> Result<(Vec<Xmp
target: domain.to_string(),
conn_type: XmppConnectionType::StartTLS,
port: if is_c2s { 5222 } else { 5269 },
secure: false,
ips: Vec::new(),
ech: None,
});
// by spec there are no default direct/quic ports, but we are going 443
ret.push(XmppConnection {
@ -262,6 +364,9 @@ pub async fn get_xmpp_connections(domain: &str, is_c2s: bool) -> Result<(Vec<Xmp
target: domain.to_string(),
conn_type: XmppConnectionType::DirectTLS,
port: 443,
secure: false,
ips: Vec::new(),
ech: None,
});
#[cfg(feature = "quic")]
ret.push(XmppConnection {
@ -270,6 +375,9 @@ pub async fn get_xmpp_connections(domain: &str, is_c2s: bool) -> Result<(Vec<Xmp
target: domain.to_string(),
conn_type: XmppConnectionType::QUIC,
port: 443,
secure: false,
ips: Vec::new(),
ech: None,
});
}
@ -328,40 +436,171 @@ pub async fn srv_connect(
}
#[cfg(not(feature = "websocket"))]
async fn collect_host_meta(domain: &str, rel: &str) -> Result<Vec<String>> {
bail!("websocket disabled")
async fn collect_host_meta(ret: &mut Vec<XmppConnection>, sha256_pinnedpubkeys: &mut Vec<String>, domain: &str, is_c2s: bool) -> Result<Option<u16>> {
collect_host_meta_json(ret, sha256_pinnedpubkeys, domain, is_c2s)
}
#[cfg(feature = "websocket")]
async fn collect_host_meta(domain: &str, rel: &str) -> Result<Vec<String>> {
match tokio::join!(collect_host_meta_xml(domain, rel), collect_host_meta_json(domain, rel)) {
(Ok(mut xml), Ok(json)) => {
combine_uniq(&mut xml, json);
Ok(xml)
async fn collect_host_meta(ret: &mut Vec<XmppConnection>, sha256_pinnedpubkeys: &mut Vec<String>, domain: &str, is_c2s: bool) -> Result<Option<u16>> {
let mut xml = Vec::new();
match tokio::join!(collect_host_meta_json(ret, sha256_pinnedpubkeys, domain, is_c2s), collect_host_meta_xml(&mut xml, domain, is_c2s)) {
(Ok(Some(ttl)), _) => Ok(Some(ttl)), // if ttl is returned, ignore host-meta.xml
(_, Ok(_)) => {
ret.append(&mut xml);
Ok(None)
}
(_, Ok(json)) => Ok(json),
(xml, _) => xml,
(json, _) => json,
}
}
#[cfg(feature = "websocket")]
async fn collect_host_meta_json(domain: &str, rel: &str) -> Result<Vec<String>> {
#[derive(Deserialize)]
struct HostMeta {
links: Vec<Link>,
}
#[derive(Deserialize)]
struct Link {
rel: String,
href: String,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "kebab-case")]
struct HostMeta {
xmpp: Option<HostMetaXmpp>,
links: Vec<Link>,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "kebab-case")]
struct HostMetaXmpp {
ttl: u16,
#[serde(default)]
public_key_pins_sha_256: Vec<String>,
}
#[derive(Deserialize, Debug)]
#[serde(tag = "rel", rename_all = "kebab-case")]
enum Link {
#[serde(rename = "urn:xmpp:alt-connections:websocket")]
WebSocket {
href: String,
#[serde(flatten)]
link: Option<LinkCommon>,
},
#[serde(rename = "urn:xmpp:alt-connections:tls")]
DirectTLS {
#[serde(flatten)]
link: LinkCommon,
port: u16,
},
#[serde(rename = "urn:xmpp:alt-connections:quic")]
Quic {
#[serde(flatten)]
link: LinkCommon,
port: u16,
},
#[serde(rename = "urn:xmpp:alt-connections:s2s-websocket")]
S2SWebSocket {
href: String,
#[serde(flatten)]
link: LinkCommon,
},
#[serde(rename = "urn:xmpp:alt-connections:s2s-tls")]
S2SDirectTLS {
#[serde(flatten)]
link: LinkCommon,
port: u16,
},
#[serde(rename = "urn:xmpp:alt-connections:s2s-quic")]
S2SQuic {
#[serde(flatten)]
link: LinkCommon,
port: u16,
},
#[serde(other)]
Unknown,
}
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "kebab-case")]
struct LinkCommon {
ips: Vec<IpAddr>,
priority: u16,
weight: u16,
sni: String,
ech: Option<String>,
}
impl LinkCommon {
pub fn into_xmpp_connection(self, conn_type: XmppConnectionType, port: u16) -> Option<XmppConnection> {
if self.ips.is_empty() {
error!("invalid empty ips");
return None;
}
Some(XmppConnection {
conn_type,
port,
priority: self.priority,
weight: self.weight,
target: self.sni,
ips: self.ips,
ech: self.ech,
secure: true,
})
}
}
impl Link {
pub fn into_xmpp_connection(self, is_c2s: bool) -> Option<XmppConnection> {
use XmppConnectionType::*;
let (srv_is_c2s, port, link, conn_type) = match self {
Link::DirectTLS { port, link } => (true, port, link, DirectTLS),
Link::Quic { port, link } => (true, port, link, QUIC),
Link::S2SDirectTLS { port, link } => (false, port, link, DirectTLS),
Link::S2SQuic { port, link } => (false, port, link, QUIC),
Link::WebSocket { href, link } => {
return if is_c2s {
let srv = wss_to_srv(&href, true)?;
if let Some(link) = link {
link.into_xmpp_connection(srv.conn_type, srv.port)
} else {
Some(srv)
}
} else {
None
};
}
Link::S2SWebSocket { href, link } => {
return if !is_c2s {
let srv = wss_to_srv(&href, true)?;
link.into_xmpp_connection(srv.conn_type, srv.port)
} else {
None
};
}
Link::Unknown => return None,
};
if srv_is_c2s == is_c2s {
link.into_xmpp_connection(conn_type, port)
} else {
None
}
}
}
impl HostMeta {
pub fn collect(self, ret: &mut Vec<XmppConnection>, sha256_pinnedpubkeys: &mut Vec<String>, is_c2s: bool) -> Option<u16> {
for link in self.links {
if let Some(srv) = link.into_xmpp_connection(is_c2s) {
ret.push(srv);
}
}
if let Some(xmpp) = self.xmpp {
sha256_pinnedpubkeys.extend(xmpp.public_key_pins_sha_256);
Some(xmpp.ttl)
} else {
None
}
}
}
async fn collect_host_meta_json(ret: &mut Vec<XmppConnection>, sha256_pinnedpubkeys: &mut Vec<String>, domain: &str, is_c2s: bool) -> Result<Option<u16>> {
let url = format!("https://{}/.well-known/host-meta.json", domain);
let resp = https_get(&url).await?;
if resp.status().is_success() {
let resp = resp.json::<HostMeta>().await?;
// we will only support wss:// (TLS) not ws:// (plain text)
Ok(resp.links.iter().filter(|l| l.rel == rel && l.href.starts_with("wss://")).map(|l| l.href.clone()).collect())
Ok(resp.collect(ret, sha256_pinnedpubkeys, is_c2s))
} else {
bail!("failed with status code {} for url {}", resp.status(), url)
}
@ -396,11 +635,21 @@ async fn parse_host_meta_xml(rel: &str, bytes: &[u8]) -> Result<Vec<String>> {
}
#[cfg(feature = "websocket")]
async fn collect_host_meta_xml(domain: &str, rel: &str) -> Result<Vec<String>> {
async fn collect_host_meta_xml(ret: &mut Vec<XmppConnection>, domain: &str, is_c2s: bool) -> Result<()> {
if !is_c2s {
bail!("host-meta XML unsupported for S2s");
}
let url = format!("https://{}/.well-known/host-meta", domain);
let resp = https_get(&url).await?;
if resp.status().is_success() {
parse_host_meta_xml(rel, resp.bytes().await?.as_ref()).await
let rel = "urn:xmpp:alt-connections:websocket";
let hosts = parse_host_meta_xml(rel, resp.bytes().await?.as_ref()).await?;
for host in hosts {
if let Some(srv) = wss_to_srv(&host, true) {
ret.push(srv);
}
}
Ok(())
} else {
bail!("failed with status code {} for url {}", resp.status(), url)
}
@ -515,7 +764,7 @@ impl Posh {
}
}
fn digest(algorithm: &'static Algorithm, buf: &[u8]) -> String {
pub fn digest(algorithm: &'static Algorithm, buf: &[u8]) -> String {
let mut context = DigestContext::new(algorithm);
context.update(buf);
let digest = context.finish();
@ -525,6 +774,7 @@ fn digest(algorithm: &'static Algorithm, buf: &[u8]) -> String {
#[cfg(test)]
mod tests {
use crate::srv::*;
use std::path::PathBuf;
fn valid_posh(posh: &[u8], cert: &[u8]) -> bool {
let posh: PoshJson = serde_json::from_slice(posh).unwrap();
@ -539,6 +789,17 @@ mod tests {
}
}
fn read_file(file: &str) -> Result<Vec<u8>> {
let mut f = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
f.push(file);
let mut file = File::open(f)?;
let mut data = Vec::new();
file.read_to_end(&mut data)?;
Ok(data)
}
#[test]
fn posh_deserialize() {
assert!(valid_posh(
@ -611,13 +872,14 @@ mod tests {
Ok(())
}
#[cfg(feature = "websocket")]
//#[tokio::test]
async fn http() -> Result<()> {
let hosts = collect_host_meta_json("burtrum.org", "urn:xmpp:alt-connections:websocket").await?;
println!("{:?}", hosts);
let hosts = collect_host_meta_xml("burtrum.org", "urn:xmpp:alt-connections:websocket").await?;
println!("{:?}", hosts);
let mut hosts = Vec::new();
let mut sha256_pinnedpubkeys = Vec::new();
let res = collect_host_meta(&mut hosts, &mut sha256_pinnedpubkeys, "burtrum.org", true).await;
println!("burtrum.org res: {:?}", res);
println!("burtrum.org hosts: {:?}", hosts);
println!("burtrum.org hosts: {:?}", sha256_pinnedpubkeys);
Ok(())
}
@ -642,6 +904,106 @@ mod tests {
let xrd = br#"<xrd xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><link rel="urn:xmpp:alt-connections:xbosh" href="https://burtrum.org/http-bind"/><link rel="urn:xmpp:alt-connections:websocket" href="wss://burtrum.org/xmpp-websocket"/><link rel="urn:xmpp:alt-connections:s2s-websocket" href="wss://burtrum.org/xmpp-websocket-s2s"/></xrd>"#;
assert_eq!(parse_host_meta_xml("urn:xmpp:alt-connections:websocket", xrd).await?, vec!["wss://burtrum.org/xmpp-websocket"]);
let xrd = read_file("contrib/host-meta/xep-0156-current.xml")?;
assert_eq!(parse_host_meta_xml("urn:xmpp:alt-connections:websocket", &xrd).await?, vec!["wss://example.org/xmpp-websocket"]);
Ok(())
}
#[cfg(feature = "websocket")]
#[tokio::test]
async fn test_parse_host_meta_json() -> Result<()> {
let xrd = read_file("contrib/host-meta/xep-0156-minimal.json")?;
let host_meta: HostMeta = serde_json::from_slice(&xrd)?;
println!("host_meta: {:?}", host_meta);
//assert_eq!(host_meta.links("urn:xmpp:alt-connections:websocket"), vec!["wss://example.org/xmpp-websocket"]);
let xrd = read_file("contrib/host-meta/xep-0156-current.json")?;
let host_meta: HostMeta = serde_json::from_slice(&xrd)?;
println!("host_meta: {:?}", host_meta);
//assert_eq!(host_meta.links("urn:xmpp:alt-connections:websocket"), vec!["wss://example.org/xmpp-websocket"]);
let xrd = read_file("contrib/host-meta/xep-0156-proposed.json")?;
let host_meta: HostMeta = serde_json::from_slice(&xrd)?;
println!("host_meta: {:?}", host_meta);
//assert_eq!(host_meta.links("urn:xmpp:alt-connections:websocket"), vec!["wss://example.org/xmpp-websocket"]);
Ok(())
}
#[test]
fn test_dedup() {
let domain = "example.org";
let mut ret = Vec::new();
ret.push(XmppConnection {
priority: 10,
weight: 0,
target: domain.to_string(),
conn_type: XmppConnectionType::DirectTLS,
port: 443,
secure: false,
ips: Vec::new(),
ech: None,
});
ret.push(XmppConnection {
priority: 0,
weight: 0,
target: domain.to_string(),
conn_type: XmppConnectionType::StartTLS,
port: 5222,
secure: false,
ips: Vec::new(),
ech: None,
});
ret.push(XmppConnection {
priority: 15,
weight: 0,
target: domain.to_string(),
conn_type: XmppConnectionType::DirectTLS,
port: 443,
secure: true,
ips: Vec::new(),
ech: None,
});
ret.push(XmppConnection {
priority: 10,
weight: 0,
target: domain.to_string(),
conn_type: XmppConnectionType::DirectTLS,
port: 443,
secure: true,
ips: Vec::new(),
ech: None,
});
ret.push(XmppConnection {
priority: 10,
weight: 50,
target: domain.to_string(),
conn_type: XmppConnectionType::DirectTLS,
port: 443,
secure: true,
ips: Vec::new(),
ech: None,
});
ret.push(XmppConnection {
priority: 10,
weight: 100,
target: "example.com".to_string(),
conn_type: XmppConnectionType::DirectTLS,
port: 443,
secure: true,
ips: Vec::new(),
ech: None,
});
ret.push(XmppConnection {
priority: 0,
weight: 100,
target: "example.com".to_string(),
conn_type: XmppConnectionType::DirectTLS,
port: 443,
secure: true,
ips: Vec::new(),
ech: None,
});
sort_dedup(&mut ret);
println!("ret dedup: {:?}", ret);
}
}

View File

@ -1,5 +1,6 @@
use crate::Posh;
use crate::{digest, Posh};
use log::debug;
use ring::digest::SHA256;
use rustls::client::{ServerCertVerified, ServerCertVerifier};
use rustls::server::{ClientCertVerified, ClientCertVerifier};
use rustls::{Certificate, DistinguishedNames, Error, ServerName};
@ -73,14 +74,31 @@ fn prepare<'a, 'b>(end_entity: &'a Certificate, intermediates: &'a [Certificate]
pub struct XmppServerCertVerifier {
names: Vec<DnsName>,
posh: Option<Posh>,
sha256_pinnedpubkeys: Vec<String>,
}
impl XmppServerCertVerifier {
pub fn new(names: Vec<DnsName>, posh: Option<Posh>) -> Self {
XmppServerCertVerifier { names, posh }
pub fn new(names: Vec<DnsName>, posh: Option<Posh>, sha256_pinnedpubkeys: Vec<String>) -> Self {
XmppServerCertVerifier { names, posh, sha256_pinnedpubkeys }
}
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);
// 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];
pubkey.extend(cert.spki);
if self.sha256_pinnedpubkeys.contains(&digest(&SHA256, &pubkey)) {
debug!("pinnedpubkey succeeded for {:?}", self.names.first());
return Ok(ServerCertVerified::assertion());
}
// todo: else fail ????
}
if let Some(ref posh) = self.posh {
if posh.valid_cert(end_entity.as_ref()) {
debug!("posh succeeded for {:?}", self.names.first());

View File

@ -74,7 +74,7 @@ pub fn to_ws_new(buf: &[u8], mut end_of_first_tag: usize, is_c2s: bool) -> Resul
.replace("<stream:stream ", "<open ")
.replace("jabber:server", "urn:ietf:params:xml:ns:xmpp-framing-server")
.replace("jabber:client", "urn:ietf:params:xml:ns:xmpp-framing")
.replace(">", "/>"));
.replace('>', "/>"));
}
if buf.starts_with(b"</stream:stream") {
return Ok(r#"<close xmlns="urn:ietf:params:xml:ns:xmpp-framing" />"#.to_string());