A simple cross-platform GUI application to quickly toggle your Tailscale exit node on and off.
- π One-click toggle for Tailscale exit node
- π¨ Modern dark UI (Windows/Linux) / Native menu bar (macOS)
- π Real-time connection status
- π₯οΈ System tray support
- Windows/Linux: Left-click to toggle, Right-click for menu
- macOS: Native menu bar integration
- π Start with system option
- β‘ Lightweight - minimal dependencies
- Tailscale installed (
sudo pacman -S tailscale) - Python 3.10 or higher
- PyQt5 (Recommended for KDE/System Tray support)
- Arch:
sudo pacman -S python-pyqt5 - Ubuntu/Debian:
sudo apt install python3-pyqt5
- Arch:
-
Clone and Setup
git clone https://github.com/yourusername/ExitNodeToggle.git cd ExitNodeToggle python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txt
-
Configure Create
config.json(or copyconfig.linux.json) and set your specific exit node IP. Fortailscale_exe, use"tailscale"if it's in your system's PATH, or its full path like"/usr/bin/tailscale".{ "tailscale_exe": "/usr/bin/tailscale", "exit_node_ip": "100.64.10.100" } -
Run
python main_linux.py
-
Run Build Script
./build_linux.sh
-
Install The executable will be in
dist/ExitNodeToggle. You can move this anywhere, but ensureconfig.jsonis in the same directory (or~/.config/exitnodetoggle/config.json).
- Main Window: Control panel with status indicator.
- System Tray:
- Left Click: Immediately toggles Exit Node ON/OFF.
- Right Click: Opens menu (Show Window, Toggle, Quit).
The system tray icons (On/Off) are dynamically generated at runtime using the Pillow library to ensure consistent styling and avoid external asset dependencies. These generated icons are stored temporarily in the application's log directory (~/.local/state/exitnodetoggle/).
| Color | Status |
|---|---|
| β« Grey | Exit node OFF (direct connection) |
| π΄ Red | Exit node ON (routing via exit node) |
- Tailscale installed on Windows
- Python 3.10 or higher (for running from source)
- An exit node configured in your Tailscale network
-
Install dependencies
pip install pystray pillow
-
Configure your exit node
Edit
config.json:{ "tailscale_exe": "C:\\Program Files\\Tailscale\\tailscale.exe", "exit_node_ip": "100.64.10.100" } -
Run the application
python main.py
-
Install dependencies
pip install pystray pillow pyinstaller
-
Build the EXE
build.bat
Or manually:
pyinstaller --onefile --windowed --name "ExitNodeToggle" main.py -
Copy
config.jsonto the same folder asExitNodeToggle.exe -
Run
ExitNodeToggle.exe
- Click the button to toggle exit node ON/OFF
- Check "Start with Windows" to auto-launch on boot
- Click X to minimize to system tray
| Action | Result |
|---|---|
| Left-click | Toggle exit node ON/OFF |
| Right-click | Show menu |
| Color | Status |
|---|---|
| β« Grey | Exit node OFF (direct connection) |
| π΄ Red | Exit node ON (routing via exit node) |
- Tailscale installed on macOS
- Python 3.10 or higher (for running from source)
- An exit node configured in your Tailscale network
-
Install dependencies
pip install rumps
-
Configure your exit node
Copy and edit
config.macos.jsontoconfig.json(the CLI pathtailscaleusually works best on macOS):{ "tailscale_exe": "tailscale", "exit_node_ip": "100.64.10.100" } -
Run the application
python main_macos.py
-
Run the build script
chmod +x build_macos.sh ./build_macos.sh
This will:
- Create a virtual environment
- Install dependencies (rumps, py2app)
- Build
Exit Node Toggle.app - Optionally create a DMG installer
- Bake in the working Tailscale CLI path (prefers
tailscaleon PATH) and default exit node IP
-
The app will be in
dist/Exit Node Toggle.app -
Configuration inside the app bundle
- Tailscale binary: auto-detected during build (CLI preferred).
- Exit node IP: defaults to
100.64.10.100(override by settingEXIT_NODE_IPbefore running the build script). - File:
dist/Exit Node Toggle.app/Contents/Resources/config.json
The app sits in your menu bar (top right of screen).
| Icon | Status |
|---|---|
| π | Exit node OFF (direct connection) |
| π | Exit node ON (routing via exit node) |
- Status - Shows current connection state
- Toggle Exit Node - Switch on/off
- Node: xxx.xxx.xxx.xxx - Shows configured exit node
- Start at Login - Enable/disable auto-start
- Quit - Exit the application
- Open the DMG file
- Drag
Exit Node Toggle.appto Applications - Open from Applications
- Configure your exit node:
- Right-click the app β Show Package Contents
- Navigate to
Contents/Resources/config.json - Edit with your exit node IP
tailscale statusLook for the device you want to use as exit node and copy its IP (starts with 100.).
For any issues, please check the application logs located at ~/.local/state/exitnodetoggle/app.log (on Linux/macOS) or %LOCALAPPDATA%\exitnodetoggle\app.log (on Windows). These logs provide detailed information that can help diagnose problems.
"Tray icon not responding"
- Ensure you have
PyQt5installed. The app uses native Qt system tray integration for best results on KDE. - If using GNOME, ensure you have AppIndicator support enabled (though Qt fallback usually works).
"Tray icon not showing after packaging (makepkg)"
- Problem: The packaged application might not display the system tray icon, even though
main_linux.pyworks when run directly. This was due to a mismatch in the tray capability check (main_linux.pywas checking forgi(AppIndicator/GTK) while the tray implementation usesPyQt5) and PyInstaller not always correctly detecting and bundlingPyQt5when dynamically imported. - Solution: Ensure your
PKGBUILDandbuild_linux.shexplicitly includePyQt5using--hidden-import PyQt5in the PyInstaller command. The application logic has been updated to check forPyQt5directly.
"Cannot enable exit node after packaging (makepkg)"
- Problem: After building with
makepkg, the application could disable the exit node but failed to enable it. This happened because theexit_node_ipwas missing from the configuration. ThePKGBUILDwas not bundlingconfig.jsoninto the executable, leading the app to start with an invalid configuration. - Solution: Ensure your
PKGBUILDexplicitly bundlesconfig.jsonusing--add-data "config.json:."in the PyInstaller command. The application will now find theexit_node_ipfrom the bundledconfig.json. - Note: For persistent configuration, it is recommended to create a
config.jsonfile in~/.config/exitnodetoggle/with your desiredexit_node_ip. This user-specific file will take precedence over the bundled configuration.
"Permission Denied"
- Ensure your user can run
tailscalecommands (add user totailscalegroup if applicable, or usesudoviatailscale_exewrapper if strictly required, though typicallytailscale setworks for users in the operator group).
"Tailscale not found"
- Verify Tailscale is installed
- Check the path in
config.jsonmatches your installation
"Operation failed"
- Ensure Tailscale is running and logged in
- Check that your exit node is online
EXE doesn't start
- Make sure
config.jsonis in the same folder as the EXE
"Tailscale not found"
- Verify Tailscale is installed in Applications
- Try setting
tailscale_exeto just"tailscale"if you have the CLI version
App doesn't appear in menu bar
- Check for notification requesting permissions
- Go to System Settings β Privacy & Security β Accessibility and allow the app
DMG build fails
- Ensure you have Xcode Command Line Tools:
xcode-select --install
ExitNodeToggle/
βββ main.py # Windows version (tkinter + pystray)
βββ main_macos.py # macOS version (rumps menu bar)
βββ main_linux.py # Linux version (tkinter + PyQt5 Tray)
βββ config.json # Your configuration
βββ config.macos.json # macOS config template
βββ config.linux.json # Linux config template
βββ requirements.txt # Python dependencies
βββ build.bat # Windows build script
βββ build_macos.sh # macOS build script
βββ build_linux.sh # Linux build script
βββ setup_macos.py # py2app configuration
βββ README.md # This file
MIT License - feel free to modify and distribute.