Block BitTorrent on the server

- awg-ctrl: optional nft table awg_torrent (GET/PUT /api/torrent, off by
  default). A connection is judged by its first packets, like Xray's
  sniffing: DHT, UDP trackers, uTP SYN, and with @ih support the plain
  TCP handshake and HTTP announce. A match marks the connection in
  conntrack and drops it whole; without @ih only the UDP part is used
- devices that tried are kept in a 10-minute set and flagged as
  "torrent" in user and device stats; with shaping on they sit in the
  background class while the flag lasts
- metrics: awg_torrent_block_enabled/active, awg_torrent_blocked_total,
  awg_torrent_devices and per-peer awg_peer_torrent
- panel: "Торренты" section in server settings, "торренты" chip on users
  and devices; Save sends only what changed
This commit is contained in:
2026-10-17 23:35:30 +03:00
parent cf6af5ce0a
commit c73da602d6
16 changed files with 575 additions and 55 deletions
+5 -3
View File
@@ -9,6 +9,7 @@ import {
} from '../../lib/shared';
import { IcoPlus, IcoRefresh, IcoQR, IcoTrash, IcoGlobe } from '../../components/icons';
import PriorityChip from '../../components/PriorityChip';
import TorrentChip from '../../components/TorrentChip';
import './users.css';
// Вкладка «Пользователи»: форма создания + таблица (десктоп) / карточки (мобильный)
@@ -29,7 +30,7 @@ export default function UsersPage({ token, showMsg }: PageProps) {
const data = await apiFetch('GET', '/api/users/stats', tok);
setUsers(prev => prev.map(u => {
const s = (data.users as User[] ?? []).find(x => x.name === u.name);
return s ? { ...u, online: s.online, lastHandshake: s.lastHandshake, rx: s.rx, tx: s.tx, priority: s.priority } : u;
return s ? { ...u, online: s.online, lastHandshake: s.lastHandshake, rx: s.rx, tx: s.tx, priority: s.priority, torrent: s.torrent } : u;
}));
} catch { /* silent */ }
}, []);
@@ -187,7 +188,7 @@ export default function UsersPage({ token, showMsg }: PageProps) {
: 'никогда'}
</span>
</td>
<td><PriorityChip priority={u.priority} /></td>
<td><span className="user-chips"><PriorityChip priority={u.priority} torrent={u.torrent} /><TorrentChip torrent={u.torrent} /></span></td>
<td>
{u.name}
{u.key_gen !== serverGen && (
@@ -254,7 +255,8 @@ export default function UsersPage({ token, showMsg }: PageProps) {
<div className="user-card-ip">
<IcoGlobe />
<span className="user-card-ip-text">{u.ip}</span>
<PriorityChip priority={u.priority} />
<PriorityChip priority={u.priority} torrent={u.torrent} />
<TorrentChip torrent={u.torrent} />
</div>
<div className="user-card-metrics">
<div className="metric-block">
+4
View File
@@ -47,6 +47,7 @@ LICENSE file in the root directory of this source tree.
}
.user-card-ip {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
color: var(--neutral-text);
@@ -78,3 +79,6 @@ LICENSE file in the root directory of this source tree.
/* .tip-wrap на мобильной растягивается на всю ширину — метке это не нужно. */
.user-gen { width: auto; }
}
/* Чипы приоритета и торрентов в одной ячейке */
.user-chips { display: inline-flex; flex-wrap: wrap; gap: 6px; }