mirror of
https://github.com/maeneko/forgetting.git
synced 2026-08-25 16:24:25 +00:00
Add AmneziaWG 3.1 support with key reissue
Protocol generation is now derived from awg1.conf rather than stored: an interface speaks 3.1 when HeaderProtectionKey is set and 2.0 otherwise. The 3.0/3.1-only keys (HeaderProtectionKey, ContentPaddingAddition, the Rekey/Reject/Keepalive timers, MaxHandshakeAttempts, RandomTrailers, DisableCookies) are read from the conf and emitted only when non-empty, so 2.0 output stays byte-identical — verified by diffing both the client .conf and the full vpn:// string against the previous implementation. Changing generation invalidates every issued vpn:// key, so add POST /api/users/reissue. It recovers the client private key from inside the stored blob (the only place it exists), keeping ip/pub_key/psk_key intact, so nothing changes on the wire and users only need to re-import. users gains key_gen and vpn_key_prev, and users.db is snapshotted before the pass. install.sh gates 3.1 on module 3.x and kernel >= 5.5 (header protection needs the chacha library API, absent before 5.5) and falls back to writing a 2.0 conf instead of aborting, so older kernels keep working as before. HeaderProtectionKey is generated with awg genpsk and preserved on KEEP_DATA. The panel marks keys issued on a different generation and offers to reissue them; /api/v1 now returns gen alongside name/ip/vpn_key. Also add MIT license headers across the awg-ui sources.
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
// Copyright (c) 2026 Ivan Vasilev
|
||||
// This source code is licensed under the MIT license found in the
|
||||
// LICENSE file in the root directory of this source tree.
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import QRCode from 'qrcode';
|
||||
import {
|
||||
apiFetch, vpnKeyToConf, downloadFile, copyText, bytes, timeAgo,
|
||||
type User, type PageProps,
|
||||
type User, type PageProps, type AwgGen,
|
||||
} from '../../lib/shared';
|
||||
import { IcoPlus, IcoRefresh, IcoQR, IcoTrash, IcoGlobe } from '../../components/icons';
|
||||
import './users.css';
|
||||
@@ -13,7 +16,11 @@ export default function UsersPage({ token, showMsg }: PageProps) {
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
const [newName, setNewName] = useState('');
|
||||
const [qrModal, setQrModal] = useState<{ name: string; dataUrl: string; vpnKey: string } | null>(null);
|
||||
// Поколение, на котором сейчас работает интерфейс. Ключи, выданные на другом
|
||||
// поколении, уже не подключатся — их надо перевыпустить.
|
||||
const [serverGen, setServerGen] = useState<AwgGen>('2');
|
||||
const statsRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const stale = users.filter(u => u.key_gen !== serverGen);
|
||||
|
||||
const loadStats = useCallback(async (tok: string) => {
|
||||
try {
|
||||
@@ -27,14 +34,34 @@ export default function UsersPage({ token, showMsg }: PageProps) {
|
||||
|
||||
const loadUsers = useCallback(async (tok: string) => {
|
||||
try {
|
||||
const data = await apiFetch('GET', '/api/users', tok);
|
||||
const [data, health] = await Promise.all([
|
||||
apiFetch('GET', '/api/users', tok),
|
||||
apiFetch('GET', '/health', tok),
|
||||
]);
|
||||
setUsers(data.users ?? []);
|
||||
setServerGen(health.gen ?? '2');
|
||||
await loadStats(tok);
|
||||
} catch {
|
||||
showMsg('Ошибка загрузки');
|
||||
}
|
||||
}, [loadStats, showMsg]);
|
||||
|
||||
// Пересобирает vpn:// ключи всех пользователей на текущих параметрах интерфейса.
|
||||
// IP и ключевая пара сохраняются, но клиентам нужно заново импортировать ключ.
|
||||
const reissueKeys = useCallback(async () => {
|
||||
if (!confirm(
|
||||
`Перевыпустить ключи (${stale.length} шт.) на поколении ${serverGen}?\n\n` +
|
||||
'Старые ключи перестанут работать — всем придётся импортировать ключ заново.',
|
||||
)) return;
|
||||
try {
|
||||
const r = await apiFetch('POST', '/api/users/reissue', token);
|
||||
showMsg(`Перевыпущено: ${r.reissued} из ${r.total}`);
|
||||
await loadUsers(token);
|
||||
} catch {
|
||||
showMsg('Не удалось перевыпустить ключи');
|
||||
}
|
||||
}, [stale.length, serverGen, token, loadUsers, showMsg]);
|
||||
|
||||
const createUser = useCallback(async () => {
|
||||
if (!newName) return;
|
||||
const u = await apiFetch('POST', '/api/users', token, { name: newName });
|
||||
@@ -93,6 +120,11 @@ export default function UsersPage({ token, showMsg }: PageProps) {
|
||||
<button className="btn btn--tonal" onClick={() => loadUsers(token)}>
|
||||
<IcoRefresh /> Обновить
|
||||
</button>
|
||||
{stale.length > 0 && (
|
||||
<button className="btn btn--danger" onClick={reissueKeys}>
|
||||
<IcoRefresh /> Перевыпустить ключи ({stale.length})
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -125,7 +157,20 @@ export default function UsersPage({ token, showMsg }: PageProps) {
|
||||
: 'никогда'}
|
||||
</span>
|
||||
</td>
|
||||
<td>{u.name}</td>
|
||||
<td>
|
||||
{u.name}
|
||||
{u.key_gen !== serverGen && (
|
||||
<span
|
||||
className="tip-wrap user-gen"
|
||||
data-tip={`Ключ выдан на AWG ${u.key_gen}, сервер работает на ${serverGen}`}
|
||||
>
|
||||
<span className="chip chip--error">
|
||||
<span className="chip-dot" />
|
||||
AWG {u.key_gen}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="td-mono">{u.ip}</td>
|
||||
<td>{bytes(u.rx)}</td>
|
||||
<td>{bytes(u.tx)}</td>
|
||||
@@ -159,6 +204,12 @@ export default function UsersPage({ token, showMsg }: PageProps) {
|
||||
<div className="user-card-title">
|
||||
<span className="user-card-num">#{i + 1}</span>
|
||||
<span className="user-card-name">{u.name}</span>
|
||||
{u.key_gen !== serverGen && (
|
||||
<span className="chip chip--error">
|
||||
<span className="chip-dot" />
|
||||
AWG {u.key_gen}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className={`chip chip--${u.online ? 'online' : 'offline'}`}>
|
||||
<span className="chip-dot" />
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
Copyright (c) 2026 Ivan Vasilev
|
||||
This source code is licensed under the MIT license found in the
|
||||
LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
/* Стили вкладки «Пользователи». Общая дизайн-система (палитра, btn, chip, field,
|
||||
table-card, toolbar, layout) остаётся в App.css — здесь только уникальное вкладки. */
|
||||
|
||||
@@ -64,7 +69,12 @@
|
||||
.user-card-actions { display: flex; gap: 8px; }
|
||||
.user-card-actions .btn--outline { flex: 1; }
|
||||
|
||||
/* Метка устаревшего ключа рядом с именем: ключ выдан на другом поколении AWG. */
|
||||
.user-gen { margin-left: 8px; vertical-align: middle; }
|
||||
|
||||
/* На мобильной таблица скрывается (.table-card в App.css), карточки показываются. */
|
||||
@media (max-width: 640px) {
|
||||
.user-cards-list { display: flex; }
|
||||
/* .tip-wrap на мобильной растягивается на всю ширину — метке это не нужно. */
|
||||
.user-gen { width: auto; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user