diff --git a/frontend/src/components/InterfaceEditModal.vue b/frontend/src/components/InterfaceEditModal.vue index a078e84..eeb98d5 100644 --- a/frontend/src/components/InterfaceEditModal.vue +++ b/frontend/src/components/InterfaceEditModal.vue @@ -72,6 +72,14 @@ const isBackendValid = computed(() => { 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 watch(() => props.visible, async (newValue, oldValue) => { @@ -499,7 +507,7 @@ async function del() { -
+
{{ $t('modals.interface-edit.header-hooks') }}
diff --git a/frontend/src/lang/translations/de.json b/frontend/src/lang/translations/de.json index efbbf84..a60b515 100644 --- a/frontend/src/lang/translations/de.json +++ b/frontend/src/lang/translations/de.json @@ -138,7 +138,9 @@ "peer-expiring": "Peer läuft ab am", "peer-connected": "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": { "headline": "Benutzerverwaltung", @@ -648,4 +650,4 @@ } } } -} +} \ No newline at end of file diff --git a/frontend/src/lang/translations/en.json b/frontend/src/lang/translations/en.json index ba3a891..ec74f88 100644 --- a/frontend/src/lang/translations/en.json +++ b/frontend/src/lang/translations/en.json @@ -138,7 +138,9 @@ "peer-expiring": "Peer is expiring at", "peer-connected": "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": { "headline": "User Administration", @@ -648,4 +650,4 @@ } } } -} +} \ No newline at end of file diff --git a/frontend/src/lang/translations/es.json b/frontend/src/lang/translations/es.json index eca0290..23e2cfc 100644 --- a/frontend/src/lang/translations/es.json +++ b/frontend/src/lang/translations/es.json @@ -153,7 +153,9 @@ "peer-expiring": "El peer expira en", "peer-connected": "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": { "headline": "Administración de usuarios", @@ -636,4 +638,4 @@ } } } -} +} \ No newline at end of file diff --git a/frontend/src/lang/translations/fr.json b/frontend/src/lang/translations/fr.json index 4c4df6a..485f848 100644 --- a/frontend/src/lang/translations/fr.json +++ b/frontend/src/lang/translations/fr.json @@ -126,7 +126,9 @@ "peer-expiring": "Le pair expire le", "peer-connected": "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": { "headline": "Administration des utilisateurs", @@ -514,4 +516,4 @@ } } } -} +} \ No newline at end of file diff --git a/frontend/src/lang/translations/ja.json b/frontend/src/lang/translations/ja.json index 4fb3db7..6d0ab7f 100644 --- a/frontend/src/lang/translations/ja.json +++ b/frontend/src/lang/translations/ja.json @@ -138,7 +138,9 @@ "peer-expiring": "ピアの有効期限:", "peer-connected": "接続中", "peer-not-connected": "未接続", - "peer-handshake": "最終ハンドシェイク:" + "peer-handshake": "最終ハンドシェイク:", + "peer-dynamic": "動的ピア", + "peer-dynamic-no-edit": "このピアは動的に作成されており、編集または削除することはできません。" }, "users": { "headline": "ユーザー管理", @@ -648,4 +650,4 @@ } } } -} +} \ No newline at end of file diff --git a/frontend/src/lang/translations/ko.json b/frontend/src/lang/translations/ko.json index 979f5d0..6628322 100644 --- a/frontend/src/lang/translations/ko.json +++ b/frontend/src/lang/translations/ko.json @@ -1,536 +1,537 @@ { - "languages": { - "ko": "한국어" + "languages": { + "ko": "한국어" + }, + "general": { + "pagination": { + "size": "항목 수", + "all": "전체 (느림)" }, - "general": { - "pagination": { - "size": "항목 수", - "all": "전체 (느림)" - }, - "search": { - "placeholder": "검색...", - "button": "검색" - }, - "select-all": "모두 선택", - "yes": "예", - "no": "아니오", - "cancel": "취소", - "close": "닫기", - "save": "저장", - "delete": "삭제" + "search": { + "placeholder": "검색...", + "button": "검색" }, - "login": { - "headline": "로그인하세요", - "username": { - "label": "사용자 이름", - "placeholder": "사용자 이름을 입력하세요" + "select-all": "모두 선택", + "yes": "예", + "no": "아니오", + "cancel": "취소", + "close": "닫기", + "save": "저장", + "delete": "삭제" + }, + "login": { + "headline": "로그인하세요", + "username": { + "label": "사용자 이름", + "placeholder": "사용자 이름을 입력하세요" + }, + "password": { + "label": "비밀번호", + "placeholder": "비밀번호를 입력하세요" + }, + "button": "로그인" + }, + "menu": { + "home": "홈", + "interfaces": "인터페이스", + "users": "사용자", + "lang": "언어 변경", + "profile": "내 프로필", + "settings": "설정", + "audit": "감사 로그", + "login": "로그인", + "logout": "로그아웃" + }, + "home": { + "headline": "WireGuard® VPN 포털", + "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 포털 정보", + "headline": "WireGuard 포털", + "content": "WireGuard 포털은 WireGuard를 위한 간단한 웹 기반 구성 포털입니다.", + "button": "더 보기" + }, + "profiles": { + "headline": "VPN 프로필", + "abstract": "사용자 프로필을 통해 개인 VPN 구성에 액세스하고 다운로드할 수 있습니다.", + "content": "구성된 모든 프로필을 찾으려면 아래 버튼을 클릭하세요.", + "button": "내 프로필 열기" + }, + "admin": { + "headline": "관리 영역", + "abstract": "관리 영역에서는 WireGuard 피어 및 서버 인터페이스뿐만 아니라 WireGuard 포털에 로그인할 수 있는 사용자도 관리할 수 있습니다.", + "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": "기본 Keepalive 간격", + "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": "피어 편집" + }, + "settings": { + "headline": "설정", + "abstract": "여기에서 개인 설정을 변경할 수 있습니다.", + "api": { + "headline": "API 설정", + "abstract": "여기에서 RESTful API 설정을 구성할 수 있습니다.", + "active-description": "현재 사용자 계정에 대해 API가 활성화되어 있습니다. 모든 API 요청은 기본 인증(Basic Auth)으로 인증됩니다. 인증에 다음 자격 증명을 사용하세요.", + "inactive-description": "현재 API가 비활성화되어 있습니다. 활성화하려면 아래 버튼을 누르세요.", + "user-label": "API 사용자 이름:", + "user-placeholder": "API 사용자", + "token-label": "API 비밀번호:", + "token-placeholder": "API 토큰", + "token-created-label": "API 액세스 권한 부여 시각: ", + "button-disable-title": "API를 비활성화합니다. 현재 토큰이 무효화됩니다.", + "button-disable-text": "API 비활성화", + "button-enable-title": "API를 활성화합니다. 새 토큰이 생성됩니다.", + "button-enable-text": "API 활성화", + "api-link": "API 문서" + } + }, + "audit": { + "headline": "감사 로그", + "abstract": "여기에서 WireGuard 포털에서 수행된 모든 작업의 감사 로그를 찾을 수 있습니다.", + "no-entries": { + "headline": "로그 항목 없음", + "abstract": "현재 기록된 감사 로그가 없습니다." + }, + "entries-headline": "로그 항목", + "table-heading": { + "id": "#", + "time": "시간", + "user": "사용자", + "severity": "심각도", + "origin": "출처", + "message": "메시지" + } + }, + "modals": { + "user-view": { + "headline": "사용자 계정:", + "tab-user": "정보", + "tab-peers": "피어", + "headline-info": "사용자 정보:", + "headline-notes": "메모:", + "email": "이메일", + "firstname": "이름", + "lastname": "성", + "phone": "전화번호", + "department": "부서", + "api-enabled": "API 액세스", + "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": { "label": "비밀번호", - "placeholder": "비밀번호를 입력하세요" + "placeholder": "매우 비밀스러운 비밀번호", + "description": "현재 비밀번호를 유지하려면 이 필드를 비워 두세요." }, - "button": "로그인" - }, - "menu": { - "home": "홈", - "interfaces": "인터페이스", - "users": "사용자", - "lang": "언어 변경", - "profile": "내 프로필", - "settings": "설정", - "audit": "감사 로그", - "login": "로그인", - "logout": "로그아웃" - }, - "home": { - "headline": "WireGuard® VPN 포털", - "info-headline": "추가 정보", - "abstract": "WireGuard®는 암호화 기술을 활용하는 매우 간단하면서도 빠르고 현대적인 VPN입니다. IPsec보다 빠르고, 간단하며, 가볍고, 더 유용하면서도 엄청난 골칫거리를 피하는 것을 목표로 합니다. OpenVPN보다 훨씬 더 성능이 뛰어날 것으로 예상됩니다.", - "installation": { - "box-header": "WireGuard 설치", - "headline": "설치", - "content": "클라이언트 소프트웨어 설치 지침은 공식 WireGuard 웹사이트에서 찾을 수 있습니다.", - "button": "지침 열기" + "email": { + "label": "이메일", + "placeholder": "이메일 주소" }, - "about-wg": { - "box-header": "WireGuard 정보", - "headline": "정보", - "content": "WireGuard®는 암호화 기술을 활용하는 매우 간단하면서도 빠르고 현대적인 VPN입니다.", - "button": "더 보기" + "phone": { + "label": "전화번호", + "placeholder": "전화번호" }, - "about-portal": { - "box-header": "WireGuard 포털 정보", - "headline": "WireGuard 포털", - "content": "WireGuard 포털은 WireGuard를 위한 간단한 웹 기반 구성 포털입니다.", - "button": "더 보기" + "department": { + "label": "부서", + "placeholder": "부서" }, - "profiles": { - "headline": "VPN 프로필", - "abstract": "사용자 프로필을 통해 개인 VPN 구성에 액세스하고 다운로드할 수 있습니다.", - "content": "구성된 모든 프로필을 찾으려면 아래 버튼을 클릭하세요.", - "button": "내 프로필 열기" + "firstname": { + "label": "이름", + "placeholder": "이름" + }, + "lastname": { + "label": "성", + "placeholder": "성" + }, + "notes": { + "label": "메모", + "placeholder": "" + }, + "disabled": { + "label": "비활성화됨 (WireGuard 연결 및 로그인 불가)" + }, + "locked": { + "label": "잠김 (로그인 불가, WireGuard 연결은 계속 작동)" }, "admin": { - "headline": "관리 영역", - "abstract": "관리 영역에서는 WireGuard 피어 및 서버 인터페이스뿐만 아니라 WireGuard 포털에 로그인할 수 있는 사용자도 관리할 수 있습니다.", - "content": "", - "button-admin": "서버 관리 열기", - "button-user": "사용자 관리 열기" + "label": "관리자 여부" } }, - "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": "기본 Keepalive 간격", - "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": "마지막 핸드셰이크:" + "confirm-delete": "사용자 '{id}'를 삭제하시겠습니까?", + "interface-view": { + "headline": "인터페이스 구성:" }, - "users": { - "headline": "사용자 관리", - "table-heading": { - "id": "ID", - "email": "이메일", - "firstname": "이름", - "lastname": "성", - "sources": "소스", - "peers": "피어", - "admin": "관리자" + "interface-edit": { + "headline-edit": "인터페이스 편집:", + "headline-new": "새 인터페이스", + "tab-interface": "인터페이스", + "tab-peerdef": "피어 기본값", + "header-general": "일반", + "header-network": "네트워크", + "header-crypto": "암호화", + "header-hooks": "인터페이스 후크", + "header-peer-hooks": "후크", + "header-state": "상태", + "identifier": { + "label": "식별자", + "placeholder": "고유한 인터페이스 식별자" }, - "no-user": { - "headline": "사용 가능한 사용자 없음", - "abstract": "현재 WireGuard 포털에 등록된 사용자가 없습니다." + "mode": { + "label": "인터페이스 모드", + "server": "서버 모드", + "client": "클라이언트 모드", + "any": "알 수 없는 모드" }, - "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": "서버 인터페이스" + "display-name": { + "label": "표시 이름", + "placeholder": "인터페이스에 대한 설명적인 이름" }, - "no-peer": { - "headline": "사용 가능한 피어 없음", - "abstract": "현재 사용자 프로필과 연결된 피어가 없습니다." + "private-key": { + "label": "개인 키", + "placeholder": "개인 키" }, - "peer-connected": "연결됨", - "button-add-peer": "피어 추가", - "button-show-peer": "피어 보기", - "button-edit-peer": "피어 편집" - }, - "settings": { - "headline": "설정", - "abstract": "여기에서 개인 설정을 변경할 수 있습니다.", - "api": { - "headline": "API 설정", - "abstract": "여기에서 RESTful API 설정을 구성할 수 있습니다.", - "active-description": "현재 사용자 계정에 대해 API가 활성화되어 있습니다. 모든 API 요청은 기본 인증(Basic Auth)으로 인증됩니다. 인증에 다음 자격 증명을 사용하세요.", - "inactive-description": "현재 API가 비활성화되어 있습니다. 활성화하려면 아래 버튼을 누르세요.", - "user-label": "API 사용자 이름:", - "user-placeholder": "API 사용자", - "token-label": "API 비밀번호:", - "token-placeholder": "API 토큰", - "token-created-label": "API 액세스 권한 부여 시각: ", - "button-disable-title": "API를 비활성화합니다. 현재 토큰이 무효화됩니다.", - "button-disable-text": "API 비활성화", - "button-enable-title": "API를 활성화합니다. 새 토큰이 생성됩니다.", - "button-enable-text": "API 활성화", - "api-link": "API 문서" - } - }, - "audit": { - "headline": "감사 로그", - "abstract": "여기에서 WireGuard 포털에서 수행된 모든 작업의 감사 로그를 찾을 수 있습니다.", - "no-entries": { - "headline": "로그 항목 없음", - "abstract": "현재 기록된 감사 로그가 없습니다." + "public-key": { + "label": "공개 키", + "placeholder": "공개 키" }, - "entries-headline": "로그 항목", - "table-heading": { - "id": "#", - "time": "시간", - "user": "사용자", - "severity": "심각도", - "origin": "출처", - "message": "메시지" - } - }, - "modals": { - "user-view": { - "headline": "사용자 계정:", - "tab-user": "정보", - "tab-peers": "피어", - "headline-info": "사용자 정보:", - "headline-notes": "메모:", - "email": "이메일", - "firstname": "이름", - "lastname": "성", - "phone": "전화번호", - "department": "부서", - "api-enabled": "API 액세스", - "disabled": "계정 비활성화됨", - "locked": "계정 잠김", - "no-peers": "사용자에게 연결된 피어가 없습니다.", - "peers": { - "name": "이름", - "interface": "인터페이스", - "ip": "IP 주소" - } + "ip": { + "label": "IP 주소", + "placeholder": "IP 주소 (CIDR 형식)" }, - "user-edit": { - "headline-edit": "사용자 편집:", - "headline-new": "새 사용자", - "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": "관리자 여부" - } + "listen-port": { + "label": "수신 포트", + "placeholder": "수신 포트" }, - "confirm-delete": "사용자 '{id}'를 삭제하시겠습니까?", - "interface-view": { - "headline": "인터페이스 구성:" + "dns": { + "label": "DNS 서버", + "placeholder": "사용해야 하는 DNS 서버" }, - "interface-edit": { - "headline-edit": "인터페이스 편집:", - "headline-new": "새 인터페이스", - "tab-interface": "인터페이스", - "tab-peerdef": "피어 기본값", - "header-general": "일반", - "header-network": "네트워크", - "header-crypto": "암호화", - "header-hooks": "인터페이스 후크", - "header-peer-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": "Pre-Up", - "placeholder": "하나 이상의 bash 명령 (;으로 구분)" - }, - "post-up": { - "label": "Post-Up", - "placeholder": "하나 이상의 bash 명령 (;으로 구분)" - }, - "pre-down": { - "label": "Pre-Down", - "placeholder": "하나 이상의 bash 명령 (;으로 구분)" - }, - "post-down": { - "label": "Post-Down", - "placeholder": "하나 이상의 bash 명령 (;으로 구분)" - }, - "disabled": { - "label": "인터페이스 비활성화됨" - }, - "save-config": { - "label": "wg-quick 구성 자동 저장" - }, - "defaults": { - "endpoint": { - "label": "엔드포인트 주소", - "placeholder": "엔드포인트 주소", - "description": "피어가 연결할 엔드포인트 주소. (예: wg.example.com 또는 wg.example.com:51820)" - }, - "networks": { - "label": "IP 네트워크", - "placeholder": "네트워크 주소", - "description": "피어는 해당 서브넷에서 IP 주소를 받습니다." - }, - "allowed-ip": { - "label": "허용된 IP 주소", - "placeholder": "기본 허용 IP 주소" - }, - "mtu": { - "label": "MTU", - "placeholder": "클라이언트 MTU (0 = 기본값 유지)" - }, - "keep-alive": { - "label": "Keep Alive 간격", - "placeholder": "영구 Keepalive (0 = 기본값)" - } - }, - "button-apply-defaults": "피어 기본값 적용", - "button-create-default-peers": "기본 피어 생성", - "confirm-delete": "인터페이스 '{id}'를 삭제하시겠습니까?" + "dns-search": { + "label": "DNS 검색 도메인", + "placeholder": "DNS 검색 접두사" }, - "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": "이메일로 구성 보내기" + "mtu": { + "label": "MTU", + "placeholder": "인터페이스 MTU (0 = 기본값 유지)" }, - "peer-edit": { - "headline-edit-peer": "피어 편집:", - "headline-edit-endpoint": "엔드포인트 편집:", - "headline-new-peer": "피어 생성", - "headline-new-endpoint": "엔드포인트 생성", - "header-general": "일반", - "header-network": "네트워크", - "header-crypto": "암호화", - "header-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": "원격 엔드포인트의 공개 키" - }, + "firewall-mark": { + "label": "방화벽 표시", + "placeholder": "나가는 트래픽에 적용되는 방화벽 표시. (0 = 자동)" + }, + "routing-table": { + "label": "라우팅 테이블", + "placeholder": "라우팅 테이블 ID", + "description": "특수 사례: off = 경로 관리 안 함, 0 = 자동" + }, + "pre-up": { + "label": "Pre-Up", + "placeholder": "하나 이상의 bash 명령 (;으로 구분)" + }, + "post-up": { + "label": "Post-Up", + "placeholder": "하나 이상의 bash 명령 (;으로 구분)" + }, + "pre-down": { + "label": "Pre-Down", + "placeholder": "하나 이상의 bash 명령 (;으로 구분)" + }, + "post-down": { + "label": "Post-Down", + "placeholder": "하나 이상의 bash 명령 (;으로 구분)" + }, + "disabled": { + "label": "인터페이스 비활성화됨" + }, + "save-config": { + "label": "wg-quick 구성 자동 저장" + }, + "defaults": { "endpoint": { "label": "엔드포인트 주소", - "placeholder": "원격 엔드포인트의 주소" + "placeholder": "엔드포인트 주소", + "description": "피어가 연결할 엔드포인트 주소. (예: wg.example.com 또는 wg.example.com:51820)" }, - "ip": { - "label": "IP 주소", - "placeholder": "IP 주소 (CIDR 형식)" + "networks": { + "label": "IP 네트워크", + "placeholder": "네트워크 주소", + "description": "피어는 해당 서브넷에서 IP 주소를 받습니다." }, "allowed-ip": { "label": "허용된 IP 주소", - "placeholder": "허용된 IP 주소 (CIDR 형식)" - }, - "extra-allowed-ip": { - "label": "추가 허용 IP 주소", - "placeholder": "추가 허용 IP (서버 측)", - "description": "이 IP 주소는 원격 WireGuard 인터페이스에 허용된 IP로 추가됩니다." - }, - "dns": { - "label": "DNS 서버", - "placeholder": "사용해야 하는 DNS 서버" - }, - "dns-search": { - "label": "DNS 검색 도메인", - "placeholder": "DNS 검색 접두사" - }, - "keep-alive": { - "label": "Keep Alive 간격", - "placeholder": "영구 Keepalive (0 = 기본값)" + "placeholder": "기본 허용 IP 주소" }, "mtu": { "label": "MTU", "placeholder": "클라이언트 MTU (0 = 기본값 유지)" }, - "pre-up": { - "label": "Pre-Up", - "placeholder": "하나 이상의 bash 명령 (;으로 구분)" - }, - "post-up": { - "label": "Post-Up", - "placeholder": "하나 이상의 bash 명령 (;으로 구분)" - }, - "pre-down": { - "label": "Pre-Down", - "placeholder": "하나 이상의 bash 명령 (;으로 구분)" - }, - "post-down": { - "label": "Post-Down", - "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": "피어 표시 이름에 추가되는 접두사." + "keep-alive": { + "label": "Keep Alive 간격", + "placeholder": "영구 Keepalive (0 = 기본값)" } + }, + "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": "후크 (피어에서 실행됨)", + "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 주소는 원격 WireGuard 인터페이스에 허용된 IP로 추가됩니다." + }, + "dns": { + "label": "DNS 서버", + "placeholder": "사용해야 하는 DNS 서버" + }, + "dns-search": { + "label": "DNS 검색 도메인", + "placeholder": "DNS 검색 접두사" + }, + "keep-alive": { + "label": "Keep Alive 간격", + "placeholder": "영구 Keepalive (0 = 기본값)" + }, + "mtu": { + "label": "MTU", + "placeholder": "클라이언트 MTU (0 = 기본값 유지)" + }, + "pre-up": { + "label": "Pre-Up", + "placeholder": "하나 이상의 bash 명령 (;으로 구분)" + }, + "post-up": { + "label": "Post-Up", + "placeholder": "하나 이상의 bash 명령 (;으로 구분)" + }, + "pre-down": { + "label": "Pre-Down", + "placeholder": "하나 이상의 bash 명령 (;으로 구분)" + }, + "post-down": { + "label": "Post-Down", + "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": "피어 표시 이름에 추가되는 접두사." } } } - \ No newline at end of file +} \ No newline at end of file diff --git a/frontend/src/lang/translations/pt.json b/frontend/src/lang/translations/pt.json index 3e1ea96..243f463 100644 --- a/frontend/src/lang/translations/pt.json +++ b/frontend/src/lang/translations/pt.json @@ -128,7 +128,9 @@ "peer-expiring": "Peer expira em", "peer-connected": "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": { "headline": "Administração de Utilizadores", @@ -553,4 +555,4 @@ } } } -} +} \ No newline at end of file diff --git a/frontend/src/lang/translations/ru.json b/frontend/src/lang/translations/ru.json index 068ed48..72649b8 100644 --- a/frontend/src/lang/translations/ru.json +++ b/frontend/src/lang/translations/ru.json @@ -134,7 +134,9 @@ "peer-expiring": "Пир истекает в", "peer-connected": "Подключено", "peer-not-connected": "Не подключено", - "peer-handshake": "Последнее рукопожатие:" + "peer-handshake": "Последнее рукопожатие:", + "peer-dynamic": "Динамический пир", + "peer-dynamic-no-edit": "Этот пир создан динамически и не может быть изменен или удален." }, "users": { "headline": "Администрирование пользователей", @@ -628,4 +630,4 @@ } } } -} +} \ No newline at end of file diff --git a/frontend/src/lang/translations/uk.json b/frontend/src/lang/translations/uk.json index e87d302..8d123c9 100644 --- a/frontend/src/lang/translations/uk.json +++ b/frontend/src/lang/translations/uk.json @@ -126,7 +126,9 @@ "peer-expiring": "Пір припиняє дію о", "peer-connected": "Підключено", "peer-not-connected": "Не підключено", - "peer-handshake": "Останнє рукостискання:" + "peer-handshake": "Останнє рукостискання:", + "peer-dynamic": "Динамічний пір", + "peer-dynamic-no-edit": "Цей пір створено динамічно і його не можна редагувати або видаляти." }, "users": { "headline": "Адміністрування користувачів", @@ -514,4 +516,4 @@ } } } -} +} \ No newline at end of file diff --git a/frontend/src/lang/translations/vi.json b/frontend/src/lang/translations/vi.json index a283766..45329bb 100644 --- a/frontend/src/lang/translations/vi.json +++ b/frontend/src/lang/translations/vi.json @@ -125,7 +125,9 @@ "peer-expiring": "Peer sẽ hết hạn vào", "peer-connected": "Đã 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": { "headline": "Quản trị Người dùng", @@ -492,4 +494,4 @@ } } } -} +} \ No newline at end of file diff --git a/frontend/src/lang/translations/zh.json b/frontend/src/lang/translations/zh.json index a76c2ba..3c2bc1d 100644 --- a/frontend/src/lang/translations/zh.json +++ b/frontend/src/lang/translations/zh.json @@ -1,496 +1,497 @@ { - "languages": { - "zh": "中文" + "languages": { + "zh": "中文" + }, + "general": { + "pagination": { + "size": "每页显示数量", + "all": "全部 (较慢)" }, - "general": { - "pagination": { - "size": "每页显示数量", - "all": "全部 (较慢)" - }, - "search": { - "placeholder": "搜索...", - "button": "搜索" - }, - "select-all": "全选", - "yes": "是", - "no": "否", - "cancel": "取消", - "close": "关闭", - "save": "保存", - "delete": "删除" + "search": { + "placeholder": "搜索...", + "button": "搜索" }, - "login": { - "headline": "请登录", - "username": { - "label": "用户名", - "placeholder": "请输入用户名" + "select-all": "全选", + "yes": "是", + "no": "否", + "cancel": "取消", + "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": { "label": "密码", - "placeholder": "请输入密码" + "placeholder": "一个复杂的密码", + "description": "留空保持当前密码不变。" }, - "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": "打开说明" + "email": { + "label": "电子邮件", + "placeholder": "电子邮件地址" }, - "about-wg": { - "box-header": "关于 WireGuard", - "headline": "关于", - "content": "WireGuard® 是一种极其简单但又快速现代的 VPN,采用先进的加密技术。", - "button": "更多" + "phone": { + "label": "电话", + "placeholder": "电话号码" }, - "about-portal": { - "box-header": "关于 WireGuard Portal", - "headline": "WireGuard Portal", - "content": "WireGuard Portal 是一个简单的基于网页的 WireGuard 配置平台。", - "button": "更多" + "department": { + "label": "部门", + "placeholder": "部门" }, - "profiles": { - "headline": "VPN 配置文件", - "abstract": "您可以通过您的用户个人资料访问并下载个人 VPN 配置。", - "content": "要查找您所有的配置文件,请点击下面的按钮。", - "button": "打开我的个人资料" + "firstname": { + "label": "名", + "placeholder": "名" + }, + "lastname": { + "label": "姓", + "placeholder": "姓" + }, + "notes": { + "label": "备注", + "placeholder": "" + }, + "disabled": { + "label": "禁用 (无法连接 WireGuard 和登录)" + }, + "locked": { + "label": "锁定 (无法登录,但 WireGuard 仍然可以连接)" }, "admin": { - "headline": "后台管理", - "abstract": "在后台管理,您可以管理 WireGuard 节点和服务器接口,以及允许登录 WireGuard Portal 的用户。", - "content": "", - "button-admin": "打开服务器管理", - "button-user": "打开用户管理" + "label": "管理员" } }, - "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": "最后一次握手: " + "confirm-delete": "确定要删除用户“{id}”吗?", + "interface-view": { + "headline": "接口配置: " }, - "users": { - "headline": "用户管理", - "table-heading": { - "id": "ID", - "email": "电子邮件", - "firstname": "名", - "lastname": "姓", - "sources": "来源", - "peers": "节点", - "admin": "管理员" + "interface-edit": { + "headline-edit": "编辑接口: ", + "headline-new": "新接口", + "tab-interface": "接口", + "tab-peerdef": "节点默认值", + "header-general": "常规", + "header-network": "网络", + "header-crypto": "加密", + "header-hooks": "接口 Hooks", + "header-peer-hooks": "Hooks", + "header-state": "状态", + "identifier": { + "label": "标识符", + "placeholder": "唯一接口标识符" }, - "no-user": { - "headline": "没有可用用户", - "abstract": "当前没有用户注册 WireGuard 门户。" + "mode": { + "label": "接口模式", + "server": "服务器模式", + "client": "客户端模式", + "any": "未知模式" }, - "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": "服务器接口" + "display-name": { + "label": "显示名称", + "placeholder": "接口的描述性名称" }, - "no-peer": { - "headline": "没有可用节点", - "abstract": "当前没有与您的用户个人资料关联的节点。" + "private-key": { + "label": "私钥", + "placeholder": "私钥" }, - "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 地址" - } + "public-key": { + "label": "公钥", + "placeholder": "公钥" }, - "user-edit": { - "headline-edit": "编辑用户: ", - "headline-new": "新用户", - "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": "管理员" - } + "ip": { + "label": "IP 地址", + "placeholder": "IP 地址 (CIDR 格式)" }, - "confirm-delete": "确定要删除用户“{id}”吗?", - "interface-view": { - "headline": "接口配置: " + "listen-port": { + "label": "监听端口", + "placeholder": "监听端口" }, - "interface-edit": { - "headline-edit": "编辑接口: ", - "headline-new": "新接口", - "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}”吗?" + "dns": { + "label": "DNS 服务器", + "placeholder": "应使用的 DNS 服务器" }, - "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": "通过电子邮件发送配置" + "dns-search": { + "label": "DNS 搜索域", + "placeholder": "DNS 搜索前缀" }, - "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": "远程节点的公钥" - }, + "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": "远程节点的地址" + "label": "服务器地址", + "placeholder": "服务器地址", + "description": "节点将连接到服务器的地址。" }, - "ip": { + "networks": { "label": "IP 地址", - "placeholder": "IP 地址(CIDR 格式)" + "placeholder": "IP 地址", + "description": "节点将从这些子网获取 IP 地址。" }, "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 = 默认)" + "placeholder": "默认允许的 IP 地址" }, "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": "添加到节点 显示名称的前缀。" + "keep-alive": { + "label": "心跳包间隔", + "placeholder": "持久保持连接 (0 = 默认)" } + }, + "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": "添加到节点 显示名称的前缀。" } } } - \ No newline at end of file +} \ No newline at end of file diff --git a/frontend/src/views/InterfaceView.vue b/frontend/src/views/InterfaceView.vue index df6ecb4..04158c2 100644 --- a/frontend/src/views/InterfaceView.vue +++ b/frontend/src/views/InterfaceView.vue @@ -151,7 +151,9 @@ async function bulkDisable() { function toggleSelectAll() { peers.FilteredAndPaged.forEach(peer => { - peer.IsSelected = selectAll.value; + if (!peer.IsDynamic) { + peer.IsSelected = selectAll.value; + } }); } @@ -444,13 +446,17 @@ onMounted(async () => { - + - {{peer.DisplayName}}{{ $filters.truncate(peer.Identifier, 10)}} + + {{peer.DisplayName}} + {{ $filters.truncate(peer.Identifier, 10)}} + {{ $t('interfaces.peer-dynamic') }} + {{peer.UserIdentifier}} {{ ip }} @@ -474,7 +480,13 @@ onMounted(async () => { - + + + + + + + diff --git a/internal/adapters/wgcontroller/mikrotik.go b/internal/adapters/wgcontroller/mikrotik.go index e730bd7..70d429c 100644 --- a/internal/adapters/wgcontroller/mikrotik.go +++ b/internal/adapters/wgcontroller/mikrotik.go @@ -296,7 +296,7 @@ func (c *MikrotikController) GetPeers(ctx context.Context, deviceId domain.Inter PropList: []string{ ".id", "name", "allowed-address", "client-address", "client-endpoint", "client-keepalive", "comment", "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{ "interface": string(deviceId), @@ -376,6 +376,7 @@ func (c *MikrotikController) convertWireGuardPeer(peer lowlevel.GenericJsonObjec Comment: peer.GetString("comment"), IsResponder: peer.GetBool("responder"), Disabled: peer.GetBool("disabled"), + Dynamic: peer.GetBool("dynamic"), ClientEndpoint: peer.GetString("client-endpoint"), ClientAddress: peer.GetString("client-address"), ClientDns: peer.GetString("client-dns"), @@ -614,15 +615,24 @@ func (c *MikrotikController) SavePeer( return err } - peerId := physicalPeer.GetExtras().(domain.MikrotikPeerExtras).Id + oldExtras := physicalPeer.GetExtras().(domain.MikrotikPeerExtras) + peerId := oldExtras.Id + dynamic := oldExtras.Dynamic + physicalPeer, err = updateFunc(physicalPeer) if err != nil { return err } 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) + 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 { return err } @@ -638,7 +648,7 @@ func (c *MikrotikController) getOrCreatePeer( wgReply := c.client.Query(ctx, "/interface/wireguard/peers", &lowlevel.MikrotikRequestOptions{ PropList: []string{ ".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{ "public-key": string(id), @@ -742,7 +752,7 @@ func (c *MikrotikController) DeletePeer( defer mutex.Unlock() wgReply := c.client.Query(ctx, "/interface/wireguard/peers", &lowlevel.MikrotikRequestOptions{ - PropList: []string{".id"}, + PropList: []string{".id", "dynamic"}, Filters: map[string]string{ "public-key": string(id), "interface": string(deviceId), @@ -754,6 +764,10 @@ func (c *MikrotikController) DeletePeer( if len(wgReply.Data) == 0 { 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") deleteReply := c.client.Delete(ctx, "/interface/wireguard/peers/"+peerId) @@ -769,12 +783,51 @@ func (c *MikrotikController) DeletePeer( // region wg-quick-related func (c *MikrotikController) ExecuteInterfaceHook( - _ context.Context, - _ domain.InterfaceIdentifier, - _ string, + ctx context.Context, + id domain.InterfaceIdentifier, + hookCmd string, ) error { - // TODO implement me - slog.Error("interface hooks are not yet supported for Mikrotik backends, please open an issue on GitHub") + if hookCmd == "" { + 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 } diff --git a/internal/app/api/v0/handlers/endpoint_config.go b/internal/app/api/v0/handlers/endpoint_config.go index 62da202..85ca630 100644 --- a/internal/app/api/v0/handlers/endpoint_config.go +++ b/internal/app/api/v0/handlers/endpoint_config.go @@ -119,6 +119,7 @@ func (e ConfigEndpoint) handleSettingsGet() http.HandlerFunc { names = append(names, model.SettingsBackendNames{ Id: controller.Id, Name: displayName, + Type: controller.Type, }) } diff --git a/internal/app/api/v0/model/models.go b/internal/app/api/v0/model/models.go index 2fbe40a..39f3a27 100644 --- a/internal/app/api/v0/model/models.go +++ b/internal/app/api/v0/model/models.go @@ -20,4 +20,5 @@ type Settings struct { type SettingsBackendNames struct { Id string `json:"Id"` Name string `json:"Name"` + Type string `json:"Type"` } diff --git a/internal/app/api/v0/model/models_interface.go b/internal/app/api/v0/model/models_interface.go index 87ef658..9994e65 100644 --- a/internal/app/api/v0/model/models_interface.go +++ b/internal/app/api/v0/model/models_interface.go @@ -17,6 +17,7 @@ type Interface struct { 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) 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 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, Disabled: src.IsDisabled(), DisabledReason: src.DisabledReason, + IsDynamic: src.IsDynamic, SaveConfig: src.SaveConfig, CreateDefaultPeer: src.CreateDefaultPeer, ListenPort: src.ListenPort, @@ -153,6 +155,7 @@ func NewDomainInterface(src *Interface) *domain.Interface { PreDown: src.PreDown, PostDown: src.PostDown, SaveConfig: src.SaveConfig, + IsDynamic: src.IsDynamic, CreateDefaultPeer: src.CreateDefaultPeer, DisplayName: src.DisplayName, Type: domain.InterfaceType(src.Mode), diff --git a/internal/app/api/v0/model/models_peer.go b/internal/app/api/v0/model/models_peer.go index c7843c5..998a5ad 100644 --- a/internal/app/api/v0/model/models_peer.go +++ b/internal/app/api/v0/model/models_peer.go @@ -49,6 +49,7 @@ type Peer struct { DisabledReason string `json:"DisabledReason"` // the reason why the peer has been disabled ExpiresAt ExpiryDate `json:"ExpiresAt,omitempty"` // expiry dates 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 EndpointPublicKey ConfigOption[string] `json:"EndpointPublicKey"` // the endpoint public key @@ -90,6 +91,7 @@ func NewPeer(src *domain.Peer) *Peer { DisabledReason: src.DisabledReason, ExpiresAt: ExpiryDate{src.ExpiresAt}, Notes: src.Notes, + IsDynamic: src.IsDynamic, Endpoint: ConfigOptionFromDomain(src.Endpoint), EndpointPublicKey: ConfigOptionFromDomain(src.EndpointPublicKey), AllowedIPs: StringSliceConfigOptionFromDomain(src.AllowedIPsStr), @@ -148,6 +150,7 @@ func NewDomainPeer(src *Peer) *domain.Peer { InterfaceIdentifier: domain.InterfaceIdentifier(src.InterfaceIdentifier), Disabled: nil, // set below DisabledReason: src.DisabledReason, + IsDynamic: src.IsDynamic, ExpiresAt: src.ExpiresAt.Time, Notes: src.Notes, Interface: domain.PeerInterfaceConfig{ diff --git a/internal/app/wireguard/controller_manager.go b/internal/app/wireguard/controller_manager.go index c8be1ba..041b4aa 100644 --- a/internal/app/wireguard/controller_manager.go +++ b/internal/app/wireguard/controller_manager.go @@ -63,6 +63,7 @@ func (c *ControllerManager) registerLocalController() error { Config: config.BackendBase{ Id: config.LocalBackendName, DisplayName: "Local WireGuard Controller", + Type: "local", IgnoredInterfaces: c.cfg.Backend.IgnoredLocalInterfaces, }, 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) } + backendConfig.BackendBase.Type = "mikrotik" c.controllers[domain.InterfaceBackend(backendConfig.Id)] = backendInstance{ Config: backendConfig.BackendBase, 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) } + backendConfig.BackendBase.Type = "pfsense" c.controllers[domain.InterfaceBackend(backendConfig.Id)] = backendInstance{ Config: backendConfig.BackendBase, Implementation: controller, diff --git a/internal/app/wireguard/wireguard_interfaces.go b/internal/app/wireguard/wireguard_interfaces.go index 8f8d193..9dc8a28 100644 --- a/internal/app/wireguard/wireguard_interfaces.go +++ b/internal/app/wireguard/wireguard_interfaces.go @@ -307,9 +307,19 @@ func (m Manager) RestoreInterfaceState( physicalPeers, _ := controller.GetPeers(ctx, iface.Identifier) for _, physicalPeer := range physicalPeers { isWgPortalPeer := false - for _, peer := range peers { - if peer.Identifier == domain.PeerIdentifier(physicalPeer.PublicKey) { + for i := range peers { + if peers[i].Identifier == domain.PeerIdentifier(physicalPeer.PublicKey) { 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 } } diff --git a/internal/app/wireguard/wireguard_peers.go b/internal/app/wireguard/wireguard_peers.go index 60bb3da..7e07a45 100644 --- a/internal/app/wireguard/wireguard_peers.go +++ b/internal/app/wireguard/wireguard_peers.go @@ -620,7 +620,11 @@ func (m Manager) getFreshPeerIpConfig(ctx context.Context, iface *domain.Interfa 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) if !currentUser.IsAdmin && !m.cfg.Core.SelfProvisioningAllowed { @@ -649,7 +653,11 @@ func (m Manager) validatePeerCreation(ctx context.Context, _, new *domain.Peer) 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) if !currentUser.IsAdmin && !m.cfg.Core.SelfProvisioningAllowed { diff --git a/internal/config/backend.go b/internal/config/backend.go index 5d54058..b1aeef5 100644 --- a/internal/config/backend.go +++ b/internal/config/backend.go @@ -61,6 +61,7 @@ func (b *Backend) Validate() error { type BackendBase struct { Id string `yaml:"id"` // A unique id 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") } diff --git a/internal/domain/controller.go b/internal/domain/controller.go index 3aec4f0..53aed05 100644 --- a/internal/domain/controller.go +++ b/internal/domain/controller.go @@ -22,6 +22,7 @@ type MikrotikPeerExtras struct { Comment string IsResponder bool Disabled bool + Dynamic bool ClientEndpoint string ClientAddress string ClientDns string diff --git a/internal/domain/interface.go b/internal/domain/interface.go index fe484ec..b26d548 100644 --- a/internal/domain/interface.go +++ b/internal/domain/interface.go @@ -58,6 +58,8 @@ type Interface struct { 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, ...) 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) DisabledReason string // the reason why the interface has been disabled diff --git a/internal/domain/peer.go b/internal/domain/peer.go index b265c20..ea79f80 100644 --- a/internal/domain/peer.go +++ b/internal/domain/peer.go @@ -56,6 +56,8 @@ type Peer struct { // Interface settings for the peer, used to generate the [interface] section in the peer config file 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 { @@ -264,6 +266,7 @@ func ConvertPhysicalPeer(pp *PhysicalPeer) *Peer { Interface: PeerInterfaceConfig{ KeyPair: pp.KeyPair, }, + IsDynamic: false, } if pp.GetExtras() == nil { @@ -277,6 +280,7 @@ func ConvertPhysicalPeer(pp *PhysicalPeer) *Peer { extras := pp.GetExtras().(MikrotikPeerExtras) peer.Notes = extras.Comment 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 peer.Endpoint = NewConfigOption(extras.ClientEndpoint, true) peer.Interface.Type = InterfaceTypeClient @@ -369,6 +373,7 @@ func MergeToPhysicalPeer(pp *PhysicalPeer, p *Peer) { ClientAddress: CidrsToString(p.Interface.Addresses), ClientDns: p.Interface.DnsStr.GetValue(), ClientKeepalive: p.PersistentKeepalive.GetValue(), + Dynamic: p.IsDynamic, } pp.SetExtras(extras) case ControllerTypeLocal: