From 5eee1be01fbab804d3dede8a060425d0989551e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Kr=C3=BCger?= Date: Thu, 10 Jul 2025 10:22:19 +0200 Subject: [PATCH 1/2] feat: Enhance functionality and validation for Tailscale integration, add internet connection check, and improve startup error handling --- .github/CODEOWNERS | 4 +++ .github/FUNDING.yml | 4 +++ .github/copilot-instructions.md | 30 +++++++++++++++++++ .github/workflows/auto-label.yml | 51 ++++++++++++++++++++++++++++++++ .github/workflows/gosec.yaml | 30 +++++++++++++++++++ .vscode/launch.json | 18 +++++++++++ .vscode/tasks.json | 16 ++++++++++ main.go | 2 +- network.go | 27 +++++++++++++++++ startup.go | 5 +++- tailscale.go | 44 +++++++++++++++++++++++---- tray.go | 26 ++++++++++++++-- update.go | 16 +++++++++- 13 files changed, 262 insertions(+), 11 deletions(-) create mode 100644 .github/CODEOWNERS create mode 100644 .github/workflows/auto-label.yml create mode 100644 .github/workflows/gosec.yaml create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..b5f0579 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: MIT +# Copyright (C) 2025 Andreas Krüger + +* @woopstar diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 24e2ac2..8b22ed3 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1,5 @@ +# SPDX-License-Identifier: MIT +# Copyright (C) 2025 Andreas Krüger + +github: woopstar buy_me_a_coffee: woopstar diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 9f88936..945cd39 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,5 +1,29 @@ # Custom GitHub Copilot Instructions +## Conventional Commits +- Always use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) for commit messages and pull request titles. + +## Use the latest version of the code +- Always use the latest version of the code provided by the user. If the user provides a file, use that file as the base for your changes. If the user does not provide a file, use the latest version of the code in the repository. + +## Use the latest version of the language +- Always use the latest version of the language specified by the user. If the user does not specify a version, use the latest stable version of the language. + +## Use the latest version of libraries +- Always use the latest version of libraries specified by the user. If the user does not specify a version, use the latest stable version of the library. + +## Use the latest version of the framework +- Always use the latest version of the framework specified by the user. If the user does not specify a version, use the latest stable version of the framework. + +## Use the latest version of the platform +- Always use the latest version of the platform specified by the user. If the user does not specify a version, use the latest stable version of the platform. + +## Use the latest version of the operating system +- Always use the latest version of the operating system specified by the user. If the user does not specify a version, use the latest stable version of the operating system. + +## Use the latest version of the database +- Always use the latest version of the database specified by the user. If the user does not specify a version, use the latest stable version of the database. + ## Avoid triggering public code warnings - Avoid generating code verbatim from public code examples. Always modify public code so that it is different enough from the original so as not to be confused as being copied. When you do so, provide a footnote to the user informing them. @@ -29,6 +53,12 @@ - For libraries or external dependencies, mention their usage and purpose in comments. - Use consistent naming conventions and follow language-specific best practices. - Write concise, efficient, and idiomatic code that is also easily understandable. +- Use meaningful variable and function names that reflect their purpose. +- Include comments for complex logic or non-obvious code sections. +- Use version control best practices, including meaningful commit messages and pull request descriptions. +- Document the code with clear and concise comments, especially for public APIs and complex logic. +- Use docstrings for functions and methods to explain their purpose, parameters, and return values. +- Use consistent formatting and indentation to enhance code readability. ## Edge Cases and Testing diff --git a/.github/workflows/auto-label.yml b/.github/workflows/auto-label.yml new file mode 100644 index 0000000..6452c8d --- /dev/null +++ b/.github/workflows/auto-label.yml @@ -0,0 +1,51 @@ +# SPDX-License-Identifier: MIT +# Copyright (C) 2025 Andreas Krüger + +name: Auto Label PRs from Conventional Commits + +on: + pull_request: + branches: [main] + types: + [opened, reopened, labeled, unlabeled] + workflow_dispatch: + +permissions: read-all + +jobs: + label: + name: Assign PR Labels + runs-on: ubuntu-latest + permissions: write-all + if: github.event.pull_request.merged == false + steps: + - uses: actions/checkout@v4 + + - name: Execute assign labels + id: action-assign-labels + uses: mauroalderete/action-assign-labels@v1 + with: + pull-request-number: ${{ github.event.pull_request.number }} + github-token: ${{ secrets.GITHUB_TOKEN }} + conventional-commits: | + conventional-commits: + - type: 'fix' + nouns: ['FIX', 'Fix', 'fix', 'FIXED', 'Fixed', 'fixed'] + labels: ['bug'] + - type: 'feature' + nouns: ['FEATURE', 'Feature', 'feature', 'FEAT', 'Feat', 'feat'] + labels: ['feature'] + - type: 'breaking_change' + nouns: ['BREAKING CHANGE', 'BREAKING', 'MAJOR'] + labels: ['BREAKING CHANGE'] + - type: 'documentation' + nouns: ['doc','docu','document','documentation'] + labels: ['documentation'] + - type: 'build' + nouns: ['build','rebuild'] + labels: ['build'] + - type: 'config' + nouns: ['config', 'conf', 'cofiguration', 'configure'] + labels: ['config'] + maintain-labels-not-matched: false + apply-changes: true diff --git a/.github/workflows/gosec.yaml b/.github/workflows/gosec.yaml new file mode 100644 index 0000000..94f293e --- /dev/null +++ b/.github/workflows/gosec.yaml @@ -0,0 +1,30 @@ +name: "Gosec Security Scan" + +permissions: read-all + +# Run workflow each time code is pushed to your repository and on a schedule. +# The scheduled workflow runs every at 00:00 on Sunday UTC time. +on: + push: + schedule: + - cron: '0 0 * * 0' + +jobs: + gosec: + runs-on: ubuntu-latest + permissions: write-all + env: + GO111MODULE: on + steps: + - name: Checkout Source + uses: actions/checkout@v3 + - name: Run Gosec Security Scanner + uses: securego/gosec@master + with: + # we let the report trigger content trigger a failure using the GitHub Security features. + args: '-no-fail -fmt sarif -out results.sarif ./...' + - name: Upload SARIF file + uses: github/codeql-action/upload-sarif@v2 + with: + # Path to SARIF file relative to the root of the repository + sarif_file: results.sarif diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..c518a39 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,18 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + + { + "name": "Launch", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}", + "env": {}, + "args": [] + } + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..0a1de9e --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,16 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "Build Go", + "type": "shell", + "command": "go build -ldflags=\"-H windowsgui\" -o AutoExitNode.exe", + "group": { + "kind": "build", + "isDefault": true + } + } + ] +} diff --git a/main.go b/main.go index 9d51107..f672bcf 100644 --- a/main.go +++ b/main.go @@ -27,7 +27,7 @@ var startupDir = os.Getenv("APPDATA") + `\Microsoft\Windows\Start Menu\Programs\ func main() { loadConfig() - tailscaleAvailable = checkTailscaleExists() + tailscaleAvailable = isValidTailscalePath(tailscalePath) systray.Run(autoExitNote, nil) } diff --git a/network.go b/network.go index 56151ae..4a480dd 100644 --- a/network.go +++ b/network.go @@ -2,10 +2,13 @@ package main import ( "errors" + "fmt" + "net" "os/exec" "regexp" "strings" "syscall" + "time" ) // getCurrentSSID returns the current WiFi SSID or an error if not found. @@ -51,3 +54,27 @@ func isSSIDTrusted(ssid string) bool { } return false } + +// hasInternetConnection checks if the device has an active internet connection. +func hasInternetConnection() bool { + timeout := 2 * time.Second + conn, err := net.DialTimeout("tcp", "8.8.8.8:53", timeout) + if err != nil { + conn, err := net.DialTimeout("tcp", "1.1.1.1:53", timeout) + if err != nil { + if conn != nil { + if cerr := conn.Close(); cerr != nil { + // Log error if closing connection fails + fmt.Println("Error closing connection:", cerr) + } + } + return false + } + } + if conn != nil { + if cerr := conn.Close(); cerr != nil { + fmt.Println("Error closing connection:", cerr) + } + } + return true +} diff --git a/startup.go b/startup.go index 58b196c..4258454 100644 --- a/startup.go +++ b/startup.go @@ -22,7 +22,10 @@ func isStartupEnabled() bool { // addStartupShortcut creates a Windows shortcut for autostart. func addStartupShortcut() { - ole.CoInitialize(0) + if err := ole.CoInitialize(0); err != nil { + fmt.Println("CoInitialize error:", err) + return + } defer ole.CoUninitialize() exePath, err := os.Executable() diff --git a/tailscale.go b/tailscale.go index 6d1ec19..2237fcd 100644 --- a/tailscale.go +++ b/tailscale.go @@ -4,13 +4,32 @@ import ( "fmt" "os" "os/exec" + "path/filepath" + "regexp" "syscall" ) -// checkTailscaleExists returns true if the Tailscale binary exists. -func checkTailscaleExists() bool { - _, err := os.Stat(tailscalePath) - return err == nil +// isValidTailscalePath checks if the path is absolute and points to an .exe file in Program Files. +// isValidTailscalePath checks if the path is absolute, points to tailscale.exe in Program Files, and is executable. +func isValidTailscalePath(path string) bool { + abs, err := filepath.Abs(path) + if err != nil { + return false + } + + // Check if file exists and is executable + info, err := os.Stat(abs) + if err != nil || info.IsDir() { + return false + } + + // On Windows, check for .exe extension and that the file is not a directory + return filepath.Ext(abs) == ".exe" +} + +// isValidExitNodeName ensures the exit node name is alphanumeric, dash or underscore (no spaces or shell metacharacters). +func isValidExitNodeName(name string) bool { + return regexp.MustCompile(`^[a-zA-Z0-9\-_]+$`).MatchString(name) } // getExitNodeName returns the first exit node from config or a default. @@ -22,10 +41,20 @@ func getExitNodeName() string { } // activateExitNode runs tailscale up with exit node. +// #nosec G204 func activateExitNode() { + if !isValidTailscalePath(tailscalePath) { + fmt.Println("Unsafe tailscalePath, aborting command") + return + } + exitNode := getExitNodeName() + if !isValidExitNodeName(exitNode) { + fmt.Println("Unsafe exit node name, aborting command") + return + } cmd := exec.Command(tailscalePath, "up", - "--exit-node="+getExitNodeName(), + "--exit-node="+exitNode, "--accept-dns=true", "--shields-up") cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} @@ -37,7 +66,12 @@ func activateExitNode() { } // deactivateExitNode disables exit node. +// #nosec G204 func deactivateExitNode() { + if !isValidTailscalePath(tailscalePath) { + fmt.Println("Unsafe tailscalePath, aborting command") + return + } cmd := exec.Command(tailscalePath, "up", "--exit-node=", diff --git a/tray.go b/tray.go index 25a3091..ea075ee 100644 --- a/tray.go +++ b/tray.go @@ -1,8 +1,9 @@ package main import ( + "crypto/rand" "fmt" - "math/rand" + "math/big" "time" "github.com/getlantern/systray" @@ -13,7 +14,19 @@ var lastCellular bool var lastCommand string var tailscaleAvailable = true var checkInterval = 15 * time.Second -var updateInterval = time.Duration(rand.Intn(60)+1) * time.Minute + +// Use crypto/rand for secure random interval +var updateInterval = getSecureRandomInterval() + +// getSecureRandomInterval returns a random duration between 1 and 60 minutes using crypto/rand. +func getSecureRandomInterval() time.Duration { + n, err := rand.Int(rand.Reader, big.NewInt(60)) + if err != nil { + // fallback to 15 minutes if crypto/rand fails + return 15 * time.Minute + } + return time.Duration(n.Int64()+1) * time.Minute +} func autoExitNote() { mStatus := systray.AddMenuItem("Status: Initializing...", "Current network status") @@ -86,6 +99,7 @@ func checkAndApply(mStatus *systray.MenuItem) { } ssid, err := getCurrentSSID() cell := isCellularConnected() + online := hasInternetConnection() statusText := "" tooltip := "" @@ -103,6 +117,12 @@ func checkAndApply(mStatus *systray.MenuItem) { tooltip = fmt.Sprintf("Active: %s (unknown network)", getExitNodeName()) icon = iconActive command = "activated" + case !online: + statusText = "No Internet" + tooltip = fmt.Sprintf("Active: %s (unknown network)", getExitNodeName()) + icon = iconActive + command = "activated" + activateExitNode() case isSSIDTrusted(ssid): statusText = fmt.Sprintf("Trusted SSID: %s", ssid) tooltip = fmt.Sprintf("Inactive: trusted network (%s)", ssid) @@ -119,7 +139,7 @@ func checkAndApply(mStatus *systray.MenuItem) { systray.SetIcon(icon) systray.SetTooltip(tooltip) - if ssid == lastSSID && cell == lastCellular && lastCommand == command { + if ssid == lastSSID && cell == lastCellular && lastCommand == command && online { return } lastSSID = ssid diff --git a/update.go b/update.go index 16ff84d..c8922af 100644 --- a/update.go +++ b/update.go @@ -53,9 +53,23 @@ func checkForUpdate(cb func(version, url string)) { } } +// isSafeForPowerShell checks for dangerous characters that could break out of the PowerShell string literal. +func isSafeForPowerShell(s string) bool { + // Disallow newlines and backticks, which can break PowerShell string literals + return !strings.ContainsAny(s, "`\r\n") +} + // showWindowsNotification displays a notification on Windows using PowerShell. +// Input is sanitized and validated to prevent command injection. +// #nosec G204 func showWindowsNotification(title, message string) { - cmd := exec.Command("powershell", "-Command", fmt.Sprintf(`Add-Type -AssemblyName PresentationFramework;[System.Windows.MessageBox]::Show('%s', '%s')`, escapeForPowerShell(message), escapeForPowerShell(title))) + if !isSafeForPowerShell(title) || !isSafeForPowerShell(message) { + fmt.Println("Unsafe notification input detected, aborting notification") + return + } + cmd := exec.Command("powershell", "-Command", + fmt.Sprintf(`Add-Type -AssemblyName PresentationFramework;[System.Windows.MessageBox]::Show('%s', '%s')`, + escapeForPowerShell(message), escapeForPowerShell(title))) cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} if err := cmd.Run(); err != nil { fmt.Println("showWindowsNotification: failed to show popup:", err) From 234754a4792715a0fc4535393a9bfed8240310c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Kr=C3=BCger?= Date: Thu, 10 Jul 2025 10:27:34 +0200 Subject: [PATCH 2/2] fix: Reduce check interval from 15 seconds to 5 seconds for improved responsiveness --- tray.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tray.go b/tray.go index ea075ee..3970f46 100644 --- a/tray.go +++ b/tray.go @@ -13,7 +13,7 @@ var lastSSID string var lastCellular bool var lastCommand string var tailscaleAvailable = true -var checkInterval = 15 * time.Second +var checkInterval = 5 * time.Second // Use crypto/rand for secure random interval var updateInterval = getSecureRandomInterval()