mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-30 14:16:38 +00:00
Compare commits
11 Commits
github-iss
...
v0.70.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57945fc328 | ||
|
|
ed828b7af4 | ||
|
|
11ac2af2f5 | ||
|
|
df197d5001 | ||
|
|
ad93dcf980 | ||
|
|
7eba5dafd8 | ||
|
|
28fe26637b | ||
|
|
407e9d304b | ||
|
|
e5474e199f | ||
|
|
db44848e2d | ||
|
|
9417ce3b3a |
154
.github/workflows/release.yml
vendored
154
.github/workflows/release.yml
vendored
@@ -115,6 +115,12 @@ jobs:
|
|||||||
|
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest-m
|
runs-on: ubuntu-latest-m
|
||||||
|
outputs:
|
||||||
|
release_artifact_url: ${{ steps.upload_release.outputs.artifact-url }}
|
||||||
|
linux_packages_artifact_url: ${{ steps.upload_linux_packages.outputs.artifact-url }}
|
||||||
|
windows_packages_artifact_url: ${{ steps.upload_windows_packages.outputs.artifact-url }}
|
||||||
|
macos_packages_artifact_url: ${{ steps.upload_macos_packages.outputs.artifact-url }}
|
||||||
|
ghcr_images: ${{ steps.tag_and_push_images.outputs.images_markdown }}
|
||||||
env:
|
env:
|
||||||
flags: ""
|
flags: ""
|
||||||
steps:
|
steps:
|
||||||
@@ -213,10 +219,13 @@ jobs:
|
|||||||
if: always()
|
if: always()
|
||||||
run: rm -f /tmp/gpg-rpm-signing-key.asc
|
run: rm -f /tmp/gpg-rpm-signing-key.asc
|
||||||
- name: Tag and push images (amd64 only)
|
- name: Tag and push images (amd64 only)
|
||||||
|
id: tag_and_push_images
|
||||||
if: |
|
if: |
|
||||||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) ||
|
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) ||
|
||||||
(github.event_name == 'push' && github.ref == 'refs/heads/main')
|
(github.event_name == 'push' && github.ref == 'refs/heads/main')
|
||||||
run: |
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
resolve_tags() {
|
resolve_tags() {
|
||||||
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||||
echo "pr-${{ github.event.pull_request.number }}"
|
echo "pr-${{ github.event.pull_request.number }}"
|
||||||
@@ -225,6 +234,17 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ghcr_package_url() {
|
||||||
|
local image="$1" package encoded_package
|
||||||
|
package="${image#ghcr.io/}"
|
||||||
|
package="${package#*/}"
|
||||||
|
package="${package%%:*}"
|
||||||
|
encoded_package="${package//\//%2F}"
|
||||||
|
echo "https://github.com/orgs/netbirdio/packages/container/package/${encoded_package}"
|
||||||
|
}
|
||||||
|
|
||||||
|
image_refs=()
|
||||||
|
|
||||||
tag_and_push() {
|
tag_and_push() {
|
||||||
local src="$1" img_name tag dst
|
local src="$1" img_name tag dst
|
||||||
img_name="${src%%:*}"
|
img_name="${src%%:*}"
|
||||||
@@ -233,35 +253,56 @@ jobs:
|
|||||||
echo "Tagging ${src} -> ${dst}"
|
echo "Tagging ${src} -> ${dst}"
|
||||||
docker tag "$src" "$dst"
|
docker tag "$src" "$dst"
|
||||||
docker push "$dst"
|
docker push "$dst"
|
||||||
|
image_refs+=("$dst")
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
export -f tag_and_push resolve_tags
|
cat > /tmp/goreleaser-artifacts.json <<'JSON'
|
||||||
|
${{ steps.goreleaser.outputs.artifacts }}
|
||||||
|
JSON
|
||||||
|
|
||||||
echo '${{ steps.goreleaser.outputs.artifacts }}' | \
|
mapfile -t src_images < <(
|
||||||
jq -r '.[] | select(.type == "Docker Image") | select(.goarch == "amd64") | .name' | \
|
jq -r '.[] | select(.type == "Docker Image") | select(.goarch == "amd64") | .name | select(startswith("ghcr.io/"))' /tmp/goreleaser-artifacts.json
|
||||||
grep '^ghcr.io/' | while read -r SRC; do
|
)
|
||||||
tag_and_push "$SRC"
|
|
||||||
|
for src in "${src_images[@]}"; do
|
||||||
|
tag_and_push "$src"
|
||||||
done
|
done
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "images_markdown<<EOF"
|
||||||
|
if [[ ${#image_refs[@]} -eq 0 ]]; then
|
||||||
|
echo "_No GHCR images were pushed._"
|
||||||
|
else
|
||||||
|
printf '%s\n' "${image_refs[@]}" | sort -u | while read -r image; do
|
||||||
|
printf -- '- [`%s`](%s)\n' "$image" "$(ghcr_package_url "$image")"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
echo "EOF"
|
||||||
|
} >> "$GITHUB_OUTPUT"
|
||||||
- name: upload non tags for debug purposes
|
- name: upload non tags for debug purposes
|
||||||
|
id: upload_release
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: release
|
name: release
|
||||||
path: dist/
|
path: dist/
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
- name: upload linux packages
|
- name: upload linux packages
|
||||||
|
id: upload_linux_packages
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: linux-packages
|
name: linux-packages
|
||||||
path: dist/netbird_linux**
|
path: dist/netbird_linux**
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
- name: upload windows packages
|
- name: upload windows packages
|
||||||
|
id: upload_windows_packages
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: windows-packages
|
name: windows-packages
|
||||||
path: dist/netbird_windows**
|
path: dist/netbird_windows**
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
- name: upload macos packages
|
- name: upload macos packages
|
||||||
|
id: upload_macos_packages
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: macos-packages
|
name: macos-packages
|
||||||
@@ -270,6 +311,8 @@ jobs:
|
|||||||
|
|
||||||
release_ui:
|
release_ui:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
release_ui_artifact_url: ${{ steps.upload_release_ui.outputs.artifact-url }}
|
||||||
steps:
|
steps:
|
||||||
- name: Parse semver string
|
- name: Parse semver string
|
||||||
id: semver_parser
|
id: semver_parser
|
||||||
@@ -360,6 +403,7 @@ jobs:
|
|||||||
if: always()
|
if: always()
|
||||||
run: rm -f /tmp/gpg-rpm-signing-key.asc
|
run: rm -f /tmp/gpg-rpm-signing-key.asc
|
||||||
- name: upload non tags for debug purposes
|
- name: upload non tags for debug purposes
|
||||||
|
id: upload_release_ui
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: release-ui
|
name: release-ui
|
||||||
@@ -368,6 +412,8 @@ jobs:
|
|||||||
|
|
||||||
release_ui_darwin:
|
release_ui_darwin:
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
|
outputs:
|
||||||
|
release_ui_darwin_artifact_url: ${{ steps.upload_release_ui_darwin.outputs.artifact-url }}
|
||||||
steps:
|
steps:
|
||||||
- if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
- if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
||||||
run: echo "flags=--snapshot" >> $GITHUB_ENV
|
run: echo "flags=--snapshot" >> $GITHUB_ENV
|
||||||
@@ -402,12 +448,110 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: upload non tags for debug purposes
|
- name: upload non tags for debug purposes
|
||||||
|
id: upload_release_ui_darwin
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: release-ui-darwin
|
name: release-ui-darwin
|
||||||
path: dist/
|
path: dist/
|
||||||
retention-days: 3
|
retention-days: 3
|
||||||
|
|
||||||
|
comment_release_artifacts:
|
||||||
|
name: Comment release artifacts
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [release, release_ui, release_ui_darwin]
|
||||||
|
if: ${{ always() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository }}
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- name: Create or update PR comment
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
env:
|
||||||
|
RELEASE_RESULT: ${{ needs.release.result }}
|
||||||
|
RELEASE_UI_RESULT: ${{ needs.release_ui.result }}
|
||||||
|
RELEASE_UI_DARWIN_RESULT: ${{ needs.release_ui_darwin.result }}
|
||||||
|
RELEASE_ARTIFACT_URL: ${{ needs.release.outputs.release_artifact_url }}
|
||||||
|
LINUX_PACKAGES_ARTIFACT_URL: ${{ needs.release.outputs.linux_packages_artifact_url }}
|
||||||
|
WINDOWS_PACKAGES_ARTIFACT_URL: ${{ needs.release.outputs.windows_packages_artifact_url }}
|
||||||
|
MACOS_PACKAGES_ARTIFACT_URL: ${{ needs.release.outputs.macos_packages_artifact_url }}
|
||||||
|
RELEASE_UI_ARTIFACT_URL: ${{ needs.release_ui.outputs.release_ui_artifact_url }}
|
||||||
|
RELEASE_UI_DARWIN_ARTIFACT_URL: ${{ needs.release_ui_darwin.outputs.release_ui_darwin_artifact_url }}
|
||||||
|
GHCR_IMAGES_MARKDOWN: ${{ needs.release.outputs.ghcr_images }}
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
script: |
|
||||||
|
const marker = '<!-- netbird-release-artifacts -->';
|
||||||
|
const { owner, repo } = context.repo;
|
||||||
|
const issue_number = context.payload.pull_request.number;
|
||||||
|
const runUrl = `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`;
|
||||||
|
const shortSha = context.payload.pull_request.head.sha.slice(0, 7);
|
||||||
|
|
||||||
|
const artifactCell = (url, result) => {
|
||||||
|
if (url) return `[Download](${url})`;
|
||||||
|
return result && result !== 'success' ? `_Not available (${result})_` : '_Not available_';
|
||||||
|
};
|
||||||
|
|
||||||
|
const artifacts = [
|
||||||
|
['All release artifacts', process.env.RELEASE_ARTIFACT_URL, process.env.RELEASE_RESULT],
|
||||||
|
['Linux packages', process.env.LINUX_PACKAGES_ARTIFACT_URL, process.env.RELEASE_RESULT],
|
||||||
|
['Windows packages', process.env.WINDOWS_PACKAGES_ARTIFACT_URL, process.env.RELEASE_RESULT],
|
||||||
|
['macOS packages', process.env.MACOS_PACKAGES_ARTIFACT_URL, process.env.RELEASE_RESULT],
|
||||||
|
['UI artifacts', process.env.RELEASE_UI_ARTIFACT_URL, process.env.RELEASE_UI_RESULT],
|
||||||
|
['UI macOS artifacts', process.env.RELEASE_UI_DARWIN_ARTIFACT_URL, process.env.RELEASE_UI_DARWIN_RESULT],
|
||||||
|
];
|
||||||
|
|
||||||
|
const artifactRows = artifacts
|
||||||
|
.map(([name, url, result]) => `| ${name} | ${artifactCell(url, result)} |`)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
const ghcrImages = (process.env.GHCR_IMAGES_MARKDOWN || '').trim() || '_No GHCR images were pushed._';
|
||||||
|
|
||||||
|
const body = [
|
||||||
|
marker,
|
||||||
|
'## Release artifacts',
|
||||||
|
'',
|
||||||
|
`Built for PR head \`${shortSha}\` in [workflow run #${process.env.GITHUB_RUN_NUMBER}](${runUrl}).`,
|
||||||
|
'',
|
||||||
|
'| Artifact | Link |',
|
||||||
|
'| --- | --- |',
|
||||||
|
artifactRows,
|
||||||
|
'',
|
||||||
|
'### GHCR images (amd64)',
|
||||||
|
ghcrImages,
|
||||||
|
'',
|
||||||
|
'_This comment is updated by the Release workflow. Artifact links expire according to the workflow retention policy._',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue_number,
|
||||||
|
per_page: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
const previous = comments.find(comment =>
|
||||||
|
comment.user?.type === 'Bot' && comment.body?.includes(marker)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (previous) {
|
||||||
|
await github.rest.issues.updateComment({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
comment_id: previous.id,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
core.info(`Updated release artifacts comment ${previous.id}`);
|
||||||
|
} else {
|
||||||
|
const { data } = await github.rest.issues.createComment({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue_number,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
core.info(`Created release artifacts comment ${data.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
trigger_signer:
|
trigger_signer:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [release, release_ui, release_ui_darwin]
|
needs: [release, release_ui, release_ui_darwin]
|
||||||
|
|||||||
28
.github/workflows/sync-tag.yml
vendored
28
.github/workflows/sync-tag.yml
vendored
@@ -9,6 +9,8 @@ concurrency:
|
|||||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
# Receiving workflows (cloud sync-tag, mobile bump-netbird) expect the short
|
||||||
|
# tag form (e.g. v0.30.0), not refs/tags/v0.30.0 — github.ref_name, not github.ref.
|
||||||
jobs:
|
jobs:
|
||||||
trigger_sync_tag:
|
trigger_sync_tag:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -21,3 +23,29 @@ jobs:
|
|||||||
repo: ${{ secrets.UPSTREAM_REPO }}
|
repo: ${{ secrets.UPSTREAM_REPO }}
|
||||||
token: ${{ secrets.NC_GITHUB_TOKEN }}
|
token: ${{ secrets.NC_GITHUB_TOKEN }}
|
||||||
inputs: '{ "tag": "${{ github.ref_name }}" }'
|
inputs: '{ "tag": "${{ github.ref_name }}" }'
|
||||||
|
|
||||||
|
trigger_android_bump:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event.created && !github.event.deleted && startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-')
|
||||||
|
steps:
|
||||||
|
- name: Trigger android-client submodule bump
|
||||||
|
uses: benc-uk/workflow-dispatch@7a027648b88c2413826b6ddd6c76114894dc5ec4 # v1.3.1
|
||||||
|
with:
|
||||||
|
workflow: bump-netbird.yml
|
||||||
|
ref: main
|
||||||
|
repo: netbirdio/android-client
|
||||||
|
token: ${{ secrets.NC_GITHUB_TOKEN }}
|
||||||
|
inputs: '{ "tag": "${{ github.ref_name }}" }'
|
||||||
|
|
||||||
|
trigger_ios_bump:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event.created && !github.event.deleted && startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-')
|
||||||
|
steps:
|
||||||
|
- name: Trigger ios-client submodule bump
|
||||||
|
uses: benc-uk/workflow-dispatch@7a027648b88c2413826b6ddd6c76114894dc5ec4 # v1.3.1
|
||||||
|
with:
|
||||||
|
workflow: bump-netbird.yml
|
||||||
|
ref: main
|
||||||
|
repo: netbirdio/ios-client
|
||||||
|
token: ${{ secrets.NC_GITHUB_TOKEN }}
|
||||||
|
inputs: '{ "tag": "${{ github.ref_name }}" }'
|
||||||
@@ -135,7 +135,7 @@ func startManagement(t *testing.T, config *config.Config, testFile string) (*grp
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
mgmtServer, err := nbgrpc.NewServer(config, accountManager, settingsMockManager, jobManager, secretsManager, nil, nil, &mgmt.MockIntegratedValidator{}, networkMapController, nil)
|
mgmtServer, err := nbgrpc.NewServer(config, accountManager, settingsMockManager, jobManager, secretsManager, nil, nil, &mgmt.MockIntegratedValidator{}, networkMapController, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -201,7 +201,18 @@ Pop $0
|
|||||||
|
|
||||||
Function .onInit
|
Function .onInit
|
||||||
StrCpy $INSTDIR "${INSTALL_DIR}"
|
StrCpy $INSTDIR "${INSTALL_DIR}"
|
||||||
|
; Default autostart to enabled so silent installs (/S) match the interactive default
|
||||||
|
StrCpy $AutostartEnabled "1"
|
||||||
|
|
||||||
|
; Pre-0.70.1 installers ran without SetRegView, so their uninstall keys live
|
||||||
|
; in the 32-bit view. Fall back to it so upgrades still find them.
|
||||||
|
SetRegView 64
|
||||||
ReadRegStr $R0 HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\$(^NAME)" "UninstallString"
|
ReadRegStr $R0 HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\$(^NAME)" "UninstallString"
|
||||||
|
${If} $R0 == ""
|
||||||
|
SetRegView 32
|
||||||
|
ReadRegStr $R0 HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\$(^NAME)" "UninstallString"
|
||||||
|
SetRegView 64
|
||||||
|
${EndIf}
|
||||||
${If} $R0 != ""
|
${If} $R0 != ""
|
||||||
# if silent install jump to uninstall step
|
# if silent install jump to uninstall step
|
||||||
IfSilent uninstall
|
IfSilent uninstall
|
||||||
@@ -214,6 +225,10 @@ ${If} $R0 != ""
|
|||||||
|
|
||||||
${EndIf}
|
${EndIf}
|
||||||
FunctionEnd
|
FunctionEnd
|
||||||
|
|
||||||
|
Function un.onInit
|
||||||
|
SetRegView 64
|
||||||
|
FunctionEnd
|
||||||
######################################################################
|
######################################################################
|
||||||
Section -MainProgram
|
Section -MainProgram
|
||||||
${INSTALL_TYPE}
|
${INSTALL_TYPE}
|
||||||
@@ -228,6 +243,7 @@ Section -MainProgram
|
|||||||
!else
|
!else
|
||||||
File /r "..\\dist\\netbird_windows_amd64\\"
|
File /r "..\\dist\\netbird_windows_amd64\\"
|
||||||
!endif
|
!endif
|
||||||
|
File "..\\client\\ui\\assets\\netbird.png"
|
||||||
SectionEnd
|
SectionEnd
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
@@ -247,9 +263,11 @@ WriteRegStr ${REG_ROOT} "${UI_REG_APP_PATH}" "" "$INSTDIR\${UI_APP_EXE}"
|
|||||||
; Create autostart registry entry based on checkbox
|
; Create autostart registry entry based on checkbox
|
||||||
DetailPrint "Autostart enabled: $AutostartEnabled"
|
DetailPrint "Autostart enabled: $AutostartEnabled"
|
||||||
${If} $AutostartEnabled == "1"
|
${If} $AutostartEnabled == "1"
|
||||||
WriteRegStr HKCU "${AUTOSTART_REG_KEY}" "${APP_NAME}" "$INSTDIR\${UI_APP_EXE}.exe"
|
WriteRegStr HKLM "${AUTOSTART_REG_KEY}" "${APP_NAME}" '"$INSTDIR\${UI_APP_EXE}.exe"'
|
||||||
DetailPrint "Added autostart registry entry: $INSTDIR\${UI_APP_EXE}.exe"
|
DetailPrint "Added autostart registry entry: $INSTDIR\${UI_APP_EXE}.exe"
|
||||||
${Else}
|
${Else}
|
||||||
|
DeleteRegValue HKLM "${AUTOSTART_REG_KEY}" "${APP_NAME}"
|
||||||
|
; Legacy: pre-HKLM installs wrote to HKCU; clean that up too.
|
||||||
DeleteRegValue HKCU "${AUTOSTART_REG_KEY}" "${APP_NAME}"
|
DeleteRegValue HKCU "${AUTOSTART_REG_KEY}" "${APP_NAME}"
|
||||||
DetailPrint "Autostart not enabled by user"
|
DetailPrint "Autostart not enabled by user"
|
||||||
${EndIf}
|
${EndIf}
|
||||||
@@ -283,6 +301,8 @@ ExecWait `taskkill /im ${UI_APP_EXE}.exe /f`
|
|||||||
|
|
||||||
; Remove autostart registry entry
|
; Remove autostart registry entry
|
||||||
DetailPrint "Removing autostart registry entry if exists..."
|
DetailPrint "Removing autostart registry entry if exists..."
|
||||||
|
DeleteRegValue HKLM "${AUTOSTART_REG_KEY}" "${APP_NAME}"
|
||||||
|
; Legacy: pre-HKLM installs wrote to HKCU; clean that up too.
|
||||||
DeleteRegValue HKCU "${AUTOSTART_REG_KEY}" "${APP_NAME}"
|
DeleteRegValue HKCU "${AUTOSTART_REG_KEY}" "${APP_NAME}"
|
||||||
|
|
||||||
; Handle data deletion based on checkbox
|
; Handle data deletion based on checkbox
|
||||||
@@ -321,6 +341,7 @@ DetailPrint "Removing registry keys..."
|
|||||||
DeleteRegKey ${REG_ROOT} "${REG_APP_PATH}"
|
DeleteRegKey ${REG_ROOT} "${REG_APP_PATH}"
|
||||||
DeleteRegKey ${REG_ROOT} "${UNINSTALL_PATH}"
|
DeleteRegKey ${REG_ROOT} "${UNINSTALL_PATH}"
|
||||||
DeleteRegKey ${REG_ROOT} "${UI_REG_APP_PATH}"
|
DeleteRegKey ${REG_ROOT} "${UI_REG_APP_PATH}"
|
||||||
|
DeleteRegKey HKCU "Software\Classes\AppUserModelId\${APP_NAME}"
|
||||||
|
|
||||||
DetailPrint "Removing application directory from PATH..."
|
DetailPrint "Removing application directory from PATH..."
|
||||||
EnVar::SetHKLM
|
EnVar::SetHKLM
|
||||||
|
|||||||
@@ -1671,7 +1671,7 @@ func startManagement(t *testing.T, dataDir, testFile string) (*grpc.Server, stri
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
mgmtServer, err := nbgrpc.NewServer(config, accountManager, settingsMockManager, jobManager, secretsManager, nil, nil, &server.MockIntegratedValidator{}, networkMapController, nil)
|
mgmtServer, err := nbgrpc.NewServer(config, accountManager, settingsMockManager, jobManager, secretsManager, nil, nil, &server.MockIntegratedValidator{}, networkMapController, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package activity
|
|||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"runtime"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -18,10 +17,6 @@ import (
|
|||||||
peerid "github.com/netbirdio/netbird/client/internal/peer/id"
|
peerid "github.com/netbirdio/netbird/client/internal/peer/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
func isBindListenerPlatform() bool {
|
|
||||||
return runtime.GOOS == "windows" || runtime.GOOS == "js"
|
|
||||||
}
|
|
||||||
|
|
||||||
// mockEndpointManager implements device.EndpointManager for testing
|
// mockEndpointManager implements device.EndpointManager for testing
|
||||||
type mockEndpointManager struct {
|
type mockEndpointManager struct {
|
||||||
endpoints map[netip.Addr]net.Conn
|
endpoints map[netip.Addr]net.Conn
|
||||||
@@ -181,10 +176,6 @@ func TestBindListener_Close(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestManager_BindMode(t *testing.T) {
|
func TestManager_BindMode(t *testing.T) {
|
||||||
if !isBindListenerPlatform() {
|
|
||||||
t.Skip("BindListener only used on Windows/JS platforms")
|
|
||||||
}
|
|
||||||
|
|
||||||
mockEndpointMgr := newMockEndpointManager()
|
mockEndpointMgr := newMockEndpointManager()
|
||||||
mockIface := &MockWGIfaceBind{endpointMgr: mockEndpointMgr}
|
mockIface := &MockWGIfaceBind{endpointMgr: mockEndpointMgr}
|
||||||
|
|
||||||
@@ -226,10 +217,6 @@ func TestManager_BindMode(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestManager_BindMode_MultiplePeers(t *testing.T) {
|
func TestManager_BindMode_MultiplePeers(t *testing.T) {
|
||||||
if !isBindListenerPlatform() {
|
|
||||||
t.Skip("BindListener only used on Windows/JS platforms")
|
|
||||||
}
|
|
||||||
|
|
||||||
mockEndpointMgr := newMockEndpointManager()
|
mockEndpointMgr := newMockEndpointManager()
|
||||||
mockIface := &MockWGIfaceBind{endpointMgr: mockEndpointMgr}
|
mockIface := &MockWGIfaceBind{endpointMgr: mockEndpointMgr}
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,12 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"runtime"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface/netstack"
|
|
||||||
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
"github.com/netbirdio/netbird/client/internal/lazyconn"
|
"github.com/netbirdio/netbird/client/internal/lazyconn"
|
||||||
peerid "github.com/netbirdio/netbird/client/internal/peer/id"
|
peerid "github.com/netbirdio/netbird/client/internal/peer/id"
|
||||||
@@ -75,16 +73,6 @@ func (m *Manager) createListener(peerCfg lazyconn.PeerConfig) (listener, error)
|
|||||||
return NewUDPListener(m.wgIface, peerCfg)
|
return NewUDPListener(m.wgIface, peerCfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// BindListener is used on Windows, JS, and netstack platforms:
|
|
||||||
// - JS: Cannot listen to UDP sockets
|
|
||||||
// - Windows: IP_UNICAST_IF socket option forces packets out the interface the default
|
|
||||||
// gateway points to, preventing them from reaching the loopback interface.
|
|
||||||
// - Netstack: Allows multiple instances on the same host without port conflicts.
|
|
||||||
// BindListener bypasses these issues by passing data directly through the bind.
|
|
||||||
if runtime.GOOS != "windows" && runtime.GOOS != "js" && !netstack.IsEnabled() {
|
|
||||||
return NewUDPListener(m.wgIface, peerCfg)
|
|
||||||
}
|
|
||||||
|
|
||||||
provider, ok := m.wgIface.(bindProvider)
|
provider, ok := m.wgIface.(bindProvider)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.New("interface claims userspace bind but doesn't implement bindProvider")
|
return nil, errors.New("interface claims userspace bind but doesn't implement bindProvider")
|
||||||
|
|||||||
@@ -89,9 +89,17 @@ func (r *SysOps) installScopedDefaultFor(unspec netip.Addr) (bool, error) {
|
|||||||
return false, fmt.Errorf("unusable default nexthop for %s (no interface)", unspec)
|
return false, fmt.Errorf("unusable default nexthop for %s (no interface)", unspec)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reused := false
|
||||||
if err := r.addScopedDefault(unspec, nexthop); err != nil {
|
if err := r.addScopedDefault(unspec, nexthop); err != nil {
|
||||||
|
if !errors.Is(err, unix.EEXIST) {
|
||||||
return false, fmt.Errorf("add scoped default on %s: %w", nexthop.Intf.Name, err)
|
return false, fmt.Errorf("add scoped default on %s: %w", nexthop.Intf.Name, err)
|
||||||
}
|
}
|
||||||
|
// macOS installs its own RTF_IFSCOPE defaults for primary service
|
||||||
|
// selection on multi-NIC setups, so a route on this ifindex can
|
||||||
|
// already exist before we try. Binding to it via IP[V6]_BOUND_IF
|
||||||
|
// still produces the scoped lookup we need.
|
||||||
|
reused = true
|
||||||
|
}
|
||||||
|
|
||||||
af := unix.AF_INET
|
af := unix.AF_INET
|
||||||
if unspec.Is6() {
|
if unspec.Is6() {
|
||||||
@@ -102,7 +110,11 @@ func (r *SysOps) installScopedDefaultFor(unspec netip.Addr) (bool, error) {
|
|||||||
if nexthop.IP.IsValid() {
|
if nexthop.IP.IsValid() {
|
||||||
via = nexthop.IP.String()
|
via = nexthop.IP.String()
|
||||||
}
|
}
|
||||||
log.Infof("installed scoped default route via %s on %s for %s", via, nexthop.Intf.Name, afOf(unspec))
|
verb := "installed"
|
||||||
|
if reused {
|
||||||
|
verb = "reused existing"
|
||||||
|
}
|
||||||
|
log.Infof("%s scoped default route via %s on %s for %s", verb, via, nexthop.Intf.Name, afOf(unspec))
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,217 +2,358 @@
|
|||||||
|
|
||||||
package sleep
|
package sleep
|
||||||
|
|
||||||
/*
|
|
||||||
#cgo LDFLAGS: -framework IOKit -framework CoreFoundation
|
|
||||||
#include <IOKit/pwr_mgt/IOPMLib.h>
|
|
||||||
#include <IOKit/IOMessage.h>
|
|
||||||
#include <CoreFoundation/CoreFoundation.h>
|
|
||||||
|
|
||||||
extern void sleepCallbackBridge();
|
|
||||||
extern void poweredOnCallbackBridge();
|
|
||||||
extern void suspendedCallbackBridge();
|
|
||||||
extern void resumedCallbackBridge();
|
|
||||||
|
|
||||||
|
|
||||||
// C global variables for IOKit state
|
|
||||||
static IONotificationPortRef g_notifyPortRef = NULL;
|
|
||||||
static io_object_t g_notifierObject = 0;
|
|
||||||
static io_object_t g_generalInterestNotifier = 0;
|
|
||||||
static io_connect_t g_rootPort = 0;
|
|
||||||
static CFRunLoopRef g_runLoop = NULL;
|
|
||||||
|
|
||||||
static void sleepCallback(void* refCon, io_service_t service, natural_t messageType, void* messageArgument) {
|
|
||||||
switch (messageType) {
|
|
||||||
case kIOMessageSystemWillSleep:
|
|
||||||
sleepCallbackBridge();
|
|
||||||
IOAllowPowerChange(g_rootPort, (long)messageArgument);
|
|
||||||
break;
|
|
||||||
case kIOMessageSystemHasPoweredOn:
|
|
||||||
poweredOnCallbackBridge();
|
|
||||||
break;
|
|
||||||
case kIOMessageServiceIsSuspended:
|
|
||||||
suspendedCallbackBridge();
|
|
||||||
break;
|
|
||||||
case kIOMessageServiceIsResumed:
|
|
||||||
resumedCallbackBridge();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void registerNotifications() {
|
|
||||||
g_rootPort = IORegisterForSystemPower(
|
|
||||||
NULL,
|
|
||||||
&g_notifyPortRef,
|
|
||||||
(IOServiceInterestCallback)sleepCallback,
|
|
||||||
&g_notifierObject
|
|
||||||
);
|
|
||||||
|
|
||||||
if (g_rootPort == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
CFRunLoopAddSource(CFRunLoopGetCurrent(),
|
|
||||||
IONotificationPortGetRunLoopSource(g_notifyPortRef),
|
|
||||||
kCFRunLoopCommonModes);
|
|
||||||
|
|
||||||
g_runLoop = CFRunLoopGetCurrent();
|
|
||||||
CFRunLoopRun();
|
|
||||||
}
|
|
||||||
|
|
||||||
static void unregisterNotifications() {
|
|
||||||
CFRunLoopRemoveSource(g_runLoop,
|
|
||||||
IONotificationPortGetRunLoopSource(g_notifyPortRef),
|
|
||||||
kCFRunLoopCommonModes);
|
|
||||||
|
|
||||||
IODeregisterForSystemPower(&g_notifierObject);
|
|
||||||
IOServiceClose(g_rootPort);
|
|
||||||
IONotificationPortDestroy(g_notifyPortRef);
|
|
||||||
CFRunLoopStop(g_runLoop);
|
|
||||||
|
|
||||||
g_notifyPortRef = NULL;
|
|
||||||
g_notifierObject = 0;
|
|
||||||
g_rootPort = 0;
|
|
||||||
g_runLoop = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
*/
|
|
||||||
import "C"
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/ebitengine/purego"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
// IOKit message types from IOKit/IOMessage.h.
|
||||||
serviceRegistry = make(map[*Detector]struct{})
|
const (
|
||||||
serviceRegistryMu sync.Mutex
|
kIOMessageCanSystemSleep uintptr = 0xe0000270
|
||||||
|
kIOMessageSystemWillSleep uintptr = 0xe0000280
|
||||||
|
kIOMessageSystemHasPoweredOn uintptr = 0xe0000300
|
||||||
)
|
)
|
||||||
|
|
||||||
//export sleepCallbackBridge
|
var (
|
||||||
func sleepCallbackBridge() {
|
ioKit iokitFuncs
|
||||||
log.Info("sleepCallbackBridge event triggered")
|
cf cfFuncs
|
||||||
|
cfCommonModes uintptr
|
||||||
|
|
||||||
serviceRegistryMu.Lock()
|
libInitOnce sync.Once
|
||||||
defer serviceRegistryMu.Unlock()
|
libInitErr error
|
||||||
|
|
||||||
for svc := range serviceRegistry {
|
// callbackThunk is the single C-callable trampoline registered with IOKit.
|
||||||
svc.triggerCallback(EventTypeSleep)
|
callbackThunk uintptr
|
||||||
}
|
|
||||||
|
serviceRegistry = make(map[*Detector]struct{})
|
||||||
|
serviceRegistryMu sync.Mutex
|
||||||
|
session *runLoopSession
|
||||||
|
|
||||||
|
// lifecycleMu serializes Register/Deregister so a new registration can't
|
||||||
|
// start a second runloop while a previous teardown is still pending.
|
||||||
|
lifecycleMu sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
|
// iokitFuncs holds IOKit symbols resolved once at init.
|
||||||
|
type iokitFuncs struct {
|
||||||
|
IORegisterForSystemPower func(refcon uintptr, portRef *uintptr, callback uintptr, notifier *uintptr) uintptr
|
||||||
|
IODeregisterForSystemPower func(notifier *uintptr) int32
|
||||||
|
IOAllowPowerChange func(kernelPort uintptr, notificationID uintptr) int32
|
||||||
|
IOServiceClose func(connect uintptr) int32
|
||||||
|
IONotificationPortGetRunLoopSource func(port uintptr) uintptr
|
||||||
|
IONotificationPortDestroy func(port uintptr)
|
||||||
}
|
}
|
||||||
|
|
||||||
//export resumedCallbackBridge
|
// cfFuncs holds CoreFoundation symbols resolved once at init.
|
||||||
func resumedCallbackBridge() {
|
type cfFuncs struct {
|
||||||
log.Info("resumedCallbackBridge event triggered")
|
CFRunLoopGetCurrent func() uintptr
|
||||||
|
CFRunLoopRun func()
|
||||||
|
CFRunLoopStop func(rl uintptr)
|
||||||
|
CFRunLoopAddSource func(rl, source, mode uintptr)
|
||||||
|
CFRunLoopRemoveSource func(rl, source, mode uintptr)
|
||||||
}
|
}
|
||||||
|
|
||||||
//export suspendedCallbackBridge
|
// runLoopSession bundles the handles owned by one CFRunLoop lifetime. A nil
|
||||||
func suspendedCallbackBridge() {
|
// session means no runloop is active and the next Register must start one.
|
||||||
log.Info("suspendedCallbackBridge event triggered")
|
type runLoopSession struct {
|
||||||
|
rl uintptr
|
||||||
|
port uintptr
|
||||||
|
notifier uintptr
|
||||||
|
rp uintptr
|
||||||
}
|
}
|
||||||
|
|
||||||
//export poweredOnCallbackBridge
|
// detectorSnapshot pins a detector's callback and done channel so dispatch
|
||||||
func poweredOnCallbackBridge() {
|
// runs with values valid at snapshot time, even if a concurrent
|
||||||
log.Info("poweredOnCallbackBridge event triggered")
|
// Deregister/Register rewrites the detector's fields.
|
||||||
serviceRegistryMu.Lock()
|
type detectorSnapshot struct {
|
||||||
defer serviceRegistryMu.Unlock()
|
detector *Detector
|
||||||
|
callback func(event EventType)
|
||||||
for svc := range serviceRegistry {
|
done <-chan struct{}
|
||||||
svc.triggerCallback(EventTypeWakeUp)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Detector delivers sleep and wake events to a registered callback.
|
||||||
type Detector struct {
|
type Detector struct {
|
||||||
callback func(event EventType)
|
callback func(event EventType)
|
||||||
ctx context.Context
|
done chan struct{}
|
||||||
cancel context.CancelFunc
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDetector() (*Detector, error) {
|
|
||||||
return &Detector{}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Register installs callback for power events. The first registration starts
|
||||||
|
// the CFRunLoop on a dedicated OS-locked thread and blocks until IOKit
|
||||||
|
// registration succeeds or fails; subsequent registrations just add to the
|
||||||
|
// dispatch set.
|
||||||
func (d *Detector) Register(callback func(event EventType)) error {
|
func (d *Detector) Register(callback func(event EventType)) error {
|
||||||
serviceRegistryMu.Lock()
|
lifecycleMu.Lock()
|
||||||
defer serviceRegistryMu.Unlock()
|
defer lifecycleMu.Unlock()
|
||||||
|
|
||||||
|
serviceRegistryMu.Lock()
|
||||||
if _, exists := serviceRegistry[d]; exists {
|
if _, exists := serviceRegistry[d]; exists {
|
||||||
|
serviceRegistryMu.Unlock()
|
||||||
return fmt.Errorf("detector service already registered")
|
return fmt.Errorf("detector service already registered")
|
||||||
}
|
}
|
||||||
|
|
||||||
d.callback = callback
|
d.callback = callback
|
||||||
|
d.done = make(chan struct{})
|
||||||
d.ctx, d.cancel = context.WithCancel(context.Background())
|
|
||||||
|
|
||||||
if len(serviceRegistry) > 0 {
|
|
||||||
serviceRegistry[d] = struct{}{}
|
serviceRegistry[d] = struct{}{}
|
||||||
|
needSetup := session == nil
|
||||||
|
serviceRegistryMu.Unlock()
|
||||||
|
|
||||||
|
if !needSetup {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
serviceRegistry[d] = struct{}{}
|
errCh := make(chan error, 1)
|
||||||
|
go runRunLoop(errCh)
|
||||||
// CFRunLoop must run on a single fixed OS thread
|
if err := <-errCh; err != nil {
|
||||||
go func() {
|
serviceRegistryMu.Lock()
|
||||||
runtime.LockOSThread()
|
delete(serviceRegistry, d)
|
||||||
defer runtime.UnlockOSThread()
|
close(d.done)
|
||||||
|
d.done = nil
|
||||||
C.registerNotifications()
|
serviceRegistryMu.Unlock()
|
||||||
}()
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
log.Info("sleep detection service started on macOS")
|
log.Info("sleep detection service started on macOS")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deregister removes the detector. When the last detector is removed, IOKit registration is torn down
|
// Deregister removes the detector. When the last detector leaves, IOKit
|
||||||
// and the runloop is stopped and cleaned up.
|
// notifications are torn down and the runloop is stopped.
|
||||||
func (d *Detector) Deregister() error {
|
func (d *Detector) Deregister() error {
|
||||||
|
lifecycleMu.Lock()
|
||||||
|
defer lifecycleMu.Unlock()
|
||||||
|
|
||||||
serviceRegistryMu.Lock()
|
serviceRegistryMu.Lock()
|
||||||
defer serviceRegistryMu.Unlock()
|
if _, exists := serviceRegistry[d]; !exists {
|
||||||
_, exists := serviceRegistry[d]
|
serviceRegistryMu.Unlock()
|
||||||
if !exists {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
close(d.done)
|
||||||
// cancel and remove this detector
|
|
||||||
d.cancel()
|
|
||||||
delete(serviceRegistry, d)
|
delete(serviceRegistry, d)
|
||||||
|
|
||||||
// If other Detectors still exist, leave IOKit running
|
|
||||||
if len(serviceRegistry) > 0 {
|
if len(serviceRegistry) > 0 {
|
||||||
|
serviceRegistryMu.Unlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
sess := session
|
||||||
|
serviceRegistryMu.Unlock()
|
||||||
|
|
||||||
log.Info("sleep detection service stopping (deregister)")
|
log.Info("sleep detection service stopping (deregister)")
|
||||||
|
|
||||||
// Deregister IOKit notifications, stop runloop, and free resources
|
if sess == nil {
|
||||||
C.unregisterNotifications()
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if sess.rl != 0 && sess.port != 0 {
|
||||||
|
source := ioKit.IONotificationPortGetRunLoopSource(sess.port)
|
||||||
|
cf.CFRunLoopRemoveSource(sess.rl, source, cfCommonModes)
|
||||||
|
}
|
||||||
|
if sess.notifier != 0 {
|
||||||
|
n := sess.notifier
|
||||||
|
ioKit.IODeregisterForSystemPower(&n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear session only after IODeregisterForSystemPower returns so any
|
||||||
|
// in-flight powerCallback can still look up session.rp to ack sleep.
|
||||||
|
serviceRegistryMu.Lock()
|
||||||
|
session = nil
|
||||||
|
serviceRegistryMu.Unlock()
|
||||||
|
|
||||||
|
if sess.rp != 0 {
|
||||||
|
ioKit.IOServiceClose(sess.rp)
|
||||||
|
}
|
||||||
|
if sess.port != 0 {
|
||||||
|
ioKit.IONotificationPortDestroy(sess.port)
|
||||||
|
}
|
||||||
|
if sess.rl != 0 {
|
||||||
|
cf.CFRunLoopStop(sess.rl)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Detector) triggerCallback(event EventType) {
|
func (d *Detector) triggerCallback(event EventType, cb func(event EventType), done <-chan struct{}) {
|
||||||
doneChan := make(chan struct{})
|
if cb == nil || done == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
doneChan := make(chan struct{})
|
||||||
timeout := time.NewTimer(500 * time.Millisecond)
|
timeout := time.NewTimer(500 * time.Millisecond)
|
||||||
defer timeout.Stop()
|
defer timeout.Stop()
|
||||||
|
|
||||||
cb := d.callback
|
go func() {
|
||||||
go func(callback func(event EventType)) {
|
defer close(doneChan)
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
log.Errorf("panic in sleep callback: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
log.Info("sleep detection event fired")
|
log.Info("sleep detection event fired")
|
||||||
callback(event)
|
cb(event)
|
||||||
close(doneChan)
|
}()
|
||||||
}(cb)
|
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-doneChan:
|
case <-doneChan:
|
||||||
case <-d.ctx.Done():
|
case <-done:
|
||||||
case <-timeout.C:
|
case <-timeout.C:
|
||||||
log.Warnf("sleep callback timed out")
|
log.Warn("sleep callback timed out")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewDetector initializes IOKit/CoreFoundation bindings and returns a Detector.
|
||||||
|
func NewDetector() (*Detector, error) {
|
||||||
|
if err := initLibs(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &Detector{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func initLibs() error {
|
||||||
|
libInitOnce.Do(func() {
|
||||||
|
iokit, err := purego.Dlopen("/System/Library/Frameworks/IOKit.framework/IOKit", purego.RTLD_NOW|purego.RTLD_GLOBAL)
|
||||||
|
if err != nil {
|
||||||
|
libInitErr = fmt.Errorf("dlopen IOKit: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cfLib, err := purego.Dlopen("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation", purego.RTLD_NOW|purego.RTLD_GLOBAL)
|
||||||
|
if err != nil {
|
||||||
|
libInitErr = fmt.Errorf("dlopen CoreFoundation: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
purego.RegisterLibFunc(&ioKit.IORegisterForSystemPower, iokit, "IORegisterForSystemPower")
|
||||||
|
purego.RegisterLibFunc(&ioKit.IODeregisterForSystemPower, iokit, "IODeregisterForSystemPower")
|
||||||
|
purego.RegisterLibFunc(&ioKit.IOAllowPowerChange, iokit, "IOAllowPowerChange")
|
||||||
|
purego.RegisterLibFunc(&ioKit.IOServiceClose, iokit, "IOServiceClose")
|
||||||
|
purego.RegisterLibFunc(&ioKit.IONotificationPortGetRunLoopSource, iokit, "IONotificationPortGetRunLoopSource")
|
||||||
|
purego.RegisterLibFunc(&ioKit.IONotificationPortDestroy, iokit, "IONotificationPortDestroy")
|
||||||
|
|
||||||
|
purego.RegisterLibFunc(&cf.CFRunLoopGetCurrent, cfLib, "CFRunLoopGetCurrent")
|
||||||
|
purego.RegisterLibFunc(&cf.CFRunLoopRun, cfLib, "CFRunLoopRun")
|
||||||
|
purego.RegisterLibFunc(&cf.CFRunLoopStop, cfLib, "CFRunLoopStop")
|
||||||
|
purego.RegisterLibFunc(&cf.CFRunLoopAddSource, cfLib, "CFRunLoopAddSource")
|
||||||
|
purego.RegisterLibFunc(&cf.CFRunLoopRemoveSource, cfLib, "CFRunLoopRemoveSource")
|
||||||
|
|
||||||
|
modeAddr, err := purego.Dlsym(cfLib, "kCFRunLoopCommonModes")
|
||||||
|
if err != nil {
|
||||||
|
libInitErr = fmt.Errorf("dlsym kCFRunLoopCommonModes: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Launder the uintptr-to-pointer conversion through a Go variable so
|
||||||
|
// go vet's unsafeptr analyzer doesn't flag a system-library global.
|
||||||
|
cfCommonModes = **(**uintptr)(unsafe.Pointer(&modeAddr))
|
||||||
|
|
||||||
|
// NewCallback slots are a finite, non-reclaimable resource, so register
|
||||||
|
// a single thunk that dispatches to the current Detector set.
|
||||||
|
callbackThunk = purego.NewCallback(powerCallback)
|
||||||
|
})
|
||||||
|
return libInitErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// powerCallback is the IOServiceInterestCallback trampoline, invoked on the
|
||||||
|
// runloop thread. A Go panic crossing the purego boundary has undefined
|
||||||
|
// behavior, so contain it here.
|
||||||
|
func powerCallback(refcon, service, messageType, messageArgument uintptr) uintptr {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
log.Errorf("panic in sleep powerCallback: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
switch messageType {
|
||||||
|
case kIOMessageCanSystemSleep:
|
||||||
|
// Not acknowledging forces a 30s IOKit timeout before idle sleep.
|
||||||
|
allowPowerChange(messageArgument)
|
||||||
|
case kIOMessageSystemWillSleep:
|
||||||
|
dispatchEvent(EventTypeSleep)
|
||||||
|
allowPowerChange(messageArgument)
|
||||||
|
case kIOMessageSystemHasPoweredOn:
|
||||||
|
dispatchEvent(EventTypeWakeUp)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func allowPowerChange(messageArgument uintptr) {
|
||||||
|
serviceRegistryMu.Lock()
|
||||||
|
var port uintptr
|
||||||
|
if session != nil {
|
||||||
|
port = session.rp
|
||||||
|
}
|
||||||
|
serviceRegistryMu.Unlock()
|
||||||
|
if port != 0 {
|
||||||
|
ioKit.IOAllowPowerChange(port, messageArgument)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func dispatchEvent(event EventType) {
|
||||||
|
serviceRegistryMu.Lock()
|
||||||
|
snaps := make([]detectorSnapshot, 0, len(serviceRegistry))
|
||||||
|
for d := range serviceRegistry {
|
||||||
|
snaps = append(snaps, detectorSnapshot{
|
||||||
|
detector: d,
|
||||||
|
callback: d.callback,
|
||||||
|
done: d.done,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
serviceRegistryMu.Unlock()
|
||||||
|
|
||||||
|
for _, s := range snaps {
|
||||||
|
s.detector.triggerCallback(event, s.callback, s.done)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// runRunLoop owns the OS-locked thread that CFRunLoop is pinned to. Setup
|
||||||
|
// result is reported on errCh so Register can surface failures synchronously.
|
||||||
|
func runRunLoop(errCh chan<- error) {
|
||||||
|
runtime.LockOSThread()
|
||||||
|
defer runtime.UnlockOSThread()
|
||||||
|
|
||||||
|
sess, err := setupSession()
|
||||||
|
if err == nil {
|
||||||
|
serviceRegistryMu.Lock()
|
||||||
|
session = sess
|
||||||
|
serviceRegistryMu.Unlock()
|
||||||
|
}
|
||||||
|
errCh <- err
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
log.Errorf("panic in sleep runloop: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
cf.CFRunLoopRun()
|
||||||
|
}
|
||||||
|
|
||||||
|
// setupSession performs the IOKit registration on the current thread. Panics
|
||||||
|
// are converted to errors so runRunLoop never leaves errCh unsent.
|
||||||
|
func setupSession() (s *runLoopSession, err error) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
err = fmt.Errorf("panic during runloop setup: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
var portRef, notifier uintptr
|
||||||
|
rp := ioKit.IORegisterForSystemPower(0, &portRef, callbackThunk, ¬ifier)
|
||||||
|
if rp == 0 {
|
||||||
|
return nil, fmt.Errorf("IORegisterForSystemPower returned zero")
|
||||||
|
}
|
||||||
|
|
||||||
|
rl := cf.CFRunLoopGetCurrent()
|
||||||
|
source := ioKit.IONotificationPortGetRunLoopSource(portRef)
|
||||||
|
cf.CFRunLoopAddSource(rl, source, cfCommonModes)
|
||||||
|
|
||||||
|
return &runLoopSession{rl: rl, port: portRef, notifier: notifier, rp: rp}, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,15 +13,25 @@
|
|||||||
|
|
||||||
<MajorUpgrade AllowSameVersionUpgrades='yes' DowngradeErrorMessage="A newer version of [ProductName] is already installed. Setup will now exit."/>
|
<MajorUpgrade AllowSameVersionUpgrades='yes' DowngradeErrorMessage="A newer version of [ProductName] is already installed. Setup will now exit."/>
|
||||||
|
|
||||||
|
<!-- Autostart: enabled by default, disable with AUTOSTART=0 on the msiexec command line -->
|
||||||
|
<Property Id="AUTOSTART" Value="1" />
|
||||||
|
|
||||||
<StandardDirectory Id="ProgramFiles64Folder">
|
<StandardDirectory Id="ProgramFiles64Folder">
|
||||||
<Directory Id="NetbirdInstallDir" Name="Netbird">
|
<Directory Id="NetbirdInstallDir" Name="Netbird">
|
||||||
<Component Id="NetbirdFiles" Guid="db3165de-cc6e-4922-8396-9d892950e23e" Bitness="always64">
|
<Component Id="NetbirdFiles" Guid="db3165de-cc6e-4922-8396-9d892950e23e" Bitness="always64">
|
||||||
<File ProcessorArchitecture="$(var.ProcessorArchitecture)" Source=".\dist\netbird_windows_$(var.ArchSuffix)\netbird.exe" KeyPath="yes" />
|
<File ProcessorArchitecture="$(var.ProcessorArchitecture)" Source=".\dist\netbird_windows_$(var.ArchSuffix)\netbird.exe" KeyPath="yes" />
|
||||||
<File ProcessorArchitecture="$(var.ProcessorArchitecture)" Source=".\dist\netbird_windows_$(var.ArchSuffix)\netbird-ui.exe">
|
<File ProcessorArchitecture="$(var.ProcessorArchitecture)" Source=".\dist\netbird_windows_$(var.ArchSuffix)\netbird-ui.exe">
|
||||||
<Shortcut Id="NetbirdDesktopShortcut" Directory="DesktopFolder" Name="NetBird" WorkingDirectory="NetbirdInstallDir" Icon="NetbirdIcon" />
|
<Shortcut Id="NetbirdDesktopShortcut" Directory="DesktopFolder" Name="NetBird" WorkingDirectory="NetbirdInstallDir" Icon="NetbirdIcon">
|
||||||
<Shortcut Id="NetbirdStartMenuShortcut" Directory="StartMenuFolder" Name="NetBird" WorkingDirectory="NetbirdInstallDir" Icon="NetbirdIcon" />
|
<ShortcutProperty Key="System.AppUserModel.ID" Value="NetBird" />
|
||||||
|
<ShortcutProperty Key="System.AppUserModel.ToastActivatorCLSID" Value="{0E1B4DE7-E148-432B-9814-544F941826EC}" />
|
||||||
|
</Shortcut>
|
||||||
|
<Shortcut Id="NetbirdStartMenuShortcut" Directory="StartMenuFolder" Name="NetBird" WorkingDirectory="NetbirdInstallDir" Icon="NetbirdIcon">
|
||||||
|
<ShortcutProperty Key="System.AppUserModel.ID" Value="NetBird" />
|
||||||
|
<ShortcutProperty Key="System.AppUserModel.ToastActivatorCLSID" Value="{0E1B4DE7-E148-432B-9814-544F941826EC}" />
|
||||||
|
</Shortcut>
|
||||||
</File>
|
</File>
|
||||||
<File ProcessorArchitecture="$(var.ProcessorArchitecture)" Source=".\dist\netbird_windows_$(var.ArchSuffix)\wintun.dll" />
|
<File ProcessorArchitecture="$(var.ProcessorArchitecture)" Source=".\dist\netbird_windows_$(var.ArchSuffix)\wintun.dll" />
|
||||||
|
<File Id="NetbirdToastIcon" Name="netbird.png" Source=".\client\ui\assets\netbird.png" />
|
||||||
<?if $(var.ArchSuffix) = "amd64" ?>
|
<?if $(var.ArchSuffix) = "amd64" ?>
|
||||||
<File ProcessorArchitecture="$(var.ProcessorArchitecture)" Source=".\dist\netbird_windows_$(var.ArchSuffix)\opengl32.dll" />
|
<File ProcessorArchitecture="$(var.ProcessorArchitecture)" Source=".\dist\netbird_windows_$(var.ArchSuffix)\opengl32.dll" />
|
||||||
<?endif ?>
|
<?endif ?>
|
||||||
@@ -46,8 +56,31 @@
|
|||||||
</Directory>
|
</Directory>
|
||||||
</StandardDirectory>
|
</StandardDirectory>
|
||||||
|
|
||||||
|
<!-- Per-user component: HKCU keypath (auto GUID via "*"), separate from
|
||||||
|
the per-machine NetbirdFiles component to satisfy ICE57. -->
|
||||||
|
<StandardDirectory Id="ProgramMenuFolder">
|
||||||
|
<Component Id="NetbirdAumidRegistry" Guid="*">
|
||||||
|
<RegistryKey Root="HKCU" Key="Software\Classes\AppUserModelId\NetBird" ForceDeleteOnUninstall="yes">
|
||||||
|
<RegistryValue Name="InstalledByMSI" Type="integer" Value="1" KeyPath="yes" />
|
||||||
|
</RegistryKey>
|
||||||
|
</Component>
|
||||||
|
</StandardDirectory>
|
||||||
|
|
||||||
|
<StandardDirectory Id="CommonAppDataFolder">
|
||||||
|
<Directory Id="NetbirdAutoStartDir" Name="Netbird">
|
||||||
|
<Component Id="NetbirdAutoStart" Guid="b199eaca-b0dd-4032-af19-679cfad48eb3" Bitness="always64">
|
||||||
|
<Condition>AUTOSTART = "1"</Condition>
|
||||||
|
<RegistryValue Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\Run"
|
||||||
|
Name="Netbird" Value=""[NetbirdInstallDir]netbird-ui.exe""
|
||||||
|
Type="string" KeyPath="yes" />
|
||||||
|
</Component>
|
||||||
|
</Directory>
|
||||||
|
</StandardDirectory>
|
||||||
|
|
||||||
<ComponentGroup Id="NetbirdFilesComponent">
|
<ComponentGroup Id="NetbirdFilesComponent">
|
||||||
<ComponentRef Id="NetbirdFiles" />
|
<ComponentRef Id="NetbirdFiles" />
|
||||||
|
<ComponentRef Id="NetbirdAumidRegistry" />
|
||||||
|
<ComponentRef Id="NetbirdAutoStart" />
|
||||||
</ComponentGroup>
|
</ComponentGroup>
|
||||||
|
|
||||||
<util:CloseApplication Id="CloseNetBird" CloseMessage="no" Target="netbird.exe" RebootPrompt="no" />
|
<util:CloseApplication Id="CloseNetBird" CloseMessage="no" Target="netbird.exe" RebootPrompt="no" />
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -104,8 +104,6 @@ service DaemonService {
|
|||||||
// StopCPUProfile stops CPU profiling in the daemon
|
// StopCPUProfile stops CPU profiling in the daemon
|
||||||
rpc StopCPUProfile(StopCPUProfileRequest) returns (StopCPUProfileResponse) {}
|
rpc StopCPUProfile(StopCPUProfileRequest) returns (StopCPUProfileResponse) {}
|
||||||
|
|
||||||
rpc NotifyOSLifecycle(OSLifecycleRequest) returns(OSLifecycleResponse) {}
|
|
||||||
|
|
||||||
rpc GetInstallerResult(InstallerResultRequest) returns (InstallerResultResponse) {}
|
rpc GetInstallerResult(InstallerResultRequest) returns (InstallerResultResponse) {}
|
||||||
|
|
||||||
// ExposeService exposes a local port via the NetBird reverse proxy
|
// ExposeService exposes a local port via the NetBird reverse proxy
|
||||||
@@ -114,20 +112,6 @@ service DaemonService {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
message OSLifecycleRequest {
|
|
||||||
// avoid collision with loglevel enum
|
|
||||||
enum CycleType {
|
|
||||||
UNKNOWN = 0;
|
|
||||||
SLEEP = 1;
|
|
||||||
WAKEUP = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
CycleType type = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message OSLifecycleResponse {}
|
|
||||||
|
|
||||||
|
|
||||||
message LoginRequest {
|
message LoginRequest {
|
||||||
// setupKey netbird setup key.
|
// setupKey netbird setup key.
|
||||||
string setupKey = 1;
|
string setupKey = 1;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -120,6 +120,7 @@ func New(ctx context.Context, logFile string, configFile string, profilesDisable
|
|||||||
}
|
}
|
||||||
agent := &serverAgent{s}
|
agent := &serverAgent{s}
|
||||||
s.sleepHandler = sleephandler.New(agent)
|
s.sleepHandler = sleephandler.New(agent)
|
||||||
|
s.startSleepDetector()
|
||||||
|
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -335,7 +335,7 @@ func startManagement(t *testing.T, signalAddr string, counter *int) (*grpc.Serve
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
mgmtServer, err := nbgrpc.NewServer(config, accountManager, settingsMockManager, jobManager, secretsManager, nil, nil, &server.MockIntegratedValidator{}, networkMapController, nil)
|
mgmtServer, err := nbgrpc.NewServer(config, accountManager, settingsMockManager, jobManager, secretsManager, nil, nil, &server.MockIntegratedValidator{}, networkMapController, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,18 @@ package server
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal"
|
"github.com/netbirdio/netbird/client/internal"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/sleep"
|
||||||
"github.com/netbirdio/netbird/client/proto"
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const envDisableSleepDetector = "NB_DISABLE_SLEEP_DETECTOR"
|
||||||
|
|
||||||
// serverAgent adapts Server to the handler.Agent and handler.StatusChecker interfaces
|
// serverAgent adapts Server to the handler.Agent and handler.StatusChecker interfaces
|
||||||
type serverAgent struct {
|
type serverAgent struct {
|
||||||
s *Server
|
s *Server
|
||||||
@@ -28,19 +33,61 @@ func (a *serverAgent) Status() (internal.StatusType, error) {
|
|||||||
return internal.CtxGetState(a.s.rootCtx).Status()
|
return internal.CtxGetState(a.s.rootCtx).Status()
|
||||||
}
|
}
|
||||||
|
|
||||||
// NotifyOSLifecycle handles operating system lifecycle events by executing appropriate logic based on the request type.
|
// startSleepDetector starts the OS sleep/wake detector and forwards events to
|
||||||
func (s *Server) NotifyOSLifecycle(callerCtx context.Context, req *proto.OSLifecycleRequest) (*proto.OSLifecycleResponse, error) {
|
// the sleep handler. On platforms without a supported detector the attempt
|
||||||
switch req.GetType() {
|
// logs a warning and returns. Setting NB_DISABLE_SLEEP_DETECTOR=true skips
|
||||||
case proto.OSLifecycleRequest_WAKEUP:
|
// registration entirely.
|
||||||
if err := s.sleepHandler.HandleWakeUp(callerCtx); err != nil {
|
func (s *Server) startSleepDetector() {
|
||||||
return &proto.OSLifecycleResponse{}, err
|
if sleepDetectorDisabled() {
|
||||||
|
log.Info("sleep detection disabled via " + envDisableSleepDetector)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
case proto.OSLifecycleRequest_SLEEP:
|
|
||||||
if err := s.sleepHandler.HandleSleep(callerCtx); err != nil {
|
svc, err := sleep.New()
|
||||||
return &proto.OSLifecycleResponse{}, err
|
if err != nil {
|
||||||
|
log.Warnf("failed to initialize sleep detection: %v", err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
default:
|
|
||||||
log.Errorf("unknown OSLifecycleRequest type: %v", req.GetType())
|
err = svc.Register(func(event sleep.EventType) {
|
||||||
|
switch event {
|
||||||
|
case sleep.EventTypeSleep:
|
||||||
|
log.Info("handling sleep event")
|
||||||
|
if err := s.sleepHandler.HandleSleep(s.rootCtx); err != nil {
|
||||||
|
log.Errorf("failed to handle sleep event: %v", err)
|
||||||
}
|
}
|
||||||
return &proto.OSLifecycleResponse{}, nil
|
case sleep.EventTypeWakeUp:
|
||||||
|
log.Info("handling wakeup event")
|
||||||
|
if err := s.sleepHandler.HandleWakeUp(s.rootCtx); err != nil {
|
||||||
|
log.Errorf("failed to handle wakeup event: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to register sleep detector: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("sleep detection service initialized")
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-s.rootCtx.Done()
|
||||||
|
log.Info("stopping sleep event listener")
|
||||||
|
if err := svc.Deregister(); err != nil {
|
||||||
|
log.Errorf("failed to deregister sleep detector: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func sleepDetectorDisabled() bool {
|
||||||
|
val := os.Getenv(envDisableSleepDetector)
|
||||||
|
if val == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
disabled, err := strconv.ParseBool(val)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to parse %s=%q: %v", envDisableSleepDetector, val, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return disabled
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,10 +38,10 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/iface"
|
"github.com/netbirdio/netbird/client/iface"
|
||||||
"github.com/netbirdio/netbird/client/internal"
|
"github.com/netbirdio/netbird/client/internal"
|
||||||
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
||||||
"github.com/netbirdio/netbird/client/internal/sleep"
|
|
||||||
"github.com/netbirdio/netbird/client/proto"
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
"github.com/netbirdio/netbird/client/ui/desktop"
|
"github.com/netbirdio/netbird/client/ui/desktop"
|
||||||
"github.com/netbirdio/netbird/client/ui/event"
|
"github.com/netbirdio/netbird/client/ui/event"
|
||||||
|
"github.com/netbirdio/netbird/client/ui/notifier"
|
||||||
"github.com/netbirdio/netbird/client/ui/process"
|
"github.com/netbirdio/netbird/client/ui/process"
|
||||||
"github.com/netbirdio/netbird/util"
|
"github.com/netbirdio/netbird/util"
|
||||||
|
|
||||||
@@ -260,6 +260,7 @@ type serviceClient struct {
|
|||||||
|
|
||||||
// application with main windows.
|
// application with main windows.
|
||||||
app fyne.App
|
app fyne.App
|
||||||
|
notifier notifier.Notifier
|
||||||
wSettings fyne.Window
|
wSettings fyne.Window
|
||||||
showAdvancedSettings bool
|
showAdvancedSettings bool
|
||||||
sendNotification bool
|
sendNotification bool
|
||||||
@@ -364,6 +365,7 @@ func newServiceClient(args *newServiceClientArgs) *serviceClient {
|
|||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
addr: args.addr,
|
addr: args.addr,
|
||||||
app: args.app,
|
app: args.app,
|
||||||
|
notifier: notifier.New(args.app),
|
||||||
logFile: args.logFile,
|
logFile: args.logFile,
|
||||||
sendNotification: false,
|
sendNotification: false,
|
||||||
|
|
||||||
@@ -892,7 +894,7 @@ func (s *serviceClient) updateStatus() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("get service status: %v", err)
|
log.Errorf("get service status: %v", err)
|
||||||
if s.connected {
|
if s.connected {
|
||||||
s.app.SendNotification(fyne.NewNotification("Error", "Connection to service lost"))
|
s.notifier.Send("Error", "Connection to service lost")
|
||||||
}
|
}
|
||||||
s.setDisconnectedStatus()
|
s.setDisconnectedStatus()
|
||||||
return err
|
return err
|
||||||
@@ -1109,7 +1111,7 @@ func (s *serviceClient) onTrayReady() {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
s.eventManager = event.NewManager(s.app, s.addr)
|
s.eventManager = event.NewManager(s.notifier, s.addr)
|
||||||
s.eventManager.SetNotificationsEnabled(s.mNotifications.Checked())
|
s.eventManager.SetNotificationsEnabled(s.mNotifications.Checked())
|
||||||
s.eventManager.AddHandler(func(event *proto.SystemEvent) {
|
s.eventManager.AddHandler(func(event *proto.SystemEvent) {
|
||||||
if event.Category == proto.SystemEvent_SYSTEM {
|
if event.Category == proto.SystemEvent_SYSTEM {
|
||||||
@@ -1146,9 +1148,6 @@ func (s *serviceClient) onTrayReady() {
|
|||||||
|
|
||||||
go s.eventManager.Start(s.ctx)
|
go s.eventManager.Start(s.ctx)
|
||||||
go s.eventHandler.listen(s.ctx)
|
go s.eventHandler.listen(s.ctx)
|
||||||
|
|
||||||
// Start sleep detection listener
|
|
||||||
go s.startSleepListener()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *serviceClient) attachOutput(cmd *exec.Cmd) *os.File {
|
func (s *serviceClient) attachOutput(cmd *exec.Cmd) *os.File {
|
||||||
@@ -1209,62 +1208,6 @@ func (s *serviceClient) getSrvClient(timeout time.Duration) (proto.DaemonService
|
|||||||
return s.conn, nil
|
return s.conn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// startSleepListener initializes the sleep detection service and listens for sleep events
|
|
||||||
func (s *serviceClient) startSleepListener() {
|
|
||||||
sleepService, err := sleep.New()
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("%v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := sleepService.Register(s.handleSleepEvents); err != nil {
|
|
||||||
log.Errorf("failed to start sleep detection: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("sleep detection service initialized")
|
|
||||||
|
|
||||||
// Cleanup on context cancellation
|
|
||||||
go func() {
|
|
||||||
<-s.ctx.Done()
|
|
||||||
log.Info("stopping sleep event listener")
|
|
||||||
if err := sleepService.Deregister(); err != nil {
|
|
||||||
log.Errorf("failed to deregister sleep detection: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleSleepEvents sends a sleep notification to the daemon via gRPC
|
|
||||||
func (s *serviceClient) handleSleepEvents(event sleep.EventType) {
|
|
||||||
conn, err := s.getSrvClient(0)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to get daemon client for sleep notification: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
req := &proto.OSLifecycleRequest{}
|
|
||||||
|
|
||||||
switch event {
|
|
||||||
case sleep.EventTypeWakeUp:
|
|
||||||
log.Infof("handle wakeup event: %v", event)
|
|
||||||
req.Type = proto.OSLifecycleRequest_WAKEUP
|
|
||||||
case sleep.EventTypeSleep:
|
|
||||||
log.Infof("handle sleep event: %v", event)
|
|
||||||
req.Type = proto.OSLifecycleRequest_SLEEP
|
|
||||||
default:
|
|
||||||
log.Infof("unknown event: %v", event)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = conn.NotifyOSLifecycle(s.ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to notify daemon about os lifecycle notification: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("successfully notified daemon about os lifecycle")
|
|
||||||
}
|
|
||||||
|
|
||||||
// setSettingsEnabled enables or disables the settings menu based on the provided state
|
// setSettingsEnabled enables or disables the settings menu based on the provided state
|
||||||
func (s *serviceClient) setSettingsEnabled(enabled bool) {
|
func (s *serviceClient) setSettingsEnabled(enabled bool) {
|
||||||
if s.mSettings != nil {
|
if s.mSettings != nil {
|
||||||
@@ -1548,7 +1491,7 @@ func (s *serviceClient) onUpdateAvailable(newVersion string, enforced bool) {
|
|||||||
|
|
||||||
if enforced && s.lastNotifiedVersion != newVersion {
|
if enforced && s.lastNotifiedVersion != newVersion {
|
||||||
s.lastNotifiedVersion = newVersion
|
s.lastNotifiedVersion = newVersion
|
||||||
s.app.SendNotification(fyne.NewNotification("Update available", "A new version "+newVersion+" is ready to install"))
|
s.notifier.Send("Update available", "A new version "+newVersion+" is ready to install")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
|
||||||
"github.com/cenkalti/backoff/v4"
|
"github.com/cenkalti/backoff/v4"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
@@ -18,10 +17,16 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/ui/desktop"
|
"github.com/netbirdio/netbird/client/ui/desktop"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Notifier sends desktop notifications. Defined here so the event package
|
||||||
|
// does not depend on fyne or the platform-specific notifier implementation.
|
||||||
|
type Notifier interface {
|
||||||
|
Send(title, body string)
|
||||||
|
}
|
||||||
|
|
||||||
type Handler func(*proto.SystemEvent)
|
type Handler func(*proto.SystemEvent)
|
||||||
|
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
app fyne.App
|
notifier Notifier
|
||||||
addr string
|
addr string
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
@@ -31,9 +36,9 @@ type Manager struct {
|
|||||||
handlers []Handler
|
handlers []Handler
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewManager(app fyne.App, addr string) *Manager {
|
func NewManager(notifier Notifier, addr string) *Manager {
|
||||||
return &Manager{
|
return &Manager{
|
||||||
app: app,
|
notifier: notifier,
|
||||||
addr: addr,
|
addr: addr,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,7 +119,7 @@ func (e *Manager) handleEvent(event *proto.SystemEvent) {
|
|||||||
if id != "" {
|
if id != "" {
|
||||||
body += fmt.Sprintf(" ID: %s", id)
|
body += fmt.Sprintf(" ID: %s", id)
|
||||||
}
|
}
|
||||||
e.app.SendNotification(fyne.NewNotification(title, body))
|
e.notifier.Send(title, body)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, handler := range handlers {
|
for _, handler := range handlers {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
|
||||||
"fyne.io/systray"
|
"fyne.io/systray"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
@@ -87,7 +86,7 @@ func (h *eventHandler) handleConnectClick() {
|
|||||||
if errors.Is(err, context.Canceled) || (ok && st.Code() == codes.Canceled) {
|
if errors.Is(err, context.Canceled) || (ok && st.Code() == codes.Canceled) {
|
||||||
log.Debugf("connect operation cancelled by user")
|
log.Debugf("connect operation cancelled by user")
|
||||||
} else {
|
} else {
|
||||||
h.client.app.SendNotification(fyne.NewNotification("Error", "Failed to connect"))
|
h.client.notifier.Send("Error", "Failed to connect")
|
||||||
log.Errorf("connect failed: %v", err)
|
log.Errorf("connect failed: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -112,7 +111,7 @@ func (h *eventHandler) handleDisconnectClick() {
|
|||||||
if err := h.client.menuDownClick(); err != nil {
|
if err := h.client.menuDownClick(); err != nil {
|
||||||
st, ok := status.FromError(err)
|
st, ok := status.FromError(err)
|
||||||
if !errors.Is(err, context.Canceled) && !(ok && st.Code() == codes.Canceled) {
|
if !errors.Is(err, context.Canceled) && !(ok && st.Code() == codes.Canceled) {
|
||||||
h.client.app.SendNotification(fyne.NewNotification("Error", "Failed to disconnect"))
|
h.client.notifier.Send("Error", "Failed to disconnect")
|
||||||
log.Errorf("disconnect failed: %v", err)
|
log.Errorf("disconnect failed: %v", err)
|
||||||
} else {
|
} else {
|
||||||
log.Debugf("disconnect cancelled or already disconnecting")
|
log.Debugf("disconnect cancelled or already disconnecting")
|
||||||
@@ -130,7 +129,7 @@ func (h *eventHandler) handleAllowSSHClick() {
|
|||||||
if err := h.updateConfigWithErr(); err != nil {
|
if err := h.updateConfigWithErr(); err != nil {
|
||||||
h.toggleCheckbox(h.client.mAllowSSH) // revert checkbox state on error
|
h.toggleCheckbox(h.client.mAllowSSH) // revert checkbox state on error
|
||||||
log.Errorf("failed to update config: %v", err)
|
log.Errorf("failed to update config: %v", err)
|
||||||
h.client.app.SendNotification(fyne.NewNotification("Error", "Failed to update SSH settings"))
|
h.client.notifier.Send("Error", "Failed to update SSH settings")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -140,7 +139,7 @@ func (h *eventHandler) handleAutoConnectClick() {
|
|||||||
if err := h.updateConfigWithErr(); err != nil {
|
if err := h.updateConfigWithErr(); err != nil {
|
||||||
h.toggleCheckbox(h.client.mAutoConnect) // revert checkbox state on error
|
h.toggleCheckbox(h.client.mAutoConnect) // revert checkbox state on error
|
||||||
log.Errorf("failed to update config: %v", err)
|
log.Errorf("failed to update config: %v", err)
|
||||||
h.client.app.SendNotification(fyne.NewNotification("Error", "Failed to update auto-connect settings"))
|
h.client.notifier.Send("Error", "Failed to update auto-connect settings")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,7 +148,7 @@ func (h *eventHandler) handleRosenpassClick() {
|
|||||||
if err := h.updateConfigWithErr(); err != nil {
|
if err := h.updateConfigWithErr(); err != nil {
|
||||||
h.toggleCheckbox(h.client.mEnableRosenpass) // revert checkbox state on error
|
h.toggleCheckbox(h.client.mEnableRosenpass) // revert checkbox state on error
|
||||||
log.Errorf("failed to update config: %v", err)
|
log.Errorf("failed to update config: %v", err)
|
||||||
h.client.app.SendNotification(fyne.NewNotification("Error", "Failed to update Rosenpass settings"))
|
h.client.notifier.Send("Error", "Failed to update Rosenpass settings")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,7 +157,7 @@ func (h *eventHandler) handleLazyConnectionClick() {
|
|||||||
if err := h.updateConfigWithErr(); err != nil {
|
if err := h.updateConfigWithErr(); err != nil {
|
||||||
h.toggleCheckbox(h.client.mLazyConnEnabled) // revert checkbox state on error
|
h.toggleCheckbox(h.client.mLazyConnEnabled) // revert checkbox state on error
|
||||||
log.Errorf("failed to update config: %v", err)
|
log.Errorf("failed to update config: %v", err)
|
||||||
h.client.app.SendNotification(fyne.NewNotification("Error", "Failed to update lazy connection settings"))
|
h.client.notifier.Send("Error", "Failed to update lazy connection settings")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,7 +166,7 @@ func (h *eventHandler) handleBlockInboundClick() {
|
|||||||
if err := h.updateConfigWithErr(); err != nil {
|
if err := h.updateConfigWithErr(); err != nil {
|
||||||
h.toggleCheckbox(h.client.mBlockInbound) // revert checkbox state on error
|
h.toggleCheckbox(h.client.mBlockInbound) // revert checkbox state on error
|
||||||
log.Errorf("failed to update config: %v", err)
|
log.Errorf("failed to update config: %v", err)
|
||||||
h.client.app.SendNotification(fyne.NewNotification("Error", "Failed to update block inbound settings"))
|
h.client.notifier.Send("Error", "Failed to update block inbound settings")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,7 +175,7 @@ func (h *eventHandler) handleNotificationsClick() {
|
|||||||
if err := h.updateConfigWithErr(); err != nil {
|
if err := h.updateConfigWithErr(); err != nil {
|
||||||
h.toggleCheckbox(h.client.mNotifications) // revert checkbox state on error
|
h.toggleCheckbox(h.client.mNotifications) // revert checkbox state on error
|
||||||
log.Errorf("failed to update config: %v", err)
|
log.Errorf("failed to update config: %v", err)
|
||||||
h.client.app.SendNotification(fyne.NewNotification("Error", "Failed to update notifications settings"))
|
h.client.notifier.Send("Error", "Failed to update notifications settings")
|
||||||
} else if h.client.eventManager != nil {
|
} else if h.client.eventManager != nil {
|
||||||
h.client.eventManager.SetNotificationsEnabled(h.client.mNotifications.Checked())
|
h.client.eventManager.SetNotificationsEnabled(h.client.mNotifications.Checked())
|
||||||
}
|
}
|
||||||
|
|||||||
27
client/ui/notifier/notifier.go
Normal file
27
client/ui/notifier/notifier.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// Package notifier sends desktop notifications. On Windows it uses the WinRT
|
||||||
|
// COM API directly via go-toast/v2 to avoid the PowerShell window flash that
|
||||||
|
// fyne's default implementation produces. On other platforms it delegates to
|
||||||
|
// fyne.
|
||||||
|
package notifier
|
||||||
|
|
||||||
|
import "fyne.io/fyne/v2"
|
||||||
|
|
||||||
|
// Notifier sends desktop notifications.
|
||||||
|
type Notifier interface {
|
||||||
|
Send(title, body string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a platform-specific Notifier. The fyne app is used as the
|
||||||
|
// fallback notifier on platforms where no native implementation is wired up,
|
||||||
|
// and on Windows when the COM path fails to initialize.
|
||||||
|
func New(app fyne.App) Notifier {
|
||||||
|
return newNotifier(app)
|
||||||
|
}
|
||||||
|
|
||||||
|
type fyneNotifier struct {
|
||||||
|
app fyne.App
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fyneNotifier) Send(title, body string) {
|
||||||
|
f.app.SendNotification(fyne.NewNotification(title, body))
|
||||||
|
}
|
||||||
9
client/ui/notifier/notifier_other.go
Normal file
9
client/ui/notifier/notifier_other.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package notifier
|
||||||
|
|
||||||
|
import "fyne.io/fyne/v2"
|
||||||
|
|
||||||
|
func newNotifier(app fyne.App) Notifier {
|
||||||
|
return &fyneNotifier{app: app}
|
||||||
|
}
|
||||||
88
client/ui/notifier/notifier_windows.go
Normal file
88
client/ui/notifier/notifier_windows.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package notifier
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
|
toast "git.sr.ht/~jackmordaunt/go-toast/v2"
|
||||||
|
"git.sr.ht/~jackmordaunt/go-toast/v2/wintoast"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// appID is the AppUserModelID shown in the Windows Action Center. It
|
||||||
|
// must match the System.AppUserModel.ID property set on the Start Menu
|
||||||
|
// shortcut by the MSI (see client/netbird.wxs); otherwise Windows
|
||||||
|
// groups toasts under a separate, unbranded entry.
|
||||||
|
appID = "NetBird"
|
||||||
|
|
||||||
|
// appGUID identifies the COM activation callback class. Generated once
|
||||||
|
// for NetBird; do not change without coordinating an installer bump,
|
||||||
|
// since old registry entries pointing at the previous GUID would orphan.
|
||||||
|
appGUID = "{0E1B4DE7-E148-432B-9814-544F941826EC}"
|
||||||
|
)
|
||||||
|
|
||||||
|
type comNotifier struct {
|
||||||
|
fallback *fyneNotifier
|
||||||
|
ready bool
|
||||||
|
iconPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
initOnce sync.Once
|
||||||
|
initErr error
|
||||||
|
)
|
||||||
|
|
||||||
|
func newNotifier(app fyne.App) Notifier {
|
||||||
|
n := &comNotifier{
|
||||||
|
fallback: &fyneNotifier{app: app},
|
||||||
|
iconPath: resolveIcon(),
|
||||||
|
}
|
||||||
|
initOnce.Do(func() {
|
||||||
|
initErr = wintoast.SetAppData(wintoast.AppData{
|
||||||
|
AppID: appID,
|
||||||
|
GUID: appGUID,
|
||||||
|
IconPath: n.iconPath,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if initErr != nil {
|
||||||
|
log.Warnf("toast: register app data failed, falling back to fyne notifications: %v", initErr)
|
||||||
|
return n.fallback
|
||||||
|
}
|
||||||
|
n.ready = true
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *comNotifier) Send(title, body string) {
|
||||||
|
if !n.ready {
|
||||||
|
n.fallback.Send(title, body)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
notification := toast.Notification{
|
||||||
|
AppID: appID,
|
||||||
|
Title: title,
|
||||||
|
Body: body,
|
||||||
|
Icon: n.iconPath,
|
||||||
|
}
|
||||||
|
if err := notification.Push(); err != nil {
|
||||||
|
log.Warnf("toast: push failed, using fyne fallback: %v", err)
|
||||||
|
n.fallback.Send(title, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveIcon returns an absolute path to the toast icon, or an empty string
|
||||||
|
// when no icon can be located. Windows requires a PNG/JPG for the
|
||||||
|
// AppUserModelId IconUri registry value; .ico is silently ignored.
|
||||||
|
func resolveIcon() string {
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
candidate := filepath.Join(filepath.Dir(exe), "netbird.png")
|
||||||
|
if _, err := os.Stat(candidate); err == nil {
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
@@ -548,7 +548,7 @@ func (p *profileMenu) refresh() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed to switch profile: %v", err)
|
log.Errorf("failed to switch profile: %v", err)
|
||||||
// show notification dialog
|
// show notification dialog
|
||||||
p.app.SendNotification(fyne.NewNotification("Error", "Failed to switch profile"))
|
p.serviceClient.notifier.Send("Error", "Failed to switch profile")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -628,9 +628,9 @@ func (p *profileMenu) refresh() {
|
|||||||
}
|
}
|
||||||
if err := p.eventHandler.logout(p.ctx); err != nil {
|
if err := p.eventHandler.logout(p.ctx); err != nil {
|
||||||
log.Errorf("logout failed: %v", err)
|
log.Errorf("logout failed: %v", err)
|
||||||
p.app.SendNotification(fyne.NewNotification("Error", "Failed to deregister"))
|
p.serviceClient.notifier.Send("Error", "Failed to deregister")
|
||||||
} else {
|
} else {
|
||||||
p.app.SendNotification(fyne.NewNotification("Success", "Deregistered successfully"))
|
p.serviceClient.notifier.Send("Success", "Deregistered successfully")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3
go.mod
3
go.mod
@@ -30,6 +30,7 @@ require (
|
|||||||
require (
|
require (
|
||||||
fyne.io/fyne/v2 v2.7.0
|
fyne.io/fyne/v2 v2.7.0
|
||||||
fyne.io/systray v1.12.1-0.20260116214250-81f8e1a496f9
|
fyne.io/systray v1.12.1-0.20260116214250-81f8e1a496f9
|
||||||
|
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3
|
||||||
github.com/awnumar/memguard v0.23.0
|
github.com/awnumar/memguard v0.23.0
|
||||||
github.com/aws/aws-sdk-go-v2 v1.38.3
|
github.com/aws/aws-sdk-go-v2 v1.38.3
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.31.6
|
github.com/aws/aws-sdk-go-v2/config v1.31.6
|
||||||
@@ -46,6 +47,7 @@ require (
|
|||||||
github.com/crowdsecurity/go-cs-bouncer v0.0.21
|
github.com/crowdsecurity/go-cs-bouncer v0.0.21
|
||||||
github.com/dexidp/dex v0.0.0-00010101000000-000000000000
|
github.com/dexidp/dex v0.0.0-00010101000000-000000000000
|
||||||
github.com/dexidp/dex/api/v2 v2.4.0
|
github.com/dexidp/dex/api/v2 v2.4.0
|
||||||
|
github.com/ebitengine/purego v0.8.4
|
||||||
github.com/eko/gocache/lib/v4 v4.2.0
|
github.com/eko/gocache/lib/v4 v4.2.0
|
||||||
github.com/eko/gocache/store/go_cache/v4 v4.2.2
|
github.com/eko/gocache/store/go_cache/v4 v4.2.2
|
||||||
github.com/eko/gocache/store/redis/v4 v4.2.2
|
github.com/eko/gocache/store/redis/v4 v4.2.2
|
||||||
@@ -178,7 +180,6 @@ require (
|
|||||||
github.com/docker/docker v28.0.1+incompatible // indirect
|
github.com/docker/docker v28.0.1+incompatible // indirect
|
||||||
github.com/docker/go-connections v0.6.0 // indirect
|
github.com/docker/go-connections v0.6.0 // indirect
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/ebitengine/purego v0.8.4 // indirect
|
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/fredbi/uri v1.1.1 // indirect
|
github.com/fredbi/uri v1.1.1 // indirect
|
||||||
github.com/fyne-io/gl-js v0.2.0 // indirect
|
github.com/fyne-io/gl-js v0.2.0 // indirect
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -15,6 +15,8 @@ fyne.io/fyne/v2 v2.7.0 h1:GvZSpE3X0liU/fqstInVvRsaboIVpIWQ4/sfjDGIGGQ=
|
|||||||
fyne.io/fyne/v2 v2.7.0/go.mod h1:xClVlrhxl7D+LT+BWYmcrW4Nf+dJTvkhnPgji7spAwE=
|
fyne.io/fyne/v2 v2.7.0/go.mod h1:xClVlrhxl7D+LT+BWYmcrW4Nf+dJTvkhnPgji7spAwE=
|
||||||
fyne.io/systray v1.12.1-0.20260116214250-81f8e1a496f9 h1:829+77I4TaMrcg9B3wf+gHhdSgoCVEgH2czlPXPbfj4=
|
fyne.io/systray v1.12.1-0.20260116214250-81f8e1a496f9 h1:829+77I4TaMrcg9B3wf+gHhdSgoCVEgH2czlPXPbfj4=
|
||||||
fyne.io/systray v1.12.1-0.20260116214250-81f8e1a496f9/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
|
fyne.io/systray v1.12.1-0.20260116214250-81f8e1a496f9/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
|
||||||
|
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf53nQmtqMA=
|
||||||
|
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc=
|
||||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
|
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
|
||||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
||||||
github.com/AppsFlyer/go-sundheit v0.6.0 h1:d2hBvCjBSb2lUsEWGfPigr4MCOt04sxB+Rppl0yUMSk=
|
github.com/AppsFlyer/go-sundheit v0.6.0 h1:d2hBvCjBSb2lUsEWGfPigr4MCOt04sxB+Rppl0yUMSk=
|
||||||
|
|||||||
@@ -231,7 +231,20 @@ get_upstream_host() {
|
|||||||
|
|
||||||
wait_management_proxy() {
|
wait_management_proxy() {
|
||||||
local proxy_container="${1:-traefik}"
|
local proxy_container="${1:-traefik}"
|
||||||
|
local use_docker_logs=false
|
||||||
set +e
|
set +e
|
||||||
|
|
||||||
|
if [[ "$proxy_container" == "detect-traefik" ]]; then
|
||||||
|
proxy_container=$(docker ps --format "{{.ID}}\t{{.Image}}\t{{.Ports}}" \
|
||||||
|
| awk -F'\t' '$2 ~ /traefik/ && $3 ~ /:(80|443)->/ {print $1; exit}')
|
||||||
|
|
||||||
|
if [[ -z "$proxy_container" ]]; then
|
||||||
|
echo "Warning: could not auto-detect Traefik container, log output will be skipped on timeout." > /dev/stderr
|
||||||
|
else
|
||||||
|
use_docker_logs=true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
echo -n "Waiting for NetBird server to become ready"
|
echo -n "Waiting for NetBird server to become ready"
|
||||||
counter=1
|
counter=1
|
||||||
while true; do
|
while true; do
|
||||||
@@ -242,7 +255,13 @@ wait_management_proxy() {
|
|||||||
if [[ $counter -eq 60 ]]; then
|
if [[ $counter -eq 60 ]]; then
|
||||||
echo ""
|
echo ""
|
||||||
echo "Taking too long. Checking logs..."
|
echo "Taking too long. Checking logs..."
|
||||||
|
if [[ -n "$proxy_container" ]]; then
|
||||||
|
if [[ "$use_docker_logs" == "true" ]]; then
|
||||||
|
docker logs --tail=20 "$proxy_container"
|
||||||
|
else
|
||||||
$DOCKER_COMPOSE_COMMAND logs --tail=20 "$proxy_container"
|
$DOCKER_COMPOSE_COMMAND logs --tail=20 "$proxy_container"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
$DOCKER_COMPOSE_COMMAND logs --tail=20 netbird-server
|
$DOCKER_COMPOSE_COMMAND logs --tail=20 netbird-server
|
||||||
fi
|
fi
|
||||||
echo -n " ."
|
echo -n " ."
|
||||||
@@ -518,7 +537,7 @@ start_services_and_show_instructions() {
|
|||||||
$DOCKER_COMPOSE_COMMAND up -d
|
$DOCKER_COMPOSE_COMMAND up -d
|
||||||
|
|
||||||
sleep 3
|
sleep 3
|
||||||
wait_management_direct
|
wait_management_proxy detect-traefik
|
||||||
|
|
||||||
echo -e "$MSG_DONE"
|
echo -e "$MSG_DONE"
|
||||||
print_post_setup_instructions
|
print_post_setup_instructions
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ func (s *BaseServer) GRPCServer() *grpc.Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
gRPCAPIHandler := grpc.NewServer(gRPCOpts...)
|
gRPCAPIHandler := grpc.NewServer(gRPCOpts...)
|
||||||
srv, err := nbgrpc.NewServer(s.Config, s.AccountManager(), s.SettingsManager(), s.JobManager(), s.SecretsManager(), s.Metrics(), s.AuthManager(), s.IntegratedValidator(), s.NetworkMapController(), s.OAuthConfigProvider())
|
srv, err := nbgrpc.NewServer(s.Config, s.AccountManager(), s.SettingsManager(), s.JobManager(), s.SecretsManager(), s.Metrics(), s.AuthManager(), s.IntegratedValidator(), s.NetworkMapController(), s.OAuthConfigProvider(), s.SessionStore())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to create management server: %v", err)
|
log.Fatalf("failed to create management server: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/netbirdio/management-integrations/integrations"
|
"github.com/netbirdio/management-integrations/integrations"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy"
|
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy"
|
||||||
proxymanager "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy/manager"
|
proxymanager "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy/manager"
|
||||||
|
|
||||||
@@ -66,6 +67,12 @@ func (s *BaseServer) SecretsManager() grpc.SecretsManager {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *BaseServer) SessionStore() *auth.SessionStore {
|
||||||
|
return Create(s, func() *auth.SessionStore {
|
||||||
|
return auth.NewSessionStore(s.CacheStore())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (s *BaseServer) AuthManager() auth.Manager {
|
func (s *BaseServer) AuthManager() auth.Manager {
|
||||||
audiences := s.Config.GetAuthAudiences()
|
audiences := s.Config.GetAuthAudiences()
|
||||||
audience := s.Config.HttpConfig.AuthAudience
|
audience := s.Config.HttpConfig.AuthAudience
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
jwtv5 "github.com/golang-jwt/jwt/v5"
|
||||||
pb "github.com/golang/protobuf/proto" // nolint
|
pb "github.com/golang/protobuf/proto" // nolint
|
||||||
"github.com/golang/protobuf/ptypes/timestamp"
|
"github.com/golang/protobuf/ptypes/timestamp"
|
||||||
"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/realip"
|
"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/realip"
|
||||||
@@ -67,6 +68,7 @@ type Server struct {
|
|||||||
appMetrics telemetry.AppMetrics
|
appMetrics telemetry.AppMetrics
|
||||||
peerLocks sync.Map
|
peerLocks sync.Map
|
||||||
authManager auth.Manager
|
authManager auth.Manager
|
||||||
|
sessionStore *auth.SessionStore
|
||||||
|
|
||||||
logBlockedPeers bool
|
logBlockedPeers bool
|
||||||
blockPeersWithSameConfig bool
|
blockPeersWithSameConfig bool
|
||||||
@@ -98,6 +100,7 @@ func NewServer(
|
|||||||
integratedPeerValidator integrated_validator.IntegratedValidator,
|
integratedPeerValidator integrated_validator.IntegratedValidator,
|
||||||
networkMapController network_map.Controller,
|
networkMapController network_map.Controller,
|
||||||
oAuthConfigProvider idp.OAuthConfigProvider,
|
oAuthConfigProvider idp.OAuthConfigProvider,
|
||||||
|
sessionStore *auth.SessionStore,
|
||||||
) (*Server, error) {
|
) (*Server, error) {
|
||||||
if appMetrics != nil {
|
if appMetrics != nil {
|
||||||
// update gauge based on number of connected peers which is equal to open gRPC streams
|
// update gauge based on number of connected peers which is equal to open gRPC streams
|
||||||
@@ -140,6 +143,7 @@ func NewServer(
|
|||||||
integratedPeerValidator: integratedPeerValidator,
|
integratedPeerValidator: integratedPeerValidator,
|
||||||
networkMapController: networkMapController,
|
networkMapController: networkMapController,
|
||||||
oAuthConfigProvider: oAuthConfigProvider,
|
oAuthConfigProvider: oAuthConfigProvider,
|
||||||
|
sessionStore: sessionStore,
|
||||||
|
|
||||||
loginFilter: newLoginFilter(),
|
loginFilter: newLoginFilter(),
|
||||||
|
|
||||||
@@ -535,7 +539,7 @@ func (s *Server) cancelPeerRoutinesWithoutLock(ctx context.Context, accountID st
|
|||||||
log.WithContext(ctx).Debugf("peer %s has been disconnected", peer.Key)
|
log.WithContext(ctx).Debugf("peer %s has been disconnected", peer.Key)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) validateToken(ctx context.Context, jwtToken string) (string, error) {
|
func (s *Server) validateToken(ctx context.Context, peerKey, jwtToken string) (string, error) {
|
||||||
if s.authManager == nil {
|
if s.authManager == nil {
|
||||||
return "", status.Errorf(codes.Internal, "missing auth manager")
|
return "", status.Errorf(codes.Internal, "missing auth manager")
|
||||||
}
|
}
|
||||||
@@ -545,6 +549,10 @@ func (s *Server) validateToken(ctx context.Context, jwtToken string) (string, er
|
|||||||
return "", status.Errorf(codes.InvalidArgument, "invalid jwt token, err: %v", err)
|
return "", status.Errorf(codes.InvalidArgument, "invalid jwt token, err: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := s.claimLoginToken(ctx, peerKey, jwtToken, token); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
// we need to call this method because if user is new, we will automatically add it to existing or create a new account
|
// we need to call this method because if user is new, we will automatically add it to existing or create a new account
|
||||||
accountId, _, err := s.accountManager.GetAccountIDFromUserAuth(ctx, userAuth)
|
accountId, _, err := s.accountManager.GetAccountIDFromUserAuth(ctx, userAuth)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -828,6 +836,31 @@ func (s *Server) prepareLoginResponse(ctx context.Context, peer *nbpeer.Peer, ne
|
|||||||
return loginResp, nil
|
return loginResp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) claimLoginToken(ctx context.Context, peerKey, jwtToken string, token *jwtv5.Token) error {
|
||||||
|
if s.sessionStore == nil || token == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
exp, err := token.Claims.GetExpirationTime()
|
||||||
|
if err != nil || exp == nil {
|
||||||
|
log.WithContext(ctx).Warnf("JWT has no usable exp claim for peer %s", peerKey)
|
||||||
|
return status.Error(codes.Unauthenticated, "jwt token has no expiration")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.sessionStore.RegisterToken(ctx, jwtToken, exp.Time)
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, auth.ErrTokenAlreadyUsed) || errors.Is(err, auth.ErrTokenExpired) {
|
||||||
|
log.WithContext(ctx).Warnf("%v for peer %s", err, peerKey)
|
||||||
|
return status.Error(codes.Unauthenticated, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
log.WithContext(ctx).Warnf("failed to claim JWT for peer %s: %v", peerKey, err)
|
||||||
|
return status.Error(codes.Unavailable, "failed to claim jwt token")
|
||||||
|
}
|
||||||
|
|
||||||
// processJwtToken validates the existence of a JWT token in the login request, and returns the corresponding user ID if
|
// processJwtToken validates the existence of a JWT token in the login request, and returns the corresponding user ID if
|
||||||
// the token is valid.
|
// the token is valid.
|
||||||
//
|
//
|
||||||
@@ -838,7 +871,7 @@ func (s *Server) processJwtToken(ctx context.Context, loginReq *proto.LoginReque
|
|||||||
if loginReq.GetJwtToken() != "" {
|
if loginReq.GetJwtToken() != "" {
|
||||||
var err error
|
var err error
|
||||||
for i := 0; i < 3; i++ {
|
for i := 0; i < 3; i++ {
|
||||||
userID, err = s.validateToken(ctx, loginReq.GetJwtToken())
|
userID, err = s.validateToken(ctx, peerKey.String(), loginReq.GetJwtToken())
|
||||||
if err == nil {
|
if err == nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
61
management/server/auth/session.go
Normal file
61
management/server/auth/session.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/eko/gocache/lib/v4/cache"
|
||||||
|
"github.com/eko/gocache/lib/v4/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
usedTokenKeyPrefix = "jwt-used:"
|
||||||
|
usedTokenMarker = "1"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrTokenAlreadyUsed = errors.New("JWT already used")
|
||||||
|
ErrTokenExpired = errors.New("JWT expired")
|
||||||
|
)
|
||||||
|
|
||||||
|
type SessionStore struct {
|
||||||
|
cache *cache.Cache[string]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSessionStore(cacheStore store.StoreInterface) *SessionStore {
|
||||||
|
return &SessionStore{cache: cache.New[string](cacheStore)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterToken records a JWT until its exp time and rejects reuse.
|
||||||
|
func (s *SessionStore) RegisterToken(ctx context.Context, token string, expiresAt time.Time) error {
|
||||||
|
ttl := time.Until(expiresAt)
|
||||||
|
if ttl <= 0 {
|
||||||
|
return ErrTokenExpired
|
||||||
|
}
|
||||||
|
|
||||||
|
key := usedTokenKeyPrefix + hashToken(token)
|
||||||
|
_, err := s.cache.Get(ctx, key)
|
||||||
|
if err == nil {
|
||||||
|
return ErrTokenAlreadyUsed
|
||||||
|
}
|
||||||
|
|
||||||
|
var notFound *store.NotFound
|
||||||
|
if !errors.As(err, ¬Found) {
|
||||||
|
return fmt.Errorf("failed to lookup used token entry: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.cache.Set(ctx, key, usedTokenMarker, store.WithExpiration(ttl)); err != nil {
|
||||||
|
return fmt.Errorf("failed to store used token entry: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func hashToken(token string) string {
|
||||||
|
sum := sha256.Sum256([]byte(token))
|
||||||
|
return hex.EncodeToString(sum[:])
|
||||||
|
}
|
||||||
82
management/server/auth/session_test.go
Normal file
82
management/server/auth/session_test.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
nbcache "github.com/netbirdio/netbird/management/server/cache"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newTestSessionStore(t *testing.T) *SessionStore {
|
||||||
|
t.Helper()
|
||||||
|
cacheStore, err := nbcache.NewStore(context.Background(), time.Hour, time.Hour, 100)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return NewSessionStore(cacheStore)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSessionStore_FirstRegisterSucceeds(t *testing.T) {
|
||||||
|
s := newTestSessionStore(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
require.NoError(t, s.RegisterToken(ctx, "token", time.Now().Add(time.Hour)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSessionStore_RegisterSameTokenTwiceIsRejected(t *testing.T) {
|
||||||
|
s := newTestSessionStore(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
token := "token"
|
||||||
|
exp := time.Now().Add(time.Hour)
|
||||||
|
|
||||||
|
require.NoError(t, s.RegisterToken(ctx, token, exp))
|
||||||
|
|
||||||
|
err := s.RegisterToken(ctx, token, exp)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.ErrorIs(t, err, ErrTokenAlreadyUsed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSessionStore_RegisterDifferentTokensAreIndependent(t *testing.T) {
|
||||||
|
s := newTestSessionStore(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
exp := time.Now().Add(time.Hour)
|
||||||
|
|
||||||
|
require.NoError(t, s.RegisterToken(ctx, "tokenA", exp))
|
||||||
|
require.NoError(t, s.RegisterToken(ctx, "tokenB", exp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSessionStore_RegisterWithPastExpiryIsRejected(t *testing.T) {
|
||||||
|
s := newTestSessionStore(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
token := "token"
|
||||||
|
|
||||||
|
err := s.RegisterToken(ctx, token, time.Now().Add(-time.Second))
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.ErrorIs(t, err, ErrTokenExpired)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSessionStore_EntryEvictsAtTTLAndAllowsReRegistration(t *testing.T) {
|
||||||
|
s := newTestSessionStore(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
token := "token"
|
||||||
|
|
||||||
|
require.NoError(t, s.RegisterToken(ctx, token, time.Now().Add(50*time.Millisecond)))
|
||||||
|
|
||||||
|
err := s.RegisterToken(ctx, token, time.Now().Add(50*time.Millisecond))
|
||||||
|
assert.ErrorIs(t, err, ErrTokenAlreadyUsed)
|
||||||
|
|
||||||
|
time.Sleep(120 * time.Millisecond)
|
||||||
|
|
||||||
|
require.NoError(t, s.RegisterToken(ctx, token, time.Now().Add(time.Hour)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHashToken_StableAndDoesNotLeak(t *testing.T) {
|
||||||
|
a := hashToken("tokenA")
|
||||||
|
b := hashToken("tokenB")
|
||||||
|
assert.Equal(t, a, hashToken("tokenA"), "hash must be deterministic")
|
||||||
|
assert.NotEqual(t, a, b, "different tokens must hash differently")
|
||||||
|
assert.Len(t, a, 64, "sha256 hex must be 64 chars")
|
||||||
|
assert.NotContains(t, a, "tokenA", "raw token must not appear in hash")
|
||||||
|
}
|
||||||
@@ -391,7 +391,7 @@ func startManagementForTest(t *testing.T, testFile string, config *config.Config
|
|||||||
return nil, nil, "", cleanup, err
|
return nil, nil, "", cleanup, err
|
||||||
}
|
}
|
||||||
|
|
||||||
mgmtServer, err := nbgrpc.NewServer(config, accountManager, settingsMockManager, jobManager, secretsManager, nil, nil, MockIntegratedValidator{}, networkMapController, nil)
|
mgmtServer, err := nbgrpc.NewServer(config, accountManager, settingsMockManager, jobManager, secretsManager, nil, nil, MockIntegratedValidator{}, networkMapController, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, "", cleanup, err
|
return nil, nil, "", cleanup, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -256,6 +256,7 @@ func startServer(
|
|||||||
server.MockIntegratedValidator{},
|
server.MockIntegratedValidator{},
|
||||||
networkMapController,
|
networkMapController,
|
||||||
nil,
|
nil,
|
||||||
|
nil,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed creating management server: %v", err)
|
t.Fatalf("failed creating management server: %v", err)
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ import (
|
|||||||
|
|
||||||
const remoteJobsMinVer = "0.64.0"
|
const remoteJobsMinVer = "0.64.0"
|
||||||
|
|
||||||
// GetPeers returns a list of peers under the given account filtering out peers that do not belong to a user if
|
// GetPeers returns peers visible to the user within an account.
|
||||||
// the current user is not an admin.
|
// Users with "peers:read" see all peers. Otherwise, users see only their own peers, or none if restricted by account settings.
|
||||||
func (am *DefaultAccountManager) GetPeers(ctx context.Context, accountID, userID, nameFilter, ipFilter string) ([]*nbpeer.Peer, error) {
|
func (am *DefaultAccountManager) GetPeers(ctx context.Context, accountID, userID, nameFilter, ipFilter string) ([]*nbpeer.Peer, error) {
|
||||||
user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthNone, userID)
|
user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthNone, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -46,14 +46,8 @@ func (am *DefaultAccountManager) GetPeers(ctx context.Context, accountID, userID
|
|||||||
return nil, status.NewPermissionValidationError(err)
|
return nil, status.NewPermissionValidationError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
accountPeers, err := am.Store.GetAccountPeers(ctx, store.LockingStrengthNone, accountID, nameFilter, ipFilter)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// @note if the user has permission to read peers it shows all account peers
|
|
||||||
if allowed {
|
if allowed {
|
||||||
return accountPeers, nil
|
return am.Store.GetAccountPeers(ctx, store.LockingStrengthNone, accountID, nameFilter, ipFilter)
|
||||||
}
|
}
|
||||||
|
|
||||||
settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthNone, accountID)
|
settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthNone, accountID)
|
||||||
@@ -65,41 +59,7 @@ func (am *DefaultAccountManager) GetPeers(ctx context.Context, accountID, userID
|
|||||||
return []*nbpeer.Peer{}, nil
|
return []*nbpeer.Peer{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// @note if it does not have permission read peers then only display it's own peers
|
return am.Store.GetUserPeers(ctx, store.LockingStrengthNone, accountID, userID)
|
||||||
peers := make([]*nbpeer.Peer, 0)
|
|
||||||
peersMap := make(map[string]*nbpeer.Peer)
|
|
||||||
|
|
||||||
for _, peer := range accountPeers {
|
|
||||||
if user.Id != peer.UserID {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
peers = append(peers, peer)
|
|
||||||
peersMap[peer.ID] = peer
|
|
||||||
}
|
|
||||||
|
|
||||||
return am.getUserAccessiblePeers(ctx, accountID, peersMap, peers)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (am *DefaultAccountManager) getUserAccessiblePeers(ctx context.Context, accountID string, peersMap map[string]*nbpeer.Peer, peers []*nbpeer.Peer) ([]*nbpeer.Peer, error) {
|
|
||||||
account, err := am.requestBuffer.GetAccountWithBackpressure(ctx, accountID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
approvedPeersMap, err := am.integratedPeerValidator.GetValidatedPeers(ctx, accountID, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetch all the peers that have access to the user's peers
|
|
||||||
for _, peer := range peers {
|
|
||||||
aclPeers, _, _, _ := account.GetPeerConnectionResources(ctx, peer, approvedPeersMap, account.GetActiveGroupUsers())
|
|
||||||
for _, p := range aclPeers {
|
|
||||||
peersMap[p.ID] = p
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return maps.Values(peersMap), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarkPeerConnected marks peer as connected (true) or disconnected (false)
|
// MarkPeerConnected marks peer as connected (true) or disconnected (false)
|
||||||
@@ -1230,7 +1190,8 @@ func peerLoginExpired(ctx context.Context, peer *nbpeer.Peer, settings *types.Se
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPeer for a given accountID, peerID and userID error if not found.
|
// GetPeer returns a peer visible to the user within an account.
|
||||||
|
// Users with "peers:read" permission can access any peer. Otherwise, users can access only their own peer.
|
||||||
func (am *DefaultAccountManager) GetPeer(ctx context.Context, accountID, peerID, userID string) (*nbpeer.Peer, error) {
|
func (am *DefaultAccountManager) GetPeer(ctx context.Context, accountID, peerID, userID string) (*nbpeer.Peer, error) {
|
||||||
peer, err := am.Store.GetPeerByID(ctx, store.LockingStrengthNone, accountID, peerID)
|
peer, err := am.Store.GetPeerByID(ctx, store.LockingStrengthNone, accountID, peerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1255,36 +1216,6 @@ func (am *DefaultAccountManager) GetPeer(ctx context.Context, accountID, peerID,
|
|||||||
return peer, nil
|
return peer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return am.checkIfUserOwnsPeer(ctx, accountID, userID, peer)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (am *DefaultAccountManager) checkIfUserOwnsPeer(ctx context.Context, accountID, userID string, peer *nbpeer.Peer) (*nbpeer.Peer, error) {
|
|
||||||
account, err := am.requestBuffer.GetAccountWithBackpressure(ctx, accountID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
approvedPeersMap, err := am.integratedPeerValidator.GetValidatedPeers(ctx, accountID, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// it is also possible that user doesn't own the peer but some of his peers have access to it,
|
|
||||||
// this is a valid case, show the peer as well.
|
|
||||||
userPeers, err := am.Store.GetUserPeers(ctx, store.LockingStrengthNone, accountID, userID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, p := range userPeers {
|
|
||||||
aclPeers, _, _, _ := account.GetPeerConnectionResources(ctx, p, approvedPeersMap, account.GetActiveGroupUsers())
|
|
||||||
for _, aclPeer := range aclPeers {
|
|
||||||
if aclPeer.ID == peer.ID {
|
|
||||||
return peer, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, status.Errorf(status.Internal, "user %s has no access to peer %s under account %s", userID, peer.ID, accountID)
|
return nil, status.Errorf(status.Internal, "user %s has no access to peer %s under account %s", userID, peer.ID, accountID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -559,25 +559,9 @@ func TestDefaultAccountManager_GetPeer(t *testing.T) {
|
|||||||
}
|
}
|
||||||
assert.NotNil(t, peer)
|
assert.NotNil(t, peer)
|
||||||
|
|
||||||
// the user can see peer2 because peer1 of the user has access to peer2 due to the All group and the default rule 0 all-to-all access
|
// the user can NOT see peer2 because it is not owned by them.
|
||||||
peer, err = manager.GetPeer(context.Background(), accountID, peer2.ID, someUser)
|
// Regular users only see peers they directly own.
|
||||||
if err != nil {
|
_, err = manager.GetPeer(context.Background(), accountID, peer2.ID, someUser)
|
||||||
t.Fatal(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
assert.NotNil(t, peer)
|
|
||||||
|
|
||||||
// delete the all-to-all policy so that user's peer1 has no access to peer2
|
|
||||||
for _, policy := range account.Policies {
|
|
||||||
err = manager.DeletePolicy(context.Background(), accountID, policy.ID, adminUser)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// at this point the user can't see the details of peer2
|
|
||||||
peer, err = manager.GetPeer(context.Background(), accountID, peer2.ID, someUser) //nolint
|
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
|
|
||||||
// admin users can always access all the peers
|
// admin users can always access all the peers
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ func startManagement(t *testing.T) (*grpc.Server, net.Listener) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
mgmtServer, err := nbgrpc.NewServer(config, accountManager, settingsMockManager, jobManager, secretsManager, nil, nil, mgmt.MockIntegratedValidator{}, networkMapController, nil)
|
mgmtServer, err := nbgrpc.NewServer(config, accountManager, settingsMockManager, jobManager, secretsManager, nil, nil, mgmt.MockIntegratedValidator{}, networkMapController, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user