From 4f5938e0cee741d43c5b14c5e62400578ba176e5 Mon Sep 17 00:00:00 2001 From: moparisthebest Date: Thu, 24 Mar 2022 22:49:50 -0400 Subject: [PATCH] Implement future host-meta.json proposal --- Cargo.lock | 1 + Cargo.toml | 1 + .../host-meta/xep-0156-proposed-minimal.json | 17 - contrib/host-meta/xep-0156-proposed.json | 12 - .../example.org.zone | 23 - .../23-s2s-websocket-host-meta/nginx1.conf | 25 - .../23-s2s-websocket-host-meta/nginx2.conf | 25 - .../prosody1.cfg.lua | 251 -------- .../prosody2.cfg.lua | 251 -------- .../xmpp-proxy1.toml | 42 -- .../xmpp-proxy2.toml | 42 -- .../xmpp-proxy3.toml | 44 -- .../nginx1.conf | 19 +- .../nginx2.conf | 19 +- integration/Dockerfile | 20 +- src/srv.rs | 538 +++++++++++++++--- src/verify.rs | 24 +- src/websocket.rs | 2 +- 18 files changed, 520 insertions(+), 836 deletions(-) delete mode 100644 integration/23-s2s-websocket-host-meta/example.org.zone delete mode 100644 integration/23-s2s-websocket-host-meta/nginx1.conf delete mode 100644 integration/23-s2s-websocket-host-meta/nginx2.conf delete mode 100644 integration/23-s2s-websocket-host-meta/prosody1.cfg.lua delete mode 100644 integration/23-s2s-websocket-host-meta/prosody2.cfg.lua delete mode 100644 integration/23-s2s-websocket-host-meta/xmpp-proxy1.toml delete mode 100644 integration/23-s2s-websocket-host-meta/xmpp-proxy2.toml delete mode 100644 integration/23-s2s-websocket-host-meta/xmpp-proxy3.toml diff --git a/Cargo.lock b/Cargo.lock index b1e3bce..8871e28 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1645,5 +1645,6 @@ dependencies = [ "tokio-tungstenite", "toml", "trust-dns-resolver", + "untrusted", "webpki-roots", ] diff --git a/Cargo.toml b/Cargo.toml index e34e715..e845577 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/contrib/host-meta/xep-0156-proposed-minimal.json b/contrib/host-meta/xep-0156-proposed-minimal.json index f48de3f..cc9a991 100644 --- a/contrib/host-meta/xep-0156-proposed-minimal.json +++ b/contrib/host-meta/xep-0156-proposed-minimal.json @@ -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=" } ] diff --git a/contrib/host-meta/xep-0156-proposed.json b/contrib/host-meta/xep-0156-proposed.json index 7b558da..a289ae7 100644 --- a/contrib/host-meta/xep-0156-proposed.json +++ b/contrib/host-meta/xep-0156-proposed.json @@ -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=" }, { diff --git a/integration/23-s2s-websocket-host-meta/example.org.zone b/integration/23-s2s-websocket-host-meta/example.org.zone deleted file mode 100644 index 3189be3..0000000 --- a/integration/23-s2s-websocket-host-meta/example.org.zone +++ /dev/null @@ -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 diff --git a/integration/23-s2s-websocket-host-meta/nginx1.conf b/integration/23-s2s-websocket-host-meta/nginx1.conf deleted file mode 100644 index 2ffaf6c..0000000 --- a/integration/23-s2s-websocket-host-meta/nginx1.conf +++ /dev/null @@ -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 ''; - } - } - -} diff --git a/integration/23-s2s-websocket-host-meta/nginx2.conf b/integration/23-s2s-websocket-host-meta/nginx2.conf deleted file mode 100644 index e548a8b..0000000 --- a/integration/23-s2s-websocket-host-meta/nginx2.conf +++ /dev/null @@ -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 ''; - } - } - -} diff --git a/integration/23-s2s-websocket-host-meta/prosody1.cfg.lua b/integration/23-s2s-websocket-host-meta/prosody1.cfg.lua deleted file mode 100644 index d2d3c30..0000000 --- a/integration/23-s2s-websocket-host-meta/prosody1.cfg.lua +++ /dev/null @@ -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" diff --git a/integration/23-s2s-websocket-host-meta/prosody2.cfg.lua b/integration/23-s2s-websocket-host-meta/prosody2.cfg.lua deleted file mode 100644 index 67a983c..0000000 --- a/integration/23-s2s-websocket-host-meta/prosody2.cfg.lua +++ /dev/null @@ -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" diff --git a/integration/23-s2s-websocket-host-meta/xmpp-proxy1.toml b/integration/23-s2s-websocket-host-meta/xmpp-proxy1.toml deleted file mode 100644 index ff8f87e..0000000 --- a/integration/23-s2s-websocket-host-meta/xmpp-proxy1.toml +++ /dev/null @@ -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" diff --git a/integration/23-s2s-websocket-host-meta/xmpp-proxy2.toml b/integration/23-s2s-websocket-host-meta/xmpp-proxy2.toml deleted file mode 100644 index 356c355..0000000 --- a/integration/23-s2s-websocket-host-meta/xmpp-proxy2.toml +++ /dev/null @@ -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" diff --git a/integration/23-s2s-websocket-host-meta/xmpp-proxy3.toml b/integration/23-s2s-websocket-host-meta/xmpp-proxy3.toml deleted file mode 100644 index 56fddce..0000000 --- a/integration/23-s2s-websocket-host-meta/xmpp-proxy3.toml +++ /dev/null @@ -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" diff --git a/integration/24-s2s-websocket-host-meta-json/nginx1.conf b/integration/24-s2s-websocket-host-meta-json/nginx1.conf index 271a016..dfa7c02 100644 --- a/integration/24-s2s-websocket-host-meta-json/nginx1.conf +++ b/integration/24-s2s-websocket-host-meta-json/nginx1.conf @@ -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" + } + ] + }'; } } diff --git a/integration/24-s2s-websocket-host-meta-json/nginx2.conf b/integration/24-s2s-websocket-host-meta-json/nginx2.conf index e4ad7ac..c6b01f0 100644 --- a/integration/24-s2s-websocket-host-meta-json/nginx2.conf +++ b/integration/24-s2s-websocket-host-meta-json/nginx2.conf @@ -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" + } + ] + }'; } } diff --git a/integration/Dockerfile b/integration/Dockerfile index 0bdf9c2..3675c7e 100644 --- a/integration/Dockerfile +++ b/integration/Dockerfile @@ -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 diff --git a/src/srv.rs b/src/srv.rs index 51cffcd..d135199 100644 --- a/src/srv.rs +++ b/src/srv.rs @@ -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 { + 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, + #[allow(dead_code)] + ech: Option, +} + +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 { + Some(self.cmp(other)) + } +} + +fn sort_dedup(ret: &mut Vec) { + 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, 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 { 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, secure_urls: Vec, txt_records: std::result::Result, is_c2s: bool) { +fn collect_txts(ret: &mut Vec, txt_records: std::result::Result, 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, secure_urls: Vec, 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, secure_urls: Vec, txt_rec pub async fn get_xmpp_connections(domain: &str, is_c2s: bool) -> Result<(Vec, XmppServerCertVerifier)> { let mut valid_tls_cert_server_names: Vec = 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 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); + } } diff --git a/src/verify.rs b/src/verify.rs index f02f83a..33c73ff 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -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()); diff --git a/src/websocket.rs b/src/websocket.rs index fa2bb72..1d5a4cf 100644 --- a/src/websocket.rs +++ b/src/websocket.rs @@ -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());