mirror of
https://github.com/holdmysocks/ps5-tailscale.git
synced 2026-10-06 07:00:22 +02:00
Ship a single payload instead of an installer
tailscale.elf now adds the home screen icon itself, the first time it runs, by handing a small embedded helper payload to the ELF loader. The separate installer is gone: nothing is copied to /data/tailscale any more, and the payload runs from wherever the user keeps it. Uninstall on the status page now logs out, stops the daemon and deletes its data directory.
This commit is contained in:
1 parent
01b6381444
commit
0d7d8ad4b0
17 files changed
+367
-417
No files matched your search
@@ -43,38 +43,36 @@ Tested on firmware 13.42 with elfldr 0.26.
|
||||
|
||||
## Install
|
||||
|
||||
1. Download `tailscale-installer.elf` from the
|
||||
[latest release](../../releases/latest).
|
||||
2. Send it to the console's ELF loader, once. Any payload sender works:
|
||||
There is one file, `tailscale.elf`, and it is an ordinary payload: running
|
||||
it starts Tailscale.
|
||||
|
||||
1. Download `tailscale.elf` from the [latest release](../../releases/latest).
|
||||
2. Send it to the console's ELF loader. Any payload sender works:
|
||||
|
||||
```bash
|
||||
# Linux / macOS
|
||||
socat -t 60 - TCP:<console-ip>:9021 < tailscale-installer.elf
|
||||
socat -t 60 - TCP:<console-ip>:9021 < tailscale.elf
|
||||
```
|
||||
|
||||
```powershell
|
||||
# Windows (script from this repository)
|
||||
.\tools\ps5send.ps1 -File tailscale-installer.elf -PS5Host <console-ip> -Seconds 70
|
||||
.\tools\ps5send.ps1 -File tailscale.elf -PS5Host <console-ip>
|
||||
```
|
||||
|
||||
The installer prints what it does. It:
|
||||
- stores the daemon payload as `/data/tailscale/tailscale.elf`,
|
||||
- adds a **Tailscale** icon to the home screen (media section),
|
||||
- starts Tailscale.
|
||||
|
||||
3. Open `http://<console-ip>:8090` on a phone or PC. Scan the QR code or
|
||||
follow the link and log in to Tailscale. If your tailnet uses device
|
||||
approval, approve the console in the admin console.
|
||||
|
||||
The console now has a tailnet address, shown on the status page.
|
||||
|
||||
Sending the installer again upgrades and restarts Tailscale. The login is
|
||||
kept.
|
||||
The console now has a tailnet address, shown on the status page. The first
|
||||
run also adds a **Tailscale** icon to the home screen (media section) that
|
||||
opens the status page.
|
||||
|
||||
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.
|
||||
send `tailscale.elf` again; the login and settings are kept. If you use a
|
||||
payload manager or autoloader, add the file there like any other payload.
|
||||
|
||||
To update, use the new `tailscale.elf` in place of the old one. Sending it
|
||||
while Tailscale is running replaces the running copy.
|
||||
|
||||
## Using it
|
||||
|
||||
@@ -215,19 +213,21 @@ 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/tailscale/tailscale.elf` | The daemon payload. |
|
||||
| `/data/tailscale/icon-installed` | Marks that the home screen icon was added. Delete it to have the icon added again on the next start. |
|
||||
| `/user/app/TSCL00001/` | The home screen icon. |
|
||||
|
||||
## Uninstall
|
||||
|
||||
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.
|
||||
Press **Uninstall** on the status page. It logs the console out of your
|
||||
tailnet, deletes `/data/tailscale` (login, settings, logs) and stops
|
||||
Tailscale.
|
||||
|
||||
Two things are left to do by hand:
|
||||
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.
|
||||
- If you added `tailscale.elf` to a payload manager or autoloader, remove it
|
||||
there, or it starts again on the next boot.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
File renamed without changes.
+151
@@ -0,0 +1,151 @@
|
||||
/* Home screen icon helper for Tailscale on PS5.
|
||||
*
|
||||
* A tiny payload that puts a "Tailscale" icon on the home screen: a media app
|
||||
* whose only content is a link to the status page, which the console opens in
|
||||
* its browser. The daemon's launcher carries this payload and hands it to the
|
||||
* ELF loader the first time Tailscale runs. It is a separate payload so that
|
||||
* the system libraries it needs are never loaded into the daemon's process.
|
||||
*
|
||||
* Prints "icon: ok" on success; the launcher looks for that.
|
||||
*
|
||||
* Build with -DASSET_DIR="path/to/appicon" and link, in this order,
|
||||
* -lSceIpmi -lSceAppInstUtil -lSceUserService -lSceSystemService (with
|
||||
* libSceAppInstUtil alone the payload is never started). */
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include <ps5/kernel.h>
|
||||
|
||||
#ifndef ASSET_DIR
|
||||
#error "ASSET_DIR must name the folder with param.json and icon0.png"
|
||||
#endif
|
||||
|
||||
#define TITLE_ID "TSCL00001"
|
||||
#define APP_DIR "/user/app/" TITLE_ID
|
||||
|
||||
#define INCASSET(name, file) \
|
||||
__asm__(".section .rodata\n" \
|
||||
".balign 16\n" \
|
||||
".global " #name "\n" #name ":\n" \
|
||||
".incbin \"" file "\"\n" \
|
||||
".global " #name "_end\n" #name "_end:\n" \
|
||||
".text\n"); \
|
||||
extern const uint8_t name[]; \
|
||||
extern const uint8_t name##_end[];
|
||||
|
||||
INCASSET(param_json, ASSET_DIR "/param.json")
|
||||
INCASSET(icon_png, ASSET_DIR "/icon0.png")
|
||||
|
||||
int sceAppInstUtilInitialize(void);
|
||||
int sceAppInstUtilTerminate(void);
|
||||
int sceAppInstUtilAppInstallAll(void *);
|
||||
|
||||
static int
|
||||
write_file(const char *path, const uint8_t *data, size_t size) {
|
||||
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||
|
||||
if (fd < 0) {
|
||||
return -1;
|
||||
}
|
||||
while (size > 0) {
|
||||
ssize_t n = write(fd, data, size);
|
||||
if (n < 0) {
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
data += n;
|
||||
size -= n;
|
||||
}
|
||||
return close(fd);
|
||||
}
|
||||
|
||||
/* 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) {
|
||||
struct stat st;
|
||||
uint8_t *buf;
|
||||
int same = 0;
|
||||
FILE *f;
|
||||
|
||||
if (stat(path, &st) || (size_t)st.st_size != size || !(f = fopen(path, "rb"))) {
|
||||
return 0;
|
||||
}
|
||||
if ((buf = malloc(size))) {
|
||||
same = fread(buf, 1, size, f) == size && !memcmp(buf, data, size);
|
||||
free(buf);
|
||||
}
|
||||
fclose(f);
|
||||
return same;
|
||||
}
|
||||
|
||||
int
|
||||
main(void) {
|
||||
int (*install_title_dir)(const char *, const char *, void *) = 0;
|
||||
size_t param_size = param_json_end - param_json;
|
||||
size_t icon_size = icon_png_end - icon_png;
|
||||
pid_t pid = getpid();
|
||||
intptr_t rootvnode;
|
||||
uint32_t handle;
|
||||
int err;
|
||||
|
||||
setvbuf(stdout, 0, _IONBF, 0);
|
||||
|
||||
/* /user/app is only writable as root outside the sandbox. */
|
||||
if ((rootvnode = kernel_get_root_vnode())) {
|
||||
kernel_set_proc_rootdir(pid, rootvnode);
|
||||
kernel_set_proc_jaildir(pid, 0);
|
||||
}
|
||||
kernel_set_ucred_uid(pid, 0);
|
||||
kernel_set_ucred_ruid(pid, 0);
|
||||
kernel_set_ucred_svuid(pid, 0);
|
||||
kernel_set_ucred_rgid(pid, 0);
|
||||
kernel_set_ucred_svgid(pid, 0);
|
||||
|
||||
if (file_matches(APP_DIR "/sce_sys/param.json", param_json, param_size) &&
|
||||
file_matches(APP_DIR "/sce_sys/icon0.png", icon_png, icon_size)) {
|
||||
printf("icon: ok (already installed)\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ((err = sceAppInstUtilInitialize())) {
|
||||
printf("icon: sceAppInstUtilInitialize failed: 0x%08x\n", err);
|
||||
return 1;
|
||||
}
|
||||
mkdir(APP_DIR, 0755);
|
||||
mkdir(APP_DIR "/sce_sys", 0755);
|
||||
if (write_file(APP_DIR "/sce_sys/param.json", param_json, param_size) ||
|
||||
write_file(APP_DIR "/sce_sys/icon0.png", icon_png, icon_size)) {
|
||||
printf("icon: could not write to %s: %s\n", APP_DIR, strerror(errno));
|
||||
sceAppInstUtilTerminate();
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Register just this title where the firmware supports it; otherwise ask
|
||||
* for a rescan of everything under /user/app. */
|
||||
if (!kernel_dynlib_handle(-1, "libSceAppInstUtil.sprx", &handle)) {
|
||||
install_title_dir = (void *)kernel_dynlib_resolve(-1, handle, "Wudg3Xe3heE");
|
||||
}
|
||||
if (install_title_dir) {
|
||||
err = install_title_dir(TITLE_ID, "/user/app/", 0);
|
||||
} else {
|
||||
err = sceAppInstUtilAppInstallAll(0);
|
||||
}
|
||||
sceAppInstUtilTerminate();
|
||||
if (err) {
|
||||
printf("icon: registering the app failed: 0x%08x\n", err);
|
||||
return 1;
|
||||
}
|
||||
printf("icon: ok (installed %s)\n", TITLE_ID);
|
||||
return 0;
|
||||
}
|
||||
File renamed without changes.
+6
-6
@@ -51,18 +51,18 @@ These folders are not in the repository.
|
||||
## Building the payloads
|
||||
|
||||
```powershell
|
||||
# daemon payload: C launcher + Go program -> out\tailscale.elf
|
||||
.\tools\build-payload.ps1 -GoDir tsd -Name tailscale -Version 0.4.0
|
||||
|
||||
# installer -> out\tailscale-installer.elf (embeds out\tailscale.elf)
|
||||
.\tools\build-installer.ps1
|
||||
# C launcher + Go program + home screen icon helper -> out\tailscale.elf
|
||||
.\tools\build-payload.ps1 -GoDir tsd -Name tailscale -Version 0.4.1 -HomeIcon
|
||||
```
|
||||
|
||||
`-HomeIcon` also builds `appicon\` into `out\appicon.elf` and embeds it in
|
||||
the launcher.
|
||||
|
||||
## Sending to the console
|
||||
|
||||
```powershell
|
||||
$env:PS5_HOST = '192.168.1.50' # your console
|
||||
.\tools\ps5send.ps1 -File out\tailscale-installer.elf -Seconds 70
|
||||
.\tools\ps5send.ps1 -File out\tailscale.elf
|
||||
```
|
||||
|
||||
`ps5send.ps1` prints whatever the payload writes back.
|
||||
|
||||
+15
-5
@@ -44,11 +44,18 @@ 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 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 icon helper** (`appicon/`, C) is a second, tiny payload embedded in the
|
||||
launcher. The first time `tailscale.elf` runs, the launcher sends it to the
|
||||
ELF loader on `127.0.0.1:9021`, where it runs as a process of its own,
|
||||
registers a home screen app whose `param.json` has a `deeplinkUri` to the
|
||||
status page, reports the result and exits. The launcher then writes
|
||||
`/data/tailscale/icon-installed` and never does it again. It is a separate
|
||||
payload so that the system libraries it needs are never loaded into the
|
||||
long-running daemon process, where their threads could receive signals meant
|
||||
for the Go runtime.
|
||||
|
||||
There is no installer. Nothing is copied anywhere and no payload autoloader
|
||||
is touched: the payload is run from wherever the user keeps it.
|
||||
|
||||
## The PS5 as a Go target
|
||||
|
||||
@@ -144,6 +151,9 @@ 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.
|
||||
|
||||
The daemon cannot remove the icon; that is left to the user (Options, then
|
||||
Delete, on the home screen).
|
||||
|
||||
## Known problems
|
||||
|
||||
- Once, switching the console from Wi-Fi to Ethernet during a Moonlight
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
@@ -1,310 +0,0 @@
|
||||
/* Tailscale installer payload for jailbroken PS5s.
|
||||
*
|
||||
* 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 <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <netinet/in.h>
|
||||
#include <sys/select.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/time.h>
|
||||
|
||||
#include <ps5/kernel.h>
|
||||
|
||||
#ifndef DAEMON_ELF
|
||||
#error "DAEMON_ELF must name the daemon payload to embed"
|
||||
#endif
|
||||
|
||||
#define DATA_DIR "/data/tailscale"
|
||||
#define DAEMON_NAME "tailscale.elf"
|
||||
#define LOADER_PORT 9021
|
||||
|
||||
extern const uint8_t daemon_elf[];
|
||||
extern const uint8_t daemon_elf_end[];
|
||||
|
||||
__asm__(".section .rodata\n"
|
||||
".balign 16\n"
|
||||
".global daemon_elf\n"
|
||||
"daemon_elf:\n"
|
||||
".incbin \"" DAEMON_ELF "\"\n"
|
||||
".global daemon_elf_end\n"
|
||||
"daemon_elf_end:\n"
|
||||
".text\n");
|
||||
|
||||
/* The home screen launcher: a media app whose only content is a link, which
|
||||
* the console opens in its browser. ASSET_DIR is set by the build. */
|
||||
#ifndef ASSET_DIR
|
||||
#error "ASSET_DIR must name the folder with param.json and icon0.png"
|
||||
#endif
|
||||
#define LAUNCHER_TITLE_ID "TSCL00001"
|
||||
#define LAUNCHER_DIR "/user/app/" LAUNCHER_TITLE_ID
|
||||
|
||||
#define INCASSET(name, file) \
|
||||
__asm__(".section .rodata\n" \
|
||||
".balign 16\n" \
|
||||
".global " #name "\n" #name ":\n" \
|
||||
".incbin \"" file "\"\n" \
|
||||
".global " #name "_end\n" #name "_end:\n" \
|
||||
".text\n"); \
|
||||
extern const uint8_t name[]; \
|
||||
extern const uint8_t name##_end[];
|
||||
|
||||
INCASSET(launcher_param_json, ASSET_DIR "/param.json")
|
||||
INCASSET(launcher_icon_png, ASSET_DIR "/icon0.png")
|
||||
|
||||
int sceAppInstUtilInitialize(void);
|
||||
int sceAppInstUtilTerminate(void);
|
||||
int sceAppInstUtilAppInstallAll(void *);
|
||||
|
||||
typedef struct notify_request {
|
||||
char useless1[45];
|
||||
char message[3075];
|
||||
} notify_request_t;
|
||||
|
||||
int sceKernelSendNotificationRequest(int, notify_request_t *, size_t, int);
|
||||
|
||||
static void
|
||||
notify(const char *fmt, ...) {
|
||||
notify_request_t req;
|
||||
va_list args;
|
||||
|
||||
memset(&req, 0, sizeof(req));
|
||||
va_start(args, fmt);
|
||||
vsnprintf(req.message, sizeof(req.message), fmt, args);
|
||||
va_end(args);
|
||||
sceKernelSendNotificationRequest(0, &req, sizeof(req), 0);
|
||||
}
|
||||
|
||||
static int
|
||||
write_all(int fd, const uint8_t *data, size_t size) {
|
||||
while (size > 0) {
|
||||
ssize_t n = write(fd, data, size);
|
||||
if (n < 0) {
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
data += n;
|
||||
size -= n;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Write a file through a temporary name so a failed write never leaves a
|
||||
* truncated payload behind. */
|
||||
static int
|
||||
write_file(const char *path, const uint8_t *data, size_t size) {
|
||||
char tmp[512];
|
||||
int fd;
|
||||
|
||||
snprintf(tmp, sizeof(tmp), "%s.tmp", path);
|
||||
if ((fd = open(tmp, O_WRONLY | O_CREAT | O_TRUNC, 0755)) < 0) {
|
||||
return -1;
|
||||
}
|
||||
if (write_all(fd, data, size)) {
|
||||
close(fd);
|
||||
unlink(tmp);
|
||||
return -1;
|
||||
}
|
||||
close(fd);
|
||||
if (rename(tmp, path)) {
|
||||
unlink(tmp);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
install_daemon(const char *dir) {
|
||||
char path[512];
|
||||
|
||||
mkdir(dir, 0755);
|
||||
snprintf(path, sizeof(path), "%s/%s", dir, DAEMON_NAME);
|
||||
if (write_file(path, daemon_elf, daemon_elf_end - daemon_elf)) {
|
||||
printf(" could not write %s: %s\n", path, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
printf(" wrote %s (%.1f MB)\n", path, (daemon_elf_end - daemon_elf) / 1048576.0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 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) {
|
||||
struct stat st;
|
||||
uint8_t *buf;
|
||||
int same = 0;
|
||||
FILE *f;
|
||||
|
||||
if (stat(path, &st) || (size_t)st.st_size != size || !(f = fopen(path, "rb"))) {
|
||||
return 0;
|
||||
}
|
||||
if ((buf = malloc(size))) {
|
||||
same = fread(buf, 1, size, f) == size && !memcmp(buf, data, size);
|
||||
free(buf);
|
||||
}
|
||||
fclose(f);
|
||||
return same;
|
||||
}
|
||||
|
||||
/* Put a "Tailscale" icon on the home screen that opens the status page in
|
||||
* the console's browser. Does nothing if it is already there and current.
|
||||
* Returns 0 on success. */
|
||||
static int
|
||||
install_launcher_app(void) {
|
||||
int (*install_title_dir)(const char *, const char *, void *) = 0;
|
||||
size_t param_size = launcher_param_json_end - launcher_param_json;
|
||||
size_t icon_size = launcher_icon_png_end - launcher_icon_png;
|
||||
uint32_t handle;
|
||||
int err;
|
||||
|
||||
if (file_matches(LAUNCHER_DIR "/sce_sys/param.json", launcher_param_json, param_size) &&
|
||||
file_matches(LAUNCHER_DIR "/sce_sys/icon0.png", launcher_icon_png, icon_size)) {
|
||||
printf(" already installed\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ((err = sceAppInstUtilInitialize())) {
|
||||
printf(" sceAppInstUtilInitialize failed: 0x%08x\n", err);
|
||||
return -1;
|
||||
}
|
||||
mkdir(LAUNCHER_DIR, 0755);
|
||||
mkdir(LAUNCHER_DIR "/sce_sys", 0755);
|
||||
if (write_file(LAUNCHER_DIR "/sce_sys/param.json", launcher_param_json, param_size) ||
|
||||
write_file(LAUNCHER_DIR "/sce_sys/icon0.png", launcher_icon_png, icon_size)) {
|
||||
printf(" could not write to %s: %s\n", LAUNCHER_DIR, strerror(errno));
|
||||
sceAppInstUtilTerminate();
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Register just this title where the firmware supports it; otherwise ask
|
||||
* for a rescan of everything under /user/app. */
|
||||
if (!kernel_dynlib_handle(-1, "libSceAppInstUtil.sprx", &handle)) {
|
||||
install_title_dir = (void *)kernel_dynlib_resolve(-1, handle, "Wudg3Xe3heE");
|
||||
}
|
||||
if (install_title_dir) {
|
||||
err = install_title_dir(LAUNCHER_TITLE_ID, "/user/app/", 0);
|
||||
} else {
|
||||
err = sceAppInstUtilAppInstallAll(0);
|
||||
}
|
||||
sceAppInstUtilTerminate();
|
||||
if (err) {
|
||||
printf(" registering the app failed: 0x%08x\n", err);
|
||||
return -1;
|
||||
}
|
||||
printf(" installed (%s), opens http://127.0.0.1:8090/\n", LAUNCHER_TITLE_ID);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Hand the daemon to the ELF loader on this console and relay what it prints
|
||||
* for a while, so that the login link reaches whoever sent the installer. */
|
||||
static int
|
||||
start_daemon(int relay_seconds) {
|
||||
struct sockaddr_in addr = {0};
|
||||
struct timeval start, now;
|
||||
char buf[4096];
|
||||
int fd;
|
||||
|
||||
if ((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
|
||||
return -1;
|
||||
}
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(LOADER_PORT);
|
||||
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||
if (connect(fd, (struct sockaddr *)&addr, sizeof(addr))) {
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
if (write_all(fd, daemon_elf, daemon_elf_end - daemon_elf)) {
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
gettimeofday(&start, 0);
|
||||
for (;;) {
|
||||
struct timeval tv = {1, 0};
|
||||
fd_set rfds;
|
||||
|
||||
gettimeofday(&now, 0);
|
||||
if (now.tv_sec - start.tv_sec >= relay_seconds) {
|
||||
break;
|
||||
}
|
||||
FD_ZERO(&rfds);
|
||||
FD_SET(fd, &rfds);
|
||||
if (select(fd + 1, &rfds, 0, 0, &tv) <= 0) {
|
||||
continue;
|
||||
}
|
||||
ssize_t n = read(fd, buf, sizeof(buf));
|
||||
if (n <= 0) {
|
||||
break;
|
||||
}
|
||||
if (write_all(STDOUT_FILENO, (uint8_t *)buf, n)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
close(fd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
main(void) {
|
||||
pid_t pid = getpid();
|
||||
intptr_t rootvnode;
|
||||
|
||||
setvbuf(stdout, 0, _IONBF, 0);
|
||||
|
||||
/* Run as root outside the sandbox so /data and USB drives are writable. */
|
||||
if ((rootvnode = kernel_get_root_vnode())) {
|
||||
kernel_set_proc_rootdir(pid, rootvnode);
|
||||
kernel_set_proc_jaildir(pid, 0);
|
||||
}
|
||||
kernel_set_ucred_uid(pid, 0);
|
||||
kernel_set_ucred_ruid(pid, 0);
|
||||
kernel_set_ucred_svuid(pid, 0);
|
||||
kernel_set_ucred_rgid(pid, 0);
|
||||
kernel_set_ucred_svgid(pid, 0);
|
||||
|
||||
#ifdef LAUNCHER_ONLY
|
||||
/* Test build: only (re)install the home screen icon. */
|
||||
printf("Home screen icon:\n");
|
||||
return install_launcher_app() ? 1 : 0;
|
||||
#endif
|
||||
|
||||
printf("Tailscale for PS5 installer\n\n");
|
||||
|
||||
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");
|
||||
install_launcher_app();
|
||||
|
||||
printf("\nStarting Tailscale...\n");
|
||||
if (start_daemon(45)) {
|
||||
printf("Could not reach the ELF loader on port %d: %s\n", LOADER_PORT, strerror(errno));
|
||||
notify("Tailscale is installed but could not be started:\nno ELF loader on port %d.", LOADER_PORT);
|
||||
return 1;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/* Installs the home screen icon the first time the payload runs.
|
||||
*
|
||||
* The icon is installed by a separate small payload (appicon/), embedded
|
||||
* here and handed to the ELF loader on this console, so that the system
|
||||
* libraries it needs never end up in this process. Build with
|
||||
* -DICON_HELPER="path/to/appicon.elf"; without it this file does nothing. */
|
||||
|
||||
#include "homeicon.h"
|
||||
|
||||
#ifdef ICON_HELPER
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <netinet/in.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/time.h>
|
||||
|
||||
#define DATA_DIR "/data/tailscale"
|
||||
/* Records that the icon has been installed, and which version of it. Once
|
||||
* it exists the icon is left alone, so an icon the user deletes from the
|
||||
* home screen stays deleted. Remove the file to get the icon back. */
|
||||
#define ICON_MARKER DATA_DIR "/icon-installed"
|
||||
#define ICON_VERSION "1\n"
|
||||
#define LOADER_PORT 9021
|
||||
|
||||
extern const uint8_t icon_helper[];
|
||||
extern const uint8_t icon_helper_end[];
|
||||
|
||||
__asm__(".section .rodata\n"
|
||||
".balign 16\n"
|
||||
".global icon_helper\n"
|
||||
"icon_helper:\n"
|
||||
".incbin \"" ICON_HELPER "\"\n"
|
||||
".global icon_helper_end\n"
|
||||
"icon_helper_end:\n"
|
||||
".text\n");
|
||||
|
||||
static int
|
||||
marker_is_current(void) {
|
||||
char buf[16] = {0};
|
||||
int fd = open(ICON_MARKER, O_RDONLY);
|
||||
|
||||
if (fd < 0) {
|
||||
return 0;
|
||||
}
|
||||
read(fd, buf, sizeof(buf) - 1);
|
||||
close(fd);
|
||||
return !strcmp(buf, ICON_VERSION);
|
||||
}
|
||||
|
||||
void
|
||||
home_icon_install_once(void) {
|
||||
struct sockaddr_in addr = {0};
|
||||
struct timeval tv = {1, 0};
|
||||
const uint8_t *data = icon_helper;
|
||||
size_t left = icon_helper_end - icon_helper;
|
||||
char reply[512] = {0};
|
||||
size_t got = 0;
|
||||
int fd;
|
||||
|
||||
if (marker_is_current()) {
|
||||
return;
|
||||
}
|
||||
mkdir(DATA_DIR, 0755);
|
||||
|
||||
if ((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
|
||||
return;
|
||||
}
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(LOADER_PORT);
|
||||
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||
if (connect(fd, (struct sockaddr *)&addr, sizeof(addr))) {
|
||||
/* No ELF loader on the usual port: go without an icon this time. */
|
||||
close(fd);
|
||||
return;
|
||||
}
|
||||
while (left > 0) {
|
||||
ssize_t n = write(fd, data, left);
|
||||
if (n <= 0) {
|
||||
close(fd);
|
||||
return;
|
||||
}
|
||||
data += n;
|
||||
left -= n;
|
||||
}
|
||||
|
||||
/* The helper reports "icon: ok" or what went wrong, then exits, which
|
||||
* closes the connection. Give it 20 seconds. */
|
||||
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||
for (int tries = 0; tries < 20 && got < sizeof(reply) - 1; tries++) {
|
||||
const char *line = strstr(reply, "icon: ");
|
||||
if (line && strchr(line, '\n')) {
|
||||
break;
|
||||
}
|
||||
ssize_t n = read(fd, reply + got, sizeof(reply) - 1 - got);
|
||||
if (n == 0) {
|
||||
break;
|
||||
}
|
||||
if (n > 0) {
|
||||
got += n;
|
||||
}
|
||||
}
|
||||
close(fd);
|
||||
|
||||
if (strstr(reply, "icon: ok")) {
|
||||
if ((fd = open(ICON_MARKER, O_WRONLY | O_CREAT | O_TRUNC, 0644)) >= 0) {
|
||||
write(fd, ICON_VERSION, sizeof(ICON_VERSION) - 1);
|
||||
close(fd);
|
||||
}
|
||||
fprintf(stderr, "launcher: home screen icon installed\n");
|
||||
} else {
|
||||
fprintf(stderr, "launcher: home screen icon not installed: %s\n", got ? reply : "no reply from the helper");
|
||||
}
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
void
|
||||
home_icon_install_once(void) {
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
/* Put the Tailscale icon on the home screen if that has not been done yet.
|
||||
* Never fails: without an icon everything else still works. */
|
||||
void home_icon_install_once(void);
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <ps5/kernel.h>
|
||||
|
||||
#include "goload.h"
|
||||
#include "homeicon.h"
|
||||
|
||||
#ifndef GO_IMAGE
|
||||
#error "GO_IMAGE must name the Go binary to embed"
|
||||
@@ -134,6 +135,8 @@ main(int argc, char **argv) {
|
||||
kernel_set_ucred_rgid(pid, 0);
|
||||
kernel_set_ucred_svgid(pid, 0);
|
||||
|
||||
home_icon_install_once();
|
||||
|
||||
if (leave_realtime_class()) {
|
||||
/* Without this a runaway goroutine could hang the console, so do not
|
||||
* take the chance. */
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
# Build the installer payload around an already built daemon payload.
|
||||
# .\tools\build-installer.ps1 [-Daemon out\tailscale.elf] [-Send]
|
||||
param(
|
||||
[string]$Daemon = 'out\tailscale.elf',
|
||||
[string]$Out = 'out\tailscale-installer.elf',
|
||||
[switch]$Send,
|
||||
[int]$Seconds = 70
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
. (Join-Path $PSScriptRoot 'env.ps1')
|
||||
|
||||
$daemonPath = (Resolve-Path (Join-Path $DevRoot $Daemon)).Path -replace '\\', '/'
|
||||
$assets = (Join-Path $DevRoot 'installer\assets') -replace '\\', '/'
|
||||
$outPath = Join-Path $DevRoot $Out
|
||||
|
||||
# The app installer library only loads when these come with it, in this
|
||||
# order (as in the SDK's install_app sample). With libSceAppInstUtil alone
|
||||
# the payload never starts: the loader leaves it stopped.
|
||||
Invoke-PS5CC -O2 -Wall "-DDAEMON_ELF=`"$daemonPath`"" "-DASSET_DIR=`"$assets`"" `
|
||||
-lSceIpmi -lSceAppInstUtil -lSceUserService -lSceSystemService `
|
||||
-o $outPath (Join-Path $DevRoot 'installer\main.c')
|
||||
|
||||
Write-Host ("built {0} ({1:N1} MB)" -f $outPath, ((Get-Item $outPath).Length / 1MB))
|
||||
if ($Send) {
|
||||
& (Join-Path $PSScriptRoot 'ps5send.ps1') -File $outPath -Seconds $Seconds
|
||||
}
|
||||
+16
-1
@@ -1,4 +1,5 @@
|
||||
# Build a Go program for the PS5 and wrap it in the launcher payload.
|
||||
# .\tools\build-payload.ps1 -GoDir tsd -Name tailscale -Version 0.4.1 -HomeIcon
|
||||
# .\tools\build-payload.ps1 -GoDir probe-go -Name probe [-DebugLoader] [-Watchdog 120] [-Send]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$GoDir,
|
||||
@@ -9,6 +10,7 @@ param(
|
||||
[string]$MaxProcs = '', # GOMAXPROCS for the Go program (launcher default: 4)
|
||||
[string]$GoDebug = '', # GODEBUG value baked into the launcher
|
||||
[int]$Watchdog = 0, # test builds: kill the process after this many seconds
|
||||
[switch]$HomeIcon, # embed the helper that adds the home screen icon on first run
|
||||
[switch]$DebugLoader, # print loader details and early crash registers
|
||||
[switch]$KeepSymbols,
|
||||
[switch]$Send,
|
||||
@@ -39,7 +41,20 @@ if ($DebugLoader) { $ccArgs += '-DGOLOAD_DEBUG' }
|
||||
if ($Watchdog -gt 0) { $ccArgs += "-DGOLOAD_WATCHDOG=$Watchdog" }
|
||||
if ($MaxProcs) { $ccArgs += "-DGO_MAXPROCS=`"$MaxProcs`"" }
|
||||
if ($GoDebug) { $ccArgs += "-DGO_DEBUG=`"$GoDebug`"" }
|
||||
$ccArgs += @('-o', $elf, (Join-Path $DevRoot 'launcher\main.c'), (Join-Path $DevRoot 'launcher\goload.c'))
|
||||
if ($HomeIcon) {
|
||||
# The icon helper is a payload of its own. The app installer library only
|
||||
# loads when these come with it, in this order (as in the SDK's
|
||||
# install_app sample); with libSceAppInstUtil alone the payload is never
|
||||
# started.
|
||||
$helper = Join-Path $out 'appicon.elf'
|
||||
$assets = (Join-Path $DevRoot 'appicon') -replace '\\', '/'
|
||||
Invoke-PS5CC -O2 -Wall "-DASSET_DIR=`"$assets`"" `
|
||||
-lSceIpmi -lSceAppInstUtil -lSceUserService -lSceSystemService `
|
||||
-o $helper (Join-Path $DevRoot 'appicon\main.c')
|
||||
$ccArgs += "-DICON_HELPER=`"$($helper -replace '\\', '/')`""
|
||||
}
|
||||
$ccArgs += @('-o', $elf, (Join-Path $DevRoot 'launcher\main.c'), (Join-Path $DevRoot 'launcher\goload.c'),
|
||||
(Join-Path $DevRoot 'launcher\homeicon.c'))
|
||||
Invoke-PS5CC @ccArgs
|
||||
|
||||
Write-Host ("built {0} ({1:N1} MB)" -f $elf, ((Get-Item $elf).Length / 1MB))
|
||||
|
||||
+10
@@ -122,6 +122,9 @@ type daemon struct {
|
||||
|
||||
quit chan struct{}
|
||||
quitOnce sync.Once
|
||||
// removeDataOnExit is set by Uninstall: delete the data directory once
|
||||
// everything that writes to it has shut down.
|
||||
removeDataOnExit bool
|
||||
}
|
||||
|
||||
func (d *daemon) run() error {
|
||||
@@ -204,6 +207,13 @@ func (d *daemon) run() error {
|
||||
d.logf("shutdown timed out")
|
||||
}
|
||||
d.logf("stopped")
|
||||
|
||||
d.mu.Lock()
|
||||
remove := d.removeDataOnExit
|
||||
d.mu.Unlock()
|
||||
if remove {
|
||||
os.RemoveAll(dataDir)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+2
-5
@@ -239,11 +239,8 @@ $('btn-sunshine').onclick = async () => {
|
||||
await act('/api/sunshine?host=' + encodeURIComponent($('sunshine-select').value));
|
||||
$('sunshine-select').dataset.signature = '';
|
||||
};
|
||||
$('btn-uninstall').onclick = () => {
|
||||
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' : ''));
|
||||
};
|
||||
$('btn-uninstall').onclick = () => act('/api/uninstall',
|
||||
'Remove Tailscale from this PS5?\n\nThis logs the console out of your tailnet, deletes its settings and logs, and stops Tailscale.');
|
||||
$('logbox').addEventListener('toggle', refresh);
|
||||
|
||||
refresh();
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
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...)
|
||||
}
|
||||
+9
-11
@@ -302,22 +302,20 @@ func (d *daemon) stop() {
|
||||
d.quitOnce.Do(func() { close(d.quit) })
|
||||
}
|
||||
|
||||
// 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.
|
||||
// handleUninstall logs the console out of the tailnet and stops the daemon,
|
||||
// which deletes its data directory (login, settings, logs) on the way out.
|
||||
// The payload file itself is wherever the user keeps it, and the home screen
|
||||
// icon can only be deleted from the home screen.
|
||||
func (d *daemon) handleUninstall(w http.ResponseWriter, r *http.Request) {
|
||||
purge := r.URL.Query().Get("purge") == "1"
|
||||
if purge && d.lc != nil {
|
||||
if d.lc != nil {
|
||||
if err := d.lc.Logout(r.Context()); err != nil {
|
||||
d.logf("uninstall: logout: %v", err)
|
||||
}
|
||||
}
|
||||
if err := uninstall(purge); err != nil {
|
||||
d.logf("uninstall: %v", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
d.logf("uninstalled (purge=%v)", purge)
|
||||
d.logf("uninstall requested from the status page")
|
||||
d.mu.Lock()
|
||||
d.removeDataOnExit = true
|
||||
d.mu.Unlock()
|
||||
notify("Tailscale was removed from this PS5.")
|
||||
io.WriteString(w, "uninstalled\n")
|
||||
d.stop()
|
||||
|
||||
Reference in new issue
Block a user