Compare commits

...

10 Commits

15 changed files with 419 additions and 124 deletions

1
.gitignore vendored
View File

@ -0,0 +1 @@
archive

View File

@ -1,11 +1,11 @@
# Ansible Role: strfry
This Ansible Role builds and installs [strfry](https://github.com/hoytech/strfry).
This Ansible Role builds and installs [strfry](https://github.com/hoytech/strfry), and also sets up [strfry-policies](https://gitlab.com/soapbox-pub/strfry-policies). It is intended to be composed with a separate role to handle the web proxy configuration.
Future role improvements:
* The git tasks in this role are not yet idempotent due to handling a submodule.
* The installation process could be made to check if strfry is already running and perform a zero-downtime upgrade.
Tested on:
* Archlinux
* Debian 11
* Ubuntu 22.04
## Requirements
@ -16,31 +16,88 @@ None.
```yaml
strfry_version: beta # git repository branch or release tag
strfry_make_jobs: "{{ ansible_processor_cores }}" # number of CPUs to build with
strfry_skip_config: False
strfry_skip_config: no
strfry_policies_enabled: yes
```
See `defaults/main.yml`
See the role [defaults](defaults/main.yml).
If you are not using the `beta` branch/version, you should override the template with your own by setting `strfry_skip_config` to true and manage the configuration manually.
If you are not using the `beta` branch/version, you should override the template with your own by enabling `strfry_skip_config` and managing the configuration manually.
```yaml
strfry_skip_config: yes
```
For more configuration info, see the relevant upstream [configuration example](https://github.com/hoytech/strfry/blob/beta/strfry.conf) for your branch/version.
## Example Playbook
```yaml
- hosts: all
become: true
- hosts: strfry
roles:
- role: bleetube.strfry
- role: nginxinc.nginx_core.nginx
become: yes
tasks:
- import_tasks: nginx_conf.yml
become: yes
```
A sample [nginx configuration](docs/examples/nginx_conf.yml) is provided.
For a fully functional production example that includes hosting multiple relays, see this [homelab stack](https://github.com/bleetube/satstack).
## Upgrades
Occasionally there are upgrades that require rebuilding the database. You need to `export` before upgrading, and then `import` with the new binary. The role might do the export step, but the import needs to be done manually. Don't rely on the role for the backup. Here's a simple example:
```shell
# Before upgrade
doas -u strfry strfry export > /tmp/backup.jsonl
# After upgrade
systemctl stop strfry
mv strfry-db/data.mdb strfry-db/backup.mdb
cat /tmp/backup.jsonl | doas -u strfry strfry import
doas -u strfry strfry compact strfry-db/compact.mdb
mv strfry-db/compact.mdb strfry-db/data.mdb
systemctl start strfry
```
This is by no means the cleanest way to upgrade, but you get the idea. It's possible to perform the import in a separate process (I think you'd just use a different config file) and then sync the two databases before performing a zero downtime restart.
## Troubleshooting
If `make` fails, try running on a single core:
* If an upgrade fails to build, it could be due to previously built objects. A simple workaround is to delete the strfry source folder `~/src/strfry` and let it try to build from scratch.
* If `make` fails, try building on a single core:
```shell
ansible-playbook playbooks/strfry/main.yml -e 'strfry_make_jobs=1'
```
* Reading your logs:
```shell
systemctl status strfry
journalctl -fu strfry
```
## Maintenance
* You should periodically run `compact` on your strfry database.
```shell
systemctl stop strfry
doas -u strfry strfry compact strfry-db/compact.mdb
mv strfry-db/compact.mdb strfry-db/data.mdb
systemctl start strfry
```
* You can prune events from the database, reducing it's size will reduce the overall compute load on the relay. Make a backup beforehand. Here is a simple example of deleting events that are more than 90 days old:
```shell
doas -u strfry strfry export > /tmp/backup.jsonl
doas -u strfry strfry delete --age=$((90 * 24 * 60 * 60))
```
For a more advanced pruning strategy, you can implement an export/import process to remove certain kinds of events more aggresively. See [bleetube/strfry-prune](https://github.com/bleetube/strfry-prune) for an example.
```shell
ansible-playbook playbooks/strfry/main.yml -e 'strfry_make_jobs=1'
```

View File

@ -9,6 +9,8 @@ strfry_system_user: strfry
strfry_data_path: /var/lib/strfry
strfry_db: "./strfry-db/" # Becomes /var/lib/strfry/strfry-db
strfry_policies_enabled: true
strfry_dbParams:
# Maximum number of threads/processes that can simultaneously have LMDB transactions open (restart required)
maxreaders: 256
@ -16,6 +18,31 @@ strfry_dbParams:
# Size of mmap() to use when loading LMDB (default is 10TB, does *not* correspond to disk-space used) (restart required)
mapsize: 10995116277760
# Disables read-ahead when accessing the LMDB mapping. Reduces IO activity when DB size is larger than RAM. (restart required)
noReadAhead: no
strfry_events:
# Maximum size of normalised JSON, in bytes
maxEventSize: 65536
# Events newer than this will be rejected
rejectEventsNewerThanSeconds: 900
# Events older than this will be rejected
rejectEventsOlderThanSeconds: 94608000
# Ephemeral events older than this will be rejected
rejectEphemeralEventsOlderThanSeconds: 60
# Ephemeral events will be deleted from the DB when older than this
ephemeralEventsLifetimeSeconds: 300
# Maximum number of tags allowed
maxNumTags: 2000
# Maximum size for tag values, in bytes
maxTagValSize: 1024
strfry_relay:
# Interface to listen on. Use 0.0.0.0 to listen on all interfaces (restart required)
bind: "127.0.0.1"
@ -62,30 +89,30 @@ strfry_relay:
writePolicy:
# If non-empty, path to an executable script that implements the writePolicy plugin logic
plugin: ""
# Number of seconds to search backwards for lookback events when starting the writePolicy plugin (0 for no lookback)
lookbackSeconds: 0
plugin: "{{ strfry_data_path }}/strfry-policy.ts"
compression:
# Use permessage-deflate compression if supported by client. Reduces bandwidth, but slight increase in CPU (restart required)
enabled: "true"
enabled: yes
# Maintain a sliding window buffer for each connection. Improves compression, but uses more memory (restart required)
slidingWindow: "true"
slidingWindow: yes
logging:
# Dump all incoming messages
dumpInAll: "false"
dumpInAll: no
# Dump all incoming EVENT messages
dumpInEvents: "false"
dumpInEvents: no
# Dump all incoming REQ/CLOSE messages
dumpInReqs: "false"
dumpInReqs: no
# Log performance metrics for initial REQ database scans
dbScanPerf: "false"
dbScanPerf: no
# Log reason for invalid event rejection? Can be disabled to silence excessive logging
invalidEvents: yes
numThreads:
# Ingester threads: route incoming requests, validate events/sigs (restart required)
@ -97,27 +124,12 @@ strfry_relay:
# reqMonitor threads: Handle filtering of new events (restart required)
reqMonitor: 3
# yesstr threads: Experimental yesstr protocol (restart required)
yesstr: 1
# negentropy threads: Handle negentropy protocol messages (restart required)
negentropy: 2
strfry_events:
# Maximum size of normalised JSON, in bytes
maxEventSize: 65536
negentropy:
# Support negentropy protocol messages
enabled: yes
# Events newer than this will be rejected
rejectEventsNewerThanSeconds: 900
# Events older than this will be rejected
rejectEventsOlderThanSeconds: 94608000
# Ephemeral events older than this will be rejected
rejectEphemeralEventsOlderThanSeconds: 60
# Ephemeral events will be deleted from the DB when older than this
ephemeralEventsLifetimeSeconds: 300
# Maximum number of tags allowed
maxNumTags: 2000
# Maximum size for tag values, in bytes
maxTagValSize: 1024
# Maximum records that sync will process before returning an error
maxSyncEvents: 1000000

View File

@ -0,0 +1,132 @@
---
- name: strfry | Configure nginx
ansible.builtin.import_role:
name: nginx_core.nginx_config
vars:
# afaict, overriding any numeric values in the main nginx config requires replacing the entire dictionary.
# See: https://github.com/nginxinc/ansible-role-nginx-config/issues/352
# The only difference between this and the main nginx config is the worker_rlimit_nofile value and worker_connections.
nginx_config_main_template_enable: true
nginx_config_main_template:
template_file: nginx.conf.j2
deployment_location: /etc/nginx/nginx.conf
backup: false
config: # https://nginx.org/en/docs/ngx_core_module.html
main:
user:
username: nginx
group: nginx
worker_processes: auto
error_log:
file: /var/log/nginx/error.log
level: notice
pid: /var/run/nginx.pid
# worker_rlimit_nofile changes the limit on the maximum number of open files (RLIMIT_NOFILE) for worker processes.
# Used to increase the limit without restarting the main process.
# The recomended value seems to be worker_connections * 2
worker_rlimit_nofile: 12288
events:
worker_connections: 4096
# include: # String or a list of strings
# - /etc/nginx/modules.conf
http: # https://nginx.org/en/docs/http/ngx_http_core_module.html
default_type: application/octet-stream
sendfile: true
server_tokens: false
tcp_nodelay: true
tcp_nopush: true
include:
- /etc/nginx/mime.types
- /etc/nginx/http.conf # These are shared http level configs that nginx_conf refuses to directly configure.
- /etc/nginx/conf.d/*.conf
nginx_config_http_template_enable: true
nginx_config_http_template:
- template_file: http/default.conf.j2
deployment_location: "/etc/nginx/conf.d/strfry_{{ nginx_strfry_domain }}.conf"
backup: false
config:
upstreams:
- name: strfry
servers:
- address: "127.0.0.1:{{ strfry_relay.port|default(7777) }}"
#- address: unix:/var/lib/strfry/strfry.sock
servers:
- core:
listen:
- address: "{{ default_interface_ipv4_address|default(ansible_default_ipv4.address) }}:{{ nginx_strfry_https_port|default(443) }} ssl"
include:
- "/etc/nginx/acme_{{ nginx_strfry_domain }}.conf"
client_max_body_size: 0 # Stream request body to backend
http2:
enable: true
locations:
- location: /
proxy:
pass: http://strfry
http_version: '1.1'
set_header:
- field: Host
value: $http_host
- field: Connection
value: $connection_upgrade
- field: Upgrade
value: $http_upgrade
- field: X-Forwarded-For
value: $proxy_add_x_forwarded_for
connect_timeout: 3m
send_timeout: 3m
read_timeout: 3m
- template_file: http/default.conf.j2
deployment_location: /etc/nginx/http.conf
backup: false
config:
core:
default_type: application/octet-stream
sendfile: true
server_tokens: false
tcp_nodelay: true
tcp_nopush: true
resolver: # required for oscp stapling
address:
- '1.1.1.1'
- '8.8.8.8'
resolver_timeout: 10s
log:
format:
- name: main
format: |
'$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" "$realip_remote_addr"'
gzip: # https://nginx.org/en/docs/http/ngx_http_gzip_module.html
enable: true
comp_level: 3
disable: "msie6"
min_length: 1100
proxied: any
types:
- text/plain
- text/css
- application/x-javascript
- text/xml
- application/xml
vary: true
- template_file: http/default.conf.j2
deployment_location: "/etc/nginx/conf.d/mappings.conf"
backup: false
config:
map:
mappings: # https://nginx.org/en/docs/http/websocket.html
- string: $http_upgrade
variable: $connection_upgrade
content:
- value: default
new_value: upgrade
- value: "''"
new_value: close

21
files/strfry-policy.ts Normal file
View File

@ -0,0 +1,21 @@
#!/bin/sh
//bin/true; exec deno run -A "$0" "$@"
import {
antiDuplicationPolicy,
hellthreadPolicy,
pipeline,
rateLimitPolicy,
readStdin,
writeStdout,
} from 'https://gitlab.com/soapbox-pub/strfry-policies/-/raw/v0.1.0/mod.ts';
for await (const msg of readStdin()) {
const result = await pipeline(msg, [
[hellthreadPolicy, { limit: 10 }],
[antiDuplicationPolicy, { ttl: 60000, minLength: 50 }],
[rateLimitPolicy, { whitelist: ['127.0.0.1'] }],
]);
writeStdout(result);
}

View File

@ -17,15 +17,32 @@
cmd: git submodule update --init
chdir: "{{ ansible_env.HOME }}/src/strfry"
when: git_repository.changed
changed_when: git_repository.changed
- name: Run make setup-golpe
ansible.builtin.command:
cmd: make setup-golpe
chdir: "{{ ansible_env.HOME }}/src/strfry"
when: git_repository.changed
changed_when: git_repository.changed
- name: Build strfry
ansible.builtin.command:
cmd: "make -j{{ strfry_make_jobs|default(1) }}"
chdir: "{{ ansible_env.HOME }}/src/strfry"
when: git_repository.changed
changed_when: git_repository.changed
- name: Check if strfry data path exists
ansible.builtin.stat:
path: "{{ strfry_data_path }}"
register: strfry_data_path_stat
- name: Backup existing strfry-db
ansible.builtin.shell:
cmd: strfry export > backup.jsonl
chdir: "{{ strfry_data_path }}"
become: yes
become_user: "{{ strfry_system_user }}"
when: git_repository.changed and strfry_data_path_stat.stat.exists
changed_when: git_repository.changed

43
tasks/deno.yml Normal file
View File

@ -0,0 +1,43 @@
---
- name: Ensure unzip is installed
ansible.builtin.package:
name: unzip
state: present
- name: Ensure custom facts directory exists
ansible.builtin.file:
mode: 0755
path: /etc/ansible/facts.d
state: directory
- name: 'Detect the latest Deno version'
ansible.builtin.uri:
url: https://api.github.com/repos/denoland/deno/releases/latest
register: deno_latest_release_tag
- name: 'Determine whether or not the latest version of Deno is already installed'
ansible.builtin.set_fact:
install_deno: "{{ (ansible_local.deno is not defined) or \
((ansible_local.deno is defined) and \
(ansible_local['deno']['settings']['version'] != deno_latest_release_tag.json.tag_name | replace('v',''))) }}"
- name: 'Ensure Deno is installed'
unarchive:
src: 'https://github.com/denoland/deno/releases/download/{{ deno_latest_release_tag.json.tag_name }}/deno-x86_64-unknown-linux-gnu.zip'
dest: /usr/local/bin
mode: 0755
remote_src: true
extra_opts:
- -j
when: install_deno
- name: 'Save meta information about the version of Deno that was installed'
community.general.ini_file:
path: /etc/ansible/facts.d/deno.fact
mode: 0644
section: settings
option: version
value: "{{ deno_latest_release_tag.json.tag_name | replace('v','') }}"
backup: true
no_extra_spaces: true
when: install_deno

View File

@ -5,13 +5,12 @@
dest: "{{ strfry_binary_path }}"
mode: 0755
remote_src: true
become: true
notify: restart strfry
- name: Setup strfry service unit
ansible.builtin.template:
src: strfry.service
dest: /etc/systemd/system/strfry.service
become: true
notify: restart strfry
- name: Ensure strfry is enabled on boot
@ -19,7 +18,6 @@
name: strfry
enabled: true
state: started
become: true
- name: Configure strfry
ansible.builtin.template:
@ -28,10 +26,20 @@
owner: "{{ strfry_system_user }}"
group: "{{ strfry_system_group }}"
when: not strfry_skip_config
become: true
tags: config
notify: restart strfry
- name: Install the strfry-policy template
ansible.builtin.copy:
src: strfry-policy.ts
dest: "{{ strfry_relay.writePolicy.plugin }}"
owner: "{{ strfry_system_user }}"
group: "{{ strfry_system_group }}"
force: false # Never overwrite, this is just a starter policy
mode: '0755'
when: strfry_policies_enabled
notify: restart strfry
- name: Ensure the configured database directory exists.
ansible.builtin.file:
path: "{{ strfry_db }}"
@ -39,7 +47,6 @@
owner: "{{ strfry_system_user }}"
group: "{{ strfry_system_group }}"
when: strfry_db.startswith('/')
become: true
- name: Ensure the configured database directory exists.
ansible.builtin.file:
@ -48,4 +55,3 @@
owner: "{{ strfry_system_user }}"
group: "{{ strfry_system_group }}"
when: not strfry_db.startswith('/')
become: true

View File

@ -6,19 +6,30 @@
- "{{ ansible_distribution }}.yml"
- "{{ ansible_os_family }}.yml"
- name: Install compiler dependencies (Debian)
- name: Set up local user account
include_tasks:
file: setup-user.yml
apply:
become: true
- name: Install Deno
include_tasks:
file: deno.yml
apply:
become: true
when: strfry_policies_enabled
- name: Install build dependencies
ansible.builtin.package:
name: "{{ compiler_packages }}"
name: "{{ strfry_build_dependencies }}"
state: present
when: ansible_os_family == 'Debian'
become: true
- name: Set up strfry user
import_tasks: setup-user.yml
- name: Run build tasks as the ansible user (must not be root)
include_tasks: build.yml
#- name: Run build tasks
# import_tasks: build.yml
# tags: build
- name: Install strfry
import_tasks: install.yml
- name: Proceeding with installation
include_tasks:
file: install.yml
apply:
become: true

View File

@ -8,7 +8,6 @@
- /usr/sbin
patterns: nologin
register: nologin_bin
become: true
- name: Create the strfry group
ansible.builtin.group:
@ -16,7 +15,6 @@
state: present
system: true
when: strfry_system_group != "root"
become: true
- name: Create the strfry system user
ansible.builtin.user:
@ -27,7 +25,6 @@
create_home: false
home: "{{ strfry_data_path }}"
when: strfry_system_user != "root"
become: true
- name: Ensure strfry_path exists.
ansible.builtin.file:
@ -36,4 +33,3 @@
group: "{{ strfry_system_group }}"
state: directory
mode: '0750'
become: true

View File

@ -13,6 +13,31 @@ dbParams {
# Size of mmap() to use when loading LMDB (default is 10TB, does *not* correspond to disk-space used) (restart required)
mapsize = {{ strfry_dbParams.mapsize }}
# Disables read-ahead when accessing the LMDB mapping. Reduces IO activity when DB size is larger than RAM. (restart required)
noReadAhead = {{ "true" if strfry_dbParams.noReadAhead else "false" }}
}
events {
# Maximum size of normalised JSON, in bytes
maxEventSize = {{ strfry_events.maxEventSize }}
# Events newer than this will be rejected
rejectEventsNewerThanSeconds = {{ strfry_events.rejectEventsNewerThanSeconds }}
# Events older than this will be rejected
rejectEventsOlderThanSeconds = {{ strfry_events.rejectEventsOlderThanSeconds }}
# Ephemeral events older than this will be rejected
rejectEphemeralEventsOlderThanSeconds = {{ strfry_events.rejectEphemeralEventsOlderThanSeconds }}
# Ephemeral events will be deleted from the DB when older than this
ephemeralEventsLifetimeSeconds = {{ strfry_events.ephemeralEventsLifetimeSeconds }}
# Maximum number of tags allowed
maxNumTags = {{ strfry_events.maxNumTags }}
# Maximum size for tag values, in bytes
maxTagValSize = {{ strfry_events.maxTagValSize }}
}
relay {
@ -49,7 +74,7 @@ relay {
autoPingSeconds = {{ strfry_relay.autoPingSeconds }}
# If TCP keep-alive should be enabled (detect dropped connections to upstream reverse proxy)
enableTcpKeepalive = {{ strfry_relay.enableTcpKeepalive }}
enableTcpKeepalive = {{ "true" if strfry_relay.enableTcpKeepalive else "false" }}
# How much uninterrupted CPU time a REQ query should get during its DB scan
queryTimesliceBudgetMicroseconds = {{ strfry_relay.queryTimesliceBudgetMicroseconds }}
@ -63,31 +88,31 @@ relay {
writePolicy {
# If non-empty, path to an executable script that implements the writePolicy plugin logic
plugin = "{{ strfry_relay.writePolicy.plugin }}"
# Number of seconds to search backwards for lookback events when starting the writePolicy plugin (0 for no lookback)
lookbackSeconds = {{ strfry_relay.writePolicy.lookbackSeconds }}
}
compression {
# Use permessage-deflate compression if supported by client. Reduces bandwidth, but slight increase in CPU (restart required)
enabled = {{ strfry_relay.compression.enabled }}
enabled = {{ "true" if strfry_relay.compression.enabled else "false" }}
# Maintain a sliding window buffer for each connection. Improves compression, but uses more memory (restart required)
slidingWindow = {{ strfry_relay.compression.slidingWindow }}
slidingWindow = {{ "true" if strfry_relay.compression.slidingWindow else "false"}}
}
logging {
# Dump all incoming messages
dumpInAll = {{ strfry_relay.logging.dumpInAll }}
dumpInAll = {{ "true" if strfry_relay.logging.dumpInAll else "false" }}
# Dump all incoming EVENT messages
dumpInEvents = {{ strfry_relay.logging.dumpInEvents }}
dumpInEvents = {{ "true" if strfry_relay.logging.dumpInEvents else "false" }}
# Dump all incoming REQ/CLOSE messages
dumpInReqs = {{ strfry_relay.logging.dumpInReqs }}
dumpInReqs = {{ "true" if strfry_relay.logging.dumpInReqs else "false" }}
# Log performance metrics for initial REQ database scans
dbScanPerf = {{ strfry_relay.logging.dbScanPerf }}
dbScanPerf = {{ "true" if strfry_relay.logging.dbScanPerf else "false" }}
# Log reason for invalid event rejection? Can be disabled to silence excessive logging
invalidEvents = {{ "true" if strfry_relay.logging.invalidEvents else "false" }}
}
numThreads {
@ -100,30 +125,15 @@ relay {
# reqMonitor threads: Handle filtering of new events (restart required)
reqMonitor = {{ strfry_relay.numThreads.reqMonitor }}
# yesstr threads: Experimental yesstr protocol (restart required)
yesstr = {{ strfry_relay.numThreads.yesstr }}
# negentropy threads: Handle negentropy protocol messages (restart required)
negentropy = {{ strfry_relay.numThreads.negentropy }}
}
negentropy {
# Support negentropy protocol messages
enabled = {{ "true" if strfry_relay.negentropy.enabled else "false" }}
# Maximum records that sync will process before returning an error
maxSyncEvents = {{ strfry_relay.negentropy.maxSyncEvents }}
}
}
events {
# Maximum size of normalised JSON, in bytes
maxEventSize = {{ strfry_events.maxEventSize }}
# Events newer than this will be rejected
rejectEventsNewerThanSeconds = {{ strfry_events.rejectEventsNewerThanSeconds }}
# Events older than this will be rejected
rejectEventsOlderThanSeconds = {{ strfry_events.rejectEventsOlderThanSeconds }}
# Ephemeral events older than this will be rejected
rejectEphemeralEventsOlderThanSeconds = {{ strfry_events.rejectEphemeralEventsOlderThanSeconds }}
# Ephemeral events will be deleted from the DB when older than this
ephemeralEventsLifetimeSeconds = {{ strfry_events.ephemeralEventsLifetimeSeconds }}
# Maximum number of tags allowed
maxNumTags = {{ strfry_events.maxNumTags }}
# Maximum size for tag values, in bytes
maxTagValSize = {{ strfry_events.maxTagValSize }}
}

View File

@ -1,11 +1,11 @@
---
compiler_packages:
strfry_build_dependencies:
- base-devel
- flatbuffers
- git
- lmdb
- openssl
- perl-regexp-grammars
- perl-template-toolkit
- perl-yaml
- zlib
- libb2

View File

@ -1,5 +1,5 @@
---
compiler_packages:
strfry_build_dependencies:
- build-essential
- git
- libflatbuffers-dev
@ -11,3 +11,4 @@ compiler_packages:
- libyaml-perl
- libzstd-dev
- zlib1g-dev
- libb2-dev

View File

@ -1,9 +1,11 @@
---
compiler_packages:
strfry_build_dependencies:
- @development-tools
# - flatbuffers # epel?
- git
- lmdb-devel
- openssl-devel
- perl-Template-Toolkit
- perl-YAML
- zlib-devel
- libb2

View File

@ -1,14 +0,0 @@
---
compiler_packages:
- build-essential
- git
- libflatbuffers-dev
- liblmdb-dev
- libregexp-grammars-perl
- libsecp256k1-dev
- libssl-dev
- libtemplate-perl
- libyaml-perl
- libzstd-dev
- zlib1g-dev
- libb2-dev # Ubuntu 22.04