Files
holdmysocks--ps5-tailscale/tsd/proxy.go
T
holdmysocks 4554502591 Tailscale for jailbroken PS5
A payload that runs the Tailscale client (tsnet, userspace networking) on a
jailbroken PS5: a C launcher built with ps5-payload-sdk that loads a Go
program in-process, an installer that registers it with the console's
payload autoloader and adds a home screen icon, and the patch that makes Go
1.27.1 speak the PS5's FreeBSD 9 era syscall interface.
2026-10-02 10:29:31 -04:00

120 lines
3.4 KiB
Go

package main
import (
"context"
"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)
}),
}
if err := srv.Serve(ln); err != nil && err != http.ErrServerClosed && !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)
}