diff --git a/README.md b/README.md index a7f9a16..78a553a 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,7 @@ Puts a jailbroken PS5 on your [Tailscale](https://tailscale.com) network. your other Tailscale devices. - **Stream games to the console over Tailscale.** A Moonlight client on the PS5 (such as ProsperoLight) can connect to a Sunshine host on your tailnet. -- **Starts by itself** after each jailbreak if you use a payload autoloader, - and adds a home screen icon for its status page. +- **Home screen icon** that opens its status page. It runs the real Tailscale client code (v1.104.0) as a single payload. @@ -36,11 +35,8 @@ VPN here. It runs inside one process: - A jailbroken PS5 with an ELF loader listening on port 9021 (for example [elfldr](https://github.com/ps5-payload-dev/elfldr)). - A Tailscale account. -- Optional: a payload autoloader, so Tailscale starts after every jailbreak. - [Payload Manager](https://github.com/itsPLK/ps5-payload-manager) and - `ps5_autoloader` folders are detected. -Tested on firmware 13.42 with elfldr 0.26 and Payload Manager 0.5.2. +Tested on firmware 13.42 with elfldr 0.26. ## Install @@ -59,8 +55,7 @@ Tested on firmware 13.42 with elfldr 0.26 and Payload Manager 0.5.2. ``` The installer prints what it does. It: - - stores the daemon payload (`tailscale.elf`), - - adds it to the end of your autoloader's load order, if it finds one, + - stores the daemon payload as `/data/tailscale/tailscale.elf`, - adds a **Tailscale** icon to the home screen (media section), - starts Tailscale. @@ -73,6 +68,11 @@ The console now has a tailnet address, shown on the status page. Sending the installer again upgrades and restarts Tailscale. The login is kept. +Tailscale runs until the console restarts. After a restart and jailbreak, +send `/data/tailscale/tailscale.elf` (or the installer) to the ELF loader +again. The installer does not change any payload autoloader; if you use one, +you can add that file to it yourself. + ## Using it ### Reaching the console @@ -141,7 +141,7 @@ The daemon also runs an HTTP proxy on `127.0.0.1:8118` that reaches tailnet hosts, for apps that have their own proxy setting. **Do not set it as the PS5's system proxy.** The system then sends everything through it, including pages on `127.0.0.1`, and it is not running until Tailscale has been loaded. -On the test console that stopped Payload Manager's page from opening. +On the test console that stopped another homebrew tool's page from opening. ## Configuration @@ -184,20 +184,19 @@ Files on the console: | `/data/tailscale/state/` | Tailscale's state, including the login. | | `/data/tailscale/tailscale.log` | The daemon's log, rotated at 2 MB. | | `/data/tailscale/tailscale-debug.log` | Tailscale's detailed log, up to 4 MB plus one older file. | -| `/data/pldmgr/payloads/Tailscale/tailscale.elf` | The daemon, when Payload Manager is used. | -| `/data/tailscale/tailscale.elf` | The daemon, when no autoloader was found. | +| `/data/tailscale/tailscale.elf` | The daemon payload. | | `/user/app/TSCL00001/` | The home screen icon. | ## Uninstall -Press **Uninstall** on the status page. It removes the autoloader entry and -the daemon payload and stops Tailscale. It asks whether to also log out and -delete the saved login. +Press **Uninstall** on the status page. It deletes the daemon payload and +stops Tailscale. It asks whether to also log out and delete the saved login. Two things are left to do by hand: - Delete the home screen icon (Options button, then Delete). - Remove the device in the Tailscale admin console. +- If you added the payload to an autoloader yourself, remove it there. ## Troubleshooting @@ -229,13 +228,12 @@ lowest scheduling priority on at most 4 cores, so it gives way to games. ## What has and has not been tested Tested on the one console: install and upgrade, login with device approval, -autostart after a reboot through Payload Manager, reaching the console over +starting again after a reboot with the saved login, reaching the console over the tailnet, a ProsperoLight stream from a Sunshine host through the forward, the HTTP proxy, the home screen icon. -Not tested: rest mode, the `ps5_autoloader` and USB autoloader paths, -Uninstall on a console, other firmware versions, coordination servers other -than Tailscale's. +Not tested: rest mode, Uninstall on a console, other firmware versions, +coordination servers other than Tailscale's. ## Building diff --git a/docs/BUILDING.md b/docs/BUILDING.md index 7336af8..31ba7a2 100644 --- a/docs/BUILDING.md +++ b/docs/BUILDING.md @@ -52,7 +52,7 @@ These folders are not in the repository. ```powershell # daemon payload: C launcher + Go program -> out\tailscale.elf -.\tools\build-payload.ps1 -GoDir tsd -Name tailscale -Version 0.2.1 +.\tools\build-payload.ps1 -GoDir tsd -Name tailscale -Version 0.3.0 # installer -> out\tailscale-installer.elf (embeds out\tailscale.elf) .\tools\build-installer.ps1 diff --git a/docs/TECHNICAL.md b/docs/TECHNICAL.md index 27449ce..792cf9d 100644 --- a/docs/TECHNICAL.md +++ b/docs/TECHNICAL.md @@ -37,10 +37,11 @@ specification. - A payload that is sent again stops the running instance (through the status page, or failing that by the pid it recorded) and takes over. -**The installer** (`installer/`, C) embeds `tailscale.elf`. It stores it -where the console's autoloader looks, appends it to the load order, registers -a home screen app whose `param.json` has a `deeplinkUri` to the status page, -and starts the daemon by sending it to the ELF loader on `127.0.0.1:9021`. +**The installer** (`installer/`, C) embeds `tailscale.elf`. It writes it to +`/data/tailscale/tailscale.elf`, registers a home screen app whose +`param.json` has a `deeplinkUri` to the status page, and starts the daemon by +sending it to the ELF loader on `127.0.0.1:9021`. It does not modify any +payload autoloader. ## The PS5 as a Go target @@ -136,17 +137,6 @@ Linking `libSceAppInstUtil` alone leaves the payload stopped before it runs. It needs `-lSceIpmi -lSceAppInstUtil -lSceUserService -lSceSystemService`, in that order, as in the SDK's `install_app` sample. -## Autoloaders - -- Payload Manager: load order in `/data/pldmgr/autoload.txt`, one file name - per line, `!N` for a delay in milliseconds. It finds a named file anywhere - below `/data/pldmgr` and launches it by streaming it to `127.0.0.1:9021`. -- `ps5_autoloader`: `autoload.txt` in `/data/ps5_autoloader` or the same - folder on a USB drive, with the payloads next to it. - -The table of autoloaders exists twice, in `installer/main.c` and -`tsd/autostart.go`. - ## Known problems - Once, switching the console from Wi-Fi to Ethernet during a Moonlight diff --git a/docs/status-page.png b/docs/status-page.png index 79e400c..e0fce8f 100644 Binary files a/docs/status-page.png and b/docs/status-page.png differ diff --git a/installer/main.c b/installer/main.c index 3c0ff0d..bb29944 100644 --- a/installer/main.c +++ b/installer/main.c @@ -1,9 +1,9 @@ /* Tailscale installer payload for jailbroken PS5s. * - * Stores the Tailscale daemon payload on the console, registers it with the - * payload autoloader if one is set up, adds a home screen icon that opens - * the status page, and starts the daemon through the ELF loader on this - * console. Build with tools\build-installer.ps1, which sets DAEMON_ELF and + * Stores the Tailscale daemon payload on the console, adds a home screen + * icon that opens the status page, and starts the daemon through the ELF + * loader on this console. It does not touch any payload autoloader. Build + * with tools\build-installer.ps1, which sets DAEMON_ELF and * ASSET_DIR and links the system libraries the app installer needs. */ #include @@ -69,23 +69,6 @@ int sceAppInstUtilInitialize(void); int sceAppInstUtilTerminate(void); int sceAppInstUtilAppInstallAll(void *); -/* Where payload autoloaders keep their load order, and where the daemon has - * to be stored for each. Keep in sync with tsd/autostart.go. */ -static const struct autoloader { - const char *name; - const char *list; /* load order: one file name per line */ - const char *payload_dir; /* where the payload goes */ -} autoloaders[] = { - /* Payload Manager finds entries by file name anywhere below /data/pldmgr - * and keeps each payload in a folder of its own. */ - {"Payload Manager", "/data/pldmgr/autoload.txt", "/data/pldmgr/payloads/Tailscale"}, - {"PS5 autoloader", "/data/ps5_autoloader/autoload.txt", "/data/ps5_autoloader"}, - {"PS5 autoloader (usb0)", "/mnt/usb0/ps5_autoloader/autoload.txt", "/mnt/usb0/ps5_autoloader"}, - {"PS5 autoloader (usb1)", "/mnt/usb1/ps5_autoloader/autoload.txt", "/mnt/usb1/ps5_autoloader"}, - {"PS5 autoloader (usb2)", "/mnt/usb2/ps5_autoloader/autoload.txt", "/mnt/usb2/ps5_autoloader"}, - {"PS5 autoloader (usb3)", "/mnt/usb3/ps5_autoloader/autoload.txt", "/mnt/usb3/ps5_autoloader"}, -}; - typedef struct notify_request { char useless1[45]; char message[3075]; @@ -122,7 +105,7 @@ write_all(int fd, const uint8_t *data, size_t size) { } /* Write a file through a temporary name so a failed write never leaves a - * truncated payload where the autoloader would pick it up. */ + * truncated payload behind. */ static int write_file(const char *path, const uint8_t *data, size_t size) { char tmp[512]; @@ -159,71 +142,6 @@ install_daemon(const char *dir) { return 0; } -/* Report whether the load order already lists the daemon on a line of its own. */ -static int -autoload_lists_daemon(const char *text) { - size_t namelen = strlen(DAEMON_NAME); - - for (const char *line = text; line && *line;) { - const char *end = strchr(line, '\n'); - size_t len = end ? (size_t)(end - line) : strlen(line); - while (len > 0 && (line[len - 1] == '\r' || line[len - 1] == ' ' || line[len - 1] == '\t')) { - len--; - } - if (len == namelen && !strncmp(line, DAEMON_NAME, namelen)) { - return 1; - } - line = end ? end + 1 : 0; - } - return 0; -} - -/* If this autoloader is set up on the console, store the daemon for it and - * add it to the end of the load order. Returns 1 if registered, 0 if the - * autoloader is not present, -1 on error. */ -static int -register_autoload(const struct autoloader *al) { - struct stat st; - char *text; - size_t got; - FILE *f; - - if (stat(al->list, &st)) { - return 0; - } - printf("%s:\n", al->name); - if (install_daemon(al->payload_dir)) { - return -1; - } - - if (!(f = fopen(al->list, "rb"))) { - printf(" could not read %s: %s\n", al->list, strerror(errno)); - return -1; - } - text = calloc(1, st.st_size + 1); - got = fread(text, 1, st.st_size, f); - fclose(f); - text[got] = 0; - - if (autoload_lists_daemon(text)) { - printf(" %s already lists %s\n", al->list, DAEMON_NAME); - } else { - if (!(f = fopen(al->list, "ab"))) { - printf(" could not update %s: %s\n", al->list, strerror(errno)); - free(text); - return -1; - } - if (got > 0 && text[got - 1] != '\n') { - fputc('\n', f); - } - fputs(DAEMON_NAME "\n", f); - fclose(f); - printf(" added %s to the end of %s\n", DAEMON_NAME, al->list); - } - free(text); - return 1; -} - /* Report whether the file at path already has exactly these contents. */ static int file_matches(const char *path, const uint8_t *data, size_t size) { @@ -346,7 +264,6 @@ int main(void) { pid_t pid = getpid(); intptr_t rootvnode; - int registered = 0; setvbuf(stdout, 0, _IONBF, 0); @@ -368,23 +285,11 @@ main(void) { #endif printf("Tailscale for PS5 installer\n\n"); - mkdir(DATA_DIR, 0755); - for (size_t i = 0; i < sizeof(autoloaders) / sizeof(autoloaders[0]); i++) { - if (register_autoload(&autoloaders[i]) > 0) { - registered++; - } - } - if (!registered) { - /* No autoloader: keep the payload where the user can find it. */ - printf("No payload autoloader found on this console.\n"); - if (install_daemon(DATA_DIR)) { - notify("Tailscale install failed:\ncould not write to %s", DATA_DIR); - return 1; - } - printf(" Tailscale will not start by itself after a reboot.\n" - " Send %s/%s to the ELF loader to start it.\n", - DATA_DIR, DAEMON_NAME); + printf("Daemon payload:\n"); + if (install_daemon(DATA_DIR)) { + notify("Tailscale install failed:\ncould not write to %s", DATA_DIR); + return 1; } printf("Home screen icon:\n"); @@ -397,6 +302,9 @@ main(void) { return 1; } - printf("\nDone. The status page is on port 8090 of this console.\n"); + printf("\nDone. The status page is on port 8090 of this console.\n" + "Tailscale runs until the console restarts. To start it again, send\n" + "%s/%s to the ELF loader.\n", + DATA_DIR, DAEMON_NAME); return 0; } diff --git a/tsd/autostart.go b/tsd/autostart.go deleted file mode 100644 index 888047b..0000000 --- a/tsd/autostart.go +++ /dev/null @@ -1,114 +0,0 @@ -package main - -import ( - "errors" - "io/fs" - "os" - "path/filepath" - "strings" -) - -// The installer registers the daemon with whichever payload autoloader the -// console uses: it copies the payload where the autoloader looks for files -// and adds its file name to the autoloader's load order (one payload per -// line, "!N" lines are delays). This file is the daemon's side of that: -// reporting the registration and undoing it. Keep the table in sync with -// installer/main.c. - -const daemonFileName = "tailscale.elf" - -// An autoloader is one place a payload autoloader keeps its load order. -type autoloader struct { - name string // for messages - list string // the load order file - payloadDir string // where the daemon payload is stored for it -} - -var autoloaders = func() []autoloader { - al := []autoloader{ - // Payload Manager finds entries by file name anywhere below - // /data/pldmgr and keeps each payload in a folder of its own. - {"Payload Manager", "/data/pldmgr/autoload.txt", "/data/pldmgr/payloads/Tailscale"}, - {"PS5 autoloader", "/data/ps5_autoloader/autoload.txt", "/data/ps5_autoloader"}, - } - for _, usb := range []string{"usb0", "usb1", "usb2", "usb3"} { - dir := "/mnt/" + usb + "/ps5_autoloader" - al = append(al, autoloader{"PS5 autoloader (" + usb + ")", dir + "/autoload.txt", dir}) - } - return al -}() - -// autostartNames returns the autoloaders whose load order includes the daemon. -func autostartNames() []string { - names := []string{} - for _, al := range autoloaders { - b, err := os.ReadFile(al.list) - if err != nil { - continue - } - if _, found := removeAutoloadEntry(string(b), daemonFileName); found { - names = append(names, al.name) - } - } - return names -} - -// removeAutoloadEntry returns text without the lines that name the payload, -// and whether there were any. Everything else, including line endings, is -// kept as it was. -func removeAutoloadEntry(text, name string) (string, bool) { - var out strings.Builder - found := false - for len(text) > 0 { - line := text - if i := strings.IndexByte(text, '\n'); i >= 0 { - line = text[:i+1] - } - text = text[len(line):] - if strings.TrimSpace(line) == name { - found = true - continue - } - out.WriteString(line) - } - return out.String(), found -} - -// uninstall removes the daemon from every autoloader and deletes the -// installed payload. With purge it also deletes the Tailscale state and -// config, which forgets the login. The running process is not affected. -func uninstall(purge bool, logf func(string, ...any)) error { - var errs []error - remove := func(path string) { - if err := os.Remove(path); err != nil && !errors.Is(err, fs.ErrNotExist) { - errs = append(errs, err) - } - } - for _, al := range autoloaders { - b, err := os.ReadFile(al.list) - if err != nil { - continue - } - if text, found := removeAutoloadEntry(string(b), daemonFileName); found { - if err := os.WriteFile(al.list, []byte(text), 0o644); err != nil { - errs = append(errs, err) - continue - } - logf("removed %s from %s", daemonFileName, al.list) - } - remove(filepath.Join(al.payloadDir, daemonFileName)) - if filepath.Base(al.payloadDir) == "Tailscale" { - // A folder the installer created just for this payload. - os.Remove(al.payloadDir) - } - } - remove(filepath.Join(dataDir, daemonFileName)) - if purge { - for _, name := range []string{"state", "config.json", ".cache"} { - if err := os.RemoveAll(filepath.Join(dataDir, name)); err != nil { - errs = append(errs, err) - } - } - } - return errors.Join(errs...) -} diff --git a/tsd/autostart_test.go b/tsd/forward_test.go similarity index 58% rename from tsd/autostart_test.go rename to tsd/forward_test.go index 8a65253..fa43398 100644 --- a/tsd/autostart_test.go +++ b/tsd/forward_test.go @@ -6,27 +6,6 @@ import ( "testing" ) -func TestRemoveAutoloadEntry(t *testing.T) { - tests := []struct { - name, in, want string - found bool - }{ - {"absent", "kstuff.elf\n!500\nftpsrv.elf\n", "kstuff.elf\n!500\nftpsrv.elf\n", false}, - {"last line", "kstuff.elf\ntailscale.elf\n", "kstuff.elf\n", true}, - {"no trailing newline", "kstuff.elf\ntailscale.elf", "kstuff.elf\n", true}, - {"middle, CRLF kept", "a.elf\r\ntailscale.elf\r\nb.elf\r\n", "a.elf\r\nb.elf\r\n", true}, - {"listed twice", "tailscale.elf\na.elf\n tailscale.elf \n", "a.elf\n", true}, - {"similar names stay", "# tailscale.elf\nmy-tailscale.elf\ntailscale.elf.bak\n", "# tailscale.elf\nmy-tailscale.elf\ntailscale.elf.bak\n", false}, - {"empty", "", "", false}, - } - for _, tt := range tests { - got, found := removeAutoloadEntry(tt.in, daemonFileName) - if got != tt.want || found != tt.found { - t.Errorf("%s: got %q, %v; want %q, %v", tt.name, got, found, tt.want, tt.found) - } - } -} - // pipe must pass a half-close through: the ELF loader protocol and FTP data // connections both rely on the reader seeing EOF while the other direction // stays open. diff --git a/tsd/status.html b/tsd/status.html index f8dbf09..f931cc8 100644 --- a/tsd/status.html +++ b/tsd/status.html @@ -161,7 +161,6 @@ async function refresh() { if (s.ips.length) row(dl, 'Tailnet address', s.ips.join(', '), true); if (s.tailnet) row(dl, 'Tailnet', s.tailnet); if (s.proxy) row(dl, 'HTTP proxy', s.proxy, true); - row(dl, 'Starts with the console', s.autostart.length ? 'Yes, via ' + s.autostart.join(', ') : 'No'); const health = $('health'); health.replaceChildren(...(s.health || []).map(h => { const li = document.createElement('li'); li.textContent = h; return li; })); @@ -240,7 +239,7 @@ $('btn-sunshine').onclick = async () => { $('sunshine-select').dataset.signature = ''; }; $('btn-uninstall').onclick = () => { - if (!confirm('Remove Tailscale from this PS5? It will stop now and no longer start with the console.')) return; + if (!confirm('Remove Tailscale from this PS5? It stops now and its payload is deleted.')) return; const purge = confirm('Also log out and delete the saved login?\n\nOK: delete everything.\nCancel: keep the login, so reinstalling reconnects without logging in again.'); act('/api/uninstall' + (purge ? '?purge=1' : '')); }; diff --git a/tsd/uninstall.go b/tsd/uninstall.go new file mode 100644 index 0000000..7783e1e --- /dev/null +++ b/tsd/uninstall.go @@ -0,0 +1,30 @@ +package main + +import ( + "errors" + "io/fs" + "os" + "path/filepath" +) + +// daemonFileName is the name the installer stores the daemon payload under, +// in the data directory. +const daemonFileName = "tailscale.elf" + +// uninstall deletes the installed daemon payload. With purge it also deletes +// the Tailscale state and config, which forgets the login. The running +// process is not affected. +func uninstall(purge bool) error { + var errs []error + if err := os.Remove(filepath.Join(dataDir, daemonFileName)); err != nil && !errors.Is(err, fs.ErrNotExist) { + errs = append(errs, err) + } + if purge { + for _, name := range []string{"state", "config.json", ".cache"} { + if err := os.RemoveAll(filepath.Join(dataDir, name)); err != nil { + errs = append(errs, err) + } + } + } + return errors.Join(errs...) +} diff --git a/tsd/web.go b/tsd/web.go index 40471da..7dd7107 100644 --- a/tsd/web.go +++ b/tsd/web.go @@ -43,8 +43,6 @@ type statusInfo struct { Health []string `json:"health,omitempty"` Peers []peerInfo `json:"peers"` Proxy string `json:"proxy,omitempty"` - // Autostart lists the autoloaders that start the daemon. - Autostart []string `json:"autostart"` // SunshineHost and Forwards describe the local forwards. SunshineHost string `json:"sunshineHost"` Forwards []string `json:"forwards"` @@ -141,7 +139,6 @@ func (d *daemon) handleStatus(w http.ResponseWriter, r *http.Request) { } info.SunshineHost = d.cfg.SunshineHost d.mu.Unlock() - info.Autostart = autostartNames() info.Forwards = []string{} for _, r := range d.fwd.rules() { info.Forwards = append(info.Forwards, r.String()) @@ -299,9 +296,9 @@ func (d *daemon) stop() { d.quitOnce.Do(func() { close(d.quit) }) } -// handleUninstall removes the autostart entry and the installed payload, then -// stops. With ?purge=1 it first logs the console out of the tailnet and -// deletes the saved state as well. +// handleUninstall removes the installed payload, then stops. With ?purge=1 it +// first logs the console out of the tailnet and deletes the saved state as +// well. func (d *daemon) handleUninstall(w http.ResponseWriter, r *http.Request) { purge := r.URL.Query().Get("purge") == "1" if purge && d.lc != nil { @@ -309,7 +306,7 @@ func (d *daemon) handleUninstall(w http.ResponseWriter, r *http.Request) { d.logf("uninstall: logout: %v", err) } } - if err := uninstall(purge, d.logf); err != nil { + if err := uninstall(purge); err != nil { d.logf("uninstall: %v", err) http.Error(w, err.Error(), http.StatusInternalServerError) return