mirror of
https://github.com/h44z/wg-portal.git
synced 2026-09-03 21:36:44 +00:00
Add read-only support and UI indicators for MikroTik dynamic peers (#733)
* Add dynamic Mikrotik peer support & UI/i18n Introduce handling for dynamically created Mikrotik peers: add Dynamic/IsDynamic fields to domain, peer and interface models and API models; include "dynamic" in Mikrotik queries; skip updates/deletes for dynamic peers; sync dynamic flag when restoring state. Prevent modifying/deleting dynamic peers in manager. UI: disable selection/edit for dynamic peers and show badge. Implement Mikrotik interface hook execution. Add i18n keys/translations for dynamic-peer messages across languages. * Fix Mikrotik hooks implementation and UI visibility
This commit is contained in:
@@ -72,6 +72,14 @@ const isBackendValid = computed(() => {
|
|||||||
return valid
|
return valid
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const isHookSupported = computed(() => {
|
||||||
|
if (!formData.value.Backend) return false;
|
||||||
|
let availableBackends = settings.Setting('AvailableBackends') || [];
|
||||||
|
let backend = availableBackends.find(b => b.Id === formData.value.Backend);
|
||||||
|
if (!backend) return false;
|
||||||
|
return backend.Type === 'local' || backend.Type === 'mikrotik';
|
||||||
|
})
|
||||||
|
|
||||||
// functions
|
// functions
|
||||||
|
|
||||||
watch(() => props.visible, async (newValue, oldValue) => {
|
watch(() => props.visible, async (newValue, oldValue) => {
|
||||||
@@ -499,7 +507,7 @@ async function del() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset v-if="formData.Backend==='local'">
|
<fieldset v-if="isHookSupported">
|
||||||
<legend class="mt-4">{{ $t('modals.interface-edit.header-hooks') }}</legend>
|
<legend class="mt-4">{{ $t('modals.interface-edit.header-hooks') }}</legend>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.pre-up.label') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.interface-edit.pre-up.label') }}</label>
|
||||||
|
|||||||
@@ -138,7 +138,9 @@
|
|||||||
"peer-expiring": "Peer läuft ab am",
|
"peer-expiring": "Peer läuft ab am",
|
||||||
"peer-connected": "Verbunden",
|
"peer-connected": "Verbunden",
|
||||||
"peer-not-connected": "Nicht verbunden",
|
"peer-not-connected": "Nicht verbunden",
|
||||||
"peer-handshake": "Letzter Handshake:"
|
"peer-handshake": "Letzter Handshake:",
|
||||||
|
"peer-dynamic": "Dynamischer Peer",
|
||||||
|
"peer-dynamic-no-edit": "Dieser Peer wurde dynamisch erstellt und kann nicht bearbeitet oder gelöscht werden."
|
||||||
},
|
},
|
||||||
"users": {
|
"users": {
|
||||||
"headline": "Benutzerverwaltung",
|
"headline": "Benutzerverwaltung",
|
||||||
@@ -648,4 +650,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,7 +138,9 @@
|
|||||||
"peer-expiring": "Peer is expiring at",
|
"peer-expiring": "Peer is expiring at",
|
||||||
"peer-connected": "Connected",
|
"peer-connected": "Connected",
|
||||||
"peer-not-connected": "Not Connected",
|
"peer-not-connected": "Not Connected",
|
||||||
"peer-handshake": "Last handshake:"
|
"peer-handshake": "Last handshake:",
|
||||||
|
"peer-dynamic": "Dynamic Peer",
|
||||||
|
"peer-dynamic-no-edit": "This peer is dynamically created and cannot be edited or deleted."
|
||||||
},
|
},
|
||||||
"users": {
|
"users": {
|
||||||
"headline": "User Administration",
|
"headline": "User Administration",
|
||||||
@@ -648,4 +650,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,7 +153,9 @@
|
|||||||
"peer-expiring": "El peer expira en",
|
"peer-expiring": "El peer expira en",
|
||||||
"peer-connected": "Conectado",
|
"peer-connected": "Conectado",
|
||||||
"peer-not-connected": "No conectado",
|
"peer-not-connected": "No conectado",
|
||||||
"peer-handshake": "Último handshake:"
|
"peer-handshake": "Último handshake:",
|
||||||
|
"peer-dynamic": "Peer dinámico",
|
||||||
|
"peer-dynamic-no-edit": "Este peer se ha creado de forma dinámica y no se puede editar ni eliminar."
|
||||||
},
|
},
|
||||||
"users": {
|
"users": {
|
||||||
"headline": "Administración de usuarios",
|
"headline": "Administración de usuarios",
|
||||||
@@ -636,4 +638,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,7 +126,9 @@
|
|||||||
"peer-expiring": "Le pair expire le",
|
"peer-expiring": "Le pair expire le",
|
||||||
"peer-connected": "Connecté",
|
"peer-connected": "Connecté",
|
||||||
"peer-not-connected": "Non connecté",
|
"peer-not-connected": "Non connecté",
|
||||||
"peer-handshake": "Dernière négociation :"
|
"peer-handshake": "Dernière négociation :",
|
||||||
|
"peer-dynamic": "Peer dynamique",
|
||||||
|
"peer-dynamic-no-edit": "Ce peer est créé dynamiquement et ne peut être ni modifié ni supprimé."
|
||||||
},
|
},
|
||||||
"users": {
|
"users": {
|
||||||
"headline": "Administration des utilisateurs",
|
"headline": "Administration des utilisateurs",
|
||||||
@@ -514,4 +516,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,7 +138,9 @@
|
|||||||
"peer-expiring": "ピアの有効期限:",
|
"peer-expiring": "ピアの有効期限:",
|
||||||
"peer-connected": "接続中",
|
"peer-connected": "接続中",
|
||||||
"peer-not-connected": "未接続",
|
"peer-not-connected": "未接続",
|
||||||
"peer-handshake": "最終ハンドシェイク:"
|
"peer-handshake": "最終ハンドシェイク:",
|
||||||
|
"peer-dynamic": "動的ピア",
|
||||||
|
"peer-dynamic-no-edit": "このピアは動的に作成されており、編集または削除することはできません。"
|
||||||
},
|
},
|
||||||
"users": {
|
"users": {
|
||||||
"headline": "ユーザー管理",
|
"headline": "ユーザー管理",
|
||||||
@@ -648,4 +650,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -128,7 +128,9 @@
|
|||||||
"peer-expiring": "Peer expira em",
|
"peer-expiring": "Peer expira em",
|
||||||
"peer-connected": "Conectado",
|
"peer-connected": "Conectado",
|
||||||
"peer-not-connected": "Não Conectado",
|
"peer-not-connected": "Não Conectado",
|
||||||
"peer-handshake": "Último handshake:"
|
"peer-handshake": "Último handshake:",
|
||||||
|
"peer-dynamic": "Peer Dinâmico",
|
||||||
|
"peer-dynamic-no-edit": "Este peer é criado dinamicamente e não pode ser editado ou excluído."
|
||||||
},
|
},
|
||||||
"users": {
|
"users": {
|
||||||
"headline": "Administração de Utilizadores",
|
"headline": "Administração de Utilizadores",
|
||||||
@@ -553,4 +555,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,7 +134,9 @@
|
|||||||
"peer-expiring": "Пир истекает в",
|
"peer-expiring": "Пир истекает в",
|
||||||
"peer-connected": "Подключено",
|
"peer-connected": "Подключено",
|
||||||
"peer-not-connected": "Не подключено",
|
"peer-not-connected": "Не подключено",
|
||||||
"peer-handshake": "Последнее рукопожатие:"
|
"peer-handshake": "Последнее рукопожатие:",
|
||||||
|
"peer-dynamic": "Динамический пир",
|
||||||
|
"peer-dynamic-no-edit": "Этот пир создан динамически и не может быть изменен или удален."
|
||||||
},
|
},
|
||||||
"users": {
|
"users": {
|
||||||
"headline": "Администрирование пользователей",
|
"headline": "Администрирование пользователей",
|
||||||
@@ -628,4 +630,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,7 +126,9 @@
|
|||||||
"peer-expiring": "Пір припиняє дію о",
|
"peer-expiring": "Пір припиняє дію о",
|
||||||
"peer-connected": "Підключено",
|
"peer-connected": "Підключено",
|
||||||
"peer-not-connected": "Не підключено",
|
"peer-not-connected": "Не підключено",
|
||||||
"peer-handshake": "Останнє рукостискання:"
|
"peer-handshake": "Останнє рукостискання:",
|
||||||
|
"peer-dynamic": "Динамічний пір",
|
||||||
|
"peer-dynamic-no-edit": "Цей пір створено динамічно і його не можна редагувати або видаляти."
|
||||||
},
|
},
|
||||||
"users": {
|
"users": {
|
||||||
"headline": "Адміністрування користувачів",
|
"headline": "Адміністрування користувачів",
|
||||||
@@ -514,4 +516,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -125,7 +125,9 @@
|
|||||||
"peer-expiring": "Peer sẽ hết hạn vào",
|
"peer-expiring": "Peer sẽ hết hạn vào",
|
||||||
"peer-connected": "Đã kết nối",
|
"peer-connected": "Đã kết nối",
|
||||||
"peer-not-connected": "Chưa kết nối",
|
"peer-not-connected": "Chưa kết nối",
|
||||||
"peer-handshake": "Lần bắt tay cuối cùng:"
|
"peer-handshake": "Lần bắt tay cuối cùng:",
|
||||||
|
"peer-dynamic": "Peer động",
|
||||||
|
"peer-dynamic-no-edit": "Peer này được tạo động và không thể chỉnh sửa hoặc xóa."
|
||||||
},
|
},
|
||||||
"users": {
|
"users": {
|
||||||
"headline": "Quản trị Người dùng",
|
"headline": "Quản trị Người dùng",
|
||||||
@@ -492,4 +494,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,496 +1,497 @@
|
|||||||
{
|
{
|
||||||
"languages": {
|
"languages": {
|
||||||
"zh": "中文"
|
"zh": "中文"
|
||||||
|
},
|
||||||
|
"general": {
|
||||||
|
"pagination": {
|
||||||
|
"size": "每页显示数量",
|
||||||
|
"all": "全部 (较慢)"
|
||||||
},
|
},
|
||||||
"general": {
|
"search": {
|
||||||
"pagination": {
|
"placeholder": "搜索...",
|
||||||
"size": "每页显示数量",
|
"button": "搜索"
|
||||||
"all": "全部 (较慢)"
|
|
||||||
},
|
|
||||||
"search": {
|
|
||||||
"placeholder": "搜索...",
|
|
||||||
"button": "搜索"
|
|
||||||
},
|
|
||||||
"select-all": "全选",
|
|
||||||
"yes": "是",
|
|
||||||
"no": "否",
|
|
||||||
"cancel": "取消",
|
|
||||||
"close": "关闭",
|
|
||||||
"save": "保存",
|
|
||||||
"delete": "删除"
|
|
||||||
},
|
},
|
||||||
"login": {
|
"select-all": "全选",
|
||||||
"headline": "请登录",
|
"yes": "是",
|
||||||
"username": {
|
"no": "否",
|
||||||
"label": "用户名",
|
"cancel": "取消",
|
||||||
"placeholder": "请输入用户名"
|
"close": "关闭",
|
||||||
|
"save": "保存",
|
||||||
|
"delete": "删除"
|
||||||
|
},
|
||||||
|
"login": {
|
||||||
|
"headline": "请登录",
|
||||||
|
"username": {
|
||||||
|
"label": "用户名",
|
||||||
|
"placeholder": "请输入用户名"
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"label": "密码",
|
||||||
|
"placeholder": "请输入密码"
|
||||||
|
},
|
||||||
|
"button": "登录"
|
||||||
|
},
|
||||||
|
"menu": {
|
||||||
|
"home": "首页",
|
||||||
|
"interfaces": "接口",
|
||||||
|
"users": "用户",
|
||||||
|
"lang": "切换语言",
|
||||||
|
"profile": "个人资料",
|
||||||
|
"login": "登录",
|
||||||
|
"logout": "注销"
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"headline": "WireGuard® VPN Portal",
|
||||||
|
"info-headline": "更多信息",
|
||||||
|
"abstract": "WireGuard® 是一种极其简单但又快速现代的 VPN,采用先进的加密技术。它旨在比 IPsec 更快、更简单、更轻量、更实用,同时避免了大量的麻烦。它的性能显著优于 OpenVPN。",
|
||||||
|
"installation": {
|
||||||
|
"box-header": "WireGuard 安装",
|
||||||
|
"headline": "安装",
|
||||||
|
"content": "客户端软件的安装说明可在官方 WireGuard 网站上找到。",
|
||||||
|
"button": "打开说明"
|
||||||
|
},
|
||||||
|
"about-wg": {
|
||||||
|
"box-header": "关于 WireGuard",
|
||||||
|
"headline": "关于",
|
||||||
|
"content": "WireGuard® 是一种极其简单但又快速现代的 VPN,采用先进的加密技术。",
|
||||||
|
"button": "更多"
|
||||||
|
},
|
||||||
|
"about-portal": {
|
||||||
|
"box-header": "关于 WireGuard Portal",
|
||||||
|
"headline": "WireGuard Portal",
|
||||||
|
"content": "WireGuard Portal 是一个简单的基于网页的 WireGuard 配置平台。",
|
||||||
|
"button": "更多"
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"headline": "VPN 配置文件",
|
||||||
|
"abstract": "您可以通过您的用户个人资料访问并下载个人 VPN 配置。",
|
||||||
|
"content": "要查找您所有的配置文件,请点击下面的按钮。",
|
||||||
|
"button": "打开我的个人资料"
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"headline": "后台管理",
|
||||||
|
"abstract": "在后台管理,您可以管理 WireGuard 节点和服务器接口,以及允许登录 WireGuard Portal 的用户。",
|
||||||
|
"content": "",
|
||||||
|
"button-admin": "打开服务器管理",
|
||||||
|
"button-user": "打开用户管理"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"interfaces": {
|
||||||
|
"headline": "接口管理",
|
||||||
|
"headline-peers": "当前 VPN 节点",
|
||||||
|
"headline-endpoints": "当前节点",
|
||||||
|
"no-interface": {
|
||||||
|
"default-selection": "没有可用接口",
|
||||||
|
"headline": "未找到接口...",
|
||||||
|
"abstract": "点击上面的加号按钮以创建新的 WireGuard 接口。"
|
||||||
|
},
|
||||||
|
"no-peer": {
|
||||||
|
"headline": "没有可用节点",
|
||||||
|
"abstract": "当前没有可用的节点与所选的 WireGuard 接口关联。"
|
||||||
|
},
|
||||||
|
"table-heading": {
|
||||||
|
"name": "名称",
|
||||||
|
"user": "用户",
|
||||||
|
"ip": "IP 地址",
|
||||||
|
"endpoint": "节点",
|
||||||
|
"status": "状态"
|
||||||
|
},
|
||||||
|
"interface": {
|
||||||
|
"headline": "接口状态",
|
||||||
|
"backend": "后端",
|
||||||
|
"key": "公钥",
|
||||||
|
"endpoint": "公开节点",
|
||||||
|
"port": "监听端口",
|
||||||
|
"peers": "启用节点",
|
||||||
|
"total-peers": "节点总数",
|
||||||
|
"endpoints": "启用节点",
|
||||||
|
"total-endpoints": "节点总数",
|
||||||
|
"ip": "IP 地址",
|
||||||
|
"default-allowed-ip": "默认允许的 IP",
|
||||||
|
"dns": "DNS 服务器",
|
||||||
|
"mtu": "MTU",
|
||||||
|
"default-keep-alive": "默认心跳包间隔",
|
||||||
|
"button-show-config": "显示配置",
|
||||||
|
"button-download-config": "下载配置",
|
||||||
|
"button-store-config": "为 wg-quick 保存配置",
|
||||||
|
"button-edit": "编辑接口"
|
||||||
|
},
|
||||||
|
"button-add-interface": "添加接口",
|
||||||
|
"button-add-peer": "添加节点",
|
||||||
|
"button-add-peers": "添加多个节点",
|
||||||
|
"button-show-peer": "显示节点",
|
||||||
|
"button-edit-peer": "编辑节点",
|
||||||
|
"peer-disabled": "节点已禁用,原因: ",
|
||||||
|
"peer-expiring": "节点将在以下时间过期: ",
|
||||||
|
"peer-connected": "已连接",
|
||||||
|
"peer-not-connected": "未连接",
|
||||||
|
"peer-handshake": "最后一次握手: ",
|
||||||
|
"peer-dynamic": "动态对等节点",
|
||||||
|
"peer-dynamic-no-edit": "此对等节点是动态创建的,无法编辑或删除。"
|
||||||
|
},
|
||||||
|
"users": {
|
||||||
|
"headline": "用户管理",
|
||||||
|
"table-heading": {
|
||||||
|
"id": "ID",
|
||||||
|
"email": "电子邮件",
|
||||||
|
"firstname": "名",
|
||||||
|
"lastname": "姓",
|
||||||
|
"sources": "来源",
|
||||||
|
"peers": "节点",
|
||||||
|
"admin": "管理员"
|
||||||
|
},
|
||||||
|
"no-user": {
|
||||||
|
"headline": "没有可用用户",
|
||||||
|
"abstract": "当前没有用户注册 WireGuard 门户。"
|
||||||
|
},
|
||||||
|
"button-add-user": "添加用户",
|
||||||
|
"button-show-user": "显示用户",
|
||||||
|
"button-edit-user": "编辑用户",
|
||||||
|
"user-disabled": "用户已禁用, 原因: ",
|
||||||
|
"user-locked": "账户已锁定, 原因: ",
|
||||||
|
"admin": "用户具有管理员权限",
|
||||||
|
"no-admin": "用户没有管理员权限"
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"headline": "我的 VPN 节点列表",
|
||||||
|
"table-heading": {
|
||||||
|
"name": "名称",
|
||||||
|
"ip": "IP 地址",
|
||||||
|
"stats": "状态",
|
||||||
|
"interface": "服务器接口"
|
||||||
|
},
|
||||||
|
"no-peer": {
|
||||||
|
"headline": "没有可用节点",
|
||||||
|
"abstract": "当前没有与您的用户个人资料关联的节点。"
|
||||||
|
},
|
||||||
|
"peer-connected": "已连接",
|
||||||
|
"button-add-peer": "添加节点",
|
||||||
|
"button-show-peer": "显示节点",
|
||||||
|
"button-edit-peer": "编辑节点"
|
||||||
|
},
|
||||||
|
"modals": {
|
||||||
|
"user-view": {
|
||||||
|
"headline": "用户账户: ",
|
||||||
|
"tab-user": "信息",
|
||||||
|
"tab-peers": "节点",
|
||||||
|
"headline-info": "用户信息: ",
|
||||||
|
"headline-notes": "备注: ",
|
||||||
|
"email": "电子邮件",
|
||||||
|
"firstname": "名",
|
||||||
|
"lastname": "姓",
|
||||||
|
"phone": "电话号码",
|
||||||
|
"department": "部门",
|
||||||
|
"disabled": "账户已禁用",
|
||||||
|
"locked": "账户已锁定",
|
||||||
|
"no-peers": "用户没有关联的节点。",
|
||||||
|
"peers": {
|
||||||
|
"name": "名称",
|
||||||
|
"interface": "接口",
|
||||||
|
"ip": "IP 地址"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"user-edit": {
|
||||||
|
"headline-edit": "编辑用户: ",
|
||||||
|
"headline-new": "新用户",
|
||||||
|
"header-general": "常规",
|
||||||
|
"header-personal": "用户信息",
|
||||||
|
"header-notes": "备注",
|
||||||
|
"header-state": "状态",
|
||||||
|
"identifier": {
|
||||||
|
"label": "标识符",
|
||||||
|
"placeholder": "唯一用户标识符"
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"label": "来源",
|
||||||
|
"placeholder": "用户来源"
|
||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"label": "密码",
|
"label": "密码",
|
||||||
"placeholder": "请输入密码"
|
"placeholder": "一个复杂的密码",
|
||||||
|
"description": "留空保持当前密码不变。"
|
||||||
},
|
},
|
||||||
"button": "登录"
|
"email": {
|
||||||
},
|
"label": "电子邮件",
|
||||||
"menu": {
|
"placeholder": "电子邮件地址"
|
||||||
"home": "首页",
|
|
||||||
"interfaces": "接口",
|
|
||||||
"users": "用户",
|
|
||||||
"lang": "切换语言",
|
|
||||||
"profile": "个人资料",
|
|
||||||
"login": "登录",
|
|
||||||
"logout": "注销"
|
|
||||||
},
|
|
||||||
"home": {
|
|
||||||
"headline": "WireGuard® VPN Portal",
|
|
||||||
"info-headline": "更多信息",
|
|
||||||
"abstract": "WireGuard® 是一种极其简单但又快速现代的 VPN,采用先进的加密技术。它旨在比 IPsec 更快、更简单、更轻量、更实用,同时避免了大量的麻烦。它的性能显著优于 OpenVPN。",
|
|
||||||
"installation": {
|
|
||||||
"box-header": "WireGuard 安装",
|
|
||||||
"headline": "安装",
|
|
||||||
"content": "客户端软件的安装说明可在官方 WireGuard 网站上找到。",
|
|
||||||
"button": "打开说明"
|
|
||||||
},
|
},
|
||||||
"about-wg": {
|
"phone": {
|
||||||
"box-header": "关于 WireGuard",
|
"label": "电话",
|
||||||
"headline": "关于",
|
"placeholder": "电话号码"
|
||||||
"content": "WireGuard® 是一种极其简单但又快速现代的 VPN,采用先进的加密技术。",
|
|
||||||
"button": "更多"
|
|
||||||
},
|
},
|
||||||
"about-portal": {
|
"department": {
|
||||||
"box-header": "关于 WireGuard Portal",
|
"label": "部门",
|
||||||
"headline": "WireGuard Portal",
|
"placeholder": "部门"
|
||||||
"content": "WireGuard Portal 是一个简单的基于网页的 WireGuard 配置平台。",
|
|
||||||
"button": "更多"
|
|
||||||
},
|
},
|
||||||
"profiles": {
|
"firstname": {
|
||||||
"headline": "VPN 配置文件",
|
"label": "名",
|
||||||
"abstract": "您可以通过您的用户个人资料访问并下载个人 VPN 配置。",
|
"placeholder": "名"
|
||||||
"content": "要查找您所有的配置文件,请点击下面的按钮。",
|
},
|
||||||
"button": "打开我的个人资料"
|
"lastname": {
|
||||||
|
"label": "姓",
|
||||||
|
"placeholder": "姓"
|
||||||
|
},
|
||||||
|
"notes": {
|
||||||
|
"label": "备注",
|
||||||
|
"placeholder": ""
|
||||||
|
},
|
||||||
|
"disabled": {
|
||||||
|
"label": "禁用 (无法连接 WireGuard 和登录)"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"label": "锁定 (无法登录,但 WireGuard 仍然可以连接)"
|
||||||
},
|
},
|
||||||
"admin": {
|
"admin": {
|
||||||
"headline": "后台管理",
|
"label": "管理员"
|
||||||
"abstract": "在后台管理,您可以管理 WireGuard 节点和服务器接口,以及允许登录 WireGuard Portal 的用户。",
|
|
||||||
"content": "",
|
|
||||||
"button-admin": "打开服务器管理",
|
|
||||||
"button-user": "打开用户管理"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"interfaces": {
|
"confirm-delete": "确定要删除用户“{id}”吗?",
|
||||||
"headline": "接口管理",
|
"interface-view": {
|
||||||
"headline-peers": "当前 VPN 节点",
|
"headline": "接口配置: "
|
||||||
"headline-endpoints": "当前节点",
|
|
||||||
"no-interface": {
|
|
||||||
"default-selection": "没有可用接口",
|
|
||||||
"headline": "未找到接口...",
|
|
||||||
"abstract": "点击上面的加号按钮以创建新的 WireGuard 接口。"
|
|
||||||
},
|
|
||||||
"no-peer": {
|
|
||||||
"headline": "没有可用节点",
|
|
||||||
"abstract": "当前没有可用的节点与所选的 WireGuard 接口关联。"
|
|
||||||
},
|
|
||||||
"table-heading": {
|
|
||||||
"name": "名称",
|
|
||||||
"user": "用户",
|
|
||||||
"ip": "IP 地址",
|
|
||||||
"endpoint": "节点",
|
|
||||||
"status": "状态"
|
|
||||||
},
|
|
||||||
"interface": {
|
|
||||||
"headline": "接口状态",
|
|
||||||
"backend": "后端",
|
|
||||||
"key": "公钥",
|
|
||||||
"endpoint": "公开节点",
|
|
||||||
"port": "监听端口",
|
|
||||||
"peers": "启用节点",
|
|
||||||
"total-peers": "节点总数",
|
|
||||||
"endpoints": "启用节点",
|
|
||||||
"total-endpoints": "节点总数",
|
|
||||||
"ip": "IP 地址",
|
|
||||||
"default-allowed-ip": "默认允许的 IP",
|
|
||||||
"dns": "DNS 服务器",
|
|
||||||
"mtu": "MTU",
|
|
||||||
"default-keep-alive": "默认心跳包间隔",
|
|
||||||
"button-show-config": "显示配置",
|
|
||||||
"button-download-config": "下载配置",
|
|
||||||
"button-store-config": "为 wg-quick 保存配置",
|
|
||||||
"button-edit": "编辑接口"
|
|
||||||
},
|
|
||||||
"button-add-interface": "添加接口",
|
|
||||||
"button-add-peer": "添加节点",
|
|
||||||
"button-add-peers": "添加多个节点",
|
|
||||||
"button-show-peer": "显示节点",
|
|
||||||
"button-edit-peer": "编辑节点",
|
|
||||||
"peer-disabled": "节点已禁用,原因: ",
|
|
||||||
"peer-expiring": "节点将在以下时间过期: ",
|
|
||||||
"peer-connected": "已连接",
|
|
||||||
"peer-not-connected": "未连接",
|
|
||||||
"peer-handshake": "最后一次握手: "
|
|
||||||
},
|
},
|
||||||
"users": {
|
"interface-edit": {
|
||||||
"headline": "用户管理",
|
"headline-edit": "编辑接口: ",
|
||||||
"table-heading": {
|
"headline-new": "新接口",
|
||||||
"id": "ID",
|
"tab-interface": "接口",
|
||||||
"email": "电子邮件",
|
"tab-peerdef": "节点默认值",
|
||||||
"firstname": "名",
|
"header-general": "常规",
|
||||||
"lastname": "姓",
|
"header-network": "网络",
|
||||||
"sources": "来源",
|
"header-crypto": "加密",
|
||||||
"peers": "节点",
|
"header-hooks": "接口 Hooks",
|
||||||
"admin": "管理员"
|
"header-peer-hooks": "Hooks",
|
||||||
|
"header-state": "状态",
|
||||||
|
"identifier": {
|
||||||
|
"label": "标识符",
|
||||||
|
"placeholder": "唯一接口标识符"
|
||||||
},
|
},
|
||||||
"no-user": {
|
"mode": {
|
||||||
"headline": "没有可用用户",
|
"label": "接口模式",
|
||||||
"abstract": "当前没有用户注册 WireGuard 门户。"
|
"server": "服务器模式",
|
||||||
|
"client": "客户端模式",
|
||||||
|
"any": "未知模式"
|
||||||
},
|
},
|
||||||
"button-add-user": "添加用户",
|
"display-name": {
|
||||||
"button-show-user": "显示用户",
|
"label": "显示名称",
|
||||||
"button-edit-user": "编辑用户",
|
"placeholder": "接口的描述性名称"
|
||||||
"user-disabled": "用户已禁用, 原因: ",
|
|
||||||
"user-locked": "账户已锁定, 原因: ",
|
|
||||||
"admin": "用户具有管理员权限",
|
|
||||||
"no-admin": "用户没有管理员权限"
|
|
||||||
},
|
|
||||||
"profile": {
|
|
||||||
"headline": "我的 VPN 节点列表",
|
|
||||||
"table-heading": {
|
|
||||||
"name": "名称",
|
|
||||||
"ip": "IP 地址",
|
|
||||||
"stats": "状态",
|
|
||||||
"interface": "服务器接口"
|
|
||||||
},
|
},
|
||||||
"no-peer": {
|
"private-key": {
|
||||||
"headline": "没有可用节点",
|
"label": "私钥",
|
||||||
"abstract": "当前没有与您的用户个人资料关联的节点。"
|
"placeholder": "私钥"
|
||||||
},
|
},
|
||||||
"peer-connected": "已连接",
|
"public-key": {
|
||||||
"button-add-peer": "添加节点",
|
"label": "公钥",
|
||||||
"button-show-peer": "显示节点",
|
"placeholder": "公钥"
|
||||||
"button-edit-peer": "编辑节点"
|
|
||||||
},
|
|
||||||
"modals": {
|
|
||||||
"user-view": {
|
|
||||||
"headline": "用户账户: ",
|
|
||||||
"tab-user": "信息",
|
|
||||||
"tab-peers": "节点",
|
|
||||||
"headline-info": "用户信息: ",
|
|
||||||
"headline-notes": "备注: ",
|
|
||||||
"email": "电子邮件",
|
|
||||||
"firstname": "名",
|
|
||||||
"lastname": "姓",
|
|
||||||
"phone": "电话号码",
|
|
||||||
"department": "部门",
|
|
||||||
"disabled": "账户已禁用",
|
|
||||||
"locked": "账户已锁定",
|
|
||||||
"no-peers": "用户没有关联的节点。",
|
|
||||||
"peers": {
|
|
||||||
"name": "名称",
|
|
||||||
"interface": "接口",
|
|
||||||
"ip": "IP 地址"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"user-edit": {
|
"ip": {
|
||||||
"headline-edit": "编辑用户: ",
|
"label": "IP 地址",
|
||||||
"headline-new": "新用户",
|
"placeholder": "IP 地址 (CIDR 格式)"
|
||||||
"header-general": "常规",
|
|
||||||
"header-personal": "用户信息",
|
|
||||||
"header-notes": "备注",
|
|
||||||
"header-state": "状态",
|
|
||||||
"identifier": {
|
|
||||||
"label": "标识符",
|
|
||||||
"placeholder": "唯一用户标识符"
|
|
||||||
},
|
|
||||||
"source": {
|
|
||||||
"label": "来源",
|
|
||||||
"placeholder": "用户来源"
|
|
||||||
},
|
|
||||||
"password": {
|
|
||||||
"label": "密码",
|
|
||||||
"placeholder": "一个复杂的密码",
|
|
||||||
"description": "留空保持当前密码不变。"
|
|
||||||
},
|
|
||||||
"email": {
|
|
||||||
"label": "电子邮件",
|
|
||||||
"placeholder": "电子邮件地址"
|
|
||||||
},
|
|
||||||
"phone": {
|
|
||||||
"label": "电话",
|
|
||||||
"placeholder": "电话号码"
|
|
||||||
},
|
|
||||||
"department": {
|
|
||||||
"label": "部门",
|
|
||||||
"placeholder": "部门"
|
|
||||||
},
|
|
||||||
"firstname": {
|
|
||||||
"label": "名",
|
|
||||||
"placeholder": "名"
|
|
||||||
},
|
|
||||||
"lastname": {
|
|
||||||
"label": "姓",
|
|
||||||
"placeholder": "姓"
|
|
||||||
},
|
|
||||||
"notes": {
|
|
||||||
"label": "备注",
|
|
||||||
"placeholder": ""
|
|
||||||
},
|
|
||||||
"disabled": {
|
|
||||||
"label": "禁用 (无法连接 WireGuard 和登录)"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"label": "锁定 (无法登录,但 WireGuard 仍然可以连接)"
|
|
||||||
},
|
|
||||||
"admin": {
|
|
||||||
"label": "管理员"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"confirm-delete": "确定要删除用户“{id}”吗?",
|
"listen-port": {
|
||||||
"interface-view": {
|
"label": "监听端口",
|
||||||
"headline": "接口配置: "
|
"placeholder": "监听端口"
|
||||||
},
|
},
|
||||||
"interface-edit": {
|
"dns": {
|
||||||
"headline-edit": "编辑接口: ",
|
"label": "DNS 服务器",
|
||||||
"headline-new": "新接口",
|
"placeholder": "应使用的 DNS 服务器"
|
||||||
"tab-interface": "接口",
|
|
||||||
"tab-peerdef": "节点默认值",
|
|
||||||
"header-general": "常规",
|
|
||||||
"header-network": "网络",
|
|
||||||
"header-crypto": "加密",
|
|
||||||
"header-hooks": "接口 Hooks",
|
|
||||||
"header-peer-hooks": "Hooks",
|
|
||||||
"header-state": "状态",
|
|
||||||
"identifier": {
|
|
||||||
"label": "标识符",
|
|
||||||
"placeholder": "唯一接口标识符"
|
|
||||||
},
|
|
||||||
"mode": {
|
|
||||||
"label": "接口模式",
|
|
||||||
"server": "服务器模式",
|
|
||||||
"client": "客户端模式",
|
|
||||||
"any": "未知模式"
|
|
||||||
},
|
|
||||||
"display-name": {
|
|
||||||
"label": "显示名称",
|
|
||||||
"placeholder": "接口的描述性名称"
|
|
||||||
},
|
|
||||||
"private-key": {
|
|
||||||
"label": "私钥",
|
|
||||||
"placeholder": "私钥"
|
|
||||||
},
|
|
||||||
"public-key": {
|
|
||||||
"label": "公钥",
|
|
||||||
"placeholder": "公钥"
|
|
||||||
},
|
|
||||||
"ip": {
|
|
||||||
"label": "IP 地址",
|
|
||||||
"placeholder": "IP 地址 (CIDR 格式)"
|
|
||||||
},
|
|
||||||
"listen-port": {
|
|
||||||
"label": "监听端口",
|
|
||||||
"placeholder": "监听端口"
|
|
||||||
},
|
|
||||||
"dns": {
|
|
||||||
"label": "DNS 服务器",
|
|
||||||
"placeholder": "应使用的 DNS 服务器"
|
|
||||||
},
|
|
||||||
"dns-search": {
|
|
||||||
"label": "DNS 搜索域",
|
|
||||||
"placeholder": "DNS 搜索前缀"
|
|
||||||
},
|
|
||||||
"mtu": {
|
|
||||||
"label": "MTU",
|
|
||||||
"placeholder": "接口 MTU (0 = 保持默认)"
|
|
||||||
},
|
|
||||||
"firewall-mark": {
|
|
||||||
"label": "防火墙掩码",
|
|
||||||
"placeholder": "应用于出站流量的防火墙掩码 (0 = 自动)"
|
|
||||||
},
|
|
||||||
"routing-table": {
|
|
||||||
"label": "路由表",
|
|
||||||
"placeholder": "路由表 ID",
|
|
||||||
"description": "特殊情况: off = 不管理路由, 0 = 自动"
|
|
||||||
},
|
|
||||||
"pre-up": {
|
|
||||||
"label": "启动前脚本",
|
|
||||||
"placeholder": "一个或多个用分号分隔的 bash 命令"
|
|
||||||
},
|
|
||||||
"post-up": {
|
|
||||||
"label": "启动后脚本",
|
|
||||||
"placeholder": "一个或多个用分号分隔的 bash 命令"
|
|
||||||
},
|
|
||||||
"pre-down": {
|
|
||||||
"label": "停止前脚本",
|
|
||||||
"placeholder": "一个或多个用分号分隔的 bash 命令"
|
|
||||||
},
|
|
||||||
"post-down": {
|
|
||||||
"label": "停止后脚本",
|
|
||||||
"placeholder": "一个或多个用分号分隔的 bash 命令"
|
|
||||||
},
|
|
||||||
"disabled": {
|
|
||||||
"label": "接口已禁用"
|
|
||||||
},
|
|
||||||
"save-config": {
|
|
||||||
"label": "自动保存 wg-quick 配置"
|
|
||||||
},
|
|
||||||
"defaults": {
|
|
||||||
"endpoint": {
|
|
||||||
"label": "服务器地址",
|
|
||||||
"placeholder": "服务器地址",
|
|
||||||
"description": "节点将连接到服务器的地址。"
|
|
||||||
},
|
|
||||||
"networks": {
|
|
||||||
"label": "IP 地址",
|
|
||||||
"placeholder": "IP 地址",
|
|
||||||
"description": "节点将从这些子网获取 IP 地址。"
|
|
||||||
},
|
|
||||||
"allowed-ip": {
|
|
||||||
"label": "允许的 IP 地址",
|
|
||||||
"placeholder": "默认允许的 IP 地址"
|
|
||||||
},
|
|
||||||
"mtu": {
|
|
||||||
"label": "MTU",
|
|
||||||
"placeholder": "客户端 MTU (0 = 保持默认)"
|
|
||||||
},
|
|
||||||
"keep-alive": {
|
|
||||||
"label": "心跳包间隔",
|
|
||||||
"placeholder": "持久保持连接 (0 = 默认)"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"button-apply-defaults": "应用节点默认值",
|
|
||||||
"button-create-default-peers": "创建默认节点",
|
|
||||||
"confirm-delete": "确定要删除接口“{id}”吗?"
|
|
||||||
},
|
},
|
||||||
"peer-view": {
|
"dns-search": {
|
||||||
"headline-peer": "节点: ",
|
"label": "DNS 搜索域",
|
||||||
"headline-endpoint": "节点: ",
|
"placeholder": "DNS 搜索前缀"
|
||||||
"section-info": "节点信息",
|
|
||||||
"section-status": "当前状态",
|
|
||||||
"section-config": "配置",
|
|
||||||
"identifier": "标识符",
|
|
||||||
"ip": "IP 地址",
|
|
||||||
"user": "关联用户",
|
|
||||||
"notes": "备注",
|
|
||||||
"expiry-status": "过期时间",
|
|
||||||
"disabled-status": "禁用时间",
|
|
||||||
"traffic": "流量",
|
|
||||||
"connection-status": "连接状态",
|
|
||||||
"upload": "上传字节 (服务器到节点)",
|
|
||||||
"download": "下载字节 (节点到服务器)",
|
|
||||||
"pingable": "连通状态",
|
|
||||||
"handshake": "最后握手",
|
|
||||||
"connected-since": "首次成功连接",
|
|
||||||
"endpoint": "节点地址",
|
|
||||||
"button-download": "下载配置",
|
|
||||||
"button-email": "通过电子邮件发送配置"
|
|
||||||
},
|
},
|
||||||
"peer-edit": {
|
"mtu": {
|
||||||
"headline-edit-peer": "编辑节点: ",
|
"label": "MTU",
|
||||||
"headline-edit-endpoint": "编辑节点: ",
|
"placeholder": "接口 MTU (0 = 保持默认)"
|
||||||
"headline-new-peer": "创建节点",
|
},
|
||||||
"headline-new-endpoint": "创建节点",
|
"firewall-mark": {
|
||||||
"header-general": "常规",
|
"label": "防火墙掩码",
|
||||||
"header-network": "网络",
|
"placeholder": "应用于出站流量的防火墙掩码 (0 = 自动)"
|
||||||
"header-crypto": "加密",
|
},
|
||||||
"header-hooks": "Hooks (在节点执行)",
|
"routing-table": {
|
||||||
"header-state": "状态",
|
"label": "路由表",
|
||||||
"display-name": {
|
"placeholder": "路由表 ID",
|
||||||
"label": "显示名称",
|
"description": "特殊情况: off = 不管理路由, 0 = 自动"
|
||||||
"placeholder": "节点的描述性名称"
|
},
|
||||||
},
|
"pre-up": {
|
||||||
"linked-user": {
|
"label": "启动前脚本",
|
||||||
"label": "关联用户",
|
"placeholder": "一个或多个用分号分隔的 bash 命令"
|
||||||
"placeholder": "拥有此节点的用户账户"
|
},
|
||||||
},
|
"post-up": {
|
||||||
"private-key": {
|
"label": "启动后脚本",
|
||||||
"label": "私钥",
|
"placeholder": "一个或多个用分号分隔的 bash 命令"
|
||||||
"placeholder": "私钥"
|
},
|
||||||
},
|
"pre-down": {
|
||||||
"public-key": {
|
"label": "停止前脚本",
|
||||||
"label": "公钥",
|
"placeholder": "一个或多个用分号分隔的 bash 命令"
|
||||||
"placeholder": "公钥"
|
},
|
||||||
},
|
"post-down": {
|
||||||
"preshared-key": {
|
"label": "停止后脚本",
|
||||||
"label": "预共享密钥",
|
"placeholder": "一个或多个用分号分隔的 bash 命令"
|
||||||
"placeholder": "可选的预共享密钥"
|
},
|
||||||
},
|
"disabled": {
|
||||||
"endpoint-public-key": {
|
"label": "接口已禁用"
|
||||||
"label": "节点公钥",
|
},
|
||||||
"placeholder": "远程节点的公钥"
|
"save-config": {
|
||||||
},
|
"label": "自动保存 wg-quick 配置"
|
||||||
|
},
|
||||||
|
"defaults": {
|
||||||
"endpoint": {
|
"endpoint": {
|
||||||
"label": "节点地址",
|
"label": "服务器地址",
|
||||||
"placeholder": "远程节点的地址"
|
"placeholder": "服务器地址",
|
||||||
|
"description": "节点将连接到服务器的地址。"
|
||||||
},
|
},
|
||||||
"ip": {
|
"networks": {
|
||||||
"label": "IP 地址",
|
"label": "IP 地址",
|
||||||
"placeholder": "IP 地址(CIDR 格式)"
|
"placeholder": "IP 地址",
|
||||||
|
"description": "节点将从这些子网获取 IP 地址。"
|
||||||
},
|
},
|
||||||
"allowed-ip": {
|
"allowed-ip": {
|
||||||
"label": "允许的 IP 地址",
|
"label": "允许的 IP 地址",
|
||||||
"placeholder": "允许的 IP 地址(CIDR 格式)"
|
"placeholder": "默认允许的 IP 地址"
|
||||||
},
|
|
||||||
"extra-allowed-ip": {
|
|
||||||
"label": "额外允许的 IP 地址",
|
|
||||||
"placeholder": "额外允许的 IP 地址(服务器端)",
|
|
||||||
"description": "这些 IP 将作为允许的 IP 添加到远程 WireGuard 接口。"
|
|
||||||
},
|
|
||||||
"dns": {
|
|
||||||
"label": "DNS 服务器",
|
|
||||||
"placeholder": "要使用的 DNS 服务器"
|
|
||||||
},
|
|
||||||
"dns-search": {
|
|
||||||
"label": "DNS 搜索域",
|
|
||||||
"placeholder": "DNS 搜索前缀"
|
|
||||||
},
|
|
||||||
"keep-alive": {
|
|
||||||
"label": "心跳包间隔",
|
|
||||||
"placeholder": "持久保持连接 (0 = 默认)"
|
|
||||||
},
|
},
|
||||||
"mtu": {
|
"mtu": {
|
||||||
"label": "MTU",
|
"label": "MTU",
|
||||||
"placeholder": "客户端 MTU (0 = 保持默认)"
|
"placeholder": "客户端 MTU (0 = 保持默认)"
|
||||||
},
|
},
|
||||||
"pre-up": {
|
"keep-alive": {
|
||||||
"label": "启动前脚本",
|
"label": "心跳包间隔",
|
||||||
"placeholder": "一个或多个用分号分隔的 bash 命令"
|
"placeholder": "持久保持连接 (0 = 默认)"
|
||||||
},
|
|
||||||
"post-up": {
|
|
||||||
"label": "启动后脚本",
|
|
||||||
"placeholder": "一个或多个用分号分隔的 bash 命令"
|
|
||||||
},
|
|
||||||
"pre-down": {
|
|
||||||
"label": "停止前脚本",
|
|
||||||
"placeholder": "一个或多个用分号分隔的 bash 命令"
|
|
||||||
},
|
|
||||||
"post-down": {
|
|
||||||
"label": "停止后脚本",
|
|
||||||
"placeholder": "一个或多个用分号分隔的 bash 命令"
|
|
||||||
},
|
|
||||||
"disabled": {
|
|
||||||
"label": "节点已禁用"
|
|
||||||
},
|
|
||||||
"ignore-global": {
|
|
||||||
"label": "忽略全局设置"
|
|
||||||
},
|
|
||||||
"expires-at": {
|
|
||||||
"label": "过期日期"
|
|
||||||
},
|
|
||||||
"confirm-delete": "确定要删除对等点“{id}”吗?"
|
|
||||||
},
|
|
||||||
"peer-multi-create": {
|
|
||||||
"headline-peer": "创建多个节点",
|
|
||||||
"headline-endpoint": "创建多个节点",
|
|
||||||
"identifiers": {
|
|
||||||
"label": "用户标识符",
|
|
||||||
"placeholder": "用户标识符",
|
|
||||||
"description": "要为其创建节点 的用户标识符(用户名)。"
|
|
||||||
},
|
|
||||||
"prefix": {
|
|
||||||
"headline-peer": "节点: ",
|
|
||||||
"headline-endpoint": "节点: ",
|
|
||||||
"label": "显示名称前缀",
|
|
||||||
"placeholder": "前缀",
|
|
||||||
"description": "添加到节点 显示名称的前缀。"
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"button-apply-defaults": "应用节点默认值",
|
||||||
|
"button-create-default-peers": "创建默认节点",
|
||||||
|
"confirm-delete": "确定要删除接口“{id}”吗?"
|
||||||
|
},
|
||||||
|
"peer-view": {
|
||||||
|
"headline-peer": "节点: ",
|
||||||
|
"headline-endpoint": "节点: ",
|
||||||
|
"section-info": "节点信息",
|
||||||
|
"section-status": "当前状态",
|
||||||
|
"section-config": "配置",
|
||||||
|
"identifier": "标识符",
|
||||||
|
"ip": "IP 地址",
|
||||||
|
"user": "关联用户",
|
||||||
|
"notes": "备注",
|
||||||
|
"expiry-status": "过期时间",
|
||||||
|
"disabled-status": "禁用时间",
|
||||||
|
"traffic": "流量",
|
||||||
|
"connection-status": "连接状态",
|
||||||
|
"upload": "上传字节 (服务器到节点)",
|
||||||
|
"download": "下载字节 (节点到服务器)",
|
||||||
|
"pingable": "连通状态",
|
||||||
|
"handshake": "最后握手",
|
||||||
|
"connected-since": "首次成功连接",
|
||||||
|
"endpoint": "节点地址",
|
||||||
|
"button-download": "下载配置",
|
||||||
|
"button-email": "通过电子邮件发送配置"
|
||||||
|
},
|
||||||
|
"peer-edit": {
|
||||||
|
"headline-edit-peer": "编辑节点: ",
|
||||||
|
"headline-edit-endpoint": "编辑节点: ",
|
||||||
|
"headline-new-peer": "创建节点",
|
||||||
|
"headline-new-endpoint": "创建节点",
|
||||||
|
"header-general": "常规",
|
||||||
|
"header-network": "网络",
|
||||||
|
"header-crypto": "加密",
|
||||||
|
"header-hooks": "Hooks (在节点执行)",
|
||||||
|
"header-state": "状态",
|
||||||
|
"display-name": {
|
||||||
|
"label": "显示名称",
|
||||||
|
"placeholder": "节点的描述性名称"
|
||||||
|
},
|
||||||
|
"linked-user": {
|
||||||
|
"label": "关联用户",
|
||||||
|
"placeholder": "拥有此节点的用户账户"
|
||||||
|
},
|
||||||
|
"private-key": {
|
||||||
|
"label": "私钥",
|
||||||
|
"placeholder": "私钥"
|
||||||
|
},
|
||||||
|
"public-key": {
|
||||||
|
"label": "公钥",
|
||||||
|
"placeholder": "公钥"
|
||||||
|
},
|
||||||
|
"preshared-key": {
|
||||||
|
"label": "预共享密钥",
|
||||||
|
"placeholder": "可选的预共享密钥"
|
||||||
|
},
|
||||||
|
"endpoint-public-key": {
|
||||||
|
"label": "节点公钥",
|
||||||
|
"placeholder": "远程节点的公钥"
|
||||||
|
},
|
||||||
|
"endpoint": {
|
||||||
|
"label": "节点地址",
|
||||||
|
"placeholder": "远程节点的地址"
|
||||||
|
},
|
||||||
|
"ip": {
|
||||||
|
"label": "IP 地址",
|
||||||
|
"placeholder": "IP 地址(CIDR 格式)"
|
||||||
|
},
|
||||||
|
"allowed-ip": {
|
||||||
|
"label": "允许的 IP 地址",
|
||||||
|
"placeholder": "允许的 IP 地址(CIDR 格式)"
|
||||||
|
},
|
||||||
|
"extra-allowed-ip": {
|
||||||
|
"label": "额外允许的 IP 地址",
|
||||||
|
"placeholder": "额外允许的 IP 地址(服务器端)",
|
||||||
|
"description": "这些 IP 将作为允许的 IP 添加到远程 WireGuard 接口。"
|
||||||
|
},
|
||||||
|
"dns": {
|
||||||
|
"label": "DNS 服务器",
|
||||||
|
"placeholder": "要使用的 DNS 服务器"
|
||||||
|
},
|
||||||
|
"dns-search": {
|
||||||
|
"label": "DNS 搜索域",
|
||||||
|
"placeholder": "DNS 搜索前缀"
|
||||||
|
},
|
||||||
|
"keep-alive": {
|
||||||
|
"label": "心跳包间隔",
|
||||||
|
"placeholder": "持久保持连接 (0 = 默认)"
|
||||||
|
},
|
||||||
|
"mtu": {
|
||||||
|
"label": "MTU",
|
||||||
|
"placeholder": "客户端 MTU (0 = 保持默认)"
|
||||||
|
},
|
||||||
|
"pre-up": {
|
||||||
|
"label": "启动前脚本",
|
||||||
|
"placeholder": "一个或多个用分号分隔的 bash 命令"
|
||||||
|
},
|
||||||
|
"post-up": {
|
||||||
|
"label": "启动后脚本",
|
||||||
|
"placeholder": "一个或多个用分号分隔的 bash 命令"
|
||||||
|
},
|
||||||
|
"pre-down": {
|
||||||
|
"label": "停止前脚本",
|
||||||
|
"placeholder": "一个或多个用分号分隔的 bash 命令"
|
||||||
|
},
|
||||||
|
"post-down": {
|
||||||
|
"label": "停止后脚本",
|
||||||
|
"placeholder": "一个或多个用分号分隔的 bash 命令"
|
||||||
|
},
|
||||||
|
"disabled": {
|
||||||
|
"label": "节点已禁用"
|
||||||
|
},
|
||||||
|
"ignore-global": {
|
||||||
|
"label": "忽略全局设置"
|
||||||
|
},
|
||||||
|
"expires-at": {
|
||||||
|
"label": "过期日期"
|
||||||
|
},
|
||||||
|
"confirm-delete": "确定要删除对等点“{id}”吗?"
|
||||||
|
},
|
||||||
|
"peer-multi-create": {
|
||||||
|
"headline-peer": "创建多个节点",
|
||||||
|
"headline-endpoint": "创建多个节点",
|
||||||
|
"identifiers": {
|
||||||
|
"label": "用户标识符",
|
||||||
|
"placeholder": "用户标识符",
|
||||||
|
"description": "要为其创建节点 的用户标识符(用户名)。"
|
||||||
|
},
|
||||||
|
"prefix": {
|
||||||
|
"headline-peer": "节点: ",
|
||||||
|
"headline-endpoint": "节点: ",
|
||||||
|
"label": "显示名称前缀",
|
||||||
|
"placeholder": "前缀",
|
||||||
|
"description": "添加到节点 显示名称的前缀。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
@@ -151,7 +151,9 @@ async function bulkDisable() {
|
|||||||
|
|
||||||
function toggleSelectAll() {
|
function toggleSelectAll() {
|
||||||
peers.FilteredAndPaged.forEach(peer => {
|
peers.FilteredAndPaged.forEach(peer => {
|
||||||
peer.IsSelected = selectAll.value;
|
if (!peer.IsDynamic) {
|
||||||
|
peer.IsSelected = selectAll.value;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -444,13 +446,17 @@ onMounted(async () => {
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="peer in peers.FilteredAndPaged" :key="peer.Identifier">
|
<tr v-for="peer in peers.FilteredAndPaged" :key="peer.Identifier">
|
||||||
<th scope="row">
|
<th scope="row">
|
||||||
<input class="form-check-input" type="checkbox" v-model="peer.IsSelected">
|
<input class="form-check-input" type="checkbox" v-model="peer.IsSelected" :disabled="peer.IsDynamic" :title="peer.IsDynamic ? $t('interfaces.peer-dynamic-no-edit') : ''">
|
||||||
</th>
|
</th>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<span v-if="peer.Disabled" class="text-danger" :title="$t('interfaces.peer-disabled') + ' ' + peer.DisabledReason"><i class="fa fa-circle-xmark"></i></span>
|
<span v-if="peer.Disabled" class="text-danger" :title="$t('interfaces.peer-disabled') + ' ' + peer.DisabledReason"><i class="fa fa-circle-xmark"></i></span>
|
||||||
<span v-if="!peer.Disabled && peer.ExpiresAt" class="text-warning" :title="$t('interfaces.peer-expiring') + ' ' + peer.ExpiresAt"><i class="fas fa-hourglass-end expiring-peer"></i></span>
|
<span v-if="!peer.Disabled && peer.ExpiresAt" class="text-warning" :title="$t('interfaces.peer-expiring') + ' ' + peer.ExpiresAt"><i class="fas fa-hourglass-end expiring-peer"></i></span>
|
||||||
</td>
|
</td>
|
||||||
<td><span v-if="peer.DisplayName" :title="peer.Identifier">{{peer.DisplayName}}</span><span v-else :title="peer.Identifier">{{ $filters.truncate(peer.Identifier, 10)}}</span></td>
|
<td>
|
||||||
|
<span v-if="peer.DisplayName" :title="peer.Identifier">{{peer.DisplayName}}</span>
|
||||||
|
<span v-else :title="peer.Identifier">{{ $filters.truncate(peer.Identifier, 10)}}</span>
|
||||||
|
<span v-if="peer.IsDynamic" class="badge bg-info-subtle text-info-emphasis ms-1 border border-info-subtle" :title="$t('interfaces.peer-dynamic')"><i class="fa-solid fa-robot me-1"></i>{{ $t('interfaces.peer-dynamic') }}</span>
|
||||||
|
</td>
|
||||||
<td><span :title="peer.UserDisplayName">{{peer.UserIdentifier}}</span></td>
|
<td><span :title="peer.UserDisplayName">{{peer.UserIdentifier}}</span></td>
|
||||||
<td>
|
<td>
|
||||||
<span v-for="ip in peer.Addresses" :key="ip" class="badge bg-light me-1">{{ ip }}</span>
|
<span v-for="ip in peer.Addresses" :key="ip" class="badge bg-light me-1">{{ ip }}</span>
|
||||||
@@ -474,7 +480,13 @@ onMounted(async () => {
|
|||||||
</td>
|
</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<a href="#" :title="$t('interfaces.button-show-peer')" @click.prevent="viewedPeerId=peer.Identifier"><i class="fas fa-eye me-2"></i></a>
|
<a href="#" :title="$t('interfaces.button-show-peer')" @click.prevent="viewedPeerId=peer.Identifier"><i class="fas fa-eye me-2"></i></a>
|
||||||
<a href="#" :title="$t('interfaces.button-edit-peer')" @click.prevent="editPeerId=peer.Identifier"><i class="fas fa-cog"></i></a>
|
|
||||||
|
<span v-if="peer.IsDynamic" :title="$t('interfaces.peer-dynamic-no-edit')" class="text-muted" style="cursor: not-allowed;">
|
||||||
|
<i class="fas fa-cog"></i>
|
||||||
|
</span>
|
||||||
|
<a v-else href="#" :title="$t('interfaces.button-edit-peer')" @click.prevent="editPeerId=peer.Identifier">
|
||||||
|
<i class="fas fa-cog"></i>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -296,7 +296,7 @@ func (c *MikrotikController) GetPeers(ctx context.Context, deviceId domain.Inter
|
|||||||
PropList: []string{
|
PropList: []string{
|
||||||
".id", "name", "allowed-address", "client-address", "client-endpoint", "client-keepalive", "comment",
|
".id", "name", "allowed-address", "client-address", "client-endpoint", "client-keepalive", "comment",
|
||||||
"current-endpoint-address", "current-endpoint-port", "last-handshake", "persistent-keepalive",
|
"current-endpoint-address", "current-endpoint-port", "last-handshake", "persistent-keepalive",
|
||||||
"public-key", "private-key", "preshared-key", "mtu", "disabled", "rx", "tx", "responder", "client-dns",
|
"public-key", "private-key", "preshared-key", "mtu", "disabled", "rx", "tx", "responder", "client-dns", "dynamic",
|
||||||
},
|
},
|
||||||
Filters: map[string]string{
|
Filters: map[string]string{
|
||||||
"interface": string(deviceId),
|
"interface": string(deviceId),
|
||||||
@@ -376,6 +376,7 @@ func (c *MikrotikController) convertWireGuardPeer(peer lowlevel.GenericJsonObjec
|
|||||||
Comment: peer.GetString("comment"),
|
Comment: peer.GetString("comment"),
|
||||||
IsResponder: peer.GetBool("responder"),
|
IsResponder: peer.GetBool("responder"),
|
||||||
Disabled: peer.GetBool("disabled"),
|
Disabled: peer.GetBool("disabled"),
|
||||||
|
Dynamic: peer.GetBool("dynamic"),
|
||||||
ClientEndpoint: peer.GetString("client-endpoint"),
|
ClientEndpoint: peer.GetString("client-endpoint"),
|
||||||
ClientAddress: peer.GetString("client-address"),
|
ClientAddress: peer.GetString("client-address"),
|
||||||
ClientDns: peer.GetString("client-dns"),
|
ClientDns: peer.GetString("client-dns"),
|
||||||
@@ -614,15 +615,24 @@ func (c *MikrotikController) SavePeer(
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
peerId := physicalPeer.GetExtras().(domain.MikrotikPeerExtras).Id
|
oldExtras := physicalPeer.GetExtras().(domain.MikrotikPeerExtras)
|
||||||
|
peerId := oldExtras.Id
|
||||||
|
dynamic := oldExtras.Dynamic
|
||||||
|
|
||||||
physicalPeer, err = updateFunc(physicalPeer)
|
physicalPeer, err = updateFunc(physicalPeer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
newExtras := physicalPeer.GetExtras().(domain.MikrotikPeerExtras)
|
newExtras := physicalPeer.GetExtras().(domain.MikrotikPeerExtras)
|
||||||
newExtras.Id = peerId // ensure the ID is not changed
|
newExtras.Id = peerId // ensure the ID is not changed
|
||||||
|
newExtras.Dynamic = dynamic // ensure the dynamic flag is preserved
|
||||||
physicalPeer.SetExtras(newExtras)
|
physicalPeer.SetExtras(newExtras)
|
||||||
|
|
||||||
|
if newExtras.Dynamic {
|
||||||
|
slog.Debug("skipping update for dynamic Mikrotik peer", "peer", id, "interface", deviceId)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if err := c.updatePeer(ctx, deviceId, physicalPeer); err != nil {
|
if err := c.updatePeer(ctx, deviceId, physicalPeer); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -638,7 +648,7 @@ func (c *MikrotikController) getOrCreatePeer(
|
|||||||
wgReply := c.client.Query(ctx, "/interface/wireguard/peers", &lowlevel.MikrotikRequestOptions{
|
wgReply := c.client.Query(ctx, "/interface/wireguard/peers", &lowlevel.MikrotikRequestOptions{
|
||||||
PropList: []string{
|
PropList: []string{
|
||||||
".id", "name", "public-key", "private-key", "preshared-key", "persistent-keepalive", "client-address",
|
".id", "name", "public-key", "private-key", "preshared-key", "persistent-keepalive", "client-address",
|
||||||
"client-endpoint", "client-keepalive", "allowed-address", "client-dns", "comment", "disabled", "responder",
|
"client-endpoint", "client-keepalive", "allowed-address", "client-dns", "comment", "disabled", "responder", "dynamic",
|
||||||
},
|
},
|
||||||
Filters: map[string]string{
|
Filters: map[string]string{
|
||||||
"public-key": string(id),
|
"public-key": string(id),
|
||||||
@@ -742,7 +752,7 @@ func (c *MikrotikController) DeletePeer(
|
|||||||
defer mutex.Unlock()
|
defer mutex.Unlock()
|
||||||
|
|
||||||
wgReply := c.client.Query(ctx, "/interface/wireguard/peers", &lowlevel.MikrotikRequestOptions{
|
wgReply := c.client.Query(ctx, "/interface/wireguard/peers", &lowlevel.MikrotikRequestOptions{
|
||||||
PropList: []string{".id"},
|
PropList: []string{".id", "dynamic"},
|
||||||
Filters: map[string]string{
|
Filters: map[string]string{
|
||||||
"public-key": string(id),
|
"public-key": string(id),
|
||||||
"interface": string(deviceId),
|
"interface": string(deviceId),
|
||||||
@@ -754,6 +764,10 @@ func (c *MikrotikController) DeletePeer(
|
|||||||
if len(wgReply.Data) == 0 {
|
if len(wgReply.Data) == 0 {
|
||||||
return nil // peer does not exist, nothing to delete
|
return nil // peer does not exist, nothing to delete
|
||||||
}
|
}
|
||||||
|
if wgReply.Data[0].GetBool("dynamic") {
|
||||||
|
slog.Debug("skipping deletion of dynamic Mikrotik peer", "peer", id, "interface", deviceId)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
peerId := wgReply.Data[0].GetString(".id")
|
peerId := wgReply.Data[0].GetString(".id")
|
||||||
deleteReply := c.client.Delete(ctx, "/interface/wireguard/peers/"+peerId)
|
deleteReply := c.client.Delete(ctx, "/interface/wireguard/peers/"+peerId)
|
||||||
@@ -769,12 +783,51 @@ func (c *MikrotikController) DeletePeer(
|
|||||||
// region wg-quick-related
|
// region wg-quick-related
|
||||||
|
|
||||||
func (c *MikrotikController) ExecuteInterfaceHook(
|
func (c *MikrotikController) ExecuteInterfaceHook(
|
||||||
_ context.Context,
|
ctx context.Context,
|
||||||
_ domain.InterfaceIdentifier,
|
id domain.InterfaceIdentifier,
|
||||||
_ string,
|
hookCmd string,
|
||||||
) error {
|
) error {
|
||||||
// TODO implement me
|
if hookCmd == "" {
|
||||||
slog.Error("interface hooks are not yet supported for Mikrotik backends, please open an issue on GitHub")
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
scriptName := fmt.Sprintf("wg-portal-hook-%s-%d", id, time.Now().UnixNano())
|
||||||
|
|
||||||
|
// Replace %i with the interface ID to mimic wg-quick behavior
|
||||||
|
scriptSource := strings.ReplaceAll(hookCmd, "%i", string(id))
|
||||||
|
// Inject the interface ID as a local variable for convenience in RouterOS scripts
|
||||||
|
scriptSource = fmt.Sprintf(":local WGInterface \"%s\";\n%s", id, scriptSource)
|
||||||
|
|
||||||
|
slog.Debug("executing Mikrotik script for interface hook", "interface", id, "script", scriptSource)
|
||||||
|
|
||||||
|
createReply := c.client.Create(ctx, "/system/script", lowlevel.GenericJsonObject{
|
||||||
|
"name": scriptName,
|
||||||
|
"source": scriptSource,
|
||||||
|
"policy": "ftp,reboot,read,write,policy,test,password,sniff,sensitive,romon",
|
||||||
|
})
|
||||||
|
if createReply.Status != lowlevel.MikrotikApiStatusOk {
|
||||||
|
return fmt.Errorf("failed to create temporary script for hook: %v", createReply.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
scriptId := createReply.Data.GetString(".id")
|
||||||
|
if scriptId == "" {
|
||||||
|
scriptId = scriptName
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
cleanupReply := c.client.Delete(ctx, "/system/script/"+scriptId)
|
||||||
|
if cleanupReply.Status != lowlevel.MikrotikApiStatusOk {
|
||||||
|
slog.Warn("failed to remove temporary Mikrotik script", "scriptId", scriptId, "error", cleanupReply.Error)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
runReply := c.client.ExecList(ctx, "/system/script/run", lowlevel.GenericJsonObject{
|
||||||
|
"number": scriptId,
|
||||||
|
})
|
||||||
|
if runReply.Status != lowlevel.MikrotikApiStatusOk {
|
||||||
|
return fmt.Errorf("failed to execute hook script %s: %v", scriptName, runReply.Error)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ func (e ConfigEndpoint) handleSettingsGet() http.HandlerFunc {
|
|||||||
names = append(names, model.SettingsBackendNames{
|
names = append(names, model.SettingsBackendNames{
|
||||||
Id: controller.Id,
|
Id: controller.Id,
|
||||||
Name: displayName,
|
Name: displayName,
|
||||||
|
Type: controller.Type,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,4 +20,5 @@ type Settings struct {
|
|||||||
type SettingsBackendNames struct {
|
type SettingsBackendNames struct {
|
||||||
Id string `json:"Id"`
|
Id string `json:"Id"`
|
||||||
Name string `json:"Name"`
|
Name string `json:"Name"`
|
||||||
|
Type string `json:"Type"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ type Interface struct {
|
|||||||
PublicKey string `json:"PublicKey" example:"abcdef=="` // public Key of the server interface
|
PublicKey string `json:"PublicKey" example:"abcdef=="` // public Key of the server interface
|
||||||
Disabled bool `json:"Disabled"` // flag that specifies if the interface is enabled (up) or not (down)
|
Disabled bool `json:"Disabled"` // flag that specifies if the interface is enabled (up) or not (down)
|
||||||
DisabledReason string `json:"DisabledReason"` // the reason why the interface has been disabled
|
DisabledReason string `json:"DisabledReason"` // the reason why the interface has been disabled
|
||||||
|
IsDynamic bool `json:"IsDynamic"` // specifies if the interface is a dynamic object
|
||||||
SaveConfig bool `json:"SaveConfig"` // automatically persist config changes to the wgX.conf file
|
SaveConfig bool `json:"SaveConfig"` // automatically persist config changes to the wgX.conf file
|
||||||
CreateDefaultPeer bool `json:"CreateDefaultPeer"` // if true, default peers will be created for this interface
|
CreateDefaultPeer bool `json:"CreateDefaultPeer"` // if true, default peers will be created for this interface
|
||||||
|
|
||||||
@@ -65,6 +66,7 @@ func NewInterface(src *domain.Interface, peers []domain.Peer) *Interface {
|
|||||||
PublicKey: src.PublicKey,
|
PublicKey: src.PublicKey,
|
||||||
Disabled: src.IsDisabled(),
|
Disabled: src.IsDisabled(),
|
||||||
DisabledReason: src.DisabledReason,
|
DisabledReason: src.DisabledReason,
|
||||||
|
IsDynamic: src.IsDynamic,
|
||||||
SaveConfig: src.SaveConfig,
|
SaveConfig: src.SaveConfig,
|
||||||
CreateDefaultPeer: src.CreateDefaultPeer,
|
CreateDefaultPeer: src.CreateDefaultPeer,
|
||||||
ListenPort: src.ListenPort,
|
ListenPort: src.ListenPort,
|
||||||
@@ -153,6 +155,7 @@ func NewDomainInterface(src *Interface) *domain.Interface {
|
|||||||
PreDown: src.PreDown,
|
PreDown: src.PreDown,
|
||||||
PostDown: src.PostDown,
|
PostDown: src.PostDown,
|
||||||
SaveConfig: src.SaveConfig,
|
SaveConfig: src.SaveConfig,
|
||||||
|
IsDynamic: src.IsDynamic,
|
||||||
CreateDefaultPeer: src.CreateDefaultPeer,
|
CreateDefaultPeer: src.CreateDefaultPeer,
|
||||||
DisplayName: src.DisplayName,
|
DisplayName: src.DisplayName,
|
||||||
Type: domain.InterfaceType(src.Mode),
|
Type: domain.InterfaceType(src.Mode),
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ type Peer struct {
|
|||||||
DisabledReason string `json:"DisabledReason"` // the reason why the peer has been disabled
|
DisabledReason string `json:"DisabledReason"` // the reason why the peer has been disabled
|
||||||
ExpiresAt ExpiryDate `json:"ExpiresAt,omitempty"` // expiry dates for peers
|
ExpiresAt ExpiryDate `json:"ExpiresAt,omitempty"` // expiry dates for peers
|
||||||
Notes string `json:"Notes"` // a note field for peers
|
Notes string `json:"Notes"` // a note field for peers
|
||||||
|
IsDynamic bool `json:"IsDynamic"` // specifies if the peer is dynamically created by mikrotik
|
||||||
|
|
||||||
Endpoint ConfigOption[string] `json:"Endpoint"` // the endpoint address
|
Endpoint ConfigOption[string] `json:"Endpoint"` // the endpoint address
|
||||||
EndpointPublicKey ConfigOption[string] `json:"EndpointPublicKey"` // the endpoint public key
|
EndpointPublicKey ConfigOption[string] `json:"EndpointPublicKey"` // the endpoint public key
|
||||||
@@ -90,6 +91,7 @@ func NewPeer(src *domain.Peer) *Peer {
|
|||||||
DisabledReason: src.DisabledReason,
|
DisabledReason: src.DisabledReason,
|
||||||
ExpiresAt: ExpiryDate{src.ExpiresAt},
|
ExpiresAt: ExpiryDate{src.ExpiresAt},
|
||||||
Notes: src.Notes,
|
Notes: src.Notes,
|
||||||
|
IsDynamic: src.IsDynamic,
|
||||||
Endpoint: ConfigOptionFromDomain(src.Endpoint),
|
Endpoint: ConfigOptionFromDomain(src.Endpoint),
|
||||||
EndpointPublicKey: ConfigOptionFromDomain(src.EndpointPublicKey),
|
EndpointPublicKey: ConfigOptionFromDomain(src.EndpointPublicKey),
|
||||||
AllowedIPs: StringSliceConfigOptionFromDomain(src.AllowedIPsStr),
|
AllowedIPs: StringSliceConfigOptionFromDomain(src.AllowedIPsStr),
|
||||||
@@ -148,6 +150,7 @@ func NewDomainPeer(src *Peer) *domain.Peer {
|
|||||||
InterfaceIdentifier: domain.InterfaceIdentifier(src.InterfaceIdentifier),
|
InterfaceIdentifier: domain.InterfaceIdentifier(src.InterfaceIdentifier),
|
||||||
Disabled: nil, // set below
|
Disabled: nil, // set below
|
||||||
DisabledReason: src.DisabledReason,
|
DisabledReason: src.DisabledReason,
|
||||||
|
IsDynamic: src.IsDynamic,
|
||||||
ExpiresAt: src.ExpiresAt.Time,
|
ExpiresAt: src.ExpiresAt.Time,
|
||||||
Notes: src.Notes,
|
Notes: src.Notes,
|
||||||
Interface: domain.PeerInterfaceConfig{
|
Interface: domain.PeerInterfaceConfig{
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ func (c *ControllerManager) registerLocalController() error {
|
|||||||
Config: config.BackendBase{
|
Config: config.BackendBase{
|
||||||
Id: config.LocalBackendName,
|
Id: config.LocalBackendName,
|
||||||
DisplayName: "Local WireGuard Controller",
|
DisplayName: "Local WireGuard Controller",
|
||||||
|
Type: "local",
|
||||||
IgnoredInterfaces: c.cfg.Backend.IgnoredLocalInterfaces,
|
IgnoredInterfaces: c.cfg.Backend.IgnoredLocalInterfaces,
|
||||||
},
|
},
|
||||||
Implementation: localController,
|
Implementation: localController,
|
||||||
@@ -82,6 +83,7 @@ func (c *ControllerManager) registerMikrotikControllers() error {
|
|||||||
return fmt.Errorf("failed to create Mikrotik controller for backend %s: %w", backendConfig.Id, err)
|
return fmt.Errorf("failed to create Mikrotik controller for backend %s: %w", backendConfig.Id, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
backendConfig.BackendBase.Type = "mikrotik"
|
||||||
c.controllers[domain.InterfaceBackend(backendConfig.Id)] = backendInstance{
|
c.controllers[domain.InterfaceBackend(backendConfig.Id)] = backendInstance{
|
||||||
Config: backendConfig.BackendBase,
|
Config: backendConfig.BackendBase,
|
||||||
Implementation: controller,
|
Implementation: controller,
|
||||||
@@ -102,6 +104,7 @@ func (c *ControllerManager) registerPfsenseControllers() error {
|
|||||||
return fmt.Errorf("failed to create pfSense controller for backend %s: %w", backendConfig.Id, err)
|
return fmt.Errorf("failed to create pfSense controller for backend %s: %w", backendConfig.Id, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
backendConfig.BackendBase.Type = "pfsense"
|
||||||
c.controllers[domain.InterfaceBackend(backendConfig.Id)] = backendInstance{
|
c.controllers[domain.InterfaceBackend(backendConfig.Id)] = backendInstance{
|
||||||
Config: backendConfig.BackendBase,
|
Config: backendConfig.BackendBase,
|
||||||
Implementation: controller,
|
Implementation: controller,
|
||||||
|
|||||||
@@ -307,9 +307,19 @@ func (m Manager) RestoreInterfaceState(
|
|||||||
physicalPeers, _ := controller.GetPeers(ctx, iface.Identifier)
|
physicalPeers, _ := controller.GetPeers(ctx, iface.Identifier)
|
||||||
for _, physicalPeer := range physicalPeers {
|
for _, physicalPeer := range physicalPeers {
|
||||||
isWgPortalPeer := false
|
isWgPortalPeer := false
|
||||||
for _, peer := range peers {
|
for i := range peers {
|
||||||
if peer.Identifier == domain.PeerIdentifier(physicalPeer.PublicKey) {
|
if peers[i].Identifier == domain.PeerIdentifier(physicalPeer.PublicKey) {
|
||||||
isWgPortalPeer = true
|
isWgPortalPeer = true
|
||||||
|
|
||||||
|
// Sync IsDynamic flag if it was changed on the backend
|
||||||
|
physConverted := domain.ConvertPhysicalPeer(&physicalPeer)
|
||||||
|
if peers[i].IsDynamic != physConverted.IsDynamic {
|
||||||
|
peers[i].IsDynamic = physConverted.IsDynamic
|
||||||
|
_ = m.db.SavePeer(ctx, peers[i].Identifier, func(p *domain.Peer) (*domain.Peer, error) {
|
||||||
|
p.IsDynamic = physConverted.IsDynamic
|
||||||
|
return p, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -620,7 +620,11 @@ func (m Manager) getFreshPeerIpConfig(ctx context.Context, iface *domain.Interfa
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Manager) validatePeerModifications(ctx context.Context, _, _ *domain.Peer) error {
|
func (m Manager) validatePeerModifications(ctx context.Context, old, _ *domain.Peer) error {
|
||||||
|
if old != nil && old.IsDynamic {
|
||||||
|
return fmt.Errorf("cannot modify dynamic peer: %w", domain.ErrInvalidData)
|
||||||
|
}
|
||||||
|
|
||||||
currentUser := domain.GetUserInfo(ctx)
|
currentUser := domain.GetUserInfo(ctx)
|
||||||
|
|
||||||
if !currentUser.IsAdmin && !m.cfg.Core.SelfProvisioningAllowed {
|
if !currentUser.IsAdmin && !m.cfg.Core.SelfProvisioningAllowed {
|
||||||
@@ -649,7 +653,11 @@ func (m Manager) validatePeerCreation(ctx context.Context, _, new *domain.Peer)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Manager) validatePeerDeletion(ctx context.Context, _ *domain.Peer) error {
|
func (m Manager) validatePeerDeletion(ctx context.Context, peer *domain.Peer) error {
|
||||||
|
if peer != nil && peer.IsDynamic {
|
||||||
|
return fmt.Errorf("cannot delete dynamic peer: %w", domain.ErrInvalidData)
|
||||||
|
}
|
||||||
|
|
||||||
currentUser := domain.GetUserInfo(ctx)
|
currentUser := domain.GetUserInfo(ctx)
|
||||||
|
|
||||||
if !currentUser.IsAdmin && !m.cfg.Core.SelfProvisioningAllowed {
|
if !currentUser.IsAdmin && !m.cfg.Core.SelfProvisioningAllowed {
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ func (b *Backend) Validate() error {
|
|||||||
type BackendBase struct {
|
type BackendBase struct {
|
||||||
Id string `yaml:"id"` // A unique id for the backend
|
Id string `yaml:"id"` // A unique id for the backend
|
||||||
DisplayName string `yaml:"display_name"` // A display name for the backend
|
DisplayName string `yaml:"display_name"` // A display name for the backend
|
||||||
|
Type string `yaml:"-"` // The type of the backend (e.g. local, mikrotik, pfsense)
|
||||||
|
|
||||||
IgnoredInterfaces []string `yaml:"ignored_interfaces"` // A list of interface names that should be ignored by this backend (e.g., "wg0")
|
IgnoredInterfaces []string `yaml:"ignored_interfaces"` // A list of interface names that should be ignored by this backend (e.g., "wg0")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ type MikrotikPeerExtras struct {
|
|||||||
Comment string
|
Comment string
|
||||||
IsResponder bool
|
IsResponder bool
|
||||||
Disabled bool
|
Disabled bool
|
||||||
|
Dynamic bool
|
||||||
ClientEndpoint string
|
ClientEndpoint string
|
||||||
ClientAddress string
|
ClientAddress string
|
||||||
ClientDns string
|
ClientDns string
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ type Interface struct {
|
|||||||
CreateDefaultPeer bool // if true, default peers will be created for this interface
|
CreateDefaultPeer bool // if true, default peers will be created for this interface
|
||||||
Backend InterfaceBackend // the backend that is used to manage the interface (wgctrl, mikrotik, ...)
|
Backend InterfaceBackend // the backend that is used to manage the interface (wgctrl, mikrotik, ...)
|
||||||
DriverType string // the interface driver type (linux, software, ...)
|
DriverType string // the interface driver type (linux, software, ...)
|
||||||
|
IsDynamic bool `gorm:"column:is_dynamic;default:false"` // specifies if the interface is a dynamic object
|
||||||
|
|
||||||
Disabled *time.Time `gorm:"index"` // flag that specifies if the interface is enabled (up) or not (down)
|
Disabled *time.Time `gorm:"index"` // flag that specifies if the interface is enabled (up) or not (down)
|
||||||
DisabledReason string // the reason why the interface has been disabled
|
DisabledReason string // the reason why the interface has been disabled
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ type Peer struct {
|
|||||||
|
|
||||||
// Interface settings for the peer, used to generate the [interface] section in the peer config file
|
// Interface settings for the peer, used to generate the [interface] section in the peer config file
|
||||||
Interface PeerInterfaceConfig `gorm:"embedded"`
|
Interface PeerInterfaceConfig `gorm:"embedded"`
|
||||||
|
|
||||||
|
IsDynamic bool `gorm:"column:is_dynamic;default:false"` // specifies if the peer is a dynamic object (from Mikrotik)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Peer) IsDisabled() bool {
|
func (p *Peer) IsDisabled() bool {
|
||||||
@@ -264,6 +266,7 @@ func ConvertPhysicalPeer(pp *PhysicalPeer) *Peer {
|
|||||||
Interface: PeerInterfaceConfig{
|
Interface: PeerInterfaceConfig{
|
||||||
KeyPair: pp.KeyPair,
|
KeyPair: pp.KeyPair,
|
||||||
},
|
},
|
||||||
|
IsDynamic: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
if pp.GetExtras() == nil {
|
if pp.GetExtras() == nil {
|
||||||
@@ -277,6 +280,7 @@ func ConvertPhysicalPeer(pp *PhysicalPeer) *Peer {
|
|||||||
extras := pp.GetExtras().(MikrotikPeerExtras)
|
extras := pp.GetExtras().(MikrotikPeerExtras)
|
||||||
peer.Notes = extras.Comment
|
peer.Notes = extras.Comment
|
||||||
peer.DisplayName = extras.Name
|
peer.DisplayName = extras.Name
|
||||||
|
peer.IsDynamic = extras.Dynamic
|
||||||
if extras.ClientEndpoint != "" { // if the client endpoint is set, we assume that this is a client peer
|
if extras.ClientEndpoint != "" { // if the client endpoint is set, we assume that this is a client peer
|
||||||
peer.Endpoint = NewConfigOption(extras.ClientEndpoint, true)
|
peer.Endpoint = NewConfigOption(extras.ClientEndpoint, true)
|
||||||
peer.Interface.Type = InterfaceTypeClient
|
peer.Interface.Type = InterfaceTypeClient
|
||||||
@@ -369,6 +373,7 @@ func MergeToPhysicalPeer(pp *PhysicalPeer, p *Peer) {
|
|||||||
ClientAddress: CidrsToString(p.Interface.Addresses),
|
ClientAddress: CidrsToString(p.Interface.Addresses),
|
||||||
ClientDns: p.Interface.DnsStr.GetValue(),
|
ClientDns: p.Interface.DnsStr.GetValue(),
|
||||||
ClientKeepalive: p.PersistentKeepalive.GetValue(),
|
ClientKeepalive: p.PersistentKeepalive.GetValue(),
|
||||||
|
Dynamic: p.IsDynamic,
|
||||||
}
|
}
|
||||||
pp.SetExtras(extras)
|
pp.SetExtras(extras)
|
||||||
case ControllerTypeLocal:
|
case ControllerTypeLocal:
|
||||||
|
|||||||
Reference in New Issue
Block a user