mirror of
https://github.com/holdmysocks/ps5-tailscale.git
synced 2026-10-06 15:00:19 +02:00
The status page can now be protected with a password and edits the settings itself, so the config file no longer has to be changed by hand. - Password for everything on the status page that shows or changes something. The console's own browser is exempt. Connections to the page from the tailnet are served directly so they are not taken for local. - Settings form: name, ports, forwards, proxy, priority, update checks. Most take effect at once; the page says which need a restart. - Game streaming: several Sunshine hosts, each with its own port. - The HTTP proxy is off by default. - The page says when a newer release exists. - Uninstall removes the home screen icon. - Priority setting for streams that stutter under a demanding game. - After the console's network is reconfigured, Tailscale is asked to rebind. Seen working across a short stay in rest mode. - Favicon, and the device list can be collapsed.
123 lines
3.6 KiB
Go
123 lines
3.6 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/netip"
|
|
"time"
|
|
)
|
|
|
|
// serveProxy runs a plain HTTP proxy whose outbound connections go through
|
|
// Tailscale's dialer: tailnet addresses and MagicDNS names are reached over
|
|
// the tailnet, everything else goes out directly. Setting it as the proxy
|
|
// server in the PS5's network settings lets the console's own apps, such as
|
|
// the browser, open tailnet hosts.
|
|
func (d *daemon) serveProxy(ln net.Listener) {
|
|
d.logf("http proxy listening on %s", ln.Addr())
|
|
transport := &http.Transport{
|
|
DialContext: d.proxyDial,
|
|
ForceAttemptHTTP2: false,
|
|
MaxIdleConns: 32,
|
|
IdleConnTimeout: 60 * time.Second,
|
|
ResponseHeaderTimeout: 60 * time.Second,
|
|
}
|
|
srv := &http.Server{
|
|
ReadHeaderTimeout: 15 * time.Second,
|
|
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method == http.MethodConnect {
|
|
d.proxyConnect(w, r)
|
|
return
|
|
}
|
|
if !r.URL.IsAbs() {
|
|
http.Error(w, "this is a proxy; requests must use an absolute URL", http.StatusBadRequest)
|
|
return
|
|
}
|
|
r.RequestURI = ""
|
|
r.Header.Del("Proxy-Connection")
|
|
r.Header.Del("Proxy-Authorization")
|
|
resp, err := transport.RoundTrip(r)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadGateway)
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
for k, vs := range resp.Header {
|
|
for _, v := range vs {
|
|
w.Header().Add(k, v)
|
|
}
|
|
}
|
|
w.WriteHeader(resp.StatusCode)
|
|
io.Copy(w, resp.Body)
|
|
}),
|
|
}
|
|
// Serve returns when the listener is closed, which is how the proxy is
|
|
// turned off or moved from the settings.
|
|
if err := srv.Serve(ln); err != nil && err != http.ErrServerClosed && !errors.Is(err, net.ErrClosed) && !d.stopping() {
|
|
d.logf("http proxy stopped: %v", err)
|
|
}
|
|
}
|
|
|
|
// proxyDial connects on behalf of a proxy client. Tailscale's dialer handles
|
|
// tailnet addresses and names but waits until Tailscale is connected, so it
|
|
// is only used once that is the case, and never for the console's own or
|
|
// LAN addresses: those must keep working whatever state Tailscale is in.
|
|
func (d *daemon) proxyDial(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
d.mu.Lock()
|
|
running := d.state == "Running"
|
|
d.mu.Unlock()
|
|
if running && !isLocalDestination(addr) {
|
|
return d.srv.Dial(ctx, network, addr)
|
|
}
|
|
var direct net.Dialer
|
|
return direct.DialContext(ctx, network, addr)
|
|
}
|
|
|
|
// isLocalDestination reports whether addr is a literal loopback, private or
|
|
// link-local address, or "localhost".
|
|
func isLocalDestination(addr string) bool {
|
|
host, _, err := net.SplitHostPort(addr)
|
|
if err != nil {
|
|
host = addr
|
|
}
|
|
if host == "localhost" {
|
|
return true
|
|
}
|
|
ip, err := netip.ParseAddr(host)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || ip.IsUnspecified()
|
|
}
|
|
|
|
func (d *daemon) proxyConnect(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
|
|
defer cancel()
|
|
upstream, err := d.proxyDial(ctx, "tcp", r.Host)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadGateway)
|
|
return
|
|
}
|
|
defer upstream.Close()
|
|
|
|
hj, ok := w.(http.Hijacker)
|
|
if !ok {
|
|
http.Error(w, "hijacking not supported", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
client, buf, err := hj.Hijack()
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer client.Close()
|
|
io.WriteString(client, "HTTP/1.1 200 Connection established\r\n\r\n")
|
|
// Anything the client sent right after its CONNECT request is already
|
|
// sitting in the server's read buffer.
|
|
if n := buf.Reader.Buffered(); n > 0 {
|
|
io.CopyN(upstream, buf.Reader, int64(n))
|
|
}
|
|
pipe(client, upstream)
|
|
}
|