forked from kenzuya/unshackle
feat(gluetun): improve VPN connection display and Windscribe support
This commit is contained in:
@@ -11,6 +11,7 @@ repos:
|
||||
rev: v0.4.0
|
||||
hooks:
|
||||
- id: poetry-ruff-check
|
||||
args: [--fix]
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: 6.0.1
|
||||
hooks:
|
||||
|
||||
@@ -1,526 +1,136 @@
|
||||
# VPN-to-HTTP Proxy Bridge for Unshackle
|
||||
# VPN Proxy Setup for Unshackle
|
||||
|
||||
## Overview
|
||||
|
||||
This guide explains how to use **Gluetun** - a Docker-based VPN client that creates an isolated HTTP proxy from VPN services (including WireGuard). This allows Unshackle to use VPN providers like ExpressVPN, Windscribe, NordVPN, and many others without affecting your system's normal internet connection.
|
||||
Unshackle has **native Gluetun integration** that automatically creates and manages Docker containers to bridge VPN connections to HTTP proxies. Simply configure your VPN credentials in `unshackle.yaml` and use `--proxy gluetun:<provider>:<region>`.
|
||||
|
||||
> **Note**: Unshackle now has **native Gluetun integration**! You can use `--proxy gluetun:windscribe:us` directly without manual Docker setup. See [CONFIG.md](../CONFIG.md#gluetun-dict) for configuration. The guide below is for advanced users who want to manage Gluetun containers manually.
|
||||
## Why Use VPN Proxies?
|
||||
|
||||
## Why This Approach?
|
||||
- **Network Isolation**: VPN runs in a Docker container, doesn't affect your system's internet
|
||||
- **Easy Switching**: Switch between regions without reconfiguring anything
|
||||
- **Multiple Regions**: Use different VPN locations for different downloads
|
||||
|
||||
- **Network Isolation**: VPN connection runs in Docker container, doesn't affect host system
|
||||
- **HTTP Proxy Interface**: Exposes standard HTTP proxy that Unshackle can use directly
|
||||
- **WireGuard Support**: Modern, fast, and secure VPN protocol
|
||||
- **Kill Switch**: Built-in protection prevents IP leaks if VPN disconnects
|
||||
- **Multi-Provider**: Supports 50+ VPN providers out of the box
|
||||
- **Cross-Platform**: Works on Linux and Windows (via Docker Desktop or WSL2)
|
||||
## Requirements
|
||||
|
||||
## Supported VPN Providers
|
||||
- Docker must be installed and running
|
||||
- Verify with: `unshackle env check`
|
||||
|
||||
Gluetun supports many providers including:
|
||||
- ExpressVPN
|
||||
- Windscribe
|
||||
- NordVPN
|
||||
- Surfshark
|
||||
- ProtonVPN
|
||||
- Private Internet Access
|
||||
- Mullvad
|
||||
- And 50+ more
|
||||
## Configuration
|
||||
|
||||
Full list: https://github.com/qdm12/gluetun/wiki
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Linux
|
||||
```bash
|
||||
# Install Docker
|
||||
curl -fsSL https://get.docker.com -o get-docker.sh
|
||||
sudo sh get-docker.sh
|
||||
sudo usermod -aG docker $USER
|
||||
# Log out and back in for group changes to take effect
|
||||
```
|
||||
|
||||
### Windows
|
||||
- Install [Docker Desktop for Windows](https://www.docker.com/products/docker-desktop/)
|
||||
- Enable WSL2 backend (recommended)
|
||||
|
||||
## Setup Instructions
|
||||
|
||||
### 1. Create Gluetun Configuration Directory
|
||||
|
||||
```bash
|
||||
mkdir -p ~/gluetun-config
|
||||
cd ~/gluetun-config
|
||||
```
|
||||
|
||||
### 2. Create Docker Compose File
|
||||
|
||||
Create `docker-compose.yml` with your VPN provider configuration:
|
||||
|
||||
#### Example: Windscribe with WireGuard
|
||||
Add your VPN provider credentials to `unshackle.yaml`:
|
||||
|
||||
```yaml
|
||||
version: "3"
|
||||
services:
|
||||
gluetun:
|
||||
image: qmcgaw/gluetun:latest
|
||||
container_name: gluetun
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
devices:
|
||||
- /dev/net/tun:/dev/net/tun
|
||||
ports:
|
||||
- 8888:8888/tcp # HTTP proxy
|
||||
- 8388:8388/tcp # Shadowsocks (optional)
|
||||
- 8388:8388/udp # Shadowsocks (optional)
|
||||
environment:
|
||||
# VPN Provider Settings
|
||||
- VPN_SERVICE_PROVIDER=windscribe
|
||||
- VPN_TYPE=wireguard
|
||||
gluetun:
|
||||
base_port: 8888 # Starting port for HTTP proxies
|
||||
auto_cleanup: true # Remove containers when done
|
||||
container_prefix: "unshackle-gluetun"
|
||||
verify_ip: true # Verify VPN IP matches expected region
|
||||
|
||||
# Get these from your Windscribe account
|
||||
- WIREGUARD_PRIVATE_KEY=your_private_key_here
|
||||
- WIREGUARD_ADDRESSES=your_address_here
|
||||
- WIREGUARD_PRESHARED_KEY=your_preshared_key_here # if applicable
|
||||
providers:
|
||||
windscribe:
|
||||
vpn_type: openvpn
|
||||
credentials:
|
||||
username: "YOUR_OPENVPN_USERNAME"
|
||||
password: "YOUR_OPENVPN_PASSWORD"
|
||||
server_countries:
|
||||
us: US
|
||||
uk: GB
|
||||
ca: CA
|
||||
|
||||
# Server location (optional)
|
||||
- SERVER_COUNTRIES=US
|
||||
# or specific city
|
||||
# - SERVER_CITIES=New York
|
||||
|
||||
# HTTP Proxy Settings
|
||||
- HTTPPROXY=on
|
||||
- HTTPPROXY_LOG=on
|
||||
- HTTPPROXY_LISTENING_ADDRESS=:8888
|
||||
|
||||
# Timezone
|
||||
- TZ=America/New_York
|
||||
|
||||
# Logging
|
||||
- LOG_LEVEL=info
|
||||
|
||||
restart: unless-stopped
|
||||
|
||||
# Health check
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--spider", "-q", "https://api.ipify.org"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
nordvpn:
|
||||
vpn_type: openvpn
|
||||
credentials:
|
||||
username: "YOUR_SERVICE_USERNAME"
|
||||
password: "YOUR_SERVICE_PASSWORD"
|
||||
server_countries:
|
||||
us: US
|
||||
de: DE
|
||||
```
|
||||
|
||||
#### Example: ExpressVPN with WireGuard
|
||||
## Getting Your VPN Credentials
|
||||
|
||||
```yaml
|
||||
version: "3"
|
||||
services:
|
||||
gluetun:
|
||||
image: qmcgaw/gluetun:latest
|
||||
container_name: gluetun
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
devices:
|
||||
- /dev/net/tun:/dev/net/tun
|
||||
ports:
|
||||
- 8888:8888/tcp # HTTP proxy
|
||||
environment:
|
||||
- VPN_SERVICE_PROVIDER=expressvpn
|
||||
- VPN_TYPE=wireguard
|
||||
### Windscribe
|
||||
|
||||
# Get these from ExpressVPN's WireGuard configuration
|
||||
- WIREGUARD_PRIVATE_KEY=your_private_key_here
|
||||
- WIREGUARD_ADDRESSES=your_address_here
|
||||
1. Go to [windscribe.com/getconfig/openvpn](https://windscribe.com/getconfig/openvpn)
|
||||
2. Generate a config file for any location
|
||||
3. Copy the username and password shown
|
||||
|
||||
- HTTPPROXY=on
|
||||
- HTTPPROXY_LISTENING_ADDRESS=:8888
|
||||
- TZ=America/New_York
|
||||
- LOG_LEVEL=info
|
||||
> **Note**: Windscribe uses region names like "US East" instead of country codes. Unshackle automatically converts codes like `us`, `ca`, `uk` to the correct region names.
|
||||
|
||||
restart: unless-stopped
|
||||
```
|
||||
### NordVPN
|
||||
|
||||
#### Example: NordVPN with WireGuard
|
||||
|
||||
```yaml
|
||||
version: "3"
|
||||
services:
|
||||
gluetun:
|
||||
image: qmcgaw/gluetun:latest
|
||||
container_name: gluetun
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
devices:
|
||||
- /dev/net/tun:/dev/net/tun
|
||||
ports:
|
||||
- 8888:8888/tcp # HTTP proxy
|
||||
environment:
|
||||
- VPN_SERVICE_PROVIDER=nordvpn
|
||||
- VPN_TYPE=wireguard
|
||||
|
||||
# NordVPN token (get from NordVPN dashboard)
|
||||
- WIREGUARD_PRIVATE_KEY=your_private_key_here
|
||||
- WIREGUARD_ADDRESSES=your_address_here
|
||||
|
||||
- SERVER_COUNTRIES=US
|
||||
|
||||
- HTTPPROXY=on
|
||||
- HTTPPROXY_LISTENING_ADDRESS=:8888
|
||||
- TZ=America/New_York
|
||||
- LOG_LEVEL=info
|
||||
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
### 3. Getting Your WireGuard Credentials
|
||||
|
||||
#### Windscribe
|
||||
1. Log into Windscribe account
|
||||
2. Go to "My Account" → "WireGuard"
|
||||
3. Generate a config file for your desired location
|
||||
4. Extract the private key and addresses from the config
|
||||
|
||||
#### ExpressVPN
|
||||
1. Log into ExpressVPN
|
||||
2. Navigate to the manual configuration section
|
||||
3. Select WireGuard and download the configuration
|
||||
4. Extract credentials from the config file
|
||||
|
||||
#### NordVPN
|
||||
1. Log into NordVPN dashboard
|
||||
2. Go to Services → NordVPN → Manual setup
|
||||
3. Generate WireGuard credentials
|
||||
4. Copy the private key and addresses
|
||||
2. Go to Services > NordVPN > Manual setup
|
||||
3. Copy your service credentials (not your account email/password)
|
||||
|
||||
### 4. Start Gluetun Container
|
||||
### Other Providers
|
||||
|
||||
Gluetun supports 50+ VPN providers. See the [Gluetun Wiki](https://github.com/qdm12/gluetun-wiki/tree/main/setup/providers) for provider-specific setup instructions.
|
||||
|
||||
## Usage
|
||||
|
||||
Use the `--proxy` flag with the format `gluetun:<provider>:<region>`:
|
||||
|
||||
```bash
|
||||
cd ~/gluetun-config
|
||||
docker-compose up -d
|
||||
# Connect via Windscribe to US
|
||||
unshackle dl SERVICE CONTENT_ID --proxy gluetun:windscribe:us
|
||||
|
||||
# Connect via NordVPN to Germany
|
||||
unshackle dl SERVICE CONTENT_ID --proxy gluetun:nordvpn:de
|
||||
```
|
||||
|
||||
Check logs to verify connection:
|
||||
```bash
|
||||
docker logs gluetun -f
|
||||
```
|
||||
Unshackle will automatically:
|
||||
|
||||
You should see messages indicating successful VPN connection and HTTP proxy starting on port 8888.
|
||||
|
||||
### 5. Test the Proxy
|
||||
|
||||
```bash
|
||||
# Test that the proxy works
|
||||
curl -x http://localhost:8888 https://api.ipify.org
|
||||
|
||||
# This should show your VPN's IP address, not your real IP
|
||||
```
|
||||
|
||||
## Integrating with Unshackle
|
||||
|
||||
### Option 1: Using Basic Proxy Configuration
|
||||
|
||||
Add to your Unshackle config (`~/.config/unshackle/config.yaml`):
|
||||
|
||||
```yaml
|
||||
proxies:
|
||||
Basic:
|
||||
us: "http://localhost:8888"
|
||||
uk: "http://localhost:8888" # if you have multiple Gluetun containers
|
||||
```
|
||||
|
||||
Then use in Unshackle:
|
||||
```bash
|
||||
uv run unshackle dl SERVICE_NAME CONTENT_ID --proxy us
|
||||
```
|
||||
|
||||
### Option 2: Creating Multiple VPN Proxy Containers
|
||||
|
||||
You can run multiple Gluetun containers for different regions:
|
||||
|
||||
**gluetun-us.yml:**
|
||||
```yaml
|
||||
version: "3"
|
||||
services:
|
||||
gluetun-us:
|
||||
image: qmcgaw/gluetun:latest
|
||||
container_name: gluetun-us
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
devices:
|
||||
- /dev/net/tun:/dev/net/tun
|
||||
ports:
|
||||
- 8888:8888/tcp # HTTP proxy
|
||||
environment:
|
||||
- VPN_SERVICE_PROVIDER=windscribe
|
||||
- VPN_TYPE=wireguard
|
||||
- SERVER_COUNTRIES=US
|
||||
- WIREGUARD_PRIVATE_KEY=your_key
|
||||
- WIREGUARD_ADDRESSES=your_address
|
||||
- HTTPPROXY=on
|
||||
- HTTPPROXY_LISTENING_ADDRESS=:8888
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
**gluetun-uk.yml:**
|
||||
```yaml
|
||||
version: "3"
|
||||
services:
|
||||
gluetun-uk:
|
||||
image: qmcgaw/gluetun:latest
|
||||
container_name: gluetun-uk
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
devices:
|
||||
- /dev/net/tun:/dev/net/tun
|
||||
ports:
|
||||
- 8889:8888/tcp # Different host port
|
||||
environment:
|
||||
- VPN_SERVICE_PROVIDER=windscribe
|
||||
- VPN_TYPE=wireguard
|
||||
- SERVER_COUNTRIES=GB
|
||||
- WIREGUARD_PRIVATE_KEY=your_key
|
||||
- WIREGUARD_ADDRESSES=your_address
|
||||
- HTTPPROXY=on
|
||||
- HTTPPROXY_LISTENING_ADDRESS=:8888
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
Then in Unshackle config:
|
||||
```yaml
|
||||
proxies:
|
||||
Basic:
|
||||
us: "http://localhost:8888"
|
||||
uk: "http://localhost:8889"
|
||||
ca: "http://localhost:8890"
|
||||
```
|
||||
|
||||
### Option 3: Using with Authentication (Recommended for Security)
|
||||
|
||||
Add authentication to your Gluetun proxy:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
- HTTPPROXY=on
|
||||
- HTTPPROXY_LISTENING_ADDRESS=:8888
|
||||
- HTTPPROXY_USER=myusername
|
||||
- HTTPPROXY_PASSWORD=mypassword
|
||||
```
|
||||
|
||||
Then in Unshackle config:
|
||||
```yaml
|
||||
proxies:
|
||||
Basic:
|
||||
us: "http://myusername:mypassword@localhost:8888"
|
||||
```
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Port Forwarding (for torrenting services)
|
||||
|
||||
Some VPN providers support port forwarding:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
- VPN_PORT_FORWARDING=on
|
||||
- VPN_PORT_FORWARDING_LISTENING_PORT=8000
|
||||
```
|
||||
|
||||
### SOCKS5 Proxy (Alternative to HTTP)
|
||||
|
||||
Gluetun also supports SOCKS5 proxy:
|
||||
|
||||
```yaml
|
||||
ports:
|
||||
- 1080:1080/tcp # SOCKS5 proxy
|
||||
environment:
|
||||
- SHADOWSOCKS=on
|
||||
- SHADOWSOCKS_LISTENING_ADDRESS=:1080
|
||||
```
|
||||
|
||||
### DNS Over TLS
|
||||
|
||||
For enhanced privacy:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
- DOT=on
|
||||
- DOT_PROVIDERS=cloudflare
|
||||
```
|
||||
|
||||
### Custom Firewall Rules
|
||||
|
||||
Block specific ports or IPs:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
- FIREWALL_OUTBOUND_SUBNETS=192.168.1.0/24 # Allow LAN access
|
||||
```
|
||||
1. Start a Gluetun Docker container with your credentials
|
||||
2. Wait for the VPN connection to establish
|
||||
3. Route your download through the VPN proxy
|
||||
4. Clean up the container when done (if `auto_cleanup: true`)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Docker Not Running
|
||||
|
||||
```
|
||||
Error: Docker is not running
|
||||
```
|
||||
|
||||
Start Docker Desktop or the Docker daemon.
|
||||
|
||||
### Invalid Credentials
|
||||
|
||||
```
|
||||
Error: VPN authentication failed
|
||||
```
|
||||
|
||||
Verify your credentials are correct. Use VPN service credentials from your provider's manual setup page, not your account login.
|
||||
|
||||
### Container Fails to Start
|
||||
|
||||
Check logs:
|
||||
Check Docker logs:
|
||||
|
||||
```bash
|
||||
docker logs gluetun
|
||||
docker logs unshackle-gluetun-windscribe-us
|
||||
```
|
||||
|
||||
Common issues:
|
||||
- Missing `NET_ADMIN` capability
|
||||
- `/dev/net/tun` not available
|
||||
- Invalid WireGuard credentials
|
||||
### VPN Connection Timeout
|
||||
|
||||
### VPN Not Connecting
|
||||
If the VPN connection hangs or times out, your network may be blocking the default UDP port 1194. Try using TCP port 443:
|
||||
|
||||
1. Verify credentials are correct
|
||||
2. Check VPN provider status
|
||||
3. Try different server location
|
||||
4. Check firewall isn't blocking VPN ports
|
||||
|
||||
### Proxy Not Working
|
||||
|
||||
Test connectivity:
|
||||
```bash
|
||||
# Check if port is open
|
||||
docker exec gluetun netstat -tlnp | grep 8888
|
||||
|
||||
# Test proxy directly
|
||||
curl -v -x http://localhost:8888 https://api.ipify.org
|
||||
```
|
||||
|
||||
### IP Leak Prevention
|
||||
|
||||
Verify your IP is hidden:
|
||||
```bash
|
||||
# Without proxy (should show your real IP)
|
||||
curl https://api.ipify.org
|
||||
|
||||
# With proxy (should show VPN IP)
|
||||
curl -x http://localhost:8888 https://api.ipify.org
|
||||
```
|
||||
|
||||
### Performance Issues
|
||||
|
||||
- WireGuard is generally faster than OpenVPN
|
||||
- Try different VPN servers closer to your location
|
||||
- Check container resource limits
|
||||
- Monitor with `docker stats gluetun`
|
||||
|
||||
## Managing Gluetun
|
||||
|
||||
### Start Container
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Stop Container
|
||||
```bash
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
### Restart Container
|
||||
```bash
|
||||
docker-compose restart
|
||||
```
|
||||
|
||||
### Update Gluetun
|
||||
```bash
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### View Logs
|
||||
```bash
|
||||
docker logs gluetun -f
|
||||
```
|
||||
|
||||
### Check Status
|
||||
```bash
|
||||
docker ps | grep gluetun
|
||||
```
|
||||
|
||||
## Windows-Specific Notes
|
||||
|
||||
### Using Docker Desktop
|
||||
|
||||
1. Ensure WSL2 backend is enabled in Docker Desktop settings
|
||||
2. Use PowerShell or WSL2 terminal for commands
|
||||
3. Access proxy from Windows: `http://localhost:8888`
|
||||
4. Access from WSL2: `http://host.docker.internal:8888`
|
||||
|
||||
### Using WSL2 Directly
|
||||
|
||||
If running Unshackle in WSL2:
|
||||
```yaml
|
||||
proxies:
|
||||
Basic:
|
||||
us: "http://localhost:8888" # If Gluetun is in same WSL2 distro
|
||||
# or
|
||||
us: "http://host.docker.internal:8888" # If Gluetun is in Docker Desktop
|
||||
windscribe:
|
||||
vpn_type: openvpn
|
||||
openvpn_port: 443 # Use TCP 443 for restricted networks
|
||||
credentials:
|
||||
username: "YOUR_USERNAME"
|
||||
password: "YOUR_PASSWORD"
|
||||
```
|
||||
|
||||
## Network Isolation Benefits
|
||||
### Verify VPN Connection
|
||||
|
||||
The Docker-based approach provides several benefits:
|
||||
|
||||
1. **Namespace Isolation**: VPN connection exists only in container
|
||||
2. **No System Route Changes**: Host routing table remains unchanged
|
||||
3. **No Connection Drops**: Host internet connection unaffected
|
||||
4. **Easy Switching**: Start/stop VPN without affecting other applications
|
||||
5. **Multiple Simultaneous VPNs**: Run multiple containers with different locations
|
||||
6. **Kill Switch**: Automatic with container networking
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- **WireGuard**: Modern protocol, faster than OpenVPN, less CPU usage
|
||||
- **Docker Overhead**: Minimal (< 5% performance impact)
|
||||
- **Memory Usage**: ~50-100MB per container
|
||||
- **Network Latency**: Negligible with localhost connection
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Enable authentication** on HTTP proxy (HTTPPROXY_USER/PASSWORD)
|
||||
2. **Bind to localhost only** (don't expose 0.0.0.0 unless needed)
|
||||
3. **Use Docker networks** for container-to-container communication
|
||||
4. **Keep Gluetun updated** for security patches
|
||||
5. **Monitor logs** for unauthorized access attempts
|
||||
The `verify_ip` option checks that your IP matches the expected region. If verification fails, try a different server location in your provider's settings.
|
||||
|
||||
## References
|
||||
|
||||
- [Gluetun GitHub Repository](https://github.com/qdm12/gluetun)
|
||||
- [Gluetun Wiki - Setup Guides](https://github.com/qdm12/gluetun/wiki)
|
||||
- [Windscribe Setup Guide](https://github.com/qdm12/gluetun/wiki/Windscribe)
|
||||
- [Docker Installation](https://docs.docker.com/engine/install/)
|
||||
|
||||
## Alternative Solutions
|
||||
|
||||
If Gluetun doesn't meet your needs, consider:
|
||||
|
||||
### 1. **Pritunl Client + Tinyproxy**
|
||||
- Run Pritunl in Docker with Tinyproxy
|
||||
- More complex setup but more control
|
||||
|
||||
### 2. **OpenConnect + Privoxy**
|
||||
- For Cisco AnyConnect VPNs
|
||||
- Network namespace isolation on Linux
|
||||
|
||||
### 3. **WireGuard + SOCKS5 Proxy**
|
||||
- Manual WireGuard setup with microsocks/dante
|
||||
- Maximum control but requires networking knowledge
|
||||
|
||||
### 4. **Network Namespaces (Linux Only)**
|
||||
```bash
|
||||
# Create namespace
|
||||
sudo ip netns add vpn
|
||||
|
||||
# Setup WireGuard in namespace
|
||||
sudo ip netns exec vpn wg-quick up wg0
|
||||
|
||||
# Run proxy in namespace
|
||||
sudo ip netns exec vpn tinyproxy -d -c /etc/tinyproxy.conf
|
||||
```
|
||||
|
||||
However, **Gluetun is recommended** for its ease of use, maintenance, and cross-platform support.
|
||||
|
||||
## Conclusion
|
||||
|
||||
Using Gluetun provides a robust, isolated, and easy-to-manage solution for connecting Unshackle to VPN services that don't offer HTTP proxies. The Docker-based approach ensures your system's network remains stable while giving you full VPN benefits for Unshackle downloads.
|
||||
- [Gluetun GitHub](https://github.com/qdm12/gluetun)
|
||||
- [Gluetun Wiki - Provider Setup](https://github.com/qdm12/gluetun-wiki)
|
||||
- [CONFIG.md - Full gluetun options](../CONFIG.md#gluetun-dict)
|
||||
|
||||
@@ -65,6 +65,7 @@ dependencies = [
|
||||
"pysubs2>=1.7.0,<2",
|
||||
"PyExecJS>=1.5.1,<2",
|
||||
"pycountry>=24.6.1",
|
||||
"language-data>=1.4.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
|
||||
@@ -97,11 +97,7 @@ class dl:
|
||||
return None
|
||||
|
||||
def prepare_temp_font(
|
||||
self,
|
||||
font_name: str,
|
||||
matched_font: Path,
|
||||
system_fonts: dict[str, Path],
|
||||
temp_font_files: list[Path]
|
||||
self, font_name: str, matched_font: Path, system_fonts: dict[str, Path], temp_font_files: list[Path]
|
||||
) -> Path:
|
||||
"""
|
||||
Copy system font to temp and log if using fallback.
|
||||
@@ -116,10 +112,7 @@ class dl:
|
||||
Path to temp font file
|
||||
"""
|
||||
# Find the matched name for logging
|
||||
matched_name = next(
|
||||
(name for name, path in system_fonts.items() if path == matched_font),
|
||||
None
|
||||
)
|
||||
matched_name = next((name for name, path in system_fonts.items() if path == matched_font), None)
|
||||
|
||||
if matched_name and matched_name.lower() != font_name.lower():
|
||||
self.log.info(f"Using '{matched_name}' as fallback for '{font_name}'")
|
||||
@@ -136,10 +129,7 @@ class dl:
|
||||
return temp_path
|
||||
|
||||
def attach_subtitle_fonts(
|
||||
self,
|
||||
font_names: list[str],
|
||||
title: Title_T,
|
||||
temp_font_files: list[Path]
|
||||
self, font_names: list[str], title: Title_T, temp_font_files: list[Path]
|
||||
) -> tuple[int, list[str]]:
|
||||
"""
|
||||
Attach fonts for subtitle rendering.
|
||||
@@ -672,16 +662,21 @@ class dl:
|
||||
self.log.info(f"Loaded {proxy_provider.__class__.__name__}: {proxy_provider}")
|
||||
|
||||
if proxy:
|
||||
# Store original proxy query for service-specific proxy_map
|
||||
original_proxy_query = proxy
|
||||
requested_provider = None
|
||||
if re.match(r"^[a-z]+:.+$", proxy, re.IGNORECASE):
|
||||
# requesting proxy from a specific proxy provider
|
||||
requested_provider, proxy = proxy.split(":", maxsplit=1)
|
||||
# Match simple region codes (us, ca, uk1) or provider:region format (nordvpn:ca, windscribe:us)
|
||||
if re.match(r"^[a-z]{2}(?:\d+)?$", proxy, re.IGNORECASE) or re.match(r"^[a-z]+:[a-z]{2}(?:\d+)?$", proxy, re.IGNORECASE):
|
||||
if re.match(r"^[a-z]{2}(?:\d+)?$", proxy, re.IGNORECASE) or re.match(
|
||||
r"^[a-z]+:[a-z]{2}(?:\d+)?$", proxy, re.IGNORECASE
|
||||
):
|
||||
proxy = proxy.lower()
|
||||
with console.status(f"Getting a Proxy to {proxy}...", spinner="dots"):
|
||||
status_msg = (
|
||||
f"Connecting to VPN ({proxy})..."
|
||||
if requested_provider == "gluetun"
|
||||
else f"Getting a Proxy to {proxy}..."
|
||||
)
|
||||
with console.status(status_msg, spinner="dots"):
|
||||
if requested_provider:
|
||||
proxy_provider = next(
|
||||
(x for x in self.proxy_providers if x.__class__.__name__.lower() == requested_provider),
|
||||
@@ -690,17 +685,39 @@ class dl:
|
||||
if not proxy_provider:
|
||||
self.log.error(f"The proxy provider '{requested_provider}' was not recognised.")
|
||||
sys.exit(1)
|
||||
proxy_query = proxy # Save query before overwriting with URI
|
||||
proxy_uri = proxy_provider.get_proxy(proxy)
|
||||
if not proxy_uri:
|
||||
self.log.error(f"The proxy provider {requested_provider} had no proxy for {proxy}")
|
||||
sys.exit(1)
|
||||
proxy = ctx.params["proxy"] = proxy_uri
|
||||
# Show connection info for Gluetun (IP, location) instead of proxy URL
|
||||
if hasattr(proxy_provider, "get_connection_info"):
|
||||
conn_info = proxy_provider.get_connection_info(proxy_query)
|
||||
if conn_info and conn_info.get("public_ip"):
|
||||
location_parts = [conn_info.get("city"), conn_info.get("country")]
|
||||
location = ", ".join(p for p in location_parts if p)
|
||||
self.log.info(f"VPN Connected: {conn_info['public_ip']} ({location})")
|
||||
else:
|
||||
self.log.info(f"Using {proxy_provider.__class__.__name__} Proxy: {proxy}")
|
||||
else:
|
||||
self.log.info(f"Using {proxy_provider.__class__.__name__} Proxy: {proxy}")
|
||||
else:
|
||||
for proxy_provider in self.proxy_providers:
|
||||
proxy_query = proxy # Save query before overwriting with URI
|
||||
proxy_uri = proxy_provider.get_proxy(proxy)
|
||||
if proxy_uri:
|
||||
proxy = ctx.params["proxy"] = proxy_uri
|
||||
# Show connection info for Gluetun (IP, location) instead of proxy URL
|
||||
if hasattr(proxy_provider, "get_connection_info"):
|
||||
conn_info = proxy_provider.get_connection_info(proxy_query)
|
||||
if conn_info and conn_info.get("public_ip"):
|
||||
location_parts = [conn_info.get("city"), conn_info.get("country")]
|
||||
location = ", ".join(p for p in location_parts if p)
|
||||
self.log.info(f"VPN Connected: {conn_info['public_ip']} ({location})")
|
||||
else:
|
||||
self.log.info(f"Using {proxy_provider.__class__.__name__} Proxy: {proxy}")
|
||||
else:
|
||||
self.log.info(f"Using {proxy_provider.__class__.__name__} Proxy: {proxy}")
|
||||
break
|
||||
# Store proxy query info for service-specific overrides
|
||||
@@ -1069,7 +1086,9 @@ class dl:
|
||||
title.tracks.add(non_sdh_sub)
|
||||
events.subscribe(
|
||||
events.Types.TRACK_MULTIPLEX,
|
||||
lambda track, sub_id=non_sdh_sub.id: (track.strip_hearing_impaired()) if track.id == sub_id else None,
|
||||
lambda track, sub_id=non_sdh_sub.id: (track.strip_hearing_impaired())
|
||||
if track.id == sub_id
|
||||
else None,
|
||||
)
|
||||
|
||||
with console.status("Sorting tracks by language and bitrate...", spinner="dots"):
|
||||
@@ -1339,7 +1358,16 @@ class dl:
|
||||
self.log.error(f"There's no {processed_lang} Audio Track, cannot continue...")
|
||||
sys.exit(1)
|
||||
|
||||
if video_only or audio_only or subs_only or chapters_only or no_subs or no_audio or no_chapters or no_video:
|
||||
if (
|
||||
video_only
|
||||
or audio_only
|
||||
or subs_only
|
||||
or chapters_only
|
||||
or no_subs
|
||||
or no_audio
|
||||
or no_chapters
|
||||
or no_video
|
||||
):
|
||||
keep_videos = False
|
||||
keep_audio = False
|
||||
keep_subtitles = False
|
||||
@@ -1579,9 +1607,7 @@ class dl:
|
||||
if line.startswith("Style: "):
|
||||
font_names.append(line.removeprefix("Style: ").split(",")[1].strip())
|
||||
|
||||
font_count, missing_fonts = self.attach_subtitle_fonts(
|
||||
font_names, title, temp_font_files
|
||||
)
|
||||
font_count, missing_fonts = self.attach_subtitle_fonts(font_names, title, temp_font_files)
|
||||
|
||||
if font_count:
|
||||
self.log.info(f"Attached {font_count} fonts for the Subtitles")
|
||||
|
||||
@@ -80,7 +80,6 @@ def status_command(remote: Optional[str]) -> None:
|
||||
|
||||
from unshackle.core.local_session_cache import get_local_session_cache
|
||||
|
||||
|
||||
# Get local session cache
|
||||
cache = get_local_session_cache()
|
||||
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
"""Cryptographic utilities for secure remote service authentication."""
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import secrets
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
|
||||
try:
|
||||
from nacl.public import Box, PrivateKey, PublicKey
|
||||
from nacl.secret import SecretBox
|
||||
from nacl.utils import random
|
||||
|
||||
NACL_AVAILABLE = True
|
||||
except ImportError:
|
||||
|
||||
@@ -10,7 +10,7 @@ import requests
|
||||
from requests.cookies import cookiejar_from_dict, get_cookie_header
|
||||
|
||||
from unshackle.core import binaries
|
||||
from unshackle.core.binaries import FFMPEG, ShakaPackager, Mp4decrypt
|
||||
from unshackle.core.binaries import FFMPEG, Mp4decrypt, ShakaPackager
|
||||
from unshackle.core.config import config
|
||||
from unshackle.core.console import console
|
||||
from unshackle.core.constants import DOWNLOAD_CANCELLED
|
||||
|
||||
@@ -102,6 +102,62 @@ class Gluetun(Proxy):
|
||||
"purevpn": "purevpn",
|
||||
}
|
||||
|
||||
# Windscribe uses specific region names instead of country codes
|
||||
# See: https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/windscribe.md
|
||||
WINDSCRIBE_REGION_MAP = {
|
||||
# Country codes to Windscribe region names
|
||||
"us": "US East",
|
||||
"us-east": "US East",
|
||||
"us-west": "US West",
|
||||
"us-central": "US Central",
|
||||
"ca": "Canada East",
|
||||
"ca-east": "Canada East",
|
||||
"ca-west": "Canada West",
|
||||
"uk": "United Kingdom",
|
||||
"gb": "United Kingdom",
|
||||
"de": "Germany",
|
||||
"fr": "France",
|
||||
"nl": "Netherlands",
|
||||
"au": "Australia",
|
||||
"jp": "Japan",
|
||||
"sg": "Singapore",
|
||||
"hk": "Hong Kong",
|
||||
"kr": "South Korea",
|
||||
"in": "India",
|
||||
"it": "Italy",
|
||||
"es": "Spain",
|
||||
"ch": "Switzerland",
|
||||
"se": "Sweden",
|
||||
"no": "Norway",
|
||||
"dk": "Denmark",
|
||||
"fi": "Finland",
|
||||
"at": "Austria",
|
||||
"be": "Belgium",
|
||||
"ie": "Ireland",
|
||||
"pl": "Poland",
|
||||
"pt": "Portugal",
|
||||
"cz": "Czech Republic",
|
||||
"ro": "Romania",
|
||||
"hu": "Hungary",
|
||||
"gr": "Greece",
|
||||
"tr": "Turkey",
|
||||
"ru": "Russia",
|
||||
"ua": "Ukraine",
|
||||
"br": "Brazil",
|
||||
"mx": "Mexico",
|
||||
"ar": "Argentina",
|
||||
"za": "South Africa",
|
||||
"nz": "New Zealand",
|
||||
"th": "Thailand",
|
||||
"ph": "Philippines",
|
||||
"id": "Indonesia",
|
||||
"my": "Malaysia",
|
||||
"vn": "Vietnam",
|
||||
"tw": "Taiwan",
|
||||
"ae": "United Arab Emirates",
|
||||
"il": "Israel",
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
providers: Optional[dict] = None,
|
||||
@@ -196,9 +252,7 @@ class Gluetun(Proxy):
|
||||
# Parse query
|
||||
parts = query.split(":")
|
||||
if len(parts) != 2:
|
||||
raise ValueError(
|
||||
f"Invalid query format: '{query}'. Expected 'provider:region' (e.g., 'windscribe:us')"
|
||||
)
|
||||
raise ValueError(f"Invalid query format: '{query}'. Expected 'provider:region' (e.g., 'windscribe:us')")
|
||||
|
||||
provider_name = parts[0].lower()
|
||||
region = parts[1].lower()
|
||||
@@ -206,9 +260,7 @@ class Gluetun(Proxy):
|
||||
# Check if provider is configured
|
||||
if provider_name not in self.providers:
|
||||
available = ", ".join(self.providers.keys())
|
||||
raise ValueError(
|
||||
f"VPN provider '{provider_name}' not configured. Available providers: {available}"
|
||||
)
|
||||
raise ValueError(f"VPN provider '{provider_name}' not configured. Available providers: {available}")
|
||||
|
||||
# Create query key for tracking
|
||||
query_key = f"{provider_name}:{region}"
|
||||
@@ -333,11 +385,11 @@ class Gluetun(Proxy):
|
||||
# Get container logs for better error message
|
||||
logs = self._get_container_logs(container_name, tail=30)
|
||||
error_msg = f"Gluetun container '{container_name}' failed to start"
|
||||
if hasattr(self, '_last_wait_error') and self._last_wait_error:
|
||||
if hasattr(self, "_last_wait_error") and self._last_wait_error:
|
||||
error_msg += f": {self._last_wait_error}"
|
||||
if logs:
|
||||
# Extract last few relevant lines
|
||||
log_lines = [line for line in logs.strip().split('\n') if line.strip()][-5:]
|
||||
log_lines = [line for line in logs.strip().split("\n") if line.strip()][-5:]
|
||||
error_msg += "\nRecent logs:\n" + "\n".join(log_lines)
|
||||
raise RuntimeError(error_msg)
|
||||
|
||||
@@ -396,22 +448,49 @@ class Gluetun(Proxy):
|
||||
success=True,
|
||||
)
|
||||
|
||||
def get_connection_info(self, query: str) -> Optional[dict]:
|
||||
"""
|
||||
Get connection info for a proxy query.
|
||||
|
||||
Args:
|
||||
query: Query format "provider:region" (e.g., "windscribe:us")
|
||||
|
||||
Returns:
|
||||
Dict with connection info including public_ip, country, city, or None if not found.
|
||||
"""
|
||||
parts = query.split(":")
|
||||
if len(parts) != 2:
|
||||
return None
|
||||
|
||||
provider_name = parts[0].lower()
|
||||
region = parts[1].lower()
|
||||
query_key = f"{provider_name}:{region}"
|
||||
|
||||
container = self.active_containers.get(query_key)
|
||||
if not container:
|
||||
return None
|
||||
|
||||
return {
|
||||
"provider": container.get("provider"),
|
||||
"region": container.get("region"),
|
||||
"public_ip": container.get("public_ip"),
|
||||
"country": container.get("ip_country"),
|
||||
"city": container.get("ip_city"),
|
||||
"org": container.get("ip_org"),
|
||||
}
|
||||
|
||||
def _validate_provider_config(self, provider_name: str, config: dict):
|
||||
"""Validate a provider's configuration."""
|
||||
vpn_type = config.get("vpn_type", "wireguard").lower()
|
||||
credentials = config.get("credentials", {})
|
||||
|
||||
if vpn_type not in ["wireguard", "openvpn"]:
|
||||
raise ValueError(
|
||||
f"Provider '{provider_name}': Invalid vpn_type '{vpn_type}'. Use 'wireguard' or 'openvpn'"
|
||||
)
|
||||
raise ValueError(f"Provider '{provider_name}': Invalid vpn_type '{vpn_type}'. Use 'wireguard' or 'openvpn'")
|
||||
|
||||
if vpn_type == "wireguard":
|
||||
# private_key is always required for WireGuard
|
||||
if "private_key" not in credentials:
|
||||
raise ValueError(
|
||||
f"Provider '{provider_name}': WireGuard requires 'private_key' in credentials"
|
||||
)
|
||||
raise ValueError(f"Provider '{provider_name}': WireGuard requires 'private_key' in credentials")
|
||||
|
||||
# Provider-specific WireGuard requirements based on Gluetun wiki:
|
||||
# - NordVPN, ProtonVPN: only private_key required
|
||||
@@ -435,9 +514,7 @@ class Gluetun(Proxy):
|
||||
# Providers that require addresses (but not preshared_key)
|
||||
elif provider_lower in ["surfshark", "mullvad", "ivpn"]:
|
||||
if "addresses" not in credentials:
|
||||
raise ValueError(
|
||||
f"Provider '{provider_name}': WireGuard requires 'addresses' in credentials"
|
||||
)
|
||||
raise ValueError(f"Provider '{provider_name}': WireGuard requires 'addresses' in credentials")
|
||||
|
||||
elif vpn_type == "openvpn":
|
||||
if "username" not in credentials or "password" not in credentials:
|
||||
@@ -651,6 +728,11 @@ class Gluetun(Proxy):
|
||||
# Use country/city selection
|
||||
if country:
|
||||
if uses_regions:
|
||||
# Convert country code to provider-specific region name
|
||||
if gluetun_provider == "windscribe":
|
||||
region_name = self.WINDSCRIBE_REGION_MAP.get(country.lower(), country)
|
||||
env_vars["SERVER_REGIONS"] = region_name
|
||||
else:
|
||||
env_vars["SERVER_REGIONS"] = country
|
||||
else:
|
||||
env_vars["SERVER_COUNTRIES"] = country
|
||||
@@ -666,6 +748,16 @@ class Gluetun(Proxy):
|
||||
# Merge extra environment variables
|
||||
env_vars.update(extra_env)
|
||||
|
||||
# Debug log environment variables (redact sensitive values)
|
||||
if debug_logger:
|
||||
safe_env = {k: ("***" if "KEY" in k or "PASSWORD" in k else v) for k, v in env_vars.items()}
|
||||
debug_logger.log(
|
||||
level="DEBUG",
|
||||
operation="gluetun_env_vars",
|
||||
message=f"Environment variables for {container_name}",
|
||||
context={"env_vars": safe_env, "gluetun_provider": gluetun_provider},
|
||||
)
|
||||
|
||||
# Build docker run command
|
||||
cmd = [
|
||||
"docker",
|
||||
@@ -791,7 +883,7 @@ class Gluetun(Proxy):
|
||||
return None
|
||||
|
||||
# Parse port from output like "map[8888/tcp:[{127.0.0.1 8888}]]"
|
||||
port_match = re.search(r'127\.0\.0\.1\s+(\d+)', result.stdout)
|
||||
port_match = re.search(r"127\.0\.0\.1\s+(\d+)", result.stdout)
|
||||
if not port_match:
|
||||
return None
|
||||
|
||||
@@ -854,11 +946,9 @@ class Gluetun(Proxy):
|
||||
Returns:
|
||||
True if container is ready, False if it failed or timed out
|
||||
"""
|
||||
log = logging.getLogger("Gluetun")
|
||||
debug_logger = get_debug_logger()
|
||||
start_time = time.time()
|
||||
last_error = None
|
||||
last_status = None
|
||||
|
||||
if debug_logger:
|
||||
debug_logger.log(
|
||||
@@ -900,21 +990,6 @@ class Gluetun(Proxy):
|
||||
proxy_ready = "[http proxy] listening" in all_logs
|
||||
vpn_ready = "initialization sequence completed" in all_logs
|
||||
|
||||
# Log status changes to help debug slow connections
|
||||
current_status = None
|
||||
if vpn_ready:
|
||||
current_status = "VPN connected"
|
||||
elif "peer connection initiated" in all_logs:
|
||||
current_status = "VPN connecting..."
|
||||
elif "[openvpn]" in all_logs or "[wireguard]" in all_logs:
|
||||
current_status = "Starting VPN..."
|
||||
elif "[firewall]" in all_logs:
|
||||
current_status = "Configuring firewall..."
|
||||
|
||||
if current_status and current_status != last_status:
|
||||
log.info(current_status)
|
||||
last_status = current_status
|
||||
|
||||
if proxy_ready and vpn_ready:
|
||||
# Give a brief moment for the proxy to fully initialize
|
||||
time.sleep(1)
|
||||
@@ -947,7 +1022,7 @@ class Gluetun(Proxy):
|
||||
for error in error_indicators:
|
||||
if error in all_logs:
|
||||
# Extract the error line for better messaging
|
||||
for line in (stdout + stderr).split('\n'):
|
||||
for line in (stdout + stderr).split("\n"):
|
||||
if error in line.lower():
|
||||
last_error = line.strip()
|
||||
break
|
||||
@@ -975,7 +1050,6 @@ class Gluetun(Proxy):
|
||||
"container_name": container_name,
|
||||
"timeout": timeout,
|
||||
"last_error": last_error,
|
||||
"last_status": last_status,
|
||||
},
|
||||
success=False,
|
||||
duration_ms=duration_ms,
|
||||
@@ -986,10 +1060,7 @@ class Gluetun(Proxy):
|
||||
"""Get exit information for a stopped container."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[
|
||||
"docker", "inspect", container_name,
|
||||
"--format", "{{.State.ExitCode}}:{{.State.Error}}"
|
||||
],
|
||||
["docker", "inspect", container_name, "--format", "{{.State.ExitCode}}:{{.State.Error}}"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
@@ -998,7 +1069,7 @@ class Gluetun(Proxy):
|
||||
parts = result.stdout.strip().split(":", 1)
|
||||
return {
|
||||
"exit_code": int(parts[0]) if parts[0].isdigit() else -1,
|
||||
"error": parts[1] if len(parts) > 1 else ""
|
||||
"error": parts[1] if len(parts) > 1 else "",
|
||||
}
|
||||
return None
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError, ValueError):
|
||||
@@ -1104,7 +1175,13 @@ class Gluetun(Proxy):
|
||||
f"(IP: {ip_info.get('ip')}, City: {ip_info.get('city')})"
|
||||
)
|
||||
|
||||
# Verification successful
|
||||
# Verification successful - store IP info in container record
|
||||
if query_key in self.active_containers:
|
||||
self.active_containers[query_key]["public_ip"] = ip_info.get("ip")
|
||||
self.active_containers[query_key]["ip_country"] = actual_country
|
||||
self.active_containers[query_key]["ip_city"] = ip_info.get("city")
|
||||
self.active_containers[query_key]["ip_org"] = ip_info.get("org")
|
||||
|
||||
duration_ms = (time.time() - start_time) * 1000
|
||||
if debug_logger:
|
||||
debug_logger.log(
|
||||
@@ -1145,7 +1222,7 @@ class Gluetun(Proxy):
|
||||
|
||||
# Wait before retry (exponential backoff)
|
||||
if attempt < max_retries - 1:
|
||||
wait_time = 2 ** attempt # 1, 2, 4 seconds
|
||||
wait_time = 2**attempt # 1, 2, 4 seconds
|
||||
time.sleep(wait_time)
|
||||
|
||||
# All retries exhausted
|
||||
@@ -1253,9 +1330,9 @@ class Gluetun(Proxy):
|
||||
|
||||
def __del__(self):
|
||||
"""Cleanup containers on object destruction."""
|
||||
if hasattr(self, 'auto_cleanup') and self.auto_cleanup:
|
||||
if hasattr(self, "auto_cleanup") and self.auto_cleanup:
|
||||
try:
|
||||
if hasattr(self, 'active_containers') and self.active_containers:
|
||||
if hasattr(self, "active_containers") and self.active_containers:
|
||||
self.cleanup()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user