Init commit
This commit is contained in:
commit
f3f831653f
104 files changed
+43827
No files matched your search
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* API mode feature-flag.
|
||||
*
|
||||
* Set NEXT_PUBLIC_API_MODE=live in .env.local to switch from static mock data
|
||||
* to real RouterOS REST API calls.
|
||||
*
|
||||
* Default: "mock"
|
||||
*/
|
||||
|
||||
export const API_MODE = (
|
||||
typeof process !== "undefined"
|
||||
? (process.env.NEXT_PUBLIC_API_MODE ?? "mock")
|
||||
: "mock"
|
||||
) as "mock" | "live"
|
||||
|
||||
export const IS_MOCK = API_MODE !== "live"
|
||||
|
||||
/**
|
||||
* Wrap an async data-fetch so it transparently returns mock data in "mock" mode
|
||||
* or calls the live resolver in "live" mode.
|
||||
*
|
||||
* Example:
|
||||
* export const fetchServers = apiOr(
|
||||
* () => servers, // mock
|
||||
* async (cfg) => apiClient(cfg).getAll() // live
|
||||
* )
|
||||
*/
|
||||
export function apiOr<T, Args extends unknown[]>(
|
||||
mock: (...args: Args) => T | Promise<T>,
|
||||
live: (...args: Args) => T | Promise<T>,
|
||||
): (...args: Args) => Promise<T> {
|
||||
return async (...args: Args): Promise<T> => {
|
||||
if (IS_MOCK) return mock(...args)
|
||||
return live(...args)
|
||||
}
|
||||
}
|
||||
+256
@@ -0,0 +1,256 @@
|
||||
/**
|
||||
* RouterOS 7.x REST API client
|
||||
*
|
||||
* RouterOS 7.1+ exposes a REST API at https://<router>/rest/
|
||||
* All responses are JSON; authentication is HTTP Basic or Bearer token.
|
||||
*
|
||||
* Usage (browser / server-component):
|
||||
* const client = new RouterOsApiClient({ host: "10.0.0.1", port: 443, ... })
|
||||
* const ifaces = await client.getInterfaces()
|
||||
*/
|
||||
|
||||
export interface ApiClientConfig {
|
||||
host: string // "10.0.0.1"
|
||||
port: number // 443 (HTTPS) or 80 (HTTP)
|
||||
tls: boolean // use HTTPS
|
||||
username: string
|
||||
password: string
|
||||
tlsVerify: boolean // verify TLS certificate (false for self-signed)
|
||||
timeout: number // ms, default 8000
|
||||
}
|
||||
|
||||
// ─── RouterOS REST response shapes ───────────────────────────────────────────
|
||||
|
||||
export interface RosSystemIdentity {
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface RosSystemResource {
|
||||
uptime: string
|
||||
version: string
|
||||
"build-time": string
|
||||
"free-memory": string
|
||||
"total-memory": string
|
||||
"cpu": string
|
||||
"cpu-count": string
|
||||
"cpu-load": string
|
||||
"board-name": string
|
||||
"platform": string
|
||||
"architecture-name": string
|
||||
"bad-blocks": string
|
||||
}
|
||||
|
||||
export interface RosInterface {
|
||||
".id": string
|
||||
name: string
|
||||
type: string
|
||||
mtu: string
|
||||
"actual-mtu": string
|
||||
"mac-address": string
|
||||
running: string
|
||||
disabled: string
|
||||
comment?: string
|
||||
}
|
||||
|
||||
export interface RosBgpSession {
|
||||
".id": string
|
||||
name: string
|
||||
"remote.address": string
|
||||
"remote.as": string
|
||||
state: string
|
||||
"uptime": string
|
||||
"prefix-count": string
|
||||
disabled: string
|
||||
}
|
||||
|
||||
export interface RosWireGuardInterface {
|
||||
".id": string
|
||||
name: string
|
||||
"listen-port": string
|
||||
mtu: string
|
||||
"public-key": string
|
||||
running: string
|
||||
disabled: string
|
||||
comment?: string
|
||||
}
|
||||
|
||||
export interface RosWireGuardPeer {
|
||||
".id": string
|
||||
interface: string
|
||||
"public-key": string
|
||||
"endpoint-address"?: string
|
||||
"endpoint-port"?: string
|
||||
"allowed-address": string
|
||||
"last-handshake"?: string
|
||||
"rx": string
|
||||
"tx": string
|
||||
disabled: string
|
||||
comment?: string
|
||||
}
|
||||
|
||||
// ─── Error class ─────────────────────────────────────────────────────────────
|
||||
|
||||
export class RouterOsApiError extends Error {
|
||||
constructor(
|
||||
public readonly status: number,
|
||||
public readonly detail: string,
|
||||
message: string,
|
||||
) {
|
||||
super(message)
|
||||
this.name = "RouterOsApiError"
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Client class ─────────────────────────────────────────────────────────────
|
||||
|
||||
export class RouterOsApiClient {
|
||||
private readonly baseUrl: string
|
||||
private readonly headers: Record<string, string>
|
||||
private readonly timeout: number
|
||||
|
||||
constructor(private readonly cfg: ApiClientConfig) {
|
||||
const scheme = cfg.tls ? "https" : "http"
|
||||
this.baseUrl = `${scheme}://${cfg.host}:${cfg.port}/rest`
|
||||
const creds = btoa(`${cfg.username}:${cfg.password}`)
|
||||
this.headers = {
|
||||
"Authorization": `Basic ${creds}`,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
this.timeout = cfg.timeout ?? 8000
|
||||
}
|
||||
|
||||
// ── low-level fetch ──────────────────────────────────────────────────────
|
||||
|
||||
private async request<T>(
|
||||
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE",
|
||||
path: string,
|
||||
body?: unknown,
|
||||
): Promise<T> {
|
||||
const controller = new AbortController()
|
||||
const timer = setTimeout(() => controller.abort(), this.timeout)
|
||||
|
||||
try {
|
||||
const res = await fetch(`${this.baseUrl}${path}`, {
|
||||
method,
|
||||
headers: this.headers,
|
||||
body: body !== undefined ? JSON.stringify(body) : undefined,
|
||||
signal: controller.signal,
|
||||
})
|
||||
clearTimeout(timer)
|
||||
|
||||
if (!res.ok) {
|
||||
let detail = ""
|
||||
try { detail = (await res.json())?.message ?? "" } catch { /* ignore */ }
|
||||
throw new RouterOsApiError(res.status, detail, `RouterOS API ${method} ${path} → ${res.status}: ${detail}`)
|
||||
}
|
||||
|
||||
if (res.status === 204) return undefined as T
|
||||
return res.json() as Promise<T>
|
||||
} catch (e) {
|
||||
clearTimeout(timer)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
async get<T>(path: string): Promise<T> {
|
||||
return this.request<T>("GET", path)
|
||||
}
|
||||
|
||||
async post<T>(path: string, body: unknown): Promise<T> {
|
||||
return this.request<T>("POST", path, body)
|
||||
}
|
||||
|
||||
/** RouterOS uses PUT for updates on a specific item by /.id */
|
||||
async put<T>(path: string, id: string, body: unknown): Promise<T> {
|
||||
return this.request<T>("PUT", `${path}/${encodeURIComponent(id)}`, body)
|
||||
}
|
||||
|
||||
async patch<T>(path: string, id: string, body: unknown): Promise<T> {
|
||||
return this.request<T>("PATCH", `${path}/${encodeURIComponent(id)}`, body)
|
||||
}
|
||||
|
||||
async delete(path: string, id: string): Promise<void> {
|
||||
return this.request<void>("DELETE", `${path}/${encodeURIComponent(id)}`)
|
||||
}
|
||||
|
||||
// ── high-level helpers ───────────────────────────────────────────────────
|
||||
|
||||
async getSystemIdentity(): Promise<RosSystemIdentity> {
|
||||
return this.get<RosSystemIdentity>("/system/identity")
|
||||
}
|
||||
|
||||
async getSystemResource(): Promise<RosSystemResource> {
|
||||
return this.get<RosSystemResource>("/system/resource")
|
||||
}
|
||||
|
||||
async getInterfaces(): Promise<RosInterface[]> {
|
||||
return this.get<RosInterface[]>("/interface")
|
||||
}
|
||||
|
||||
async getBgpSessions(): Promise<RosBgpSession[]> {
|
||||
return this.get<RosBgpSession[]>("/routing/bgp/session")
|
||||
}
|
||||
|
||||
async getWireGuardInterfaces(): Promise<RosWireGuardInterface[]> {
|
||||
return this.get<RosWireGuardInterface[]>("/interface/wireguard")
|
||||
}
|
||||
|
||||
async getWireGuardPeers(): Promise<RosWireGuardPeer[]> {
|
||||
return this.get<RosWireGuardPeer[]>("/interface/wireguard/peers")
|
||||
}
|
||||
|
||||
/** Add a WireGuard interface */
|
||||
async addWireGuardInterface(params: {
|
||||
name: string
|
||||
listenPort: number
|
||||
mtu?: number
|
||||
comment?: string
|
||||
}): Promise<RosWireGuardInterface> {
|
||||
return this.post<RosWireGuardInterface>("/interface/wireguard", {
|
||||
name: params.name,
|
||||
"listen-port": String(params.listenPort),
|
||||
mtu: String(params.mtu ?? 1420),
|
||||
comment: params.comment ?? "",
|
||||
})
|
||||
}
|
||||
|
||||
/** Add a WireGuard peer */
|
||||
async addWireGuardPeer(params: {
|
||||
iface: string
|
||||
publicKey: string
|
||||
allowedAddresses: string[]
|
||||
endpoint?: string
|
||||
endpointPort?: number
|
||||
persistentKeepalive?: number
|
||||
comment?: string
|
||||
}): Promise<RosWireGuardPeer> {
|
||||
return this.post<RosWireGuardPeer>("/interface/wireguard/peers", {
|
||||
interface: params.iface,
|
||||
"public-key": params.publicKey,
|
||||
"allowed-address": params.allowedAddresses.join(","),
|
||||
...(params.endpoint ? { "endpoint-address": params.endpoint } : {}),
|
||||
...(params.endpointPort ? { "endpoint-port": String(params.endpointPort) } : {}),
|
||||
...(params.persistentKeepalive ? { "persistent-keepalive": String(params.persistentKeepalive) } : {}),
|
||||
comment: params.comment ?? "",
|
||||
})
|
||||
}
|
||||
|
||||
/** Ping test from the router — uses /tool/ping */
|
||||
async ping(address: string, count = 4): Promise<{ "avg-rtt": string; "packet-loss": string }[]> {
|
||||
return this.post("/tool/ping", { address, count: String(count) })
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Factory helper ───────────────────────────────────────────────────────────
|
||||
|
||||
export function createClient(overrides: Partial<ApiClientConfig> & { host: string }): RouterOsApiClient {
|
||||
return new RouterOsApiClient({
|
||||
port: 443,
|
||||
tls: true,
|
||||
username: "admin",
|
||||
password: "",
|
||||
tlsVerify: false,
|
||||
timeout: 8000,
|
||||
...overrides,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
"use client"
|
||||
|
||||
import { createContext, useContext, useEffect, useState, useCallback } from "react"
|
||||
|
||||
// ── types ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
export type DataSourceMode = "mock" | "live"
|
||||
|
||||
interface DataSourceContextValue {
|
||||
mode: DataSourceMode
|
||||
setMode: (m: DataSourceMode) => void
|
||||
backendUrl: string
|
||||
setBackendUrl: (url: string) => void
|
||||
/** undefined = не проверялось, true = OK, false = недоступен */
|
||||
backendStatus: boolean | undefined
|
||||
checkBackend: () => Promise<void>
|
||||
}
|
||||
|
||||
// ── context ───────────────────────────────────────────────────────────────────
|
||||
|
||||
const DataSourceContext = createContext<DataSourceContextValue | null>(null)
|
||||
|
||||
const LS_MODE = "routerlists:data-source"
|
||||
const LS_BACKEND = "routerlists:backend-url"
|
||||
const DEFAULT_URL = "http://localhost:8000"
|
||||
|
||||
export function DataSourceProvider({ children }: { children: React.ReactNode }) {
|
||||
const [mode, setModeState] = useState<DataSourceMode>("mock")
|
||||
const [backendUrl, setBackendUrlState] = useState(DEFAULT_URL)
|
||||
const [backendStatus, setBackendStatus] = useState<boolean | undefined>(undefined)
|
||||
|
||||
// Restore from localStorage on mount
|
||||
useEffect(() => {
|
||||
const storedMode = localStorage.getItem(LS_MODE) as DataSourceMode | null
|
||||
const storedUrl = localStorage.getItem(LS_BACKEND)
|
||||
if (storedMode === "mock" || storedMode === "live") setModeState(storedMode)
|
||||
if (storedUrl) setBackendUrlState(storedUrl)
|
||||
}, [])
|
||||
|
||||
const setMode = useCallback((m: DataSourceMode) => {
|
||||
setModeState(m)
|
||||
localStorage.setItem(LS_MODE, m)
|
||||
}, [])
|
||||
|
||||
const setBackendUrl = useCallback((url: string) => {
|
||||
setBackendUrlState(url)
|
||||
localStorage.setItem(LS_BACKEND, url)
|
||||
}, [])
|
||||
|
||||
const checkBackend = useCallback(async () => {
|
||||
const url = (localStorage.getItem(LS_BACKEND) ?? DEFAULT_URL).replace(/\/$/, "")
|
||||
try {
|
||||
const res = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3000) })
|
||||
setBackendStatus(res.ok)
|
||||
} catch {
|
||||
setBackendStatus(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Auto-check when mode switches to live
|
||||
useEffect(() => {
|
||||
if (mode === "live") checkBackend()
|
||||
}, [mode, checkBackend])
|
||||
|
||||
return (
|
||||
<DataSourceContext.Provider value={{ mode, setMode, backendUrl, setBackendUrl, backendStatus, checkBackend }}>
|
||||
{children}
|
||||
</DataSourceContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useDataSource(): DataSourceContextValue {
|
||||
const ctx = useContext(DataSourceContext)
|
||||
if (!ctx) throw new Error("useDataSource must be used inside <DataSourceProvider>")
|
||||
return ctx
|
||||
}
|
||||
+807
@@ -0,0 +1,807 @@
|
||||
export type ServerStatus = "online" | "offline" | "degraded"
|
||||
export type ServerType = "jump-host" | "exit-node" | "home-router"
|
||||
|
||||
export interface WanUplink {
|
||||
id: string
|
||||
name: string // "WAN1-RT"
|
||||
isp: string // "Rostelecom"
|
||||
iface: string // "ether1"
|
||||
ip: string // external IP
|
||||
maxDl: number // Mbps
|
||||
maxUl: number // Mbps
|
||||
}
|
||||
|
||||
// ─── WireGuard ───────────────────────────────────────────────────────────────
|
||||
|
||||
export interface WireGuardPeer {
|
||||
publicKey: string
|
||||
allowedIps: string[]
|
||||
endpoint?: string // "1.2.3.4:13231"
|
||||
latestHandshake?: string // "2 минуты назад"
|
||||
transferRx?: number // bytes
|
||||
transferTx?: number // bytes
|
||||
persistent?: boolean
|
||||
comment?: string
|
||||
}
|
||||
|
||||
export interface WireGuardInterface {
|
||||
id: string
|
||||
name: string // e.g. "wg-msk-spb"
|
||||
listenPort: number // default 13231
|
||||
mtu: number // 1420 default in ROS 7.x
|
||||
peers: WireGuardPeer[]
|
||||
comment: string
|
||||
enabled: boolean
|
||||
status: "up" | "down"
|
||||
}
|
||||
|
||||
export interface Server {
|
||||
id: string
|
||||
name: string
|
||||
host: string // management IP / API address
|
||||
model: string
|
||||
os: string
|
||||
site: string
|
||||
country: string // ISO 3166-1 alpha-2, e.g. "RU", "DE"
|
||||
asn: string
|
||||
type: ServerType
|
||||
enabled: boolean
|
||||
status: ServerStatus
|
||||
latency: number | null
|
||||
sessions: number
|
||||
comment?: string
|
||||
// RouterOS 7.x extras
|
||||
ipv6Address?: string // global or link-local
|
||||
wireGuardIfaces?: WireGuardInterface[]
|
||||
vrfNames?: string[] // VRF instances on this router
|
||||
rpkiEnabled?: boolean
|
||||
// home-router specific
|
||||
wanUplinks?: WanUplink[]
|
||||
lanSubnet?: string // e.g. "192.168.10.0/24"
|
||||
// snapshot / poll data
|
||||
uptime?: string
|
||||
cpuLoad?: number
|
||||
polledAt?: string
|
||||
}
|
||||
|
||||
export interface FlowspecRule {
|
||||
id: string
|
||||
dstPrefix?: string
|
||||
srcPrefix?: string
|
||||
ipProto?: string
|
||||
dstPort?: string
|
||||
action: "rate-limit" | "discard" | "redirect"
|
||||
rate?: number // Mbps, for rate-limit
|
||||
comment?: string
|
||||
}
|
||||
|
||||
export interface Filter {
|
||||
id: string
|
||||
name: string
|
||||
domains: number
|
||||
ips: number
|
||||
asns: number
|
||||
servers: string[]
|
||||
enabled: boolean
|
||||
updated: string
|
||||
communities: string[]
|
||||
gateway: string
|
||||
// RouterOS 7.x extras
|
||||
largeCommunities?: string[] // RFC 8092 format "AS:VALUE1:VALUE2"
|
||||
rpkiValid?: boolean // filter only RPKI-valid routes
|
||||
flowspecRules?: FlowspecRule[]
|
||||
}
|
||||
|
||||
export interface Domain {
|
||||
id: string
|
||||
domain: string
|
||||
resolvedIp: string
|
||||
asn: string
|
||||
purpose: string
|
||||
filter: string
|
||||
updated: string
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export interface IpRange {
|
||||
id: string
|
||||
cidr: string
|
||||
asn: string
|
||||
country: string
|
||||
purpose: string
|
||||
filter: string
|
||||
updated: string
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export interface Asn {
|
||||
id: string
|
||||
asn: string
|
||||
org: string
|
||||
country: string
|
||||
prefixes: number
|
||||
filter: string
|
||||
updated: string
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export type FirewallAction =
|
||||
| "accept" | "drop" | "reject" | "mark-routing" | "mark-conn"
|
||||
| "masquerade" | "src-nat" | "dst-nat"
|
||||
| "fasttrack-connection" | "nfqueue" | "passthrough" | "return"
|
||||
| "add-src-to-address-list" | "add-dst-to-address-list"
|
||||
|
||||
export interface FirewallRule {
|
||||
id: string
|
||||
chain: string
|
||||
action: FirewallAction | string
|
||||
proto: string
|
||||
src: string
|
||||
dst: string
|
||||
port: string
|
||||
iface: string
|
||||
comment: string
|
||||
enabled: boolean
|
||||
hits: number
|
||||
// RouterOS 7.x extras
|
||||
family?: "ip" | "ip6" | "any"
|
||||
tlsHost?: string // tls-host matcher (SNI)
|
||||
connRateLimit?: string // connection-rate e.g. "100/s"
|
||||
layer7Proto?: string // /ip firewall layer7-protocol
|
||||
nfqueueId?: number
|
||||
log?: boolean
|
||||
logPrefix?: string
|
||||
}
|
||||
|
||||
export interface Backup {
|
||||
id: string
|
||||
server: string
|
||||
filename: string
|
||||
size: string
|
||||
created: string
|
||||
kind: "auto" | "manual"
|
||||
notes: string
|
||||
}
|
||||
|
||||
// ─── VXLAN ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface VxlanTunnel {
|
||||
id: string
|
||||
name: string // "vxlan-overlay-1"
|
||||
vni: number // 0–16777215
|
||||
port: number // source port (default 0 = auto)
|
||||
dstPort: number // destination port (default 8472)
|
||||
serverId: string
|
||||
vtepIp: string // VTEP address on the server
|
||||
remoteVteps: string[] // remote VTEP IPs (unicast-flood)
|
||||
l2mtu: number
|
||||
arpProxy: boolean
|
||||
macLearning: boolean
|
||||
comment: string
|
||||
enabled: boolean
|
||||
status: "up" | "down"
|
||||
}
|
||||
|
||||
// ─── VRF ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface VrfInstance {
|
||||
id: string
|
||||
name: string // VRF name
|
||||
serverId: string
|
||||
interfaces: string[] // list of interfaces in this VRF
|
||||
routingTable: string // associated routing table
|
||||
comment: string
|
||||
}
|
||||
|
||||
// ─── Container (RouterOS 7.4+) ───────────────────────────────────────────────
|
||||
|
||||
export interface RouterContainer {
|
||||
id: string
|
||||
name: string
|
||||
serverId: string
|
||||
image: string // e.g. "nginx:alpine"
|
||||
tag: string
|
||||
status: "running" | "stopped" | "error"
|
||||
envs: { key: string; value: string }[]
|
||||
mounts: { dst: string; src?: string }[]
|
||||
interfaces: string[] // veth interface names
|
||||
cmd?: string
|
||||
startOnBoot: boolean
|
||||
comment: string
|
||||
uptime?: string
|
||||
cpu?: number // CPU % 0-100
|
||||
memMb?: number // MB
|
||||
}
|
||||
|
||||
// ─── Certificate ─────────────────────────────────────────────────────────────
|
||||
|
||||
export type CertUsage = "server" | "client" | "ca" | "crl-sign"
|
||||
export type CertStatus = "valid" | "expired" | "revoked"
|
||||
|
||||
export interface RouterCertificate {
|
||||
id: string
|
||||
name: string
|
||||
serverId: string
|
||||
commonName: string
|
||||
sans: string[]
|
||||
issuedBy: string
|
||||
validFrom: string
|
||||
validUntil: string
|
||||
daysLeft: number
|
||||
keySize: number
|
||||
usage: CertUsage[]
|
||||
trusted: boolean
|
||||
status: CertStatus
|
||||
}
|
||||
|
||||
export interface PingProbe {
|
||||
id: string
|
||||
srcServerId: string // which server sends the ping
|
||||
srcInterface?: string // optional interface name on source router
|
||||
name: string
|
||||
target: string
|
||||
filter: string
|
||||
rtt: number | null
|
||||
loss: number
|
||||
status: "up" | "warn" | "down"
|
||||
series: number[]
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export interface SystemEvent {
|
||||
id: string
|
||||
sev: "destructive" | "warning" | "info"
|
||||
title: string
|
||||
message: string
|
||||
when: string
|
||||
}
|
||||
|
||||
function spark(n: number, base: number, range: number): number[] {
|
||||
let s = base * 1000 + range
|
||||
return Array.from({ length: n }, () => {
|
||||
s = (s * 9301 + 49297) % 233280
|
||||
const r = s / 233280
|
||||
return Math.max(1, Math.round(base + (r - 0.5) * range * 2))
|
||||
})
|
||||
}
|
||||
|
||||
export const servers: Server[] = [
|
||||
{
|
||||
id: "srv1", name: "mt-msk-core-01", host: "10.0.0.1", model: "CCR2004-1G-12S+2XS", os: "7.20.1",
|
||||
site: "MSK", country: "RU", asn: "AS65001", type: "jump-host", enabled: true, status: "online", latency: 12, sessions: 4,
|
||||
ipv6Address: "2a01:4f8:1c1c:a1b2::1",
|
||||
rpkiEnabled: true,
|
||||
vrfNames: ["mgmt", "bypass"],
|
||||
wireGuardIfaces: [
|
||||
{
|
||||
id: "wg1", name: "wg-msk-spb", listenPort: 13231, mtu: 1420, comment: "MSK → SPB overlay", enabled: true, status: "up",
|
||||
peers: [
|
||||
{ publicKey: "SPBPublicKeyBase64AAAAAAAAAAAAAAAAAAAAAA=", allowedIps: ["10.210.0.2/32","192.168.20.0/24"], endpoint: "10.0.1.1:13231", latestHandshake: "2 мин назад", transferRx: 182_400_000, transferTx: 64_800_000, persistent: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "wg2", name: "wg-msk-fra", listenPort: 13232, mtu: 1420, comment: "MSK → FRA overlay", enabled: true, status: "up",
|
||||
peers: [
|
||||
{ publicKey: "FRAPublicKeyBase64AAAAAAAAAAAAAAAAAAAAAA=", allowedIps: ["10.210.0.6/32"], endpoint: "10.0.2.1:13231", latestHandshake: "1 мин назад", transferRx: 421_000_000, transferTx: 210_000_000, persistent: true },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "srv2", name: "mt-spb-edge-01", host: "10.0.1.1", model: "CCR2116-12G-4S+", os: "7.20.1",
|
||||
site: "SPB", country: "RU", asn: "AS65002", type: "exit-node", enabled: true, status: "online", latency: 28, sessions: 6,
|
||||
ipv6Address: "2a01:4f8:1c2c:b2c3::1",
|
||||
rpkiEnabled: true,
|
||||
wireGuardIfaces: [
|
||||
{
|
||||
id: "wg3", name: "wg-spb-msk", listenPort: 13231, mtu: 1420, comment: "SPB → MSK overlay", enabled: true, status: "up",
|
||||
peers: [
|
||||
{ publicKey: "MSKPublicKeyBase64AAAAAAAAAAAAAAAAAAAAAA=", allowedIps: ["10.210.0.1/32"], endpoint: "10.0.0.1:13231", latestHandshake: "2 мин назад", transferRx: 64_800_000, transferTx: 182_400_000, persistent: true },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "srv3", name: "mt-fra-edge-01", host: "10.0.2.1", model: "CCR2004-16G-2S+", os: "7.20.1",
|
||||
site: "FRA", country: "DE", asn: "AS65003", type: "exit-node", enabled: true, status: "online", latency: 41, sessions: 3,
|
||||
ipv6Address: "2a01:4f8:1c3c:c3d4::1",
|
||||
rpkiEnabled: true,
|
||||
wireGuardIfaces: [
|
||||
{
|
||||
id: "wg4", name: "wg-fra-msk", listenPort: 13231, mtu: 1420, comment: "FRA → MSK overlay", enabled: true, status: "up",
|
||||
peers: [
|
||||
{ publicKey: "MSKPublicKeyBase64AAAAAAAAAAAAAAAAAAAAAA=", allowedIps: ["10.210.0.1/32"], endpoint: "10.0.0.1:13232", latestHandshake: "1 мин назад", transferRx: 210_000_000, transferTx: 421_000_000, persistent: true },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "srv4", name: "mt-ams-edge-01", host: "10.0.3.1", model: "RB5009UG+S+IN", os: "7.19.3",
|
||||
site: "AMS", country: "NL", asn: "AS65004", type: "exit-node", enabled: true, status: "degraded", latency: 88, sessions: 2,
|
||||
},
|
||||
{
|
||||
id: "srv5", name: "mt-sgp-edge-01", host: "10.0.4.1", model: "CCR2004-1G-12S+2XS", os: "7.14.2",
|
||||
site: "SGP", country: "SG", asn: "AS65005", type: "exit-node", enabled: false, status: "offline", latency: null, sessions: 0,
|
||||
},
|
||||
{
|
||||
id: "srv6", name: "mt-ams-test-01", host: "10.0.3.2", model: "hEX S", os: "7.20.1",
|
||||
site: "AMS", country: "NL", asn: "AS65006", type: "jump-host", enabled: true, status: "online", latency: 92, sessions: 1,
|
||||
},
|
||||
{
|
||||
id: "srv7", name: "mt-msk-lab-01", host: "10.0.0.7", model: "RB5009UG+S+IN", os: "7.20.1",
|
||||
site: "MSK", country: "RU", asn: "AS65007", type: "jump-host", enabled: true, status: "online", latency: 14, sessions: 2,
|
||||
vrfNames: ["lab"],
|
||||
},
|
||||
{
|
||||
id: "home1", name: "home-msk-01", host: "192.168.10.1", model: "hEX S (RB760iGS)", os: "7.20.1",
|
||||
site: "MSK", country: "RU", asn: "", type: "home-router", enabled: true, status: "online", latency: 3, sessions: 0,
|
||||
comment: "MSK офис — основной роутер",
|
||||
lanSubnet: "192.168.10.0/24",
|
||||
wanUplinks: [
|
||||
{ id: "w1a", name: "WAN1-RT", isp: "Rostelecom", iface: "ether1", ip: "94.25.168.1", maxDl: 500, maxUl: 500 },
|
||||
{ id: "w1b", name: "WAN2-BL", isp: "Beeline", iface: "ether2", ip: "85.174.12.1", maxDl: 100, maxUl: 100 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "home2", name: "home-spb-01", host: "192.168.20.1", model: "RB5009UG+S+IN", os: "7.20.1",
|
||||
site: "SPB", country: "RU", asn: "", type: "home-router", enabled: true, status: "online", latency: 5, sessions: 0,
|
||||
comment: "SPB офис — основной роутер",
|
||||
lanSubnet: "192.168.20.0/24",
|
||||
wanUplinks: [
|
||||
{ id: "w2a", name: "WAN1-MTS", isp: "МТС", iface: "ether1", ip: "178.214.4.1", maxDl: 300, maxUl: 300 },
|
||||
{ id: "w2b", name: "WAN2-ERT", isp: "ЭР-Телеком", iface: "ether2", ip: "95.31.14.1", maxDl: 200, maxUl: 150 },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export const filters: Filter[] = [
|
||||
{
|
||||
id: "f1", name: "youtube-bypass", domains: 1842, ips: 4218, asns: 12,
|
||||
servers: ["srv1","srv2","srv3","srv4"], enabled: true, updated: "12 мин назад",
|
||||
communities: ["65001:100","65001:200"], gateway: "10.0.0.1",
|
||||
largeCommunities: ["65001:100:1","65001:100:2"],
|
||||
rpkiValid: true,
|
||||
},
|
||||
{
|
||||
id: "f2", name: "streaming-eu", domains: 612, ips: 2104, asns: 8,
|
||||
servers: ["srv2","srv3","srv4"], enabled: true, updated: "2 ч назад",
|
||||
communities: ["65001:200"], gateway: "10.0.1.1",
|
||||
largeCommunities: ["65001:200:1"],
|
||||
rpkiValid: true,
|
||||
},
|
||||
{
|
||||
id: "f3", name: "cdn-bypass", domains: 4218, ips: 18211, asns: 22,
|
||||
servers: ["srv1","srv2","srv3","srv4","srv6"], enabled: true, updated: "1 ч назад",
|
||||
communities: ["65001:300","65002:100"], gateway: "10.0.0.1",
|
||||
flowspecRules: [
|
||||
{ id: "fs1", dstPrefix: "104.16.0.0/12", ipProto: "tcp", dstPort: "443", action: "rate-limit", rate: 100, comment: "CF rate-limit" },
|
||||
],
|
||||
},
|
||||
{ id: "f4", name: "social-block-school", domains: 84, ips: 412, asns: 4, servers: ["srv1","srv7"], enabled: false, updated: "3 дн назад", communities: ["65007:999"], gateway: "" },
|
||||
{ id: "f5", name: "office-direct", domains: 142, ips: 884, asns: 6, servers: ["srv1","srv2","srv3","srv4","srv6","srv7"], enabled: true, updated: "4 ч назад", communities: ["65001:400"], gateway: "10.0.0.1" },
|
||||
{ id: "f6", name: "gaming-low-latency", domains: 88, ips: 1212, asns: 14, servers: ["srv1","srv2","srv3","srv7"], enabled: true, updated: "24 мин назад", communities: ["65001:500","65002:200"], gateway: "10.0.2.1" },
|
||||
]
|
||||
|
||||
export const domains: Domain[] = [
|
||||
{ id: "d1", domain: "youtube.com", purpose: "streaming", filter: "youtube-bypass", resolvedIp: "142.250.74.110", asn: "AS15169", updated: "12 мин", enabled: true },
|
||||
{ id: "d2", domain: "googlevideo.com", purpose: "streaming", filter: "youtube-bypass", resolvedIp: "142.250.74.142", asn: "AS15169", updated: "12 мин", enabled: true },
|
||||
{ id: "d3", domain: "ytimg.com", purpose: "streaming", filter: "youtube-bypass", resolvedIp: "172.217.16.46", asn: "AS15169", updated: "12 мин", enabled: true },
|
||||
{ id: "d4", domain: "netflix.com", purpose: "streaming", filter: "streaming-eu", resolvedIp: "54.246.79.9", asn: "AS2906", updated: "2 ч", enabled: true },
|
||||
{ id: "d5", domain: "nflxvideo.net", purpose: "streaming", filter: "streaming-eu", resolvedIp: "45.57.32.18", asn: "AS2906", updated: "2 ч", enabled: true },
|
||||
{ id: "d6", domain: "cloudflare.com", purpose: "cdn", filter: "cdn-bypass", resolvedIp: "104.16.132.229", asn: "AS13335", updated: "1 ч", enabled: true },
|
||||
{ id: "d7", domain: "akamaized.net", purpose: "cdn", filter: "cdn-bypass", resolvedIp: "23.46.18.94", asn: "AS20940", updated: "1 ч", enabled: true },
|
||||
{ id: "d8", domain: "fastly.net", purpose: "cdn", filter: "cdn-bypass", resolvedIp: "151.101.1.95", asn: "AS54113", updated: "1 ч", enabled: true },
|
||||
{ id: "d9", domain: "twitch.tv", purpose: "streaming", filter: "streaming-eu", resolvedIp: "52.223.241.72", asn: "AS16509", updated: "2 ч", enabled: false },
|
||||
{ id: "d10", domain: "discord.com", purpose: "voip", filter: "gaming-low-latency", resolvedIp: "162.159.135.232", asn: "AS13335", updated: "24 мин", enabled: true },
|
||||
{ id: "d11", domain: "steam.com", purpose: "gaming", filter: "gaming-low-latency", resolvedIp: "23.45.139.40", asn: "AS20940", updated: "24 мин", enabled: true },
|
||||
{ id: "d12", domain: "epicgames.com", purpose: "gaming", filter: "gaming-low-latency", resolvedIp: "34.205.247.51", asn: "AS16509", updated: "24 мин", enabled: true },
|
||||
]
|
||||
|
||||
export const ipRanges: IpRange[] = [
|
||||
{ id: "i1", cidr: "142.250.0.0/15", purpose: "streaming", filter: "youtube-bypass", asn: "AS15169", country: "US", updated: "12 мин", enabled: true },
|
||||
{ id: "i2", cidr: "172.217.0.0/16", purpose: "streaming", filter: "youtube-bypass", asn: "AS15169", country: "US", updated: "12 мин", enabled: true },
|
||||
{ id: "i3", cidr: "104.16.0.0/12", purpose: "cdn", filter: "cdn-bypass", asn: "AS13335", country: "US", updated: "1 ч", enabled: true },
|
||||
{ id: "i4", cidr: "23.32.0.0/11", purpose: "cdn", filter: "cdn-bypass", asn: "AS20940", country: "US", updated: "1 ч", enabled: true },
|
||||
{ id: "i5", cidr: "151.101.0.0/16", purpose: "cdn", filter: "cdn-bypass", asn: "AS54113", country: "US", updated: "1 ч", enabled: true },
|
||||
{ id: "i6", cidr: "54.246.0.0/16", purpose: "streaming", filter: "streaming-eu", asn: "AS2906", country: "IE", updated: "2 ч", enabled: true },
|
||||
{ id: "i7", cidr: "45.57.0.0/16", purpose: "streaming", filter: "streaming-eu", asn: "AS2906", country: "NL", updated: "2 ч", enabled: true },
|
||||
{ id: "i8", cidr: "162.159.128.0/19", purpose: "voip", filter: "gaming-low-latency", asn: "AS13335", country: "US", updated: "24 мин", enabled: true },
|
||||
{ id: "i9", cidr: "52.223.240.0/20", purpose: "streaming", filter: "streaming-eu", asn: "AS16509", country: "IE", updated: "2 ч", enabled: false },
|
||||
]
|
||||
|
||||
export const asns: Asn[] = [
|
||||
{ id: "a1", asn: "AS15169", org: "GOOGLE", country: "US", prefixes: 842, filter: "youtube-bypass", updated: "12 мин", enabled: true },
|
||||
{ id: "a2", asn: "AS13335", org: "CLOUDFLARENET", country: "US", prefixes: 1284, filter: "cdn-bypass", updated: "1 ч", enabled: true },
|
||||
{ id: "a3", asn: "AS16509", org: "AMAZON-02", country: "US", prefixes: 4128, filter: "streaming-eu", updated: "2 ч", enabled: true },
|
||||
{ id: "a4", asn: "AS20940", org: "AKAMAI-ASN1", country: "US", prefixes: 614, filter: "cdn-bypass", updated: "1 ч", enabled: true },
|
||||
{ id: "a5", asn: "AS54113", org: "FASTLY", country: "US", prefixes: 122, filter: "cdn-bypass", updated: "1 ч", enabled: true },
|
||||
{ id: "a6", asn: "AS2906", org: "AS-SSI (Netflix)", country: "US", prefixes: 88, filter: "streaming-eu", updated: "2 ч", enabled: true },
|
||||
{ id: "a7", asn: "AS32934", org: "FACEBOOK", country: "US", prefixes: 412, filter: "social-block-school",updated: "3 дн", enabled: false },
|
||||
{ id: "a8", asn: "AS8075", org: "MICROSOFT-CORP", country: "US", prefixes: 882, filter: "office-direct", updated: "4 ч", enabled: true },
|
||||
]
|
||||
|
||||
export const firewallRules: FirewallRule[] = [
|
||||
{ id: "fw1", chain: "forward", action: "accept", proto: "tcp", src: "youtube-bypass", dst: "0.0.0.0/0", port: "443", iface: "wan-bgp", comment: "YouTube bypass traffic", enabled: true, hits: 188421 },
|
||||
{ id: "fw2", chain: "forward", action: "mark-routing", proto: "tcp", src: "10.10.0.0/24", dst: "cdn-bypass", port: "—", iface: "lan", comment: "CDN routing mark", enabled: true, hits: 4128821 },
|
||||
{ id: "fw3", chain: "srcnat", action: "masquerade", proto: "all", src: "10.10.0.0/16", dst: "0.0.0.0/0", port: "—", iface: "wan-msk", comment: "Main NAT rule", enabled: true, hits: 12889241 },
|
||||
{ id: "fw4", chain: "forward", action: "drop", proto: "tcp", src: "0.0.0.0/0", dst: "social-block-school", port: "—", iface: "lan-school", comment: "School social block", enabled: true, hits: 4218 },
|
||||
{ id: "fw5", chain: "input", action: "accept", proto: "tcp", src: "10.10.0.0/16", dst: "—", port: "22,443,8291",iface: "—", comment: "Management access", enabled: true, hits: 18412 },
|
||||
{ id: "fw6", chain: "input", action: "drop", proto: "tcp", src: "0.0.0.0/0", dst: "—", port: "22", iface: "wan-msk", comment: "Block SSH from WAN", enabled: true, hits: 882412 },
|
||||
]
|
||||
|
||||
export const backups: Backup[] = [
|
||||
{ id: "b1", server: "mt-msk-core-01", filename: "mt-msk-core-01_2026-04-26_03-00.rsc", size: "124 КБ", created: "Сегодня, 03:00", kind: "auto", notes: "снапшот перед обновлением" },
|
||||
{ id: "b2", server: "mt-msk-core-01", filename: "mt-msk-core-01_2026-04-25_03-00.rsc", size: "124 КБ", created: "Вчера, 03:00", kind: "auto", notes: "" },
|
||||
{ id: "b3", server: "mt-spb-edge-01", filename: "mt-spb-edge-01_2026-04-26_03-00.rsc", size: "88 КБ", created: "Сегодня, 03:00", kind: "auto", notes: "" },
|
||||
{ id: "b4", server: "mt-fra-edge-01", filename: "mt-fra-edge-01_2026-04-26_manual.rsc",size: "92 КБ", created: "Сегодня, 14:18", kind: "manual", notes: "перед изменением BGP" },
|
||||
{ id: "b5", server: "mt-ams-edge-01", filename: "mt-ams-edge-01_2026-04-25_03-00.rsc", size: "64 КБ", created: "Вчера, 03:00", kind: "auto", notes: "" },
|
||||
]
|
||||
|
||||
export const pingProbes: PingProbe[] = [
|
||||
// srv1 — mt-msk-core-01 (JH, MSK) → основной мониторинг
|
||||
{ id: "p1", srcServerId: "srv1", name: "youtube.com", target: "142.250.74.110", filter: "youtube-bypass", rtt: 18, loss: 0, status: "up", series: spark(40, 18, 4), enabled: true },
|
||||
{ id: "p2", srcServerId: "srv1", name: "cloudflare.com", target: "104.16.132.229", filter: "cdn-bypass", rtt: 4, loss: 0, status: "up", series: spark(40, 4, 1), enabled: true },
|
||||
{ id: "p3", srcServerId: "srv1", name: "netflix-eu", target: "54.246.79.9", filter: "streaming-eu", rtt: 28, loss: 0, status: "up", series: spark(40, 28, 5), enabled: true },
|
||||
{ id: "p4", srcServerId: "srv1", name: "fastly-edge", target: "151.101.1.95", filter: "cdn-bypass", rtt: 8, loss: 0, status: "up", series: spark(40, 8, 2), enabled: true },
|
||||
// srv7 — mt-msk-lab-01 (JH, MSK) → лаборатория
|
||||
{ id: "p5", srcServerId: "srv7", name: "discord-voip", target: "162.159.135.232", filter: "gaming-low-latency", rtt: 12, loss: 0, status: "up", series: spark(40, 12, 2), enabled: true },
|
||||
{ id: "p6", srcServerId: "srv7", name: "steam-cdn", target: "23.45.139.40", filter: "gaming-low-latency", rtt: 38, loss: 1.2, status: "warn", series: spark(40, 38, 12), enabled: true },
|
||||
{ id: "p7", srcServerId: "srv7", name: "akamai-edge", target: "23.46.18.94", filter: "cdn-bypass", rtt: 22, loss: 0, status: "up", series: spark(40, 22, 4), enabled: true },
|
||||
// srv2 — mt-spb-edge-01 (EN, SPB) → исходящий трафик SPB
|
||||
{ id: "p8", srcServerId: "srv2", name: "youtube.com", target: "142.250.74.110", filter: "youtube-bypass", rtt: 22, loss: 0, status: "up", series: spark(40, 22, 5), enabled: true },
|
||||
{ id: "p9", srcServerId: "srv2", name: "office-365", target: "104.47.0.22", filter: "office-direct", rtt: 31, loss: 0, status: "up", series: spark(40, 31, 6), enabled: false },
|
||||
// srv3 — mt-fra-edge-01 (EN, FRA) → исходящий трафик Frankfurt
|
||||
{ id: "p10", srcServerId: "srv3", name: "youtube.com", target: "142.250.74.110", filter: "youtube-bypass", rtt: 12, loss: 0, status: "up", series: spark(40, 12, 3), enabled: true },
|
||||
{ id: "p11", srcServerId: "srv3", name: "netflix-eu", target: "54.246.79.9", filter: "streaming-eu", rtt: 9, loss: 0, status: "up", series: spark(40, 9, 2), enabled: true },
|
||||
// srv4 — mt-ams-edge-01 (EN, AMS, degraded) → деградировавший узел
|
||||
{ id: "p12", srcServerId: "srv4", name: "cloudflare.com", target: "104.16.132.229", filter: "cdn-bypass", rtt: 88, loss: 3.4, status: "warn", series: spark(40, 88, 22), enabled: true },
|
||||
{ id: "p13", srcServerId: "srv4", name: "sgp-probe", target: "139.180.22.4", filter: "—", rtt: null,loss: 100, status: "down", series: Array(40).fill(0), enabled: true },
|
||||
]
|
||||
|
||||
export const systemEvents: SystemEvent[] = [
|
||||
{ id: "e1", sev: "destructive", title: "BGP-сессия упала", message: "mt-sgp-edge-01 · пир 139.180.22.4 · простой 2ч", when: "2ч" },
|
||||
{ id: "e2", sev: "destructive", title: "Ошибка синхронизации фильтра", message: "cdn-bypass → mt-ams-edge-01 · таймаут", when: "14м" },
|
||||
{ id: "e3", sev: "warning", title: "Высокая задержка на AMS", message: "среднее 88мс · порог 60мс", when: "8м" },
|
||||
{ id: "e4", sev: "warning", title: "Бэкап просрочен", message: "mt-msk-lab-01 · последний 5д назад", when: "1ч" },
|
||||
{ id: "e5", sev: "info", title: "Обновление BGP завершено", message: "+412 префиксов · −188 отозвано", when: "12м" },
|
||||
{ id: "e6", sev: "info", title: "Фильтр youtube-bypass обновлён", message: "+88 доменов · ↑ 24 сервера", when: "12м" },
|
||||
]
|
||||
|
||||
export const dashLatency = {
|
||||
msk: spark(60, 12, 4),
|
||||
spb: spark(60, 28, 6),
|
||||
fra: spark(60, 41, 9),
|
||||
ams: spark(60, 88, 18),
|
||||
}
|
||||
|
||||
export const traffic = {
|
||||
rx: Array.from({ length: 60 }, (_, i) => Math.round(280 + Math.sin(i / 6) * 60 + ((i * 7919) % 31))),
|
||||
tx: Array.from({ length: 60 }, (_, i) => Math.round(220 + Math.sin(i / 5) * 50 + ((i * 6271) % 21))),
|
||||
}
|
||||
|
||||
// ─── GRE ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export type GreStatus = "up" | "down" | "degraded"
|
||||
export type IpsecEncAlg = "aes-128" | "aes-192" | "aes-256"
|
||||
export type IpsecAuthAlg = "sha1" | "sha256" | "sha512"
|
||||
export type IpsecDhGroup = "modp1024" | "modp2048" | "modp4096" | "ecp256" | "ecp384" | "ecp521"
|
||||
export type IkeVersion = "ikev1" | "ikev2"
|
||||
export type DscpMode = "inherit" | number
|
||||
|
||||
export interface GreIpsec {
|
||||
secret: string // ipsec-secret → auto-creates peer+policy+proposal
|
||||
encAlg: IpsecEncAlg // proposal enc-algorithms
|
||||
authAlg: IpsecAuthAlg // proposal auth-algorithms
|
||||
dhGroup: IpsecDhGroup // proposal pfs-group / peer dh-group
|
||||
ikeVersion: IkeVersion // peer exchange-mode
|
||||
lifetime: string // proposal lifetime (e.g. "1d 00:00:00")
|
||||
pfs: boolean // perfect forward secrecy
|
||||
}
|
||||
|
||||
export interface GreTunnel {
|
||||
id: string
|
||||
name: string // interface name, e.g. gre-msk-spb
|
||||
serverId: string // which MikroTik runs this tunnel
|
||||
localAddress: string // local GRE endpoint ("0.0.0.0" = auto)
|
||||
remoteAddress: string // remote GRE endpoint (required)
|
||||
localInnerIp: string // /ip address on this side, e.g. 10.200.0.1/30
|
||||
remoteInnerIp: string // /ip address on remote side, e.g. 10.200.0.2/30
|
||||
poolId: string // which IP pool the inner IPs come from
|
||||
ipsec: GreIpsec | null // null = no IPsec (plain GRE)
|
||||
mtu: number // default 1476
|
||||
keepaliveInterval: number // seconds, 0 = disabled
|
||||
keepaliveRetries: number
|
||||
dscp: DscpMode // "inherit" or 0-63
|
||||
clampTcpMss: boolean
|
||||
allowFastPath: boolean
|
||||
comment: string
|
||||
enabled: boolean
|
||||
status: GreStatus
|
||||
}
|
||||
|
||||
export interface GrePool {
|
||||
id: string
|
||||
name: string
|
||||
cidr: string
|
||||
allocated: number
|
||||
total: number
|
||||
comment: string
|
||||
}
|
||||
|
||||
const defaultIpsec = (secret: string, ikeVersion: IkeVersion = "ikev2"): GreIpsec => ({
|
||||
secret,
|
||||
encAlg: "aes-256",
|
||||
authAlg: "sha256",
|
||||
dhGroup: "modp2048",
|
||||
ikeVersion,
|
||||
lifetime: "1d 00:00:00",
|
||||
pfs: true,
|
||||
})
|
||||
|
||||
export const grePools: GrePool[] = [
|
||||
{ id: "gp1", name: "pool-gre-core", cidr: "10.200.0.0/24", allocated: 16, total: 64, comment: "Ядровые межузловые туннели" },
|
||||
{ id: "gp2", name: "pool-gre-mgmt", cidr: "10.201.0.0/24", allocated: 8, total: 64, comment: "Управляющие туннели (lab, test)" },
|
||||
]
|
||||
|
||||
export const greTunnels: GreTunnel[] = [
|
||||
{
|
||||
id: "gre1", name: "gre-msk-spb", serverId: "srv1",
|
||||
localAddress: "10.0.0.1", remoteAddress: "10.0.1.1",
|
||||
localInnerIp: "10.200.0.1/30", remoteInnerIp: "10.200.0.2/30", poolId: "gp1",
|
||||
ipsec: defaultIpsec("xK9#mQ2$vLp8nR4"),
|
||||
mtu: 1476, keepaliveInterval: 10, keepaliveRetries: 10,
|
||||
dscp: "inherit", clampTcpMss: true, allowFastPath: true,
|
||||
comment: "MSK → SPB core interconnect", enabled: true, status: "up",
|
||||
},
|
||||
{
|
||||
id: "gre2", name: "gre-msk-fra", serverId: "srv1",
|
||||
localAddress: "10.0.0.1", remoteAddress: "10.0.2.1",
|
||||
localInnerIp: "10.200.0.5/30", remoteInnerIp: "10.200.0.6/30", poolId: "gp1",
|
||||
ipsec: defaultIpsec("pW3@jT7!hD5kYe1"),
|
||||
mtu: 1476, keepaliveInterval: 10, keepaliveRetries: 10,
|
||||
dscp: "inherit", clampTcpMss: true, allowFastPath: true,
|
||||
comment: "MSK → FRA core interconnect", enabled: true, status: "up",
|
||||
},
|
||||
{
|
||||
id: "gre3", name: "gre-msk-ams", serverId: "srv1",
|
||||
localAddress: "10.0.0.1", remoteAddress: "10.0.3.1",
|
||||
localInnerIp: "10.200.0.9/30", remoteInnerIp: "10.200.0.10/30", poolId: "gp1",
|
||||
ipsec: defaultIpsec("rN6%bF2*cM8sZa0"),
|
||||
mtu: 1476, keepaliveInterval: 10, keepaliveRetries: 10,
|
||||
dscp: "inherit", clampTcpMss: true, allowFastPath: false,
|
||||
comment: "MSK → AMS core interconnect", enabled: true, status: "degraded",
|
||||
},
|
||||
{
|
||||
id: "gre4", name: "gre-msk-sgp", serverId: "srv1",
|
||||
localAddress: "10.0.0.1", remoteAddress: "10.0.4.1",
|
||||
localInnerIp: "10.200.0.13/30", remoteInnerIp: "10.200.0.14/30", poolId: "gp1",
|
||||
ipsec: defaultIpsec("qA4^eU9#wG1oXi6"),
|
||||
mtu: 1476, keepaliveInterval: 10, keepaliveRetries: 10,
|
||||
dscp: "inherit", clampTcpMss: true, allowFastPath: true,
|
||||
comment: "MSK → SGP core interconnect", enabled: false, status: "down",
|
||||
},
|
||||
{
|
||||
id: "gre5", name: "gre-lab-core", serverId: "srv7",
|
||||
localAddress: "10.0.0.7", remoteAddress: "10.0.0.1",
|
||||
localInnerIp: "10.201.0.1/30", remoteInnerIp: "10.201.0.2/30", poolId: "gp2",
|
||||
ipsec: null,
|
||||
mtu: 1500, keepaliveInterval: 0, keepaliveRetries: 0,
|
||||
dscp: "inherit", clampTcpMss: false, allowFastPath: true,
|
||||
comment: "Lab → core (internal, no IPsec)", enabled: true, status: "up",
|
||||
},
|
||||
{
|
||||
id: "gre6", name: "gre-ams-test", serverId: "srv6",
|
||||
localAddress: "10.0.3.2", remoteAddress: "10.0.3.1",
|
||||
localInnerIp: "10.201.0.5/30", remoteInnerIp: "10.201.0.6/30", poolId: "gp2",
|
||||
ipsec: null,
|
||||
mtu: 1476, keepaliveInterval: 5, keepaliveRetries: 5,
|
||||
dscp: 0, clampTcpMss: true, allowFastPath: true,
|
||||
comment: "AMS test → AMS edge", enabled: true, status: "up",
|
||||
},
|
||||
]
|
||||
|
||||
// ─── Filter Rules (Community → Gateway per server) ────────────────────────────
|
||||
|
||||
export interface FilterRule {
|
||||
id: string
|
||||
community: string // BGP community, e.g. "65001:100"
|
||||
communityName?: string // optional human-readable name
|
||||
action?: "route" | "blackhole" // default "route"; "blackhole" → null-route (RTBH)
|
||||
gateway: string // remote inner IP of GRE tunnel; empty for blackhole
|
||||
gatewayTunnelId: string // reference to greTunnel.id; empty for blackhole
|
||||
description: string
|
||||
}
|
||||
|
||||
export interface ServerFilterRuleset {
|
||||
serverId: string
|
||||
rules: FilterRule[]
|
||||
}
|
||||
|
||||
export const serverFilterRulesets: ServerFilterRuleset[] = [
|
||||
// srv1 — mt-msk-core-01 (jump-host, MSK): gre1=SPB, gre2=FRA, gre3=AMS(deg), gre4=SGP(down)
|
||||
{
|
||||
serverId: "srv1",
|
||||
rules: [
|
||||
{ id: "r1", community: "65001:100", communityName: "youtube-bypass", gateway: "10.200.0.6", gatewayTunnelId: "gre2", description: "YouTube/Google Video → Frankfurt (IX)" },
|
||||
{ id: "r2", community: "65001:200", communityName: "streaming-eu", gateway: "10.200.0.6", gatewayTunnelId: "gre2", description: "Netflix, Twitch EU → Frankfurt" },
|
||||
{ id: "r3", community: "65001:300", communityName: "cdn-bypass", gateway: "10.200.0.2", gatewayTunnelId: "gre1", description: "Cloudflare / Akamai / Fastly → SPB" },
|
||||
{ id: "r4", community: "65002:100", communityName: "cdn-secondary", gateway: "10.200.0.10", gatewayTunnelId: "gre3", description: "CDN резерв → Amsterdam (degraded)" },
|
||||
{ id: "r5", community: "65001:400", communityName: "office-direct", gateway: "10.200.0.2", gatewayTunnelId: "gre1", description: "MS Office 365 / Teams → SPB direct" },
|
||||
{ id: "r6", community: "65001:500", communityName: "gaming-ll", gateway: "10.200.0.2", gatewayTunnelId: "gre1", description: "Discord / Steam / Epic — низкая задержка → SPB" },
|
||||
{ id: "r10", community: "65535:666", communityName: "BLACKHOLE (RFC 7999)", action: "blackhole", gateway: "", gatewayTunnelId: "", description: "RTBH — null-route входящих маршрутов" },
|
||||
{ id: "r11", community: "65001:666", communityName: "blackhole-local", action: "blackhole", gateway: "", gatewayTunnelId: "", description: "Локальная блокировка — сброс трафика" },
|
||||
],
|
||||
},
|
||||
// srv7 — mt-msk-lab-01 (jump-host, MSK lab): gre5=core
|
||||
{
|
||||
serverId: "srv7",
|
||||
rules: [
|
||||
{ id: "r7", community: "65001:100", communityName: "youtube-bypass", gateway: "10.201.0.2", gatewayTunnelId: "gre5", description: "Lab: YouTube → core" },
|
||||
{ id: "r8", community: "65007:999", communityName: "school-block", gateway: "10.201.0.2", gatewayTunnelId: "gre5", description: "Lab: школьная блокировка" },
|
||||
],
|
||||
},
|
||||
// srv6 — mt-ams-test-01 (jump-host, AMS test): gre6=AMS edge
|
||||
{
|
||||
serverId: "srv6",
|
||||
rules: [
|
||||
{ id: "r9", community: "65001:300", communityName: "cdn-bypass", gateway: "10.201.0.6", gatewayTunnelId: "gre6", description: "AMS test: CDN → AMS edge" },
|
||||
],
|
||||
},
|
||||
// Exit nodes — нет правил по умолчанию
|
||||
{ serverId: "srv2", rules: [] },
|
||||
{ serverId: "srv3", rules: [] },
|
||||
{ serverId: "srv4", rules: [] },
|
||||
{ serverId: "srv5", rules: [] },
|
||||
]
|
||||
|
||||
// ─── VXLAN mock data ─────────────────────────────────────────────────────────
|
||||
|
||||
export const vxlanTunnels: VxlanTunnel[] = [
|
||||
{
|
||||
id: "vx1", name: "vxlan-overlay-msk", vni: 10010, port: 0, dstPort: 8472,
|
||||
serverId: "srv1", vtepIp: "10.0.0.1", remoteVteps: ["10.0.1.1","10.0.2.1"],
|
||||
l2mtu: 1500, arpProxy: true, macLearning: true,
|
||||
comment: "MSK overlay — сегмент bypass", enabled: true, status: "up",
|
||||
},
|
||||
{
|
||||
id: "vx2", name: "vxlan-overlay-spb", vni: 10010, port: 0, dstPort: 8472,
|
||||
serverId: "srv2", vtepIp: "10.0.1.1", remoteVteps: ["10.0.0.1","10.0.2.1"],
|
||||
l2mtu: 1500, arpProxy: true, macLearning: true,
|
||||
comment: "SPB overlay — сегмент bypass", enabled: true, status: "up",
|
||||
},
|
||||
{
|
||||
id: "vx3", name: "vxlan-overlay-fra", vni: 10010, port: 0, dstPort: 8472,
|
||||
serverId: "srv3", vtepIp: "10.0.2.1", remoteVteps: ["10.0.0.1","10.0.1.1"],
|
||||
l2mtu: 1500, arpProxy: true, macLearning: false,
|
||||
comment: "FRA overlay — сегмент bypass", enabled: true, status: "up",
|
||||
},
|
||||
{
|
||||
id: "vx4", name: "vxlan-mgmt-lab", vni: 20001, port: 0, dstPort: 4789,
|
||||
serverId: "srv7", vtepIp: "10.0.0.7", remoteVteps: ["10.0.0.1"],
|
||||
l2mtu: 1500, arpProxy: false, macLearning: true,
|
||||
comment: "Lab management VXLAN", enabled: true, status: "up",
|
||||
},
|
||||
{
|
||||
id: "vx5", name: "vxlan-overlay-ams", vni: 10010, port: 0, dstPort: 8472,
|
||||
serverId: "srv4", vtepIp: "10.0.3.1", remoteVteps: ["10.0.0.1"],
|
||||
l2mtu: 1500, arpProxy: true, macLearning: true,
|
||||
comment: "AMS overlay (degraded)", enabled: true, status: "down",
|
||||
},
|
||||
]
|
||||
|
||||
// ─── Container mock data ─────────────────────────────────────────────────────
|
||||
|
||||
export const routerContainers: RouterContainer[] = [
|
||||
{
|
||||
id: "c1", name: "adguard-msk", serverId: "srv1",
|
||||
image: "adguard/adguardhome", tag: "latest",
|
||||
status: "running",
|
||||
envs: [{ key: "AGH_CONFIG", value: "/opt/adguardhome/conf/AdGuardHome.yaml" }],
|
||||
mounts: [{ dst: "/opt/adguardhome/conf", src: "/disk1/adguard" }, { dst: "/opt/adguardhome/work", src: "/disk1/adguard-work" }],
|
||||
interfaces: ["veth-adguard"],
|
||||
startOnBoot: true, comment: "AdGuard Home — DNS фильтрация",
|
||||
uptime: "12д 4ч", cpu: 3, memMb: 64,
|
||||
},
|
||||
{
|
||||
id: "c2", name: "prometheus-msk", serverId: "srv1",
|
||||
image: "prom/prometheus", tag: "v2.52.0",
|
||||
status: "running",
|
||||
envs: [],
|
||||
mounts: [{ dst: "/prometheus", src: "/disk1/prom-data" }],
|
||||
interfaces: ["veth-prom"],
|
||||
cmd: "--config.file=/etc/prometheus/prometheus.yml --storage.tsdb.retention.time=30d",
|
||||
startOnBoot: true, comment: "Prometheus метрики",
|
||||
uptime: "5д 11ч", cpu: 8, memMb: 256,
|
||||
},
|
||||
{
|
||||
id: "c3", name: "grafana-msk", serverId: "srv1",
|
||||
image: "grafana/grafana", tag: "10.4.2",
|
||||
status: "running",
|
||||
envs: [{ key: "GF_SECURITY_ADMIN_PASSWORD", value: "*** (hidden)" }],
|
||||
mounts: [{ dst: "/var/lib/grafana", src: "/disk1/grafana" }],
|
||||
interfaces: ["veth-grafana"],
|
||||
startOnBoot: true, comment: "Grafana — дашборды мониторинга",
|
||||
uptime: "5д 11ч", cpu: 5, memMb: 128,
|
||||
},
|
||||
{
|
||||
id: "c4", name: "wg-portal-spb", serverId: "srv2",
|
||||
image: "wgportal/wg-portal", tag: "v1.3.1",
|
||||
status: "running",
|
||||
envs: [{ key: "WG_PORTAL_ADMIN_USER", value: "admin" }],
|
||||
mounts: [{ dst: "/app/data", src: "/disk1/wgportal" }],
|
||||
interfaces: ["veth-wgportal"],
|
||||
startOnBoot: true, comment: "WireGuard Portal — управление клиентами",
|
||||
uptime: "3д 7ч", cpu: 1, memMb: 48,
|
||||
},
|
||||
{
|
||||
id: "c5", name: "unbound-fra", serverId: "srv3",
|
||||
image: "mvance/unbound", tag: "1.19.3",
|
||||
status: "stopped",
|
||||
envs: [],
|
||||
mounts: [],
|
||||
interfaces: ["veth-unbound"],
|
||||
startOnBoot: false, comment: "Unbound DNS резолвер (в резерве)",
|
||||
cpu: 0, memMb: 0,
|
||||
},
|
||||
]
|
||||
|
||||
// ─── Certificate mock data ────────────────────────────────────────────────────
|
||||
|
||||
export const routerCertificates: RouterCertificate[] = [
|
||||
{
|
||||
id: "cert1", name: "mt-msk-core-01-server", serverId: "srv1",
|
||||
commonName: "mt-msk-core-01.routerlists.io",
|
||||
sans: ["mt-msk-core-01.routerlists.io","10.0.0.1"],
|
||||
issuedBy: "RouterLists Internal CA", validFrom: "2024-01-15",
|
||||
validUntil: "2026-01-15", daysLeft: 261,
|
||||
keySize: 4096, usage: ["server"], trusted: true, status: "valid",
|
||||
},
|
||||
{
|
||||
id: "cert2", name: "routerlists-internal-ca", serverId: "srv1",
|
||||
commonName: "RouterLists Internal CA",
|
||||
sans: [],
|
||||
issuedBy: "RouterLists Internal CA (self-signed)", validFrom: "2023-06-01",
|
||||
validUntil: "2033-06-01", daysLeft: 2589,
|
||||
keySize: 4096, usage: ["ca","crl-sign"], trusted: true, status: "valid",
|
||||
},
|
||||
{
|
||||
id: "cert3", name: "mt-spb-edge-01-server", serverId: "srv2",
|
||||
commonName: "mt-spb-edge-01.routerlists.io",
|
||||
sans: ["mt-spb-edge-01.routerlists.io","10.0.1.1"],
|
||||
issuedBy: "RouterLists Internal CA", validFrom: "2024-01-15",
|
||||
validUntil: "2026-01-15", daysLeft: 261,
|
||||
keySize: 4096, usage: ["server"], trusted: true, status: "valid",
|
||||
},
|
||||
{
|
||||
id: "cert4", name: "mt-fra-edge-01-server", serverId: "srv3",
|
||||
commonName: "mt-fra-edge-01.routerlists.io",
|
||||
sans: ["mt-fra-edge-01.routerlists.io","10.0.2.1"],
|
||||
issuedBy: "Let's Encrypt", validFrom: "2026-01-10",
|
||||
validUntil: "2026-04-10", daysLeft: -19,
|
||||
keySize: 2048, usage: ["server"], trusted: true, status: "expired",
|
||||
},
|
||||
{
|
||||
id: "cert5", name: "wg-client-admin", serverId: "srv1",
|
||||
commonName: "admin.routerlists.io",
|
||||
sans: ["admin.routerlists.io"],
|
||||
issuedBy: "RouterLists Internal CA", validFrom: "2025-06-01",
|
||||
validUntil: "2026-06-01", daysLeft: 33,
|
||||
keySize: 2048, usage: ["client"], trusted: true, status: "valid",
|
||||
},
|
||||
{
|
||||
id: "cert6", name: "mt-ams-edge-01-server", serverId: "srv4",
|
||||
commonName: "mt-ams-edge-01.routerlists.io",
|
||||
sans: ["mt-ams-edge-01.routerlists.io"],
|
||||
issuedBy: "RouterLists Internal CA", validFrom: "2024-04-01",
|
||||
validUntil: "2026-05-15", daysLeft: 16,
|
||||
keySize: 2048, usage: ["server"], trusted: false, status: "valid",
|
||||
},
|
||||
{
|
||||
id: "cert7", name: "mt-msk-lab-01-server", serverId: "srv7",
|
||||
commonName: "mt-msk-lab-01.routerlists.io",
|
||||
sans: ["mt-msk-lab-01.routerlists.io","10.0.0.7"],
|
||||
issuedBy: "RouterLists Internal CA", validFrom: "2025-09-01",
|
||||
validUntil: "2026-09-01", daysLeft: 125,
|
||||
keySize: 4096, usage: ["server"], trusted: true, status: "valid",
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
Reference in new issue
Block a user