Skip to content

Commit 046c732

Browse files
committed
update
1 parent ca7fc0f commit 046c732

File tree

2 files changed

+104
-154
lines changed

2 files changed

+104
-154
lines changed

.traefik.yml

Lines changed: 4 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,111 +1,11 @@
11
displayName: Tailscale Connectivity Authentication
22
type: middleware
3-
iconPath: .assets/icon.png
43

54
import: github.com/hhftechnology/tailscale-access
65

7-
summary: 'Smart Tailscale authentication using real connectivity testing instead of unreliable IP checking'
6+
summary: Smart Tailscale authentication using real connectivity testing instead of unreliable IP checking
87

98
testData:
10-
testDomain: "example.ts.net"
11-
sessionTimeout: "24h"
12-
allowLocalhost: true
13-
enableDebugLogging: false
14-
customErrorMessage: "Tailscale VPN connection required to access this service"
15-
successMessage: "Tailscale connectivity verified! Redirecting..."
16-
secureOnly: true
17-
cookieDomain: ""
18-
19-
compatibility:
20-
traefik: ">=3.0.0"
21-
22-
description: |
23-
# Tailscale Connectivity Authentication
24-
25-
A revolutionary Traefik middleware that provides secure access control by **actually testing Tailscale connectivity**
26-
rather than relying on unreliable IP address detection.
27-
28-
## Key Features
29-
30-
🎯 **Real Connectivity Testing** - Tests actual ability to reach your Tailscale network, not just IP ranges
31-
🔄 **Proxy-Agnostic** - Works regardless of reverse proxies, containers, or network complexity
32-
🎨 **Beautiful UI** - Modern verification interface with real-time feedback
33-
🔒 **Secure Sessions** - Cryptographically secure session management
34-
⚡ **High Performance** - Minimal overhead for verified users
35-
🛠 **Highly Configurable** - Custom styling, messages, timeouts, and behavior
36-
37-
## How It Works
38-
39-
1. Intercepts requests to protected resources
40-
2. Checks for valid verification session
41-
3. If unverified, serves interactive verification page
42-
4. JavaScript tests connectivity to your `.ts.net` domain
43-
5. Creates secure session on successful verification
44-
6. Allows access with session cookie
45-
46-
## Configuration
47-
48-
```yaml
49-
http:
50-
middlewares:
51-
tailscale-auth:
52-
plugin:
53-
tailscale-connectivity:
54-
testDomain: "your-company.ts.net" # Required
55-
sessionTimeout: "24h"
56-
allowLocalhost: true
57-
customErrorMessage: "VPN connection required"
58-
```
59-
60-
## Why This Approach Is Superior
61-
62-
Traditional IP-based authentication fails in modern networking:
63-
- Multiple reverse proxies mangle client IPs
64-
- Container networking obscures real IPs
65-
- Cloud load balancers replace source IPs
66-
- Headers can be spoofed by malicious actors
67-
68-
Our connectivity-based approach actually verifies real Tailscale access regardless of network complexity.
69-
70-
author:
71-
name: "HHF Technology"
72-
email: "support@hhf.technology"
73-
74-
keywords:
75-
- tailscale
76-
- vpn
77-
- authentication
78-
- security
79-
- access-control
80-
- middleware
81-
- connectivity-testing
82-
83-
documentation: https://github.com/hhftechnology/tailscale-access/blob/main/README.md
84-
repository: https://github.com/hhftechnology/tailscale-access
85-
issues: https://github.com/hhftechnology/tailscale-access/issues
86-
87-
changelog:
88-
- version: "2.0.0"
89-
date: "2025-05-26"
90-
changes:
91-
- "🎉 Complete rewrite using connectivity-based verification"
92-
- "🎨 Beautiful modern verification UI with real-time feedback"
93-
- "🔒 Secure session management with cryptographic tokens"
94-
- "⚡ Multi-method connectivity testing (fetch, image, WebSocket)"
95-
- "🛠 Extensive customization options (CSS, messages, timeouts)"
96-
- "🐳 Enhanced Docker and Kubernetes integration examples"
97-
- "📚 Comprehensive documentation and troubleshooting guide"
98-
- "🔧 Removed unreliable IP-based detection completely"
99-
breaking:
100-
- "Configuration format changed from IP ranges to domain testing"
101-
- "Session-based authentication replaces per-request IP checking"
102-
- "JavaScript-based verification requires client browser support"
103-
104-
- version: "1.0.0"
105-
date: "2025-05-15"
106-
changes:
107-
- "Initial release with IP-based authentication"
108-
- "Support for Tailscale IP range detection"
109-
- "Header-based IP extraction for proxy environments"
110-
deprecated: true
111-
reason: "IP-based approach unreliable in modern networking environments"
9+
testDomain: example.ts.net
10+
sessionTimeout: 24h
11+
allowLocalhost: true

tailscale-access.go

Lines changed: 100 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,11 @@ type Config struct {
1919
// Custom verification endpoint (optional)
2020
VerificationEndpoint string `json:"verificationEndpoint,omitempty"`
2121

22-
// Session timeout for verified connections
23-
SessionTimeout time.Duration `json:"sessionTimeout,omitempty"`
22+
// Session timeout for verified connections (in seconds)
23+
SessionTimeoutSeconds int `json:"sessionTimeoutSeconds,omitempty"`
24+
25+
// Session timeout as duration string (e.g., "24h", "30m") - will be converted to seconds
26+
SessionTimeout string `json:"sessionTimeout,omitempty"`
2427

2528
// Custom error messages
2629
CustomErrorMessage string `json:"customErrorMessage,omitempty"`
@@ -44,32 +47,51 @@ type Config struct {
4447

4548
func CreateConfig() *Config {
4649
return &Config{
47-
TestDomain: "your-tailscale-network.ts.net", // User should configure this
48-
SessionTimeout: 24 * time.Hour,
49-
CustomErrorMessage: "Tailscale connection required to access this service",
50-
SuccessMessage: "Tailscale connectivity verified! Redirecting...",
51-
EnableDebugLogging: false,
52-
AllowLocalhost: true,
53-
SecureOnly: true,
54-
RequireUserAgent: true,
50+
TestDomain: "your-tailscale-network.ts.net", // User should configure this
51+
SessionTimeout: "24h",
52+
SessionTimeoutSeconds: 86400, // 24 hours in seconds as fallback
53+
CustomErrorMessage: "Tailscale connection required to access this service",
54+
SuccessMessage: "Tailscale connectivity verified! Redirecting...",
55+
EnableDebugLogging: false,
56+
AllowLocalhost: true,
57+
SecureOnly: true,
58+
RequireUserAgent: true,
5559
}
5660
}
5761

5862
type TailscaleConnectivityAuth struct {
59-
next http.Handler
60-
name string
61-
config *Config
62-
secrets map[string]time.Time // Simple in-memory session store
63+
next http.Handler
64+
name string
65+
config *Config
66+
sessionTimeout time.Duration // Parsed duration for internal use
67+
secrets map[string]time.Time // Simple in-memory session store
6368
}
6469

6570
func New(_ context.Context, next http.Handler, config *Config, name string) (http.Handler, error) {
66-
// Apply defaults
71+
// Apply defaults and validate
6772
if config.TestDomain == "" {
6873
return nil, fmt.Errorf("testDomain must be configured")
6974
}
70-
if config.SessionTimeout == 0 {
71-
config.SessionTimeout = 24 * time.Hour
75+
76+
// Parse session timeout
77+
var sessionTimeout time.Duration
78+
var err error
79+
80+
if config.SessionTimeout != "" {
81+
// Try to parse duration string (e.g., "24h", "30m")
82+
sessionTimeout, err = time.ParseDuration(config.SessionTimeout)
83+
if err != nil {
84+
return nil, fmt.Errorf("invalid sessionTimeout format: %v (use format like '24h', '30m', '45s')", err)
85+
}
86+
} else if config.SessionTimeoutSeconds > 0 {
87+
// Use seconds if provided
88+
sessionTimeout = time.Duration(config.SessionTimeoutSeconds) * time.Second
89+
} else {
90+
// Default to 24 hours
91+
sessionTimeout = 24 * time.Hour
7292
}
93+
94+
// Apply other defaults
7395
if config.CustomErrorMessage == "" {
7496
config.CustomErrorMessage = "Tailscale connection required to access this service"
7597
}
@@ -78,10 +100,11 @@ func New(_ context.Context, next http.Handler, config *Config, name string) (htt
78100
}
79101

80102
return &TailscaleConnectivityAuth{
81-
next: next,
82-
name: name,
83-
config: config,
84-
secrets: make(map[string]time.Time),
103+
next: next,
104+
name: name,
105+
config: config,
106+
sessionTimeout: sessionTimeout,
107+
secrets: make(map[string]time.Time),
85108
}, nil
86109
}
87110

@@ -151,7 +174,7 @@ func (t *TailscaleConnectivityAuth) handleVerification(rw http.ResponseWriter, r
151174
if status == "success" {
152175
// Generate verification token
153176
token := t.generateToken()
154-
expiry := time.Now().Add(t.config.SessionTimeout)
177+
expiry := time.Now().Add(t.sessionTimeout)
155178
t.secrets[token] = expiry
156179

157180
// Set secure cookie
@@ -305,49 +328,59 @@ func (t *TailscaleConnectivityAuth) generateVerificationHTML(originalURL string)
305328
verificationAttempts++;
306329
307330
try {
308-
// Test 1: Try to fetch from the Tailscale domain
309-
const testUrl = 'https://' + testDomain + '/';
331+
// Test 1: Try HTTP first (more reliable for Tailscale domains)
332+
const httpUrl = 'http://' + testDomain + '/';
310333
311-
// Use fetch with a timeout
312-
const controller = new AbortController();
313-
const timeoutId = setTimeout(() => controller.abort(), 5000);
334+
const controllerHttp = new AbortController();
335+
const timeoutIdHttp = setTimeout(() => controllerHttp.abort(), 5000);
314336
315-
const response = await fetch(testUrl, {
337+
const httpResponse = await fetch(httpUrl, {
316338
method: 'GET',
317-
mode: 'no-cors', // This allows the request even if CORS fails
339+
mode: 'no-cors',
318340
cache: 'no-cache',
319-
signal: controller.signal
341+
signal: controllerHttp.signal
320342
});
321343
322-
clearTimeout(timeoutId);
323-
324-
// If we get here, the domain is reachable
325-
await reportVerificationResult(true, 'Tailscale domain reachable');
344+
clearTimeout(timeoutIdHttp);
345+
await reportVerificationResult(true, 'Tailscale domain reachable via HTTP');
326346
327-
} catch (error) {
328-
console.log('Primary test failed:', error.message);
347+
} catch (httpError) {
348+
console.log('HTTP test failed:', httpError.message);
329349
330-
// Test 2: Try alternative method - image loading
350+
// Test 2: Try HTTPS fallback
331351
try {
332-
await testImageLoad();
333-
await reportVerificationResult(true, 'Tailscale connectivity confirmed via image test');
334-
} catch (imgError) {
335-
console.log('Image test failed:', imgError.message);
352+
const httpsUrl = 'https://' + testDomain + '/';
353+
const controllerHttps = new AbortController();
354+
const timeoutIdHttps = setTimeout(() => controllerHttps.abort(), 5000);
355+
356+
const httpsResponse = await fetch(httpsUrl, {
357+
method: 'GET',
358+
mode: 'no-cors',
359+
cache: 'no-cache',
360+
signal: controllerHttps.signal
361+
});
336362
337-
// Test 3: Try WebSocket if available
363+
clearTimeout(timeoutIdHttps);
364+
await reportVerificationResult(true, 'Tailscale domain reachable via HTTPS');
365+
366+
} catch (httpsError) {
367+
console.log('HTTPS test failed:', httpsError.message);
368+
369+
// Test 3: Try image loading with HTTP first
338370
try {
339-
await testWebSocket();
340-
await reportVerificationResult(true, 'Tailscale connectivity confirmed via WebSocket');
341-
} catch (wsError) {
342-
console.log('WebSocket test failed:', wsError.message);
343-
await reportVerificationResult(false, error.message + ' (all tests failed)');
371+
await testImageLoad();
372+
await reportVerificationResult(true, 'Tailscale connectivity confirmed via image test');
373+
} catch (imgError) {
374+
console.log('Image test failed:', imgError.message);
375+
await reportVerificationResult(false, 'All connectivity tests failed. HTTP: ' + httpError.message + ', HTTPS: ' + httpsError.message);
344376
}
345377
}
346378
}
347379
}
348380
349381
function testImageLoad() {
350382
return new Promise((resolve, reject) => {
383+
// Try HTTP first (more reliable for Tailscale)
351384
const img = new Image();
352385
const timeout = setTimeout(() => {
353386
reject(new Error('Image load timeout'));
@@ -360,11 +393,28 @@ func (t *TailscaleConnectivityAuth) generateVerificationHTML(originalURL string)
360393
361394
img.onerror = () => {
362395
clearTimeout(timeout);
363-
reject(new Error('Image load failed'));
396+
// Try HTTPS fallback
397+
const img2 = new Image();
398+
const timeout2 = setTimeout(() => {
399+
reject(new Error('Image load failed (both HTTP and HTTPS)'));
400+
}, 3000);
401+
402+
img2.onload = () => {
403+
clearTimeout(timeout2);
404+
resolve();
405+
};
406+
407+
img2.onerror = () => {
408+
clearTimeout(timeout2);
409+
reject(new Error('Image load failed (both HTTP and HTTPS)'));
410+
};
411+
412+
// Try HTTPS as fallback
413+
img2.src = 'https://' + testDomain + '/favicon.ico?t=' + Date.now();
364414
};
365415
366-
// Try to load a favicon or small image from the Tailscale domain
367-
img.src = 'https://' + testDomain + '/favicon.ico?t=' + Date.now();
416+
// Try HTTP first
417+
img.src = 'http://' + testDomain + '/favicon.ico?t=' + Date.now();
368418
});
369419
}
370420

0 commit comments

Comments
 (0)