feat: Adds docs, config and scripts around the visitor mode. (#12658)

* feat: Moves handle of vnode from conferenceIQ stanza error to result.

* feat: Handles redirected to visitor node event.

* feat: Adds README and configurations.

* squash: Drop comment.

* copy edits

* image fix

* fix background for dark mode

* fix the background

* feat: Update s2soutinjection.

* Update README commands formatting.

* Update doc/extra-large-conference/README.md

Co-authored-by: Saúl Ibarra Corretgé <saghul@jitsi.org>

* squash: Creates a generateVisitorConfig helper.

* squash: Moves the folder from doc.

* squash: Update example.

* squash: Drop config.

* squash: Rename var to look like template.

* squash: Fix plugins path.

* squash: Fix sort order of import.

* squash: Fix lint errors.

Co-authored-by: scott boone <scott.e.boone@gmail.com>
Co-authored-by: Scott Boone <scott.boone@8x8.com>
Co-authored-by: Saúl Ibarra Corretgé <saghul@jitsi.org>
This commit is contained in:
Дамян Минков 2022-12-13 08:26:22 -06:00 committed by GitHub
parent 3445c513ba
commit 9fbbe05d6c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 2639 additions and 41 deletions

View File

@ -47,6 +47,7 @@ import {
dataChannelClosed,
dataChannelOpened,
e2eRttChanged,
generateVisitorConfig,
getConferenceOptions,
kickedOut,
lockStateChanged,
@ -277,7 +278,8 @@ class ConferenceConnector {
/**
*
*/
constructor(resolve, reject) {
constructor(resolve, reject, conference) {
this._conference = conference;
this._resolve = resolve;
this._reject = reject;
this.reconnectTimeout = null;
@ -336,6 +338,26 @@ class ConferenceConnector {
break;
}
case JitsiConferenceErrors.REDIRECTED: {
generateVisitorConfig(APP.store.getState(), params);
connection.disconnect().then(() => {
connect(this._conference.roomName).then(con => {
const localTracks = getLocalTracks(APP.store.getState()['features/base/tracks']);
const jitsiTracks = localTracks.map(t => t.jitsiTrack);
// visitors connect muted
jitsiTracks.forEach(t => t.mute());
// TODO disable option to unmute audio or video
this._conference.startConference(con, jitsiTracks);
});
});
break;
}
case JitsiConferenceErrors.GRACEFUL_SHUTDOWN:
APP.UI.notifyGracefulShutdown();
break;
@ -732,7 +754,7 @@ export default {
// XXX The API will take care of disconnecting from the XMPP
// server (and, thus, leaving the room) on unload.
return new Promise((resolve, reject) => {
new ConferenceConnector(resolve, reject).connect();
new ConferenceConnector(resolve, reject, this).connect();
});
},
@ -1349,7 +1371,7 @@ export default {
this._createRoom(localTracks);
return new Promise((resolve, reject) => {
new ConferenceConnector(resolve, reject).connect();
new ConferenceConnector(resolve, reject, this).connect();
});
},

View File

@ -15,6 +15,17 @@ upstream jvb1 {
server 127.0.0.1:9090;
keepalive 2;
}
map $arg_vnode $prosody_node {
default prosody;
v1 v1;
v2 v2;
v3 v3;
v4 v4;
v5 v5;
v6 v6;
v7 v7;
v8 v8;
}
server {
listen 80;
listen [::]:80;
@ -95,7 +106,7 @@ server {
# BOSH
location = /http-bind {
proxy_pass http://prosody/http-bind?prefix=$prefix&$args;
proxy_pass http://$prosody_node/http-bind?prefix=$prefix&$args;
proxy_http_version 1.1;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Host $http_host;
@ -104,7 +115,7 @@ server {
# xmpp websockets
location = /xmpp-websocket {
proxy_pass http://prosody/xmpp-websocket?prefix=$prefix&$args;
proxy_pass http://$prosody_node/xmpp-websocket?prefix=$prefix&$args;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

View File

@ -252,6 +252,34 @@ export function getConferenceOptions(stateful: IStateful) {
return options;
}
/**
* Returns an object aggregating the conference options.
*
* @param {IStateful} stateful - The redux store state.
* @param {Array<string>} params - The received parameters.
* @returns {void}
*/
export function generateVisitorConfig(stateful: IStateful, params: Array<string>) {
const [ vnode, focusJid ] = params;
const config = toState(stateful)['features/base/config'];
if (!config || !config.hosts) {
logger.warn('Wrong configuration, missing hosts.');
return;
}
const oldDomain = config.hosts.domain;
config.hosts.domain = `${vnode}.meet.jitsi`;
config.hosts.muc = config.hosts.muc.replace(oldDomain, config.hosts.domain);
config.hosts.visitorFocus = focusJid;
config.bosh += `?vnode=${vnode}`;
config.websocket += `?vnode=${vnode}`;
}
/**
* Returns the UTC timestamp when the first participant joined the conference.
*

View File

@ -339,6 +339,7 @@ export interface IConfig {
domain: string;
focus?: string;
muc: string;
visitorFocus: string;
};
iAmRecorder?: boolean;
iAmSipGateway?: boolean;

View File

@ -0,0 +1,88 @@
WARNING: This is still a Work In Progress
The final implementation may diverge. Currently, only the participants after a
configured threshold will be just viewers (visitors) and there is no promotion
mechanism to become a main participant yet.
TODO:
* Merge messaging between visitor nodes and main conference
* Polls
* Raise hand to be promoted to enter the main conference
* Make sure it works with tenants.
# Low-latency conference streaming to very large audiences
To have a low-latency conference with a very large audience, the media and
signaling load must be spread beyond what can be handled by a typical Jitsi
installation. A call with 10k participants requires around 50 bridges on decent
vms (8+ cores). The main participants of a conference with a very large
audience will share a main prosody, like with normal conferences, and
additional prosody vms are needed to support signaling to the audience.
In the example configuration we use a 16 core machine. Eight of the cores are
used for the main prosody and other services (nginx, jicofo, etc) and the other
eight cores are used to run prosody services for visitors, i.e., "visitor
prosodies".
We consider 2000 participants per visitor node a safe value. So eight visitor
prosodies will be enough for one 10k participants meeting.
<img src="imgs/visitors-prosody.svg" alt="diagram of a central prosody connected to several visitor prosodies" width="500"/>
# Configuration
Use the `pre-configure.sh` script to configure your system, passing it the
number of visitor prosodies to set up.
`./pre-configure.sh 8`
The script will add for each visitor prosody:
- folders in `/etc/`
- a systemd unit file in `/lib/systemd/system/`
- a user for jicofo
- a config entry in jicofo.conf
Setting up configuration for the main prosody is a manual process:
- Add to the enabled modules list in the general part (e.g. [here](https://github.com/bjc/prosody/blob/76bf6d511f851c7cde8a81257afaaae0fb7a4160/prosody.cfg.lua.dist#L33)):
```
"s2s_bidi";
"certs_s2soutinjection";
"s2soutinjection";
"s2s_whitelist";
```
- Add the following config also in the general part (matching the number of prosodies you generated config for):
```
-- targets must be IPs, not hostnames
s2s_connect_overrides = {
["conference.v1.meet.jitsi"] = { "127.0.0.1", 52691 };
["conference.v2.meet.jitsi"] = { "127.0.0.1", 52692 };
["conference.v3.meet.jitsi"] = { "127.0.0.1", 52693 };
["conference.v4.meet.jitsi"] = { "127.0.0.1", 52694 };
["conference.v5.meet.jitsi"] = { "127.0.0.1", 52695 };
["conference.v6.meet.jitsi"] = { "127.0.0.1", 52696 };
["conference.v7.meet.jitsi"] = { "127.0.0.1", 52697 };
["conference.v8.meet.jitsi"] = { "127.0.0.1", 52698 };
}
-- allowed list of server-2-server connections
s2s_whitelist = {
"conference.v1.meet.jitsi", "conference.v2.meet.jitsi", "conference.v3.meet.jitsi", "conference.v4.meet.jitsi",
"conference.v5.meet.jitsi", "conference.v6.meet.jitsi", "conference.v7.meet.jitsi", "conference.v8.meet.jitsi"
};
```
- Make sure s2s is not in modules_disabled
- Enable `"xxl_conference";` module under the main virtual host (e.g. [here](https://github.com/jitsi/jitsi-meet/blob/f42772ec5bcc87ff6de17423d36df9bcad6e770d/doc/debian/jitsi-meet-prosody/prosody.cfg.lua-jvb.example#L57))
After configuring you can set the maximum number of main participants, before
redirecting to visitors.
```
hocon -f /etc/jitsi/jicofo/jicofo.conf set "jicofo.visitors.max-participants" 30
```
Now restart prosody and jicofo
```
service prosody restart
service jicofo restart
```
Now after the main 30 participants join, the rest will be visitors using the
visitor nodes.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 198 KiB

View File

@ -0,0 +1,51 @@
#!/bin/bash
SCRIPT_DIR=`dirname "$0"`
cd $SCRIPT_DIR
NUMBER_OF_INSTANCES=$1
if ! [[ $NUMBER_OF_INSTANCES =~ ^[0-9]+([.][0-9]+)?$ ]] ; then
echo "error: Not a number param" >&2;
exit 1
fi
echo "Will configure $NUMBER_OF_INSTANCES number of visitor prosodies"
set -e
set -x
# Configure prosody instances
for (( i=1 ; i<=${NUMBER_OF_INSTANCES} ; i++ ));
do
cp prosody-v.service.template /lib/systemd/system/prosody-v${i}.service
sed -i "s/vX/v${i}/g" /lib/systemd/system/prosody-v${i}.service
mkdir /etc/prosody-v${i}
ln -s /etc/prosody/certs /etc/prosody-v${i}/certs
cp prosody.cfg.lua.visitor.template /etc/prosody-v${i}/prosody.cfg.lua
sed -i "s/vX/v${i}/g" /etc/prosody-v${i}/prosody.cfg.lua
done
# Configure jicofo
HOCON_CONFIG="/etc/jitsi/jicofo/jicofo.conf"
hocon -f $HOCON_CONFIG set "jicofo.bridge.selection-strategy" "VisitorSelectionStrategy"
hocon -f $HOCON_CONFIG set "jicofo.bridge.visitor-selection-strategy" "RegionBasedBridgeSelectionStrategy"
hocon -f $HOCON_CONFIG set "jicofo.bridge.topology-strategy" "VisitorTopologyStrategy"
PASS=$(hocon -f $HOCON_CONFIG get "jicofo.xmpp.client.password")
for (( i=1 ; i<=${NUMBER_OF_INSTANCES} ; i++ ));
do
prosodyctl --config /etc/prosody-v${i}/prosody.cfg.lua register focus auth.meet.jitsi $PASS
hocon -f $HOCON_CONFIG set "jicofo.xmpp.visitors.v${i}.enabled" true
hocon -f $HOCON_CONFIG set "jicofo.xmpp.visitors.v${i}.conference-service" "conference.v${i}.meet.jitsi"
hocon -f $HOCON_CONFIG set "jicofo.xmpp.visitors.v${i}.hostname" 127.0.0.1
hocon -f $HOCON_CONFIG set "jicofo.xmpp.visitors.v${i}.port" 5222${i}
hocon -f $HOCON_CONFIG set "jicofo.xmpp.visitors.v${i}.domain" "auth.meet.jitsi"
hocon -f $HOCON_CONFIG set "jicofo.xmpp.visitors.v${i}.password" "${PASS}"
hocon -f $HOCON_CONFIG set "jicofo.xmpp.visitors.v${i}.disable-certificate-verification" true
done
for (( i=1 ; i<=${NUMBER_OF_INSTANCES} ; i++ ));
do
service prosody-v${i} restart
done
service jicofo restart

View File

@ -0,0 +1,46 @@
[Unit]
### see man systemd.unit
Description=Prosody vX (visitor vX) JVB XMPP Server
Documentation=https://prosody.im/doc
Requires=network-online.target
After=network-online.target network.target mariadb.service mysql.service postgresql.service
Before=biboumi.service
[Service]
### see man systemd.service
Type=simple
# Start by executing the main executable
# Note: -F option requires Prosody 0.11.5 or later
ExecStart=/usr/bin/prosody --config /etc/prosody-vX/prosody.cfg.lua -F
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-abnormal
### see man systemd.exec
User=prosody
Group=prosody
UMask=0027
RuntimeDirectory=prosody-vX
ConfigurationDirectory=prosody-vX
StateDirectory=prosody-vX
StateDirectoryMode=0750
LogsDirectory=prosody-vX
WorkingDirectory=~
# Set stdin to /dev/null since Prosody does not need it
StandardInput=null
# Direct stdout/-err to journald for use with log = "*stdout"
StandardOutput=journal
StandardError=inherit
# Allow binding low ports
AmbientCapabilities=CAP_NET_BIND_SERVICE
[Install]
### see man systemd.unit
WantedBy=multi-user.target
# vim: filetype=systemd

View File

@ -0,0 +1,121 @@
---------- Server-wide settings ----------
s2s_ports = { 52691 };
c2s_ports = { 52221 }
http_ports = { 52801 }
https_ports = { 52811 }
daemonize = true;
-- we use a common jid for jicofo
admins = {
'focus@auth.meet.jitsi'
}
-- Enable use of native prosody 0.11 support for epoll over select
network_backend = 'epoll';
network_settings = {
tcp_backlog = 511;
}
modules_enabled = {
'saslauth';
'tls';
'disco';
'posix';
'secure_interfaces';
-- jitsi
'websocket';
'bosh';
's2s_bidi';
's2s_whitelist';
};
s2s_whitelist = {};
external_service_secret = '__turnSecret__';
external_services = {
{ type = 'stun', host = 'jitmeet.example.com', port = 3478 },
{ type = 'turn', host = 'jitmeet.example.com', port = 3478, transport = 'udp', secret = true, ttl = 86400, algorithm = 'turn' },
{ type = 'turns', host = 'jitmeet.example.com', port = 5349, transport = 'tcp', secret = true, ttl = 86400, algorithm = 'turn' }
};
muc_mapper_domain_base = 'vX.meet.jitsi';
-- https://prosody.im/doc/modules/mod_smacks
smacks_max_unacked_stanzas = 5;
smacks_hibernation_time = 60;
-- this is dropped in 0.12
smacks_max_hibernated_sessions = 1;
smacks_max_old_sessions = 1;
unlimited_jids = { 'focus@auth.meet.jitsi' }
limits = {
c2s = {
rate = '512kb/s';
};
}
modules_disabled = {
'offline';
'pubsub';
'register';
};
allow_registration = false;
authentication = 'internal_hashed'
storage = 'internal'
log = {
-- Log files (change 'info' to 'debug' for debug logs):
info = '/var/log/prosody-vX/prosody.log';
error = '/var/log/prosody-vX/prosody.err';
}
consider_websocket_secure = true;
consider_bosh_secure = true;
bosh_max_inactivity = 60;
plugin_paths = { '/usr/share/jitsi-meet/prosody-plugins/' }
----------- Virtual hosts -----------
VirtualHost 'vX.meet.jitsi'
authentication = 'jitsi-anonymous'
ssl = {
key = '/etc/prosody/certs/jitmeet.example.com.key';
certificate = '/etc/prosody/certs/jitmeet.example.com.crt';
}
modules_enabled = {
'bosh';
'ping';
'external_services';
'smacks';
'jiconop';
}
main_muc = 'conference.vX.meet.jitsi';
VirtualHost 'auth.meet.jitsi'
modules_enabled = {
'limits_exception';
'ping';
}
authentication = 'internal_hashed'
Component 'conference.vX.meet.jitsi' 'muc'
storage = 'memory'
muc_room_cache_size = 10000
restrict_room_creation = true
modules_enabled = {
'muc_hide_all';
'muc_domain_mapper';
'muc_meeting_id';
'fmuc';
}
muc_room_default_presence_broadcast = {
visitor = false;
participant = true;
moderator = true;
};
muc_room_locking = false
muc_room_default_public_jids = true

View File

@ -1,16 +0,0 @@
-- validates all certificates, global module
-- Warning: use this only for testing purposes as it will accept all kind of certificates for s2s connections
-- you can use https://modules.prosody.im/mod_s2s_whitelist.html for whitelisting only certain destinations
module:set_global();
function attach(event)
local session = event.session;
session.cert_chain_status = 'valid';
session.cert_identity_status = 'valid';
return true;
end
module:wrap_event('s2s-check-certificate', function (handlers, event_name, event_data)
return attach(event_data);
end);

View File

@ -0,0 +1,19 @@
-- global module
-- validates certificates for all hosts used for s2soutinjection
module:set_global();
local s2s_overrides = module:get_option("s2s_connect_overrides");
function attach(event)
local session = event.session;
if s2s_overrides and s2s_overrides[event.host] then
session.cert_chain_status = 'valid';
session.cert_identity_status = 'valid';
return true;
end
end
module:wrap_event('s2s-check-certificate', function (handlers, event_name, event_data)
return attach(event_data);
end);

View File

@ -7,12 +7,6 @@
--- };
--- Enable in global modules: 's2s_bidi'
--- Make sure 's2s' is not in modules_disabled
--- TODO: Do we need the /etc/hosts changes? We can drop it for https://modules.prosody.im/mod_s2soutinjection.html
--- In /etc/hosts add:
--- vmmain-ip-address focus.domain.com
--- vmmain-ip-address conference.domain.com
--- vmmain-ip-address domain.com
--- Open port 5269 on the provider side and on the firewall of the machine, so the core node can access this visitor one
local jid = require 'util.jid';
local st = require 'util.stanza';

View File

@ -0,0 +1,20 @@
-- Using version https://hg.prosody.im/prosody-modules/file/c1a8ce147885/mod_s2s_whitelist/mod_s2s_whitelist.lua
local st = require "util.stanza";
local whitelist = module:get_option_inherited_set("s2s_whitelist", {});
module:hook("route/remote", function (event)
if not whitelist:contains(event.to_host) then
module:send(st.error_reply(event.stanza, "cancel", "not-allowed", "Communication with this domain is restricted"));
return true;
end
end, 100);
module:hook("s2s-stream-features", function (event)
if not whitelist:contains(event.origin.from_host) then
event.origin:close({
condition = "policy-violation";
text = "Communication with this domain is restricted";
});
end
end, 1000);

View File

@ -0,0 +1,90 @@
-- Using version https://hg.prosody.im/prosody-modules/file/4fb922aa0ace/mod_s2soutinjection/mod_s2soutinjection.lua
local st = require"util.stanza";
local new_outgoing = require"core.s2smanager".new_outgoing;
local bounce_sendq = module:depends"s2s".route_to_new_session.bounce_sendq;
local initialize_filters = require "util.filters".initialize;
local portmanager = require "core.portmanager";
local addclient = require "net.server".addclient;
module:depends("s2s");
local sessions = module:shared("sessions");
local injected = module:get_option("s2s_connect_overrides");
-- The proxy_listener handles connection while still connecting to the proxy,
-- then it hands them over to the normal listener (in mod_s2s)
local proxy_listener = { default_port = nil, default_mode = "*a", default_interface = "*" };
function proxy_listener.onconnect(conn)
local session = sessions[conn];
-- Now the real s2s listener can take over the connection.
local listener = portmanager.get_service("s2s").listener;
local log = session.log;
local filter = initialize_filters(session);
session.version = 1;
session.sends2s = function (t)
log("debug", "sending (s2s over proxy): %s", (t.top_tag and t:top_tag()) or t:match("^[^>]*>?"));
if t.name then
t = filter("stanzas/out", t);
end
if t then
t = filter("bytes/out", tostring(t));
if t then
return conn:write(tostring(t));
end
end
end
session.open_stream = function ()
session.sends2s(st.stanza("stream:stream", {
xmlns='jabber:server', ["xmlns:db"]='jabber:server:dialback',
["xmlns:stream"]='http://etherx.jabber.org/streams',
from=session.from_host, to=session.to_host, version='1.0', ["xml:lang"]='en'}):top_tag());
end
conn.setlistener(conn, listener);
listener.register_outgoing(conn, session);
listener.onconnect(conn);
end
function proxy_listener.register_outgoing(conn, session)
session.direction = "outgoing";
sessions[conn] = session;
end
function proxy_listener.ondisconnect(conn, err)
sessions[conn] = nil;
end
module:hook("route/remote", function(event)
local from_host, to_host, stanza = event.from_host, event.to_host, event.stanza;
local inject = injected and injected[to_host];
if not inject then return end
module:log("debug", "opening a new outgoing connection for this stanza");
local host_session = new_outgoing(from_host, to_host);
-- Store in buffer
host_session.bounce_sendq = bounce_sendq;
host_session.sendq = { {tostring(stanza), stanza.attr.type ~= "error" and stanza.attr.type ~= "result" and st.reply(stanza)} };
host_session.log("debug", "stanza [%s] queued until connection complete", tostring(stanza.name));
local host, port = inject[1] or inject, tonumber(inject[2]) or 5269;
local conn = addclient(host, port, proxy_listener, "*a");
proxy_listener.register_outgoing(conn, host_session);
host_session.conn = conn;
return true;
end, -2);

View File

@ -0,0 +1,20 @@
-- Using version https://hg.prosody.im/prosody-modules/file/6c806a99f802/mod_secure_interfaces/mod_secure_interfaces.lua
local secure_interfaces = module:get_option_set("secure_interfaces", { "127.0.0.1", "::1" });
module:hook("stream-features", function (event)
local session = event.origin;
if session.type ~= "c2s_unauthed" then return; end
local socket = session.conn:socket();
if not socket.getsockname then
module:log("debug", "Unable to determine local address of incoming connection");
return;
end
local localip = socket:getsockname();
if secure_interfaces:contains(localip) then
module:log("debug", "Marking session from %s to %s as secure", session.ip or "[?]", localip);
session.secure = true;
session.conn.starttls = false;
else
module:log("debug", "Not marking session from %s to %s as secure", session.ip or "[?]", localip);
end
end, 2500);

View File

@ -56,29 +56,23 @@ local visitors_nodes = {};
--- Jicofo is connected to the room when sending this error
module:log('info', 'Hook to iq/host');
module:hook('iq/full', function(event)
local session, stanza = event.origin, event.stanza;
local stanza = event.stanza;
if stanza.name ~= 'iq' or stanza.attr.type ~= 'error' or stanza.attr.from ~= focus_component_host then
if stanza.name ~= 'iq' or stanza.attr.type ~= 'result' or stanza.attr.from ~= focus_component_host then
return; -- not IQ from jicofo. Ignore this event.
end
local error = stanza:get_child('error');
if error == nil then
return; -- not Conference IQ error. Ignore.
end
local redirect = error:get_child('redirect', 'urn:ietf:params:xml:ns:xmpp-stanzas');
local redirect_host = error:get_child_text('url', 'http://jitsi.org/jitmeet');
if not redirect or not redirect_host then
local conference = stanza:get_child('conference', 'http://jitsi.org/protocol/focus');
if not conference then
return;
end
-- let's send participants if any from the room to the visitors room
-- TODO fix room name extract, make sure it works wit tenants
local main_room = error:get_child_text('main-room', 'http://jitsi.org/jitmeet');
local main_room = conference.attr.room;
local vnode = conference.attr.vnode;
if not main_room then
if not main_room or not vnode then
return;
end
@ -88,7 +82,7 @@ module:hook('iq/full', function(event)
return; -- room does not exists. Continue with normal flow
end
local conference_service = muc_domain_prefix..'.'..redirect_host;
local conference_service = muc_domain_prefix..'.'..vnode..'.meet.jitsi';
if visitors_nodes[room.jid] and
visitors_nodes[room.jid].nodes and