mirror of
https://github.com/holdmysocks/ps5-tailscale.git
synced 2026-10-11 11:00:28 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b31667d3f0 | ||
|
|
edbaaed240 | ||
|
|
4c6d9223b5 | ||
|
|
070e838135 |
No files matched your search
@@ -51,10 +51,14 @@ Tested on firmware 13.42 with elfldr 0.26.
|
||||
|
||||
## Install
|
||||
|
||||
There is one file, `tailscale.elf`, and it is an ordinary payload: running
|
||||
it starts Tailscale.
|
||||
There is one file, `tailscale-<version>.elf` (for example
|
||||
`tailscale-0.6.1.elf`), and it is an ordinary payload: running it starts
|
||||
Tailscale. The examples below call it `tailscale.elf`; use the name of the
|
||||
file you downloaded, or rename it.
|
||||
|
||||
1. Download `tailscale.elf` from the [latest release](../../releases/latest).
|
||||
1. Download `tailscale-<version>.elf` from the
|
||||
[latest release](../../releases/latest). The `.sig` file next to it is
|
||||
for the status page's Install button; you do not need it.
|
||||
2. Send it to the console's ELF loader. Any payload sender works:
|
||||
|
||||
```bash
|
||||
@@ -76,11 +80,20 @@ 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 `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.
|
||||
send the file 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.
|
||||
To update, use the new file in place of the old one. Sending it while
|
||||
Tailscale is running replaces the running copy. The file name changes with
|
||||
every release, so **check your autoload settings after updating**: a payload
|
||||
manager or autoloader that still points at the old file starts the old
|
||||
version with the console, or nothing if you deleted it.
|
||||
|
||||
The easy way to avoid that: keep the copy your payload manager or autoloader
|
||||
starts under a fixed name without a version, such as `tailscale.elf`. When
|
||||
you update, save the new release over it under that same name, and the
|
||||
autoload entry never needs to change. The status page always shows which
|
||||
version is really running.
|
||||
|
||||
## Using it
|
||||
|
||||
@@ -150,11 +163,20 @@ shows a notification once. **Install** downloads the release, checks that it
|
||||
is signed with this project's release key and is the version it claims to be,
|
||||
and starts it; the login and settings are kept. Nothing is ever installed
|
||||
without that button being pressed. It needs an ELF loader on port 9021, like
|
||||
sending the payload by hand does. If you keep `tailscale.elf` in a payload
|
||||
manager or autoloader so that it starts with the console, put that file's
|
||||
path under **Payload file to keep up to date** in the settings, for example
|
||||
`/data/pldmgr/payloads/Tailscale/tailscale.elf`; the update then replaces
|
||||
that copy as well. Otherwise the old version is back after the next restart.
|
||||
sending the payload by hand does.
|
||||
|
||||
Installing from the page replaces the copy that is running, not the file
|
||||
your payload manager or autoloader starts with the console. **Check your
|
||||
autoload settings after updating**, or the old version is back after the
|
||||
next restart. To have that file replaced as well, put its path under
|
||||
**Payload file to keep up to date** in the settings, for example
|
||||
`/data/pldmgr/payloads/Tailscale/tailscale.elf`. The file keeps the name it
|
||||
has there, whatever version is in it, so your autoload entry keeps working.
|
||||
That is one more reason to give that copy a fixed name such as
|
||||
`tailscale.elf` and not the versioned name it was downloaded under.
|
||||
|
||||
Releases up to 0.6.0 cannot install later ones from the page, because the
|
||||
release files were renamed after 0.6.0; update those by hand once.
|
||||
|
||||
**Devices.** The list is grouped into your tailnet's devices and devices
|
||||
shared with you, and marks the ones that can be used as an exit node. It can
|
||||
@@ -284,7 +306,7 @@ optional.
|
||||
| `blockedPorts` | Local TCP ports that are never exposed to the tailnet. |
|
||||
| `allowFrom` | `"own"` lets only devices logged in as the same user as the console connect; other users' devices and devices shared into the tailnet are turned away. Anything else is the default: every device your tailnet's access rules allow. If the console is tagged, `"own"` means the devices of its own tailnet. |
|
||||
| `receiveDir` | Where files sent to the console with Taildrop are put. |
|
||||
| `payloadPath` | The copy of `tailscale.elf` that is started with the console, if there is one. An update installed from the status page replaces it. Empty: none. |
|
||||
| `payloadPath` | The copy of the payload that is started with the console, if there is one; best kept under a fixed name such as `tailscale.elf`. An update installed from the status page replaces its contents and leaves its name alone. Empty: none. |
|
||||
| `priority` | `"high"` lets the daemon compete with games for CPU time; anything else is the default, low. Applied when Tailscale starts. |
|
||||
| `checkUpdates` | Ask GitHub twice a day whether a newer release exists, to show it on the status page and announce it once on the console. Nothing is downloaded. |
|
||||
| `verbose` | Put Tailscale's own log in the main log as well. |
|
||||
@@ -331,6 +353,13 @@ Left to do by hand:
|
||||
`127.0.0.1` (or `127.0.0.1:<port>` for a host on another port), and the
|
||||
Sunshine host must be listed on the status page with the port its Sunshine
|
||||
uses.
|
||||
- **It stays on "Starting", or the update check never finds anything.** The
|
||||
daemon looks names up itself: at a DNS payload on the console
|
||||
(`127.0.0.1:53`) if one is running, otherwise at your router, otherwise at
|
||||
a public resolver (1.1.1.1, 8.8.8.8, 9.9.9.9). The log says which one it
|
||||
uses in a line starting with `DNS:`. If none answers, the console has no
|
||||
working internet connection for payloads. The DNS server set in the PS5's
|
||||
network settings plays no part.
|
||||
- **"Not logged in" after logging in.** Press **Log in again** for a fresh
|
||||
link.
|
||||
- **Forgot the status page password.** Delete the `passwordHash` line from
|
||||
@@ -354,6 +383,10 @@ Left to do by hand:
|
||||
- The local forwards and the proxy are for the console's own apps and are
|
||||
not exposed to the tailnet.
|
||||
- With update checks on, the console contacts `api.github.com` twice a day.
|
||||
- Name lookups by the daemon go to the console's DNS payload if there is
|
||||
one, otherwise to your router or a public resolver. They are only the
|
||||
daemon's own lookups (Tailscale's servers, GitHub, what you send through
|
||||
the HTTP proxy), not the console's.
|
||||
- An update is only installed when **Install** is pressed, and only if it
|
||||
carries a valid signature made with the project's release key, which is
|
||||
not kept on GitHub. A release put up by someone who got into the GitHub
|
||||
|
||||
+3
-1
@@ -65,7 +65,9 @@ the launcher.
|
||||
```
|
||||
|
||||
builds the payload and puts three files in `out\release-1.2.3`:
|
||||
`tailscale.elf`, its signature `tailscale.elf.sig`, and `SHA256SUMS.txt`.
|
||||
`tailscale-1.2.3.elf`, its signature `tailscale-1.2.3.elf.sig`, and
|
||||
`SHA256SUMS.txt`. The version in the file name is what the status page looks
|
||||
for (`payloadAssetName` in `tsd\selfupdate.go`).
|
||||
Attach all three to the GitHub release, and tag it `v1.2.3`. The status
|
||||
page's **Install** button only offers a release that has the first two, and
|
||||
only installs it if the signature is good and is for that very version.
|
||||
|
||||
+7
-2
@@ -66,7 +66,7 @@ way is a failure in the SDK's crt, which runs before any of this.
|
||||
on the page and announced once on the console; the announced version is
|
||||
kept in `/data/tailscale/update-notified`.
|
||||
- Installing an update (`selfupdate.go`, `relsig/`): on request only. The
|
||||
release's `tailscale.elf.sig` names a version and a SHA-256 and carries an
|
||||
release's `tailscale-<version>.elf.sig` names a version and a SHA-256 and carries an
|
||||
Ed25519 signature over both. The daemon checks the signature against the
|
||||
public key built into it, that the version is the release's and newer than
|
||||
its own, downloads the payload to `/data/tailscale/update/`, compares the
|
||||
@@ -163,7 +163,12 @@ apply `SOCK_NONBLOCK`/`SOCK_CLOEXEC` with `fcntl`.
|
||||
blocks and hands the converted entries out across calls.
|
||||
- Unix domain sockets cannot be bound on `/data`.
|
||||
- There is no `/etc/resolv.conf` and no CA bundle. Go's resolver falls back
|
||||
to `127.0.0.1:53`; the daemon imports `x509roots/fallback` for TLS roots.
|
||||
to `127.0.0.1:53`, which only answers if a DNS payload runs on the
|
||||
console. The daemon therefore installs its own resolver (`dns.go`): it
|
||||
probes `127.0.0.1:53`, the default gateway and three public resolvers,
|
||||
uses the first that answers, and checks again every five minutes or when
|
||||
the network changes. The daemon imports `x509roots/fallback` for TLS
|
||||
roots.
|
||||
- A payload's stdin, stdout and stderr are the ELF loader's TCP connection.
|
||||
Go kills a process whose write to fd 1 or 2 fails with `EPIPE`, so the
|
||||
daemon moves that connection to another descriptor and points 1 and 2 at
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 108 KiB |
@@ -1,6 +1,6 @@
|
||||
# Build the files of a release into out\release-<version>:
|
||||
# tailscale.elf the payload
|
||||
# tailscale.elf.sig its signature, which the status page's "Install" checks
|
||||
# tailscale-<version>.elf the payload
|
||||
# tailscale-<version>.elf.sig its signature, which the status page's "Install" checks
|
||||
# SHA256SUMS.txt
|
||||
#
|
||||
# .\tools\make-release.ps1 -Version 1.2.3
|
||||
@@ -19,7 +19,8 @@ if ($Version -notmatch '^\d+\.\d+\.\d+$') { throw "version must look like 1.2.3,
|
||||
|
||||
$rel = Join-Path $DevRoot "out\release-$Version"
|
||||
New-Item -ItemType Directory -Force $rel | Out-Null
|
||||
$elf = Join-Path $rel 'tailscale.elf'
|
||||
$name = "tailscale-$Version.elf" # the name the status page's Install looks for
|
||||
$elf = Join-Path $rel $name
|
||||
Copy-Item (Join-Path $DevRoot 'out\tailscale.elf') $elf -Force
|
||||
|
||||
Push-Location (Join-Path $DevRoot 'tsd')
|
||||
@@ -37,7 +38,7 @@ try {
|
||||
} finally { Pop-Location }
|
||||
|
||||
$hash = (Get-FileHash $elf -Algorithm SHA256).Hash.ToLower()
|
||||
[IO.File]::WriteAllText((Join-Path $rel 'SHA256SUMS.txt'), "$hash tailscale.elf`n")
|
||||
[IO.File]::WriteAllText((Join-Path $rel 'SHA256SUMS.txt'), "$hash $name`n")
|
||||
|
||||
Get-ChildItem $rel | Select-Object Name, Length | Format-Table -AutoSize
|
||||
Write-Host "release files are in $rel"
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tailscale.com/net/netmon"
|
||||
)
|
||||
|
||||
// Name lookups by the daemon itself: Tailscale's servers, GitHub for the
|
||||
// update check, whatever goes through the HTTP proxy.
|
||||
//
|
||||
// The PS5 has no resolv.conf, so Go asks 127.0.0.1:53. That only works on a
|
||||
// console that runs a DNS payload, which most jailbreak setups do and some do
|
||||
// not. Rather than depend on it, the daemon uses the first of these that
|
||||
// answers: the local DNS payload, the router, a public resolver.
|
||||
|
||||
// dnsLocal is where a DNS payload on the console listens. A variable so that
|
||||
// a test build can pretend there is none.
|
||||
var dnsLocal = "127.0.0.1:53"
|
||||
|
||||
// dnsPublic are used when neither the console nor the router answers.
|
||||
var dnsPublic = []string{"1.1.1.1:53", "8.8.8.8:53", "9.9.9.9:53"}
|
||||
|
||||
const (
|
||||
dnsRecheckGood = 5 * time.Minute // how long a working server is kept
|
||||
dnsRecheckBad = 20 * time.Second // how soon to look again when none works
|
||||
dnsProbeWait = 1200 * time.Millisecond
|
||||
)
|
||||
|
||||
type dnsPicker struct {
|
||||
logf func(format string, args ...any)
|
||||
// candidates lists the servers to try, in order of preference.
|
||||
candidates func() []string
|
||||
// probe reports whether a server answers queries.
|
||||
probe func(ctx context.Context, server string) bool
|
||||
|
||||
mu sync.Mutex
|
||||
server string
|
||||
working bool
|
||||
checked time.Time
|
||||
}
|
||||
|
||||
func newDNSPicker(logf func(format string, args ...any)) *dnsPicker {
|
||||
return &dnsPicker{logf: logf, candidates: dnsCandidates, probe: dnsAnswers}
|
||||
}
|
||||
|
||||
// dnsCandidates returns the local DNS payload, the router and the public
|
||||
// resolvers, in that order.
|
||||
func dnsCandidates() []string {
|
||||
list := []string{dnsLocal}
|
||||
if gw, _, ok := netmon.LikelyHomeRouterIP(); ok && gw.IsValid() {
|
||||
list = append(list, net.JoinHostPort(gw.String(), "53"))
|
||||
}
|
||||
return append(list, dnsPublic...)
|
||||
}
|
||||
|
||||
// pick returns the server to ask. The choice is kept for a while, so the
|
||||
// candidates are not probed for every lookup.
|
||||
func (p *dnsPicker) pick(ctx context.Context) string {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
keep := dnsRecheckBad
|
||||
if p.working {
|
||||
keep = dnsRecheckGood
|
||||
}
|
||||
if p.server != "" && time.Since(p.checked) < keep {
|
||||
return p.server
|
||||
}
|
||||
candidates := p.candidates()
|
||||
chosen, working := candidates[0], false
|
||||
for _, c := range candidates {
|
||||
if p.probe(ctx, c) {
|
||||
chosen, working = c, true
|
||||
break
|
||||
}
|
||||
}
|
||||
if chosen != p.server || working != p.working {
|
||||
switch {
|
||||
case !working:
|
||||
p.logf("DNS: no server answers (tried %v); name lookups will fail until one does", candidates)
|
||||
case chosen == candidates[0]:
|
||||
p.logf("DNS: using the console's own DNS at %s", chosen)
|
||||
default:
|
||||
p.logf("DNS: nothing answers at %s; using %s instead (in order of preference: %v)", candidates[0], chosen, candidates)
|
||||
}
|
||||
}
|
||||
p.server, p.working, p.checked = chosen, working, time.Now()
|
||||
return chosen
|
||||
}
|
||||
|
||||
// reset makes the next lookup choose again, for when the network changed.
|
||||
func (p *dnsPicker) reset() {
|
||||
p.mu.Lock()
|
||||
p.checked = time.Time{}
|
||||
p.mu.Unlock()
|
||||
}
|
||||
|
||||
// dial is the resolver's Dial: whatever address Go wants to ask, the chosen
|
||||
// server is asked instead.
|
||||
func (p *dnsPicker) dial(ctx context.Context, network, _ string) (net.Conn, error) {
|
||||
var d net.Dialer
|
||||
return d.DialContext(ctx, network, p.pick(ctx))
|
||||
}
|
||||
|
||||
// install makes every name lookup in the process go through the picker.
|
||||
func (p *dnsPicker) install() {
|
||||
net.DefaultResolver = &net.Resolver{PreferGo: true, Dial: p.dial}
|
||||
}
|
||||
|
||||
// dnsAnswers asks server for the address of a name that certainly exists
|
||||
// and reports whether a proper answer came back.
|
||||
func dnsAnswers(ctx context.Context, server string) bool {
|
||||
ctx, cancel := context.WithTimeout(ctx, dnsProbeWait)
|
||||
defer cancel()
|
||||
var d net.Dialer
|
||||
c, err := d.DialContext(ctx, "udp", server)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer c.Close()
|
||||
c.SetDeadline(time.Now().Add(dnsProbeWait))
|
||||
query := dnsQuery(uint16(time.Now().UnixNano()), "github.com")
|
||||
if _, err := c.Write(query); err != nil {
|
||||
return false
|
||||
}
|
||||
buf := make([]byte, 1500)
|
||||
n, err := c.Read(buf)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return dnsReplyOK(query, buf[:n])
|
||||
}
|
||||
|
||||
// dnsQuery builds a query for the IPv4 address of name.
|
||||
func dnsQuery(id uint16, name string) []byte {
|
||||
q := make([]byte, 12, 64)
|
||||
binary.BigEndian.PutUint16(q[0:], id)
|
||||
q[2] = 0x01 // recursion desired
|
||||
binary.BigEndian.PutUint16(q[4:], 1)
|
||||
start := 0
|
||||
for i := 0; i <= len(name); i++ {
|
||||
if i == len(name) || name[i] == '.' {
|
||||
q = append(q, byte(i-start))
|
||||
q = append(q, name[start:i]...)
|
||||
start = i + 1
|
||||
}
|
||||
}
|
||||
return append(q, 0, 0, 1, 0, 1) // root label, type A, class IN
|
||||
}
|
||||
|
||||
// dnsReplyOK reports whether reply is a successful answer to query.
|
||||
func dnsReplyOK(query, reply []byte) bool {
|
||||
if len(reply) < 12 || reply[0] != query[0] || reply[1] != query[1] {
|
||||
return false
|
||||
}
|
||||
isResponse := reply[2]&0x80 != 0
|
||||
rcode := reply[3] & 0x0f
|
||||
answers := binary.BigEndian.Uint16(reply[6:])
|
||||
return isResponse && rcode == 0 && answers > 0
|
||||
}
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func testPicker(t *testing.T, candidates []string, answering map[string]bool) (*dnsPicker, *[]string, *[]string) {
|
||||
var logs, probed []string
|
||||
p := &dnsPicker{
|
||||
logf: func(format string, args ...any) { logs = append(logs, format) },
|
||||
candidates: func() []string { return candidates },
|
||||
probe: func(ctx context.Context, server string) bool {
|
||||
probed = append(probed, server)
|
||||
return answering[server]
|
||||
},
|
||||
}
|
||||
return p, &logs, &probed
|
||||
}
|
||||
|
||||
func TestDNSPicker(t *testing.T) {
|
||||
candidates := []string{"127.0.0.1:53", "192.168.1.1:53", "1.1.1.1:53"}
|
||||
answering := map[string]bool{"127.0.0.1:53": true, "192.168.1.1:53": true, "1.1.1.1:53": true}
|
||||
p, logs, probed := testPicker(t, candidates, answering)
|
||||
ctx := context.Background()
|
||||
|
||||
// The console's own DNS is preferred, and the choice is kept.
|
||||
if got := p.pick(ctx); got != "127.0.0.1:53" {
|
||||
t.Fatalf("picked %s", got)
|
||||
}
|
||||
p.pick(ctx)
|
||||
if len(*probed) != 1 {
|
||||
t.Errorf("probed %v; the choice should have been kept", *probed)
|
||||
}
|
||||
|
||||
// No DNS payload: the router is used, and that is logged.
|
||||
answering["127.0.0.1:53"] = false
|
||||
p.reset()
|
||||
if got := p.pick(ctx); got != "192.168.1.1:53" {
|
||||
t.Errorf("without a local DNS: picked %s", got)
|
||||
}
|
||||
if last := (*logs)[len(*logs)-1]; !strings.Contains(last, "instead") {
|
||||
t.Errorf("log: %q", last)
|
||||
}
|
||||
|
||||
// Nor the router: a public resolver.
|
||||
answering["192.168.1.1:53"] = false
|
||||
p.reset()
|
||||
if got := p.pick(ctx); got != "1.1.1.1:53" {
|
||||
t.Errorf("without local DNS or router: picked %s", got)
|
||||
}
|
||||
|
||||
// Nothing at all: stay with the first, and look again soon.
|
||||
answering["1.1.1.1:53"] = false
|
||||
p.reset()
|
||||
if got := p.pick(ctx); got != "127.0.0.1:53" {
|
||||
t.Errorf("with nothing answering: picked %s", got)
|
||||
}
|
||||
if p.working {
|
||||
t.Error("the picker thinks it has a working server")
|
||||
}
|
||||
before := len(*probed)
|
||||
p.checked = time.Now().Add(-dnsRecheckBad - time.Second)
|
||||
answering["127.0.0.1:53"] = true
|
||||
if got := p.pick(ctx); got != "127.0.0.1:53" || len(*probed) == before || !p.working {
|
||||
t.Errorf("after the short wait: picked %s, working %v", got, p.working)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNSQueryAndReply(t *testing.T) {
|
||||
q := dnsQuery(0x1234, "github.com")
|
||||
want := []byte{0x12, 0x34, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 6, 'g', 'i', 't', 'h', 'u', 'b', 3, 'c', 'o', 'm', 0, 0, 1, 0, 1}
|
||||
if string(q) != string(want) {
|
||||
t.Fatalf("query = % x", q)
|
||||
}
|
||||
reply := func(id0, id1, flags2, flags3 byte, answers byte) []byte {
|
||||
return []byte{id0, id1, flags2, flags3, 0, 1, 0, answers, 0, 0, 0, 0}
|
||||
}
|
||||
for name, tt := range map[string]struct {
|
||||
r []byte
|
||||
want bool
|
||||
}{
|
||||
"good answer": {reply(0x12, 0x34, 0x81, 0x80, 2), true},
|
||||
"another query's id": {reply(0x12, 0x35, 0x81, 0x80, 2), false},
|
||||
"server failure": {reply(0x12, 0x34, 0x81, 0x82, 0), false},
|
||||
"no such name": {reply(0x12, 0x34, 0x81, 0x83, 0), false},
|
||||
"no answers": {reply(0x12, 0x34, 0x81, 0x80, 0), false},
|
||||
"not a response": {reply(0x12, 0x34, 0x01, 0x00, 1), false},
|
||||
"too short": {[]byte{0x12, 0x34}, false},
|
||||
} {
|
||||
if got := dnsReplyOK(q, tt.r); got != tt.want {
|
||||
t.Errorf("%s: got %v", name, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A real exchange over UDP with a stand-in server, and a port where nothing
|
||||
// answers.
|
||||
func TestDNSAnswers(t *testing.T) {
|
||||
pc, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer pc.Close()
|
||||
go func() {
|
||||
buf := make([]byte, 1500)
|
||||
for {
|
||||
n, from, err := pc.ReadFrom(buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
r := append([]byte(nil), buf[:n]...)
|
||||
r[2], r[3], r[7] = 0x81, 0x80, 1 // response, no error, one answer
|
||||
pc.WriteTo(r, from)
|
||||
}
|
||||
}()
|
||||
if !dnsAnswers(context.Background(), pc.LocalAddr().String()) {
|
||||
t.Error("a server that answers was reported as silent")
|
||||
}
|
||||
|
||||
silent, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer silent.Close()
|
||||
start := time.Now()
|
||||
if dnsAnswers(context.Background(), silent.LocalAddr().String()) {
|
||||
t.Error("a silent server was reported as answering")
|
||||
}
|
||||
if time.Since(start) > 3*time.Second {
|
||||
t.Errorf("the probe took %v", time.Since(start))
|
||||
}
|
||||
}
|
||||
+14
-1
@@ -15,6 +15,7 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -64,6 +65,14 @@ func main() {
|
||||
}
|
||||
logf("ps5-tailscale %s starting, hostname %q, web UI on %s", version, cfg.Hostname, cfg.WebAddr)
|
||||
|
||||
// On the console, name lookups go to whichever DNS server answers; see
|
||||
// dns.go. Elsewhere the system's resolver is left alone.
|
||||
var dns *dnsPicker
|
||||
if runtime.GOOS == "freebsd" {
|
||||
dns = newDNSPicker(logf)
|
||||
dns.install()
|
||||
}
|
||||
|
||||
// A payload that is sent again replaces the running instance, which is
|
||||
// how an upgrade or a restart is done.
|
||||
if stopRunningInstance(cfg.WebAddr) {
|
||||
@@ -92,7 +101,7 @@ func main() {
|
||||
// the very copy that is running now, and it is not needed again.
|
||||
os.RemoveAll(filepath.Join(dataDir, updateDirName))
|
||||
|
||||
d := &daemon{cfg: cfg, cfgPath: filepath.Join(dataDir, "config.json"), logf: logf, debug: debug, console: console, started: time.Now()}
|
||||
d := &daemon{dns: dns, cfg: cfg, cfgPath: filepath.Join(dataDir, "config.json"), logf: logf, debug: debug, console: console, started: time.Now()}
|
||||
if err := d.run(); err != nil {
|
||||
logf("fatal: %v", err)
|
||||
notify("Tailscale failed to start:\n%v", err)
|
||||
@@ -111,6 +120,7 @@ type daemon struct {
|
||||
srv *tsnet.Server
|
||||
lc *local.Client
|
||||
fwd *forwarder
|
||||
dns *dnsPicker // nil when the system's resolver is used
|
||||
udp *udpExposer
|
||||
|
||||
mu sync.Mutex
|
||||
@@ -256,6 +266,9 @@ func (d *daemon) networkChanged() {
|
||||
d.lastNetChange = time.Now()
|
||||
lc := d.lc
|
||||
d.mu.Unlock()
|
||||
if d.dns != nil {
|
||||
d.dns.reset()
|
||||
}
|
||||
if recent || lc == nil {
|
||||
return
|
||||
}
|
||||
|
||||
+25
-5
@@ -33,7 +33,6 @@ import (
|
||||
var updatePublicKey = "HUcHCXGe3vNEeyt4frmoKZUqYDamdA1dzsr+Hsybda0="
|
||||
|
||||
const (
|
||||
payloadAsset = "tailscale.elf"
|
||||
maxPayload = 128 << 20
|
||||
loaderAddr = "127.0.0.1:9021"
|
||||
updateDirName = "update"
|
||||
@@ -57,7 +56,24 @@ func (d *daemon) setUpdate(p updateProgress) {
|
||||
|
||||
// canInstall reports whether rel can be installed from the status page.
|
||||
func canInstall(rel releaseInfo) bool {
|
||||
return newerVersion(version, rel.Version) && rel.Assets[payloadAsset] != "" && rel.Assets[payloadAsset+relsig.FileSuffix] != ""
|
||||
_, _, _, ok := releaseAssets(rel)
|
||||
return ok && newerVersion(version, rel.Version)
|
||||
}
|
||||
|
||||
// payloadAssetName is what the payload of a release is called: the version is
|
||||
// part of the name, so that a downloaded file says what it is.
|
||||
func payloadAssetName(ver string) string { return "tailscale-" + ver + ".elf" }
|
||||
|
||||
// releaseAssets finds a release's payload and its signature. Releases up to
|
||||
// 0.6.0 called the payload plain "tailscale.elf"; that name is still
|
||||
// understood.
|
||||
func releaseAssets(rel releaseInfo) (name, payloadURL, sigURL string, ok bool) {
|
||||
for _, name := range []string{payloadAssetName(rel.Version), "tailscale.elf"} {
|
||||
if p, s := rel.Assets[name], rel.Assets[name+relsig.FileSuffix]; p != "" && s != "" {
|
||||
return name, p, s, true
|
||||
}
|
||||
}
|
||||
return "", "", "", false
|
||||
}
|
||||
|
||||
// startUpdate begins installing the newest known release. It returns at
|
||||
@@ -96,7 +112,11 @@ func (d *daemon) runUpdate(rel releaseInfo) error {
|
||||
defer cancel()
|
||||
|
||||
// The signature first: it is small, and says what the payload must be.
|
||||
sigBytes, err := httpGetSmall(ctx, rel.Assets[payloadAsset+relsig.FileSuffix])
|
||||
name, payloadURL, sigURL, ok := releaseAssets(rel)
|
||||
if !ok {
|
||||
return errors.New("that release has no signed payload")
|
||||
}
|
||||
sigBytes, err := httpGetSmall(ctx, sigURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("downloading the signature: %w", err)
|
||||
}
|
||||
@@ -109,8 +129,8 @@ func (d *daemon) runUpdate(rel releaseInfo) error {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
file := filepath.Join(dir, payloadAsset)
|
||||
sum, err := d.download(ctx, rel.Assets[payloadAsset], file, rel.Version)
|
||||
file := filepath.Join(dir, name)
|
||||
sum, err := d.download(ctx, payloadURL, file, rel.Version)
|
||||
if err != nil {
|
||||
os.Remove(file)
|
||||
return fmt.Errorf("downloading the payload: %w", err)
|
||||
|
||||
@@ -66,14 +66,18 @@ func TestBuiltInKeyIsValid(t *testing.T) {
|
||||
|
||||
func TestCanInstall(t *testing.T) {
|
||||
withVersion(t, "1.0.0")
|
||||
both := map[string]string{"tailscale.elf": "u1", "tailscale.elf.sig": "u2"}
|
||||
both := map[string]string{"tailscale-1.1.0.elf": "u1", "tailscale-1.1.0.elf.sig": "u2"}
|
||||
oldNames := map[string]string{"tailscale.elf": "u1", "tailscale.elf.sig": "u2"}
|
||||
otherVersion := map[string]string{"tailscale-1.0.9.elf": "u1", "tailscale-1.0.9.elf.sig": "u2"}
|
||||
for _, tt := range []struct {
|
||||
rel releaseInfo
|
||||
want bool
|
||||
}{
|
||||
{releaseInfo{Version: "1.1.0", Assets: both}, true},
|
||||
{releaseInfo{Version: "1.0.0", Assets: both}, false},
|
||||
{releaseInfo{Version: "1.1.0", Assets: map[string]string{"tailscale.elf": "u1"}}, false},
|
||||
{releaseInfo{Version: "1.1.0", Assets: oldNames}, true},
|
||||
{releaseInfo{Version: "1.1.0", Assets: otherVersion}, false},
|
||||
{releaseInfo{Version: "1.1.0", Assets: map[string]string{"tailscale-1.1.0.elf": "u1"}}, false},
|
||||
{releaseInfo{Version: "1.1.0"}, false},
|
||||
} {
|
||||
if got := canInstall(tt.rel); got != tt.want {
|
||||
|
||||
+14
-4
@@ -104,6 +104,7 @@
|
||||
<div class="actions" style="margin-top: 12px">
|
||||
<button id="btn-update" class="hidden">Install it</button>
|
||||
</div>
|
||||
<p class="health hidden" id="updatewarn" style="padding-left: 0"></p>
|
||||
<p class="note hidden" id="updatestate" style="margin: 12px 0 0"></p>
|
||||
<p class="msg hidden" id="updatemsg"></p>
|
||||
</section>
|
||||
@@ -226,9 +227,11 @@
|
||||
</label>
|
||||
<label class="field">Payload file to keep up to date
|
||||
<input type="text" id="set-payloadpath" autocomplete="off" placeholder="None">
|
||||
<span class="hint">Where the copy of <code>tailscale.elf</code> that starts with the console is kept, for
|
||||
example <code>/data/pldmgr/payloads/Tailscale/tailscale.elf</code>. Installing an update from this
|
||||
page then replaces that file too. Empty: only the running copy is updated, until the next restart.</span>
|
||||
<span class="hint">Where the copy that starts with the console is kept, for example
|
||||
<code>/data/pldmgr/payloads/Tailscale/tailscale.elf</code>. Installing an update from this page then
|
||||
replaces that file too; it keeps its name, and your autoload entry keeps working. Give that copy a
|
||||
fixed name such as <code>tailscale.elf</code>, without a version in it, so the name stays true.
|
||||
Empty: only the running copy is updated, until the next restart.</span>
|
||||
</label>
|
||||
<label class="field">Status page address
|
||||
<input type="text" id="set-webaddr" autocomplete="off">
|
||||
@@ -444,6 +447,12 @@ async function refresh() {
|
||||
}
|
||||
$('btn-update').classList.toggle('hidden', !s.canUpdate || busy);
|
||||
$('btn-update').textContent = 'Install ' + (s.latestVersion || 'it');
|
||||
// Installing replaces the running copy. What starts with the console is
|
||||
// a file somewhere else, which the user has to keep an eye on.
|
||||
autoloadWarning = s.payloadPath
|
||||
? 'Installing also replaces the copy at ' + s.payloadPath + '. Afterwards, check your autoload settings: your payload manager or autoloader must still start that file.'
|
||||
: 'After installing, check your autoload settings. If a payload manager or autoloader starts an older Tailscale file with the console, the old version comes back after a restart: replace that file with the new release, or name it under Settings ("Payload file to keep up to date") before installing.';
|
||||
show('updatewarn', s.canUpdate && !busy ? autoloadWarning : '');
|
||||
show('updatestate', {
|
||||
downloading: 'Downloading version ' + upd.version + (upd.percent ? ' (' + upd.percent + '%)' : '') + '…',
|
||||
verifying: 'Checking the signature of version ' + upd.version + '…',
|
||||
@@ -575,9 +584,10 @@ async function refreshFiles() {
|
||||
}
|
||||
|
||||
let updateRefused = ''; // why the daemon would not start an update
|
||||
let autoloadWarning = '';
|
||||
$('btn-update').onclick = async () => {
|
||||
const v = $('btn-update').textContent.replace('Install ', '');
|
||||
if (!confirm('Download and install version ' + v + '?\n\nTailscale on this PS5 restarts, which interrupts a stream or Remote Play session that runs through it.')) return;
|
||||
if (!confirm('Download and install version ' + v + '?\n\nTailscale on this PS5 restarts, which interrupts a stream or Remote Play session that runs through it.\n\n' + autoloadWarning)) return;
|
||||
updateRefused = '';
|
||||
try {
|
||||
const r = await api('/api/update', {method: 'POST'});
|
||||
|
||||
+6
-3
@@ -68,9 +68,11 @@ type statusInfo struct {
|
||||
UpdateURL string `json:"updateURL,omitempty"`
|
||||
// CanUpdate says that the newer release can be installed from the page;
|
||||
// Update reports on an installation in progress.
|
||||
CanUpdate bool `json:"canUpdate"`
|
||||
Update updateProgress `json:"update"`
|
||||
Uptime int64 `json:"uptimeSeconds"`
|
||||
CanUpdate bool `json:"canUpdate"`
|
||||
// PayloadPath is the copy an update replaces as well, if one is set.
|
||||
PayloadPath string `json:"payloadPath,omitempty"`
|
||||
Update updateProgress `json:"update"`
|
||||
Uptime int64 `json:"uptimeSeconds"`
|
||||
}
|
||||
|
||||
// webHandler builds the status page and its API.
|
||||
@@ -208,6 +210,7 @@ func (d *daemon) handleStatus(w http.ResponseWriter, r *http.Request) {
|
||||
info.CanUpdate = canInstall(d.latest)
|
||||
}
|
||||
info.Update = d.update
|
||||
info.PayloadPath = d.cfg.PayloadPath
|
||||
d.mu.Unlock()
|
||||
info.UDPPorts = []uint16{}
|
||||
if d.udp != nil {
|
||||
|
||||
Reference in new issue
Block a user