feat(ssh): bump restty and follow upstream tailscale ssh

This commit is contained in:
Aarnav Tale
2026-08-27 14:59:47 -07:00
parent b9aa99c45e
commit 29afac60f6
31 changed files with 1653 additions and 1814 deletions
-46
View File
@@ -1,46 +0,0 @@
Fix DERP browser URLs to include non-standard ports.
Tailscale's browser DERP paths ignore DERPPort when building WebSocket
and HTTP-only netcheck probe URLs, causing connections to fail when DERP
servers run on non-443 ports (e.g. :8443). The TCP dial path correctly
handles DERPPort but the browser paths used by WASM builds do not.
--- a/derp/derphttp/derphttp_client.go
+++ b/derp/derphttp/derphttp_client.go
@@ -279,10 +279,19 @@
return c.url.String()
}
proto := "https"
+ var port string
if debugUseDERPHTTP() {
proto = "http"
+ port = "3340"
}
- return fmt.Sprintf("%s://%s/derp", proto, node.HostName)
+ if node != nil && node.DERPPort != 0 {
+ port = fmt.Sprint(node.DERPPort)
+ }
+ host := node.HostName
+ if port != "" {
+ host = net.JoinHostPort(node.HostName, port)
+ }
+ return fmt.Sprintf("%s://%s/derp", proto, host)
}
// AddressFamilySelector decides whether IPv6 is preferred for
--- a/net/netcheck/netcheck.go
+++ b/net/netcheck/netcheck.go
@@ -1103,7 +1103,11 @@
go func() {
defer wg.Done()
node := rg.Nodes[0]
- req, _ := http.NewRequestWithContext(ctx, "HEAD", "https://"+node.HostName+"/derp/probe", nil)
+ host := node.HostName
+ if node.DERPPort != 0 {
+ host = net.JoinHostPort(node.HostName, fmt.Sprint(node.DERPPort))
+ }
+ req, _ := http.NewRequestWithContext(ctx, "HEAD", "https://"+host+"/derp/probe", nil)
// One warm-up one to get HTTP connection set
// up and get a connection from the browser's
// pool.
@@ -0,0 +1,25 @@
netcheck: include DERPPort in the browser HTTPS probe URL
The js/wasm netcheck path probes each DERP region over HTTPS to measure
latency and pick a home relay. It builds that URL from HostName alone, so
a DERP server on a non-443 port is never reachable and the client ends up
with no home DERP.
derphttp's urlString already handles DERPPort; this is the same fix for
the one remaining browser path that does not.
--- a/net/netcheck/netcheck.go
+++ b/net/netcheck/netcheck.go
@@ -1075,7 +1075,11 @@
}
wg.Go(func() {
node := rg.Nodes[0]
- req, _ := http.NewRequestWithContext(ctx, "HEAD", "https://"+node.HostName+"/derp/probe", nil)
+ host := node.HostName
+ if node.DERPPort != 0 && node.DERPPort != 443 {
+ host = net.JoinHostPort(host, fmt.Sprint(node.DERPPort))
+ }
+ req, _ := http.NewRequestWithContext(ctx, "HEAD", "https://"+host+"/derp/probe", nil)
// One warm-up one to get HTTP connection set
// up and get a connection from the browser's
// pool.
+26
View File
@@ -0,0 +1,26 @@
tsconnect: let the caller choose the PTY terminal type and modes
Upstream requests a bare "xterm" PTY with no terminal modes. Headplane
renders with Ghostty and ships a Nerd Font, so it needs xterm-256color
and sane modes for anything colour-aware on the far side.
Applied to cmd/hp_ssh/wasm_js.go by scripts/sync-tsconnect.sh.
--- a/wasm/wasm_js.go
+++ b/wasm/wasm_js.go
@@ -484,7 +484,14 @@
if s.pendingResizeCols != 0 {
cols = s.pendingResizeCols
}
- err = session.RequestPty("xterm", rows, cols, ssh.TerminalModes{})
+ termType := "xterm"
+ if v := s.termConfig.Get("termType"); v.Type() == js.TypeString {
+ termType = v.String()
+ }
+ err = session.RequestPty(termType, rows, cols, ssh.TerminalModes{
+ ssh.ECHO: 1, ssh.ICANON: 1, ssh.ISIG: 1, ssh.ICRNL: 1, ssh.IUTF8: 1,
+ ssh.TTY_OP_ISPEED: 14400, ssh.TTY_OP_OSPEED: 14400,
+ })
if err != nil {
writeError("Pseudo Terminal", err)
return
+38
View File
@@ -0,0 +1,38 @@
tsconnect: type the termType option and the wasm_exec Go global
termType matches patches/tsconnect-term-type.patch. Go is declared by the
wasm_exec.js helper the Go toolchain ships, which we load ourselves rather
than through @tailscale/connect.
@@ -4,11 +4,22 @@
/**
* @fileoverview Type definitions for types exported by the wasm_js.go Go
* module.
+ *
+ * Vendored from tailscale.com/cmd/tsconnect/src/types/wasm_js.d.ts; see
+ * cmd/hp_ssh/wasm_js.go for the upstream ref. Local changes live in
+ * patches/tsconnect-types.patch and are already applied here.
*/
declare global {
function newIPN(config: IPNConfig): IPN
+ var Go: {
+ new (): {
+ importObject: WebAssembly.Imports
+ run(instance: WebAssembly.Instance): Promise<void>
+ }
+ }
+
interface IPN {
run(callbacks: IPNCallbacks): void
login(): void
@@ -22,6 +33,8 @@
setReadFn: (readFn: (data: string) => void) => void
rows: number
cols: number
+ /** Defaults to "xterm" */
+ termType?: string
/** Defaults to 5 seconds */
timeoutSeconds?: number
onConnectionProgress: (message: string) => void