Skip to content

feat(tailscale): add Tailscale Serve support.#115

Open
sarinex wants to merge 2 commits intosipeed:mainfrom
sarinex:feature/tailscale-serve
Open

feat(tailscale): add Tailscale Serve support.#115
sarinex wants to merge 2 commits intosipeed:mainfrom
sarinex:feature/tailscale-serve

Conversation

@sarinex
Copy link
Copy Markdown

@sarinex sarinex commented Mar 4, 2026

Summary

Adds Tailscale Serve integration, letting users expose the NanoKVM web UI over HTTPS with automatic TLS certificates — all from a simple toggle in the existing Tailscale settings panel.

While all Tailscale traffic is already encrypted end-to-end, browsers still display connection warnings when accessing NanoKVM via plain HTTP. This feature uses Tailscale Serve to provision public-CA-signed certificates for the device's .ts.net domain, giving users a fully browser-trusted https://<device>.tailnet.ts.net URL with zero manual certificate management.

Motivation

  • Security UX: Eliminates browser "Not Secure" warnings when accessing NanoKVM over Tailscale.
  • Zero configuration: No need to manually provision certificates, configure reverse proxies etc. The toggle handles everything.
  • Complements existing Tailscale integration: This is a natural extension of the Tailscale panel already present in NanoKVM. Users who already use Tailscale for remote access get HTTPS "for free".

What Changed

Backend (Go)

File Change
server/proto/tailscale.go Added Serve and ServeUrl fields to GetTailscaleStatusRsp; new ServeTailscaleReq/ServeTailscaleRsp types
server/router/extensions.go Registered POST /api/extensions/tailscale/serve route
server/service/extensions/tailscale/cli.go New ServeStatus() and Serve(enable) CLI wrapper methods
server/service/extensions/tailscale/service.go New Serve() handler; GetStatus() now also returns serve state

Key implementation details

  • cli.Serve(enable) runs tailscale serve --bg --https=443 https+insecure://localhost:443 and reads the process output asynchronously:
    • If Tailscale needs the admin to enable HTTPS certificates on the tailnet, it returns an authorization URL (https://login.tailscale.com/f/serve...).
    • Otherwise the serve starts silently and the handler queries tailscale serve status (after a short settle delay) to retrieve the resulting HTTPS URL.
  • cli.ServeStatus() parses tailscale serve status output to determine if serve is active and extracts the .ts.net HTTPS URL.
  • Status polling now includes serve: bool and serveUrl: string so the frontend always reflects the current serve state on load.

Frontend (React/TypeScript)

File Change
web/src/api/extensions/tailscale.ts New serve(enable) API function
web/src/i18n/locales/en.ts 10 new English translation keys
web/src/pages/desktop/menu/settings/tailscale/device.tsx "Enable HTTPS" toggle with auth consent flow, HTTPS URL display, auto-redirect
web/src/pages/desktop/menu/settings/tailscale/types.ts Added serve and serveUrl to Status type

User flow

  1. User toggles "Enable HTTPS" in the Tailscale settings panel.
  2. If the tailnet doesn't have HTTPS certificates enabled yet, an authorization link is shown and opened in a new tab. After authorizing, the user clicks "Authorization Success" to retry.
  3. Once enabled, the panel shows the HTTPS URL with a green lock icon. If the user is currently accessing NanoKVM via the Tailscale IP, the browser automatically redirects to the new HTTPS URL.
  4. Disabling HTTPS shows a confirmation popover listing fallback access methods (wired IP, wireless IP, Tailscale IP). If the user is on the .ts.net URL, the browser automatically redirects to the Tailscale IP fallback.

Scope & Compatibility

  • No breaking changes: Existing behavior is untouched. The HTTPS toggle defaults to off; the Tailscale panel works exactly as before until the user opts in.
  • No new dependencies: Uses only the existing tailscale CLI binary that is already managed by NanoKVM.
  • Backward-compatible API: The two new fields in GetTailscaleStatusRsp (serve, serveUrl) are additive — older frontends will simply ignore them.
  • Only English translations included: Other locale files are not modified — the new keys will gracefully fall back to English until community translations are added.

Testing

All scenarios were verified on a physical NanoKVM device with Tailscale installed and logged in:

Scenario Steps Result
Enable (certs already enabled) Toggle "Enable HTTPS" on a tailnet with HTTPS certificates already provisioned Serve starts, HTTPS URL displayed, auto-redirect works
Enable (auth required) Toggle "Enable HTTPS" on a tailnet that has not yet enabled HTTPS certificates Auth consent URL shown and opened in new tab; after authorizing in the Tailscale admin console, clicking "Authorization Success" re-runs serve and completes successfully
Disable Toggle "Enable HTTPS" off while serve is active Confirmation popover shown with fallback IPs; serve stopped; auto-redirect to Tailscale IP when accessed via .ts.net
Status persistence Refresh the page while serve is active / inactive UI correctly reflects the current serve state on load
Tailscale toggle interaction Toggle Tailscale off/on while serve is active Serve state resets correctly

Copilot AI review requested due to automatic review settings March 4, 2026 21:44
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds Tailscale Serve integration to NanoKVM, allowing users to expose the web UI over HTTPS with browser-trusted TLS certificates via a toggle in the Tailscale settings panel. It introduces a new backend endpoint to enable/disable tailscale serve, CLI wrappers to manage serve state, and a frontend toggle with authorization consent flow and auto-redirect behavior.

Changes:

  • Backend: New POST /api/extensions/tailscale/serve endpoint with Cli.Serve() and Cli.ServeStatus() CLI wrappers, plus serve and serveUrl fields added to the status response.
  • Frontend: "Enable HTTPS" toggle in the Tailscale device panel with auth consent flow, HTTPS URL display, disable confirmation with fallback IPs, and automatic browser redirect.
  • New English i18n keys for all HTTPS-related UI strings.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
server/proto/tailscale.go Added Serve/ServeUrl to status response; new ServeTailscaleReq/ServeTailscaleRsp types
server/router/extensions.go Registered POST /api/extensions/tailscale/serve route
server/service/extensions/tailscale/cli.go New ServeStatus() and Serve(enable) methods for CLI interaction
server/service/extensions/tailscale/service.go New Serve() handler; GetStatus() now includes serve state
web/src/api/extensions/tailscale.ts New serve(enable) API function
web/src/i18n/locales/en.ts 10 new English translation keys for HTTPS feature
web/src/pages/desktop/menu/settings/tailscale/device.tsx HTTPS toggle UI with auth flow, URL display, and redirect logic
web/src/pages/desktop/menu/settings/tailscale/types.ts Added serve and serveUrl to Status type

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

- Disable "Enable HTTPS" toggle when Tailscale is not running
- Show error message when auth consent was not completed
- Fix swallowed error in Serve() by checking cmd.Wait() exit code
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

func (c *Cli) Serve(enable bool) (string, error) {
command := serveBaseCommand
if !enable {
command += " off"
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The disable command is constructed by appending " off" to serveBaseCommand, resulting in "tailscale serve --bg --https=443 https+insecure://localhost:443 off". This passes both the proxy target URL and off as positional arguments. The idiomatic Tailscale CLI usage for disabling is tailscale serve --https=443 off (where off replaces the target URL). While this may work in practice if the CLI ignores the extra positional arg, it's fragile and may break with future Tailscale CLI updates. Consider constructing the disable command separately (e.g., "tailscale serve --https=443 off") rather than appending to the base command.

Suggested change
command += " off"
parts := strings.Fields(serveBaseCommand)
if len(parts) == 0 {
return "", errors.New("invalid tailscale serve base command")
}
parts[len(parts)-1] = "off"
command = strings.Join(parts, " ")

Copilot uses AI. Check for mistakes.
// If it still wants authorization, the user probably didn't authorize it properly
if (rsp.data && rsp.data.authUrl) {
setAuthConsentUrl('');
setErrMsg(t('settings.tailscale.enableHttpsCerts'));
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The enableHttpsCerts translation key contains HTML-like interpolation tags (<1>HTTPS certificates</1>), which are processed correctly by the <Trans> component at line 251. However, here at line 138, t('settings.tailscale.enableHttpsCerts') is used directly as an error message, which will render the raw <1> tags as literal text in the error message (e.g., "Enable <1>HTTPS certificates</1> on your tailnet to continue"). Consider using a separate translation key without interpolation tags for the error message, or stripping the tags before setting the error.

Copilot uses AI. Check for mistakes.
Comment on lines +212 to +223
<a href={`https://${ip.addr}`} target="_blank" rel="noreferrer" className="text-blue-500 hover:underline">
https://{ip.addr}
</a>
</div>
))}
<div className="flex items-center space-x-2">
<div className="size-[16px] flex items-center justify-center">
<Tailscale />
</div>
<a href={`https://${status.ip}`} target="_blank" rel="noreferrer" className="text-blue-500 hover:underline">
https://{status.ip}
</a>
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fallback IP links in the disable-HTTPS confirmation popover all use https:// (lines 212-214 for wired/wireless IPs, and lines 221-222 for the Tailscale IP). However, since HTTPS is being disabled, these IPs won't have valid TLS certificates, so https:// links will result in browser certificate errors. This is also inconsistent with redirectToFallback() on line 60 which correctly uses http://. These links should use http:// to match the actual protocol available after disabling HTTPS serve.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants