- Go 69.9%
- Python 28.2%
- Makefile 1.3%
- Shell 0.6%
| cmd/tribes | ||
| internal | ||
| ipfs | ||
| nntp | ||
| packaging/debian | ||
| release | ||
| smoketest | ||
| .gitignore | ||
| go.mod | ||
| go.sum | ||
| Makefile | ||
| README.md | ||
| README_it.md | ||
| TODO.md | ||
| tribes.yaml.example | ||
Tribes
Tribes is a federated NNTP server. It exposes local newsgroups to standard Usenet clients and exchanges articles and group catalogs with other instances through IPFS, IPNS, libp2p, and GossipSub. It does not require Kubo or an external IPFS daemon.
Remote groups do not need to be listed in the configuration. Instances discover them through PubSub, remember them in remote_groups_root/registry.json, and expose them to clients through LIST, GROUP, and NEWGROUPS.
Italian documentation is available in README_it.md.
Installing a prebuilt executable
Prebuilt executables are published as downloadable assets on the Forgejo Releases page. Copies and checksums are also kept in the release directory:
tribes-linux-amd64.ziptribes-freebsd-amd64.ziptribes-windows-amd64.ziptribes_1.0.0~rc7_amd64.debSHA256SUMS
Always verify checksums before use:
cd release
sha256sum -c SHA256SUMS
Each ZIP contains the platform executable and its own SHA256SUMS. On Linux or FreeBSD:
unzip tribes-linux-amd64.zip
sha256sum -c SHA256SUMS
chmod +x tribes-linux-amd64
sudo install -m 0755 tribes-linux-amd64 /usr/local/bin/tribes
On Windows, extract tribes-windows-amd64.zip, verify the included checksum, and copy tribes-windows-amd64.exe to a directory in %PATH% or run it directly from PowerShell.
On Debian or Ubuntu, install the package directly:
sudo apt install ./tribes_1.0.0~rc7_amd64.deb
systemctl status tribes
The package creates an unprivileged tribes system user, installs /etc/tribes.yml, enables and starts the service on 127.0.0.1:119, stores all state below /var/local/tribes, and rotates /var/log/tribes.txt daily. No local groups are configured by the package; uncomment local_groups in /etc/tribes.yml to publish your own groups, then run sudo systemctl restart tribes.
Building from source
Go 1.25 or later and GNU Make are required:
git clone https://git.keinpfusch.net/loweel/tribes.git
cd tribes
make tribes
The local binary is created as ./tribes. To regenerate all amd64 release artifacts:
make release
Initial configuration
Copy tribes.yaml.example and adjust paths, ports, and local groups:
cp tribes.yaml.example tribes.yaml
tribes -c ./tribes.yaml
Minimal example for a node on a local network:
nntp_addr: "127.0.0.1:119"
nntp_port: 119
max_article_bytes: 10485760
logging:
level: "info"
format: "json"
add_source: false
spool_root: "/var/lib/tribes"
local_groups_root: "/var/lib/tribes/local"
remote_groups_root: "/var/cache/tribes/remote"
ipfs:
mode: "embedded"
datastore_path: "/var/lib/tribes/ipfs"
listen_addrs:
- "/ip4/0.0.0.0/tcp/4001"
bootstrap_peers:
- "/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN"
- "/dnsaddr/bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa"
- "/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb"
- "/dnsaddr/bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt"
- "/dnsaddr/va1.bootstrap.libp2p.io/p2p/12D3KooWKnDdG3iXw9eTFijk3EWSunZcFi54Zka4wmtqtt6rPxc8"
enable_mdns: true
enable_autonat: false
enable_relay: false
discovery:
enabled: true
topic: "tribes/groups/v1"
request_interval: 30s
response_window: 5s
max_responses: 32
max_groups_per_peer: 256
max_total_groups: 4096
cleanup:
local_interval: 24h
remote_interval: 6h
local_groups:
- name: "example.general"
title: "Example General"
NNTP port
nntp_addr defines the listen address and port. When greater than zero, nntp_port overrides only the port contained in nntp_addr:
nntp_addr: "0.0.0.0:119"
nntp_port: 8119
The node will listen on 0.0.0.0:8119. The --nntp-addr command-line flag takes precedence over the configuration file:
tribes -c tribes.yaml --nntp-addr 127.0.0.1:1119
Ports below 1024 may require administrative privileges. A non-privileged port such as 8119 is usually easier to operate.
Networking and discovery
ipfs.listen_addrs configures libp2p, not NNTP. With enable_mdns: true, nodes on the same LAN can find each other automatically. To connect separate networks, add at least one known address to bootstrap_peers:
bootstrap_peers:
- "/dns4/peer.example.net/tcp/4001/p2p/12D3KooW..."
After connecting, GossipSub exchanges requests and announcements for locally known groups. Discovered IPNS sources are saved in the registry; Tribes creates/updates the matching local group folder and imports missing article files by IPFS CID. There is no manual remote_groups list to maintain.
IPNS naming and node keys are managed automatically. The persistent libp2p identity is created on first startup inside ipfs.datastore_path; local group names are used as stable IPNS identifiers. Operators never need to generate or configure an IPNS key.
Storage
local_groups_root: articles and metadata for groups published by this node.remote_groups_root: registry and reconstructable metadata for discovered IPNS sources.ipfs.datastore_path: persistent blockstore and IPNS records.spool_root/objects: local article-object cache.
The cleanup.local_interval and cleanup.remote_interval jobs remove reconstructable article files while retaining indexes and IPFS blocks. A value of 0 disables the corresponding job. If a client requests a removed article again, Tribes retrieves it by CID.
Known IPNS sources are refreshed in the background according to sync.interval (five minutes by default). The job resolves each source's IPNS head, imports missing article files into the local group folder by IPFS CID, and updates metadata.
Cleanup requires ipfs.mode: embedded. Offline mode is intended for local development and tests.
Article size limit
max_article_bytes limits every article received through POST, IHAVE, or TAKETHIS. The default is 10 MiB. Oversized articles are rejected without terminating the NNTP session.
Logging
Logging is structured and can be emitted as JSON or text:
logging:
level: "debug" # debug, info, warn, error
format: "json" # json, text
add_source: false
debug: NNTP commands and responses, sessions, peer connections, announcements, and PubSub messages sent or received.info: node startup, connected peers, learned groups, stored articles, and cleanup jobs.warn: conflicts, invalid PubSub input, and rejected articles.error: operational failures and fatal shutdowns.
Article bodies and AUTHINFO secrets are never written to logs.
Using an NNTP client
Configure the client with the host and port from nntp_addr/nntp_port. A quick Python check:
import nntplib
with nntplib.NNTP("127.0.0.1", 8119) as client:
print(client.list()[1])
print(client.group("example.general"))
Posting a test article:
import io
import nntplib
article = b"""From: user@example.net
Newsgroups: example.general
Subject: First message
Hello from Tribes.
"""
with nntplib.NNTP("127.0.0.1", 8119) as client:
client.post(io.BytesIO(article))
On the first post, Tribes updates the group index, publishes the new head through IPNS, and announces it to other nodes.
Running tests
Quick Go checks:
go test ./...
go test -race ./...
Complete pipeline, including security checks, vulnerability scanning, Docker, and federation smoke tests:
make smoke
Operational notes
- Do not expose NNTP to an untrusted network without an external access-control layer.
- Make the configured libp2p port reachable when connecting nodes across separate networks.
- Preserve the IPFS blockstore: cleaned spool files depend on their CIDs for reconstruction.
- Back up the keys and IPNS storage for locally hosted groups.