Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ Improvements chosen after studying similar projects (ssh-chat, devzat, SoftRoom,
- [x] **Hardening after a security review**: modern SSH algorithms only, invisible and tag characters stripped (with a fuzz test), global TCP cap, fresh ban key when no ban is active
- [x] **Renamed to VIVANT**: old servers migrate by running the installer
- [x] **Offline one-click install**: one self-extracting file, `--to root@SERVER` deploys straight from your computer; plus Windows / macOS binaries for testing and LANs
- [x] **Nobody is kicked** (v0.11.5): quiet sessions and locked phones stay connected and are back in their room on unlock; a slot is freed only when the peer is really gone (about 15 minutes without a network). Upgrading removes the old "disconnect after 1 hour without posting" default

## Documentation

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ curl -fsSL https://raw.githubusercontent.com/Freecode100Year/vivant/main/install
- [x] **安全审计后的加固**:只用现代 SSH 算法、剔除隐形字符和标签字符(附模糊测试)、TCP 总连接上限、没有生效封禁时自动换封禁密钥
- [x] **改名 VIVANT**:旧服务器运行一键安装命令自动迁移
- [x] **离线一键安装**:一个自解压文件,`--to root@服务器` 从自己电脑直接部署;另附 Windows / macOS 程序供测试和局域网使用
- [x] **不踢人下线**(v0.11.5):不发言挂机、手机锁屏都不会被断开,解锁后还在原来的房间;只有对方真正掉线(网络消失约 15 分钟)才释放名额。旧服务器升级时自动去掉原来"1 小时不发言断开"的默认设置

## 文档

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.11.4
0.11.5
2 changes: 1 addition & 1 deletion docs/agents.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ echo '{"text":"deploy finished ✅"}' | ssh -T -p 2222 bot@HOST

## Good to know

- **Staying resident:** bot-mode sessions are not disconnected for being quiet (`idle_kick` in `hello` is 0; the operator can set a limit with `-bot-idle-timeout`). The server sends an SSH keepalive every 30 seconds, so dead connections are cleaned up within about 2 minutes.
- **Staying resident:** bot-mode sessions are not disconnected for being quiet (`idle_kick` in `hello` is 0; the operator can set a limit with `-bot-idle-timeout`). The server sends an SSH keepalive every 30 seconds; an unanswered one does not end the session, and a connection whose peer is really gone is closed by the kernel after about 15 minutes.
- **Reconnecting:** after `disconnect` or when the connection ends, reconnect, backing off (with some randomness, so not every bot rushes in after a server restart) if it keeps failing. You start with a new random nick; send `/nick` again: a nickname you chose stays reserved for your address for 10 minutes after a disconnect, so nobody else can take it (`-nick-hold`). If it says the nick is taken, your previous connection probably hasn't been cleaned up yet — try again in 30 seconds.
- **De-duplicating:** joining a room replays its history; recognise messages you've already seen by `id` plus `time` (ids start over when the server restarts).
- **Rate limits:** the same as everyone's — burst 5, then 1 per second; the same text three times in a row is blocked. `{"text":...}` is queued for you; plain text lines are not.
Expand Down
2 changes: 1 addition & 1 deletion docs/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ echo '{"text":"部署完成 ✅"}' | ssh -T -p 2222 bot@HOST

## 注意事项

- **常驻:** bot 模式的会话默认不会因为不发言被断开(`hello` 里 `idle_kick` 为 0;部署者可以用 `-bot-idle-timeout` 设上限)。服务器每 30 秒发一次 SSH 保活,掉线的连接约 2 分钟内会被清理。
- **常驻:** bot 模式的会话默认不会因为不发言被断开(`hello` 里 `idle_kick` 为 0;部署者可以用 `-bot-idle-timeout` 设上限)。服务器每 30 秒发一次 SSH 保活;没回应不会断开,真正掉线(对方网络消失)的连接约 15 分钟后由系统清理。
- **重连:** 收到 `disconnect` 或连接结束后请自动重连,失败时逐渐加长等待时间(加点随机,免得服务器重启后所有 bot 同时涌入)。重连后先是新的随机名,再发一次 `/nick`:自己选的昵称在断线后 10 分钟内为同一地址保留,别人拿不走(`-nick-hold`)。如果提示已被占用,多半是上一个连接还没被清理,过 30 秒再试。
- **去重:** 重连进房间会回放历史,用 `id` 加 `time` 识别已经见过的消息(服务器重启后 `id` 从头编号)。
- **限流:** 和所有人一样,突发 5 条、每秒 1 条,同一句话连发 3 次会被拦。`{"text":...}` 会自动排队;直接发的纯文本行不会排队。
Expand Down
2 changes: 1 addition & 1 deletion docs/deploy.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ Config file `/etc/vivant.env`; after editing run `systemctl restart vivant`:
| `-max-clients` / `-max-per-ip` / `-max-tcp-per-ip` / `-conn-per-min` | auto (`VIVANT_MAX_CLIENTS`) / 3 / 8 / 10 | connection limits; the installer works out the most people online for the machine and writes it to `VIVANT_MAX_CLIENTS`, e.g. 512 MB → 900, 1 GB → 2200, 2 GB 2 cores → 4900, 4 GB 2 cores → 5000, 8 GB 4 cores → 10000 |
| `-read-rate` | 32768 | bytes per second a connection may send before it's slowed down, not dropped (burst 8×; 0 = off). Typing and pasting never get near it |
| `-msg-rate` / `-msg-burst` / `-max-len` | 1/s / 5 / 500 | posting limits |
| `-idle-timeout` | 1h | disconnect after this long without posting (0 = never) |
| `-idle-timeout` | 0 | disconnect after this long without posting (0 = never; the default since v0.11.5) |
| `-bot-idle-timeout` | 0 | the same for bot mode, i.e. resident agents (0 = never) |
| `-nick-hold` | 10m | how long a chosen nickname stays reserved for its address after a disconnect |

Expand Down
2 changes: 1 addition & 1 deletion docs/deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ sudo vivant-ctl status # 输出里的 Host key: SHA256:…
| `-max-clients` / `-max-per-ip` / `-max-tcp-per-ip` / `-conn-per-min` | 自动(`VIVANT_MAX_CLIENTS`)/ 3 / 8 / 10 | 连接限制;安装脚本按机器算出最多在线人数写进 `VIVANT_MAX_CLIENTS`,参考:512 MB → 900,1 GB → 2200,2 GB 2 核 → 4900,4 GB 2 核 → 5000,8 GB 4 核 → 10000 |
| `-read-rate` | 32768 | 每个连接每秒最多发来多少字节,超出会被放慢而不是断开(突发 8 倍;0 = 不限)。正常打字和粘贴远远用不到 |
| `-msg-rate` / `-msg-burst` / `-max-len` | 1/s / 5 / 500 | 发言限制 |
| `-idle-timeout` | 1h | 不发言多久后断开(0 = 不断开) |
| `-idle-timeout` | 0 | 不发言多久后断开(0 = 不断开;v0.11.5 起默认不断开) |
| `-bot-idle-timeout` | 0 | bot 模式(常驻智能体)不发言多久后断开(0 = 不断开) |
| `-nick-hold` | 10m | 断线后自选昵称为原地址保留多久 |

Expand Down
2 changes: 1 addition & 1 deletion docs/development.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ To release: change the `VERSION` file (e.g. `1.0.1`) and push to main, or push a

```
src/main.rs flags, health endpoint, graceful shutdown
src/flags.rs flag parsing with Go's flag syntax (-addr :2222, -idle-timeout 1h)
src/flags.rs flag parsing with Go's flag syntax (-addr :2222, -idle-timeout 30m)
install.sh one-line install / upgrade / uninstall (also vivant-ctl)
src/chat/ rooms, identities, private messages, history, rate limits, bans, reports (no SSH)
src/ui/term.rs full interface: small terminal renderer
Expand Down
2 changes: 1 addition & 1 deletion docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ make bench # 压测

```
src/main.rs 参数、健康检查、优雅退出
src/flags.rs 与 Go flag 包相同语法的参数解析(-addr :2222、-idle-timeout 1h)
src/flags.rs 与 Go flag 包相同语法的参数解析(-addr :2222、-idle-timeout 30m)
install.sh 一键安装 / 升级 / 卸载(也是 vivant-ctl)
src/chat/ 房间、身份、私聊、历史、限流、封禁、举报(与 SSH 无关)
src/ui/term.rs 完整界面:轻量终端渲染器
Expand Down
4 changes: 2 additions & 2 deletions docs/security.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
- **Messages never touch disk.** Each room keeps the last 500 messages, for at most 24 hours, in memory (private rooms at most 100), and a restart wipes them; private rooms vanish when empty; private messages are not stored at all.
- **Terminal injection protection.** Control characters in nicknames and messages are stripped — and with them every ANSI/OSC/DCS escape sequence, including OSC 52 (clipboard access) and `ESC [6n` (cursor position report) — as are Unicode bidi overrides, invisible characters such as zero-width spaces, and Unicode "tag" characters that can hide a line of invisible text. Zero-width joiners needed by emoji sequences, Persian and Indic scripts are kept. A test walks every code point in the control, format and tag ranges and checks none survives (`cargo test`, run by CI every time).
- **Modern SSH algorithms only.** Key exchange is limited to mlkem768x25519 (post-quantum hybrid), curve25519 and ECDH; SHA-1 and finite-field Diffie-Hellman are gone (the latter costs 2048–4096-bit modular exponentiation before the user even authenticates, the easiest way to burn server CPU). hmac-sha1 is no longer accepted. Every SSH client from the last decade (OpenSSH 6.5+, PuTTY 0.68+, Termius, iSH) supports these.
- **Abuse protection.** Per address: at most 3 sessions, 8 TCP connections and 10 new connections a minute; each connection may send 32 KB/s before it is slowed down (so junk floods can't burn CPU); a message token bucket (burst 5, 1 per second); repeated messages blocked; automatic mute for repeated flooding; disconnect after 1 hour without posting.
- **Handshake timeout and a global connection cap.** Connections that haven't entered the chat within 30 seconds (port scanners, stalled handshakes) are closed instead of holding a slot until the 1-hour idle timeout. The total number of TCP connections is capped too (twice the session limit plus 500), so a flood from many different addresses can't exhaust file descriptors.
- **Abuse protection.** Per address: at most 3 sessions, 8 TCP connections and 10 new connections a minute; each connection may send 32 KB/s before it is slowed down (so junk floods can't burn CPU); a message token bucket (burst 5, 1 per second); repeated messages blocked; automatic mute for repeated flooding. Nobody is disconnected for being quiet or for a locked phone (`-idle-timeout` sets a limit, default 0); when a peer is really gone the kernel closes the connection after about 15 minutes and frees the slot.
- **Handshake timeout and a global connection cap.** Connections that haven't entered the chat within 30 seconds (port scanners, stalled handshakes) are closed instead of holding a slot. The total number of TCP connections is capped too (twice the session limit plus 500), so a flood from many different addresses can't exhaust file descriptors.
- **Design rule: passphrases are asked in the chat, never at SSH authentication.** If a passphrase-protected private instance is ever added, the connection will be accepted with `none` first and the passphrase typed in the chat. The reason: once an SSH server stops accepting `none`, clients start offering every public key on the machine, and the user's identity is exposed. ssh-chat's server password and sshoosh's invite tokens are both asked at authentication; this project doesn't do that.
- **Tor onion service (optional).** On a normal connection the VPS and the cloud provider still see the source IP; that can't be removed at the application level. Install with `--tor` and the chat is also published at a `.onion` address: for users arriving through Tor the server only sees "from Tor", never their real IP. The cost: those users can't be banned by address (`/ban` kicks instead), and they share one set of more generous limits.

Expand Down
4 changes: 2 additions & 2 deletions docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
- **消息不落盘。** 每个房间只在内存保留最近 500 条、最长 24 小时(私人房间最多 100 条),服务器重启即清空;私人房间清空即销毁;私聊不保存。
- **终端注入防护。** 昵称和消息里的控制字符(因此所有 ANSI/OSC/DCS 转义序列,包括读写剪贴板的 OSC 52、让终端回报光标位置的 `ESC [6n`)、Unicode 双向覆盖字符、零宽空格之类的隐形字符、可以藏一段看不见文字的 Unicode “标签”字符一律剔除;表情组合和波斯语、印度诸文字需要的零宽连接符保留。有测试遍历控制字符、格式字符和标签字符所在的全部码位,确认没有一个能留下来(`cargo test`,CI 每次都跑)。
- **只用现代 SSH 算法。** 密钥交换只接受 mlkem768x25519(抗量子混合)、curve25519 和 ECDH;去掉了 SHA-1 和有限域 Diffie-Hellman(后者在用户认证前就要做 2048–4096 位模幂运算,最容易被用来消耗服务器 CPU)。MAC 不再接受 hmac-sha1。近十年的 SSH 客户端(OpenSSH 6.5+、PuTTY 0.68+、Termius、iSH)都支持。
- **防滥用。** 每个地址最多 3 个会话、8 个 TCP 连接、每分钟 10 次新连接;每个连接每秒最多收 32 KB,超出的数据会被放慢(防止有人灌垃圾数据消耗 CPU);消息令牌桶(突发 5 条、每秒 1 条);重复消息拦截;频繁超限自动禁言;1 小时不发言自动断开。
- **握手超时与总连接上限。** 连上来 30 秒内没有进入聊天的连接(端口扫描器、卡住的握手)会被断开,不会占着连接名额等到 1 小时空闲超时。另外全服务器的 TCP 连接总数有上限(会话上限的 2 倍再加 500),来自大量不同地址的连接洪水也耗不尽文件描述符。
- **防滥用。** 每个地址最多 3 个会话、8 个 TCP 连接、每分钟 10 次新连接;每个连接每秒最多收 32 KB,超出的数据会被放慢(防止有人灌垃圾数据消耗 CPU);消息令牌桶(突发 5 条、每秒 1 条);重复消息拦截;频繁超限自动禁言。不发言挂机、手机锁屏都不会被断开(`-idle-timeout` 可设上限,默认 0);对方真正掉线时,系统约 15 分钟后关闭连接、释放名额。
- **握手超时与总连接上限。** 连上来 30 秒内没有进入聊天的连接(端口扫描器、卡住的握手)会被断开,不会一直占着连接名额。另外全服务器的 TCP 连接总数有上限(会话上限的 2 倍再加 500),来自大量不同地址的连接洪水也耗不尽文件描述符。
- **设计原则:口令在聊天界面里问,不在 SSH 认证层问。** 如果将来要做需要口令的私有实例,会先用 `none` 认证放行连接,再在聊天界面里输入口令。原因是:SSH 服务器一旦不放行 `none` 认证,客户端就会先把本机所有公钥逐个出示给服务器,用户的身份就暴露了。ssh-chat 的整站口令和 sshoosh 的邀请令牌都在认证层索取,本项目不这样做。
- **Tor 洋葱服务(可选)。** 普通连接下,VPS 和云服务商仍然能看到来源 IP,这在应用层无法消除。安装时加 `--tor`,聊天室会同时发布为 `.onion` 地址:通过 Tor 进来的用户,服务器只能看到“来自 Tor”,看不到真实 IP。代价是这些用户无法按地址封禁(`/ban` 会改为踢出),他们共用一组更宽松的限流额度。

Expand Down
9 changes: 8 additions & 1 deletion install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -475,13 +475,20 @@ GOMEMLIMIT=$(mem_limit)
# 其他参数 · Any other flags.
# 聊天记录默认只在内存保留 24 小时、每房间 500 条(-history / -history-ttl 可改)。
# Chat history: memory only, 24 hours, 500 per room by default (-history / -history-ttl).
VIVANT_ARGS=-max-per-ip 3 -idle-timeout 1h
VIVANT_ARGS=-max-per-ip 3
EOF
chmod 0644 "$ENV_FILE"
ok "已创建 Created $ENV_FILE"
elif [ -n "$PORT" ]; then
set_env VIVANT_ADDR ":$PORT"
fi
# Upgrades: the old default of disconnecting people after 1 hour without
# posting is dropped (v0.11.5: nobody is kicked for being quiet or for a
# locked phone). A different value someone chose themselves is left alone.
if grep -q '^VIVANT_ARGS=.*-idle-timeout 1h\b' "$ENV_FILE" 2>/dev/null; then
sed -i '/^VIVANT_ARGS=/s/ *-idle-timeout 1h\b//' "$ENV_FILE"
ok "不再断开不发言的人 · Quiet sessions are no longer disconnected"
fi
# Upgrades: the old built-in 1-hour history setting gives way to the new
# 24-hour default. Values someone chose themselves are left alone.
if grep -q '^VIVANT_ARGS=.*-history 50 -history-ttl 1h' "$ENV_FILE" 2>/dev/null; then
Expand Down
4 changes: 2 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,8 @@ fn main() {
fs.int("max-len", 500, "单条消息最大字数 · maximum message length in characters");
fs.duration(
"idle-timeout",
Duration::from_secs(3600),
"多久不发言就断开(0 = 不断开)· disconnect sessions silent for this long (0 = never)",
Duration::ZERO,
"多久不发言就断开(默认 0 = 不断开,挂机也不踢)· disconnect sessions silent for this long (default 0 = never)",
);
fs.duration(
"bot-idle-timeout",
Expand Down
15 changes: 10 additions & 5 deletions src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,17 @@ impl Server {
auth_rejection_time_initial: Some(Duration::ZERO),
keys: vec![host_key],
inactivity_timeout: (!opt.idle_timeout.is_zero()).then_some(opt.idle_timeout),
// Detects dead connections (a laptop that went to sleep, a NAT that
// forgot the mapping) within about two minutes, freeing their
// slots, and keeps idle long-lived sessions — resident agents —
// from being dropped by middleboxes.
// A keepalive every 30 s keeps NAT mappings and middleboxes from
// dropping quiet sessions. Unanswered keepalives never end a session
// (keepalive_max 0): a phone app that the OS suspends on lock screen
// cannot answer them, but the phone's TCP stack still acknowledges
// the packets, so the session survives and is there on unlock. A
// peer that is really gone (network lost, laptop powered off) stops
// acknowledging; the kernel then gives up retransmitting the
// keepalives (net.ipv4.tcp_retries2, about 15 minutes) and closes
// the connection, which frees the slot.
keepalive_interval: Some(Duration::from_secs(30)),
keepalive_max: 4,
keepalive_max: 0,
max_auth_attempts: 6,
nodelay: true,
preferred: preferred(),
Expand Down
Loading