mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-16 23:56:39 +00:00
Compare commits
27 Commits
dev
...
crowdin_de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
720db1a64d | ||
|
|
6ae907f8aa | ||
|
|
ef1041f3ce | ||
|
|
469132d487 | ||
|
|
798eb8aab4 | ||
|
|
906ee8651a | ||
|
|
2ad3fcc402 | ||
|
|
9479df1627 | ||
|
|
e85ad5fedc | ||
|
|
2975f99d48 | ||
|
|
114a645323 | ||
|
|
e118b29859 | ||
|
|
f5ad78e04c | ||
|
|
1ee42633eb | ||
|
|
e00704b9bb | ||
|
|
60111b1a0e | ||
|
|
b3c889dc2f | ||
|
|
59dac54a35 | ||
|
|
e0fd58227f | ||
|
|
88e3a89ddc | ||
|
|
3825a35a6c | ||
|
|
71b74ea383 | ||
|
|
ac1e1c127e | ||
|
|
3496e270d0 | ||
|
|
1bb0dc1cd7 | ||
|
|
7c2b3bfb99 | ||
|
|
6238a19527 |
@@ -2342,8 +2342,8 @@
|
|||||||
"logRetentionEndOfFollowingYear": "Край на следващата година",
|
"logRetentionEndOfFollowingYear": "Край на следващата година",
|
||||||
"actionLogsDescription": "Прегледайте историята на действията, извършени в тази организация",
|
"actionLogsDescription": "Прегледайте историята на действията, извършени в тази организация",
|
||||||
"accessLogsDescription": "Прегледайте заявките за удостоверяване на достъпа до ресурсите в тази организация",
|
"accessLogsDescription": "Прегледайте заявките за удостоверяване на достъпа до ресурсите в тази организация",
|
||||||
"licenseRequiredToUse": "Изисква се лиценз за <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink>, за да използвате тази функция. Тази функция е също достъпна в <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
|
"licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a demo or POC trial</bookADemoLink>.",
|
||||||
"ossEnterpriseEditionRequired": "Необходимо е <enterpriseEditionLink>изданието Enterprise</enterpriseEditionLink>, за да използвате тази функция. Тази функция е също достъпна в <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
|
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a demo or POC trial</bookADemoLink>.",
|
||||||
"certResolver": "Решавач на сертификати",
|
"certResolver": "Решавач на сертификати",
|
||||||
"certResolverDescription": "Изберете решавач на сертификати за използване за този ресурс.",
|
"certResolverDescription": "Изберете решавач на сертификати за използване за този ресурс.",
|
||||||
"selectCertResolver": "Изберете решавач на сертификати",
|
"selectCertResolver": "Изберете решавач на сертификати",
|
||||||
@@ -2680,5 +2680,6 @@
|
|||||||
"approvalsEmptyStateStep2Title": "Активирайте одобрения на устройства",
|
"approvalsEmptyStateStep2Title": "Активирайте одобрения на устройства",
|
||||||
"approvalsEmptyStateStep2Description": "Редактирайте ролята и активирайте опцията 'Изискване на одобрения за устройства'. Потребители с тази роля ще трябва администраторско одобрение за нови устройства.",
|
"approvalsEmptyStateStep2Description": "Редактирайте ролята и активирайте опцията 'Изискване на одобрения за устройства'. Потребители с тази роля ще трябва администраторско одобрение за нови устройства.",
|
||||||
"approvalsEmptyStatePreviewDescription": "Преглед: Когато е активирано, чакащите заявки за устройства ще се появят тук за преглед",
|
"approvalsEmptyStatePreviewDescription": "Преглед: Когато е активирано, чакащите заявки за устройства ще се появят тук за преглед",
|
||||||
"approvalsEmptyStateButtonText": "Управлявайте роли"
|
"approvalsEmptyStateButtonText": "Управлявайте роли",
|
||||||
|
"domainErrorTitle": "We are having trouble verifying your domain"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2342,8 +2342,8 @@
|
|||||||
"logRetentionEndOfFollowingYear": "Konec následujícího roku",
|
"logRetentionEndOfFollowingYear": "Konec následujícího roku",
|
||||||
"actionLogsDescription": "Zobrazit historii akcí provedených v této organizaci",
|
"actionLogsDescription": "Zobrazit historii akcí provedených v této organizaci",
|
||||||
"accessLogsDescription": "Zobrazit žádosti o ověření přístupu pro zdroje v této organizaci",
|
"accessLogsDescription": "Zobrazit žádosti o ověření přístupu pro zdroje v této organizaci",
|
||||||
"licenseRequiredToUse": "Pro použití této funkce je vyžadována licence <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> . Tato funkce je také dostupná v <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
|
"licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a demo or POC trial</bookADemoLink>.",
|
||||||
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> je vyžadována pro použití této funkce. Tato funkce je také k dispozici v <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
|
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a demo or POC trial</bookADemoLink>.",
|
||||||
"certResolver": "Oddělovač certifikátů",
|
"certResolver": "Oddělovač certifikátů",
|
||||||
"certResolverDescription": "Vyberte řešitele certifikátů pro tento dokument.",
|
"certResolverDescription": "Vyberte řešitele certifikátů pro tento dokument.",
|
||||||
"selectCertResolver": "Vyberte řešič certifikátů",
|
"selectCertResolver": "Vyberte řešič certifikátů",
|
||||||
@@ -2680,5 +2680,6 @@
|
|||||||
"approvalsEmptyStateStep2Title": "Povolit schválení zařízení",
|
"approvalsEmptyStateStep2Title": "Povolit schválení zařízení",
|
||||||
"approvalsEmptyStateStep2Description": "Upravte roli a povolte možnost 'Vyžadovat schválení zařízení'. Uživatelé s touto rolí budou potřebovat schválení pro nová zařízení správce.",
|
"approvalsEmptyStateStep2Description": "Upravte roli a povolte možnost 'Vyžadovat schválení zařízení'. Uživatelé s touto rolí budou potřebovat schválení pro nová zařízení správce.",
|
||||||
"approvalsEmptyStatePreviewDescription": "Náhled: Pokud je povoleno, čekající na zařízení se zde zobrazí žádosti o recenzi",
|
"approvalsEmptyStatePreviewDescription": "Náhled: Pokud je povoleno, čekající na zařízení se zde zobrazí žádosti o recenzi",
|
||||||
"approvalsEmptyStateButtonText": "Spravovat role"
|
"approvalsEmptyStateButtonText": "Spravovat role",
|
||||||
|
"domainErrorTitle": "We are having trouble verifying your domain"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2342,8 +2342,8 @@
|
|||||||
"logRetentionEndOfFollowingYear": "Ende des folgenden Jahres",
|
"logRetentionEndOfFollowingYear": "Ende des folgenden Jahres",
|
||||||
"actionLogsDescription": "Verlauf der in dieser Organisation durchgeführten Aktionen anzeigen",
|
"actionLogsDescription": "Verlauf der in dieser Organisation durchgeführten Aktionen anzeigen",
|
||||||
"accessLogsDescription": "Zugriffsauth-Anfragen für Ressourcen in dieser Organisation anzeigen",
|
"accessLogsDescription": "Zugriffsauth-Anfragen für Ressourcen in dieser Organisation anzeigen",
|
||||||
"licenseRequiredToUse": "Um diese Funktion nutzen zu können, ist eine <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> Lizenz erforderlich. Diese Funktion ist auch in der <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> verfügbar.",
|
"licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a demo or POC trial</bookADemoLink>.",
|
||||||
"ossEnterpriseEditionRequired": "Um diese Funktion nutzen zu können, ist die <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> erforderlich. Diese Funktion ist auch in der <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> verfügbar.",
|
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a demo or POC trial</bookADemoLink>.",
|
||||||
"certResolver": "Zertifikatsauflöser",
|
"certResolver": "Zertifikatsauflöser",
|
||||||
"certResolverDescription": "Wählen Sie den Zertifikatslöser aus, der für diese Ressource verwendet werden soll.",
|
"certResolverDescription": "Wählen Sie den Zertifikatslöser aus, der für diese Ressource verwendet werden soll.",
|
||||||
"selectCertResolver": "Zertifikatsauflöser auswählen",
|
"selectCertResolver": "Zertifikatsauflöser auswählen",
|
||||||
@@ -2680,5 +2680,6 @@
|
|||||||
"approvalsEmptyStateStep2Title": "Gerätegenehmigungen aktivieren",
|
"approvalsEmptyStateStep2Title": "Gerätegenehmigungen aktivieren",
|
||||||
"approvalsEmptyStateStep2Description": "Bearbeite eine Rolle und aktiviere die Option 'Gerätegenehmigung erforderlich'. Benutzer mit dieser Rolle benötigen Administrator-Genehmigung für neue Geräte.",
|
"approvalsEmptyStateStep2Description": "Bearbeite eine Rolle und aktiviere die Option 'Gerätegenehmigung erforderlich'. Benutzer mit dieser Rolle benötigen Administrator-Genehmigung für neue Geräte.",
|
||||||
"approvalsEmptyStatePreviewDescription": "Vorschau: Wenn aktiviert, werden ausstehende Geräteanfragen hier zur Überprüfung angezeigt",
|
"approvalsEmptyStatePreviewDescription": "Vorschau: Wenn aktiviert, werden ausstehende Geräteanfragen hier zur Überprüfung angezeigt",
|
||||||
"approvalsEmptyStateButtonText": "Rollen verwalten"
|
"approvalsEmptyStateButtonText": "Rollen verwalten",
|
||||||
|
"domainErrorTitle": "We are having trouble verifying your domain"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2681,6 +2681,5 @@
|
|||||||
"approvalsEmptyStateStep2Title": "Enable Device Approvals",
|
"approvalsEmptyStateStep2Title": "Enable Device Approvals",
|
||||||
"approvalsEmptyStateStep2Description": "Edit a role and enable the 'Require Device Approvals' option. Users with this role will need admin approval for new devices.",
|
"approvalsEmptyStateStep2Description": "Edit a role and enable the 'Require Device Approvals' option. Users with this role will need admin approval for new devices.",
|
||||||
"approvalsEmptyStatePreviewDescription": "Preview: When enabled, pending device requests will appear here for review",
|
"approvalsEmptyStatePreviewDescription": "Preview: When enabled, pending device requests will appear here for review",
|
||||||
"approvalsEmptyStateButtonText": "Manage Roles",
|
"approvalsEmptyStateButtonText": "Manage Roles"
|
||||||
"domainErrorTitle": "We are having trouble verifying your domain"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2342,8 +2342,8 @@
|
|||||||
"logRetentionEndOfFollowingYear": "Fin del año siguiente",
|
"logRetentionEndOfFollowingYear": "Fin del año siguiente",
|
||||||
"actionLogsDescription": "Ver un historial de acciones realizadas en esta organización",
|
"actionLogsDescription": "Ver un historial de acciones realizadas en esta organización",
|
||||||
"accessLogsDescription": "Ver solicitudes de acceso a los recursos de esta organización",
|
"accessLogsDescription": "Ver solicitudes de acceso a los recursos de esta organización",
|
||||||
"licenseRequiredToUse": "Se requiere una licencia <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> para utilizar esta función. Esta característica también está disponible en <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
|
"licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a demo or POC trial</bookADemoLink>.",
|
||||||
"ossEnterpriseEditionRequired": "La <enterpriseEditionLink>versión Enterprise</enterpriseEditionLink> es necesaria para utilizar esta función. Esta función también está disponible en <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
|
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a demo or POC trial</bookADemoLink>.",
|
||||||
"certResolver": "Resolver certificado",
|
"certResolver": "Resolver certificado",
|
||||||
"certResolverDescription": "Seleccione la resolución de certificados a utilizar para este recurso.",
|
"certResolverDescription": "Seleccione la resolución de certificados a utilizar para este recurso.",
|
||||||
"selectCertResolver": "Seleccionar Resolver Certificado",
|
"selectCertResolver": "Seleccionar Resolver Certificado",
|
||||||
@@ -2680,5 +2680,6 @@
|
|||||||
"approvalsEmptyStateStep2Title": "Habilitar aprobaciones de dispositivo",
|
"approvalsEmptyStateStep2Title": "Habilitar aprobaciones de dispositivo",
|
||||||
"approvalsEmptyStateStep2Description": "Editar un rol y habilitar la opción 'Requerir aprobaciones de dispositivos'. Los usuarios con este rol necesitarán la aprobación del administrador para nuevos dispositivos.",
|
"approvalsEmptyStateStep2Description": "Editar un rol y habilitar la opción 'Requerir aprobaciones de dispositivos'. Los usuarios con este rol necesitarán la aprobación del administrador para nuevos dispositivos.",
|
||||||
"approvalsEmptyStatePreviewDescription": "Vista previa: Cuando está habilitado, las solicitudes de dispositivo pendientes aparecerán aquí para su revisión",
|
"approvalsEmptyStatePreviewDescription": "Vista previa: Cuando está habilitado, las solicitudes de dispositivo pendientes aparecerán aquí para su revisión",
|
||||||
"approvalsEmptyStateButtonText": "Administrar roles"
|
"approvalsEmptyStateButtonText": "Administrar roles",
|
||||||
|
"domainErrorTitle": "We are having trouble verifying your domain"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2342,8 +2342,8 @@
|
|||||||
"logRetentionEndOfFollowingYear": "Fin de l'année suivante",
|
"logRetentionEndOfFollowingYear": "Fin de l'année suivante",
|
||||||
"actionLogsDescription": "Voir l'historique des actions effectuées dans cette organisation",
|
"actionLogsDescription": "Voir l'historique des actions effectuées dans cette organisation",
|
||||||
"accessLogsDescription": "Voir les demandes d'authentification d'accès aux ressources de cette organisation",
|
"accessLogsDescription": "Voir les demandes d'authentification d'accès aux ressources de cette organisation",
|
||||||
"licenseRequiredToUse": "Une licence <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> est nécessaire pour utiliser cette fonctionnalité. Cette fonctionnalité est également disponible dans <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
|
"licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a demo or POC trial</bookADemoLink>.",
|
||||||
"ossEnterpriseEditionRequired": "La version <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> est requise pour utiliser cette fonctionnalité. Cette fonctionnalité est également disponible dans <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
|
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a demo or POC trial</bookADemoLink>.",
|
||||||
"certResolver": "Résolveur de certificat",
|
"certResolver": "Résolveur de certificat",
|
||||||
"certResolverDescription": "Sélectionnez le solveur de certificat à utiliser pour cette ressource.",
|
"certResolverDescription": "Sélectionnez le solveur de certificat à utiliser pour cette ressource.",
|
||||||
"selectCertResolver": "Sélectionnez le résolveur de certificat",
|
"selectCertResolver": "Sélectionnez le résolveur de certificat",
|
||||||
@@ -2680,5 +2680,6 @@
|
|||||||
"approvalsEmptyStateStep2Title": "Activer les autorisations de l'appareil",
|
"approvalsEmptyStateStep2Title": "Activer les autorisations de l'appareil",
|
||||||
"approvalsEmptyStateStep2Description": "Modifier un rôle et activer l'option 'Exiger les autorisations de l'appareil'. Les utilisateurs avec ce rôle auront besoin de l'approbation de l'administrateur pour les nouveaux appareils.",
|
"approvalsEmptyStateStep2Description": "Modifier un rôle et activer l'option 'Exiger les autorisations de l'appareil'. Les utilisateurs avec ce rôle auront besoin de l'approbation de l'administrateur pour les nouveaux appareils.",
|
||||||
"approvalsEmptyStatePreviewDescription": "Aperçu: Lorsque cette option est activée, les demandes de périphérique en attente apparaîtront ici pour vérification",
|
"approvalsEmptyStatePreviewDescription": "Aperçu: Lorsque cette option est activée, les demandes de périphérique en attente apparaîtront ici pour vérification",
|
||||||
"approvalsEmptyStateButtonText": "Gérer les rôles"
|
"approvalsEmptyStateButtonText": "Gérer les rôles",
|
||||||
|
"domainErrorTitle": "We are having trouble verifying your domain"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2342,8 +2342,8 @@
|
|||||||
"logRetentionEndOfFollowingYear": "Fine dell'anno successivo",
|
"logRetentionEndOfFollowingYear": "Fine dell'anno successivo",
|
||||||
"actionLogsDescription": "Visualizza una cronologia delle azioni eseguite in questa organizzazione",
|
"actionLogsDescription": "Visualizza una cronologia delle azioni eseguite in questa organizzazione",
|
||||||
"accessLogsDescription": "Visualizza le richieste di autenticazione di accesso per le risorse in questa organizzazione",
|
"accessLogsDescription": "Visualizza le richieste di autenticazione di accesso per le risorse in questa organizzazione",
|
||||||
"licenseRequiredToUse": "Per utilizzare questa funzione è necessaria una licenza <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> . Questa funzionalità è disponibile anche in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
|
"licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a demo or POC trial</bookADemoLink>.",
|
||||||
"ossEnterpriseEditionRequired": "L' <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> è necessaria per utilizzare questa funzione. Questa funzionalità è disponibile anche in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
|
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a demo or POC trial</bookADemoLink>.",
|
||||||
"certResolver": "Risolutore Di Certificato",
|
"certResolver": "Risolutore Di Certificato",
|
||||||
"certResolverDescription": "Selezionare il risolutore di certificati da usare per questa risorsa.",
|
"certResolverDescription": "Selezionare il risolutore di certificati da usare per questa risorsa.",
|
||||||
"selectCertResolver": "Seleziona Risolutore Di Certificato",
|
"selectCertResolver": "Seleziona Risolutore Di Certificato",
|
||||||
@@ -2680,5 +2680,6 @@
|
|||||||
"approvalsEmptyStateStep2Title": "Abilita Approvazioni Dispositivo",
|
"approvalsEmptyStateStep2Title": "Abilita Approvazioni Dispositivo",
|
||||||
"approvalsEmptyStateStep2Description": "Modifica un ruolo e abilita l'opzione 'Richiedi l'approvazione del dispositivo'. Gli utenti con questo ruolo avranno bisogno dell'approvazione dell'amministratore per i nuovi dispositivi.",
|
"approvalsEmptyStateStep2Description": "Modifica un ruolo e abilita l'opzione 'Richiedi l'approvazione del dispositivo'. Gli utenti con questo ruolo avranno bisogno dell'approvazione dell'amministratore per i nuovi dispositivi.",
|
||||||
"approvalsEmptyStatePreviewDescription": "Anteprima: quando abilitato, le richieste di dispositivo in attesa appariranno qui per la revisione",
|
"approvalsEmptyStatePreviewDescription": "Anteprima: quando abilitato, le richieste di dispositivo in attesa appariranno qui per la revisione",
|
||||||
"approvalsEmptyStateButtonText": "Gestisci Ruoli"
|
"approvalsEmptyStateButtonText": "Gestisci Ruoli",
|
||||||
|
"domainErrorTitle": "We are having trouble verifying your domain"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2342,8 +2342,8 @@
|
|||||||
"logRetentionEndOfFollowingYear": "다음 연도 말",
|
"logRetentionEndOfFollowingYear": "다음 연도 말",
|
||||||
"actionLogsDescription": "이 조직에서 수행된 작업의 기록을 봅니다",
|
"actionLogsDescription": "이 조직에서 수행된 작업의 기록을 봅니다",
|
||||||
"accessLogsDescription": "이 조직의 자원에 대한 접근 인증 요청을 확인합니다",
|
"accessLogsDescription": "이 조직의 자원에 대한 접근 인증 요청을 확인합니다",
|
||||||
"licenseRequiredToUse": "이 기능을 사용하려면 <enterpriseLicenseLink>엔터프라이즈 에디션</enterpriseLicenseLink> 라이선스가 필요합니다. 이 기능은 <pangolinCloudLink>판골린 클라우드</pangolinCloudLink>에서도 사용할 수 있습니다.",
|
"licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a demo or POC trial</bookADemoLink>.",
|
||||||
"ossEnterpriseEditionRequired": "이 기능을 사용하려면 <enterpriseEditionLink>엔터프라이즈 에디션</enterpriseEditionLink>이 필요합니다. 이 기능은 <pangolinCloudLink>판골린 클라우드</pangolinCloudLink>에서도 사용할 수 있습니다.",
|
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a demo or POC trial</bookADemoLink>.",
|
||||||
"certResolver": "인증서 해결사",
|
"certResolver": "인증서 해결사",
|
||||||
"certResolverDescription": "이 리소스에 사용할 인증서 해결사를 선택하세요.",
|
"certResolverDescription": "이 리소스에 사용할 인증서 해결사를 선택하세요.",
|
||||||
"selectCertResolver": "인증서 해결사 선택",
|
"selectCertResolver": "인증서 해결사 선택",
|
||||||
@@ -2680,5 +2680,6 @@
|
|||||||
"approvalsEmptyStateStep2Title": "장치 승인 활성화",
|
"approvalsEmptyStateStep2Title": "장치 승인 활성화",
|
||||||
"approvalsEmptyStateStep2Description": "역할을 편집하고 '장치 승인 요구' 옵션을 활성화하세요. 이 역할을 가진 사용자는 새 장치에 대해 관리자의 승인이 필요합니다.",
|
"approvalsEmptyStateStep2Description": "역할을 편집하고 '장치 승인 요구' 옵션을 활성화하세요. 이 역할을 가진 사용자는 새 장치에 대해 관리자의 승인이 필요합니다.",
|
||||||
"approvalsEmptyStatePreviewDescription": "미리 보기: 활성화된 경우, 승인 대기 중인 장치 요청이 검토용으로 여기에 표시됩니다.",
|
"approvalsEmptyStatePreviewDescription": "미리 보기: 활성화된 경우, 승인 대기 중인 장치 요청이 검토용으로 여기에 표시됩니다.",
|
||||||
"approvalsEmptyStateButtonText": "역할 관리"
|
"approvalsEmptyStateButtonText": "역할 관리",
|
||||||
|
"domainErrorTitle": "We are having trouble verifying your domain"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2342,8 +2342,8 @@
|
|||||||
"logRetentionEndOfFollowingYear": "Slutt på neste år",
|
"logRetentionEndOfFollowingYear": "Slutt på neste år",
|
||||||
"actionLogsDescription": "Vis historikk for handlinger som er utført i denne organisasjonen",
|
"actionLogsDescription": "Vis historikk for handlinger som er utført i denne organisasjonen",
|
||||||
"accessLogsDescription": "Vis autoriseringsforespørsler for ressurser i denne organisasjonen",
|
"accessLogsDescription": "Vis autoriseringsforespørsler for ressurser i denne organisasjonen",
|
||||||
"licenseRequiredToUse": "En <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> lisens er påkrevd for å bruke denne funksjonen. Denne funksjonen er også tilgjengelig i <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
|
"licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a demo or POC trial</bookADemoLink>.",
|
||||||
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> er nødvendig for å bruke denne funksjonen. Denne funksjonen er også tilgjengelig i <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
|
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a demo or POC trial</bookADemoLink>.",
|
||||||
"certResolver": "Sertifikat løser",
|
"certResolver": "Sertifikat løser",
|
||||||
"certResolverDescription": "Velg sertifikatløser som skal brukes for denne ressursen.",
|
"certResolverDescription": "Velg sertifikatløser som skal brukes for denne ressursen.",
|
||||||
"selectCertResolver": "Velg sertifikatløser",
|
"selectCertResolver": "Velg sertifikatløser",
|
||||||
@@ -2680,5 +2680,6 @@
|
|||||||
"approvalsEmptyStateStep2Title": "Aktiver enhetsgodkjenninger",
|
"approvalsEmptyStateStep2Title": "Aktiver enhetsgodkjenninger",
|
||||||
"approvalsEmptyStateStep2Description": "Rediger en rolle og aktiver alternativet 'Kreve enhetsgodkjenninger'. Brukere med denne rollen vil trenge administratorgodkjenning for nye enheter.",
|
"approvalsEmptyStateStep2Description": "Rediger en rolle og aktiver alternativet 'Kreve enhetsgodkjenninger'. Brukere med denne rollen vil trenge administratorgodkjenning for nye enheter.",
|
||||||
"approvalsEmptyStatePreviewDescription": "Forhåndsvisning: Når aktivert, ventende enhets forespørsler vil vises her for vurdering",
|
"approvalsEmptyStatePreviewDescription": "Forhåndsvisning: Når aktivert, ventende enhets forespørsler vil vises her for vurdering",
|
||||||
"approvalsEmptyStateButtonText": "Administrer Roller"
|
"approvalsEmptyStateButtonText": "Administrer Roller",
|
||||||
|
"domainErrorTitle": "We are having trouble verifying your domain"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2342,8 +2342,8 @@
|
|||||||
"logRetentionEndOfFollowingYear": "Einde van volgend jaar",
|
"logRetentionEndOfFollowingYear": "Einde van volgend jaar",
|
||||||
"actionLogsDescription": "Bekijk een geschiedenis van acties die worden uitgevoerd in deze organisatie",
|
"actionLogsDescription": "Bekijk een geschiedenis van acties die worden uitgevoerd in deze organisatie",
|
||||||
"accessLogsDescription": "Toegangsverificatieverzoeken voor resources in deze organisatie bekijken",
|
"accessLogsDescription": "Toegangsverificatieverzoeken voor resources in deze organisatie bekijken",
|
||||||
"licenseRequiredToUse": "Een <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> licentie is vereist om deze functie te gebruiken. Deze functie is ook beschikbaar in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
|
"licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a demo or POC trial</bookADemoLink>.",
|
||||||
"ossEnterpriseEditionRequired": "De <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is vereist om deze functie te gebruiken. Deze functie is ook beschikbaar in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
|
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a demo or POC trial</bookADemoLink>.",
|
||||||
"certResolver": "Certificaat Resolver",
|
"certResolver": "Certificaat Resolver",
|
||||||
"certResolverDescription": "Selecteer de certificaat resolver die moet worden gebruikt voor deze resource.",
|
"certResolverDescription": "Selecteer de certificaat resolver die moet worden gebruikt voor deze resource.",
|
||||||
"selectCertResolver": "Certificaat Resolver selecteren",
|
"selectCertResolver": "Certificaat Resolver selecteren",
|
||||||
@@ -2680,5 +2680,6 @@
|
|||||||
"approvalsEmptyStateStep2Title": "Toestel goedkeuringen inschakelen",
|
"approvalsEmptyStateStep2Title": "Toestel goedkeuringen inschakelen",
|
||||||
"approvalsEmptyStateStep2Description": "Bewerk een rol en schakel de optie 'Vereist Apparaat Goedkeuringen' in. Gebruikers met deze rol hebben admin goedkeuring nodig voor nieuwe apparaten.",
|
"approvalsEmptyStateStep2Description": "Bewerk een rol en schakel de optie 'Vereist Apparaat Goedkeuringen' in. Gebruikers met deze rol hebben admin goedkeuring nodig voor nieuwe apparaten.",
|
||||||
"approvalsEmptyStatePreviewDescription": "Voorbeeld: Indien ingeschakeld, zullen in afwachting van apparaatverzoeken hier verschijnen om te beoordelen",
|
"approvalsEmptyStatePreviewDescription": "Voorbeeld: Indien ingeschakeld, zullen in afwachting van apparaatverzoeken hier verschijnen om te beoordelen",
|
||||||
"approvalsEmptyStateButtonText": "Rollen beheren"
|
"approvalsEmptyStateButtonText": "Rollen beheren",
|
||||||
|
"domainErrorTitle": "We are having trouble verifying your domain"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2342,8 +2342,8 @@
|
|||||||
"logRetentionEndOfFollowingYear": "Koniec następnego roku",
|
"logRetentionEndOfFollowingYear": "Koniec następnego roku",
|
||||||
"actionLogsDescription": "Zobacz historię działań wykonywanych w tej organizacji",
|
"actionLogsDescription": "Zobacz historię działań wykonywanych w tej organizacji",
|
||||||
"accessLogsDescription": "Wyświetl prośby o autoryzację dostępu do zasobów w tej organizacji",
|
"accessLogsDescription": "Wyświetl prośby o autoryzację dostępu do zasobów w tej organizacji",
|
||||||
"licenseRequiredToUse": "Do korzystania z tej funkcji wymagana jest licencja <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> . Ta funkcja jest również dostępna w <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
|
"licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a demo or POC trial</bookADemoLink>.",
|
||||||
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> jest wymagany do korzystania z tej funkcji. Ta funkcja jest również dostępna w <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
|
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a demo or POC trial</bookADemoLink>.",
|
||||||
"certResolver": "Rozwiązywanie certyfikatów",
|
"certResolver": "Rozwiązywanie certyfikatów",
|
||||||
"certResolverDescription": "Wybierz resolver certyfikatów do użycia dla tego zasobu.",
|
"certResolverDescription": "Wybierz resolver certyfikatów do użycia dla tego zasobu.",
|
||||||
"selectCertResolver": "Wybierz Resolver certyfikatów",
|
"selectCertResolver": "Wybierz Resolver certyfikatów",
|
||||||
@@ -2680,5 +2680,6 @@
|
|||||||
"approvalsEmptyStateStep2Title": "Włącz zatwierdzanie urządzenia",
|
"approvalsEmptyStateStep2Title": "Włącz zatwierdzanie urządzenia",
|
||||||
"approvalsEmptyStateStep2Description": "Edytuj rolę i włącz opcję \"Wymagaj zatwierdzenia urządzenia\". Użytkownicy z tą rolą będą potrzebowali zatwierdzenia administratora dla nowych urządzeń.",
|
"approvalsEmptyStateStep2Description": "Edytuj rolę i włącz opcję \"Wymagaj zatwierdzenia urządzenia\". Użytkownicy z tą rolą będą potrzebowali zatwierdzenia administratora dla nowych urządzeń.",
|
||||||
"approvalsEmptyStatePreviewDescription": "Podgląd: Gdy włączone, oczekujące prośby o sprawdzenie pojawią się tutaj",
|
"approvalsEmptyStatePreviewDescription": "Podgląd: Gdy włączone, oczekujące prośby o sprawdzenie pojawią się tutaj",
|
||||||
"approvalsEmptyStateButtonText": "Zarządzaj rolami"
|
"approvalsEmptyStateButtonText": "Zarządzaj rolami",
|
||||||
|
"domainErrorTitle": "We are having trouble verifying your domain"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2342,8 +2342,8 @@
|
|||||||
"logRetentionEndOfFollowingYear": "Fim do ano seguinte",
|
"logRetentionEndOfFollowingYear": "Fim do ano seguinte",
|
||||||
"actionLogsDescription": "Visualizar histórico de ações realizadas nesta organização",
|
"actionLogsDescription": "Visualizar histórico de ações realizadas nesta organização",
|
||||||
"accessLogsDescription": "Ver solicitações de autenticação de recursos nesta organização",
|
"accessLogsDescription": "Ver solicitações de autenticação de recursos nesta organização",
|
||||||
"licenseRequiredToUse": "Uma licença <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> é necessária para usar este recurso. Este recurso também está disponível no <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
|
"licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a demo or POC trial</bookADemoLink>.",
|
||||||
"ossEnterpriseEditionRequired": "O <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> é necessário para usar este recurso. Este recurso também está disponível no <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
|
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a demo or POC trial</bookADemoLink>.",
|
||||||
"certResolver": "Resolvedor de Certificado",
|
"certResolver": "Resolvedor de Certificado",
|
||||||
"certResolverDescription": "Selecione o resolvedor de certificados para este recurso.",
|
"certResolverDescription": "Selecione o resolvedor de certificados para este recurso.",
|
||||||
"selectCertResolver": "Selecionar solucionador de certificado",
|
"selectCertResolver": "Selecionar solucionador de certificado",
|
||||||
@@ -2680,5 +2680,6 @@
|
|||||||
"approvalsEmptyStateStep2Title": "Habilitar Aprovações do Dispositivo",
|
"approvalsEmptyStateStep2Title": "Habilitar Aprovações do Dispositivo",
|
||||||
"approvalsEmptyStateStep2Description": "Editar uma função e habilitar a opção 'Exigir aprovação de dispositivos'. Usuários com essa função precisarão de aprovação de administrador para novos dispositivos.",
|
"approvalsEmptyStateStep2Description": "Editar uma função e habilitar a opção 'Exigir aprovação de dispositivos'. Usuários com essa função precisarão de aprovação de administrador para novos dispositivos.",
|
||||||
"approvalsEmptyStatePreviewDescription": "Pré-visualização: Quando ativado, solicitações de dispositivo pendentes aparecerão aqui para revisão",
|
"approvalsEmptyStatePreviewDescription": "Pré-visualização: Quando ativado, solicitações de dispositivo pendentes aparecerão aqui para revisão",
|
||||||
"approvalsEmptyStateButtonText": "Gerir Funções"
|
"approvalsEmptyStateButtonText": "Gerir Funções",
|
||||||
|
"domainErrorTitle": "We are having trouble verifying your domain"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2342,8 +2342,8 @@
|
|||||||
"logRetentionEndOfFollowingYear": "Конец следующего года",
|
"logRetentionEndOfFollowingYear": "Конец следующего года",
|
||||||
"actionLogsDescription": "Просмотр истории действий, выполненных в этой организации",
|
"actionLogsDescription": "Просмотр истории действий, выполненных в этой организации",
|
||||||
"accessLogsDescription": "Просмотр запросов авторизации доступа к ресурсам этой организации",
|
"accessLogsDescription": "Просмотр запросов авторизации доступа к ресурсам этой организации",
|
||||||
"licenseRequiredToUse": "Лицензия на <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> требуется для использования этой функции. Эта функция также доступна в <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
|
"licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a demo or POC trial</bookADemoLink>.",
|
||||||
"ossEnterpriseEditionRequired": "Для использования этой функции требуется <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink>. Эта функция также доступна в <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
|
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a demo or POC trial</bookADemoLink>.",
|
||||||
"certResolver": "Резольвер сертификата",
|
"certResolver": "Резольвер сертификата",
|
||||||
"certResolverDescription": "Выберите резолвер сертификата, который будет использоваться для этого ресурса.",
|
"certResolverDescription": "Выберите резолвер сертификата, который будет использоваться для этого ресурса.",
|
||||||
"selectCertResolver": "Выберите резолвер сертификата",
|
"selectCertResolver": "Выберите резолвер сертификата",
|
||||||
@@ -2680,5 +2680,6 @@
|
|||||||
"approvalsEmptyStateStep2Title": "Включить утверждения устройства",
|
"approvalsEmptyStateStep2Title": "Включить утверждения устройства",
|
||||||
"approvalsEmptyStateStep2Description": "Редактировать роль и включить опцию 'Требовать утверждения устройств'. Пользователям с этой ролью потребуется подтверждение администратора для новых устройств.",
|
"approvalsEmptyStateStep2Description": "Редактировать роль и включить опцию 'Требовать утверждения устройств'. Пользователям с этой ролью потребуется подтверждение администратора для новых устройств.",
|
||||||
"approvalsEmptyStatePreviewDescription": "Предпросмотр: Если включено, ожидающие запросы на устройство появятся здесь для проверки",
|
"approvalsEmptyStatePreviewDescription": "Предпросмотр: Если включено, ожидающие запросы на устройство появятся здесь для проверки",
|
||||||
"approvalsEmptyStateButtonText": "Управление ролями"
|
"approvalsEmptyStateButtonText": "Управление ролями",
|
||||||
|
"domainErrorTitle": "We are having trouble verifying your domain"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2342,8 +2342,8 @@
|
|||||||
"logRetentionEndOfFollowingYear": "Bir sonraki yılın sonu",
|
"logRetentionEndOfFollowingYear": "Bir sonraki yılın sonu",
|
||||||
"actionLogsDescription": "Bu organizasyondaki eylemler geçmişini görüntüleyin",
|
"actionLogsDescription": "Bu organizasyondaki eylemler geçmişini görüntüleyin",
|
||||||
"accessLogsDescription": "Bu organizasyondaki kaynaklar için erişim kimlik doğrulama isteklerini görüntüleyin",
|
"accessLogsDescription": "Bu organizasyondaki kaynaklar için erişim kimlik doğrulama isteklerini görüntüleyin",
|
||||||
"licenseRequiredToUse": "Bu özelliği kullanmak için bir <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> lisansı gereklidir. Bu özellik ayrıca <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>'da da mevcuttur.",
|
"licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a demo or POC trial</bookADemoLink>.",
|
||||||
"ossEnterpriseEditionRequired": "Bu özelliği kullanmak için <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> gereklidir. Bu özellik ayrıca <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>'da da mevcuttur.",
|
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a demo or POC trial</bookADemoLink>.",
|
||||||
"certResolver": "Sertifika Çözücü",
|
"certResolver": "Sertifika Çözücü",
|
||||||
"certResolverDescription": "Bu kaynak için kullanılacak sertifika çözücüsünü seçin.",
|
"certResolverDescription": "Bu kaynak için kullanılacak sertifika çözücüsünü seçin.",
|
||||||
"selectCertResolver": "Sertifika Çözücü Seçin",
|
"selectCertResolver": "Sertifika Çözücü Seçin",
|
||||||
@@ -2680,5 +2680,6 @@
|
|||||||
"approvalsEmptyStateStep2Title": "Cihaz Onaylarını Etkinleştir",
|
"approvalsEmptyStateStep2Title": "Cihaz Onaylarını Etkinleştir",
|
||||||
"approvalsEmptyStateStep2Description": "Bir rolü düzenleyin ve 'Cihaz Onaylarını Gerektir' seçeneğini etkinleştirin. Bu role sahip kullanıcıların yeni cihazlar için yönetici onayına ihtiyacı olacaktır.",
|
"approvalsEmptyStateStep2Description": "Bir rolü düzenleyin ve 'Cihaz Onaylarını Gerektir' seçeneğini etkinleştirin. Bu role sahip kullanıcıların yeni cihazlar için yönetici onayına ihtiyacı olacaktır.",
|
||||||
"approvalsEmptyStatePreviewDescription": "Önizleme: Etkinleştirildiğinde, bekleyen cihaz talepleri incelenmek üzere burada görünecektir.",
|
"approvalsEmptyStatePreviewDescription": "Önizleme: Etkinleştirildiğinde, bekleyen cihaz talepleri incelenmek üzere burada görünecektir.",
|
||||||
"approvalsEmptyStateButtonText": "Rolleri Yönet"
|
"approvalsEmptyStateButtonText": "Rolleri Yönet",
|
||||||
|
"domainErrorTitle": "We are having trouble verifying your domain"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2342,8 +2342,8 @@
|
|||||||
"logRetentionEndOfFollowingYear": "下一年结束",
|
"logRetentionEndOfFollowingYear": "下一年结束",
|
||||||
"actionLogsDescription": "查看此机构执行的操作历史",
|
"actionLogsDescription": "查看此机构执行的操作历史",
|
||||||
"accessLogsDescription": "查看此机构资源的访问认证请求",
|
"accessLogsDescription": "查看此机构资源的访问认证请求",
|
||||||
"licenseRequiredToUse": "需要 <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> 许可才能使用此功能。此功能也可在 <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> 中使用。",
|
"licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a demo or POC trial</bookADemoLink>.",
|
||||||
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> 需要使用此功能。此功能也可在 <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> 中使用。",
|
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a demo or POC trial</bookADemoLink>.",
|
||||||
"certResolver": "证书解决器",
|
"certResolver": "证书解决器",
|
||||||
"certResolverDescription": "选择用于此资源的证书解析器。",
|
"certResolverDescription": "选择用于此资源的证书解析器。",
|
||||||
"selectCertResolver": "选择证书解析",
|
"selectCertResolver": "选择证书解析",
|
||||||
@@ -2680,5 +2680,6 @@
|
|||||||
"approvalsEmptyStateStep2Title": "启用设备批准",
|
"approvalsEmptyStateStep2Title": "启用设备批准",
|
||||||
"approvalsEmptyStateStep2Description": "编辑角色并启用“需要设备审批”选项。具有此角色的用户需要管理员批准新设备。",
|
"approvalsEmptyStateStep2Description": "编辑角色并启用“需要设备审批”选项。具有此角色的用户需要管理员批准新设备。",
|
||||||
"approvalsEmptyStatePreviewDescription": "预览:如果启用,待处理设备请求将出现在这里供审核",
|
"approvalsEmptyStatePreviewDescription": "预览:如果启用,待处理设备请求将出现在这里供审核",
|
||||||
"approvalsEmptyStateButtonText": "管理角色"
|
"approvalsEmptyStateButtonText": "管理角色",
|
||||||
|
"domainErrorTitle": "We are having trouble verifying your domain"
|
||||||
}
|
}
|
||||||
|
|||||||
3948
package-lock.json
generated
3948
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
37
package.json
37
package.json
@@ -33,7 +33,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@asteasolutions/zod-to-openapi": "8.4.1",
|
"@asteasolutions/zod-to-openapi": "8.4.1",
|
||||||
"@aws-sdk/client-s3": "3.1004.0",
|
"@aws-sdk/client-s3": "3.989.0",
|
||||||
"@faker-js/faker": "10.3.0",
|
"@faker-js/faker": "10.3.0",
|
||||||
"@headlessui/react": "2.2.9",
|
"@headlessui/react": "2.2.9",
|
||||||
"@hookform/resolvers": "5.2.2",
|
"@hookform/resolvers": "5.2.2",
|
||||||
@@ -80,16 +80,16 @@
|
|||||||
"d3": "7.9.0",
|
"d3": "7.9.0",
|
||||||
"drizzle-orm": "0.45.1",
|
"drizzle-orm": "0.45.1",
|
||||||
"express": "5.2.1",
|
"express": "5.2.1",
|
||||||
"express-rate-limit": "8.3.0",
|
"express-rate-limit": "8.2.1",
|
||||||
"glob": "13.0.6",
|
"glob": "13.0.6",
|
||||||
"helmet": "8.1.0",
|
"helmet": "8.1.0",
|
||||||
"http-errors": "2.0.1",
|
"http-errors": "2.0.1",
|
||||||
"input-otp": "1.4.2",
|
"input-otp": "1.4.2",
|
||||||
"ioredis": "5.10.0",
|
"ioredis": "5.9.3",
|
||||||
"jmespath": "0.16.0",
|
"jmespath": "0.16.0",
|
||||||
"js-yaml": "4.1.1",
|
"js-yaml": "4.1.1",
|
||||||
"jsonwebtoken": "9.0.3",
|
"jsonwebtoken": "9.0.3",
|
||||||
"lucide-react": "0.577.0",
|
"lucide-react": "0.563.0",
|
||||||
"maxmind": "5.0.5",
|
"maxmind": "5.0.5",
|
||||||
"moment": "2.30.1",
|
"moment": "2.30.1",
|
||||||
"next": "15.5.12",
|
"next": "15.5.12",
|
||||||
@@ -99,21 +99,20 @@
|
|||||||
"node-cache": "5.1.2",
|
"node-cache": "5.1.2",
|
||||||
"nodemailer": "8.0.1",
|
"nodemailer": "8.0.1",
|
||||||
"oslo": "1.2.1",
|
"oslo": "1.2.1",
|
||||||
"pg": "8.20.0",
|
"pg": "8.19.0",
|
||||||
"posthog-node": "5.28.0",
|
"posthog-node": "5.26.0",
|
||||||
"qrcode.react": "4.2.0",
|
"qrcode.react": "4.2.0",
|
||||||
"react": "19.2.4",
|
"react": "19.2.4",
|
||||||
"react-day-picker": "9.14.0",
|
"react-day-picker": "9.13.2",
|
||||||
"react-dom": "19.2.4",
|
"react-dom": "19.2.4",
|
||||||
"react-easy-sort": "1.8.0",
|
"react-easy-sort": "1.8.0",
|
||||||
"react-hook-form": "7.71.2",
|
"react-hook-form": "7.71.2",
|
||||||
"react-icons": "5.6.0",
|
"react-icons": "5.5.0",
|
||||||
"recharts": "2.15.4",
|
"recharts": "2.15.4",
|
||||||
"reodotdev": "1.1.0",
|
"reodotdev": "1.0.0",
|
||||||
"resend": "6.9.2",
|
|
||||||
"semver": "7.7.4",
|
"semver": "7.7.4",
|
||||||
"sshpk": "^1.18.0",
|
"sshpk": "^1.18.0",
|
||||||
"stripe": "20.4.1",
|
"stripe": "20.3.1",
|
||||||
"swagger-ui-express": "5.0.1",
|
"swagger-ui-express": "5.0.1",
|
||||||
"tailwind-merge": "3.5.0",
|
"tailwind-merge": "3.5.0",
|
||||||
"topojson-client": "3.1.0",
|
"topojson-client": "3.1.0",
|
||||||
@@ -131,10 +130,10 @@
|
|||||||
"zod-validation-error": "5.0.0"
|
"zod-validation-error": "5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@dotenvx/dotenvx": "1.54.1",
|
"@dotenvx/dotenvx": "1.52.0",
|
||||||
"@esbuild-plugins/tsconfig-paths": "0.1.2",
|
"@esbuild-plugins/tsconfig-paths": "0.1.2",
|
||||||
"@react-email/preview-server": "5.2.8",
|
"@react-email/preview-server": "5.2.8",
|
||||||
"@tailwindcss/postcss": "4.2.1",
|
"@tailwindcss/postcss": "4.1.18",
|
||||||
"@tanstack/react-query-devtools": "5.91.3",
|
"@tanstack/react-query-devtools": "5.91.3",
|
||||||
"@types/better-sqlite3": "7.6.13",
|
"@types/better-sqlite3": "7.6.13",
|
||||||
"@types/cookie-parser": "1.4.10",
|
"@types/cookie-parser": "1.4.10",
|
||||||
@@ -146,10 +145,10 @@
|
|||||||
"@types/jmespath": "0.15.2",
|
"@types/jmespath": "0.15.2",
|
||||||
"@types/js-yaml": "4.0.9",
|
"@types/js-yaml": "4.0.9",
|
||||||
"@types/jsonwebtoken": "9.0.10",
|
"@types/jsonwebtoken": "9.0.10",
|
||||||
"@types/node": "25.3.5",
|
"@types/node": "25.2.3",
|
||||||
"@types/nodemailer": "7.0.11",
|
"@types/nodemailer": "7.0.11",
|
||||||
"@types/nprogress": "0.2.3",
|
"@types/nprogress": "0.2.3",
|
||||||
"@types/pg": "8.18.0",
|
"@types/pg": "8.16.0",
|
||||||
"@types/react": "19.2.14",
|
"@types/react": "19.2.14",
|
||||||
"@types/react-dom": "19.2.3",
|
"@types/react-dom": "19.2.3",
|
||||||
"@types/semver": "7.7.1",
|
"@types/semver": "7.7.1",
|
||||||
@@ -167,14 +166,10 @@
|
|||||||
"postcss": "8.5.6",
|
"postcss": "8.5.6",
|
||||||
"prettier": "3.8.1",
|
"prettier": "3.8.1",
|
||||||
"react-email": "5.2.8",
|
"react-email": "5.2.8",
|
||||||
"tailwindcss": "4.2.1",
|
"tailwindcss": "4.1.18",
|
||||||
"tsc-alias": "1.8.16",
|
"tsc-alias": "1.8.16",
|
||||||
"tsx": "4.21.0",
|
"tsx": "4.21.0",
|
||||||
"typescript": "5.9.3",
|
"typescript": "5.9.3",
|
||||||
"typescript-eslint": "8.56.1"
|
"typescript-eslint": "8.55.0"
|
||||||
},
|
|
||||||
"overrides": {
|
|
||||||
"esbuild": "0.27.3",
|
|
||||||
"dompurify": "3.3.2"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import { flushBandwidthToDb } from "@server/routers/newt/handleReceiveBandwidthMessage";
|
|
||||||
import { flushSiteBandwidthToDb } from "@server/routers/gerbil/receiveBandwidth";
|
|
||||||
import { cleanup as wsCleanup } from "#dynamic/routers/ws";
|
import { cleanup as wsCleanup } from "#dynamic/routers/ws";
|
||||||
|
|
||||||
async function cleanup() {
|
async function cleanup() {
|
||||||
await flushBandwidthToDb();
|
|
||||||
await flushSiteBandwidthToDb();
|
|
||||||
await wsCleanup();
|
await wsCleanup();
|
||||||
|
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
|
|||||||
@@ -328,14 +328,6 @@ export const approvals = pgTable("approvals", {
|
|||||||
.notNull()
|
.notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const bannedEmails = pgTable("bannedEmails", {
|
|
||||||
email: varchar("email", { length: 255 }).primaryKey(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const bannedIps = pgTable("bannedIps", {
|
|
||||||
ip: varchar("ip", { length: 255 }).primaryKey(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type Approval = InferSelectModel<typeof approvals>;
|
export type Approval = InferSelectModel<typeof approvals>;
|
||||||
export type Limit = InferSelectModel<typeof limits>;
|
export type Limit = InferSelectModel<typeof limits>;
|
||||||
export type Account = InferSelectModel<typeof account>;
|
export type Account = InferSelectModel<typeof account>;
|
||||||
|
|||||||
@@ -22,8 +22,7 @@ export const domains = pgTable("domains", {
|
|||||||
tries: integer("tries").notNull().default(0),
|
tries: integer("tries").notNull().default(0),
|
||||||
certResolver: varchar("certResolver"),
|
certResolver: varchar("certResolver"),
|
||||||
customCertResolver: varchar("customCertResolver"),
|
customCertResolver: varchar("customCertResolver"),
|
||||||
preferWildcardCert: boolean("preferWildcardCert"),
|
preferWildcardCert: boolean("preferWildcardCert")
|
||||||
errorMessage: text("errorMessage")
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const dnsRecords = pgTable("dnsRecords", {
|
export const dnsRecords = pgTable("dnsRecords", {
|
||||||
@@ -89,7 +88,6 @@ export const sites = pgTable("sites", {
|
|||||||
lastBandwidthUpdate: varchar("lastBandwidthUpdate"),
|
lastBandwidthUpdate: varchar("lastBandwidthUpdate"),
|
||||||
type: varchar("type").notNull(), // "newt" or "wireguard"
|
type: varchar("type").notNull(), // "newt" or "wireguard"
|
||||||
online: boolean("online").notNull().default(false),
|
online: boolean("online").notNull().default(false),
|
||||||
lastPing: integer("lastPing"),
|
|
||||||
address: varchar("address"),
|
address: varchar("address"),
|
||||||
endpoint: varchar("endpoint"),
|
endpoint: varchar("endpoint"),
|
||||||
publicKey: varchar("publicKey"),
|
publicKey: varchar("publicKey"),
|
||||||
@@ -722,7 +720,6 @@ export const clientSitesAssociationsCache = pgTable(
|
|||||||
.notNull(),
|
.notNull(),
|
||||||
siteId: integer("siteId").notNull(),
|
siteId: integer("siteId").notNull(),
|
||||||
isRelayed: boolean("isRelayed").notNull().default(false),
|
isRelayed: boolean("isRelayed").notNull().default(false),
|
||||||
isJitMode: boolean("isJitMode").notNull().default(false),
|
|
||||||
endpoint: varchar("endpoint"),
|
endpoint: varchar("endpoint"),
|
||||||
publicKey: varchar("publicKey") // this will act as the session's public key for hole punching so we can track when it changes
|
publicKey: varchar("publicKey") // this will act as the session's public key for hole punching so we can track when it changes
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -318,15 +318,6 @@ export const approvals = sqliteTable("approvals", {
|
|||||||
.notNull()
|
.notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
export const bannedEmails = sqliteTable("bannedEmails", {
|
|
||||||
email: text("email").primaryKey()
|
|
||||||
});
|
|
||||||
|
|
||||||
export const bannedIps = sqliteTable("bannedIps", {
|
|
||||||
ip: text("ip").primaryKey()
|
|
||||||
});
|
|
||||||
|
|
||||||
export type Approval = InferSelectModel<typeof approvals>;
|
export type Approval = InferSelectModel<typeof approvals>;
|
||||||
export type Limit = InferSelectModel<typeof limits>;
|
export type Limit = InferSelectModel<typeof limits>;
|
||||||
export type Account = InferSelectModel<typeof account>;
|
export type Account = InferSelectModel<typeof account>;
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ export const domains = sqliteTable("domains", {
|
|||||||
failed: integer("failed", { mode: "boolean" }).notNull().default(false),
|
failed: integer("failed", { mode: "boolean" }).notNull().default(false),
|
||||||
tries: integer("tries").notNull().default(0),
|
tries: integer("tries").notNull().default(0),
|
||||||
certResolver: text("certResolver"),
|
certResolver: text("certResolver"),
|
||||||
preferWildcardCert: integer("preferWildcardCert", { mode: "boolean" }),
|
preferWildcardCert: integer("preferWildcardCert", { mode: "boolean" })
|
||||||
errorMessage: text("errorMessage")
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const dnsRecords = sqliteTable("dnsRecords", {
|
export const dnsRecords = sqliteTable("dnsRecords", {
|
||||||
@@ -90,7 +89,6 @@ export const sites = sqliteTable("sites", {
|
|||||||
lastBandwidthUpdate: text("lastBandwidthUpdate"),
|
lastBandwidthUpdate: text("lastBandwidthUpdate"),
|
||||||
type: text("type").notNull(), // "newt" or "wireguard"
|
type: text("type").notNull(), // "newt" or "wireguard"
|
||||||
online: integer("online", { mode: "boolean" }).notNull().default(false),
|
online: integer("online", { mode: "boolean" }).notNull().default(false),
|
||||||
lastPing: integer("lastPing"),
|
|
||||||
|
|
||||||
// exit node stuff that is how to connect to the site when it has a wg server
|
// exit node stuff that is how to connect to the site when it has a wg server
|
||||||
address: text("address"), // this is the address of the wireguard interface in newt
|
address: text("address"), // this is the address of the wireguard interface in newt
|
||||||
@@ -411,9 +409,6 @@ export const clientSitesAssociationsCache = sqliteTable(
|
|||||||
isRelayed: integer("isRelayed", { mode: "boolean" })
|
isRelayed: integer("isRelayed", { mode: "boolean" })
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(false),
|
.default(false),
|
||||||
isJitMode: integer("isJitMode", { mode: "boolean" })
|
|
||||||
.notNull()
|
|
||||||
.default(false),
|
|
||||||
endpoint: text("endpoint"),
|
endpoint: text("endpoint"),
|
||||||
publicKey: text("publicKey") // this will act as the session's public key for hole punching so we can track when it changes
|
publicKey: text("publicKey") // this will act as the session's public key for hole punching so we can track when it changes
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export async function applyBlueprint({
|
|||||||
[target],
|
[target],
|
||||||
matchingHealthcheck ? [matchingHealthcheck] : [],
|
matchingHealthcheck ? [matchingHealthcheck] : [],
|
||||||
result.proxyResource.protocol,
|
result.proxyResource.protocol,
|
||||||
site.newt.version
|
result.proxyResource.proxyPort
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,8 @@ import { cleanUpOldLogs as cleanUpOldActionLogs } from "#dynamic/middlewares/log
|
|||||||
import { cleanUpOldLogs as cleanUpOldRequestLogs } from "@server/routers/badger/logRequestAudit";
|
import { cleanUpOldLogs as cleanUpOldRequestLogs } from "@server/routers/badger/logRequestAudit";
|
||||||
import { gt, or } from "drizzle-orm";
|
import { gt, or } from "drizzle-orm";
|
||||||
import { cleanUpOldFingerprintSnapshots } from "@server/routers/olm/fingerprintingUtils";
|
import { cleanUpOldFingerprintSnapshots } from "@server/routers/olm/fingerprintingUtils";
|
||||||
import { build } from "@server/build";
|
|
||||||
|
|
||||||
export function initLogCleanupInterval() {
|
export function initLogCleanupInterval() {
|
||||||
if (build == "saas") { // skip log cleanup for saas builds
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return setInterval(
|
return setInterval(
|
||||||
async () => {
|
async () => {
|
||||||
const orgsToClean = await db
|
const orgsToClean = await db
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
import semver from "semver";
|
|
||||||
|
|
||||||
export function canCompress(
|
|
||||||
clientVersion: string | null | undefined,
|
|
||||||
type: "newt" | "olm"
|
|
||||||
): boolean {
|
|
||||||
try {
|
|
||||||
if (!clientVersion) return false;
|
|
||||||
// check if it is a valid semver
|
|
||||||
if (!semver.valid(clientVersion)) return false;
|
|
||||||
if (type === "newt") {
|
|
||||||
return semver.gte(clientVersion, "1.10.3");
|
|
||||||
} else if (type === "olm") {
|
|
||||||
return semver.gte(clientVersion, "1.4.3");
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -85,7 +85,9 @@ export async function deleteOrgById(
|
|||||||
deletedNewtIds.push(deletedNewt.newtId);
|
deletedNewtIds.push(deletedNewt.newtId);
|
||||||
await trx
|
await trx
|
||||||
.delete(newtSessions)
|
.delete(newtSessions)
|
||||||
.where(eq(newtSessions.newtId, deletedNewt.newtId));
|
.where(
|
||||||
|
eq(newtSessions.newtId, deletedNewt.newtId)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,38 +121,33 @@ export async function deleteOrgById(
|
|||||||
eq(clientSitesAssociationsCache.clientId, client.clientId)
|
eq(clientSitesAssociationsCache.clientId, client.clientId)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await trx.delete(resources).where(eq(resources.orgId, orgId));
|
|
||||||
|
|
||||||
const allOrgDomains = await trx
|
const allOrgDomains = await trx
|
||||||
.select()
|
.select()
|
||||||
.from(orgDomains)
|
.from(orgDomains)
|
||||||
.innerJoin(domains, eq(orgDomains.domainId, domains.domainId))
|
.innerJoin(domains, eq(domains.domainId, orgDomains.domainId))
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(orgDomains.orgId, orgId),
|
eq(orgDomains.orgId, orgId),
|
||||||
eq(domains.configManaged, false)
|
eq(domains.configManaged, false)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
logger.info(`Found ${allOrgDomains.length} domains to delete`);
|
|
||||||
const domainIdsToDelete: string[] = [];
|
const domainIdsToDelete: string[] = [];
|
||||||
for (const orgDomain of allOrgDomains) {
|
for (const orgDomain of allOrgDomains) {
|
||||||
const domainId = orgDomain.domains.domainId;
|
const domainId = orgDomain.domains.domainId;
|
||||||
const [orgCount] = await trx
|
const orgCount = await trx
|
||||||
.select({ count: count() })
|
.select({ count: sql<number>`count(*)` })
|
||||||
.from(orgDomains)
|
.from(orgDomains)
|
||||||
.where(eq(orgDomains.domainId, domainId));
|
.where(eq(orgDomains.domainId, domainId));
|
||||||
logger.info(`Found ${orgCount.count} orgs using domain ${domainId}`);
|
if (orgCount[0].count === 1) {
|
||||||
if (orgCount.count === 1) {
|
|
||||||
domainIdsToDelete.push(domainId);
|
domainIdsToDelete.push(domainId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logger.info(`Found ${domainIdsToDelete.length} domains to delete`);
|
|
||||||
if (domainIdsToDelete.length > 0) {
|
if (domainIdsToDelete.length > 0) {
|
||||||
await trx
|
await trx
|
||||||
.delete(domains)
|
.delete(domains)
|
||||||
.where(inArray(domains.domainId, domainIdsToDelete));
|
.where(inArray(domains.domainId, domainIdsToDelete));
|
||||||
}
|
}
|
||||||
|
await trx.delete(resources).where(eq(resources.orgId, orgId));
|
||||||
|
|
||||||
await usageService.add(orgId, FeatureId.ORGINIZATIONS, -1, trx); // here we are decreasing the org count BEFORE deleting the org because we need to still be able to get the org to get the billing org inside of here
|
await usageService.add(orgId, FeatureId.ORGINIZATIONS, -1, trx); // here we are decreasing the org count BEFORE deleting the org because we need to still be able to get the org to get the billing org inside of here
|
||||||
|
|
||||||
@@ -234,13 +231,15 @@ export function sendTerminationMessages(result: DeleteOrgByIdResult): void {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
for (const olmId of result.olmsToTerminate) {
|
for (const olmId of result.olmsToTerminate) {
|
||||||
sendTerminateClient(0, OlmErrorCodes.TERMINATED_REKEYED, olmId).catch(
|
sendTerminateClient(
|
||||||
(error) => {
|
0,
|
||||||
|
OlmErrorCodes.TERMINATED_REKEYED,
|
||||||
|
olmId
|
||||||
|
).catch((error) => {
|
||||||
logger.error(
|
logger.error(
|
||||||
"Failed to send termination message to olm:",
|
"Failed to send termination message to olm:",
|
||||||
error
|
error
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -477,7 +477,6 @@ async function handleMessagesForSiteClients(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isAdd) {
|
if (isAdd) {
|
||||||
// TODO: if we are in jit mode here should we really be sending this?
|
|
||||||
await initPeerAddHandshake(
|
await initPeerAddHandshake(
|
||||||
// this will kick off the add peer process for the client
|
// this will kick off the add peer process for the client
|
||||||
client.clientId,
|
client.clientId,
|
||||||
@@ -670,11 +669,7 @@ async function handleSubnetProxyTargetUpdates(
|
|||||||
`Adding ${targetsToAdd.length} subnet proxy targets for siteResource ${siteResource.siteResourceId}`
|
`Adding ${targetsToAdd.length} subnet proxy targets for siteResource ${siteResource.siteResourceId}`
|
||||||
);
|
);
|
||||||
proxyJobs.push(
|
proxyJobs.push(
|
||||||
addSubnetProxyTargets(
|
addSubnetProxyTargets(newt.newtId, targetsToAdd)
|
||||||
newt.newtId,
|
|
||||||
targetsToAdd,
|
|
||||||
newt.version
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -710,11 +705,7 @@ async function handleSubnetProxyTargetUpdates(
|
|||||||
`Removing ${targetsToRemove.length} subnet proxy targets for siteResource ${siteResource.siteResourceId}`
|
`Removing ${targetsToRemove.length} subnet proxy targets for siteResource ${siteResource.siteResourceId}`
|
||||||
);
|
);
|
||||||
proxyJobs.push(
|
proxyJobs.push(
|
||||||
removeSubnetProxyTargets(
|
removeSubnetProxyTargets(newt.newtId, targetsToRemove)
|
||||||
newt.newtId,
|
|
||||||
targetsToRemove,
|
|
||||||
newt.version
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1089,7 +1080,6 @@ async function handleMessagesForClientSites(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: if we are in jit mode here should we really be sending this?
|
|
||||||
await initPeerAddHandshake(
|
await initPeerAddHandshake(
|
||||||
// this will kick off the add peer process for the client
|
// this will kick off the add peer process for the client
|
||||||
client.clientId,
|
client.clientId,
|
||||||
@@ -1156,7 +1146,7 @@ async function handleMessagesForClientResources(
|
|||||||
// Add subnet proxy targets for each site
|
// Add subnet proxy targets for each site
|
||||||
for (const [siteId, resources] of addedBySite.entries()) {
|
for (const [siteId, resources] of addedBySite.entries()) {
|
||||||
const [newt] = await trx
|
const [newt] = await trx
|
||||||
.select({ newtId: newts.newtId, version: newts.version })
|
.select({ newtId: newts.newtId })
|
||||||
.from(newts)
|
.from(newts)
|
||||||
.where(eq(newts.siteId, siteId))
|
.where(eq(newts.siteId, siteId))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
@@ -1178,13 +1168,7 @@ async function handleMessagesForClientResources(
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
if (targets.length > 0) {
|
if (targets.length > 0) {
|
||||||
proxyJobs.push(
|
proxyJobs.push(addSubnetProxyTargets(newt.newtId, targets));
|
||||||
addSubnetProxyTargets(
|
|
||||||
newt.newtId,
|
|
||||||
targets,
|
|
||||||
newt.version
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -1233,7 +1217,7 @@ async function handleMessagesForClientResources(
|
|||||||
// Remove subnet proxy targets for each site
|
// Remove subnet proxy targets for each site
|
||||||
for (const [siteId, resources] of removedBySite.entries()) {
|
for (const [siteId, resources] of removedBySite.entries()) {
|
||||||
const [newt] = await trx
|
const [newt] = await trx
|
||||||
.select({ newtId: newts.newtId, version: newts.version })
|
.select({ newtId: newts.newtId })
|
||||||
.from(newts)
|
.from(newts)
|
||||||
.where(eq(newts.siteId, siteId))
|
.where(eq(newts.siteId, siteId))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
@@ -1256,11 +1240,7 @@ async function handleMessagesForClientResources(
|
|||||||
|
|
||||||
if (targets.length > 0) {
|
if (targets.length > 0) {
|
||||||
proxyJobs.push(
|
proxyJobs.push(
|
||||||
removeSubnetProxyTargets(
|
removeSubnetProxyTargets(newt.newtId, targets)
|
||||||
newt.newtId,
|
|
||||||
targets,
|
|
||||||
newt.version
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,12 +13,8 @@
|
|||||||
|
|
||||||
import { rateLimitService } from "#private/lib/rateLimit";
|
import { rateLimitService } from "#private/lib/rateLimit";
|
||||||
import { cleanup as wsCleanup } from "#private/routers/ws";
|
import { cleanup as wsCleanup } from "#private/routers/ws";
|
||||||
import { flushBandwidthToDb } from "@server/routers/newt/handleReceiveBandwidthMessage";
|
|
||||||
import { flushSiteBandwidthToDb } from "@server/routers/gerbil/receiveBandwidth";
|
|
||||||
|
|
||||||
async function cleanup() {
|
async function cleanup() {
|
||||||
await flushBandwidthToDb();
|
|
||||||
await flushSiteBandwidthToDb();
|
|
||||||
await rateLimitService.cleanup();
|
await rateLimitService.cleanup();
|
||||||
await wsCleanup();
|
await wsCleanup();
|
||||||
|
|
||||||
|
|||||||
@@ -515,6 +515,6 @@ authenticated.post(
|
|||||||
verifyOrgAccess,
|
verifyOrgAccess,
|
||||||
verifyLimits,
|
verifyLimits,
|
||||||
verifyUserHasAction(ActionsEnum.signSshKey),
|
verifyUserHasAction(ActionsEnum.signSshKey),
|
||||||
// logActionAudit(ActionsEnum.signSshKey), // it is handled inside of the function below so we can include more metadata
|
logActionAudit(ActionsEnum.signSshKey),
|
||||||
ssh.signSshKey
|
ssh.signSshKey
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,9 +14,7 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
import {
|
||||||
actionAuditLog,
|
|
||||||
db,
|
db,
|
||||||
logsDb,
|
|
||||||
newts,
|
newts,
|
||||||
roles,
|
roles,
|
||||||
roundTripMessageTracker,
|
roundTripMessageTracker,
|
||||||
@@ -31,12 +29,12 @@ import HttpCode from "@server/types/HttpCode";
|
|||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
import { eq, or, and } from "drizzle-orm";
|
import { eq, or, and } from "drizzle-orm";
|
||||||
import { canUserAccessSiteResource } from "@server/auth/canUserAccessSiteResource";
|
import { canUserAccessSiteResource } from "@server/auth/canUserAccessSiteResource";
|
||||||
import { signPublicKey, getOrgCAKeys } from "@server/lib/sshCA";
|
import { signPublicKey, getOrgCAKeys } from "@server/lib/sshCA";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import { sendToClient } from "#private/routers/ws";
|
import { sendToClient } from "#private/routers/ws";
|
||||||
import { ActionsEnum } from "@server/auth/actions";
|
|
||||||
|
|
||||||
const paramsSchema = z.strictObject({
|
const paramsSchema = z.strictObject({
|
||||||
orgId: z.string().nonempty()
|
orgId: z.string().nonempty()
|
||||||
@@ -66,7 +64,6 @@ export type SignSshKeyResponse = {
|
|||||||
sshUsername: string;
|
sshUsername: string;
|
||||||
sshHost: string;
|
sshHost: string;
|
||||||
resourceId: number;
|
resourceId: number;
|
||||||
siteId: number;
|
|
||||||
keyId: string;
|
keyId: string;
|
||||||
validPrincipals: string[];
|
validPrincipals: string[];
|
||||||
validAfter: string;
|
validAfter: string;
|
||||||
@@ -449,20 +446,6 @@ export async function signSshKey(
|
|||||||
sshHost = resource.destination;
|
sshHost = resource.destination;
|
||||||
}
|
}
|
||||||
|
|
||||||
await logsDb.insert(actionAuditLog).values({
|
|
||||||
timestamp: Math.floor(Date.now() / 1000),
|
|
||||||
orgId: orgId,
|
|
||||||
actorType: "user",
|
|
||||||
actor: req.user?.username ?? "",
|
|
||||||
actorId: req.user?.userId ?? "",
|
|
||||||
action: ActionsEnum.signSshKey,
|
|
||||||
metadata: JSON.stringify({
|
|
||||||
resourceId: resource.siteResourceId,
|
|
||||||
resource: resource.name,
|
|
||||||
siteId: resource.siteId,
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
return response<SignSshKeyResponse>(res, {
|
return response<SignSshKeyResponse>(res, {
|
||||||
data: {
|
data: {
|
||||||
certificate: cert.certificate,
|
certificate: cert.certificate,
|
||||||
@@ -470,7 +453,6 @@ export async function signSshKey(
|
|||||||
sshUsername: usernameToUse,
|
sshUsername: usernameToUse,
|
||||||
sshHost: sshHost,
|
sshHost: sshHost,
|
||||||
resourceId: resource.siteResourceId,
|
resourceId: resource.siteResourceId,
|
||||||
siteId: resource.siteId,
|
|
||||||
keyId: cert.keyId,
|
keyId: cert.keyId,
|
||||||
validPrincipals: cert.validPrincipals,
|
validPrincipals: cert.validPrincipals,
|
||||||
validAfter: cert.validAfter.toISOString(),
|
validAfter: cert.validAfter.toISOString(),
|
||||||
|
|||||||
@@ -17,13 +17,10 @@ import {
|
|||||||
startRemoteExitNodeOfflineChecker
|
startRemoteExitNodeOfflineChecker
|
||||||
} from "#private/routers/remoteExitNode";
|
} from "#private/routers/remoteExitNode";
|
||||||
import { MessageHandler } from "@server/routers/ws";
|
import { MessageHandler } from "@server/routers/ws";
|
||||||
import { build } from "@server/build";
|
|
||||||
|
|
||||||
export const messageHandlers: Record<string, MessageHandler> = {
|
export const messageHandlers: Record<string, MessageHandler> = {
|
||||||
"remoteExitNode/register": handleRemoteExitNodeRegisterMessage,
|
"remoteExitNode/register": handleRemoteExitNodeRegisterMessage,
|
||||||
"remoteExitNode/ping": handleRemoteExitNodePingMessage
|
"remoteExitNode/ping": handleRemoteExitNodePingMessage
|
||||||
};
|
};
|
||||||
|
|
||||||
if (build != "saas") {
|
startRemoteExitNodeOfflineChecker(); // this is to handle the offline check for remote exit nodes
|
||||||
startRemoteExitNodeOfflineChecker(); // this is to handle the offline check for remote exit nodes
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Router, Request, Response } from "express";
|
import { Router, Request, Response } from "express";
|
||||||
import zlib from "zlib";
|
|
||||||
import { Server as HttpServer } from "http";
|
import { Server as HttpServer } from "http";
|
||||||
import { WebSocket, WebSocketServer } from "ws";
|
import { WebSocket, WebSocketServer } from "ws";
|
||||||
import { Socket } from "net";
|
import { Socket } from "net";
|
||||||
@@ -25,8 +24,7 @@ import {
|
|||||||
OlmSession,
|
OlmSession,
|
||||||
RemoteExitNode,
|
RemoteExitNode,
|
||||||
RemoteExitNodeSession,
|
RemoteExitNodeSession,
|
||||||
remoteExitNodes,
|
remoteExitNodes
|
||||||
sites
|
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { db } from "@server/db";
|
import { db } from "@server/db";
|
||||||
@@ -59,13 +57,11 @@ const MAX_PENDING_MESSAGES = 50; // Maximum messages to queue during connection
|
|||||||
const processMessage = async (
|
const processMessage = async (
|
||||||
ws: AuthenticatedWebSocket,
|
ws: AuthenticatedWebSocket,
|
||||||
data: Buffer,
|
data: Buffer,
|
||||||
isBinary: boolean,
|
|
||||||
clientId: string,
|
clientId: string,
|
||||||
clientType: ClientType
|
clientType: ClientType
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const messageBuffer = isBinary ? zlib.gunzipSync(data) : data;
|
const message: WSMessage = JSON.parse(data.toString());
|
||||||
const message: WSMessage = JSON.parse(messageBuffer.toString());
|
|
||||||
|
|
||||||
// logger.debug(
|
// logger.debug(
|
||||||
// `Processing message from ${clientType.toUpperCase()} ID: ${clientId}, type: ${message.type}`
|
// `Processing message from ${clientType.toUpperCase()} ID: ${clientId}, type: ${message.type}`
|
||||||
@@ -80,7 +76,7 @@ const processMessage = async (
|
|||||||
clientId,
|
clientId,
|
||||||
message.type, // Pass message type for granular limiting
|
message.type, // Pass message type for granular limiting
|
||||||
100, // max requests per window
|
100, // max requests per window
|
||||||
100, // max requests per message type per window
|
20, // max requests per message type per window
|
||||||
60 * 1000 // window in milliseconds
|
60 * 1000 // window in milliseconds
|
||||||
);
|
);
|
||||||
if (rateLimitResult.isLimited) {
|
if (rateLimitResult.isLimited) {
|
||||||
@@ -167,16 +163,8 @@ const processPendingMessages = async (
|
|||||||
);
|
);
|
||||||
|
|
||||||
const jobs = [];
|
const jobs = [];
|
||||||
for (const pending of ws.pendingMessages) {
|
for (const messageData of ws.pendingMessages) {
|
||||||
jobs.push(
|
jobs.push(processMessage(ws, messageData, clientId, clientType));
|
||||||
processMessage(
|
|
||||||
ws,
|
|
||||||
pending.data,
|
|
||||||
pending.isBinary,
|
|
||||||
clientId,
|
|
||||||
clientType
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(jobs);
|
await Promise.all(jobs);
|
||||||
@@ -197,12 +185,6 @@ const connectedClients: Map<string, AuthenticatedWebSocket[]> = new Map();
|
|||||||
// Config version tracking map (local to this node, resets on server restart)
|
// Config version tracking map (local to this node, resets on server restart)
|
||||||
const clientConfigVersions: Map<string, number> = new Map();
|
const clientConfigVersions: Map<string, number> = new Map();
|
||||||
|
|
||||||
// Tracks the last Unix timestamp (seconds) at which a ping was flushed to the
|
|
||||||
// DB for a given siteId. Resets on server restart which is fine – the first
|
|
||||||
// ping after startup will always write, re-establishing the online state.
|
|
||||||
const lastPingDbWrite: Map<number, number> = new Map();
|
|
||||||
const PING_DB_WRITE_INTERVAL = 45; // seconds
|
|
||||||
|
|
||||||
// Recovery tracking
|
// Recovery tracking
|
||||||
let isRedisRecoveryInProgress = false;
|
let isRedisRecoveryInProgress = false;
|
||||||
|
|
||||||
@@ -343,9 +325,7 @@ const addClient = async (
|
|||||||
// Check Redis first if enabled
|
// Check Redis first if enabled
|
||||||
if (redisManager.isRedisEnabled()) {
|
if (redisManager.isRedisEnabled()) {
|
||||||
try {
|
try {
|
||||||
const redisVersion = await redisManager.get(
|
const redisVersion = await redisManager.get(getConfigVersionKey(clientId));
|
||||||
getConfigVersionKey(clientId)
|
|
||||||
);
|
|
||||||
if (redisVersion !== null) {
|
if (redisVersion !== null) {
|
||||||
configVersion = parseInt(redisVersion, 10);
|
configVersion = parseInt(redisVersion, 10);
|
||||||
// Sync to local cache
|
// Sync to local cache
|
||||||
@@ -357,10 +337,7 @@ const addClient = async (
|
|||||||
} else {
|
} else {
|
||||||
// Use local cache version and sync to Redis
|
// Use local cache version and sync to Redis
|
||||||
configVersion = clientConfigVersions.get(clientId) || 0;
|
configVersion = clientConfigVersions.get(clientId) || 0;
|
||||||
await redisManager.set(
|
await redisManager.set(getConfigVersionKey(clientId), configVersion.toString());
|
||||||
getConfigVersionKey(clientId),
|
|
||||||
configVersion.toString()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Failed to get/set config version in Redis:", error);
|
logger.error("Failed to get/set config version in Redis:", error);
|
||||||
@@ -455,9 +432,7 @@ const removeClient = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Helper to get the current config version for a client
|
// Helper to get the current config version for a client
|
||||||
const getClientConfigVersion = async (
|
const getClientConfigVersion = async (clientId: string): Promise<number | undefined> => {
|
||||||
clientId: string
|
|
||||||
): Promise<number | undefined> => {
|
|
||||||
// Try Redis first if available
|
// Try Redis first if available
|
||||||
if (redisManager.isRedisEnabled()) {
|
if (redisManager.isRedisEnabled()) {
|
||||||
try {
|
try {
|
||||||
@@ -527,26 +502,11 @@ const sendToClientLocal = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const messageString = JSON.stringify(messageWithVersion);
|
const messageString = JSON.stringify(messageWithVersion);
|
||||||
if (options.compress) {
|
|
||||||
logger.debug(
|
|
||||||
`Message size before compression: ${messageString.length} bytes`
|
|
||||||
);
|
|
||||||
const compressed = zlib.gzipSync(Buffer.from(messageString, "utf8"));
|
|
||||||
logger.debug(
|
|
||||||
`Message size after compression: ${compressed.length} bytes`
|
|
||||||
);
|
|
||||||
clients.forEach((client) => {
|
|
||||||
if (client.readyState === WebSocket.OPEN) {
|
|
||||||
client.send(compressed);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
clients.forEach((client) => {
|
clients.forEach((client) => {
|
||||||
if (client.readyState === WebSocket.OPEN) {
|
if (client.readyState === WebSocket.OPEN) {
|
||||||
client.send(messageString);
|
client.send(messageString);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
@@ -572,16 +532,6 @@ const broadcastToAllExceptLocal = async (
|
|||||||
configVersion
|
configVersion
|
||||||
};
|
};
|
||||||
|
|
||||||
if (options.compress) {
|
|
||||||
const compressed = zlib.gzipSync(
|
|
||||||
Buffer.from(JSON.stringify(messageWithVersion), "utf8")
|
|
||||||
);
|
|
||||||
clients.forEach((client) => {
|
|
||||||
if (client.readyState === WebSocket.OPEN) {
|
|
||||||
client.send(compressed);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
clients.forEach((client) => {
|
clients.forEach((client) => {
|
||||||
if (client.readyState === WebSocket.OPEN) {
|
if (client.readyState === WebSocket.OPEN) {
|
||||||
client.send(JSON.stringify(messageWithVersion));
|
client.send(JSON.stringify(messageWithVersion));
|
||||||
@@ -589,7 +539,6 @@ const broadcastToAllExceptLocal = async (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Cross-node message sending (via Redis)
|
// Cross-node message sending (via Redis)
|
||||||
@@ -813,7 +762,7 @@ const setupConnection = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set up message handler FIRST to prevent race condition
|
// Set up message handler FIRST to prevent race condition
|
||||||
ws.on("message", async (data, isBinary) => {
|
ws.on("message", async (data) => {
|
||||||
if (!ws.isFullyConnected) {
|
if (!ws.isFullyConnected) {
|
||||||
// Queue message for later processing with limits
|
// Queue message for later processing with limits
|
||||||
ws.pendingMessages = ws.pendingMessages || [];
|
ws.pendingMessages = ws.pendingMessages || [];
|
||||||
@@ -828,17 +777,11 @@ const setupConnection = async (
|
|||||||
logger.debug(
|
logger.debug(
|
||||||
`Queueing message from ${clientType.toUpperCase()} ID: ${clientId} (connection not fully established)`
|
`Queueing message from ${clientType.toUpperCase()} ID: ${clientId} (connection not fully established)`
|
||||||
);
|
);
|
||||||
ws.pendingMessages.push({ data: data as Buffer, isBinary });
|
ws.pendingMessages.push(data as Buffer);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await processMessage(
|
await processMessage(ws, data as Buffer, clientId, clientType);
|
||||||
ws,
|
|
||||||
data as Buffer,
|
|
||||||
isBinary,
|
|
||||||
clientId,
|
|
||||||
clientType
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set up other event handlers before async operations
|
// Set up other event handlers before async operations
|
||||||
@@ -853,35 +796,6 @@ const setupConnection = async (
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle WebSocket protocol-level pings from older newt clients that do
|
|
||||||
// not send application-level "newt/ping" messages. Update the site's
|
|
||||||
// online state and lastPing timestamp so the offline checker treats them
|
|
||||||
// the same as modern newt clients.
|
|
||||||
if (clientType === "newt") {
|
|
||||||
const newtClient = client as Newt;
|
|
||||||
ws.on("ping", async () => {
|
|
||||||
if (!newtClient.siteId) return;
|
|
||||||
const now = Math.floor(Date.now() / 1000);
|
|
||||||
const lastWrite = lastPingDbWrite.get(newtClient.siteId) ?? 0;
|
|
||||||
if (now - lastWrite < PING_DB_WRITE_INTERVAL) return;
|
|
||||||
lastPingDbWrite.set(newtClient.siteId, now);
|
|
||||||
try {
|
|
||||||
await db
|
|
||||||
.update(sites)
|
|
||||||
.set({
|
|
||||||
online: true,
|
|
||||||
lastPing: now
|
|
||||||
})
|
|
||||||
.where(eq(sites.siteId, newtClient.siteId));
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(
|
|
||||||
"Error updating newt site online state on WS ping",
|
|
||||||
{ error }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ws.on("error", (error: Error) => {
|
ws.on("error", (error: Error) => {
|
||||||
logger.error(
|
logger.error(
|
||||||
`WebSocket error for ${clientType.toUpperCase()} ID ${clientId}:`,
|
`WebSocket error for ${clientType.toUpperCase()} ID ${clientId}:`,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { NextFunction, Request, Response } from "express";
|
import { NextFunction, Request, Response } from "express";
|
||||||
import { bannedEmails, bannedIps, db, users } from "@server/db";
|
import { db, users } from "@server/db";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import { email, z } from "zod";
|
import { email, z } from "zod";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
@@ -65,30 +65,6 @@ export async function signup(
|
|||||||
skipVerificationEmail
|
skipVerificationEmail
|
||||||
} = parsedBody.data;
|
} = parsedBody.data;
|
||||||
|
|
||||||
const [bannedEmail] = await db
|
|
||||||
.select()
|
|
||||||
.from(bannedEmails)
|
|
||||||
.where(eq(bannedEmails.email, email))
|
|
||||||
.limit(1);
|
|
||||||
if (bannedEmail) {
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.FORBIDDEN, "Signup blocked. Do not attempt to continue to use this service.")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.ip) {
|
|
||||||
const [bannedIp] = await db
|
|
||||||
.select()
|
|
||||||
.from(bannedIps)
|
|
||||||
.where(eq(bannedIps.ip, req.ip))
|
|
||||||
.limit(1);
|
|
||||||
if (bannedIp) {
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.FORBIDDEN, "Signup blocked. Do not attempt to continue to use this service.")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const passwordHash = await hashPassword(password);
|
const passwordHash = await hashPassword(password);
|
||||||
const userId = generateId(15);
|
const userId = generateId(15);
|
||||||
|
|
||||||
|
|||||||
@@ -1,38 +1,51 @@
|
|||||||
import { sendToClient } from "#dynamic/routers/ws";
|
import { sendToClient } from "#dynamic/routers/ws";
|
||||||
import { db, olms, Transaction } from "@server/db";
|
import { db, olms, Transaction } from "@server/db";
|
||||||
import { canCompress } from "@server/lib/clientVersionChecks";
|
|
||||||
import { Alias, SubnetProxyTarget } from "@server/lib/ip";
|
import { Alias, SubnetProxyTarget } from "@server/lib/ip";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
export async function addTargets(
|
const BATCH_SIZE = 50;
|
||||||
newtId: string,
|
const BATCH_DELAY_MS = 50;
|
||||||
targets: SubnetProxyTarget[],
|
|
||||||
version?: string | null
|
function sleep(ms: number): Promise<void> {
|
||||||
) {
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
await sendToClient(
|
}
|
||||||
newtId,
|
|
||||||
{
|
function chunkArray<T>(array: T[], size: number): T[][] {
|
||||||
|
const chunks: T[][] = [];
|
||||||
|
for (let i = 0; i < array.length; i += size) {
|
||||||
|
chunks.push(array.slice(i, i + size));
|
||||||
|
}
|
||||||
|
return chunks;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function addTargets(newtId: string, targets: SubnetProxyTarget[]) {
|
||||||
|
const batches = chunkArray(targets, BATCH_SIZE);
|
||||||
|
for (let i = 0; i < batches.length; i++) {
|
||||||
|
if (i > 0) {
|
||||||
|
await sleep(BATCH_DELAY_MS);
|
||||||
|
}
|
||||||
|
await sendToClient(newtId, {
|
||||||
type: `newt/wg/targets/add`,
|
type: `newt/wg/targets/add`,
|
||||||
data: targets
|
data: batches[i]
|
||||||
},
|
}, { incrementConfigVersion: true });
|
||||||
{ incrementConfigVersion: true, compress: canCompress(version, "newt") }
|
}
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removeTargets(
|
export async function removeTargets(
|
||||||
newtId: string,
|
newtId: string,
|
||||||
targets: SubnetProxyTarget[],
|
targets: SubnetProxyTarget[]
|
||||||
version?: string | null
|
|
||||||
) {
|
) {
|
||||||
await sendToClient(
|
const batches = chunkArray(targets, BATCH_SIZE);
|
||||||
newtId,
|
for (let i = 0; i < batches.length; i++) {
|
||||||
{
|
if (i > 0) {
|
||||||
|
await sleep(BATCH_DELAY_MS);
|
||||||
|
}
|
||||||
|
await sendToClient(newtId, {
|
||||||
type: `newt/wg/targets/remove`,
|
type: `newt/wg/targets/remove`,
|
||||||
data: targets
|
data: batches[i]
|
||||||
},
|
},{ incrementConfigVersion: true });
|
||||||
{ incrementConfigVersion: true, compress: canCompress(version, "newt") }
|
}
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateTargets(
|
export async function updateTargets(
|
||||||
@@ -40,22 +53,26 @@ export async function updateTargets(
|
|||||||
targets: {
|
targets: {
|
||||||
oldTargets: SubnetProxyTarget[];
|
oldTargets: SubnetProxyTarget[];
|
||||||
newTargets: SubnetProxyTarget[];
|
newTargets: SubnetProxyTarget[];
|
||||||
},
|
}
|
||||||
version?: string | null
|
|
||||||
) {
|
) {
|
||||||
await sendToClient(
|
const oldBatches = chunkArray(targets.oldTargets, BATCH_SIZE);
|
||||||
newtId,
|
const newBatches = chunkArray(targets.newTargets, BATCH_SIZE);
|
||||||
{
|
const maxBatches = Math.max(oldBatches.length, newBatches.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < maxBatches; i++) {
|
||||||
|
if (i > 0) {
|
||||||
|
await sleep(BATCH_DELAY_MS);
|
||||||
|
}
|
||||||
|
await sendToClient(newtId, {
|
||||||
type: `newt/wg/targets/update`,
|
type: `newt/wg/targets/update`,
|
||||||
data: {
|
data: {
|
||||||
oldTargets: targets.oldTargets,
|
oldTargets: oldBatches[i] || [],
|
||||||
newTargets: targets.newTargets
|
newTargets: newBatches[i] || []
|
||||||
}
|
}
|
||||||
},
|
}, { incrementConfigVersion: true }).catch((error) => {
|
||||||
{ incrementConfigVersion: true, compress: canCompress(version, "newt") }
|
|
||||||
).catch((error) => {
|
|
||||||
logger.warn(`Error sending message:`, error);
|
logger.warn(`Error sending message:`, error);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addPeerData(
|
export async function addPeerData(
|
||||||
@@ -63,8 +80,7 @@ export async function addPeerData(
|
|||||||
siteId: number,
|
siteId: number,
|
||||||
remoteSubnets: string[],
|
remoteSubnets: string[],
|
||||||
aliases: Alias[],
|
aliases: Alias[],
|
||||||
olmId?: string,
|
olmId?: string
|
||||||
version?: string | null
|
|
||||||
) {
|
) {
|
||||||
if (!olmId) {
|
if (!olmId) {
|
||||||
const [olm] = await db
|
const [olm] = await db
|
||||||
@@ -76,21 +92,16 @@ export async function addPeerData(
|
|||||||
return; // ignore this because an olm might not be associated with the client anymore
|
return; // ignore this because an olm might not be associated with the client anymore
|
||||||
}
|
}
|
||||||
olmId = olm.olmId;
|
olmId = olm.olmId;
|
||||||
version = olm.version;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await sendToClient(
|
await sendToClient(olmId, {
|
||||||
olmId,
|
|
||||||
{
|
|
||||||
type: `olm/wg/peer/data/add`,
|
type: `olm/wg/peer/data/add`,
|
||||||
data: {
|
data: {
|
||||||
siteId: siteId,
|
siteId: siteId,
|
||||||
remoteSubnets: remoteSubnets,
|
remoteSubnets: remoteSubnets,
|
||||||
aliases: aliases
|
aliases: aliases
|
||||||
}
|
}
|
||||||
},
|
}, { incrementConfigVersion: true }).catch((error) => {
|
||||||
{ incrementConfigVersion: true, compress: canCompress(version, "olm") }
|
|
||||||
).catch((error) => {
|
|
||||||
logger.warn(`Error sending message:`, error);
|
logger.warn(`Error sending message:`, error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -100,8 +111,7 @@ export async function removePeerData(
|
|||||||
siteId: number,
|
siteId: number,
|
||||||
remoteSubnets: string[],
|
remoteSubnets: string[],
|
||||||
aliases: Alias[],
|
aliases: Alias[],
|
||||||
olmId?: string,
|
olmId?: string
|
||||||
version?: string | null
|
|
||||||
) {
|
) {
|
||||||
if (!olmId) {
|
if (!olmId) {
|
||||||
const [olm] = await db
|
const [olm] = await db
|
||||||
@@ -113,21 +123,16 @@ export async function removePeerData(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
olmId = olm.olmId;
|
olmId = olm.olmId;
|
||||||
version = olm.version;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await sendToClient(
|
await sendToClient(olmId, {
|
||||||
olmId,
|
|
||||||
{
|
|
||||||
type: `olm/wg/peer/data/remove`,
|
type: `olm/wg/peer/data/remove`,
|
||||||
data: {
|
data: {
|
||||||
siteId: siteId,
|
siteId: siteId,
|
||||||
remoteSubnets: remoteSubnets,
|
remoteSubnets: remoteSubnets,
|
||||||
aliases: aliases
|
aliases: aliases
|
||||||
}
|
}
|
||||||
},
|
}, { incrementConfigVersion: true }).catch((error) => {
|
||||||
{ incrementConfigVersion: true, compress: canCompress(version, "olm") }
|
|
||||||
).catch((error) => {
|
|
||||||
logger.warn(`Error sending message:`, error);
|
logger.warn(`Error sending message:`, error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -147,8 +152,7 @@ export async function updatePeerData(
|
|||||||
newAliases: Alias[];
|
newAliases: Alias[];
|
||||||
}
|
}
|
||||||
| undefined,
|
| undefined,
|
||||||
olmId?: string,
|
olmId?: string
|
||||||
version?: string | null
|
|
||||||
) {
|
) {
|
||||||
if (!olmId) {
|
if (!olmId) {
|
||||||
const [olm] = await db
|
const [olm] = await db
|
||||||
@@ -160,21 +164,16 @@ export async function updatePeerData(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
olmId = olm.olmId;
|
olmId = olm.olmId;
|
||||||
version = olm.version;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await sendToClient(
|
await sendToClient(olmId, {
|
||||||
olmId,
|
|
||||||
{
|
|
||||||
type: `olm/wg/peer/data/update`,
|
type: `olm/wg/peer/data/update`,
|
||||||
data: {
|
data: {
|
||||||
siteId: siteId,
|
siteId: siteId,
|
||||||
...remoteSubnets,
|
...remoteSubnets,
|
||||||
...aliases
|
...aliases
|
||||||
}
|
}
|
||||||
},
|
}, { incrementConfigVersion: true }).catch((error) => {
|
||||||
{ incrementConfigVersion: true, compress: canCompress(version, "olm") }
|
|
||||||
).catch((error) => {
|
|
||||||
logger.warn(`Error sending message:`, error);
|
logger.warn(`Error sending message:`, error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,8 +40,7 @@ async function queryDomains(orgId: string, limit: number, offset: number) {
|
|||||||
tries: domains.tries,
|
tries: domains.tries,
|
||||||
configManaged: domains.configManaged,
|
configManaged: domains.configManaged,
|
||||||
certResolver: domains.certResolver,
|
certResolver: domains.certResolver,
|
||||||
preferWildcardCert: domains.preferWildcardCert,
|
preferWildcardCert: domains.preferWildcardCert
|
||||||
errorMessage: domains.errorMessage
|
|
||||||
})
|
})
|
||||||
.from(orgDomains)
|
.from(orgDomains)
|
||||||
.where(eq(orgDomains.orgId, orgId))
|
.where(eq(orgDomains.orgId, orgId))
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { eq, sql } from "drizzle-orm";
|
import { eq, and, lt, inArray, sql } from "drizzle-orm";
|
||||||
import { sites } from "@server/db";
|
import { sites } from "@server/db";
|
||||||
import { db } from "@server/db";
|
import { db } from "@server/db";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
@@ -11,30 +11,18 @@ import { FeatureId } from "@server/lib/billing/features";
|
|||||||
import { checkExitNodeOrg } from "#dynamic/lib/exitNodes";
|
import { checkExitNodeOrg } from "#dynamic/lib/exitNodes";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
|
|
||||||
interface PeerBandwidth {
|
// Track sites that are already offline to avoid unnecessary queries
|
||||||
publicKey: string;
|
const offlineSites = new Set<string>();
|
||||||
bytesIn: number;
|
|
||||||
bytesOut: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AccumulatorEntry {
|
|
||||||
bytesIn: number;
|
|
||||||
bytesOut: number;
|
|
||||||
/** Present when the update came through a remote exit node. */
|
|
||||||
exitNodeId?: number;
|
|
||||||
/** Whether to record egress usage for billing purposes. */
|
|
||||||
calcUsage: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retry configuration for deadlock handling
|
// Retry configuration for deadlock handling
|
||||||
const MAX_RETRIES = 3;
|
const MAX_RETRIES = 3;
|
||||||
const BASE_DELAY_MS = 50;
|
const BASE_DELAY_MS = 50;
|
||||||
|
|
||||||
// How often to flush accumulated bandwidth data to the database
|
interface PeerBandwidth {
|
||||||
const FLUSH_INTERVAL_MS = 30_000; // 30 seconds
|
publicKey: string;
|
||||||
|
bytesIn: number;
|
||||||
// In-memory accumulator: publicKey -> AccumulatorEntry
|
bytesOut: number;
|
||||||
let accumulator = new Map<string, AccumulatorEntry>();
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if an error is a deadlock error
|
* Check if an error is a deadlock error
|
||||||
@@ -75,220 +63,6 @@ async function withDeadlockRetry<T>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Flush all accumulated site bandwidth data to the database.
|
|
||||||
*
|
|
||||||
* Swaps out the accumulator before writing so that any bandwidth messages
|
|
||||||
* received during the flush are captured in the new accumulator rather than
|
|
||||||
* being lost or causing contention. Entries that fail to write are re-queued
|
|
||||||
* back into the accumulator so they will be retried on the next flush.
|
|
||||||
*
|
|
||||||
* This function is exported so that the application's graceful-shutdown
|
|
||||||
* cleanup handler can call it before the process exits.
|
|
||||||
*/
|
|
||||||
export async function flushSiteBandwidthToDb(): Promise<void> {
|
|
||||||
if (accumulator.size === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Atomically swap out the accumulator so new data keeps flowing in
|
|
||||||
// while we write the snapshot to the database.
|
|
||||||
const snapshot = accumulator;
|
|
||||||
accumulator = new Map<string, AccumulatorEntry>();
|
|
||||||
|
|
||||||
const currentTime = new Date().toISOString();
|
|
||||||
|
|
||||||
// Sort by publicKey for consistent lock ordering across concurrent
|
|
||||||
// writers — deadlock-prevention strategy.
|
|
||||||
const sortedEntries = [...snapshot.entries()].sort(([a], [b]) =>
|
|
||||||
a.localeCompare(b)
|
|
||||||
);
|
|
||||||
|
|
||||||
logger.debug(
|
|
||||||
`Flushing accumulated bandwidth data for ${sortedEntries.length} site(s) to the database`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Aggregate billing usage by org, collected during the DB update loop.
|
|
||||||
const orgUsageMap = new Map<string, number>();
|
|
||||||
|
|
||||||
for (const [publicKey, { bytesIn, bytesOut, exitNodeId, calcUsage }] of sortedEntries) {
|
|
||||||
try {
|
|
||||||
const updatedSite = await withDeadlockRetry(async () => {
|
|
||||||
const [result] = await db
|
|
||||||
.update(sites)
|
|
||||||
.set({
|
|
||||||
megabytesOut: sql`COALESCE(${sites.megabytesOut}, 0) + ${bytesIn}`,
|
|
||||||
megabytesIn: sql`COALESCE(${sites.megabytesIn}, 0) + ${bytesOut}`,
|
|
||||||
lastBandwidthUpdate: currentTime
|
|
||||||
})
|
|
||||||
.where(eq(sites.pubKey, publicKey))
|
|
||||||
.returning({
|
|
||||||
orgId: sites.orgId,
|
|
||||||
siteId: sites.siteId
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}, `flush bandwidth for site ${publicKey}`);
|
|
||||||
|
|
||||||
if (updatedSite) {
|
|
||||||
if (exitNodeId) {
|
|
||||||
const notAllowed = await checkExitNodeOrg(
|
|
||||||
exitNodeId,
|
|
||||||
updatedSite.orgId
|
|
||||||
);
|
|
||||||
if (notAllowed) {
|
|
||||||
logger.warn(
|
|
||||||
`Exit node ${exitNodeId} is not allowed for org ${updatedSite.orgId}`
|
|
||||||
);
|
|
||||||
// Skip usage tracking for this site but continue
|
|
||||||
// processing the rest.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (calcUsage) {
|
|
||||||
const totalBandwidth = bytesIn + bytesOut;
|
|
||||||
const current = orgUsageMap.get(updatedSite.orgId) ?? 0;
|
|
||||||
orgUsageMap.set(updatedSite.orgId, current + totalBandwidth);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(
|
|
||||||
`Failed to flush bandwidth for site ${publicKey}:`,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
|
|
||||||
// Re-queue the failed entry so it is retried on the next flush
|
|
||||||
// rather than silently dropped.
|
|
||||||
const existing = accumulator.get(publicKey);
|
|
||||||
if (existing) {
|
|
||||||
existing.bytesIn += bytesIn;
|
|
||||||
existing.bytesOut += bytesOut;
|
|
||||||
} else {
|
|
||||||
accumulator.set(publicKey, {
|
|
||||||
bytesIn,
|
|
||||||
bytesOut,
|
|
||||||
exitNodeId,
|
|
||||||
calcUsage
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process billing usage updates outside the site-update loop to keep
|
|
||||||
// lock scope small and concerns separated.
|
|
||||||
if (orgUsageMap.size > 0) {
|
|
||||||
// Sort org IDs for consistent lock ordering.
|
|
||||||
const sortedOrgIds = [...orgUsageMap.keys()].sort();
|
|
||||||
|
|
||||||
for (const orgId of sortedOrgIds) {
|
|
||||||
try {
|
|
||||||
const totalBandwidth = orgUsageMap.get(orgId)!;
|
|
||||||
const bandwidthUsage = await usageService.add(
|
|
||||||
orgId,
|
|
||||||
FeatureId.EGRESS_DATA_MB,
|
|
||||||
totalBandwidth
|
|
||||||
);
|
|
||||||
if (bandwidthUsage) {
|
|
||||||
// Fire-and-forget — don't block the flush on limit checking.
|
|
||||||
usageService
|
|
||||||
.checkLimitSet(
|
|
||||||
orgId,
|
|
||||||
FeatureId.EGRESS_DATA_MB,
|
|
||||||
bandwidthUsage
|
|
||||||
)
|
|
||||||
.catch((error: any) => {
|
|
||||||
logger.error(
|
|
||||||
`Error checking bandwidth limits for org ${orgId}:`,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(
|
|
||||||
`Error processing usage for org ${orgId}:`,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
// Continue with other orgs.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Periodic flush timer
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const flushTimer = setInterval(async () => {
|
|
||||||
try {
|
|
||||||
await flushSiteBandwidthToDb();
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(
|
|
||||||
"Unexpected error during periodic site bandwidth flush:",
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, FLUSH_INTERVAL_MS);
|
|
||||||
|
|
||||||
// Allow the process to exit normally even while the timer is pending.
|
|
||||||
// The graceful-shutdown path (see server/cleanup.ts) will call
|
|
||||||
// flushSiteBandwidthToDb() explicitly before process.exit(), so no data
|
|
||||||
// is lost.
|
|
||||||
flushTimer.unref();
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Public API
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Accumulate bandwidth data reported by a gerbil or remote exit node.
|
|
||||||
*
|
|
||||||
* Only peers that actually transferred data (bytesIn > 0) are added to the
|
|
||||||
* accumulator; peers with no activity are silently ignored, which means the
|
|
||||||
* flush will only write rows that have genuinely changed.
|
|
||||||
*
|
|
||||||
* The function is intentionally synchronous in its fast path so that the
|
|
||||||
* HTTP handler can respond immediately without waiting for any I/O.
|
|
||||||
*/
|
|
||||||
export async function updateSiteBandwidth(
|
|
||||||
bandwidthData: PeerBandwidth[],
|
|
||||||
calcUsageAndLimits: boolean,
|
|
||||||
exitNodeId?: number
|
|
||||||
): Promise<void> {
|
|
||||||
for (const { publicKey, bytesIn, bytesOut } of bandwidthData) {
|
|
||||||
// Skip peers that haven't transferred any data — writing zeros to the
|
|
||||||
// database would be a no-op anyway.
|
|
||||||
if (bytesIn <= 0 && bytesOut <= 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const existing = accumulator.get(publicKey);
|
|
||||||
if (existing) {
|
|
||||||
existing.bytesIn += bytesIn;
|
|
||||||
existing.bytesOut += bytesOut;
|
|
||||||
// Retain the most-recent exitNodeId for this peer.
|
|
||||||
if (exitNodeId !== undefined) {
|
|
||||||
existing.exitNodeId = exitNodeId;
|
|
||||||
}
|
|
||||||
// Once calcUsage has been requested for a peer, keep it set for
|
|
||||||
// the lifetime of this flush window.
|
|
||||||
if (calcUsageAndLimits) {
|
|
||||||
existing.calcUsage = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
accumulator.set(publicKey, {
|
|
||||||
bytesIn,
|
|
||||||
bytesOut,
|
|
||||||
exitNodeId,
|
|
||||||
calcUsage: calcUsageAndLimits
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// HTTP handler
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
export const receiveBandwidth = async (
|
export const receiveBandwidth = async (
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
@@ -301,9 +75,7 @@ export const receiveBandwidth = async (
|
|||||||
throw new Error("Invalid bandwidth data");
|
throw new Error("Invalid bandwidth data");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Accumulate in memory; the periodic timer (and the shutdown hook)
|
await updateSiteBandwidth(bandwidthData, build == "saas"); // we are checking the usage on saas only
|
||||||
// will write to the database.
|
|
||||||
await updateSiteBandwidth(bandwidthData, build == "saas");
|
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
data: {},
|
data: {},
|
||||||
@@ -322,3 +94,201 @@ export const receiveBandwidth = async (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export async function updateSiteBandwidth(
|
||||||
|
bandwidthData: PeerBandwidth[],
|
||||||
|
calcUsageAndLimits: boolean,
|
||||||
|
exitNodeId?: number
|
||||||
|
) {
|
||||||
|
const currentTime = new Date();
|
||||||
|
const oneMinuteAgo = new Date(currentTime.getTime() - 60000); // 1 minute ago
|
||||||
|
|
||||||
|
// Sort bandwidth data by publicKey to ensure consistent lock ordering across all instances
|
||||||
|
// This is critical for preventing deadlocks when multiple instances update the same sites
|
||||||
|
const sortedBandwidthData = [...bandwidthData].sort((a, b) =>
|
||||||
|
a.publicKey.localeCompare(b.publicKey)
|
||||||
|
);
|
||||||
|
|
||||||
|
// First, handle sites that are actively reporting bandwidth
|
||||||
|
const activePeers = sortedBandwidthData.filter((peer) => peer.bytesIn > 0);
|
||||||
|
|
||||||
|
// Aggregate usage data by organization (collected outside transaction)
|
||||||
|
const orgUsageMap = new Map<string, number>();
|
||||||
|
|
||||||
|
if (activePeers.length > 0) {
|
||||||
|
// Remove any active peers from offline tracking since they're sending data
|
||||||
|
activePeers.forEach((peer) => offlineSites.delete(peer.publicKey));
|
||||||
|
|
||||||
|
// Update each active site individually with retry logic
|
||||||
|
// This reduces transaction scope and allows retries per-site
|
||||||
|
for (const peer of activePeers) {
|
||||||
|
try {
|
||||||
|
const updatedSite = await withDeadlockRetry(async () => {
|
||||||
|
const [result] = await db
|
||||||
|
.update(sites)
|
||||||
|
.set({
|
||||||
|
megabytesOut: sql`${sites.megabytesOut} + ${peer.bytesIn}`,
|
||||||
|
megabytesIn: sql`${sites.megabytesIn} + ${peer.bytesOut}`,
|
||||||
|
lastBandwidthUpdate: currentTime.toISOString(),
|
||||||
|
online: true
|
||||||
|
})
|
||||||
|
.where(eq(sites.pubKey, peer.publicKey))
|
||||||
|
.returning({
|
||||||
|
online: sites.online,
|
||||||
|
orgId: sites.orgId,
|
||||||
|
siteId: sites.siteId,
|
||||||
|
lastBandwidthUpdate: sites.lastBandwidthUpdate
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}, `update active site ${peer.publicKey}`);
|
||||||
|
|
||||||
|
if (updatedSite) {
|
||||||
|
if (exitNodeId) {
|
||||||
|
const notAllowed = await checkExitNodeOrg(
|
||||||
|
exitNodeId,
|
||||||
|
updatedSite.orgId
|
||||||
|
);
|
||||||
|
if (notAllowed) {
|
||||||
|
logger.warn(
|
||||||
|
`Exit node ${exitNodeId} is not allowed for org ${updatedSite.orgId}`
|
||||||
|
);
|
||||||
|
// Skip this site but continue processing others
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aggregate bandwidth usage for the org
|
||||||
|
const totalBandwidth = peer.bytesIn + peer.bytesOut;
|
||||||
|
const currentOrgUsage =
|
||||||
|
orgUsageMap.get(updatedSite.orgId) || 0;
|
||||||
|
orgUsageMap.set(
|
||||||
|
updatedSite.orgId,
|
||||||
|
currentOrgUsage + totalBandwidth
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`Failed to update bandwidth for site ${peer.publicKey}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
// Continue with other sites
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process usage updates outside of site update transactions
|
||||||
|
// This separates the concerns and reduces lock contention
|
||||||
|
if (calcUsageAndLimits && orgUsageMap.size > 0) {
|
||||||
|
// Sort org IDs to ensure consistent lock ordering
|
||||||
|
const allOrgIds = [...new Set([...orgUsageMap.keys()])].sort();
|
||||||
|
|
||||||
|
for (const orgId of allOrgIds) {
|
||||||
|
try {
|
||||||
|
// Process bandwidth usage for this org
|
||||||
|
const totalBandwidth = orgUsageMap.get(orgId);
|
||||||
|
if (totalBandwidth) {
|
||||||
|
const bandwidthUsage = await usageService.add(
|
||||||
|
orgId,
|
||||||
|
FeatureId.EGRESS_DATA_MB,
|
||||||
|
totalBandwidth
|
||||||
|
);
|
||||||
|
if (bandwidthUsage) {
|
||||||
|
// Fire and forget - don't block on limit checking
|
||||||
|
usageService
|
||||||
|
.checkLimitSet(
|
||||||
|
orgId,
|
||||||
|
FeatureId.EGRESS_DATA_MB,
|
||||||
|
bandwidthUsage
|
||||||
|
)
|
||||||
|
.catch((error: any) => {
|
||||||
|
logger.error(
|
||||||
|
`Error checking bandwidth limits for org ${orgId}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error processing usage for org ${orgId}:`, error);
|
||||||
|
// Continue with other orgs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle sites that reported zero bandwidth but need online status updated
|
||||||
|
const zeroBandwidthPeers = sortedBandwidthData.filter(
|
||||||
|
(peer) => peer.bytesIn === 0 && !offlineSites.has(peer.publicKey)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (zeroBandwidthPeers.length > 0) {
|
||||||
|
// Fetch all zero bandwidth sites in one query
|
||||||
|
const zeroBandwidthSites = await db
|
||||||
|
.select()
|
||||||
|
.from(sites)
|
||||||
|
.where(
|
||||||
|
inArray(
|
||||||
|
sites.pubKey,
|
||||||
|
zeroBandwidthPeers.map((p) => p.publicKey)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Sort by siteId to ensure consistent lock ordering
|
||||||
|
const sortedZeroBandwidthSites = zeroBandwidthSites.sort(
|
||||||
|
(a, b) => a.siteId - b.siteId
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const site of sortedZeroBandwidthSites) {
|
||||||
|
let newOnlineStatus = site.online;
|
||||||
|
|
||||||
|
// Check if site should go offline based on last bandwidth update WITH DATA
|
||||||
|
if (site.lastBandwidthUpdate) {
|
||||||
|
const lastUpdateWithData = new Date(site.lastBandwidthUpdate);
|
||||||
|
if (lastUpdateWithData < oneMinuteAgo) {
|
||||||
|
newOnlineStatus = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No previous data update recorded, set to offline
|
||||||
|
newOnlineStatus = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only update online status if it changed
|
||||||
|
if (site.online !== newOnlineStatus) {
|
||||||
|
try {
|
||||||
|
const updatedSite = await withDeadlockRetry(async () => {
|
||||||
|
const [result] = await db
|
||||||
|
.update(sites)
|
||||||
|
.set({
|
||||||
|
online: newOnlineStatus
|
||||||
|
})
|
||||||
|
.where(eq(sites.siteId, site.siteId))
|
||||||
|
.returning();
|
||||||
|
return result;
|
||||||
|
}, `update offline status for site ${site.siteId}`);
|
||||||
|
|
||||||
|
if (updatedSite && exitNodeId) {
|
||||||
|
const notAllowed = await checkExitNodeOrg(
|
||||||
|
exitNodeId,
|
||||||
|
updatedSite.orgId
|
||||||
|
);
|
||||||
|
if (notAllowed) {
|
||||||
|
logger.warn(
|
||||||
|
`Exit node ${exitNodeId} is not allowed for org ${updatedSite.orgId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If site went offline, add it to our tracking set
|
||||||
|
if (!newOnlineStatus && site.pubKey) {
|
||||||
|
offlineSites.add(site.pubKey);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`Failed to update offline status for site ${site.siteId}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
// Continue with other sites
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ export async function updateHolePunch(
|
|||||||
destinations: destinations
|
destinations: destinations
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error);
|
// logger.error(error); // FIX THIS
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.INTERNAL_SERVER_ERROR,
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
@@ -339,10 +339,10 @@ export async function updateAndGenerateEndpointDestinations(
|
|||||||
handleSiteEndpointChange(newt.siteId, updatedSite.endpoint!);
|
handleSiteEndpointChange(newt.siteId, updatedSite.endpoint!);
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (!updatedSite || !updatedSite.subnet) {
|
if (!updatedSite || !updatedSite.subnet) {
|
||||||
// logger.warn(`Site not found: ${newt.siteId}`);
|
logger.warn(`Site not found: ${newt.siteId}`);
|
||||||
// throw new Error("Site not found");
|
throw new Error("Site not found");
|
||||||
// }
|
}
|
||||||
|
|
||||||
// Find all clients that connect to this site
|
// Find all clients that connect to this site
|
||||||
// const sitesClientPairs = await db
|
// const sitesClientPairs = await db
|
||||||
|
|||||||
@@ -1,15 +1,4 @@
|
|||||||
import {
|
import { clients, clientSiteResourcesAssociationsCache, clientSitesAssociationsCache, db, ExitNode, resources, Site, siteResources, targetHealthCheck, targets } from "@server/db";
|
||||||
clients,
|
|
||||||
clientSiteResourcesAssociationsCache,
|
|
||||||
clientSitesAssociationsCache,
|
|
||||||
db,
|
|
||||||
ExitNode,
|
|
||||||
resources,
|
|
||||||
Site,
|
|
||||||
siteResources,
|
|
||||||
targetHealthCheck,
|
|
||||||
targets
|
|
||||||
} from "@server/db";
|
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { initPeerAddHandshake, updatePeer } from "../olm/peers";
|
import { initPeerAddHandshake, updatePeer } from "../olm/peers";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
@@ -80,7 +69,6 @@ export async function buildClientConfigurationForNewtClient(
|
|||||||
// )
|
// )
|
||||||
// );
|
// );
|
||||||
|
|
||||||
if (!client.clientSitesAssociationsCache.isJitMode) { // if we are adding sites through jit then dont add the site to the olm
|
|
||||||
// update the peer info on the olm
|
// update the peer info on the olm
|
||||||
// if the peer has not been added yet this will be a no-op
|
// if the peer has not been added yet this will be a no-op
|
||||||
await updatePeer(client.clients.clientId, {
|
await updatePeer(client.clients.clientId, {
|
||||||
@@ -115,7 +103,6 @@ export async function buildClientConfigurationForNewtClient(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
publicKey: client.clients.pubKey!,
|
publicKey: client.clients.pubKey!,
|
||||||
@@ -243,9 +230,9 @@ export async function buildTargetConfigurationForNewtClient(siteId: number) {
|
|||||||
!target.hcInterval ||
|
!target.hcInterval ||
|
||||||
!target.hcMethod
|
!target.hcMethod
|
||||||
) {
|
) {
|
||||||
// logger.debug(
|
logger.debug(
|
||||||
// `Skipping adding target health check ${target.targetId} due to missing health check fields`
|
`Skipping adding target health check ${target.targetId} due to missing health check fields`
|
||||||
// );
|
);
|
||||||
return null; // Skip targets with missing health check fields
|
return null; // Skip targets with missing health check fields
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { db, ExitNode, exitNodes, Newt, sites } from "@server/db";
|
|||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { sendToExitNode } from "#dynamic/lib/exitNodes";
|
import { sendToExitNode } from "#dynamic/lib/exitNodes";
|
||||||
import { buildClientConfigurationForNewtClient } from "./buildConfiguration";
|
import { buildClientConfigurationForNewtClient } from "./buildConfiguration";
|
||||||
import { canCompress } from "@server/lib/clientVersionChecks";
|
|
||||||
|
|
||||||
const inputSchema = z.object({
|
const inputSchema = z.object({
|
||||||
publicKey: z.string(),
|
publicKey: z.string(),
|
||||||
@@ -136,9 +135,6 @@ export const handleGetConfigMessage: MessageHandler = async (context) => {
|
|||||||
targets
|
targets
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
options: {
|
|
||||||
compress: canCompress(newt.version, "newt")
|
|
||||||
},
|
|
||||||
broadcast: false,
|
broadcast: false,
|
||||||
excludeSender: false
|
excludeSender: false
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
import { MessageHandler } from "@server/routers/ws";
|
|
||||||
import { db, Newt, sites } from "@server/db";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import logger from "@server/logger";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles disconnecting messages from sites to show disconnected in the ui
|
|
||||||
*/
|
|
||||||
export const handleNewtDisconnectingMessage: MessageHandler = async (context) => {
|
|
||||||
const { message, client: c, sendToClient } = context;
|
|
||||||
const newt = c as Newt;
|
|
||||||
|
|
||||||
if (!newt) {
|
|
||||||
logger.warn("Newt not found");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!newt.siteId) {
|
|
||||||
logger.warn("Newt has no client ID!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Update the client's last ping timestamp
|
|
||||||
await db
|
|
||||||
.update(sites)
|
|
||||||
.set({
|
|
||||||
online: false
|
|
||||||
})
|
|
||||||
.where(eq(sites.siteId, sites.siteId));
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Error handling disconnecting message", { error });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,107 +1,105 @@
|
|||||||
import { db, newts, sites } from "@server/db";
|
import { db, sites } from "@server/db";
|
||||||
import { hasActiveConnections, getClientConfigVersion } from "#dynamic/routers/ws";
|
import { disconnectClient, getClientConfigVersion } from "#dynamic/routers/ws";
|
||||||
import { MessageHandler } from "@server/routers/ws";
|
import { MessageHandler } from "@server/routers/ws";
|
||||||
import { Newt } from "@server/db";
|
import { clients, Newt } from "@server/db";
|
||||||
import { eq, lt, isNull, and, or } from "drizzle-orm";
|
import { eq, lt, isNull, and, or } from "drizzle-orm";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
|
import { validateSessionToken } from "@server/auth/sessions/app";
|
||||||
|
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
|
||||||
|
import { sendTerminateClient } from "../client/terminate";
|
||||||
|
import { encodeHexLowerCase } from "@oslojs/encoding";
|
||||||
|
import { sha256 } from "@oslojs/crypto/sha2";
|
||||||
import { sendNewtSyncMessage } from "./sync";
|
import { sendNewtSyncMessage } from "./sync";
|
||||||
|
|
||||||
// Track if the offline checker interval is running
|
// Track if the offline checker interval is running
|
||||||
let offlineCheckerInterval: NodeJS.Timeout | null = null;
|
// let offlineCheckerInterval: NodeJS.Timeout | null = null;
|
||||||
const OFFLINE_CHECK_INTERVAL = 30 * 1000; // Check every 30 seconds
|
// const OFFLINE_CHECK_INTERVAL = 30 * 1000; // Check every 30 seconds
|
||||||
const OFFLINE_THRESHOLD_MS = 2 * 60 * 1000; // 2 minutes
|
// const OFFLINE_THRESHOLD_MS = 2 * 60 * 1000; // 2 minutes
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts the background interval that checks for newt sites that haven't
|
* Starts the background interval that checks for clients that haven't pinged recently
|
||||||
* pinged recently and marks them as offline. For backward compatibility,
|
* and marks them as offline
|
||||||
* a site is only marked offline when there is no active WebSocket connection
|
|
||||||
* either — so older newt versions that don't send pings but remain connected
|
|
||||||
* continue to be treated as online.
|
|
||||||
*/
|
*/
|
||||||
export const startNewtOfflineChecker = (): void => {
|
// export const startNewtOfflineChecker = (): void => {
|
||||||
if (offlineCheckerInterval) {
|
// if (offlineCheckerInterval) {
|
||||||
return; // Already running
|
// return; // Already running
|
||||||
}
|
// }
|
||||||
|
|
||||||
offlineCheckerInterval = setInterval(async () => {
|
// offlineCheckerInterval = setInterval(async () => {
|
||||||
try {
|
// try {
|
||||||
const twoMinutesAgo = Math.floor(
|
// const twoMinutesAgo = Math.floor(
|
||||||
(Date.now() - OFFLINE_THRESHOLD_MS) / 1000
|
// (Date.now() - OFFLINE_THRESHOLD_MS) / 1000
|
||||||
);
|
// );
|
||||||
|
|
||||||
// Find all online newt-type sites that haven't pinged recently
|
// // TODO: WE NEED TO MAKE SURE THIS WORKS WITH DISTRIBUTED NODES ALL DOING THE SAME THING
|
||||||
// (or have never pinged at all). Join newts to obtain the newtId
|
|
||||||
// needed for the WebSocket connection check.
|
|
||||||
const staleSites = await db
|
|
||||||
.select({
|
|
||||||
siteId: sites.siteId,
|
|
||||||
newtId: newts.newtId,
|
|
||||||
lastPing: sites.lastPing
|
|
||||||
})
|
|
||||||
.from(sites)
|
|
||||||
.innerJoin(newts, eq(newts.siteId, sites.siteId))
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(sites.online, true),
|
|
||||||
eq(sites.type, "newt"),
|
|
||||||
or(
|
|
||||||
lt(sites.lastPing, twoMinutesAgo),
|
|
||||||
isNull(sites.lastPing)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const staleSite of staleSites) {
|
// // Find clients that haven't pinged in the last 2 minutes and mark them as offline
|
||||||
// Backward-compatibility check: if the newt still has an
|
// const offlineClients = await db
|
||||||
// active WebSocket connection (older clients that don't send
|
// .update(clients)
|
||||||
// pings), keep the site online.
|
// .set({ online: false })
|
||||||
const isConnected = await hasActiveConnections(staleSite.newtId);
|
// .where(
|
||||||
if (isConnected) {
|
// and(
|
||||||
logger.debug(
|
// eq(clients.online, true),
|
||||||
`Newt ${staleSite.newtId} has not pinged recently but is still connected via WebSocket — keeping site ${staleSite.siteId} online`
|
// or(
|
||||||
);
|
// lt(clients.lastPing, twoMinutesAgo),
|
||||||
continue;
|
// isNull(clients.lastPing)
|
||||||
}
|
// )
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// .returning();
|
||||||
|
|
||||||
logger.info(
|
// for (const offlineClient of offlineClients) {
|
||||||
`Marking site ${staleSite.siteId} offline: newt ${staleSite.newtId} has no recent ping and no active WebSocket connection`
|
// logger.info(
|
||||||
);
|
// `Kicking offline newt client ${offlineClient.clientId} due to inactivity`
|
||||||
|
// );
|
||||||
|
|
||||||
await db
|
// if (!offlineClient.newtId) {
|
||||||
.update(sites)
|
// logger.warn(
|
||||||
.set({ online: false })
|
// `Offline client ${offlineClient.clientId} has no newtId, cannot disconnect`
|
||||||
.where(eq(sites.siteId, staleSite.siteId));
|
// );
|
||||||
}
|
// continue;
|
||||||
} catch (error) {
|
// }
|
||||||
logger.error("Error in newt offline checker interval", { error });
|
|
||||||
}
|
|
||||||
}, OFFLINE_CHECK_INTERVAL);
|
|
||||||
|
|
||||||
logger.debug("Started newt offline checker interval");
|
// // Send a disconnect message to the client if connected
|
||||||
};
|
// try {
|
||||||
|
// await sendTerminateClient(
|
||||||
|
// offlineClient.clientId,
|
||||||
|
// offlineClient.newtId
|
||||||
|
// ); // terminate first
|
||||||
|
// // wait a moment to ensure the message is sent
|
||||||
|
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
// await disconnectClient(offlineClient.newtId);
|
||||||
|
// } catch (error) {
|
||||||
|
// logger.error(
|
||||||
|
// `Error sending disconnect to offline newt ${offlineClient.clientId}`,
|
||||||
|
// { error }
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// } catch (error) {
|
||||||
|
// logger.error("Error in offline checker interval", { error });
|
||||||
|
// }
|
||||||
|
// }, OFFLINE_CHECK_INTERVAL);
|
||||||
|
|
||||||
|
// logger.debug("Started offline checker interval");
|
||||||
|
// };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stops the background interval that checks for offline newt sites.
|
* Stops the background interval that checks for offline clients
|
||||||
*/
|
*/
|
||||||
export const stopNewtOfflineChecker = (): void => {
|
// export const stopNewtOfflineChecker = (): void => {
|
||||||
if (offlineCheckerInterval) {
|
// if (offlineCheckerInterval) {
|
||||||
clearInterval(offlineCheckerInterval);
|
// clearInterval(offlineCheckerInterval);
|
||||||
offlineCheckerInterval = null;
|
// offlineCheckerInterval = null;
|
||||||
logger.info("Stopped newt offline checker interval");
|
// logger.info("Stopped offline checker interval");
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles ping messages from newt clients.
|
* Handles ping messages from clients and responds with pong
|
||||||
*
|
|
||||||
* On each ping:
|
|
||||||
* - Marks the associated site as online.
|
|
||||||
* - Records the current timestamp as the newt's last-ping time.
|
|
||||||
* - Triggers a config sync if the newt is running an outdated config version.
|
|
||||||
* - Responds with a pong message.
|
|
||||||
*/
|
*/
|
||||||
export const handleNewtPingMessage: MessageHandler = async (context) => {
|
export const handleNewtPingMessage: MessageHandler = async (context) => {
|
||||||
const { message, client: c } = context;
|
const { message, client: c, sendToClient } = context;
|
||||||
const newt = c as Newt;
|
const newt = c as Newt;
|
||||||
|
|
||||||
if (!newt) {
|
if (!newt) {
|
||||||
@@ -114,31 +112,15 @@ export const handleNewtPingMessage: MessageHandler = async (context) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// get the version
|
||||||
// Mark the site as online and record the ping timestamp.
|
|
||||||
await db
|
|
||||||
.update(sites)
|
|
||||||
.set({
|
|
||||||
online: true,
|
|
||||||
lastPing: Math.floor(Date.now() / 1000)
|
|
||||||
})
|
|
||||||
.where(eq(sites.siteId, newt.siteId));
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Error updating online state on newt ping", { error });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check config version and sync if stale.
|
|
||||||
const configVersion = await getClientConfigVersion(newt.newtId);
|
const configVersion = await getClientConfigVersion(newt.newtId);
|
||||||
|
|
||||||
if (
|
if (message.configVersion && configVersion != null && configVersion != message.configVersion) {
|
||||||
message.configVersion != null &&
|
|
||||||
configVersion != null &&
|
|
||||||
configVersion !== message.configVersion
|
|
||||||
) {
|
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Newt ping with outdated config version: ${message.configVersion} (current: ${configVersion})`
|
`Newt ping with outdated config version: ${message.configVersion} (current: ${configVersion})`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// get the site
|
||||||
const [site] = await db
|
const [site] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(sites)
|
.from(sites)
|
||||||
@@ -155,6 +137,19 @@ export const handleNewtPingMessage: MessageHandler = async (context) => {
|
|||||||
await sendNewtSyncMessage(newt, site);
|
await sendNewtSyncMessage(newt, site);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// // Update the client's last ping timestamp
|
||||||
|
// await db
|
||||||
|
// .update(clients)
|
||||||
|
// .set({
|
||||||
|
// lastPing: Math.floor(Date.now() / 1000),
|
||||||
|
// online: true
|
||||||
|
// })
|
||||||
|
// .where(eq(clients.clientId, newt.clientId));
|
||||||
|
// } catch (error) {
|
||||||
|
// logger.error("Error handling ping message", { error });
|
||||||
|
// }
|
||||||
|
|
||||||
return {
|
return {
|
||||||
message: {
|
message: {
|
||||||
type: "pong",
|
type: "pong",
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import { eq } from "drizzle-orm";
|
|||||||
import { addPeer, deletePeer } from "../gerbil/peers";
|
import { addPeer, deletePeer } from "../gerbil/peers";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import { findNextAvailableCidr } from "@server/lib/ip";
|
import {
|
||||||
|
findNextAvailableCidr,
|
||||||
|
} from "@server/lib/ip";
|
||||||
import {
|
import {
|
||||||
selectBestExitNode,
|
selectBestExitNode,
|
||||||
verifyExitNodeOrgAccess
|
verifyExitNodeOrgAccess
|
||||||
@@ -13,7 +15,6 @@ import {
|
|||||||
import { fetchContainers } from "./dockerSocket";
|
import { fetchContainers } from "./dockerSocket";
|
||||||
import { lockManager } from "#dynamic/lib/lock";
|
import { lockManager } from "#dynamic/lib/lock";
|
||||||
import { buildTargetConfigurationForNewtClient } from "./buildConfiguration";
|
import { buildTargetConfigurationForNewtClient } from "./buildConfiguration";
|
||||||
import { canCompress } from "@server/lib/clientVersionChecks";
|
|
||||||
|
|
||||||
export type ExitNodePingResult = {
|
export type ExitNodePingResult = {
|
||||||
exitNodeId: number;
|
exitNodeId: number;
|
||||||
@@ -214,9 +215,6 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
|
|||||||
healthCheckTargets: validHealthCheckTargets
|
healthCheckTargets: validHealthCheckTargets
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
options: {
|
|
||||||
compress: canCompress(newt.version, "newt")
|
|
||||||
},
|
|
||||||
broadcast: false, // Send to all clients
|
broadcast: false, // Send to all clients
|
||||||
excludeSender: false // Include sender in broadcast
|
excludeSender: false // Include sender in broadcast
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,21 +10,10 @@ interface PeerBandwidth {
|
|||||||
bytesOut: number;
|
bytesOut: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BandwidthAccumulator {
|
|
||||||
bytesIn: number;
|
|
||||||
bytesOut: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retry configuration for deadlock handling
|
// Retry configuration for deadlock handling
|
||||||
const MAX_RETRIES = 3;
|
const MAX_RETRIES = 3;
|
||||||
const BASE_DELAY_MS = 50;
|
const BASE_DELAY_MS = 50;
|
||||||
|
|
||||||
// How often to flush accumulated bandwidth data to the database
|
|
||||||
const FLUSH_INTERVAL_MS = 120_000; // 120 seconds
|
|
||||||
|
|
||||||
// In-memory accumulator: publicKey -> { bytesIn, bytesOut }
|
|
||||||
let accumulator = new Map<string, BandwidthAccumulator>();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if an error is a deadlock error
|
* Check if an error is a deadlock error
|
||||||
*/
|
*/
|
||||||
@@ -64,90 +53,6 @@ async function withDeadlockRetry<T>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Flush all accumulated bandwidth data to the database.
|
|
||||||
*
|
|
||||||
* Swaps out the accumulator before writing so that any bandwidth messages
|
|
||||||
* received during the flush are captured in the new accumulator rather than
|
|
||||||
* being lost or causing contention. Entries that fail to write are re-queued
|
|
||||||
* back into the accumulator so they will be retried on the next flush.
|
|
||||||
*
|
|
||||||
* This function is exported so that the application's graceful-shutdown
|
|
||||||
* cleanup handler can call it before the process exits.
|
|
||||||
*/
|
|
||||||
export async function flushBandwidthToDb(): Promise<void> {
|
|
||||||
if (accumulator.size === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Atomically swap out the accumulator so new data keeps flowing in
|
|
||||||
// while we write the snapshot to the database.
|
|
||||||
const snapshot = accumulator;
|
|
||||||
accumulator = new Map<string, BandwidthAccumulator>();
|
|
||||||
|
|
||||||
const currentTime = new Date().toISOString();
|
|
||||||
|
|
||||||
// Sort by publicKey for consistent lock ordering across concurrent
|
|
||||||
// writers — this is the same deadlock-prevention strategy used in the
|
|
||||||
// original per-message implementation.
|
|
||||||
const sortedEntries = [...snapshot.entries()].sort(([a], [b]) =>
|
|
||||||
a.localeCompare(b)
|
|
||||||
);
|
|
||||||
|
|
||||||
logger.debug(
|
|
||||||
`Flushing accumulated bandwidth data for ${sortedEntries.length} client(s) to the database`
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const [publicKey, { bytesIn, bytesOut }] of sortedEntries) {
|
|
||||||
try {
|
|
||||||
await withDeadlockRetry(async () => {
|
|
||||||
// Use atomic SQL increment to avoid the SELECT-then-UPDATE
|
|
||||||
// anti-pattern and the races it would introduce.
|
|
||||||
await db
|
|
||||||
.update(clients)
|
|
||||||
.set({
|
|
||||||
// Note: bytesIn from peer goes to megabytesOut (data
|
|
||||||
// sent to client) and bytesOut from peer goes to
|
|
||||||
// megabytesIn (data received from client).
|
|
||||||
megabytesOut: sql`COALESCE(${clients.megabytesOut}, 0) + ${bytesIn}`,
|
|
||||||
megabytesIn: sql`COALESCE(${clients.megabytesIn}, 0) + ${bytesOut}`,
|
|
||||||
lastBandwidthUpdate: currentTime
|
|
||||||
})
|
|
||||||
.where(eq(clients.pubKey, publicKey));
|
|
||||||
}, `flush bandwidth for client ${publicKey}`);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(
|
|
||||||
`Failed to flush bandwidth for client ${publicKey}:`,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
|
|
||||||
// Re-queue the failed entry so it is retried on the next flush
|
|
||||||
// rather than silently dropped.
|
|
||||||
const existing = accumulator.get(publicKey);
|
|
||||||
if (existing) {
|
|
||||||
existing.bytesIn += bytesIn;
|
|
||||||
existing.bytesOut += bytesOut;
|
|
||||||
} else {
|
|
||||||
accumulator.set(publicKey, { bytesIn, bytesOut });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const flushTimer = setInterval(async () => {
|
|
||||||
try {
|
|
||||||
await flushBandwidthToDb();
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Unexpected error during periodic bandwidth flush:", error);
|
|
||||||
}
|
|
||||||
}, FLUSH_INTERVAL_MS);
|
|
||||||
|
|
||||||
// Calling unref() means this timer will not keep the Node.js event loop alive
|
|
||||||
// on its own — the process can still exit normally when there is no other work
|
|
||||||
// left. The graceful-shutdown path (see server/cleanup.ts) will call
|
|
||||||
// flushBandwidthToDb() explicitly before process.exit(), so no data is lost.
|
|
||||||
flushTimer.unref();
|
|
||||||
|
|
||||||
export const handleReceiveBandwidthMessage: MessageHandler = async (
|
export const handleReceiveBandwidthMessage: MessageHandler = async (
|
||||||
context
|
context
|
||||||
) => {
|
) => {
|
||||||
@@ -164,21 +69,40 @@ export const handleReceiveBandwidthMessage: MessageHandler = async (
|
|||||||
throw new Error("Invalid bandwidth data");
|
throw new Error("Invalid bandwidth data");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Accumulate the incoming data in memory; the periodic timer (and the
|
// Sort bandwidth data by publicKey to ensure consistent lock ordering across all instances
|
||||||
// shutdown hook) will take care of writing it to the database.
|
// This is critical for preventing deadlocks when multiple instances update the same clients
|
||||||
for (const { publicKey, bytesIn, bytesOut } of bandwidthData) {
|
const sortedBandwidthData = [...bandwidthData].sort((a, b) =>
|
||||||
// Skip peers that haven't transferred any data — writing zeros to the
|
a.publicKey.localeCompare(b.publicKey)
|
||||||
// database would be a no-op anyway.
|
);
|
||||||
if (bytesIn <= 0 && bytesOut <= 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const existing = accumulator.get(publicKey);
|
const currentTime = new Date().toISOString();
|
||||||
if (existing) {
|
|
||||||
existing.bytesIn += bytesIn;
|
// Update each client individually with retry logic
|
||||||
existing.bytesOut += bytesOut;
|
// This reduces transaction scope and allows retries per-client
|
||||||
} else {
|
for (const peer of sortedBandwidthData) {
|
||||||
accumulator.set(publicKey, { bytesIn, bytesOut });
|
const { publicKey, bytesIn, bytesOut } = peer;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await withDeadlockRetry(async () => {
|
||||||
|
// Use atomic SQL increment to avoid SELECT then UPDATE pattern
|
||||||
|
// This eliminates the need to read the current value first
|
||||||
|
await db
|
||||||
|
.update(clients)
|
||||||
|
.set({
|
||||||
|
// Note: bytesIn from peer goes to megabytesOut (data sent to client)
|
||||||
|
// and bytesOut from peer goes to megabytesIn (data received from client)
|
||||||
|
megabytesOut: sql`COALESCE(${clients.megabytesOut}, 0) + ${bytesIn}`,
|
||||||
|
megabytesIn: sql`COALESCE(${clients.megabytesIn}, 0) + ${bytesOut}`,
|
||||||
|
lastBandwidthUpdate: currentTime
|
||||||
|
})
|
||||||
|
.where(eq(clients.pubKey, publicKey));
|
||||||
|
}, `update client bandwidth ${publicKey}`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`Failed to update bandwidth for client ${publicKey}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
// Continue with other clients even if one fails
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,4 +7,3 @@ export * from "./handleSocketMessages";
|
|||||||
export * from "./handleNewtPingRequestMessage";
|
export * from "./handleNewtPingRequestMessage";
|
||||||
export * from "./handleApplyBlueprintMessage";
|
export * from "./handleApplyBlueprintMessage";
|
||||||
export * from "./handleNewtPingMessage";
|
export * from "./handleNewtPingMessage";
|
||||||
export * from "./handleNewtDisconnectingMessage";
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
buildClientConfigurationForNewtClient,
|
buildClientConfigurationForNewtClient,
|
||||||
buildTargetConfigurationForNewtClient
|
buildTargetConfigurationForNewtClient
|
||||||
} from "./buildConfiguration";
|
} from "./buildConfiguration";
|
||||||
import { canCompress } from "@server/lib/clientVersionChecks";
|
|
||||||
|
|
||||||
export async function sendNewtSyncMessage(newt: Newt, site: Site) {
|
export async function sendNewtSyncMessage(newt: Newt, site: Site) {
|
||||||
const { tcpTargets, udpTargets, validHealthCheckTargets } =
|
const { tcpTargets, udpTargets, validHealthCheckTargets } =
|
||||||
@@ -25,9 +24,7 @@ export async function sendNewtSyncMessage(newt: Newt, site: Site) {
|
|||||||
exitNode
|
exitNode
|
||||||
);
|
);
|
||||||
|
|
||||||
await sendToClient(
|
await sendToClient(newt.newtId, {
|
||||||
newt.newtId,
|
|
||||||
{
|
|
||||||
type: "newt/sync",
|
type: "newt/sync",
|
||||||
data: {
|
data: {
|
||||||
proxyTargets: {
|
proxyTargets: {
|
||||||
@@ -38,11 +35,7 @@ export async function sendNewtSyncMessage(newt: Newt, site: Site) {
|
|||||||
peers: peers,
|
peers: peers,
|
||||||
clientTargets: targets
|
clientTargets: targets
|
||||||
}
|
}
|
||||||
},
|
}).catch((error) => {
|
||||||
{
|
|
||||||
compress: canCompress(newt.version, "newt")
|
|
||||||
}
|
|
||||||
).catch((error) => {
|
|
||||||
logger.warn(`Error sending newt sync message:`, error);
|
logger.warn(`Error sending newt sync message:`, error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,13 @@ import { Target, TargetHealthCheck, db, targetHealthCheck } from "@server/db";
|
|||||||
import { sendToClient } from "#dynamic/routers/ws";
|
import { sendToClient } from "#dynamic/routers/ws";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { eq, inArray } from "drizzle-orm";
|
import { eq, inArray } from "drizzle-orm";
|
||||||
import { canCompress } from "@server/lib/clientVersionChecks";
|
|
||||||
|
|
||||||
export async function addTargets(
|
export async function addTargets(
|
||||||
newtId: string,
|
newtId: string,
|
||||||
targets: Target[],
|
targets: Target[],
|
||||||
healthCheckData: TargetHealthCheck[],
|
healthCheckData: TargetHealthCheck[],
|
||||||
protocol: string,
|
protocol: string,
|
||||||
version?: string | null
|
port: number | null = null
|
||||||
) {
|
) {
|
||||||
//create a list of udp and tcp targets
|
//create a list of udp and tcp targets
|
||||||
const payloadTargets = targets.map((target) => {
|
const payloadTargets = targets.map((target) => {
|
||||||
@@ -23,7 +22,7 @@ export async function addTargets(
|
|||||||
data: {
|
data: {
|
||||||
targets: payloadTargets
|
targets: payloadTargets
|
||||||
}
|
}
|
||||||
}, { incrementConfigVersion: true, compress: canCompress(version, "newt") });
|
}, { incrementConfigVersion: true });
|
||||||
|
|
||||||
// Create a map for quick lookup
|
// Create a map for quick lookup
|
||||||
const healthCheckMap = new Map<number, TargetHealthCheck>();
|
const healthCheckMap = new Map<number, TargetHealthCheck>();
|
||||||
@@ -104,14 +103,14 @@ export async function addTargets(
|
|||||||
data: {
|
data: {
|
||||||
targets: validHealthCheckTargets
|
targets: validHealthCheckTargets
|
||||||
}
|
}
|
||||||
}, { incrementConfigVersion: true, compress: canCompress(version, "newt") });
|
}, { incrementConfigVersion: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removeTargets(
|
export async function removeTargets(
|
||||||
newtId: string,
|
newtId: string,
|
||||||
targets: Target[],
|
targets: Target[],
|
||||||
protocol: string,
|
protocol: string,
|
||||||
version?: string | null
|
port: number | null = null
|
||||||
) {
|
) {
|
||||||
//create a list of udp and tcp targets
|
//create a list of udp and tcp targets
|
||||||
const payloadTargets = targets.map((target) => {
|
const payloadTargets = targets.map((target) => {
|
||||||
@@ -136,5 +135,5 @@ export async function removeTargets(
|
|||||||
data: {
|
data: {
|
||||||
ids: healthCheckTargets
|
ids: healthCheckTargets
|
||||||
}
|
}
|
||||||
}, { incrementConfigVersion: true, compress: canCompress(version, "newt") });
|
}, { incrementConfigVersion: true });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,5 @@
|
|||||||
import {
|
import { Client, clientSiteResourcesAssociationsCache, clientSitesAssociationsCache, db, exitNodes, siteResources, sites } from "@server/db";
|
||||||
Client,
|
import { generateAliasConfig, generateRemoteSubnets } from "@server/lib/ip";
|
||||||
clientSiteResourcesAssociationsCache,
|
|
||||||
clientSitesAssociationsCache,
|
|
||||||
db,
|
|
||||||
exitNodes,
|
|
||||||
siteResources,
|
|
||||||
sites
|
|
||||||
} from "@server/db";
|
|
||||||
import {
|
|
||||||
Alias,
|
|
||||||
generateAliasConfig,
|
|
||||||
generateRemoteSubnets
|
|
||||||
} from "@server/lib/ip";
|
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import { addPeer, deletePeer } from "../newt/peers";
|
import { addPeer, deletePeer } from "../newt/peers";
|
||||||
@@ -20,19 +8,9 @@ import config from "@server/lib/config";
|
|||||||
export async function buildSiteConfigurationForOlmClient(
|
export async function buildSiteConfigurationForOlmClient(
|
||||||
client: Client,
|
client: Client,
|
||||||
publicKey: string | null,
|
publicKey: string | null,
|
||||||
relay: boolean,
|
relay: boolean
|
||||||
jitMode: boolean = false
|
|
||||||
) {
|
) {
|
||||||
const siteConfigurations: {
|
const siteConfigurations = [];
|
||||||
siteId: number;
|
|
||||||
name?: string
|
|
||||||
endpoint?: string
|
|
||||||
publicKey?: string
|
|
||||||
serverIP?: string | null
|
|
||||||
serverPort?: number | null
|
|
||||||
remoteSubnets?: string[];
|
|
||||||
aliases: Alias[];
|
|
||||||
}[] = [];
|
|
||||||
|
|
||||||
// Get all sites data
|
// Get all sites data
|
||||||
const sitesData = await db
|
const sitesData = await db
|
||||||
@@ -49,40 +27,6 @@ export async function buildSiteConfigurationForOlmClient(
|
|||||||
sites: site,
|
sites: site,
|
||||||
clientSitesAssociationsCache: association
|
clientSitesAssociationsCache: association
|
||||||
} of sitesData) {
|
} of sitesData) {
|
||||||
const allSiteResources = await db // only get the site resources that this client has access to
|
|
||||||
.select()
|
|
||||||
.from(siteResources)
|
|
||||||
.innerJoin(
|
|
||||||
clientSiteResourcesAssociationsCache,
|
|
||||||
eq(
|
|
||||||
siteResources.siteResourceId,
|
|
||||||
clientSiteResourcesAssociationsCache.siteResourceId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(siteResources.siteId, site.siteId),
|
|
||||||
eq(
|
|
||||||
clientSiteResourcesAssociationsCache.clientId,
|
|
||||||
client.clientId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (jitMode) {
|
|
||||||
// Add site configuration to the array
|
|
||||||
siteConfigurations.push({
|
|
||||||
siteId: site.siteId,
|
|
||||||
// remoteSubnets: generateRemoteSubnets(
|
|
||||||
// allSiteResources.map(({ siteResources }) => siteResources)
|
|
||||||
// ),
|
|
||||||
aliases: generateAliasConfig(
|
|
||||||
allSiteResources.map(({ siteResources }) => siteResources)
|
|
||||||
)
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!site.exitNodeId) {
|
if (!site.exitNodeId) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Site ${site.siteId} does not have exit node, skipping`
|
`Site ${site.siteId} does not have exit node, skipping`
|
||||||
@@ -98,13 +42,6 @@ export async function buildSiteConfigurationForOlmClient(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!site.publicKey || site.publicKey == "") { // the site is not ready to accept new peers
|
|
||||||
logger.warn(
|
|
||||||
`Site ${site.siteId} has no public key, skipping`
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if (site.lastHolePunch && now - site.lastHolePunch > 6 && relay) {
|
// if (site.lastHolePunch && now - site.lastHolePunch > 6 && relay) {
|
||||||
// logger.warn(
|
// logger.warn(
|
||||||
// `Site ${site.siteId} last hole punch is too old, skipping`
|
// `Site ${site.siteId} last hole punch is too old, skipping`
|
||||||
@@ -166,6 +103,26 @@ export async function buildSiteConfigurationForOlmClient(
|
|||||||
relayEndpoint = `${exitNode.endpoint}:${config.getRawConfig().gerbil.clients_start_port}`;
|
relayEndpoint = `${exitNode.endpoint}:${config.getRawConfig().gerbil.clients_start_port}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const allSiteResources = await db // only get the site resources that this client has access to
|
||||||
|
.select()
|
||||||
|
.from(siteResources)
|
||||||
|
.innerJoin(
|
||||||
|
clientSiteResourcesAssociationsCache,
|
||||||
|
eq(
|
||||||
|
siteResources.siteResourceId,
|
||||||
|
clientSiteResourcesAssociationsCache.siteResourceId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(siteResources.siteId, site.siteId),
|
||||||
|
eq(
|
||||||
|
clientSiteResourcesAssociationsCache.clientId,
|
||||||
|
client.clientId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
// Add site configuration to the array
|
// Add site configuration to the array
|
||||||
siteConfigurations.push({
|
siteConfigurations.push({
|
||||||
siteId: site.siteId,
|
siteId: site.siteId,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import logger from "@server/logger";
|
|||||||
/**
|
/**
|
||||||
* Handles disconnecting messages from clients to show disconnected in the ui
|
* Handles disconnecting messages from clients to show disconnected in the ui
|
||||||
*/
|
*/
|
||||||
export const handleOlmDisconnectingMessage: MessageHandler = async (context) => {
|
export const handleOlmDisconnecingMessage: MessageHandler = async (context) => {
|
||||||
const { message, client: c, sendToClient } = context;
|
const { message, client: c, sendToClient } = context;
|
||||||
const olm = c as Olm;
|
const olm = c as Olm;
|
||||||
|
|
||||||
|
|||||||
@@ -17,9 +17,6 @@ import { getUserDeviceName } from "@server/db/names";
|
|||||||
import { buildSiteConfigurationForOlmClient } from "./buildConfiguration";
|
import { buildSiteConfigurationForOlmClient } from "./buildConfiguration";
|
||||||
import { OlmErrorCodes, sendOlmError } from "./error";
|
import { OlmErrorCodes, sendOlmError } from "./error";
|
||||||
import { handleFingerprintInsertion } from "./fingerprintingUtils";
|
import { handleFingerprintInsertion } from "./fingerprintingUtils";
|
||||||
import { Alias } from "@server/lib/ip";
|
|
||||||
import { build } from "@server/build";
|
|
||||||
import { canCompress } from "@server/lib/clientVersionChecks";
|
|
||||||
|
|
||||||
export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
||||||
logger.info("Handling register olm message!");
|
logger.info("Handling register olm message!");
|
||||||
@@ -210,32 +207,6 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all sites data
|
|
||||||
const sitesCountResult = await db
|
|
||||||
.select({ count: count() })
|
|
||||||
.from(sites)
|
|
||||||
.innerJoin(
|
|
||||||
clientSitesAssociationsCache,
|
|
||||||
eq(sites.siteId, clientSitesAssociationsCache.siteId)
|
|
||||||
)
|
|
||||||
.where(eq(clientSitesAssociationsCache.clientId, client.clientId));
|
|
||||||
|
|
||||||
// Extract the count value from the result array
|
|
||||||
const sitesCount =
|
|
||||||
sitesCountResult.length > 0 ? sitesCountResult[0].count : 0;
|
|
||||||
|
|
||||||
// Prepare an array to store site configurations
|
|
||||||
logger.debug(`Found ${sitesCount} sites for client ${client.clientId}`);
|
|
||||||
|
|
||||||
let jitMode = false;
|
|
||||||
if (sitesCount > 250 && build == "saas") {
|
|
||||||
// THIS IS THE MAX ON THE BUSINESS TIER
|
|
||||||
// we have too many sites
|
|
||||||
// If we have too many sites we need to drop into fully JIT mode by not sending any of the sites
|
|
||||||
logger.info("Too many sites (%d), dropping into JIT mode", sitesCount);
|
|
||||||
jitMode = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Olm client ID: ${client.clientId}, Public Key: ${publicKey}, Relay: ${relay}`
|
`Olm client ID: ${client.clientId}, Public Key: ${publicKey}, Relay: ${relay}`
|
||||||
);
|
);
|
||||||
@@ -262,12 +233,28 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
await db
|
await db
|
||||||
.update(clientSitesAssociationsCache)
|
.update(clientSitesAssociationsCache)
|
||||||
.set({
|
.set({
|
||||||
isRelayed: relay == true,
|
isRelayed: relay == true
|
||||||
isJitMode: jitMode
|
|
||||||
})
|
})
|
||||||
.where(eq(clientSitesAssociationsCache.clientId, client.clientId));
|
.where(eq(clientSitesAssociationsCache.clientId, client.clientId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get all sites data
|
||||||
|
const sitesCountResult = await db
|
||||||
|
.select({ count: count() })
|
||||||
|
.from(sites)
|
||||||
|
.innerJoin(
|
||||||
|
clientSitesAssociationsCache,
|
||||||
|
eq(sites.siteId, clientSitesAssociationsCache.siteId)
|
||||||
|
)
|
||||||
|
.where(eq(clientSitesAssociationsCache.clientId, client.clientId));
|
||||||
|
|
||||||
|
// Extract the count value from the result array
|
||||||
|
const sitesCount =
|
||||||
|
sitesCountResult.length > 0 ? sitesCountResult[0].count : 0;
|
||||||
|
|
||||||
|
// Prepare an array to store site configurations
|
||||||
|
logger.debug(`Found ${sitesCount} sites for client ${client.clientId}`);
|
||||||
|
|
||||||
// this prevents us from accepting a register from an olm that has not hole punched yet.
|
// this prevents us from accepting a register from an olm that has not hole punched yet.
|
||||||
// the olm will pump the register so we can keep checking
|
// the olm will pump the register so we can keep checking
|
||||||
// TODO: I still think there is a better way to do this rather than locking it out here but ???
|
// TODO: I still think there is a better way to do this rather than locking it out here but ???
|
||||||
@@ -282,10 +269,15 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
const siteConfigurations = await buildSiteConfigurationForOlmClient(
|
const siteConfigurations = await buildSiteConfigurationForOlmClient(
|
||||||
client,
|
client,
|
||||||
publicKey,
|
publicKey,
|
||||||
relay,
|
relay
|
||||||
jitMode
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// REMOVED THIS SO IT CREATES THE INTERFACE AND JUST WAITS FOR THE SITES
|
||||||
|
// if (siteConfigurations.length === 0) {
|
||||||
|
// logger.warn("No valid site configurations found");
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
// Return connect message with all site configurations
|
// Return connect message with all site configurations
|
||||||
return {
|
return {
|
||||||
message: {
|
message: {
|
||||||
@@ -296,9 +288,6 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
utilitySubnet: org.utilitySubnet
|
utilitySubnet: org.utilitySubnet
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
options: {
|
|
||||||
compress: canCompress(olm.version, "olm")
|
|
||||||
},
|
|
||||||
broadcast: false,
|
broadcast: false,
|
||||||
excludeSender: false
|
excludeSender: false
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const handleOlmRelayMessage: MessageHandler = async (context) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!olm.clientId) {
|
if (!olm.clientId) {
|
||||||
logger.warn("Olm has no client!");
|
logger.warn("Olm has no site!"); // TODO: Maybe we create the site here?
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ export const handleOlmRelayMessage: MessageHandler = async (context) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { siteId, chainId } = message.data;
|
const { siteId } = message.data;
|
||||||
|
|
||||||
// Get the site
|
// Get the site
|
||||||
const [site] = await db
|
const [site] = await db
|
||||||
@@ -90,8 +90,7 @@ export const handleOlmRelayMessage: MessageHandler = async (context) => {
|
|||||||
data: {
|
data: {
|
||||||
siteId: siteId,
|
siteId: siteId,
|
||||||
relayEndpoint: exitNode.endpoint,
|
relayEndpoint: exitNode.endpoint,
|
||||||
relayPort: config.getRawConfig().gerbil.clients_start_port,
|
relayPort: config.getRawConfig().gerbil.clients_start_port
|
||||||
chainId
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
broadcast: false,
|
broadcast: false,
|
||||||
|
|||||||
@@ -1,241 +0,0 @@
|
|||||||
import {
|
|
||||||
clientSiteResourcesAssociationsCache,
|
|
||||||
clientSitesAssociationsCache,
|
|
||||||
db,
|
|
||||||
exitNodes,
|
|
||||||
Site,
|
|
||||||
siteResources
|
|
||||||
} from "@server/db";
|
|
||||||
import { MessageHandler } from "@server/routers/ws";
|
|
||||||
import { clients, Olm, sites } from "@server/db";
|
|
||||||
import { and, eq, or } from "drizzle-orm";
|
|
||||||
import logger from "@server/logger";
|
|
||||||
import { initPeerAddHandshake } from "./peers";
|
|
||||||
|
|
||||||
export const handleOlmServerInitAddPeerHandshake: MessageHandler = async (
|
|
||||||
context
|
|
||||||
) => {
|
|
||||||
logger.info("Handling register olm message!");
|
|
||||||
const { message, client: c, sendToClient } = context;
|
|
||||||
const olm = c as Olm;
|
|
||||||
|
|
||||||
if (!olm) {
|
|
||||||
logger.warn("Olm not found");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!olm.clientId) {
|
|
||||||
logger.warn("Olm has no client!"); // TODO: Maybe we create the site here?
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const clientId = olm.clientId;
|
|
||||||
|
|
||||||
const [client] = await db
|
|
||||||
.select()
|
|
||||||
.from(clients)
|
|
||||||
.where(eq(clients.clientId, clientId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!client) {
|
|
||||||
logger.warn("Client not found");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { siteId, resourceId, chainId } = message.data;
|
|
||||||
|
|
||||||
let site: Site | null = null;
|
|
||||||
if (siteId) {
|
|
||||||
// get the site
|
|
||||||
const [siteRes] = await db
|
|
||||||
.select()
|
|
||||||
.from(sites)
|
|
||||||
.where(eq(sites.siteId, siteId))
|
|
||||||
.limit(1);
|
|
||||||
if (siteRes) {
|
|
||||||
site = siteRes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resourceId && !site) {
|
|
||||||
const resources = await db
|
|
||||||
.select()
|
|
||||||
.from(siteResources)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
or(
|
|
||||||
eq(siteResources.niceId, resourceId),
|
|
||||||
eq(siteResources.alias, resourceId)
|
|
||||||
),
|
|
||||||
eq(siteResources.orgId, client.orgId)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!resources || resources.length === 0) {
|
|
||||||
logger.error(`handleOlmServerPeerAddMessage: Resource not found`);
|
|
||||||
// cancel the request from the olm side to not keep doing this
|
|
||||||
await sendToClient(
|
|
||||||
olm.olmId,
|
|
||||||
{
|
|
||||||
type: "olm/wg/peer/chain/cancel",
|
|
||||||
data: {
|
|
||||||
chainId
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ incrementConfigVersion: false }
|
|
||||||
).catch((error) => {
|
|
||||||
logger.warn(`Error sending message:`, error);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resources.length > 1) {
|
|
||||||
// error but this should not happen because the nice id cant contain a dot and the alias has to have a dot and both have to be unique within the org so there should never be multiple matches
|
|
||||||
logger.error(
|
|
||||||
`handleOlmServerPeerAddMessage: Multiple resources found matching the criteria`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const resource = resources[0];
|
|
||||||
|
|
||||||
const currentResourceAssociationCaches = await db
|
|
||||||
.select()
|
|
||||||
.from(clientSiteResourcesAssociationsCache)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(
|
|
||||||
clientSiteResourcesAssociationsCache.siteResourceId,
|
|
||||||
resource.siteResourceId
|
|
||||||
),
|
|
||||||
eq(
|
|
||||||
clientSiteResourcesAssociationsCache.clientId,
|
|
||||||
client.clientId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (currentResourceAssociationCaches.length === 0) {
|
|
||||||
logger.error(
|
|
||||||
`handleOlmServerPeerAddMessage: Client ${client.clientId} does not have access to resource ${resource.siteResourceId}`
|
|
||||||
);
|
|
||||||
// cancel the request from the olm side to not keep doing this
|
|
||||||
await sendToClient(
|
|
||||||
olm.olmId,
|
|
||||||
{
|
|
||||||
type: "olm/wg/peer/chain/cancel",
|
|
||||||
data: {
|
|
||||||
chainId
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ incrementConfigVersion: false }
|
|
||||||
).catch((error) => {
|
|
||||||
logger.warn(`Error sending message:`, error);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const siteIdFromResource = resource.siteId;
|
|
||||||
|
|
||||||
// get the site
|
|
||||||
const [siteRes] = await db
|
|
||||||
.select()
|
|
||||||
.from(sites)
|
|
||||||
.where(eq(sites.siteId, siteIdFromResource));
|
|
||||||
if (!siteRes) {
|
|
||||||
logger.error(
|
|
||||||
`handleOlmServerPeerAddMessage: Site with ID ${site} not found`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
site = siteRes;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!site) {
|
|
||||||
logger.error(`handleOlmServerPeerAddMessage: Site not found`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if the client can access this site using the cache
|
|
||||||
const currentSiteAssociationCaches = await db
|
|
||||||
.select()
|
|
||||||
.from(clientSitesAssociationsCache)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(clientSitesAssociationsCache.clientId, client.clientId),
|
|
||||||
eq(clientSitesAssociationsCache.siteId, site.siteId)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (currentSiteAssociationCaches.length === 0) {
|
|
||||||
logger.error(
|
|
||||||
`handleOlmServerPeerAddMessage: Client ${client.clientId} does not have access to site ${site.siteId}`
|
|
||||||
);
|
|
||||||
// cancel the request from the olm side to not keep doing this
|
|
||||||
await sendToClient(
|
|
||||||
olm.olmId,
|
|
||||||
{
|
|
||||||
type: "olm/wg/peer/chain/cancel",
|
|
||||||
data: {
|
|
||||||
chainId
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ incrementConfigVersion: false }
|
|
||||||
).catch((error) => {
|
|
||||||
logger.warn(`Error sending message:`, error);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!site.exitNodeId) {
|
|
||||||
logger.error(
|
|
||||||
`handleOlmServerPeerAddMessage: Site with ID ${site.siteId} has no exit node`
|
|
||||||
);
|
|
||||||
// cancel the request from the olm side to not keep doing this
|
|
||||||
await sendToClient(
|
|
||||||
olm.olmId,
|
|
||||||
{
|
|
||||||
type: "olm/wg/peer/chain/cancel",
|
|
||||||
data: {
|
|
||||||
chainId
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ incrementConfigVersion: false }
|
|
||||||
).catch((error) => {
|
|
||||||
logger.warn(`Error sending message:`, error);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the exit node from the side
|
|
||||||
const [exitNode] = await db
|
|
||||||
.select()
|
|
||||||
.from(exitNodes)
|
|
||||||
.where(eq(exitNodes.exitNodeId, site.exitNodeId));
|
|
||||||
|
|
||||||
if (!exitNode) {
|
|
||||||
logger.error(
|
|
||||||
`handleOlmServerPeerAddMessage: Site with ID ${site.siteId} has no exit node`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// also trigger the peer add handshake in case the peer was not already added to the olm and we need to hole punch
|
|
||||||
// if it has already been added this will be a no-op
|
|
||||||
await initPeerAddHandshake(
|
|
||||||
// this will kick off the add peer process for the client
|
|
||||||
client.clientId,
|
|
||||||
{
|
|
||||||
siteId: site.siteId,
|
|
||||||
exitNode: {
|
|
||||||
publicKey: exitNode.publicKey,
|
|
||||||
endpoint: exitNode.endpoint
|
|
||||||
}
|
|
||||||
},
|
|
||||||
olm.olmId,
|
|
||||||
chainId
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
@@ -54,7 +54,7 @@ export const handleOlmServerPeerAddMessage: MessageHandler = async (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { siteId, chainId } = message.data;
|
const { siteId } = message.data;
|
||||||
|
|
||||||
// get the site
|
// get the site
|
||||||
const [site] = await db
|
const [site] = await db
|
||||||
@@ -179,8 +179,7 @@ export const handleOlmServerPeerAddMessage: MessageHandler = async (
|
|||||||
),
|
),
|
||||||
aliases: generateAliasConfig(
|
aliases: generateAliasConfig(
|
||||||
allSiteResources.map(({ siteResources }) => siteResources)
|
allSiteResources.map(({ siteResources }) => siteResources)
|
||||||
),
|
)
|
||||||
chainId: chainId,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
broadcast: false,
|
broadcast: false,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export const handleOlmUnRelayMessage: MessageHandler = async (context) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!olm.clientId) {
|
if (!olm.clientId) {
|
||||||
logger.warn("Olm has no client!");
|
logger.warn("Olm has no site!"); // TODO: Maybe we create the site here?
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ export const handleOlmUnRelayMessage: MessageHandler = async (context) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { siteId, chainId } = message.data;
|
const { siteId } = message.data;
|
||||||
|
|
||||||
// Get the site
|
// Get the site
|
||||||
const [site] = await db
|
const [site] = await db
|
||||||
@@ -87,8 +87,7 @@ export const handleOlmUnRelayMessage: MessageHandler = async (context) => {
|
|||||||
type: "olm/wg/peer/unrelay",
|
type: "olm/wg/peer/unrelay",
|
||||||
data: {
|
data: {
|
||||||
siteId: siteId,
|
siteId: siteId,
|
||||||
endpoint: site.endpoint,
|
endpoint: site.endpoint
|
||||||
chainId
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
broadcast: false,
|
broadcast: false,
|
||||||
|
|||||||
@@ -11,4 +11,3 @@ export * from "./handleOlmServerPeerAddMessage";
|
|||||||
export * from "./handleOlmUnRelayMessage";
|
export * from "./handleOlmUnRelayMessage";
|
||||||
export * from "./recoverOlmWithFingerprint";
|
export * from "./recoverOlmWithFingerprint";
|
||||||
export * from "./handleOlmDisconnectingMessage";
|
export * from "./handleOlmDisconnectingMessage";
|
||||||
export * from "./handleOlmServerInitAddPeerHandshake";
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { sendToClient } from "#dynamic/routers/ws";
|
import { sendToClient } from "#dynamic/routers/ws";
|
||||||
import { clientSitesAssociationsCache, db, olms } from "@server/db";
|
import { db, olms } from "@server/db";
|
||||||
import { canCompress } from "@server/lib/clientVersionChecks";
|
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { Alias } from "yaml";
|
import { Alias } from "yaml";
|
||||||
|
|
||||||
export async function addPeer(
|
export async function addPeer(
|
||||||
@@ -19,8 +18,7 @@ export async function addPeer(
|
|||||||
remoteSubnets: string[] | null; // optional, comma-separated list of subnets that this site can access
|
remoteSubnets: string[] | null; // optional, comma-separated list of subnets that this site can access
|
||||||
aliases: Alias[];
|
aliases: Alias[];
|
||||||
},
|
},
|
||||||
olmId?: string,
|
olmId?: string
|
||||||
version?: string | null
|
|
||||||
) {
|
) {
|
||||||
if (!olmId) {
|
if (!olmId) {
|
||||||
const [olm] = await db
|
const [olm] = await db
|
||||||
@@ -32,7 +30,6 @@ export async function addPeer(
|
|||||||
return; // ignore this because an olm might not be associated with the client anymore
|
return; // ignore this because an olm might not be associated with the client anymore
|
||||||
}
|
}
|
||||||
olmId = olm.olmId;
|
olmId = olm.olmId;
|
||||||
version = olm.version;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await sendToClient(
|
await sendToClient(
|
||||||
@@ -51,7 +48,7 @@ export async function addPeer(
|
|||||||
aliases: peer.aliases
|
aliases: peer.aliases
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ incrementConfigVersion: true, compress: canCompress(version, "olm") }
|
{ incrementConfigVersion: true }
|
||||||
).catch((error) => {
|
).catch((error) => {
|
||||||
logger.warn(`Error sending message:`, error);
|
logger.warn(`Error sending message:`, error);
|
||||||
});
|
});
|
||||||
@@ -63,8 +60,7 @@ export async function deletePeer(
|
|||||||
clientId: number,
|
clientId: number,
|
||||||
siteId: number,
|
siteId: number,
|
||||||
publicKey: string,
|
publicKey: string,
|
||||||
olmId?: string,
|
olmId?: string
|
||||||
version?: string | null
|
|
||||||
) {
|
) {
|
||||||
if (!olmId) {
|
if (!olmId) {
|
||||||
const [olm] = await db
|
const [olm] = await db
|
||||||
@@ -76,7 +72,6 @@ export async function deletePeer(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
olmId = olm.olmId;
|
olmId = olm.olmId;
|
||||||
version = olm.version;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await sendToClient(
|
await sendToClient(
|
||||||
@@ -88,7 +83,7 @@ export async function deletePeer(
|
|||||||
siteId: siteId
|
siteId: siteId
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ incrementConfigVersion: true, compress: canCompress(version, "olm") }
|
{ incrementConfigVersion: true }
|
||||||
).catch((error) => {
|
).catch((error) => {
|
||||||
logger.warn(`Error sending message:`, error);
|
logger.warn(`Error sending message:`, error);
|
||||||
});
|
});
|
||||||
@@ -108,8 +103,7 @@ export async function updatePeer(
|
|||||||
remoteSubnets?: string[] | null; // optional, comma-separated list of subnets that
|
remoteSubnets?: string[] | null; // optional, comma-separated list of subnets that
|
||||||
aliases?: Alias[] | null;
|
aliases?: Alias[] | null;
|
||||||
},
|
},
|
||||||
olmId?: string,
|
olmId?: string
|
||||||
version?: string | null
|
|
||||||
) {
|
) {
|
||||||
if (!olmId) {
|
if (!olmId) {
|
||||||
const [olm] = await db
|
const [olm] = await db
|
||||||
@@ -121,7 +115,6 @@ export async function updatePeer(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
olmId = olm.olmId;
|
olmId = olm.olmId;
|
||||||
version = olm.version;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await sendToClient(
|
await sendToClient(
|
||||||
@@ -139,7 +132,7 @@ export async function updatePeer(
|
|||||||
aliases: peer.aliases
|
aliases: peer.aliases
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ incrementConfigVersion: true, compress: canCompress(version, "olm") }
|
{ incrementConfigVersion: true }
|
||||||
).catch((error) => {
|
).catch((error) => {
|
||||||
logger.warn(`Error sending message:`, error);
|
logger.warn(`Error sending message:`, error);
|
||||||
});
|
});
|
||||||
@@ -156,8 +149,7 @@ export async function initPeerAddHandshake(
|
|||||||
endpoint: string;
|
endpoint: string;
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
olmId?: string,
|
olmId?: string
|
||||||
chainId?: string
|
|
||||||
) {
|
) {
|
||||||
if (!olmId) {
|
if (!olmId) {
|
||||||
const [olm] = await db
|
const [olm] = await db
|
||||||
@@ -181,8 +173,7 @@ export async function initPeerAddHandshake(
|
|||||||
publicKey: peer.exitNode.publicKey,
|
publicKey: peer.exitNode.publicKey,
|
||||||
relayPort: config.getRawConfig().gerbil.clients_start_port,
|
relayPort: config.getRawConfig().gerbil.clients_start_port,
|
||||||
endpoint: peer.exitNode.endpoint
|
endpoint: peer.exitNode.endpoint
|
||||||
},
|
}
|
||||||
chainId
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ incrementConfigVersion: true }
|
{ incrementConfigVersion: true }
|
||||||
@@ -190,17 +181,6 @@ export async function initPeerAddHandshake(
|
|||||||
logger.warn(`Error sending message:`, error);
|
logger.warn(`Error sending message:`, error);
|
||||||
});
|
});
|
||||||
|
|
||||||
// update the clientSiteAssociationsCache to make the isJitMode flag false so that JIT mode is disabled for this site if it restarts or something after the connection
|
|
||||||
await db
|
|
||||||
.update(clientSitesAssociationsCache)
|
|
||||||
.set({ isJitMode: false })
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(clientSitesAssociationsCache.clientId, clientId),
|
|
||||||
eq(clientSitesAssociationsCache.siteId, peer.siteId)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`Initiated peer add handshake for site ${peer.siteId} to olm ${olmId}`
|
`Initiated peer add handshake for site ${peer.siteId} to olm ${olmId}`
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,17 +1,9 @@
|
|||||||
import {
|
import { Client, db, exitNodes, Olm, sites, clientSitesAssociationsCache } from "@server/db";
|
||||||
Client,
|
|
||||||
db,
|
|
||||||
exitNodes,
|
|
||||||
Olm,
|
|
||||||
sites,
|
|
||||||
clientSitesAssociationsCache
|
|
||||||
} from "@server/db";
|
|
||||||
import { buildSiteConfigurationForOlmClient } from "./buildConfiguration";
|
import { buildSiteConfigurationForOlmClient } from "./buildConfiguration";
|
||||||
import { sendToClient } from "#dynamic/routers/ws";
|
import { sendToClient } from "#dynamic/routers/ws";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { eq, inArray } from "drizzle-orm";
|
import { eq, inArray } from "drizzle-orm";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import { canCompress } from "@server/lib/clientVersionChecks";
|
|
||||||
|
|
||||||
export async function sendOlmSyncMessage(olm: Olm, client: Client) {
|
export async function sendOlmSyncMessage(olm: Olm, client: Client) {
|
||||||
// NOTE: WE ARE HARDCODING THE RELAY PARAMETER TO FALSE HERE BUT IN THE REGISTER MESSAGE ITS DEFINED BY THE CLIENT
|
// NOTE: WE ARE HARDCODING THE RELAY PARAMETER TO FALSE HERE BUT IN THE REGISTER MESSAGE ITS DEFINED BY THE CLIENT
|
||||||
@@ -25,7 +17,10 @@ export async function sendOlmSyncMessage(olm: Olm, client: Client) {
|
|||||||
const clientSites = await db
|
const clientSites = await db
|
||||||
.select()
|
.select()
|
||||||
.from(clientSitesAssociationsCache)
|
.from(clientSitesAssociationsCache)
|
||||||
.innerJoin(sites, eq(sites.siteId, clientSitesAssociationsCache.siteId))
|
.innerJoin(
|
||||||
|
sites,
|
||||||
|
eq(sites.siteId, clientSitesAssociationsCache.siteId)
|
||||||
|
)
|
||||||
.where(eq(clientSitesAssociationsCache.clientId, client.clientId));
|
.where(eq(clientSitesAssociationsCache.clientId, client.clientId));
|
||||||
|
|
||||||
// Extract unique exit node IDs
|
// Extract unique exit node IDs
|
||||||
@@ -73,20 +68,13 @@ export async function sendOlmSyncMessage(olm: Olm, client: Client) {
|
|||||||
|
|
||||||
logger.debug("sendOlmSyncMessage: sending sync message");
|
logger.debug("sendOlmSyncMessage: sending sync message");
|
||||||
|
|
||||||
await sendToClient(
|
await sendToClient(olm.olmId, {
|
||||||
olm.olmId,
|
|
||||||
{
|
|
||||||
type: "olm/sync",
|
type: "olm/sync",
|
||||||
data: {
|
data: {
|
||||||
sites: siteConfigurations,
|
sites: siteConfigurations,
|
||||||
exitNodes: exitNodesData
|
exitNodes: exitNodesData
|
||||||
}
|
}
|
||||||
},
|
}).catch((error) => {
|
||||||
|
|
||||||
{
|
|
||||||
compress: canCompress(olm.version, "olm")
|
|
||||||
}
|
|
||||||
).catch((error) => {
|
|
||||||
logger.warn(`Error sending olm sync message:`, error);
|
logger.warn(`Error sending olm sync message:`, error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -223,20 +223,6 @@ async function createHttpResource(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent creating resource with same domain as dashboard
|
|
||||||
const dashboardUrl = config.getRawConfig().app.dashboard_url;
|
|
||||||
if (dashboardUrl) {
|
|
||||||
const dashboardHost = new URL(dashboardUrl).hostname;
|
|
||||||
if (fullDomain === dashboardHost) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.CONFLICT,
|
|
||||||
"Resource domain cannot be the same as the dashboard domain"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (build != "oss") {
|
if (build != "oss") {
|
||||||
const existingLoginPages = await db
|
const existingLoginPages = await db
|
||||||
.select()
|
.select()
|
||||||
|
|||||||
@@ -353,20 +353,6 @@ async function updateHttpResource(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent updating resource with same domain as dashboard
|
|
||||||
const dashboardUrl = config.getRawConfig().app.dashboard_url;
|
|
||||||
if (dashboardUrl) {
|
|
||||||
const dashboardHost = new URL(dashboardUrl).hostname;
|
|
||||||
if (fullDomain === dashboardHost) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.CONFLICT,
|
|
||||||
"Resource domain cannot be the same as the dashboard domain"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (build != "oss") {
|
if (build != "oss") {
|
||||||
const existingLoginPages = await db
|
const existingLoginPages = await db
|
||||||
.select()
|
.select()
|
||||||
|
|||||||
@@ -620,7 +620,7 @@ export async function handleMessagingForUpdatedSiteResource(
|
|||||||
await updateTargets(newt.newtId, {
|
await updateTargets(newt.newtId, {
|
||||||
oldTargets: oldTargets,
|
oldTargets: oldTargets,
|
||||||
newTargets: newTargets
|
newTargets: newTargets
|
||||||
}, newt.version);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const olmJobs: Promise<void>[] = [];
|
const olmJobs: Promise<void>[] = [];
|
||||||
|
|||||||
@@ -264,7 +264,7 @@ export async function createTarget(
|
|||||||
newTarget,
|
newTarget,
|
||||||
healthCheck,
|
healthCheck,
|
||||||
resource.protocol,
|
resource.protocol,
|
||||||
newt.version
|
resource.proxyPort
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -262,7 +262,7 @@ export async function updateTarget(
|
|||||||
[updatedTarget],
|
[updatedTarget],
|
||||||
[updatedHc],
|
[updatedHc],
|
||||||
resource.protocol,
|
resource.protocol,
|
||||||
newt.version
|
resource.proxyPort
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { build } from "@server/build";
|
|
||||||
import {
|
import {
|
||||||
handleNewtRegisterMessage,
|
handleNewtRegisterMessage,
|
||||||
handleReceiveBandwidthMessage,
|
handleReceiveBandwidthMessage,
|
||||||
@@ -7,9 +6,7 @@ import {
|
|||||||
handleDockerContainersMessage,
|
handleDockerContainersMessage,
|
||||||
handleNewtPingRequestMessage,
|
handleNewtPingRequestMessage,
|
||||||
handleApplyBlueprintMessage,
|
handleApplyBlueprintMessage,
|
||||||
handleNewtPingMessage,
|
handleNewtPingMessage
|
||||||
startNewtOfflineChecker,
|
|
||||||
handleNewtDisconnectingMessage
|
|
||||||
} from "../newt";
|
} from "../newt";
|
||||||
import {
|
import {
|
||||||
handleOlmRegisterMessage,
|
handleOlmRegisterMessage,
|
||||||
@@ -18,8 +15,7 @@ import {
|
|||||||
startOlmOfflineChecker,
|
startOlmOfflineChecker,
|
||||||
handleOlmServerPeerAddMessage,
|
handleOlmServerPeerAddMessage,
|
||||||
handleOlmUnRelayMessage,
|
handleOlmUnRelayMessage,
|
||||||
handleOlmDisconnectingMessage,
|
handleOlmDisconnecingMessage
|
||||||
handleOlmServerInitAddPeerHandshake
|
|
||||||
} from "../olm";
|
} from "../olm";
|
||||||
import { handleHealthcheckStatusMessage } from "../target";
|
import { handleHealthcheckStatusMessage } from "../target";
|
||||||
import { handleRoundTripMessage } from "./handleRoundTripMessage";
|
import { handleRoundTripMessage } from "./handleRoundTripMessage";
|
||||||
@@ -27,13 +23,11 @@ import { MessageHandler } from "./types";
|
|||||||
|
|
||||||
export const messageHandlers: Record<string, MessageHandler> = {
|
export const messageHandlers: Record<string, MessageHandler> = {
|
||||||
"olm/wg/server/peer/add": handleOlmServerPeerAddMessage,
|
"olm/wg/server/peer/add": handleOlmServerPeerAddMessage,
|
||||||
"olm/wg/server/peer/init": handleOlmServerInitAddPeerHandshake,
|
|
||||||
"olm/wg/register": handleOlmRegisterMessage,
|
"olm/wg/register": handleOlmRegisterMessage,
|
||||||
"olm/wg/relay": handleOlmRelayMessage,
|
"olm/wg/relay": handleOlmRelayMessage,
|
||||||
"olm/wg/unrelay": handleOlmUnRelayMessage,
|
"olm/wg/unrelay": handleOlmUnRelayMessage,
|
||||||
"olm/ping": handleOlmPingMessage,
|
"olm/ping": handleOlmPingMessage,
|
||||||
"olm/disconnecting": handleOlmDisconnectingMessage,
|
"olm/disconnecting": handleOlmDisconnecingMessage,
|
||||||
"newt/disconnecting": handleNewtDisconnectingMessage,
|
|
||||||
"newt/ping": handleNewtPingMessage,
|
"newt/ping": handleNewtPingMessage,
|
||||||
"newt/wg/register": handleNewtRegisterMessage,
|
"newt/wg/register": handleNewtRegisterMessage,
|
||||||
"newt/wg/get-config": handleGetConfigMessage,
|
"newt/wg/get-config": handleGetConfigMessage,
|
||||||
@@ -46,7 +40,4 @@ export const messageHandlers: Record<string, MessageHandler> = {
|
|||||||
"ws/round-trip/complete": handleRoundTripMessage
|
"ws/round-trip/complete": handleRoundTripMessage
|
||||||
};
|
};
|
||||||
|
|
||||||
if (build != "saas") {
|
startOlmOfflineChecker(); // this is to handle the offline check for olms
|
||||||
startOlmOfflineChecker(); // this is to handle the offline check for olms
|
|
||||||
startNewtOfflineChecker(); // this is to handle the offline check for newts
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export interface AuthenticatedWebSocket extends WebSocket {
|
|||||||
clientType?: ClientType;
|
clientType?: ClientType;
|
||||||
connectionId?: string;
|
connectionId?: string;
|
||||||
isFullyConnected?: boolean;
|
isFullyConnected?: boolean;
|
||||||
pendingMessages?: { data: Buffer; isBinary: boolean }[];
|
pendingMessages?: Buffer[];
|
||||||
configVersion?: number;
|
configVersion?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,7 +73,6 @@ export type MessageHandler = (
|
|||||||
// Options for sending messages with config version tracking
|
// Options for sending messages with config version tracking
|
||||||
export interface SendMessageOptions {
|
export interface SendMessageOptions {
|
||||||
incrementConfigVersion?: boolean;
|
incrementConfigVersion?: boolean;
|
||||||
compress?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redis message type for cross-node communication
|
// Redis message type for cross-node communication
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { Router, Request, Response } from "express";
|
import { Router, Request, Response } from "express";
|
||||||
import zlib from "zlib";
|
|
||||||
import { Server as HttpServer } from "http";
|
import { Server as HttpServer } from "http";
|
||||||
import { WebSocket, WebSocketServer } from "ws";
|
import { WebSocket, WebSocketServer } from "ws";
|
||||||
import { Socket } from "net";
|
import { Socket } from "net";
|
||||||
import { Newt, newts, NewtSession, olms, Olm, OlmSession, sites } from "@server/db";
|
import { Newt, newts, NewtSession, olms, Olm, OlmSession } from "@server/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { db } from "@server/db";
|
import { db } from "@server/db";
|
||||||
import { validateNewtSessionToken } from "@server/auth/sessions/newt";
|
import { validateNewtSessionToken } from "@server/auth/sessions/newt";
|
||||||
@@ -117,20 +116,11 @@ const sendToClientLocal = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const messageString = JSON.stringify(messageWithVersion);
|
const messageString = JSON.stringify(messageWithVersion);
|
||||||
if (options.compress) {
|
|
||||||
const compressed = zlib.gzipSync(Buffer.from(messageString, "utf8"));
|
|
||||||
clients.forEach((client) => {
|
|
||||||
if (client.readyState === WebSocket.OPEN) {
|
|
||||||
client.send(compressed);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
clients.forEach((client) => {
|
clients.forEach((client) => {
|
||||||
if (client.readyState === WebSocket.OPEN) {
|
if (client.readyState === WebSocket.OPEN) {
|
||||||
client.send(messageString);
|
client.send(messageString);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -157,23 +147,12 @@ const broadcastToAllExceptLocal = async (
|
|||||||
...message,
|
...message,
|
||||||
configVersion
|
configVersion
|
||||||
};
|
};
|
||||||
if (options.compress) {
|
|
||||||
const compressed = zlib.gzipSync(
|
|
||||||
Buffer.from(JSON.stringify(messageWithVersion), "utf8")
|
|
||||||
);
|
|
||||||
clients.forEach((client) => {
|
|
||||||
if (client.readyState === WebSocket.OPEN) {
|
|
||||||
client.send(compressed);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
clients.forEach((client) => {
|
clients.forEach((client) => {
|
||||||
if (client.readyState === WebSocket.OPEN) {
|
if (client.readyState === WebSocket.OPEN) {
|
||||||
client.send(JSON.stringify(messageWithVersion));
|
client.send(JSON.stringify(messageWithVersion));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -307,12 +286,9 @@ const setupConnection = async (
|
|||||||
clientType === "newt" ? (client as Newt).newtId : (client as Olm).olmId;
|
clientType === "newt" ? (client as Newt).newtId : (client as Olm).olmId;
|
||||||
await addClient(clientType, clientId, ws);
|
await addClient(clientType, clientId, ws);
|
||||||
|
|
||||||
ws.on("message", async (data, isBinary) => {
|
ws.on("message", async (data) => {
|
||||||
try {
|
try {
|
||||||
const messageBuffer = isBinary
|
const message: WSMessage = JSON.parse(data.toString());
|
||||||
? zlib.gunzipSync(data as Buffer)
|
|
||||||
: (data as Buffer);
|
|
||||||
const message: WSMessage = JSON.parse(messageBuffer.toString());
|
|
||||||
|
|
||||||
if (!message.type || typeof message.type !== "string") {
|
if (!message.type || typeof message.type !== "string") {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -380,31 +356,6 @@ const setupConnection = async (
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle WebSocket protocol-level pings from older newt clients that do
|
|
||||||
// not send application-level "newt/ping" messages. Update the site's
|
|
||||||
// online state and lastPing timestamp so the offline checker treats them
|
|
||||||
// the same as modern newt clients.
|
|
||||||
if (clientType === "newt") {
|
|
||||||
const newtClient = client as Newt;
|
|
||||||
ws.on("ping", async () => {
|
|
||||||
if (!newtClient.siteId) return;
|
|
||||||
try {
|
|
||||||
await db
|
|
||||||
.update(sites)
|
|
||||||
.set({
|
|
||||||
online: true,
|
|
||||||
lastPing: Math.floor(Date.now() / 1000)
|
|
||||||
})
|
|
||||||
.where(eq(sites.siteId, newtClient.siteId));
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(
|
|
||||||
"Error updating newt site online state on WS ping",
|
|
||||||
{ error }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ws.on("error", (error: Error) => {
|
ws.on("error", (error: Error) => {
|
||||||
logger.error(
|
logger.error(
|
||||||
`WebSocket error for ${clientType.toUpperCase()} ID ${clientId}:`,
|
`WebSocket error for ${clientType.toUpperCase()} ID ${clientId}:`,
|
||||||
|
|||||||
@@ -69,7 +69,6 @@ export default async function DomainSettingsPage({
|
|||||||
failed={domain.failed}
|
failed={domain.failed}
|
||||||
verified={domain.verified}
|
verified={domain.verified}
|
||||||
type={domain.type}
|
type={domain.type}
|
||||||
errorMessage={domain.errorMessage}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DNSRecordsTable records={dnsRecords} type={domain.type} />
|
<DNSRecordsTable records={dnsRecords} type={domain.type} />
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger
|
TooltipTrigger
|
||||||
} from "@app/components/ui/tooltip";
|
} from "@app/components/ui/tooltip";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
@@ -66,7 +65,6 @@ import { build } from "@server/build";
|
|||||||
import { Resource } from "@server/db";
|
import { Resource } from "@server/db";
|
||||||
import { isTargetValid } from "@server/lib/validators";
|
import { isTargetValid } from "@server/lib/validators";
|
||||||
import { ListTargetsResponse } from "@server/routers/target";
|
import { ListTargetsResponse } from "@server/routers/target";
|
||||||
import { ListRemoteExitNodesResponse } from "@server/routers/remoteExitNode/types";
|
|
||||||
import { ArrayElement } from "@server/types/ArrayElement";
|
import { ArrayElement } from "@server/types/ArrayElement";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
@@ -83,7 +81,6 @@ import {
|
|||||||
CircleCheck,
|
CircleCheck,
|
||||||
CircleX,
|
CircleX,
|
||||||
Info,
|
Info,
|
||||||
InfoIcon,
|
|
||||||
Plus,
|
Plus,
|
||||||
Settings,
|
Settings,
|
||||||
SquareArrowOutUpRight
|
SquareArrowOutUpRight
|
||||||
@@ -213,13 +210,6 @@ export default function Page() {
|
|||||||
orgQueries.sites({ orgId: orgId as string })
|
orgQueries.sites({ orgId: orgId as string })
|
||||||
);
|
);
|
||||||
|
|
||||||
const [remoteExitNodes, setRemoteExitNodes] = useState<
|
|
||||||
ListRemoteExitNodesResponse["remoteExitNodes"]
|
|
||||||
>([]);
|
|
||||||
const [loadingExitNodes, setLoadingExitNodes] = useState(
|
|
||||||
build === "saas"
|
|
||||||
);
|
|
||||||
|
|
||||||
const [createLoading, setCreateLoading] = useState(false);
|
const [createLoading, setCreateLoading] = useState(false);
|
||||||
const [showSnippets, setShowSnippets] = useState(false);
|
const [showSnippets, setShowSnippets] = useState(false);
|
||||||
const [niceId, setNiceId] = useState<string>("");
|
const [niceId, setNiceId] = useState<string>("");
|
||||||
@@ -234,27 +224,6 @@ export default function Page() {
|
|||||||
useState<LocalTarget | null>(null);
|
useState<LocalTarget | null>(null);
|
||||||
const [healthCheckDialogOpen, setHealthCheckDialogOpen] = useState(false);
|
const [healthCheckDialogOpen, setHealthCheckDialogOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (build !== "saas") return;
|
|
||||||
|
|
||||||
const fetchExitNodes = async () => {
|
|
||||||
try {
|
|
||||||
const res = await api.get<
|
|
||||||
AxiosResponse<ListRemoteExitNodesResponse>
|
|
||||||
>(`/org/${orgId}/remote-exit-nodes`);
|
|
||||||
if (res && res.status === 200) {
|
|
||||||
setRemoteExitNodes(res.data.data.remoteExitNodes);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Failed to fetch remote exit nodes:", e);
|
|
||||||
} finally {
|
|
||||||
setLoadingExitNodes(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchExitNodes();
|
|
||||||
}, [orgId]);
|
|
||||||
|
|
||||||
const [isAdvancedMode, setIsAdvancedMode] = useState(() => {
|
const [isAdvancedMode, setIsAdvancedMode] = useState(() => {
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
const saved = localStorage.getItem("create-advanced-mode");
|
const saved = localStorage.getItem("create-advanced-mode");
|
||||||
@@ -319,26 +288,16 @@ export default function Page() {
|
|||||||
description: t("resourceHTTPDescription")
|
description: t("resourceHTTPDescription")
|
||||||
},
|
},
|
||||||
...(!env.flags.allowRawResources
|
...(!env.flags.allowRawResources
|
||||||
? []
|
|
||||||
: build === "saas" && remoteExitNodes.length === 0
|
|
||||||
? []
|
? []
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
id: "raw" as ResourceType,
|
id: "raw" as ResourceType,
|
||||||
title: t("resourceRaw"),
|
title: t("resourceRaw"),
|
||||||
description:
|
description: build == "saas" ? t("resourceRawDescriptionCloud") : t("resourceRawDescription")
|
||||||
build == "saas"
|
|
||||||
? t("resourceRawDescriptionCloud")
|
|
||||||
: t("resourceRawDescription")
|
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
];
|
];
|
||||||
|
|
||||||
// In saas mode with no exit nodes, force HTTP
|
|
||||||
const showTypeSelector =
|
|
||||||
build !== "saas" ||
|
|
||||||
(!loadingExitNodes && remoteExitNodes.length > 0);
|
|
||||||
|
|
||||||
const baseForm = useForm({
|
const baseForm = useForm({
|
||||||
resolver: zodResolver(baseResourceFormSchema),
|
resolver: zodResolver(baseResourceFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -600,7 +559,7 @@ export default function Page() {
|
|||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: t("resourceErrorCreate"),
|
title: t("resourceErrorCreate"),
|
||||||
description: formatAxiosError(e, t("resourceErrorCreateMessageDescription"))
|
description: t("resourceErrorCreateMessageDescription")
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1025,8 +984,7 @@ export default function Page() {
|
|||||||
</SettingsSectionTitle>
|
</SettingsSectionTitle>
|
||||||
</SettingsSectionHeader>
|
</SettingsSectionHeader>
|
||||||
<SettingsSectionBody>
|
<SettingsSectionBody>
|
||||||
{showTypeSelector &&
|
{resourceTypes.length > 1 && (
|
||||||
resourceTypes.length > 1 && (
|
|
||||||
<>
|
<>
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ const CredenzaContent = ({ className, children, ...props }: CredenzaProps) => {
|
|||||||
return (
|
return (
|
||||||
<CredenzaContent
|
<CredenzaContent
|
||||||
className={cn(
|
className={cn(
|
||||||
"overflow-y-auto max-h-[100dvh] md:max-h-[calc(100vh-clamp(3rem,24vh,400px))] md:top-[clamp(1.5rem,12vh,200px)] md:translate-y-0",
|
"overflow-y-auto max-h-[100dvh] md:max-h-screen md:top-[clamp(1.5rem,12vh,200px)] md:translate-y-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -10,20 +10,17 @@ import {
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { Badge } from "./ui/badge";
|
import { Badge } from "./ui/badge";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { AlertTriangle } from "lucide-react";
|
|
||||||
|
|
||||||
type DomainInfoCardProps = {
|
type DomainInfoCardProps = {
|
||||||
failed: boolean;
|
failed: boolean;
|
||||||
verified: boolean;
|
verified: boolean;
|
||||||
type: string | null;
|
type: string | null;
|
||||||
errorMessage?: string | null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function DomainInfoCard({
|
export default function DomainInfoCard({
|
||||||
failed,
|
failed,
|
||||||
verified,
|
verified,
|
||||||
type,
|
type
|
||||||
errorMessage
|
|
||||||
}: DomainInfoCardProps) {
|
}: DomainInfoCardProps) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const env = useEnvContext();
|
const env = useEnvContext();
|
||||||
@@ -42,7 +39,6 @@ export default function DomainInfoCard({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
|
||||||
<Alert>
|
<Alert>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
<InfoSections cols={3}>
|
<InfoSections cols={3}>
|
||||||
@@ -83,19 +79,5 @@ export default function DomainInfoCard({
|
|||||||
</InfoSections>
|
</InfoSections>
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
{errorMessage && (failed || !verified) && (
|
|
||||||
<Alert variant={failed ? "destructive" : "warning"}>
|
|
||||||
<AlertTriangle className="h-4 w-4" />
|
|
||||||
<AlertTitle>
|
|
||||||
{failed
|
|
||||||
? t("domainErrorTitle", { fallback: "Domain Error" })
|
|
||||||
: t("domainPendingErrorTitle", { fallback: "Verification Issue" })}
|
|
||||||
</AlertTitle>
|
|
||||||
<AlertDescription className="font-mono text-xs break-all">
|
|
||||||
{errorMessage}
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,12 +27,6 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger
|
DropdownMenuTrigger
|
||||||
} from "./ui/dropdown-menu";
|
} from "./ui/dropdown-menu";
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger
|
|
||||||
} from "./ui/tooltip";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
export type DomainRow = {
|
export type DomainRow = {
|
||||||
@@ -45,7 +39,6 @@ export type DomainRow = {
|
|||||||
configManaged: boolean;
|
configManaged: boolean;
|
||||||
certResolver: string;
|
certResolver: string;
|
||||||
preferWildcardCert: boolean;
|
preferWildcardCert: boolean;
|
||||||
errorMessage?: string | null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -182,7 +175,7 @@ export default function DomainsTable({ domains, orgId }: Props) {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const { verified, failed, type, errorMessage } = row.original;
|
const { verified, failed, type } = row.original;
|
||||||
if (verified) {
|
if (verified) {
|
||||||
return type == "wildcard" ? (
|
return type == "wildcard" ? (
|
||||||
<Badge variant="outlinePrimary">{t("manual")}</Badge>
|
<Badge variant="outlinePrimary">{t("manual")}</Badge>
|
||||||
@@ -190,44 +183,12 @@ export default function DomainsTable({ domains, orgId }: Props) {
|
|||||||
<Badge variant="green">{t("verified")}</Badge>
|
<Badge variant="green">{t("verified")}</Badge>
|
||||||
);
|
);
|
||||||
} else if (failed) {
|
} else if (failed) {
|
||||||
if (errorMessage) {
|
|
||||||
return (
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Badge variant="red" className="cursor-help">
|
|
||||||
{t("failed", { fallback: "Failed" })}
|
|
||||||
</Badge>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent className="max-w-xs">
|
|
||||||
<p className="break-words">{errorMessage}</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<Badge variant="red">
|
<Badge variant="red">
|
||||||
{t("failed", { fallback: "Failed" })}
|
{t("failed", { fallback: "Failed" })}
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
if (errorMessage) {
|
|
||||||
return (
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Badge variant="yellow" className="cursor-help">
|
|
||||||
{t("pending")}
|
|
||||||
</Badge>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent className="max-w-xs">
|
|
||||||
<p className="break-words">{errorMessage}</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return <Badge variant="yellow">{t("pending")}</Badge>;
|
return <Badge variant="yellow">{t("pending")}</Badge>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user