diff --git a/Cargo.lock b/Cargo.lock index 6d99bdf..3e313d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1920,6 +1920,43 @@ version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" +[[package]] +name = "webtransport-generic" +version = "0.5.0" +source = "git+https://github.com/kixelated/webtransport-rs?rev=ba1a372a7a89e4ba9f9bc027733f82f87aa9a4fd#ba1a372a7a89e4ba9f9bc027733f82f87aa9a4fd" +dependencies = [ + "tokio", +] + +[[package]] +name = "webtransport-proto" +version = "0.6.0" +source = "git+https://github.com/kixelated/webtransport-rs?rev=ba1a372a7a89e4ba9f9bc027733f82f87aa9a4fd#ba1a372a7a89e4ba9f9bc027733f82f87aa9a4fd" +dependencies = [ + "bytes", + "http", + "thiserror", + "url", +] + +[[package]] +name = "webtransport-quinn" +version = "0.6.1" +source = "git+https://github.com/kixelated/webtransport-rs?rev=ba1a372a7a89e4ba9f9bc027733f82f87aa9a4fd#ba1a372a7a89e4ba9f9bc027733f82f87aa9a4fd" +dependencies = [ + "bytes", + "futures", + "http", + "log", + "quinn", + "quinn-proto", + "thiserror", + "tokio", + "url", + "webtransport-generic", + "webtransport-proto", +] + [[package]] name = "widestring" version = "1.0.2" @@ -2088,4 +2125,5 @@ dependencies = [ "toml", "trust-dns-resolver", "webpki-roots", + "webtransport-quinn", ] diff --git a/Cargo.toml b/Cargo.toml index 75782c1..9e1a490 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,11 +68,14 @@ rustls-pemfile = { version = "1.0", optional = true } tokio-tungstenite = { version = "0.19", optional = true, default-features = false, features = ["handshake"] } futures-util = { version = "0.3", default-features = false, features = ["async-await", "sink", "std"], optional = true } +# webtransport deps +webtransport-quinn = { version = "0.6", optional = true } + # systemd dep nix = { version = "0.26", optional = true, default-features = false, features = ["socket"]} [features] -default = ["c2s-incoming", "c2s-outgoing", "s2s-incoming", "s2s-outgoing", "tls", "quic", "websocket", "logging", "tls-ca-roots-native", "systemd"] +default = ["c2s-incoming", "c2s-outgoing", "s2s-incoming", "s2s-outgoing", "tls", "quic", "websocket", "webtransport", "logging", "tls-ca-roots-native", "systemd"] # you must pick one of these or the other, not both: todo: enable picking both and choosing at runtime # don't need either of these if only doing c2s-incoming @@ -97,6 +100,7 @@ s2s-outgoing = ["outgoing", "s2s"] tls = ["tokio-rustls", "webpki", "rustls"] quic = ["quinn", "rustls"] websocket = ["tokio-tungstenite", "futures-util", "tls"] # websocket+incoming also enables incoming TLS support as it's free +webtransport = ["webtransport-quinn", "quic"] # webtransport requires quic logging = ["rand", "env_logger"] systemd = ["nix"] @@ -106,3 +110,8 @@ net-test = [] [dev-dependencies] serde_json = "1.0" + +# need this until a release is made with this commit in it +[patch.crates-io] +webtransport-quinn = { git = "https://github.com/kixelated/webtransport-rs", rev = "ba1a372a7a89e4ba9f9bc027733f82f87aa9a4fd" } + diff --git a/README.md b/README.md index 61fbe2a..e58a868 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,11 @@ [![Build Status](https://ci.moparisthe.best/job/moparisthebest/job/xmpp-proxy/job/master/badge/icon%3Fstyle=plastic)](https://ci.moparisthe.best/job/moparisthebest/job/xmpp-proxy/job/master/) xmpp-proxy is a reverse proxy and outgoing proxy for XMPP servers and clients, providing [STARTTLS], [Direct TLS], [QUIC], -[WebSocket C2S], and [WebSocket S2S] connectivity to plain-text XMPP servers and clients and limiting stanza sizes without an XML parser. +[WebSocket C2S], [WebSocket S2S], and [WebTransport] connectivity to plain-text XMPP servers and clients and limiting stanza sizes without an XML parser. xmpp-proxy in reverse proxy (incoming) mode will: 1. listen on any number of interfaces/ports - 2. accept any STARTTLS, Direct TLS, QUIC, or WebSocket c2s or s2s connections from the internet + 2. accept any STARTTLS, Direct TLS, QUIC, WebSocket, or WebTransport c2s or s2s connections from the internet 3. terminate TLS 4. for s2s require a client cert and validate it correctly (using CAs, host-meta, host-meta2, and POSH) for SASL EXTERNAL auth 5. connect them to a local real XMPP server over plain-text TCP @@ -25,8 +25,8 @@ xmpp-proxy in reverse proxy (incoming) mode will: xmpp-proxy in outgoing mode will: 1. listen on any number of interfaces/ports 2. accept any plain-text TCP or WebSocket connection from a local XMPP server or client - 3. look up the required SRV, [host-meta], host-meta2, and [POSH] records - 4. connect to a real XMPP server across the internet over STARTTLS, Direct TLS, QUIC, or WebSocket + 3. look up the required SRV, [host-meta], [host-meta2], and [POSH] records + 4. connect to a real XMPP server across the internet over STARTTLS, Direct TLS, QUIC, WebSocket, or WebTransport 5. fallback to next SRV target or defaults as required to fully connect 6. perform all the proper required certificate validation logic 7. limit incoming stanza sizes as configured @@ -137,10 +137,11 @@ choose between 1-4 directions: 3. `s2s-incoming` - enables a server to accept incoming s2s connections 4. `s2s-outgoing` - enables a server to make outgoing s2s connections -choose between 1-3 transport protocols: +choose between 1-4 transport protocols: 1. `tls` - enables STARTTLS/TLS support 2. `quic` - enables QUIC support 3. `websocket` - enables WebSocket support, also enables TLS incoming support if the appropriate directions are enabled + 4. `webtransport` - enables WebTransport support, also enables QUIC choose exactly 1 of these methods to get trusted CA roots, not needed if only `c2s-incoming` is enabled: 1. `tls-ca-roots-native` - reads CA roots from operating system @@ -168,15 +169,18 @@ GNU/AGPLv3 - Check LICENSE.md for details Thanks [rxml](https://github.com/horazont/rxml) for afl-fuzz seeds #### Todo - 1. write "host-meta2" XEP for QUIC and WebSocket S2S Discovery - 2. optional [systemd](https://www.freedesktop.org/software/systemd/man/sd_listen_fds.html) [integration](https://mgdm.net/weblog/systemd/) - 3. seamless Tor integration, connecting to and from .onion domains + 1. seamless Tor integration, connecting to and from .onion domains + 2. Write WebTransport XEP + 3. Document systemd activation support + 4. Document use-as-a-library support [STARTTLS]: https://datatracker.ietf.org/doc/html/rfc6120#section-5 [Direct TLS]: https://xmpp.org/extensions/xep-0368.html [QUIC]: https://xmpp.org/extensions/xep-0467.html [WebSocket C2S]: https://datatracker.ietf.org/doc/html/rfc7395 [WebSocket S2S]: https://xmpp.org/extensions/xep-0468.html +[WebTransport]: https://www.w3.org/TR/webtransport/ [POSH]: https://datatracker.ietf.org/doc/html/rfc7711 [host-meta]: https://xmpp.org/extensions/xep-0156.html +[host-meta2]: https://xmpp.org/extensions/inbox/host-meta-2.html [PROXY protocol]: https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt diff --git a/check-all-features.sh b/check-all-features.sh index 45d35e4..93b5b67 100755 --- a/check-all-features.sh +++ b/check-all-features.sh @@ -47,7 +47,7 @@ perms_optional() { all_features() { for optional in "" $(perms_optional logging systemd) do - for proto in $(perms tls quic websocket) + for proto in $(perms tls quic websocket webtransport) do for direction in $(perms c2s-incoming c2s-outgoing s2s-incoming s2s-outgoing) do @@ -61,7 +61,7 @@ all_features() { for optional in "" $(perms_optional logging systemd) do - for proto in $(perms tls quic websocket) + for proto in $(perms tls quic websocket webtransport) do echo c2s-incoming,$proto$optional done diff --git a/integration/29-webtransport-host-meta-json/example.org.zone b/integration/29-webtransport-host-meta-json/example.org.zone new file mode 100644 index 0000000..3189be3 --- /dev/null +++ b/integration/29-webtransport-host-meta-json/example.org.zone @@ -0,0 +1,23 @@ +$TTL 300 +; example.org +@ IN SOA ns1.example.org. postmaster.example.org. ( + 2018111111 ; Serial + 28800 ; Refresh + 1800 ; Retry + 604800 ; Expire - 1 week + 86400 ) ; Negative Cache TTL + IN NS ns1 +ns1 IN A 192.5.0.10 +server1 IN A 192.5.0.20 +server2 IN A 192.5.0.30 +xp1 IN A 192.5.0.40 +xp2 IN A 192.5.0.50 +xp3 IN A 192.5.0.60 +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/29-webtransport-host-meta-json/nginx1.conf b/integration/29-webtransport-host-meta-json/nginx1.conf new file mode 100644 index 0000000..18ff1b7 --- /dev/null +++ b/integration/29-webtransport-host-meta-json/nginx1.conf @@ -0,0 +1,48 @@ +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.json { + default_type application/json; + return 200 '{ + "links": [ + { + "rel": "urn:xmpp:alt-connections:s2s-webtransport", + "href": "https://xp1.example.org/xmpp-webtransport", + "ips": [ + "192.5.0.40" + ], + "priority": 15, + "weight": 50, + "sni": "xp1.example.org" + }, + { + "rel": "urn:xmpp:alt-connections:webtransport", + "href": "https://xp1.example.org/xmpp-webtransport", + "ips": [ + "192.5.0.40" + ], + "priority": 15, + "weight": 50, + "sni": "xp1.example.org" + } + ] + }'; + } + } + +} diff --git a/integration/29-webtransport-host-meta-json/nginx2.conf b/integration/29-webtransport-host-meta-json/nginx2.conf new file mode 100644 index 0000000..cd6875d --- /dev/null +++ b/integration/29-webtransport-host-meta-json/nginx2.conf @@ -0,0 +1,48 @@ +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.json { + default_type application/json; + return 200 '{ + "links": [ + { + "rel": "urn:xmpp:alt-connections:s2s-webtransport", + "href": "https://xp2.example.org/xmpp-webtransport", + "ips": [ + "192.5.0.50" + ], + "priority": 15, + "weight": 50, + "sni": "xp2.example.org" + }, + { + "rel": "urn:xmpp:alt-connections:webtransport", + "href": "https://xp2.example.org/xmpp-webtransport", + "ips": [ + "192.5.0.50" + ], + "priority": 15, + "weight": 50, + "sni": "xp2.example.org" + } + ] + }'; + } + } + +} diff --git a/integration/29-webtransport-host-meta-json/prosody1.cfg.lua b/integration/29-webtransport-host-meta-json/prosody1.cfg.lua new file mode 100644 index 0000000..d2d3c30 --- /dev/null +++ b/integration/29-webtransport-host-meta-json/prosody1.cfg.lua @@ -0,0 +1,251 @@ +--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/29-webtransport-host-meta-json/prosody2.cfg.lua b/integration/29-webtransport-host-meta-json/prosody2.cfg.lua new file mode 100644 index 0000000..67a983c --- /dev/null +++ b/integration/29-webtransport-host-meta-json/prosody2.cfg.lua @@ -0,0 +1,251 @@ +--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/29-webtransport-host-meta-json/xmpp-proxy1.toml b/integration/29-webtransport-host-meta-json/xmpp-proxy1.toml new file mode 100644 index 0000000..c8c536b --- /dev/null +++ b/integration/29-webtransport-host-meta-json/xmpp-proxy1.toml @@ -0,0 +1,42 @@ + +# 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 = [ "0.0.0.0:443" ] +# 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/29-webtransport-host-meta-json/xmpp-proxy2.toml b/integration/29-webtransport-host-meta-json/xmpp-proxy2.toml new file mode 100644 index 0000000..57d6640 --- /dev/null +++ b/integration/29-webtransport-host-meta-json/xmpp-proxy2.toml @@ -0,0 +1,42 @@ + +# 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 = [ "0.0.0.0:443" ] +# 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/29-webtransport-host-meta-json/xmpp-proxy3.toml b/integration/29-webtransport-host-meta-json/xmpp-proxy3.toml new file mode 100644 index 0000000..56fddce --- /dev/null +++ b/integration/29-webtransport-host-meta-json/xmpp-proxy3.toml @@ -0,0 +1,44 @@ + +# interfaces to listen for reverse proxy STARTTLS/Direct TLS XMPP connections on, should be open to the internet +incoming_listen = [ ] +# 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/src/common/incoming.rs b/src/common/incoming.rs index 2a410b1..e304ab5 100644 --- a/src/common/incoming.rs +++ b/src/common/incoming.rs @@ -39,24 +39,35 @@ pub fn server_config(certs_key: Arc) -> Result { Ok(config) } -#[cfg(not(feature = "s2s-incoming"))] +#[cfg(not(any(feature = "s2s-incoming", feature = "webtransport")))] pub type ServerCerts = (); -#[cfg(feature = "s2s-incoming")] +#[cfg(any(feature = "s2s-incoming", feature = "webtransport"))] #[derive(Clone)] pub enum ServerCerts { Tls(&'static ServerConnection), #[cfg(feature = "quic")] - Quic(Arc), + Quic(Option>, Option, Option>), // todo: wrap this in arc or something now } -#[cfg(feature = "s2s-incoming")] +#[cfg(any(feature = "s2s-incoming", feature = "webtransport"))] impl ServerCerts { + #[cfg(feature = "quic")] + pub fn quic(conn: &quinn::Connection) -> ServerCerts { + let certs = conn.peer_identity().and_then(|v| v.downcast::>().ok()).map(|v| v.to_vec()); + let (sni, alpn) = conn + .handshake_data() + .and_then(|v| v.downcast::().ok()) + .map(|h| (h.server_name, h.protocol)) + .unwrap_or_default(); + ServerCerts::Quic(certs, sni, alpn) + } + pub fn peer_certificates(&self) -> Option> { match self { ServerCerts::Tls(c) => c.peer_certificates().map(|c| c.to_vec()), #[cfg(feature = "quic")] - ServerCerts::Quic(c) => c.peer_identity().and_then(|v| v.downcast::>().ok()).map(|v| v.to_vec()), + ServerCerts::Quic(certs, _, _) => certs.clone(), } } @@ -64,7 +75,7 @@ impl ServerCerts { match self { ServerCerts::Tls(c) => c.server_name().map(|s| s.to_string()), #[cfg(feature = "quic")] - ServerCerts::Quic(c) => c.handshake_data().and_then(|v| v.downcast::().ok()).and_then(|h| h.server_name), + ServerCerts::Quic(_, sni, _) => sni.clone(), } } @@ -72,7 +83,7 @@ impl ServerCerts { match self { ServerCerts::Tls(c) => c.alpn_protocol().map(|s| s.to_vec()), #[cfg(feature = "quic")] - ServerCerts::Quic(c) => c.handshake_data().and_then(|v| v.downcast::().ok()).and_then(|h| h.protocol), + ServerCerts::Quic(_, _, alpn) => alpn.clone(), } } @@ -80,7 +91,7 @@ impl ServerCerts { match self { ServerCerts::Tls(_) => true, #[cfg(feature = "quic")] - ServerCerts::Quic(_) => false, + ServerCerts::Quic(_, _, _) => false, } } } diff --git a/src/common/outgoing.rs b/src/common/outgoing.rs index 75e4509..d37b831 100644 --- a/src/common/outgoing.rs +++ b/src/common/outgoing.rs @@ -19,6 +19,13 @@ impl OutgoingConfig { _ => ClientConfig::builder().with_safe_defaults().with_custom_certificate_verifier(cert_verifier).with_no_client_auth(), }; + #[cfg(feature = "webtransport")] + let config_webtransport_alpn = { + let mut config = config.clone(); + config.alpn_protocols.push(webtransport_quinn::ALPN.to_vec()); + Arc::new(config) + }; + let mut config_alpn = config.clone(); config_alpn.alpn_protocols.push(if is_c2s { ALPN_XMPP_CLIENT } else { ALPN_XMPP_SERVER }.to_vec()); @@ -30,6 +37,8 @@ impl OutgoingConfig { OutgoingVerifierConfig { max_stanza_size_bytes: self.max_stanza_size_bytes, + #[cfg(feature = "webtransport")] + config_webtransport_alpn, config_alpn, connector_alpn, connector, @@ -41,6 +50,9 @@ impl OutgoingConfig { pub struct OutgoingVerifierConfig { pub max_stanza_size_bytes: usize, + #[cfg(feature = "webtransport")] + pub config_webtransport_alpn: Arc, + pub config_alpn: Arc, pub connector_alpn: TlsConnector, diff --git a/src/lib.rs b/src/lib.rs index 85b71dd..4aab4aa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,6 +17,9 @@ pub mod srv; #[cfg(feature = "websocket")] pub mod websocket; +#[cfg(feature = "webtransport")] +pub mod webtransport; + #[cfg(any(feature = "s2s-incoming", feature = "outgoing"))] pub mod verify; diff --git a/src/quic/incoming.rs b/src/quic/incoming.rs index 114d938..2b7cd6c 100644 --- a/src/quic/incoming.rs +++ b/src/quic/incoming.rs @@ -36,26 +36,21 @@ fn internal_spawn_quic_listener(incoming: Endpoint, local_addr: SocketAddr, conf tokio::spawn(async move { if let Ok(new_conn) = incoming_conn.await { let client_addr = Context::new("quic-in", new_conn.remote_address()); - - let new_conn = Arc::new(new_conn); - #[cfg(feature = "s2s-incoming")] - let server_certs = ServerCerts::Quic(new_conn.clone()); - #[cfg(not(feature = "s2s-incoming"))] - let server_certs = (); - info!("{} connected new connection", client_addr.log_from()); - while let Ok((wrt, rd)) = new_conn.accept_bi().await { - let config = config.clone(); - let mut client_addr = client_addr.clone(); - let server_certs = server_certs.clone(); - info!("{} connected new stream", client_addr.log_from()); - tokio::spawn(async move { - if let Err(e) = shuffle_rd_wr(StanzaRead::new(rd), StanzaWrite::new(wrt), config, server_certs, local_addr, &mut client_addr).await { - error!("{} {}", client_addr.log_from(), e); - } - }); - } + #[cfg(any(feature = "s2s-incoming", feature = "webtransport"))] + let server_certs = { + let server_certs = ServerCerts::quic(&new_conn); + #[cfg(feature = "webtransport")] + if server_certs.alpn().map(|a| a == webtransport_quinn::ALPN).unwrap_or(false) { + return crate::webtransport::incoming::handle_webtransport_session(new_conn, config, server_certs, local_addr, client_addr).await; + } + server_certs + }; + #[cfg(not(any(feature = "s2s-incoming", feature = "webtransport")))] + let server_certs = (); + + handle_quic_session(new_conn, config, server_certs, local_addr, client_addr).await } }); } @@ -64,7 +59,23 @@ fn internal_spawn_quic_listener(incoming: Endpoint, local_addr: SocketAddr, conf }) } -pub fn quic_server_config(server_config: rustls::ServerConfig) -> ServerConfig { +pub async fn handle_quic_session(conn: quinn::Connection, config: Arc, server_certs: ServerCerts, local_addr: SocketAddr, client_addr: Context<'static>) { + while let Ok((wrt, rd)) = conn.accept_bi().await { + let config = config.clone(); + let mut client_addr = client_addr.clone(); + let server_certs = server_certs.clone(); + info!("{} connected new stream", client_addr.log_from()); + tokio::spawn(async move { + if let Err(e) = shuffle_rd_wr(StanzaRead::new(rd), StanzaWrite::new(wrt), config, server_certs, local_addr, &mut client_addr).await { + error!("{} {}", client_addr.log_from(), e); + } + }); + } +} + +pub fn quic_server_config(mut server_config: rustls::ServerConfig) -> ServerConfig { + #[cfg(feature = "webtransport")] + server_config.alpn_protocols.push(webtransport_quinn::ALPN.to_vec()); let transport_config = quinn::TransportConfig::default(); // todo: configure transport_config here if needed let mut server_config = ServerConfig::with_crypto(Arc::new(server_config)); diff --git a/src/srv.rs b/src/srv.rs index ed0a68f..99e0bdf 100644 --- a/src/srv.rs +++ b/src/srv.rs @@ -13,7 +13,7 @@ use crate::{ use anyhow::{bail, Result}; use data_encoding::BASE64; use log::{debug, error, trace}; -use reqwest::Client; +use reqwest::{Client, Url}; use ring::digest::{Algorithm, Context as DigestContext, SHA256, SHA512}; use serde::Deserialize; use std::{ @@ -60,7 +60,10 @@ enum XmppConnectionType { #[cfg(feature = "quic")] QUIC, #[cfg(feature = "websocket")] + // uri, origin WebSocket(Uri, String), + #[cfg(feature = "webtransport")] + WebTransport(Url), } impl XmppConnectionType { @@ -69,11 +72,13 @@ impl XmppConnectionType { #[cfg(feature = "quic")] XmppConnectionType::QUIC => 0, #[cfg(feature = "tls")] - XmppConnectionType::DirectTLS => 1, + XmppConnectionType::DirectTLS => 2, #[cfg(feature = "tls")] - XmppConnectionType::StartTLS => 2, + XmppConnectionType::StartTLS => 3, #[cfg(feature = "websocket")] - XmppConnectionType::WebSocket(_, _) => 3, + XmppConnectionType::WebSocket(_, _) => 4, + #[cfg(feature = "webtransport")] + XmppConnectionType::WebTransport(_) => 1, } } } @@ -84,7 +89,7 @@ impl Ord for XmppConnectionType { if cmp != Ordering::Equal { return cmp; } - // so they are the same type, but WebSocket is a special case... + // so they are the same type, but WebSocket and WebTransport is a special case... match (self, other) { #[cfg(feature = "websocket")] (XmppConnectionType::WebSocket(self_uri, self_origin), XmppConnectionType::WebSocket(other_uri, other_origin)) => { @@ -94,6 +99,8 @@ impl Ord for XmppConnectionType { } self_origin.cmp(other_origin) } + #[cfg(feature = "webtransport")] + (XmppConnectionType::WebTransport(self_url), XmppConnectionType::WebTransport(other_url)) => self_url.cmp(other_url), (_, _) => Ordering::Equal, } } @@ -237,6 +244,20 @@ impl XmppConnection { } } }, + #[cfg(feature = "webtransport")] + XmppConnectionType::WebTransport(ref url) => match crate::webtransport::outgoing::webtransport_connect(to_addr, domain, url, config).await { + Ok((wr, rd)) => return Ok((wr, rd, to_addr, "webtransport-out")), + Err(e) => { + if self.secure && self.target != orig_domain { + match crate::webtransport::outgoing::webtransport_connect(to_addr, orig_domain, url, config).await { + Ok((wr, rd)) => return Ok((wr, rd, to_addr, "webtransport-out")), + Err(e2) => error!("webtransport connection failed to IP {} from URL {}, error try 1: {}, error try 2: {}", to_addr, url, e, e2), + } + } else { + error!("websocket connection failed to IP {} from URL {}, error: {}", to_addr, url, e) + } + } + }, } } bail!("cannot connect to any IPs for SRV: {}", self.target) @@ -301,6 +322,21 @@ fn wss_to_srv(url: &str, secure: bool) -> Option { }) } +#[cfg(feature = "webtransport")] +fn wt_to_srv(url: &str) -> Option<(XmppConnectionType, u16)> { + let url = match Url::parse(url) { + Ok(url) => url, + Err(e) => { + debug!("invalid URL record '{}': {}", url, e); + return None; + } + }; + + let port = if let Some(port) = url.port() { port } else { 443 }; + + Some((XmppConnectionType::WebTransport(url), port)) +} + #[cfg(feature = "websocket")] fn collect_txts(ret: &mut Vec, txt_records: std::result::Result, is_c2s: bool) { if let Ok(txt_records) = txt_records { @@ -534,6 +570,12 @@ enum Link { #[serde(flatten)] link: Option, }, + #[serde(rename = "urn:xmpp:alt-connections:webtransport")] + WebTransport { + href: String, + #[serde(flatten)] + link: LinkCommon, + }, #[serde(rename = "urn:xmpp:alt-connections:tls")] DirectTLS { #[serde(flatten)] @@ -546,6 +588,12 @@ enum Link { link: LinkCommon, port: u16, }, + #[serde(rename = "urn:xmpp:alt-connections:s2s-webtransport")] + S2SWebTransport { + href: String, + #[serde(flatten)] + link: LinkCommon, + }, #[serde(rename = "urn:xmpp:alt-connections:s2s-websocket")] S2SWebSocket { href: String, @@ -630,6 +678,24 @@ impl Link { None }; } + #[cfg(feature = "webtransport")] + Link::WebTransport { href, link } => { + return if is_c2s { + let (conn_type, port) = wt_to_srv(&href)?; + link.into_xmpp_connection(conn_type, port) + } else { + None + }; + } + #[cfg(feature = "webtransport")] + Link::S2SWebTransport { href, link } => { + return if !is_c2s { + let (conn_type, port) = wt_to_srv(&href)?; + link.into_xmpp_connection(conn_type, port) + } else { + None + }; + } _ => return None, }; diff --git a/src/tls/incoming.rs b/src/tls/incoming.rs index d04c0a4..5ae3283 100644 --- a/src/tls/incoming.rs +++ b/src/tls/incoming.rs @@ -127,12 +127,12 @@ pub async fn handle_tls_connection(mut stream: S, cl // where we read the first stanza, where we are guaranteed the handshake is complete, but I can't // do that without ignoring the lifetime and just pulling a C programmer and pinky promising to be // *very careful* that this reference doesn't outlive stream... - #[cfg(feature = "s2s-incoming")] + #[cfg(any(feature = "s2s-incoming", feature = "webtransport"))] let server_certs = { let server_connection: &'static ServerConnection = unsafe { std::mem::transmute(server_connection) }; ServerCerts::Tls(server_connection) }; - #[cfg(not(feature = "s2s-incoming"))] + #[cfg(not(any(feature = "s2s-incoming", feature = "webtransport")))] let server_certs = (); #[cfg(not(feature = "websocket"))] diff --git a/src/webtransport/incoming.rs b/src/webtransport/incoming.rs new file mode 100644 index 0000000..a4f9c59 --- /dev/null +++ b/src/webtransport/incoming.rs @@ -0,0 +1,44 @@ +use crate::{ + common::incoming::{shuffle_rd_wr, IncomingConfig, ServerCerts}, + context::Context, + in_out::{StanzaRead, StanzaWrite}, +}; + +use log::{error, info}; +use std::{net::SocketAddr, sync::Arc}; + +pub async fn handle_webtransport_session(conn: quinn::Connection, config: Arc, server_certs: ServerCerts, local_addr: SocketAddr, mut client_addr: Context<'static>) { + client_addr.set_proto("webtransport-in"); + + // Perform the WebTransport handshake. + let request = match webtransport_quinn::accept(conn).await { + Ok(r) => r, + Err(e) => { + error!("{} {}", client_addr.log_from(), e); + return; + } + }; + info!("{} received request URL: {}", client_addr.log_from(), request.url()); + + // Accept the session. + let session = match request.ok().await { + Ok(r) => r, + Err(e) => { + error!("{} {}", client_addr.log_from(), e); + return; + } + }; + info!("{} connected new session", client_addr.log_from()); + + while let Ok((wrt, rd)) = session.accept_bi().await { + let config = config.clone(); + let mut client_addr = client_addr.clone(); + let server_certs = server_certs.clone(); + info!("{} connected new stream", client_addr.log_from()); + tokio::spawn(async move { + if let Err(e) = shuffle_rd_wr(StanzaRead::new(rd), StanzaWrite::new(wrt), config, server_certs, local_addr, &mut client_addr).await { + error!("{} {}", client_addr.log_from(), e); + } + }); + } +} diff --git a/src/webtransport/mod.rs b/src/webtransport/mod.rs new file mode 100644 index 0000000..82d4c6a --- /dev/null +++ b/src/webtransport/mod.rs @@ -0,0 +1,5 @@ +#[cfg(feature = "incoming")] +pub mod incoming; + +#[cfg(feature = "outgoing")] +pub mod outgoing; diff --git a/src/webtransport/outgoing.rs b/src/webtransport/outgoing.rs new file mode 100644 index 0000000..9dca8d4 --- /dev/null +++ b/src/webtransport/outgoing.rs @@ -0,0 +1,26 @@ +use std::net::SocketAddr; + +use crate::{ + common::outgoing::OutgoingVerifierConfig, + in_out::{StanzaRead, StanzaWrite}, +}; +use anyhow::Result; +use log::trace; +use reqwest::Url; + +pub async fn webtransport_connect(target: SocketAddr, server_name: &str, url: &Url, config: &OutgoingVerifierConfig) -> Result<(StanzaWrite, StanzaRead)> { + let bind_addr = "0.0.0.0:0".parse().unwrap(); + + let mut endpoint = quinn::Endpoint::client(bind_addr)?; + endpoint.set_default_client_config(quinn::ClientConfig::new(config.config_webtransport_alpn.clone())); + + // connect to server + let connection = endpoint.connect(target, server_name)?.await?; + trace!("quic pre-wt connected: addr={}", connection.remote_address()); + + let connection = webtransport_quinn::connect_with(connection, url).await?; + trace!("webtransport connected: addr={}", connection.remote_address()); + + let (wrt, rd) = connection.open_bi().await?; + Ok((StanzaWrite::new(wrt), StanzaRead::new(rd))) +} diff --git a/xmpp-proxy.toml b/xmpp-proxy.toml index b1f7618..8884b2b 100644 --- a/xmpp-proxy.toml +++ b/xmpp-proxy.toml @@ -1,7 +1,7 @@ # interfaces to listen for reverse proxy STARTTLS/Direct TLS/TLS WebSocket (wss) XMPP connections on, should be open to the internet incoming_listen = [ "0.0.0.0:5222", "0.0.0.0:5269", "0.0.0.0:443" ] -# interfaces to listen for reverse proxy QUIC XMPP connections on, should be open to the internet +# interfaces to listen for reverse proxy QUIC/WebTransport XMPP connections on, should be open to the internet quic_listen = [ "0.0.0.0:443" ] # interfaces to listen for outgoing proxy TCP or WebSocket XMPP connections on, should be localhost or a path for a unix socket outgoing_listen = [ "127.0.0.1:15270" ] @@ -39,4 +39,4 @@ tls_cert = "/etc/xmpp-proxy/fullchain.cer" #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" \ No newline at end of file +#log_style = "never"