mirror of
https://github.com/holdmysocks/ps5-tailscale.git
synced 2026-10-06 21:00:20 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d7d8ad4b0 |
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