@@ -393,4 +394,11 @@ Allows a user to save the ACL configuration to the headscale server or load a ne
Store API URL and API Key information in the browser's LocalStorage. Set API refresh interval (how frequently users, preauth keys, nodes, and routes are updated) and toggle console debugging.
-
\ No newline at end of file
+
+
+
+### gRPC Debug Page
+
+A diagnostic page to test the gRPC connection and configuration.
+
+
\ No newline at end of file
diff --git a/analyze-envoy-response.js b/analyze-envoy-response.js
new file mode 100644
index 0000000..5684299
--- /dev/null
+++ b/analyze-envoy-response.js
@@ -0,0 +1,259 @@
+// Envoy gRPC-Web Response Analysis Tool
+// Run this in browser console to analyze the 128-byte response
+
+console.log('๐ Envoy gRPC-Web Response Analysis Tool\n');
+
+// Simulate the 128-byte response structure based on your Envoy configuration
+function analyzeEnvoyResponse() {
+ console.log('๐ Analyzing Envoy gRPC-Web Response Structure:');
+
+ // Envoy gRPC-Web response typically has this structure:
+ // [Frame Type][Length][Data][Trailer Type][Trailer Length][Trailers]
+
+ console.log('\n๐ง Envoy Configuration Analysis:');
+ console.log('Your Envoy config shows:');
+ console.log('- gRPC-Web filter enabled');
+ console.log('- CORS properly configured');
+ console.log('- Proxying to example.com:50443');
+ console.log('- HTTP/2 protocol options enabled');
+
+ console.log('\n๐ฆ Expected Response Structure:');
+ console.log('1. Message Frame:');
+ console.log(' [0x00][4-byte length][protobuf message data]');
+ console.log('2. Trailer Frame (optional):');
+ console.log(' [0x80][4-byte length][grpc-status, grpc-message headers]');
+
+ return true;
+}
+
+// Analyze the specific error pattern
+function analyzeErrorPattern() {
+ console.log('\n๐จ Error Pattern Analysis:');
+
+ const errorInfo = {
+ current: 'index out of range: 6 + 10 > 9',
+ previous: 'index out of range: 7 + 102 > 88',
+ pattern: 'offset + length > available_data'
+ };
+
+ console.log('Current Error:', errorInfo.current);
+ console.log('Previous Error:', errorInfo.previous);
+ console.log('Pattern:', errorInfo.pattern);
+
+ console.log('\n๐ก Analysis:');
+ console.log('- Error offset changed from 7 to 6 (progress!)');
+ console.log('- Length changed from 102 to 10 (much smaller)');
+ console.log('- Available data is only 9 bytes at that point');
+ console.log('- This suggests we\'re getting closer to the correct parsing');
+
+ return errorInfo;
+}
+
+// Analyze character encoding issues
+function analyzeEncodingIssues() {
+ console.log('\n๐ค Character Encoding Analysis:');
+
+ const observedText = 'fangzhou93ๆฅ๏ฟฝ๏ฟฝ๏ฟฝ๏ฟฝ๏ฟฝ fangzhou94๏ฟฝ๏ฟฝ๏ฟฝ๏ฟฝ๏ฟฝ๏ฟฝ๏ฟฝw fz96๏ฟฝ๏ฟฝ๏ฟฝ๏ฟฝ๏ฟฝ๏ฟฝ{"ding๏ฟฝgrpc-';
+
+ console.log('Observed text:', observedText);
+ console.log('\n๐ Encoding Issues Detected:');
+
+ // Analyze the garbled characters
+ const issues = [
+ {
+ text: 'ๆฅ๏ฟฝ๏ฟฝ๏ฟฝ๏ฟฝ๏ฟฝ',
+ issue: 'Chinese character followed by replacement characters',
+ cause: 'UTF-8 decoding error or truncated multi-byte sequence'
+ },
+ {
+ text: '๏ฟฝ๏ฟฝ๏ฟฝ๏ฟฝ๏ฟฝ๏ฟฝ๏ฟฝw',
+ issue: 'Multiple replacement characters',
+ cause: 'Invalid UTF-8 byte sequence'
+ },
+ {
+ text: '๏ฟฝ๏ฟฝ๏ฟฝ๏ฟฝ๏ฟฝ๏ฟฝ{',
+ issue: 'Replacement chars before JSON-like structure',
+ cause: 'Binary data interpreted as text'
+ }
+ ];
+
+ issues.forEach((issue, index) => {
+ console.log(`${index + 1}. "${issue.text}"`);
+ console.log(` Issue: ${issue.issue}`);
+ console.log(` Cause: ${issue.cause}`);
+ console.log('');
+ });
+
+ console.log('๐ก Solutions:');
+ console.log('1. Use TextDecoder with { fatal: false } for UTF-8');
+ console.log('2. Separate binary protobuf data from text display');
+ console.log('3. Parse protobuf first, then extract text fields');
+ console.log('4. Handle multi-byte UTF-8 sequences properly');
+
+ return issues;
+}
+
+// Test different parsing strategies
+function testParsingStrategies() {
+ console.log('\n๐ฏ Testing Parsing Strategies:');
+
+ // Simulate a 128-byte response with mixed content
+ const strategies = [
+ {
+ name: 'Strategy 1: Find gRPC-Web message frame',
+ description: 'Look for 0x00 followed by length, extract message',
+ implementation: 'Scan for [0x00][length] pattern'
+ },
+ {
+ name: 'Strategy 2: Skip Envoy headers',
+ description: 'Skip first N bytes that might be Envoy-specific',
+ implementation: 'Try offsets 0, 5, 8, 10, 12, 16'
+ },
+ {
+ name: 'Strategy 3: Find protobuf field markers',
+ description: 'Look for 0x0A (field 1, length-delimited)',
+ implementation: 'Scan for protobuf field tags'
+ },
+ {
+ name: 'Strategy 4: Parse until error',
+ description: 'Parse as much as possible, stop at errors',
+ implementation: 'Progressive parsing with error boundaries'
+ },
+ {
+ name: 'Strategy 5: Trailer separation',
+ description: 'Separate message frame from trailer frame',
+ implementation: 'Look for 0x80 trailer marker'
+ }
+ ];
+
+ strategies.forEach((strategy, index) => {
+ console.log(`${index + 1}. ${strategy.name}`);
+ console.log(` Description: ${strategy.description}`);
+ console.log(` Implementation: ${strategy.implementation}`);
+ console.log('');
+ });
+
+ return strategies;
+}
+
+// Generate test recommendations
+function generateTestRecommendations() {
+ console.log('\n๐ Test Recommendations:');
+
+ const recommendations = [
+ {
+ priority: 'HIGH',
+ action: 'Display raw hex dump in gRPC Debug page',
+ reason: 'Need to see exact byte structure'
+ },
+ {
+ priority: 'HIGH',
+ action: 'Show UTF-8 and ASCII interpretations separately',
+ reason: 'Distinguish between binary and text data'
+ },
+ {
+ priority: 'MEDIUM',
+ action: 'Implement frame-by-frame parsing',
+ reason: 'Envoy may send multiple frames'
+ },
+ {
+ priority: 'MEDIUM',
+ action: 'Add protobuf field analysis',
+ reason: 'Understand field structure and boundaries'
+ },
+ {
+ priority: 'LOW',
+ action: 'Test with different Envoy configurations',
+ reason: 'Verify if issue is Envoy-specific'
+ }
+ ];
+
+ recommendations.forEach((rec, index) => {
+ console.log(`${index + 1}. [${rec.priority}] ${rec.action}`);
+ console.log(` Reason: ${rec.reason}`);
+ console.log('');
+ });
+
+ return recommendations;
+}
+
+// Envoy-specific considerations
+function analyzeEnvoySpecifics() {
+ console.log('\n๐ Envoy-Specific Considerations:');
+
+ console.log('Based on your Envoy configuration:');
+ console.log('');
+
+ const envoyFeatures = [
+ {
+ feature: 'gRPC-Web Filter',
+ impact: 'Converts gRPC to gRPC-Web format',
+ note: 'May add frame headers and trailers'
+ },
+ {
+ feature: 'CORS Filter',
+ impact: 'Adds CORS headers to response',
+ note: 'Should not affect message body'
+ },
+ {
+ feature: 'HTTP/2 Backend',
+ impact: 'Communicates with Headscale via HTTP/2',
+ note: 'May affect frame structure'
+ },
+ {
+ feature: 'Timeout Configuration',
+ impact: 'timeout: 0s, grpc_timeout_header_max: 0s',
+ note: 'Unlimited timeouts - good for debugging'
+ }
+ ];
+
+ envoyFeatures.forEach((feature, index) => {
+ console.log(`${index + 1}. ${feature.feature}`);
+ console.log(` Impact: ${feature.impact}`);
+ console.log(` Note: ${feature.note}`);
+ console.log('');
+ });
+
+ console.log('๐ง Debugging Steps:');
+ console.log('1. Check Envoy logs for any transformation warnings');
+ console.log('2. Compare direct gRPC call vs Envoy-proxied call');
+ console.log('3. Verify Headscale gRPC service is returning valid protobuf');
+ console.log('4. Test with minimal protobuf message first');
+
+ return envoyFeatures;
+}
+
+// Main analysis runner
+function runEnvoyAnalysis() {
+ console.log('๐ Starting Envoy gRPC-Web Response Analysis...\n');
+
+ analyzeEnvoyResponse();
+ const errorInfo = analyzeErrorPattern();
+ const encodingIssues = analyzeEncodingIssues();
+ const strategies = testParsingStrategies();
+ const recommendations = generateTestRecommendations();
+ const envoyFeatures = analyzeEnvoySpecifics();
+
+ console.log('โ
Envoy Analysis Complete!');
+ console.log('\n๐ฏ Next Steps:');
+ console.log('1. Run gRPC Debug test with API key');
+ console.log('2. Examine the detailed hex dump output');
+ console.log('3. Look for frame boundaries and protobuf markers');
+ console.log('4. Apply the parsing strategies based on findings');
+ console.log('\n๐ก The new debug interface will show:');
+ console.log('- Complete hex dump of 128-byte response');
+ console.log('- UTF-8 and ASCII interpretations');
+ console.log('- Protobuf field analysis');
+ console.log('- Frame structure detection');
+
+ return {
+ errorInfo,
+ encodingIssues,
+ strategies,
+ recommendations,
+ envoyFeatures
+ };
+}
+
+// Auto-run analysis
+runEnvoyAnalysis();
diff --git a/envoy-headscale.yaml b/envoy-headscale.yaml
new file mode 100644
index 0000000..05e5e91
--- /dev/null
+++ b/envoy-headscale.yaml
@@ -0,0 +1,56 @@
+static_resources:
+ listeners:
+ - name: listener_0
+ address:
+ socket_address: { address: 0.0.0.0, port_value: 8080 }
+ filter_chains:
+ - filters:
+ - name: envoy.filters.network.http_connection_manager
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+ codec_type: auto
+ stat_prefix: ingress_http
+ route_config:
+ name: local_route
+ virtual_hosts:
+ - name: local_service
+ domains: ["*"]
+ routes:
+ - match: { prefix: "/" }
+ route:
+ cluster: headscale_service
+ timeout: 0s
+ max_stream_duration:
+ grpc_timeout_header_max: 0s
+ cors:
+ allow_origin_string_match:
+ - prefix: "*"
+ allow_methods: GET, PUT, DELETE, POST, OPTIONS
+ allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout,authorization
+ max_age: "1728000"
+ expose_headers: custom-header-1,grpc-status,grpc-message
+ http_filters:
+ - name: envoy.extensions.filters.http.grpc_web.v3.GrpcWeb
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb
+ - name: envoy.extensions.filters.http.cors.v3.Cors
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors
+ - name: envoy.extensions.filters.http.router.v3.Router
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
+ clusters:
+ - name: headscale_service
+ connect_timeout: 0.25s
+ type: logical_dns
+ http2_protocol_options: {}
+ lb_policy: round_robin
+ load_assignment:
+ cluster_name: headscale_service
+ endpoints:
+ - lb_endpoints:
+ - endpoint:
+ address:
+ socket_address:
+ address: example.com
+ port_value: 50443
\ No newline at end of file
diff --git a/img/HA-gRpc-Debug.png b/img/HA-gRpc-Debug.png
new file mode 100644
index 0000000..4217105
Binary files /dev/null and b/img/HA-gRpc-Debug.png differ
diff --git a/package-lock.json b/package-lock.json
index 9d7e9e4..a731649 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,12 +9,16 @@
"version": "0.0.1",
"dependencies": {
"@floating-ui/dom": "^1.6.13",
+ "@types/google-protobuf": "^3.15.12",
"async-mutex": "^0.5.0",
"dompurify": "^3.2.4",
+ "google-protobuf": "^3.21.4",
+ "grpc-web": "^1.5.0",
"highlight.js": "11.11.1",
"ipaddr.js": "^2.2.0",
"js-xxhash": "^4.0.0",
"json5": "^2.2.3",
+ "protobufjs": "^7.5.3",
"svelte-jsoneditor": "^2.4.0",
"unplugin-icons": "^22.1.0"
},
@@ -1435,6 +1439,70 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@protobufjs/aspromise": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmmirror.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
+ "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/base64": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmmirror.com/@protobufjs/base64/-/base64-1.1.2.tgz",
+ "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/codegen": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmmirror.com/@protobufjs/codegen/-/codegen-2.0.4.tgz",
+ "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/eventemitter": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
+ "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/fetch": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/@protobufjs/fetch/-/fetch-1.1.0.tgz",
+ "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.1",
+ "@protobufjs/inquire": "^1.1.0"
+ }
+ },
+ "node_modules/@protobufjs/float": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/@protobufjs/float/-/float-1.0.2.tgz",
+ "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/inquire": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/@protobufjs/inquire/-/inquire-1.1.0.tgz",
+ "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/path": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmmirror.com/@protobufjs/path/-/path-1.1.2.tgz",
+ "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/pool": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/@protobufjs/pool/-/pool-1.1.0.tgz",
+ "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/utf8": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/@protobufjs/utf8/-/utf8-1.1.0.tgz",
+ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
+ "license": "BSD-3-Clause"
+ },
"node_modules/@replit/codemirror-indentation-markers": {
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/@replit/codemirror-indentation-markers/-/codemirror-indentation-markers-6.5.3.tgz",
@@ -1884,6 +1952,12 @@
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
"license": "MIT"
},
+ "node_modules/@types/google-protobuf": {
+ "version": "3.15.12",
+ "resolved": "https://registry.npmmirror.com/@types/google-protobuf/-/google-protobuf-3.15.12.tgz",
+ "integrity": "sha512-40um9QqwHjRS92qnOaDpL7RmDK15NuZYo9HihiJRbYkMQZlWnuH8AdvbMy8/o6lgLmKbDUKa+OALCltHdbOTpQ==",
+ "license": "MIT"
+ },
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -1895,7 +1969,6 @@
"version": "22.13.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.5.tgz",
"integrity": "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.20.0"
@@ -3498,6 +3571,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/google-protobuf": {
+ "version": "3.21.4",
+ "resolved": "https://registry.npmmirror.com/google-protobuf/-/google-protobuf-3.21.4.tgz",
+ "integrity": "sha512-MnG7N936zcKTco4Jd2PX2U96Kf9PxygAPKBug+74LHzmHXmceN16MmRcdgZv+DGef/S9YvQAfRsNCn4cjf9yyQ==",
+ "license": "(BSD-3-Clause AND Apache-2.0)"
+ },
"node_modules/graphemer": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
@@ -3505,6 +3584,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/grpc-web": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmmirror.com/grpc-web/-/grpc-web-1.5.0.tgz",
+ "integrity": "sha512-y1tS3BBIoiVSzKTDF3Hm7E8hV2n7YY7pO0Uo7depfWJqKzWE+SKr0jvHNIJsJJYILQlpYShpi/DRJJMbosgDMQ==",
+ "license": "Apache-2.0"
+ },
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -3955,6 +4040,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/long": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmmirror.com/long/-/long-5.3.2.tgz",
+ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
+ "license": "Apache-2.0"
+ },
"node_modules/loupe": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz",
@@ -4673,6 +4764,30 @@
"svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0"
}
},
+ "node_modules/protobufjs": {
+ "version": "7.5.3",
+ "resolved": "https://registry.npmmirror.com/protobufjs/-/protobufjs-7.5.3.tgz",
+ "integrity": "sha512-sildjKwVqOI2kmFDiXQ6aEB0fjYTafpEvIBs8tOR8qI4spuL9OPROLVu2qZqi/xgCfsHIwVqlaF8JBjWFHnKbw==",
+ "hasInstallScript": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.2",
+ "@protobufjs/base64": "^1.1.2",
+ "@protobufjs/codegen": "^2.0.4",
+ "@protobufjs/eventemitter": "^1.1.0",
+ "@protobufjs/fetch": "^1.1.0",
+ "@protobufjs/float": "^1.0.2",
+ "@protobufjs/inquire": "^1.1.0",
+ "@protobufjs/path": "^1.1.2",
+ "@protobufjs/pool": "^1.1.0",
+ "@protobufjs/utf8": "^1.1.0",
+ "@types/node": ">=13.7.0",
+ "long": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -5718,7 +5833,6 @@
"version": "6.20.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
- "dev": true,
"license": "MIT"
},
"node_modules/unplugin": {
diff --git a/package.json b/package.json
index 6540639..bb47981 100644
--- a/package.json
+++ b/package.json
@@ -44,12 +44,16 @@
"type": "module",
"dependencies": {
"@floating-ui/dom": "^1.6.13",
+ "@types/google-protobuf": "^3.15.12",
"async-mutex": "^0.5.0",
"dompurify": "^3.2.4",
+ "google-protobuf": "^3.21.4",
+ "grpc-web": "^1.5.0",
"highlight.js": "11.11.1",
"ipaddr.js": "^2.2.0",
"js-xxhash": "^4.0.0",
"json5": "^2.2.3",
+ "protobufjs": "^7.5.3",
"svelte-jsoneditor": "^2.4.0",
"unplugin-icons": "^22.1.0"
}
diff --git a/src/lib/Navigation.svelte b/src/lib/Navigation.svelte
index 772d1de..f14cfb0 100644
--- a/src/lib/Navigation.svelte
+++ b/src/lib/Navigation.svelte
@@ -9,6 +9,7 @@
import RawMdiRouter from '~icons/mdi/router';
import RawMdiSecurity from '~icons/mdi/security';
import RawMdiSettings from '~icons/mdi/settings';
+ import RawMdiBug from '~icons/mdi/bug';
// import { ApiKeyInfoStore, ApiKeyStore, hasValidApi } from './Stores';
import { onMount, type Component } from 'svelte';
@@ -49,6 +50,7 @@
{ path: '/routes', name: 'Routes', logo: RawMdiRouter },
{ path: '/acls', name: 'ACLs', logo: RawMdiSecurity },
{ path: '/settings', name: 'Settings', logo: RawMdiSettings },
+ { path: '/grpc-debug', name: 'gRPC Debug', logo: RawMdiBug },
].filter((p) => p != undefined);
const pages = $derived.by(() => App.hasValidApi ? allPages : allPages.slice(-1));
diff --git a/src/lib/States.svelte.ts b/src/lib/States.svelte.ts
index 022847e..80d5a6c 100644
--- a/src/lib/States.svelte.ts
+++ b/src/lib/States.svelte.ts
@@ -1,8 +1,8 @@
import { Mutex } from 'async-mutex';
import { browser } from '$app/environment';
-import type { User, Node, PreAuthKey, ApiKeyInfo, ApiApiKeys, Deployment } from '$lib/common/types';
-import { getUsers, getPreAuthKeys, getNodes } from '$lib/common/api/get';
+import type { User, Node, PreAuthKey, Route, ApiKeyInfo, ApiApiKeys, Deployment, GrpcConfig, GrpcConnectionStatus } from '$lib/common/types';
+import { getUsers, getPreAuthKeys, getNodes, getRoutes } from '$lib/common/api/get';
import type { ToastStore } from '@skeletonlabs/skeleton';
import { apiGet } from './common/api';
import { arraysEqual, clone, toastError, toastWarning } from './common/funcs';
@@ -173,6 +173,22 @@ export class HeadscaleAdmin {
acceptExitNodeValue: '',
})
+ // gRPC configuration
+ grpcConfig = new StateLocal1. Basic Connectivity Test: Check if server is reachable, no API Key required
+2. gRPC Connection Test: Requires valid API Key, tests complete gRPC communication
+3. Envoy Configuration: If using Envoy proxy, use port 8080 and disable TLS
+4. Direct Connection: Direct connection to Headscale uses port 50443 and enable TLS
+{generatedEnvoyConfig}
+ envoy.yamldocker run -d -p {envoyConfig.proxyPort}:{envoyConfig.proxyPort} -v $(pwd)/envoy.yaml:/etc/envoy/envoy.yaml envoyproxy/envoy:v1.28-latestlocalhost, Port {envoyConfig.proxyPort}, TLS disabledgrpcwebproxy \\
+ --backend_addr={envoyConfig.headscaleServer}:{envoyConfig.headscalePort} \\
+ --run_tls_server=false \\
+ --allow_all_origins \\
+ --backend_tls_noverify
+ + Then use in settings: localhost:8000, TLS disabled +
+โ ๏ธ gRPC not configured. Please configure gRPC connection in Settings page first.
+ID: {result.user.id}
+Username: {result.user.name}
+Display Name: {result.user.displayName || '(empty)'}
+Created At: {result.user.createdAt}
+Provider: {result.user.provider}
+{result.error}
+{debugLogs.join('\n')}
+ {JSON.stringify(App.grpcConfig.value, null, 2)}
+ + Configure gRPC connection for advanced features like namespace creation. +
+ ++ Headscale only provides gRPC API by default. You need to configure a gRPC-Web proxy to access it from browsers. +
++ We recommend using Envoy or grpcwebproxy as proxy. + View configuration help or + detailed guide. +
++ If using a proxy, enter the proxy server address (e.g., Envoy proxy address) +
+