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:
@@ -352,6 +352,8 @@ body {
|
||||
.chip--online .chip-dot { background: var(--success-dot); }
|
||||
.chip--offline { background: var(--neutral-bg); color: var(--neutral-text); }
|
||||
.chip--offline .chip-dot { background: var(--neutral-dot); }
|
||||
.chip--error { background: var(--error-bg); color: var(--error-text); }
|
||||
.chip--error .chip-dot { background: var(--error-dot); }
|
||||
|
||||
.main {
|
||||
flex: 1;
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ export default function App() {
|
||||
// вкладок (юзеры, ключи) грузят сами вкладки.
|
||||
const startSession = useCallback(async (tok: string) => {
|
||||
const { data: h } = await axios.get('/health', { headers: { Authorization: `Bearer ${tok}` } });
|
||||
setServerInfo({ name: h.server || 'VPN', ip: h.ip || '', peers: h.awg?.peers ?? 0 });
|
||||
setServerInfo({ name: h.server || 'VPN', ip: h.ip || '', peers: h.awg?.peers ?? 0, gen: h.gen || '2' });
|
||||
setStatusText('online:' + (h.server || 'ok') + ' · peers: ' + (h.awg?.peers ?? '?'));
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
|
||||
// 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 { type ServerInfo } from '../lib/shared';
|
||||
import { IcoPlus, IcoRefresh } from './icons';
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// 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.
|
||||
// Иконки интерфейса (inline SVG, currentColor). Добавляя вкладку — добавь сюда
|
||||
// её иконку и зарегистрируй имя в ICONS (его указывают в tabs/<name>/metadata.json).
|
||||
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
// 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 axios from 'axios';
|
||||
// Поколение протокола AmneziaWG, на параметрах которого выдан vpn:// ключ.
|
||||
export type AwgGen = '2' | '3.1';
|
||||
export interface User {
|
||||
name: string;
|
||||
ip: string;
|
||||
pub_key: string;
|
||||
vpn_key: string;
|
||||
key_gen: AwgGen;
|
||||
online: boolean;
|
||||
lastHandshake: number;
|
||||
rx?: number;
|
||||
@@ -14,6 +20,7 @@ export interface ServerInfo {
|
||||
name: string;
|
||||
ip: string;
|
||||
peers: number;
|
||||
gen: AwgGen;
|
||||
}
|
||||
export interface ApiKey {
|
||||
id: number;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// 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 { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
/* Стили вкладки «API-ключи». Общая дизайн-система — в App.css. */
|
||||
|
||||
/* Открытый API-ключ (показывается один раз при создании) */
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// 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, useCallback } from 'react';
|
||||
import { apiFetch, copyText, timeAgo, type ApiKey, type PageProps } from '../../lib/shared';
|
||||
import { IcoPlus, IcoTrash, IcoCopy } from '../../components/icons';
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// 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.
|
||||
// Автообнаружение вкладок. Каждая вкладка — самодостаточная папка tabs/<name>/:
|
||||
// index.tsx — компонент + логика
|
||||
// <name>.css — стили вкладки (импортит сам компонент)
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
Vendored
+3
@@ -1 +1,4 @@
|
||||
// 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.
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
Reference in New Issue
Block a user