feat(ipsec): управление IKEv2/IPsec VPN и клиентами из одного окна
Docker images / prepare-release (push) Successful in 15s
Docker images / backend-test (push) Successful in 2m32s
Docker images / frontend-image (push) Successful in 4m19s
Docker images / updater-image (push) Successful in 50s
Docker images / backend-image (push) Successful in 2m40s
Docker images / notify-webhook (push) Skipped
Docker images / publish-release (push) Successful in 8s
Docker images / prepare-release (push) Successful in 15s
Docker images / backend-test (push) Successful in 2m32s
Docker images / frontend-image (push) Successful in 4m19s
Docker images / updater-image (push) Successful in 50s
Docker images / backend-image (push) Successful in 2m40s
Docker images / notify-webhook (push) Skipped
Docker images / publish-release (push) Successful in 8s
- мастер инициализации сервера: CA и серверный сертификаты, peer/profile/proposal, пул, mode-config, policy-template, managed NAT masquerade - клиенты по сертификату (RSA) и PSK: статический IP или из пула, онлайн-статус по active-peers - скачивание .p12 и strongSwan .sswan с инструкцией, перекачка с новой passphrase - история изменений (config_revisions, секция ipsec) и restore только managed-объектов - привязка IPsec-клиентов к пользователям приложения по Common Name - страница /ipsec с KPI и вкладками Клиенты/Сервер/CLI, сайдбар, command palette
This commit is contained in:
1 parent
564aae21f0
commit
7755d77340
35 files changed
+4154
-13
No files matched your search
@@ -1,4 +1,4 @@
|
||||
export type InterfaceType = "ether" | "gre" | "wg" | "other"
|
||||
export type InterfaceType = "ether" | "gre" | "wg" | "ipsec" | "other"
|
||||
|
||||
export function mapRosInterfaceType(raw: string | undefined | null, name?: string): InterfaceType {
|
||||
const t = String(raw ?? "").trim().toLowerCase()
|
||||
@@ -10,6 +10,7 @@ export function mapRosInterfaceType(raw: string | undefined | null, name?: strin
|
||||
const n = String(name ?? "").trim().toLowerCase()
|
||||
if (n.startsWith("gre") || n.includes("gre-tunnel")) return "gre"
|
||||
if (n.startsWith("wg-") || n.startsWith("wireguard")) return "wg"
|
||||
if (n.startsWith("ipsec")) return "ipsec"
|
||||
if (n.startsWith("ether") || n.startsWith("sfp")) return "ether"
|
||||
return "other"
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export function peerDisplayName(opts: {
|
||||
return truncPeerKey(opts.publicKey)
|
||||
}
|
||||
|
||||
/** Ether/GRE — пустой ключ. WG — обязательный public-key. */
|
||||
/** Ether/GRE — пустой ключ. WG — обязательный public-key. IPsec — CN сертификата клиента. */
|
||||
export function normalizeBindingPeer(
|
||||
type: InterfaceType,
|
||||
peerPublicKey: string | undefined,
|
||||
@@ -40,5 +40,11 @@ export function normalizeBindingPeer(
|
||||
}
|
||||
return key
|
||||
}
|
||||
if (type === "ipsec") {
|
||||
if (!key) {
|
||||
throw new PeerBindError("Для IPsec укажите клиента (CN сертификата)", 400)
|
||||
}
|
||||
return key
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
peerDisplayName,
|
||||
} from "../peer-bind.js"
|
||||
import { listWireGuardPeersForCatalog } from "../../../services/wireguard-live.js"
|
||||
import { listIpsecClientsForCatalog } from "../../../services/ipsec-live.js"
|
||||
|
||||
export class UsersServiceError extends Error {
|
||||
constructor(
|
||||
@@ -305,7 +306,10 @@ export async function listInterfaceCatalog(serverId: number): Promise<CatalogInt
|
||||
peersByIface.set(peer.interfaceName, list)
|
||||
}
|
||||
|
||||
return ifaces.map((iface) => {
|
||||
// IKEv2-клиенты — псевдо-интерфейс «ipsec-vpn» с пирами = клиенты (ключ = CN сертификата).
|
||||
const ipsecLive = await listIpsecClientsForCatalog(serverId)
|
||||
|
||||
const entries: CatalogInterface[] = ifaces.map((iface) => {
|
||||
const ifaceBind = bindings.find((b) => b.interfaceName === iface.name && !(b.peerPublicKey ?? ""))
|
||||
const owner = ifaceBind ? usersById.get(ifaceBind.userId) : undefined
|
||||
const base: CatalogInterface = {
|
||||
@@ -335,7 +339,37 @@ export async function listInterfaceCatalog(serverId: number): Promise<CatalogInt
|
||||
}
|
||||
}),
|
||||
}
|
||||
}).sort((a, b) => a.name.localeCompare(b.name))
|
||||
})
|
||||
|
||||
if (ipsecLive.clients.length > 0) {
|
||||
const ifaceBind = bindings.find((b) => b.interfaceName === "ipsec-vpn" && !(b.peerPublicKey ?? ""))
|
||||
const owner = ifaceBind ? usersById.get(ifaceBind.userId) : undefined
|
||||
entries.push({
|
||||
name: "ipsec-vpn",
|
||||
type: "ipsec",
|
||||
running: true,
|
||||
disabled: false,
|
||||
boundUserId: ifaceBind?.userId ?? null,
|
||||
boundUserLogin: owner?.login ?? null,
|
||||
peersError: ipsecLive.error,
|
||||
peers: ipsecLive.clients.map((c) => {
|
||||
const cn = c.commonName ?? c.name
|
||||
const bind = bindings.find((b) => b.interfaceName === "ipsec-vpn" && b.peerPublicKey === cn)
|
||||
const peerOwner = bind ? usersById.get(bind.userId) : undefined
|
||||
return {
|
||||
publicKey: cn,
|
||||
name: c.name,
|
||||
comment: c.comment ?? "",
|
||||
allowedIps: c.staticIp ? [c.staticIp] : [],
|
||||
latestHandshake: c.activeSince,
|
||||
boundUserId: bind?.userId ?? null,
|
||||
boundUserLogin: peerOwner?.login ?? null,
|
||||
}
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
return entries.sort((a, b) => a.name.localeCompare(b.name))
|
||||
}
|
||||
|
||||
export { parseRawInterfaces, mapRosInterfaceType }
|
||||
Reference in new issue
Block a user