mirror of
https://github.com/holdmysocks/ps5-tailscale.git
synced 2026-10-06 06:00:17 +02:00
Tailscale for jailbroken PS5
A payload that runs the Tailscale client (tsnet, userspace networking) on a jailbroken PS5: a C launcher built with ps5-payload-sdk that loads a Go program in-process, an installer that registers it with the console's payload autoloader and adds a home screen icon, and the patch that makes Go 1.27.1 speak the PS5's FreeBSD 9 era syscall interface.
This commit is contained in:
commit
4554502591
46 files changed
+7033
No files matched your search
@@ -0,0 +1,4 @@
|
||||
* text=auto eol=lf
|
||||
*.ps1 text eol=crlf
|
||||
*.png binary
|
||||
*.patch text eol=lf
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
# Toolchains and third-party trees (see docs/BUILDING.md)
|
||||
/toolchain/
|
||||
/goroot/
|
||||
/goroot-*/
|
||||
/ref/
|
||||
|
||||
# Build output, caches and scratch space
|
||||
/out/
|
||||
/scratch/
|
||||
/.gotmp/
|
||||
/.gocache/
|
||||
|
||||
# Local working notes
|
||||
/NOTES.md
|
||||
@@ -0,0 +1,674 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
@@ -0,0 +1,278 @@
|
||||
# Tailscale for jailbroken PS5
|
||||
|
||||
Puts a jailbroken PS5 on your [Tailscale](https://tailscale.com) network.
|
||||
|
||||
- **Reach the console from anywhere.** FTP, the payload loader, web tools:
|
||||
whatever listens on the console is available at its tailnet address from
|
||||
your other Tailscale devices.
|
||||
- **Stream games to the console over Tailscale.** A Moonlight client on the
|
||||
PS5 (such as ProsperoLight) can connect to a Sunshine host on your tailnet.
|
||||
- **Starts by itself** after each jailbreak if you use a payload autoloader,
|
||||
and adds a home screen icon for its status page.
|
||||
|
||||
It runs the real Tailscale client code (v1.104.0) as a single payload.
|
||||
|
||||

|
||||
|
||||
> **Unofficial and experimental.** This project is not affiliated with or
|
||||
> endorsed by Tailscale Inc. or Sony. It has been tested on one console. It
|
||||
> runs homebrew with full privileges on a jailbroken system; use it at your
|
||||
> own risk.
|
||||
|
||||
## What it is not
|
||||
|
||||
The PS5 kernel has no tunnel device, so Tailscale cannot become a system-wide
|
||||
VPN here. It runs inside one process:
|
||||
|
||||
- Games and PSN traffic do **not** go through Tailscale.
|
||||
- Other apps on the console cannot open connections to tailnet addresses
|
||||
directly. They can through a *local forward* (see
|
||||
[game streaming](#game-streaming-moonlight-to-sunshine) and
|
||||
[configuration](#configuration)).
|
||||
- No exit node, subnet routing, Tailscale SSH, Taildrop or Funnel.
|
||||
|
||||
## Requirements
|
||||
|
||||
- A jailbroken PS5 with an ELF loader listening on port 9021
|
||||
(for example [elfldr](https://github.com/ps5-payload-dev/elfldr)).
|
||||
- A Tailscale account.
|
||||
- Optional: a payload autoloader, so Tailscale starts after every jailbreak.
|
||||
[Payload Manager](https://github.com/itsPLK/ps5-payload-manager) and
|
||||
`ps5_autoloader` folders are detected.
|
||||
|
||||
Tested on firmware 13.42 with elfldr 0.26 and Payload Manager 0.5.2.
|
||||
|
||||
## 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:
|
||||
|
||||
```bash
|
||||
# Linux / macOS
|
||||
socat -t 60 - TCP:<console-ip>:9021 < tailscale-installer.elf
|
||||
```
|
||||
|
||||
```powershell
|
||||
# Windows (script from this repository)
|
||||
.\tools\ps5send.ps1 -File tailscale-installer.elf -PS5Host <console-ip> -Seconds 70
|
||||
```
|
||||
|
||||
The installer prints what it does. It:
|
||||
- stores the daemon payload (`tailscale.elf`),
|
||||
- adds it to the end of your autoloader's load order, if it finds one,
|
||||
- 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.
|
||||
|
||||
## Using it
|
||||
|
||||
### Reaching the console
|
||||
|
||||
Connect to the console's tailnet address or MagicDNS name on the port you
|
||||
want, for example FTP on 2121 or the payload loader on 9021.
|
||||
|
||||
- Every TCP port that something on the console listens on is forwarded.
|
||||
Ports with no listener refuse the connection.
|
||||
- UDP is not forwarded in this direction.
|
||||
- To keep a port off the tailnet, add it to `blockedPorts` in the
|
||||
[configuration](#configuration).
|
||||
|
||||
### The status page
|
||||
|
||||
`http://<console address>:8090`, on the LAN or over the tailnet, or the
|
||||
**Tailscale** icon on the home screen. It shows the connection state, the
|
||||
login link, and your devices, and has controls for game streaming, logging
|
||||
out, stopping and uninstalling.
|
||||
|
||||
It has **no password**, like the console's other homebrew services. Anyone on
|
||||
your LAN, or on your tailnet if your ACLs allow it, can use it.
|
||||
|
||||
### Game streaming (Moonlight to Sunshine)
|
||||
|
||||
This lets a Moonlight client on the PS5 stream from a PC that is somewhere
|
||||
else, over Tailscale.
|
||||
|
||||
On the PC:
|
||||
|
||||
1. Install [Sunshine](https://github.com/LizardByte/Sunshine) and leave it on
|
||||
its default port (47989).
|
||||
2. Install Tailscale and log in to the same tailnet as the console.
|
||||
|
||||
On the console:
|
||||
|
||||
1. Open the status page and find **Game streaming**.
|
||||
2. Choose the device that runs Sunshine and press **Save**. The page shows
|
||||
"Forwarding 127.0.0.1 to *your-pc* (7 ports)".
|
||||
3. In your Moonlight client on the PS5, add a host manually with the address
|
||||
**`127.0.0.1`**. Do not enter the PC's tailnet address: the client cannot
|
||||
reach it.
|
||||
4. Pair as usual: the client shows a PIN, which you enter in Sunshine's web
|
||||
interface on the PC.
|
||||
|
||||
How it works: the daemon listens on `127.0.0.1` on Sunshine's ports (TCP
|
||||
47984, 47989, 48010 and UDP 47998, 47999, 48000, 48002) and relays them to
|
||||
the chosen host through Tailscale. To the Moonlight client the Sunshine host
|
||||
appears to be the console itself.
|
||||
|
||||
Notes:
|
||||
|
||||
- One Sunshine host at a time. Change it on the status page at any time.
|
||||
- A wired connection on the console helps, as with any streaming.
|
||||
- **Do not change the console's network (Wi-Fi to Ethernet, connection
|
||||
settings) while a stream is running.** That froze the test console once;
|
||||
the cause was not established.
|
||||
- Tested with ProsperoLight.
|
||||
|
||||
### Other apps on the console
|
||||
|
||||
`forwards` in the [configuration](#configuration) relays any localhost port
|
||||
to a tailnet host in the same way, TCP or UDP.
|
||||
|
||||
The daemon also runs an HTTP proxy on `127.0.0.1:8118` that reaches tailnet
|
||||
hosts, for apps that have their own proxy setting. **Do not set it as the
|
||||
PS5's system proxy.** The system then sends everything through it, including
|
||||
pages on `127.0.0.1`, and it is not running until Tailscale has been loaded.
|
||||
On the test console that stopped Payload Manager's page from opening.
|
||||
|
||||
## Configuration
|
||||
|
||||
`/data/tailscale/config.json` is created on first start. Every field is
|
||||
optional. Restart Tailscale (send the payload again) to apply edits.
|
||||
|
||||
```json
|
||||
{
|
||||
"hostname": "ps5",
|
||||
"authKey": "",
|
||||
"webAddr": ":8090",
|
||||
"httpProxyAddr": "127.0.0.1:8118",
|
||||
"controlURL": "",
|
||||
"sunshineHost": "",
|
||||
"forwards": [
|
||||
{"proto": "tcp", "listen": "127.0.0.1:8096", "target": "my-nas:8096"}
|
||||
],
|
||||
"blockedPorts": [],
|
||||
"verbose": false
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Meaning |
|
||||
| --- | --- |
|
||||
| `hostname` | The console's name on the tailnet. |
|
||||
| `authKey` | A Tailscale auth key, to log in without the browser step. |
|
||||
| `webAddr` | Where the status page listens. |
|
||||
| `httpProxyAddr` | Where the HTTP proxy listens. Empty turns it off. |
|
||||
| `controlURL` | A coordination server other than Tailscale's. |
|
||||
| `sunshineHost` | The Sunshine host; set from the status page. |
|
||||
| `forwards` | Extra local forwards: `proto` is `tcp` or `udp`, `listen` a localhost address, `target` a tailnet host and port. |
|
||||
| `blockedPorts` | Local TCP ports that are never exposed to the tailnet. |
|
||||
| `verbose` | Put Tailscale's own log in the main log as well. |
|
||||
|
||||
Files on the console:
|
||||
|
||||
| Path | What |
|
||||
| --- | --- |
|
||||
| `/data/tailscale/config.json` | Settings. |
|
||||
| `/data/tailscale/state/` | Tailscale's state, including the login. |
|
||||
| `/data/tailscale/tailscale.log` | The daemon's log, rotated at 2 MB. |
|
||||
| `/data/tailscale/tailscale-debug.log` | Tailscale's detailed log, up to 4 MB plus one older file. |
|
||||
| `/data/pldmgr/payloads/Tailscale/tailscale.elf` | The daemon, when Payload Manager is used. |
|
||||
| `/data/tailscale/tailscale.elf` | The daemon, when no autoloader was found. |
|
||||
| `/user/app/TSCL00001/` | The home screen icon. |
|
||||
|
||||
## Uninstall
|
||||
|
||||
Press **Uninstall** on the status page. It removes the autoloader entry and
|
||||
the daemon payload and stops Tailscale. It asks whether to also log out and
|
||||
delete the saved login.
|
||||
|
||||
Two things are left to do by hand:
|
||||
|
||||
- Delete the home screen icon (Options button, then Delete).
|
||||
- Remove the device in the Tailscale admin console.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **The status page does not open on the console, but does from a PC.**
|
||||
Check that the PS5's proxy server setting is "Do Not Use".
|
||||
- **The Moonlight client cannot find the host.** The host to add is
|
||||
`127.0.0.1`, and a Sunshine host must be selected on the status page.
|
||||
Sunshine must be on its default port.
|
||||
- **"Not logged in" after logging in.** Press **Log in again** for a fresh
|
||||
link.
|
||||
- **Something else.** `http://<console>:8090/api/logs?full=1` is the daemon's
|
||||
log and `/api/logs?debug=1` is Tailscale's detailed log. Please attach them
|
||||
to bug reports, after checking them for anything you consider private.
|
||||
|
||||
## Security
|
||||
|
||||
- The status page and its controls are unauthenticated.
|
||||
- All listening TCP ports on the console become reachable from your tailnet,
|
||||
including the payload loader, which runs anything sent to it. Use Tailscale
|
||||
ACLs if other people share your tailnet.
|
||||
- The local forwards and the proxy listen on `127.0.0.1` only and are not
|
||||
exposed to the tailnet.
|
||||
|
||||
## Resource use
|
||||
|
||||
About 60 MB of memory and next to no CPU when idle. The daemon runs at the
|
||||
lowest scheduling priority on at most 4 cores, so it gives way to games.
|
||||
|
||||
## What has and has not been tested
|
||||
|
||||
Tested on the one console: install and upgrade, login with device approval,
|
||||
autostart after a reboot through Payload Manager, reaching the console over
|
||||
the tailnet, a ProsperoLight stream from a Sunshine host through the forward,
|
||||
the HTTP proxy, the home screen icon.
|
||||
|
||||
Not tested: rest mode, the `ps5_autoloader` and USB autoloader paths,
|
||||
Uninstall on a console, other firmware versions, coordination servers other
|
||||
than Tailscale's.
|
||||
|
||||
## Building
|
||||
|
||||
See [docs/BUILDING.md](docs/BUILDING.md). The build runs on Windows with
|
||||
PowerShell, clang, ps5-payload-sdk and a patched Go 1.27.1.
|
||||
|
||||
## How it works
|
||||
|
||||
`tailscale.elf` is a small C program built with
|
||||
[ps5-payload-sdk](https://github.com/ps5-payload-dev/sdk) with a Go program
|
||||
embedded in it. The SDK's startup code lifts the PS5's restriction on where
|
||||
system calls may be issued from. The C part drops the process to the lowest
|
||||
scheduling priority, maps the Go program into memory and enters it the way
|
||||
the FreeBSD kernel would. Go's runtime and standard library are patched to
|
||||
speak the PS5's FreeBSD 9 era system call interface. The Go program is a
|
||||
[tsnet](https://pkg.go.dev/tailscale.com/tsnet) server that pipes every
|
||||
incoming tailnet TCP connection to the same port on localhost.
|
||||
|
||||
[docs/TECHNICAL.md](docs/TECHNICAL.md) has the details, including what was
|
||||
learned about running Go on the PS5.
|
||||
|
||||
## Credits
|
||||
|
||||
- [Tailscale](https://github.com/tailscale/tailscale), whose client this runs.
|
||||
- John Törnblom's [ps5-payload-sdk](https://github.com/ps5-payload-dev/sdk)
|
||||
and [elfldr](https://github.com/ps5-payload-dev/elfldr).
|
||||
- itsPLK's [Payload Manager](https://github.com/itsPLK/ps5-payload-manager),
|
||||
whose app-icon installer this follows.
|
||||
|
||||
## License
|
||||
|
||||
The project's own code is licensed under the GNU General Public License
|
||||
version 3 or later (see [LICENSE](LICENSE)); the payloads link the SDK's
|
||||
GPL-licensed startup code. `patches/` is a patch to the Go source tree and is
|
||||
under Go's BSD-style license. Tailscale is BSD-3-Clause. The release binaries
|
||||
also contain Tailscale's dependencies under their own licenses; they are
|
||||
listed in `tsd/go.mod`.
|
||||
|
||||
"Tailscale" is a trademark of Tailscale Inc. "PlayStation" and "PS5" are
|
||||
trademarks of Sony Interactive Entertainment Inc.
|
||||
@@ -0,0 +1,106 @@
|
||||
# Building
|
||||
|
||||
The build scripts are PowerShell and were written on Windows 11. Nothing is
|
||||
installed system-wide; everything lives in the repository folder.
|
||||
|
||||
## Layout the scripts expect
|
||||
|
||||
```
|
||||
toolchain\llvm\bin\ clang.exe, ld.lld.exe (LLVM 20 or newer; 23.1.2 was used)
|
||||
toolchain\ps5-payload-sdk\ ps5-payload-sdk release (v0.43 was used)
|
||||
goroot\ Go 1.27.1 with patches\go1.27.1-ps5.patch applied
|
||||
```
|
||||
|
||||
These folders are not in the repository.
|
||||
|
||||
## Setting up the toolchain
|
||||
|
||||
1. **LLVM.** Download `clang+llvm-23.1.2-x86_64-pc-windows-msvc.tar.xz` from
|
||||
the [LLVM releases](https://github.com/llvm/llvm-project/releases) and
|
||||
extract at least `bin\clang.exe`, `bin\ld.lld.exe`, `bin\lld.exe` and
|
||||
`lib\clang\` into `toolchain\llvm`.
|
||||
|
||||
```powershell
|
||||
tar -xf llvm.tar.xz -C toolchain\llvm --strip-components=1 "*/bin/clang.exe" "*/bin/lld.exe" "*/bin/ld.lld.exe" "*/bin/llvm-readelf.exe" "*/lib/clang/*"
|
||||
```
|
||||
|
||||
2. **ps5-payload-sdk.** Download `ps5-payload-sdk.zip` from the
|
||||
[SDK releases](https://github.com/ps5-payload-dev/sdk/releases) and unpack
|
||||
it into `toolchain\`, which creates `toolchain\ps5-payload-sdk`.
|
||||
|
||||
3. **Go.** Download `go1.27.1.windows-amd64.zip` from
|
||||
[go.dev](https://go.dev/dl/), unpack it, rename the `go` folder to
|
||||
`goroot`, apply the patch, and rebuild the go command and the linker (the
|
||||
patch changes a package both of them compile in):
|
||||
|
||||
```powershell
|
||||
tar -xf go1.27.1.windows-amd64.zip
|
||||
Rename-Item go goroot
|
||||
Set-Location goroot
|
||||
git -c core.autocrlf=false apply -p1 ..\patches\go1.27.1-ps5.patch
|
||||
Copy-Item bin\go.exe bin\go-bootstrap.exe
|
||||
$env:GOTOOLCHAIN = 'local'
|
||||
.\bin\go-bootstrap.exe install cmd/go cmd/link
|
||||
Set-Location ..
|
||||
```
|
||||
|
||||
If Windows Security blocks the build, keep Go's temporary and cache folders
|
||||
inside the repository folder (the scripts do: `.gotmp`, `.gocache`) and
|
||||
exclude that folder.
|
||||
|
||||
## Building the payloads
|
||||
|
||||
```powershell
|
||||
# daemon payload: C launcher + Go program -> out\tailscale.elf
|
||||
.\tools\build-payload.ps1 -GoDir tsd -Name tailscale -Version 0.2.1
|
||||
|
||||
# installer -> out\tailscale-installer.elf (embeds out\tailscale.elf)
|
||||
.\tools\build-installer.ps1
|
||||
```
|
||||
|
||||
## Sending to the console
|
||||
|
||||
```powershell
|
||||
$env:PS5_HOST = '192.168.1.50' # your console
|
||||
.\tools\ps5send.ps1 -File out\tailscale-installer.elf -Seconds 70
|
||||
```
|
||||
|
||||
`ps5send.ps1` prints whatever the payload writes back.
|
||||
|
||||
## Tests
|
||||
|
||||
The daemon's tests run on the build machine:
|
||||
|
||||
```powershell
|
||||
Set-Location tsd
|
||||
..\goroot\bin\go.exe test .
|
||||
```
|
||||
|
||||
The daemon also runs on Windows for work on the status page. Build `tsd`
|
||||
without the PS5 settings, set `PS5TS_DATA` to an empty folder and put a
|
||||
`config.json` with a free `webAddr` in it.
|
||||
|
||||
## Probes
|
||||
|
||||
`probe-c\` and `probe-go\` are the small payloads used to find out how the
|
||||
console behaves. They are useful when porting to another firmware.
|
||||
|
||||
```powershell
|
||||
. .\tools\env.ps1
|
||||
Invoke-PS5CC -O1 -Wall -o out\sysprobe.elf probe-c\sysprobe.c
|
||||
|
||||
# Go probe with the debug loader and a watchdog that kills it after 120 s
|
||||
.\tools\build-payload.ps1 -GoDir probe-go -Name probe -DebugLoader -Watchdog 120 -Send -Seconds 150
|
||||
```
|
||||
|
||||
Read the scheduling section of [TECHNICAL.md](TECHNICAL.md) before writing
|
||||
new tests: a payload that spins on every core at the default priority freezes
|
||||
the console.
|
||||
|
||||
## Updating Tailscale or Go
|
||||
|
||||
- Tailscale: `go get tailscale.com@<version>` in `tsd` with the patched Go,
|
||||
then rebuild. A Tailscale release that needs a newer Go needs the patch
|
||||
ported to that Go first.
|
||||
- Go: apply `patches\go1.27.1-ps5.patch` to the new tree and fix what does
|
||||
not apply. The patch touches nine files; TECHNICAL.md says why for each.
|
||||
@@ -0,0 +1,162 @@
|
||||
# Technical notes
|
||||
|
||||
How Tailscale, a Go program, runs on a PS5, and what was learned about the
|
||||
console along the way. Everything here was measured on one console
|
||||
(firmware 13.42, `kern.osreldate` 900000); treat it as observations, not a
|
||||
specification.
|
||||
|
||||
## Architecture
|
||||
|
||||
`tailscale.elf` is one payload made of two parts.
|
||||
|
||||
**The launcher** (`launcher/`, C, built with ps5-payload-sdk):
|
||||
|
||||
1. The SDK's crt runs first. Among other things it patches the process so
|
||||
that system calls may be issued from any address. Without that, the kernel
|
||||
only accepts syscalls made from libkernel, and Go makes its own.
|
||||
2. `main.c` raises privileges, moves the process to the lowest scheduling
|
||||
priority (see [Scheduling](#scheduling)) and sets `GOMAXPROCS=4`.
|
||||
3. `goload.c` maps the embedded Go program (a position-independent ELF),
|
||||
applies its `R_X86_64_RELATIVE` relocations, makes its text executable,
|
||||
builds the argc/argv/envp/auxv block a FreeBSD kernel would pass, switches
|
||||
to a fresh 1 MB stack and jumps to the Go entry point. The embedded copy
|
||||
is then released with `madvise(MADV_FREE)`.
|
||||
|
||||
**The daemon** (`tsd/`, Go): a `tsnet` server.
|
||||
|
||||
- Inbound: tsnet's fallback TCP handler pipes each tailnet connection to
|
||||
`127.0.0.1:<same port>`. It dials the local port before accepting, so
|
||||
ports with no listener are refused properly.
|
||||
- Outbound: local forwards (`localforward.go`) listen on localhost and relay
|
||||
TCP and UDP to a tailnet host through `tsnet.Server.Dial`. UDP is relayed
|
||||
per client address with an idle timeout. The Sunshine setting is a preset
|
||||
of seven such forwards.
|
||||
- A status page and small JSON API on port 8090, an HTTP proxy on
|
||||
`127.0.0.1:8118`, PS5 notifications by writing a request to
|
||||
`/dev/notification0`.
|
||||
- A payload that is sent again stops the running instance (through the
|
||||
status page, or failing that by the pid it recorded) and takes over.
|
||||
|
||||
**The installer** (`installer/`, C) embeds `tailscale.elf`. It stores it
|
||||
where the console's autoloader looks, appends it to the load order, registers
|
||||
a home screen app whose `param.json` has a `deeplinkUri` to the status page,
|
||||
and starts the daemon by sending it to the ELF loader on `127.0.0.1:9021`.
|
||||
|
||||
## The PS5 as a Go target
|
||||
|
||||
Go is built for `GOOS=freebsd GOARCH=amd64` with cgo off and
|
||||
`-buildmode=pie`, from a Go 1.27.1 tree with
|
||||
`patches/go1.27.1-ps5.patch`. What the patch changes and why:
|
||||
|
||||
| File | Change | Reason |
|
||||
| --- | --- | --- |
|
||||
| `internal/platform/supported.go` | Allow internally linked PIE on freebsd/amd64. | The image must load at any address and no C toolchain is involved. |
|
||||
| `runtime/defs_freebsd_amd64.go` | 48 bytes of padding in `ucontext` before `uc_mcontext`. | The PS5's signal context has extra fields there. Without it, nil-dereference panics and preemption read garbage. |
|
||||
| `runtime/defs_freebsd_amd64.go`, `sys_freebsd_amd64.s` | `kevent` 363 with the 32 byte struct. | The FreeBSD 12 `kevent` (560) does not exist. |
|
||||
| `runtime/sys_freebsd_amd64.s`, `os_freebsd.go` | `pipe2` built from `pipe` and `fcntl`. | `pipe2` (542) does not exist. |
|
||||
| `runtime/os_freebsd.go` | CPU count from `hw.ncpu`. | `kern.smp.maxcpus` does not exist. |
|
||||
| `syscall/asm_unix_amd64.s`, `asm9_unix2_amd64.s`, `ps5_freebsd_amd64.go` | Syscall emulation layer. | See below. |
|
||||
| `internal/routebsd/interface_freebsd.go` | Find the link address using `if_data`'s own length field. | Sony's `if_data` is 0xb0 bytes, so interface names came back empty. |
|
||||
| `os/executable_sysctl.go` | `os.Executable` falls back to a name from argv[0]. | The kernel has no path for a payload; tsnet treats the error as fatal. |
|
||||
|
||||
### The syscall table
|
||||
|
||||
The kernel implements the FreeBSD 9 system call numbers. **Every number from
|
||||
532 up is a Sony syscall with an unrelated meaning**, so nothing FreeBSD
|
||||
added later exists: `pipe2`, `accept4`, `ppoll`, `utimensat`, `futimens`,
|
||||
`getrandom`, the 64-bit inode `fstat`/`fstatat`/`getdirentries`/`statfs`
|
||||
family, the extended `kevent`. Issuing those numbers would call something
|
||||
else entirely.
|
||||
|
||||
Go's `Syscall`, `Syscall6`, `RawSyscall`, `RawSyscall6` and `Syscall9` are
|
||||
patched to divert numbers >= 532 to `ps5_freebsd_amd64.go`, which rebuilds
|
||||
them from FreeBSD 9 calls and converts the structures (old `stat`, `dirent`
|
||||
and `statfs` layouts). Anything not emulated returns `ENOSYS`. Because
|
||||
`golang.org/x/sys/unix` and `internal/syscall/unix` go through the same entry
|
||||
points, they are covered too. `socket` and `socketpair` are also diverted, to
|
||||
apply `SOCK_NONBLOCK`/`SOCK_CLOEXEC` with `fcntl`.
|
||||
|
||||
### Other kernel behaviour
|
||||
|
||||
- Page size is 16384. The Go linker aligns segments to 4096; the loader
|
||||
applies protection per 16 KiB page.
|
||||
- Working as on FreeBSD: `thr_new`, `_umtx_op`, `sigaction`, `sigaltstack`,
|
||||
`thr_kill`, kqueue including `EVFILT_USER`, `mmap` reservations with
|
||||
`PROT_NONE` (64 MB and 4 GB tried), `MAP_FIXED`, `madvise(MADV_FREE)`,
|
||||
`sysctl` for `hw.pagesize`, `hw.ncpu`, `kern.arandom`, `kern.boottime`,
|
||||
`NET_RT_IFLIST` and `NET_RT_DUMP`, `AF_ROUTE` sockets.
|
||||
- Directory reads: `/data` (nullfs) and `/user` (bfs) only accept a buffer of
|
||||
exactly 65536 bytes for `getdirentries`. The emulation always reads 64 KiB
|
||||
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.
|
||||
- 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
|
||||
its log file.
|
||||
- The SDK's `kernel_mprotect()` rewrites the protection of the whole kernel
|
||||
map entry that contains the address. The loader first splits the text range
|
||||
off with an ordinary `mprotect()`.
|
||||
- When the network is reconfigured (connection settings changed, Wi-Fi to
|
||||
Ethernet), listening sockets fail with errno 163, a Sony-specific code, and
|
||||
do not recover. The daemon's listeners reopen themselves
|
||||
(`tsd/listener.go`).
|
||||
|
||||
## Scheduling
|
||||
|
||||
This is the part to read before running any new test on a console.
|
||||
|
||||
Payload threads start as FIFO threads at priority 700 (`rtprio_thread`
|
||||
reports class 10, priority 700; the range is 256 to 767 and lower runs
|
||||
first). A FIFO thread that never blocks is never descheduled. A Go test with
|
||||
a busy loop on each of the 16 logical cores froze the console completely:
|
||||
the kernel still answered ping, but no user process ran, and only a
|
||||
power-cycle recovered it.
|
||||
|
||||
`rtprio_thread(RTP_SET)` silently ignores priorities outside that range, so
|
||||
the usual `RTP_PRIO_NORMAL, 0` does nothing and reports success. These are
|
||||
accepted and read back: class 3 (time-sharing) at 767, class 2 (round-robin)
|
||||
at 767, class 10 (FIFO) at 767. New threads inherit the setting.
|
||||
|
||||
The launcher sets class 3, priority 767 before Go starts, reads it back, and
|
||||
refuses to start if it did not take. With that, Go's signal-based preemption
|
||||
works across cores (4 spinning goroutines, 5 garbage collections in about
|
||||
350 ms). Test builds can add a watchdog thread that kills the process after
|
||||
a fixed time (`build-payload.ps1 -Watchdog`).
|
||||
|
||||
## Home screen icon
|
||||
|
||||
`/user/app/TSCL00001/sce_sys/param.json` with `applicationCategoryType` 65536
|
||||
and a `deeplinkUri`, plus `icon0.png`, registered with
|
||||
`sceAppInstUtilAppInstallTitleDir`. The console opens the link in its
|
||||
browser.
|
||||
|
||||
Linking `libSceAppInstUtil` alone leaves the payload stopped before it runs.
|
||||
It needs `-lSceIpmi -lSceAppInstUtil -lSceUserService -lSceSystemService`, in
|
||||
that order, as in the SDK's `install_app` sample.
|
||||
|
||||
## Autoloaders
|
||||
|
||||
- Payload Manager: load order in `/data/pldmgr/autoload.txt`, one file name
|
||||
per line, `!N` for a delay in milliseconds. It finds a named file anywhere
|
||||
below `/data/pldmgr` and launches it by streaming it to `127.0.0.1:9021`.
|
||||
- `ps5_autoloader`: `autoload.txt` in `/data/ps5_autoloader` or the same
|
||||
folder on a USB drive, with the payloads next to it.
|
||||
|
||||
The table of autoloaders exists twice, in `installer/main.c` and
|
||||
`tsd/autostart.go`.
|
||||
|
||||
## Known problems
|
||||
|
||||
- Once, switching the console from Wi-Fi to Ethernet during a Moonlight
|
||||
stream through the forwards froze the console. At that moment two daemon
|
||||
instances were running because of a bug that has since been fixed; whether
|
||||
that, the Moonlight client or something else caused the freeze is not
|
||||
known.
|
||||
- Once, a first login completed in the browser but the daemon's follow-up
|
||||
request was answered with "auth path not found". The daemon now requests a
|
||||
new link when it sees that error; the recovery path has not been observed
|
||||
in practice.
|
||||
- Whether Tailscale's own UDP sockets recover after a network
|
||||
reconfiguration has not been examined.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"titleId": "TSCL00001",
|
||||
"applicationCategoryType": 65536,
|
||||
"deeplinkUri": "http://127.0.0.1:8090/",
|
||||
"localizedParameters": {
|
||||
"defaultLanguage": "en-US",
|
||||
"en-US": {
|
||||
"titleName": "Tailscale"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,402 @@
|
||||
/* Tailscale installer payload for jailbroken PS5s.
|
||||
*
|
||||
* Stores the Tailscale daemon payload on the console, registers it with the
|
||||
* payload autoloader if one is set up, adds a home screen icon that opens
|
||||
* the status page, and starts the daemon through the ELF loader on this
|
||||
* console. Build with tools\build-installer.ps1, which sets DAEMON_ELF and
|
||||
* 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 *);
|
||||
|
||||
/* Where payload autoloaders keep their load order, and where the daemon has
|
||||
* to be stored for each. Keep in sync with tsd/autostart.go. */
|
||||
static const struct autoloader {
|
||||
const char *name;
|
||||
const char *list; /* load order: one file name per line */
|
||||
const char *payload_dir; /* where the payload goes */
|
||||
} autoloaders[] = {
|
||||
/* Payload Manager finds entries by file name anywhere below /data/pldmgr
|
||||
* and keeps each payload in a folder of its own. */
|
||||
{"Payload Manager", "/data/pldmgr/autoload.txt", "/data/pldmgr/payloads/Tailscale"},
|
||||
{"PS5 autoloader", "/data/ps5_autoloader/autoload.txt", "/data/ps5_autoloader"},
|
||||
{"PS5 autoloader (usb0)", "/mnt/usb0/ps5_autoloader/autoload.txt", "/mnt/usb0/ps5_autoloader"},
|
||||
{"PS5 autoloader (usb1)", "/mnt/usb1/ps5_autoloader/autoload.txt", "/mnt/usb1/ps5_autoloader"},
|
||||
{"PS5 autoloader (usb2)", "/mnt/usb2/ps5_autoloader/autoload.txt", "/mnt/usb2/ps5_autoloader"},
|
||||
{"PS5 autoloader (usb3)", "/mnt/usb3/ps5_autoloader/autoload.txt", "/mnt/usb3/ps5_autoloader"},
|
||||
};
|
||||
|
||||
typedef struct notify_request {
|
||||
char useless1[45];
|
||||
char message[3075];
|
||||
} 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 where the autoloader would pick it up. */
|
||||
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 load order already lists the daemon on a line of its own. */
|
||||
static int
|
||||
autoload_lists_daemon(const char *text) {
|
||||
size_t namelen = strlen(DAEMON_NAME);
|
||||
|
||||
for (const char *line = text; line && *line;) {
|
||||
const char *end = strchr(line, '\n');
|
||||
size_t len = end ? (size_t)(end - line) : strlen(line);
|
||||
while (len > 0 && (line[len - 1] == '\r' || line[len - 1] == ' ' || line[len - 1] == '\t')) {
|
||||
len--;
|
||||
}
|
||||
if (len == namelen && !strncmp(line, DAEMON_NAME, namelen)) {
|
||||
return 1;
|
||||
}
|
||||
line = end ? end + 1 : 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* If this autoloader is set up on the console, store the daemon for it and
|
||||
* add it to the end of the load order. Returns 1 if registered, 0 if the
|
||||
* autoloader is not present, -1 on error. */
|
||||
static int
|
||||
register_autoload(const struct autoloader *al) {
|
||||
struct stat st;
|
||||
char *text;
|
||||
size_t got;
|
||||
FILE *f;
|
||||
|
||||
if (stat(al->list, &st)) {
|
||||
return 0;
|
||||
}
|
||||
printf("%s:\n", al->name);
|
||||
if (install_daemon(al->payload_dir)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!(f = fopen(al->list, "rb"))) {
|
||||
printf(" could not read %s: %s\n", al->list, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
text = calloc(1, st.st_size + 1);
|
||||
got = fread(text, 1, st.st_size, f);
|
||||
fclose(f);
|
||||
text[got] = 0;
|
||||
|
||||
if (autoload_lists_daemon(text)) {
|
||||
printf(" %s already lists %s\n", al->list, DAEMON_NAME);
|
||||
} else {
|
||||
if (!(f = fopen(al->list, "ab"))) {
|
||||
printf(" could not update %s: %s\n", al->list, strerror(errno));
|
||||
free(text);
|
||||
return -1;
|
||||
}
|
||||
if (got > 0 && text[got - 1] != '\n') {
|
||||
fputc('\n', f);
|
||||
}
|
||||
fputs(DAEMON_NAME "\n", f);
|
||||
fclose(f);
|
||||
printf(" added %s to the end of %s\n", DAEMON_NAME, al->list);
|
||||
}
|
||||
free(text);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Report whether the file at path already has exactly these contents. */
|
||||
static int
|
||||
file_matches(const char *path, const uint8_t *data, size_t size) {
|
||||
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;
|
||||
int registered = 0;
|
||||
|
||||
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");
|
||||
mkdir(DATA_DIR, 0755);
|
||||
|
||||
for (size_t i = 0; i < sizeof(autoloaders) / sizeof(autoloaders[0]); i++) {
|
||||
if (register_autoload(&autoloaders[i]) > 0) {
|
||||
registered++;
|
||||
}
|
||||
}
|
||||
if (!registered) {
|
||||
/* No autoloader: keep the payload where the user can find it. */
|
||||
printf("No payload autoloader found on this console.\n");
|
||||
if (install_daemon(DATA_DIR)) {
|
||||
notify("Tailscale install failed:\ncould not write to %s", DATA_DIR);
|
||||
return 1;
|
||||
}
|
||||
printf(" Tailscale will not start by itself after a reboot.\n"
|
||||
" Send %s/%s to the ELF loader to start it.\n",
|
||||
DATA_DIR, DAEMON_NAME);
|
||||
}
|
||||
|
||||
printf("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");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
/* In-process loader for a Go program built for the PS5 (GOOS=freebsd,
|
||||
* -buildmode=pie, internal linking).
|
||||
*
|
||||
* The SDK crt has already widened the address range that is allowed to issue
|
||||
* syscalls, so the Go runtime can use its own raw SYSCALL instructions. All
|
||||
* that is left is to map the image, relocate it, and enter it the way the
|
||||
* FreeBSD kernel would: %rdi pointing at argc/argv/envp/auxv. */
|
||||
|
||||
#include <elf.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <sys/mman.h>
|
||||
|
||||
#include <ps5/kernel.h>
|
||||
|
||||
#include "goload.h"
|
||||
|
||||
#define PS5_PAGE_SIZE 0x4000ul
|
||||
#define PAGE_TRUNC(x) ((x) & ~(PS5_PAGE_SIZE - 1))
|
||||
#define PAGE_ROUND(x) PAGE_TRUNC((x) + PS5_PAGE_SIZE - 1)
|
||||
|
||||
#define GO_STACK_SIZE (1ul << 20)
|
||||
|
||||
#ifndef AT_PAGESZ
|
||||
#define AT_PAGESZ 6
|
||||
#endif
|
||||
#ifndef AT_NULL
|
||||
#define AT_NULL 0
|
||||
#endif
|
||||
|
||||
void goload_enter(uintptr_t entry, uintptr_t *args, void *stack_top) __attribute__((noreturn));
|
||||
|
||||
/* Switch to the new stack and jump to the Go entry point. Never returns. */
|
||||
__asm__(".intel_syntax noprefix\n"
|
||||
".text\n"
|
||||
".global goload_enter\n"
|
||||
".type goload_enter, @function\n"
|
||||
"goload_enter:\n"
|
||||
" mov rax, rdi\n"
|
||||
" mov rdi, rsi\n"
|
||||
" mov rsp, rdx\n"
|
||||
" xor ebp, ebp\n"
|
||||
" jmp rax\n"
|
||||
".att_syntax prefix\n");
|
||||
|
||||
#ifdef GOLOAD_DEBUG
|
||||
#include <signal.h>
|
||||
#include <stdlib.h>
|
||||
#include <ucontext.h>
|
||||
|
||||
static uintptr_t dbg_base;
|
||||
static uintptr_t dbg_vaddr;
|
||||
|
||||
/* Report faults that happen before the Go runtime installs its own signal
|
||||
* handlers. Only async-signal-safe calls are used. */
|
||||
static void
|
||||
dbg_fault(int sig, siginfo_t *info, void *uctx) {
|
||||
ucontext_t *uc = uctx;
|
||||
/* The PS5 kernel puts 0x30 extra bytes between uc_sigmask and uc_mcontext. */
|
||||
mcontext_t *mc = (mcontext_t *)((char *)&uc->uc_mcontext + 0x30);
|
||||
char buf[1024];
|
||||
uintptr_t rip = mc->mc_rip;
|
||||
int n = snprintf(buf, sizeof(buf),
|
||||
"goload: fatal signal %d code=%d addr=%p rip=%#lx (image vaddr %#lx) rsp=%#lx\n"
|
||||
" rax=%#lx rbx=%#lx rcx=%#lx rdx=%#lx rsi=%#lx rdi=%#lx rbp=%#lx\n"
|
||||
" r8=%#lx r9=%#lx r10=%#lx r11=%#lx r12=%#lx r13=%#lx r14=%#lx r15=%#lx\n"
|
||||
" trapno=%ld err=%#lx flags=%#lx fsbase=%#lx getpid=%p\n",
|
||||
sig, info->si_code, info->si_addr, (unsigned long)rip,
|
||||
(unsigned long)(rip - dbg_base + dbg_vaddr), (unsigned long)mc->mc_rsp,
|
||||
(unsigned long)mc->mc_rax, (unsigned long)mc->mc_rbx, (unsigned long)mc->mc_rcx,
|
||||
(unsigned long)mc->mc_rdx, (unsigned long)mc->mc_rsi, (unsigned long)mc->mc_rdi,
|
||||
(unsigned long)mc->mc_rbp, (unsigned long)mc->mc_r8, (unsigned long)mc->mc_r9,
|
||||
(unsigned long)mc->mc_r10, (unsigned long)mc->mc_r11, (unsigned long)mc->mc_r12,
|
||||
(unsigned long)mc->mc_r13, (unsigned long)mc->mc_r14, (unsigned long)mc->mc_r15,
|
||||
(long)mc->mc_trapno, (unsigned long)mc->mc_err, (unsigned long)mc->mc_rflags,
|
||||
(unsigned long)mc->mc_fsbase, (void *)getpid);
|
||||
write(2, buf, n);
|
||||
_exit(128 + sig);
|
||||
}
|
||||
|
||||
static void
|
||||
dbg_install(uintptr_t base, uintptr_t vaddr) {
|
||||
static const int sigs[] = {SIGSEGV, SIGBUS, SIGILL, SIGFPE, SIGSYS, SIGTRAP, SIGABRT};
|
||||
struct sigaction sa = {0};
|
||||
stack_t ss = {0};
|
||||
|
||||
dbg_base = base;
|
||||
dbg_vaddr = vaddr;
|
||||
ss.ss_size = 64 * 1024;
|
||||
ss.ss_sp = malloc(ss.ss_size);
|
||||
sigaltstack(&ss, 0);
|
||||
sa.sa_sigaction = dbg_fault;
|
||||
sa.sa_flags = SA_SIGINFO | SA_ONSTACK;
|
||||
sigfillset(&sa.sa_mask);
|
||||
for (size_t i = 0; i < sizeof(sigs) / sizeof(sigs[0]); i++) {
|
||||
sigaction(sigs[i], &sa, 0);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
static int
|
||||
image_check(const uint8_t *image, size_t size) {
|
||||
const Elf64_Ehdr *ehdr = (const Elf64_Ehdr *)image;
|
||||
|
||||
if (size < sizeof(*ehdr) || memcmp(ehdr->e_ident, ELFMAG, SELFMAG)) {
|
||||
return -1;
|
||||
}
|
||||
if (ehdr->e_type != ET_DYN || ehdr->e_machine != EM_X86_64) {
|
||||
return -1;
|
||||
}
|
||||
if (ehdr->e_phoff + (size_t)ehdr->e_phnum * sizeof(Elf64_Phdr) > size) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
goload_run(const uint8_t *image, size_t size, char *const argv[], char *const envp[]) {
|
||||
const Elf64_Ehdr *ehdr = (const Elf64_Ehdr *)image;
|
||||
const Elf64_Phdr *phdr = (const Elf64_Phdr *)(image + ehdr->e_phoff);
|
||||
const Elf64_Dyn *dyn = 0;
|
||||
const Elf64_Rela *rela = 0;
|
||||
size_t relasz = 0;
|
||||
uintptr_t min_vaddr = UINTPTR_MAX;
|
||||
uintptr_t max_vaddr = 0;
|
||||
uintptr_t bias;
|
||||
uint8_t *base;
|
||||
uint8_t *stack;
|
||||
uintptr_t *sp;
|
||||
int argc = 0;
|
||||
int envc = 0;
|
||||
|
||||
if (image_check(image, size)) {
|
||||
fprintf(stderr, "goload: not a relocatable x86-64 ELF image\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (int i = 0; i < ehdr->e_phnum; i++) {
|
||||
if (phdr[i].p_type != PT_LOAD || !phdr[i].p_memsz) {
|
||||
continue;
|
||||
}
|
||||
if (phdr[i].p_offset + phdr[i].p_filesz > size) {
|
||||
fprintf(stderr, "goload: truncated image\n");
|
||||
return -1;
|
||||
}
|
||||
if (phdr[i].p_vaddr < min_vaddr) {
|
||||
min_vaddr = phdr[i].p_vaddr;
|
||||
}
|
||||
if (phdr[i].p_vaddr + phdr[i].p_memsz > max_vaddr) {
|
||||
max_vaddr = phdr[i].p_vaddr + phdr[i].p_memsz;
|
||||
}
|
||||
}
|
||||
if (min_vaddr >= max_vaddr) {
|
||||
fprintf(stderr, "goload: image has no loadable segments\n");
|
||||
return -1;
|
||||
}
|
||||
min_vaddr = PAGE_TRUNC(min_vaddr);
|
||||
max_vaddr = PAGE_ROUND(max_vaddr);
|
||||
|
||||
base = mmap(0, max_vaddr - min_vaddr, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
if (base == MAP_FAILED) {
|
||||
perror("goload: mmap image");
|
||||
return -1;
|
||||
}
|
||||
bias = (uintptr_t)base - min_vaddr;
|
||||
|
||||
for (int i = 0; i < ehdr->e_phnum; i++) {
|
||||
if (phdr[i].p_type == PT_LOAD && phdr[i].p_filesz) {
|
||||
memcpy((void *)(bias + phdr[i].p_vaddr), image + phdr[i].p_offset, phdr[i].p_filesz);
|
||||
} else if (phdr[i].p_type == PT_DYNAMIC) {
|
||||
dyn = (const Elf64_Dyn *)(bias + phdr[i].p_vaddr);
|
||||
}
|
||||
}
|
||||
|
||||
/* The Go linker only emits R_X86_64_RELATIVE for an internally linked PIE. */
|
||||
for (; dyn && dyn->d_tag != DT_NULL; dyn++) {
|
||||
if (dyn->d_tag == DT_RELA) {
|
||||
rela = (const Elf64_Rela *)(bias + dyn->d_un.d_ptr);
|
||||
} else if (dyn->d_tag == DT_RELASZ) {
|
||||
relasz = dyn->d_un.d_val;
|
||||
}
|
||||
}
|
||||
for (size_t i = 0; rela && i < relasz / sizeof(*rela); i++) {
|
||||
if (ELF64_R_TYPE(rela[i].r_info) != R_X86_64_RELATIVE) {
|
||||
fprintf(stderr, "goload: unsupported relocation type %u\n", (unsigned)ELF64_R_TYPE(rela[i].r_info));
|
||||
return -1;
|
||||
}
|
||||
*(uintptr_t *)(bias + rela[i].r_offset) = bias + rela[i].r_addend;
|
||||
}
|
||||
|
||||
/* Segments are 4 KiB aligned but the PS5 uses 16 KiB pages, so protection
|
||||
* is applied to whole pages: executable ones become R+X, the rest stays
|
||||
* R+W. A page shared by text and read-only data ends up executable, which
|
||||
* is harmless. */
|
||||
for (int i = 0; i < ehdr->e_phnum; i++) {
|
||||
if (phdr[i].p_type != PT_LOAD || !(phdr[i].p_flags & PF_X)) {
|
||||
continue;
|
||||
}
|
||||
uintptr_t start = PAGE_TRUNC(bias + phdr[i].p_vaddr);
|
||||
uintptr_t end = PAGE_ROUND(bias + phdr[i].p_vaddr + phdr[i].p_memsz);
|
||||
/* kernel_mprotect rewrites the protection of the whole kernel map entry
|
||||
* that contains the address, so first let a regular mprotect split the
|
||||
* text range off into an entry of its own. */
|
||||
if (mprotect((void *)start, end - start, PROT_READ)) {
|
||||
perror("goload: mprotect");
|
||||
return -1;
|
||||
}
|
||||
if (kernel_mprotect(-1, start, end - start, PROT_READ | PROT_EXEC)) {
|
||||
fprintf(stderr, "goload: kernel_mprotect failed\n");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
stack = mmap(0, GO_STACK_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
if (stack == MAP_FAILED) {
|
||||
perror("goload: mmap stack");
|
||||
return -1;
|
||||
}
|
||||
|
||||
while (argv && argv[argc]) {
|
||||
argc++;
|
||||
}
|
||||
while (envp && envp[envc]) {
|
||||
envc++;
|
||||
}
|
||||
|
||||
/* argc, argv[], NULL, envp[], NULL, auxv pairs, AT_NULL */
|
||||
sp = (uintptr_t *)(stack + GO_STACK_SIZE);
|
||||
sp -= 1 + (argc + 1) + (envc + 1) + 4;
|
||||
sp = (uintptr_t *)((uintptr_t)sp & ~0xful);
|
||||
|
||||
uintptr_t *p = sp;
|
||||
*p++ = (uintptr_t)argc;
|
||||
for (int i = 0; i < argc; i++) {
|
||||
*p++ = (uintptr_t)argv[i];
|
||||
}
|
||||
*p++ = 0;
|
||||
for (int i = 0; i < envc; i++) {
|
||||
*p++ = (uintptr_t)envp[i];
|
||||
}
|
||||
*p++ = 0;
|
||||
*p++ = AT_PAGESZ;
|
||||
*p++ = PS5_PAGE_SIZE;
|
||||
*p++ = AT_NULL;
|
||||
*p++ = 0;
|
||||
|
||||
#ifdef GOLOAD_DEBUG
|
||||
fprintf(stderr, "goload: image %p..%p entry %#lx stack %p..%p argc=%d\n", base,
|
||||
base + (max_vaddr - min_vaddr), (unsigned long)(bias + ehdr->e_entry), stack, stack + GO_STACK_SIZE, argc);
|
||||
dbg_install((uintptr_t)base, min_vaddr);
|
||||
#endif
|
||||
|
||||
fflush(stdout);
|
||||
fflush(stderr);
|
||||
|
||||
/* The embedded image has been copied and is not read again. Let the kernel
|
||||
* reclaim its pages rather than keep two copies of the program in memory. */
|
||||
uintptr_t entry = bias + ehdr->e_entry;
|
||||
uintptr_t free_start = PAGE_ROUND((uintptr_t)image);
|
||||
uintptr_t free_end = PAGE_TRUNC((uintptr_t)image + size);
|
||||
if (free_end > free_start) {
|
||||
madvise((void *)free_start, free_end - free_start, MADV_FREE);
|
||||
}
|
||||
|
||||
goload_enter(entry, sp, sp);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* Map the Go PIE image held in memory, relocate it and jump into it on a
|
||||
* fresh stack. Only returns (with -1) if loading failed. */
|
||||
int goload_run(const uint8_t *image, size_t size, char *const argv[], char *const envp[]);
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
/* PS5 payload entry point: prepare the process, then hand control to the
|
||||
* embedded Go program. Build with -DGO_IMAGE="path/to/program.bin". */
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <sys/mman.h>
|
||||
|
||||
#include <ps5/kernel.h>
|
||||
|
||||
#include "goload.h"
|
||||
|
||||
#ifndef GO_IMAGE
|
||||
#error "GO_IMAGE must name the Go binary to embed"
|
||||
#endif
|
||||
|
||||
/* How many OS threads may run Go code at once. The console has 16 logical
|
||||
* cores, but this is a background service and games need them more. */
|
||||
#ifndef GO_MAXPROCS
|
||||
#define GO_MAXPROCS "4"
|
||||
#endif
|
||||
|
||||
extern const uint8_t go_image[];
|
||||
extern const uint8_t go_image_end[];
|
||||
|
||||
__asm__(".section .rodata\n"
|
||||
".balign 0x4000\n"
|
||||
".global go_image\n"
|
||||
"go_image:\n"
|
||||
".incbin \"" GO_IMAGE "\"\n"
|
||||
".global go_image_end\n"
|
||||
"go_image_end:\n"
|
||||
".text\n");
|
||||
|
||||
#define SYS_rtprio_thread 466
|
||||
#define RTP_LOOKUP 0
|
||||
#define RTP_SET 1
|
||||
#define RTP_PRIO_REALTIME 2 /* round-robin */
|
||||
#define RTP_PRIO_NORMAL 3 /* time-sharing */
|
||||
#define PS5_PRIO_DEFAULT 700
|
||||
#define PS5_PRIO_LOWEST 767
|
||||
|
||||
struct rtprio {
|
||||
unsigned short type;
|
||||
unsigned short prio;
|
||||
};
|
||||
|
||||
static long
|
||||
raw_syscall3(long n, long a, long b, long c) {
|
||||
unsigned char failed;
|
||||
__asm__ volatile("syscall; setc %1" : "+a"(n), "=q"(failed) : "D"(a), "S"(b), "d"(c) : "rcx", "r11", "memory");
|
||||
return failed ? -n : n;
|
||||
}
|
||||
|
||||
/* Payload threads start as FIFO threads at the PS5's default priority (class
|
||||
* 10, priority 700; lower numbers run first, 767 is the lowest). A FIFO
|
||||
* thread that never blocks is never descheduled, so a busy loop on every core
|
||||
* freezes the whole console.
|
||||
*
|
||||
* Before any Go thread exists, move to the time-sharing class at the lowest
|
||||
* priority, where nothing this process does can keep the system's own threads
|
||||
* off the CPU. Threads created later inherit the setting. The kernel ignores
|
||||
* priorities outside its own range without reporting an error, so the result
|
||||
* is read back. Round-robin at the lowest priority is the fallback. */
|
||||
static int
|
||||
leave_realtime_class(void) {
|
||||
static const struct rtprio choices[] = {
|
||||
{RTP_PRIO_NORMAL, PS5_PRIO_LOWEST},
|
||||
{RTP_PRIO_REALTIME, PS5_PRIO_LOWEST},
|
||||
};
|
||||
struct rtprio before = {0}, after = {0};
|
||||
|
||||
raw_syscall3(SYS_rtprio_thread, RTP_LOOKUP, 0, (long)&before);
|
||||
for (size_t i = 0; i < sizeof(choices) / sizeof(choices[0]); i++) {
|
||||
struct rtprio want = choices[i];
|
||||
raw_syscall3(SYS_rtprio_thread, RTP_SET, 0, (long)&want);
|
||||
raw_syscall3(SYS_rtprio_thread, RTP_LOOKUP, 0, (long)&after);
|
||||
if (after.type == choices[i].type && after.prio == choices[i].prio) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
#ifdef GOLOAD_DEBUG
|
||||
fprintf(stderr, "launcher: scheduling class %u/%u -> %u/%u\n", before.type, before.prio, after.type, after.prio);
|
||||
#else
|
||||
(void)before;
|
||||
#endif
|
||||
return after.prio > PS5_PRIO_DEFAULT && after.type != before.type ? 0 : -1;
|
||||
}
|
||||
|
||||
#ifdef GOLOAD_WATCHDOG
|
||||
#include <pthread.h>
|
||||
#include <signal.h>
|
||||
|
||||
/* Test builds only: kill the process after a fixed time, from a thread the Go
|
||||
* scheduler knows nothing about, so that a hung test cannot outlive its
|
||||
* welcome. */
|
||||
static void *
|
||||
watchdog_main(void *arg) {
|
||||
sleep(GOLOAD_WATCHDOG);
|
||||
static const char msg[] = "launcher: watchdog expired, killing the process\n";
|
||||
write(2, msg, sizeof(msg) - 1);
|
||||
kill(getpid(), SIGKILL);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
start_watchdog(void) {
|
||||
sigset_t all, old;
|
||||
pthread_t thread;
|
||||
|
||||
/* The thread must never run a Go signal handler, so it starts with every
|
||||
* signal blocked. */
|
||||
sigfillset(&all);
|
||||
pthread_sigmask(SIG_SETMASK, &all, &old);
|
||||
pthread_create(&thread, 0, watchdog_main, 0);
|
||||
pthread_sigmask(SIG_SETMASK, &old, 0);
|
||||
}
|
||||
#endif
|
||||
|
||||
int
|
||||
main(int argc, char **argv) {
|
||||
pid_t pid = getpid();
|
||||
intptr_t rootvnode;
|
||||
|
||||
/* Run as root outside the sandbox so /data and the network are reachable. */
|
||||
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 (leave_realtime_class()) {
|
||||
/* Without this a runaway goroutine could hang the console, so do not
|
||||
* take the chance. */
|
||||
fprintf(stderr, "launcher: could not leave the real-time scheduling class; not starting\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
#ifdef GOLOAD_WATCHDOG
|
||||
start_watchdog();
|
||||
#endif
|
||||
|
||||
static char *envp[] = {
|
||||
"HOME=/data/tailscale",
|
||||
"TMPDIR=/data/tailscale/tmp",
|
||||
"GOMAXPROCS=" GO_MAXPROCS,
|
||||
#ifdef GO_DEBUG
|
||||
"GODEBUG=" GO_DEBUG,
|
||||
#endif
|
||||
"PS5=1",
|
||||
0,
|
||||
};
|
||||
|
||||
return goload_run(go_image, (size_t)(go_image_end - go_image), argv, envp);
|
||||
}
|
||||
@@ -0,0 +1,926 @@
|
||||
diff --git a/src/internal/platform/supported.go b/src/internal/platform/supported.go
|
||||
index 6f37e36..f23af91 100644
|
||||
--- a/src/internal/platform/supported.go
|
||||
+++ b/src/internal/platform/supported.go
|
||||
@@ -218,6 +218,7 @@ func InternalLinkPIESupported(goos, goarch string) bool {
|
||||
switch goos + "/" + goarch {
|
||||
case "android/arm64",
|
||||
"darwin/amd64", "darwin/arm64",
|
||||
+ "freebsd/amd64", // PS5: payloads are loaded at an arbitrary address by our own loader
|
||||
"linux/amd64", "linux/arm64", "linux/loong64", "linux/ppc64", "linux/ppc64le", "linux/s390x",
|
||||
"windows/386", "windows/amd64", "windows/arm64":
|
||||
return true
|
||||
diff --git a/src/runtime/defs_freebsd_amd64.go b/src/runtime/defs_freebsd_amd64.go
|
||||
index 8f0b08d..9d3ac79 100644
|
||||
--- a/src/runtime/defs_freebsd_amd64.go
|
||||
+++ b/src/runtime/defs_freebsd_amd64.go
|
||||
@@ -205,6 +205,7 @@ type mcontext struct {
|
||||
|
||||
type ucontext struct {
|
||||
uc_sigmask sigset
|
||||
+ ps5_pad [48]byte // PS5: the kernel has extra fields before the machine context
|
||||
uc_mcontext mcontext
|
||||
uc_link *ucontext
|
||||
uc_stack stackt
|
||||
@@ -251,7 +252,7 @@ type keventt struct {
|
||||
fflags uint32
|
||||
data int64
|
||||
udata *byte
|
||||
- ext [4]uint64
|
||||
+ // PS5: no ext field. The kernel only has the 32 byte kevent of FreeBSD 11 and earlier.
|
||||
}
|
||||
|
||||
type bintime struct {
|
||||
diff --git a/src/runtime/sys_freebsd_amd64.s b/src/runtime/sys_freebsd_amd64.s
|
||||
index 977ea09..9b675ba 100644
|
||||
--- a/src/runtime/sys_freebsd_amd64.s
|
||||
+++ b/src/runtime/sys_freebsd_amd64.s
|
||||
@@ -43,8 +43,10 @@
|
||||
#define SYS_thr_new 455
|
||||
#define SYS_mmap 477
|
||||
#define SYS_cpuset_getaffinity 487
|
||||
-#define SYS_pipe2 542
|
||||
-#define SYS_kevent 560
|
||||
+// PS5: syscall numbers from 532 up belong to Sony, so pipe2 (542) and the
|
||||
+// FreeBSD 12 kevent (560) are replaced by their older equivalents.
|
||||
+#define SYS_pipe 42
|
||||
+#define SYS_kevent 363
|
||||
|
||||
TEXT runtime·sys_umtx_op(SB),NOSPLIT,$0
|
||||
MOVQ addr+0(FP), DI
|
||||
@@ -137,15 +139,21 @@ TEXT runtime·read(SB),NOSPLIT,$-8
|
||||
MOVL AX, ret+24(FP)
|
||||
RET
|
||||
|
||||
-// func pipe2(flags int32) (r, w int32, errno int32)
|
||||
-TEXT runtime·pipe2(SB),NOSPLIT,$0-20
|
||||
- LEAQ r+8(FP), DI
|
||||
- MOVL flags+0(FP), SI
|
||||
- MOVL $SYS_pipe2, AX
|
||||
+// func pipe() (r, w int32, errno int32)
|
||||
+// The two descriptors come back in AX and DX.
|
||||
+TEXT runtime·pipe(SB),NOSPLIT,$0-12
|
||||
+ MOVL $SYS_pipe, AX
|
||||
SYSCALL
|
||||
- JCC 2(PC)
|
||||
+ JCC pipeok
|
||||
NEGQ AX
|
||||
- MOVL AX, errno+16(FP)
|
||||
+ MOVL $-1, r+0(FP)
|
||||
+ MOVL $-1, w+4(FP)
|
||||
+ MOVL AX, errno+8(FP)
|
||||
+ RET
|
||||
+pipeok:
|
||||
+ MOVL AX, r+0(FP)
|
||||
+ MOVL DX, w+4(FP)
|
||||
+ MOVL $0, errno+8(FP)
|
||||
RET
|
||||
|
||||
TEXT runtime·write1(SB),NOSPLIT,$-8
|
||||
diff --git a/src/runtime/os_freebsd.go b/src/runtime/os_freebsd.go
|
||||
index 68d895b..efa6b32 100644
|
||||
--- a/src/runtime/os_freebsd.go
|
||||
+++ b/src/runtime/os_freebsd.go
|
||||
@@ -49,9 +49,33 @@ func kqueue() int32
|
||||
//go:noescape
|
||||
func kevent(kq int32, ch *keventt, nch int32, ev *keventt, nev int32, ts *timespec) int32
|
||||
|
||||
-func pipe2(flags int32) (r, w int32, errno int32)
|
||||
+func pipe() (r, w int32, errno int32)
|
||||
func fcntl(fd, cmd, arg int32) (ret int32, errno int32)
|
||||
|
||||
+// pipe2 is not implemented by the PS5 kernel, so build it from pipe and fcntl.
|
||||
+func pipe2(flags int32) (r, w int32, errno int32) {
|
||||
+ const (
|
||||
+ _F_GETFL = 3
|
||||
+ _F_SETFL = 4
|
||||
+ )
|
||||
+ r, w, errno = pipe()
|
||||
+ if errno != 0 {
|
||||
+ return -1, -1, errno
|
||||
+ }
|
||||
+ if flags&_O_CLOEXEC != 0 {
|
||||
+ closeonexec(r)
|
||||
+ closeonexec(w)
|
||||
+ }
|
||||
+ if flags&_O_NONBLOCK != 0 {
|
||||
+ for _, fd := range [2]int32{r, w} {
|
||||
+ if fl, e := fcntl(fd, _F_GETFL, 0); e == 0 {
|
||||
+ fcntl(fd, _F_SETFL, fl|_O_NONBLOCK)
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ return r, w, 0
|
||||
+}
|
||||
+
|
||||
func issetugid() int32
|
||||
|
||||
// From FreeBSD's <sys/sysctl.h>
|
||||
@@ -108,14 +132,15 @@ func getCPUCount() int32 {
|
||||
// with dynamically assigned sysctl entries.
|
||||
miblen := sysctlnametomib([]byte("kern.smp.maxcpus"), &mib)
|
||||
if miblen == 0 {
|
||||
- return 1
|
||||
+ // PS5: kern.smp.maxcpus does not exist.
|
||||
+ return getCPUCountHW()
|
||||
}
|
||||
|
||||
// Query kern.smp.maxcpus.
|
||||
dstsize := uintptr(4)
|
||||
maxcpus := uint32(0)
|
||||
if sysctl(&mib[0], miblen, (*byte)(unsafe.Pointer(&maxcpus)), &dstsize, nil, 0) != 0 {
|
||||
- return 1
|
||||
+ return getCPUCountHW()
|
||||
}
|
||||
|
||||
maskSize := int(maxcpus+7) / 8
|
||||
@@ -128,7 +153,7 @@ func getCPUCount() int32 {
|
||||
|
||||
if cpuset_getaffinity(_CPU_LEVEL_WHICH, _CPU_WHICH_PID, _CPU_CURRENT_PID,
|
||||
maskSize, (*byte)(unsafe.Pointer(&mask[0]))) != 0 {
|
||||
- return 1
|
||||
+ return getCPUCountHW()
|
||||
}
|
||||
n := int32(0)
|
||||
for _, v := range mask[:maskSize] {
|
||||
@@ -143,6 +168,18 @@ func getCPUCount() int32 {
|
||||
return n
|
||||
}
|
||||
|
||||
+// getCPUCountHW reads hw.ncpu. It is the PS5 fallback for getCPUCount.
|
||||
+func getCPUCountHW() int32 {
|
||||
+ const _HW_NCPU = 3
|
||||
+ mib := [2]uint32{_CTL_HW, _HW_NCPU}
|
||||
+ out := uint32(0)
|
||||
+ nout := unsafe.Sizeof(out)
|
||||
+ if sysctl(&mib[0], 2, (*byte)(unsafe.Pointer(&out)), &nout, nil, 0) >= 0 && out > 0 {
|
||||
+ return int32(out)
|
||||
+ }
|
||||
+ return 1
|
||||
+}
|
||||
+
|
||||
func getPageSize() uintptr {
|
||||
mib := [2]uint32{_CTL_HW, _HW_PAGESIZE}
|
||||
out := uint32(0)
|
||||
diff --git a/src/syscall/asm_unix_amd64.s b/src/syscall/asm_unix_amd64.s
|
||||
index 6d8da71..5f44736 100644
|
||||
--- a/src/syscall/asm_unix_amd64.s
|
||||
+++ b/src/syscall/asm_unix_amd64.s
|
||||
@@ -11,11 +11,74 @@
|
||||
// System call support for AMD64 unixes
|
||||
//
|
||||
|
||||
+// PS5: the kernel implements the FreeBSD 9 syscall table. Numbers from 532 up
|
||||
+// are Sony syscalls with unrelated meanings, so they must never reach the
|
||||
+// kernel. The exported entry points below hand those numbers, plus socket (97)
|
||||
+// and socketpair (135) whose SOCK_NONBLOCK/SOCK_CLOEXEC type flags need
|
||||
+// translating, to the emulation in ps5_freebsd_amd64.go. Everything else goes
|
||||
+// straight to the unchanged implementations, renamed to sysDirect*.
|
||||
+
|
||||
+#define PS5_FIRST_SONY_SYSCALL 532
|
||||
+#define PS5_SYS_SOCKET 97
|
||||
+#define PS5_SYS_SOCKETPAIR 135
|
||||
+
|
||||
// func Syscall(trap int64, a1, a2, a3 int64) (r1, r2, err int64)
|
||||
+TEXT ·Syscall(SB),NOSPLIT,$0-56
|
||||
+ MOVQ trap+0(FP), AX
|
||||
+ CMPQ AX, $PS5_FIRST_SONY_SYSCALL
|
||||
+ JAE emulate
|
||||
+ CMPQ AX, $PS5_SYS_SOCKET
|
||||
+ JEQ emulate
|
||||
+ CMPQ AX, $PS5_SYS_SOCKETPAIR
|
||||
+ JEQ emulate
|
||||
+ JMP ·sysDirect(SB)
|
||||
+emulate:
|
||||
+ JMP ·ps5Syscall(SB)
|
||||
+
|
||||
// func Syscall6(trap int64, a1, a2, a3, a4, a5, a6 int64) (r1, r2, err int64)
|
||||
+TEXT ·Syscall6(SB),NOSPLIT,$0-80
|
||||
+ MOVQ trap+0(FP), AX
|
||||
+ CMPQ AX, $PS5_FIRST_SONY_SYSCALL
|
||||
+ JAE emulate6
|
||||
+ CMPQ AX, $PS5_SYS_SOCKET
|
||||
+ JEQ emulate6
|
||||
+ CMPQ AX, $PS5_SYS_SOCKETPAIR
|
||||
+ JEQ emulate6
|
||||
+ JMP ·sysDirect6(SB)
|
||||
+emulate6:
|
||||
+ JMP ·ps5Syscall6(SB)
|
||||
+
|
||||
+// func RawSyscall(trap int64, a1, a2, a3 int64) (r1, r2, err int64)
|
||||
+TEXT ·RawSyscall(SB),NOSPLIT,$0-56
|
||||
+ MOVQ trap+0(FP), AX
|
||||
+ CMPQ AX, $PS5_FIRST_SONY_SYSCALL
|
||||
+ JAE emulateraw
|
||||
+ CMPQ AX, $PS5_SYS_SOCKET
|
||||
+ JEQ emulateraw
|
||||
+ CMPQ AX, $PS5_SYS_SOCKETPAIR
|
||||
+ JEQ emulateraw
|
||||
+ JMP ·rawSysDirect(SB)
|
||||
+emulateraw:
|
||||
+ JMP ·ps5RawSyscall(SB)
|
||||
+
|
||||
+// func RawSyscall6(trap int64, a1, a2, a3, a4, a5, a6 int64) (r1, r2, err int64)
|
||||
+TEXT ·RawSyscall6(SB),NOSPLIT,$0-80
|
||||
+ MOVQ trap+0(FP), AX
|
||||
+ CMPQ AX, $PS5_FIRST_SONY_SYSCALL
|
||||
+ JAE emulateraw6
|
||||
+ CMPQ AX, $PS5_SYS_SOCKET
|
||||
+ JEQ emulateraw6
|
||||
+ CMPQ AX, $PS5_SYS_SOCKETPAIR
|
||||
+ JEQ emulateraw6
|
||||
+ JMP ·rawSysDirect6(SB)
|
||||
+emulateraw6:
|
||||
+ JMP ·ps5RawSyscall6(SB)
|
||||
+
|
||||
+// func sysDirect(trap int64, a1, a2, a3 int64) (r1, r2, err int64)
|
||||
+// func sysDirect6(trap int64, a1, a2, a3, a4, a5, a6 int64) (r1, r2, err int64)
|
||||
// Trap # in AX, args in DI SI DX, return in AX DX
|
||||
|
||||
-TEXT ·Syscall(SB),NOSPLIT,$0-56
|
||||
+TEXT ·sysDirect(SB),NOSPLIT,$0-56
|
||||
CALL runtime·entersyscall<ABIInternal>(SB)
|
||||
MOVQ trap+0(FP), AX // syscall entry
|
||||
MOVQ a1+8(FP), DI
|
||||
@@ -35,7 +98,7 @@ ok:
|
||||
CALL runtime·exitsyscall<ABIInternal>(SB)
|
||||
RET
|
||||
|
||||
-TEXT ·Syscall6(SB),NOSPLIT,$0-80
|
||||
+TEXT ·sysDirect6(SB),NOSPLIT,$0-80
|
||||
CALL runtime·entersyscall<ABIInternal>(SB)
|
||||
MOVQ trap+0(FP), AX // syscall entry
|
||||
MOVQ a1+8(FP), DI
|
||||
@@ -58,7 +121,7 @@ ok6:
|
||||
CALL runtime·exitsyscall<ABIInternal>(SB)
|
||||
RET
|
||||
|
||||
-TEXT ·RawSyscall(SB),NOSPLIT,$0-56
|
||||
+TEXT ·rawSysDirect(SB),NOSPLIT,$0-56
|
||||
MOVQ a1+8(FP), DI
|
||||
MOVQ a2+16(FP), SI
|
||||
MOVQ a3+24(FP), DX
|
||||
@@ -75,7 +138,7 @@ ok1:
|
||||
MOVQ $0, err+48(FP) // errno
|
||||
RET
|
||||
|
||||
-TEXT ·RawSyscall6(SB),NOSPLIT,$0-80
|
||||
+TEXT ·rawSysDirect6(SB),NOSPLIT,$0-80
|
||||
MOVQ a1+8(FP), DI
|
||||
MOVQ a2+16(FP), SI
|
||||
MOVQ a3+24(FP), DX
|
||||
diff --git a/src/syscall/asm9_unix2_amd64.s b/src/syscall/asm9_unix2_amd64.s
|
||||
index bb4e9db..5316f1f 100644
|
||||
--- a/src/syscall/asm9_unix2_amd64.s
|
||||
+++ b/src/syscall/asm9_unix2_amd64.s
|
||||
@@ -11,8 +11,23 @@
|
||||
// Syscall9 support for AMD64, DragonFly, FreeBSD and NetBSD
|
||||
//
|
||||
|
||||
+// PS5: see asm_unix_amd64.s. Sony's syscall numbers are diverted to the
|
||||
+// emulation, the rest go to the unchanged implementation.
|
||||
// func Syscall9(trap int64, a1, a2, a3, a4, a5, a6, a7, a8, a9 int64) (r1, r2, err int64);
|
||||
-TEXT ·Syscall9(SB),NOSPLIT,$32-104
|
||||
+TEXT ·Syscall9(SB),NOSPLIT,$0-104
|
||||
+ MOVQ num+0(FP), AX
|
||||
+ CMPQ AX, $532
|
||||
+ JAE emulate9
|
||||
+ CMPQ AX, $97
|
||||
+ JEQ emulate9
|
||||
+ CMPQ AX, $135
|
||||
+ JEQ emulate9
|
||||
+ JMP ·sysDirect9(SB)
|
||||
+emulate9:
|
||||
+ JMP ·ps5Syscall9(SB)
|
||||
+
|
||||
+// func sysDirect9(trap int64, a1, a2, a3, a4, a5, a6, a7, a8, a9 int64) (r1, r2, err int64);
|
||||
+TEXT ·sysDirect9(SB),NOSPLIT,$32-104
|
||||
NO_LOCAL_POINTERS
|
||||
CALL runtime·entersyscall<ABIInternal>(SB)
|
||||
MOVQ num+0(FP), AX // syscall entry
|
||||
diff --git a/src/syscall/ps5_freebsd_amd64.go b/src/syscall/ps5_freebsd_amd64.go
|
||||
new file mode 100644
|
||||
index 0000000..c369a88
|
||||
--- /dev/null
|
||||
+++ b/src/syscall/ps5_freebsd_amd64.go
|
||||
@@ -0,0 +1,575 @@
|
||||
+// This file is part of the PS5 patch for Go from the ps5-tailscale project.
|
||||
+// Like the rest of the patch it is distributed under Go's BSD-style license,
|
||||
+// which can be found in the LICENSE file.
|
||||
+
|
||||
+// PS5 support.
|
||||
+//
|
||||
+// The PS5 kernel implements the FreeBSD 9 system call table. Every number from
|
||||
+// 532 up is a Sony system call with an unrelated meaning, so the calls FreeBSD
|
||||
+// added in releases 10 to 13 (pipe2, accept4, the 64-bit inode stat family, the
|
||||
+// extended kevent, getrandom, ...) do not exist. The assembly entry points
|
||||
+// Syscall, Syscall6, RawSyscall, RawSyscall6 and Syscall9 divert those numbers
|
||||
+// here, where they are rebuilt from the older calls. Because golang.org/x/sys
|
||||
+// and internal/syscall/unix funnel through the same entry points, this covers
|
||||
+// them too.
|
||||
+//
|
||||
+// socket and socketpair are diverted as well: the SOCK_NONBLOCK and
|
||||
+// SOCK_CLOEXEC type flags postdate FreeBSD 9, so they are applied with fcntl.
|
||||
+
|
||||
+package syscall
|
||||
+
|
||||
+import (
|
||||
+ "runtime"
|
||||
+ "sync"
|
||||
+ "unsafe"
|
||||
+)
|
||||
+
|
||||
+func sysDirect(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno)
|
||||
+func sysDirect6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err Errno)
|
||||
+func sysDirect9(trap, a1, a2, a3, a4, a5, a6, a7, a8, a9 uintptr) (r1, r2 uintptr, err Errno)
|
||||
+func rawSysDirect(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno)
|
||||
+func rawSysDirect6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err Errno)
|
||||
+
|
||||
+func ps5Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno) {
|
||||
+ return ps5Emulate(sysDirect6, trap, a1, a2, a3, 0, 0, 0)
|
||||
+}
|
||||
+
|
||||
+func ps5Syscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err Errno) {
|
||||
+ return ps5Emulate(sysDirect6, trap, a1, a2, a3, a4, a5, a6)
|
||||
+}
|
||||
+
|
||||
+func ps5Syscall9(trap, a1, a2, a3, a4, a5, a6, a7, a8, a9 uintptr) (r1, r2 uintptr, err Errno) {
|
||||
+ return ps5Emulate(sysDirect6, trap, a1, a2, a3, a4, a5, a6)
|
||||
+}
|
||||
+
|
||||
+func ps5RawSyscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno) {
|
||||
+ return ps5Emulate(rawSysDirect6, trap, a1, a2, a3, 0, 0, 0)
|
||||
+}
|
||||
+
|
||||
+func ps5RawSyscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err Errno) {
|
||||
+ return ps5Emulate(rawSysDirect6, trap, a1, a2, a3, a4, a5, a6)
|
||||
+}
|
||||
+
|
||||
+// Syscall numbers of the FreeBSD 9 calls the emulation is built from.
|
||||
+const (
|
||||
+ ps5SysAccept = 30
|
||||
+ ps5SysPipe = 42
|
||||
+ ps5SysFsync = 95
|
||||
+ ps5SysSocket = 97
|
||||
+ ps5SysSocketpair = 135
|
||||
+ ps5SysUtimes = 138
|
||||
+ ps5SysStat = 188
|
||||
+ ps5SysFstat = 189
|
||||
+ ps5SysLstat = 190
|
||||
+ ps5SysGetdirentries = 196
|
||||
+ ps5SysSysctl = 202
|
||||
+ ps5SysFutimes = 206
|
||||
+ ps5SysPoll = 209
|
||||
+ ps5SysClockGettime = 232
|
||||
+ ps5SysLutimes = 276
|
||||
+ ps5SysKevent = 363
|
||||
+ ps5SysStatfs = 396
|
||||
+ ps5SysFstatfs = 397
|
||||
+ ps5SysLseek = 478
|
||||
+ ps5SysFstatat = 493
|
||||
+ ps5SysFutimesat = 494
|
||||
+ ps5SysMknodat = 498
|
||||
+)
|
||||
+
|
||||
+// Syscall numbers that do not exist on the PS5 and are emulated.
|
||||
+const (
|
||||
+ ps5NewAccept4 = 541
|
||||
+ ps5NewPipe2 = 542
|
||||
+ ps5NewPpoll = 545
|
||||
+ ps5NewFutimens = 546
|
||||
+ ps5NewUtimensat = 547
|
||||
+ ps5NewFdatasync = 550
|
||||
+ ps5NewFstat = 551
|
||||
+ ps5NewFstatat = 552
|
||||
+ ps5NewGetdirentries = 554
|
||||
+ ps5NewStatfs = 555
|
||||
+ ps5NewFstatfs = 556
|
||||
+ ps5NewMknodat = 559
|
||||
+ ps5NewKevent = 560
|
||||
+ ps5NewGetrandom = 563
|
||||
+)
|
||||
+
|
||||
+const (
|
||||
+ ps5SockCloexec = 0x10000000
|
||||
+ ps5SockNonblock = 0x20000000
|
||||
+ ps5AtFdcwd = -100
|
||||
+ ps5AtNofollow = 0x200
|
||||
+ ps5UtimeNow = -1
|
||||
+ ps5UtimeOmit = -2
|
||||
+)
|
||||
+
|
||||
+type ps5Call func(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err Errno)
|
||||
+
|
||||
+func ps5Emulate(call ps5Call, trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err Errno) {
|
||||
+ switch trap {
|
||||
+ case ps5SysSocket:
|
||||
+ flags := a2 & (ps5SockCloexec | ps5SockNonblock)
|
||||
+ r1, r2, err = call(ps5SysSocket, a1, a2&^flags, a3, 0, 0, 0)
|
||||
+ if err == 0 {
|
||||
+ ps5SetFdFlags(r1, flags&ps5SockCloexec != 0, flags&ps5SockNonblock != 0)
|
||||
+ }
|
||||
+ return
|
||||
+
|
||||
+ case ps5SysSocketpair:
|
||||
+ flags := a2 & (ps5SockCloexec | ps5SockNonblock)
|
||||
+ r1, r2, err = call(ps5SysSocketpair, a1, a2&^flags, a3, a4, 0, 0)
|
||||
+ if err == 0 && flags != 0 {
|
||||
+ fds := (*[2]int32)(unsafe.Pointer(a4))
|
||||
+ ps5SetFdFlags(uintptr(fds[0]), flags&ps5SockCloexec != 0, flags&ps5SockNonblock != 0)
|
||||
+ ps5SetFdFlags(uintptr(fds[1]), flags&ps5SockCloexec != 0, flags&ps5SockNonblock != 0)
|
||||
+ }
|
||||
+ return
|
||||
+
|
||||
+ case ps5NewAccept4:
|
||||
+ r1, r2, err = call(ps5SysAccept, a1, a2, a3, 0, 0, 0)
|
||||
+ if err == 0 {
|
||||
+ ps5SetFdFlags(r1, a4&ps5SockCloexec != 0, a4&ps5SockNonblock != 0)
|
||||
+ }
|
||||
+ return
|
||||
+
|
||||
+ case ps5NewPipe2:
|
||||
+ // pipe returns the two descriptors in the result registers.
|
||||
+ r1, r2, err = call(ps5SysPipe, 0, 0, 0, 0, 0, 0)
|
||||
+ if err != 0 {
|
||||
+ return
|
||||
+ }
|
||||
+ fds := (*[2]int32)(unsafe.Pointer(a1))
|
||||
+ fds[0], fds[1] = int32(r1), int32(r2)
|
||||
+ ps5SetFdFlags(r1, a2&O_CLOEXEC != 0, a2&O_NONBLOCK != 0)
|
||||
+ ps5SetFdFlags(r2, a2&O_CLOEXEC != 0, a2&O_NONBLOCK != 0)
|
||||
+ return 0, 0, 0
|
||||
+
|
||||
+ case ps5NewPpoll:
|
||||
+ timeout := -1
|
||||
+ if a3 != 0 {
|
||||
+ ts := (*Timespec)(unsafe.Pointer(a3))
|
||||
+ timeout = int(ts.Sec*1000 + (ts.Nsec+999999)/1000000)
|
||||
+ }
|
||||
+ return call(ps5SysPoll, a1, a2, uintptr(timeout), 0, 0, 0)
|
||||
+
|
||||
+ case ps5NewFutimens:
|
||||
+ tv, omit, e := ps5Timevals(a2, func(st *ps5Stat) Errno {
|
||||
+ _, _, e := rawSysDirect(ps5SysFstat, a1, uintptr(unsafe.Pointer(st)), 0)
|
||||
+ return e
|
||||
+ })
|
||||
+ if e != 0 || omit {
|
||||
+ return ps5Result(e)
|
||||
+ }
|
||||
+ r1, r2, err = call(ps5SysFutimes, a1, uintptr(unsafe.Pointer(tv)), 0, 0, 0, 0)
|
||||
+ runtime.KeepAlive(tv)
|
||||
+ return
|
||||
+
|
||||
+ case ps5NewUtimensat:
|
||||
+ tv, omit, e := ps5Timevals(a3, func(st *ps5Stat) Errno {
|
||||
+ _, _, e := rawSysDirect6(ps5SysFstatat, a1, a2, uintptr(unsafe.Pointer(st)), a4&ps5AtNofollow, 0, 0)
|
||||
+ return e
|
||||
+ })
|
||||
+ if e != 0 || omit {
|
||||
+ return ps5Result(e)
|
||||
+ }
|
||||
+ switch {
|
||||
+ case a4&ps5AtNofollow != 0:
|
||||
+ r1, r2, err = call(ps5SysLutimes, a2, uintptr(unsafe.Pointer(tv)), 0, 0, 0, 0)
|
||||
+ case int32(a1) == ps5AtFdcwd:
|
||||
+ r1, r2, err = call(ps5SysUtimes, a2, uintptr(unsafe.Pointer(tv)), 0, 0, 0, 0)
|
||||
+ default:
|
||||
+ r1, r2, err = call(ps5SysFutimesat, a1, a2, uintptr(unsafe.Pointer(tv)), 0, 0, 0)
|
||||
+ }
|
||||
+ runtime.KeepAlive(tv)
|
||||
+ return
|
||||
+
|
||||
+ case ps5NewFdatasync:
|
||||
+ return call(ps5SysFsync, a1, 0, 0, 0, 0, 0)
|
||||
+
|
||||
+ case ps5NewFstat:
|
||||
+ var st ps5Stat
|
||||
+ r1, r2, err = call(ps5SysFstat, a1, uintptr(unsafe.Pointer(&st)), 0, 0, 0, 0)
|
||||
+ if err == 0 {
|
||||
+ st.convert((*Stat_t)(unsafe.Pointer(a2)))
|
||||
+ }
|
||||
+ return
|
||||
+
|
||||
+ case ps5NewFstatat:
|
||||
+ var st ps5Stat
|
||||
+ r1, r2, err = call(ps5SysFstatat, a1, a2, uintptr(unsafe.Pointer(&st)), a4, 0, 0)
|
||||
+ if err == 0 {
|
||||
+ st.convert((*Stat_t)(unsafe.Pointer(a3)))
|
||||
+ }
|
||||
+ return
|
||||
+
|
||||
+ case ps5NewGetdirentries:
|
||||
+ n, e := ps5Getdirentries(call, a1, unsafe.Slice((*byte)(unsafe.Pointer(a2)), int(a3)), (*int64)(unsafe.Pointer(a4)))
|
||||
+ if e != 0 {
|
||||
+ return ps5Result(e)
|
||||
+ }
|
||||
+ return uintptr(n), 0, 0
|
||||
+
|
||||
+ case ps5NewStatfs, ps5NewFstatfs:
|
||||
+ old := uintptr(ps5SysStatfs)
|
||||
+ if trap == ps5NewFstatfs {
|
||||
+ old = ps5SysFstatfs
|
||||
+ }
|
||||
+ var st ps5Statfs
|
||||
+ r1, r2, err = call(old, a1, uintptr(unsafe.Pointer(&st)), 0, 0, 0, 0)
|
||||
+ if err == 0 {
|
||||
+ st.convert((*Statfs_t)(unsafe.Pointer(a2)))
|
||||
+ }
|
||||
+ return
|
||||
+
|
||||
+ case ps5NewMknodat:
|
||||
+ return call(ps5SysMknodat, a1, a2, a3, a4, 0, 0)
|
||||
+
|
||||
+ case ps5NewKevent:
|
||||
+ return ps5Kevent(call, a1, a2, a3, a4, a5, a6)
|
||||
+
|
||||
+ case ps5NewGetrandom:
|
||||
+ n, e := ps5Getrandom(unsafe.Slice((*byte)(unsafe.Pointer(a1)), int(a2)))
|
||||
+ if e != 0 {
|
||||
+ return ps5Result(e)
|
||||
+ }
|
||||
+ return uintptr(n), 0, 0
|
||||
+ }
|
||||
+
|
||||
+ return ps5Result(ENOSYS)
|
||||
+}
|
||||
+
|
||||
+func ps5Result(e Errno) (r1, r2 uintptr, err Errno) {
|
||||
+ if e != 0 {
|
||||
+ return ^uintptr(0), 0, e
|
||||
+ }
|
||||
+ return 0, 0, 0
|
||||
+}
|
||||
+
|
||||
+func ps5SetFdFlags(fd uintptr, cloexec, nonblock bool) {
|
||||
+ if cloexec {
|
||||
+ rawSysDirect(SYS_FCNTL, fd, F_SETFD, FD_CLOEXEC)
|
||||
+ }
|
||||
+ if nonblock {
|
||||
+ if fl, _, e := rawSysDirect(SYS_FCNTL, fd, F_GETFL, 0); e == 0 {
|
||||
+ rawSysDirect(SYS_FCNTL, fd, F_SETFL, fl|O_NONBLOCK)
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+// ps5Stat is struct stat as the PS5 kernel fills it in (FreeBSD 9 to 11).
|
||||
+type ps5Stat struct {
|
||||
+ Dev uint32
|
||||
+ Ino uint32
|
||||
+ Mode uint16
|
||||
+ Nlink uint16
|
||||
+ Uid uint32
|
||||
+ Gid uint32
|
||||
+ Rdev uint32
|
||||
+ Atim Timespec
|
||||
+ Mtim Timespec
|
||||
+ Ctim Timespec
|
||||
+ Size int64
|
||||
+ Blocks int64
|
||||
+ Blksize uint32
|
||||
+ Flags uint32
|
||||
+ Gen uint32
|
||||
+ Lspare int32
|
||||
+ Birthtim Timespec
|
||||
+}
|
||||
+
|
||||
+func (old *ps5Stat) convert(st *Stat_t) {
|
||||
+ *st = Stat_t{
|
||||
+ Dev: uint64(old.Dev),
|
||||
+ Ino: uint64(old.Ino),
|
||||
+ Nlink: uint64(old.Nlink),
|
||||
+ Mode: old.Mode,
|
||||
+ Uid: old.Uid,
|
||||
+ Gid: old.Gid,
|
||||
+ Rdev: uint64(old.Rdev),
|
||||
+ Atimespec: old.Atim,
|
||||
+ Mtimespec: old.Mtim,
|
||||
+ Ctimespec: old.Ctim,
|
||||
+ Birthtimespec: old.Birthtim,
|
||||
+ Size: old.Size,
|
||||
+ Blocks: old.Blocks,
|
||||
+ Blksize: int32(old.Blksize),
|
||||
+ Flags: old.Flags,
|
||||
+ Gen: uint64(old.Gen),
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+// ps5Statfs is struct statfs as the PS5 kernel fills it in. It differs from
|
||||
+// the current layout only in the length of the two mount names.
|
||||
+type ps5Statfs struct {
|
||||
+ Version uint32
|
||||
+ Type uint32
|
||||
+ Flags uint64
|
||||
+ Bsize uint64
|
||||
+ Iosize uint64
|
||||
+ Blocks uint64
|
||||
+ Bfree uint64
|
||||
+ Bavail int64
|
||||
+ Files uint64
|
||||
+ Ffree int64
|
||||
+ Syncwrites uint64
|
||||
+ Asyncwrites uint64
|
||||
+ Syncreads uint64
|
||||
+ Asyncreads uint64
|
||||
+ Spare [10]uint64
|
||||
+ Namemax uint32
|
||||
+ Owner uint32
|
||||
+ Fsid Fsid
|
||||
+ Charspare [80]int8
|
||||
+ Fstypename [16]int8
|
||||
+ Mntfromname [88]int8
|
||||
+ Mntonname [88]int8
|
||||
+}
|
||||
+
|
||||
+func (old *ps5Statfs) convert(st *Statfs_t) {
|
||||
+ *st = Statfs_t{
|
||||
+ Version: old.Version,
|
||||
+ Type: old.Type,
|
||||
+ Flags: old.Flags,
|
||||
+ Bsize: old.Bsize,
|
||||
+ Iosize: old.Iosize,
|
||||
+ Blocks: old.Blocks,
|
||||
+ Bfree: old.Bfree,
|
||||
+ Bavail: old.Bavail,
|
||||
+ Files: old.Files,
|
||||
+ Ffree: old.Ffree,
|
||||
+ Syncwrites: old.Syncwrites,
|
||||
+ Asyncwrites: old.Asyncwrites,
|
||||
+ Syncreads: old.Syncreads,
|
||||
+ Asyncreads: old.Asyncreads,
|
||||
+ Spare: old.Spare,
|
||||
+ Namemax: old.Namemax,
|
||||
+ Owner: old.Owner,
|
||||
+ Fsid: old.Fsid,
|
||||
+ Charspare: old.Charspare,
|
||||
+ Fstypename: old.Fstypename,
|
||||
+ }
|
||||
+ copy(st.Mntfromname[:], old.Mntfromname[:])
|
||||
+ copy(st.Mntonname[:], old.Mntonname[:])
|
||||
+}
|
||||
+
|
||||
+// ps5Timevals converts the [2]Timespec argument of futimens/utimensat into the
|
||||
+// [2]Timeval that futimes/utimes take. UTIME_NOW and UTIME_OMIT are resolved
|
||||
+// here; stat is called only when a current timestamp has to be kept. omit
|
||||
+// reports that both times are UTIME_OMIT, in which case there is nothing to do.
|
||||
+func ps5Timevals(times uintptr, stat func(*ps5Stat) Errno) (tv *[2]Timeval, omit bool, err Errno) {
|
||||
+ if times == 0 {
|
||||
+ return nil, false, 0
|
||||
+ }
|
||||
+ ts := (*[2]Timespec)(unsafe.Pointer(times))
|
||||
+ if ts[0].Nsec == ps5UtimeOmit && ts[1].Nsec == ps5UtimeOmit {
|
||||
+ return nil, true, 0
|
||||
+ }
|
||||
+ var now Timespec
|
||||
+ var st ps5Stat
|
||||
+ haveNow, haveStat := false, false
|
||||
+ tv = new([2]Timeval)
|
||||
+ for i := range ts {
|
||||
+ t := ts[i]
|
||||
+ switch t.Nsec {
|
||||
+ case ps5UtimeNow:
|
||||
+ if !haveNow {
|
||||
+ rawSysDirect(ps5SysClockGettime, 0 /* CLOCK_REALTIME */, uintptr(unsafe.Pointer(&now)), 0)
|
||||
+ haveNow = true
|
||||
+ }
|
||||
+ t = now
|
||||
+ case ps5UtimeOmit:
|
||||
+ if !haveStat {
|
||||
+ if e := stat(&st); e != 0 {
|
||||
+ return nil, false, e
|
||||
+ }
|
||||
+ haveStat = true
|
||||
+ }
|
||||
+ if i == 0 {
|
||||
+ t = st.Atim
|
||||
+ } else {
|
||||
+ t = st.Mtim
|
||||
+ }
|
||||
+ }
|
||||
+ tv[i] = Timeval{Sec: t.Sec, Usec: t.Nsec / 1000}
|
||||
+ }
|
||||
+ return tv, false, 0
|
||||
+}
|
||||
+
|
||||
+// Directory reading.
|
||||
+//
|
||||
+// The kernel returns the old directory entry (32-bit inode, 8 byte header)
|
||||
+// and several PS5 filesystems, /data among them, reject any read that is not
|
||||
+// exactly one 64 KiB block. A block can expand to more converted entries than
|
||||
+// fit in the caller's buffer, so the remainder is kept per descriptor and
|
||||
+// handed out by the following calls. The saved file offset detects a seek or a
|
||||
+// reused descriptor, which invalidates the remainder.
|
||||
+
|
||||
+const ps5DirBlock = 65536
|
||||
+
|
||||
+type ps5DirState struct {
|
||||
+ off int64 // file offset after the block that produced data
|
||||
+ data []byte // converted entries not yet returned
|
||||
+}
|
||||
+
|
||||
+var (
|
||||
+ ps5DirMu sync.Mutex
|
||||
+ ps5Dirs map[uintptr]*ps5DirState
|
||||
+)
|
||||
+
|
||||
+func ps5Getdirentries(call ps5Call, fd uintptr, buf []byte, basep *int64) (int, Errno) {
|
||||
+ cur, _, e := rawSysDirect(ps5SysLseek, fd, 0, 1 /* SEEK_CUR */)
|
||||
+ if e != 0 {
|
||||
+ return 0, e
|
||||
+ }
|
||||
+
|
||||
+ ps5DirMu.Lock()
|
||||
+ st := ps5Dirs[fd]
|
||||
+ if st != nil && (st.off != int64(cur) || len(st.data) == 0) {
|
||||
+ delete(ps5Dirs, fd)
|
||||
+ st = nil
|
||||
+ }
|
||||
+ ps5DirMu.Unlock()
|
||||
+
|
||||
+ if st == nil {
|
||||
+ block := make([]byte, ps5DirBlock)
|
||||
+ for {
|
||||
+ var base int64
|
||||
+ n, _, e := call(ps5SysGetdirentries, fd, uintptr(unsafe.Pointer(&block[0])), ps5DirBlock, uintptr(unsafe.Pointer(&base)), 0, 0)
|
||||
+ if e != 0 {
|
||||
+ return 0, e
|
||||
+ }
|
||||
+ if n == 0 {
|
||||
+ return 0, 0
|
||||
+ }
|
||||
+ if data := ps5ConvertDirents(block[:n]); len(data) > 0 {
|
||||
+ off, _, _ := rawSysDirect(ps5SysLseek, fd, 0, 1)
|
||||
+ st = &ps5DirState{off: int64(off), data: data}
|
||||
+ break
|
||||
+ }
|
||||
+ // A block holding only unused entries: keep reading.
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ n := 0
|
||||
+ for n < len(st.data) {
|
||||
+ reclen := int(st.data[n+16]) | int(st.data[n+17])<<8
|
||||
+ if n+reclen > len(buf) {
|
||||
+ break
|
||||
+ }
|
||||
+ n += reclen
|
||||
+ }
|
||||
+ if n == 0 {
|
||||
+ // Not even one entry fits.
|
||||
+ ps5DirMu.Lock()
|
||||
+ if ps5Dirs == nil {
|
||||
+ ps5Dirs = make(map[uintptr]*ps5DirState)
|
||||
+ }
|
||||
+ ps5Dirs[fd] = st
|
||||
+ ps5DirMu.Unlock()
|
||||
+ return 0, EINVAL
|
||||
+ }
|
||||
+ copy(buf, st.data[:n])
|
||||
+ st.data = st.data[n:]
|
||||
+
|
||||
+ ps5DirMu.Lock()
|
||||
+ if len(st.data) > 0 {
|
||||
+ if ps5Dirs == nil {
|
||||
+ ps5Dirs = make(map[uintptr]*ps5DirState)
|
||||
+ }
|
||||
+ ps5Dirs[fd] = st
|
||||
+ } else {
|
||||
+ delete(ps5Dirs, fd)
|
||||
+ }
|
||||
+ ps5DirMu.Unlock()
|
||||
+
|
||||
+ if basep != nil {
|
||||
+ *basep = st.off
|
||||
+ }
|
||||
+ return n, 0
|
||||
+}
|
||||
+
|
||||
+// ps5ConvertDirents rewrites a block of old directory entries
|
||||
+// (fileno uint32, reclen uint16, type uint8, namlen uint8, name) as Dirent
|
||||
+// records. Unused entries (fileno 0) are dropped.
|
||||
+func ps5ConvertDirents(old []byte) []byte {
|
||||
+ const (
|
||||
+ oldHeader = 8
|
||||
+ newHeader = int(unsafe.Offsetof(Dirent{}.Name))
|
||||
+ )
|
||||
+ out := make([]byte, 0, len(old)*2)
|
||||
+ for pos := 0; pos+oldHeader <= len(old); {
|
||||
+ fileno := uint32(old[pos]) | uint32(old[pos+1])<<8 | uint32(old[pos+2])<<16 | uint32(old[pos+3])<<24
|
||||
+ reclen := int(old[pos+4]) | int(old[pos+5])<<8
|
||||
+ typ := old[pos+6]
|
||||
+ namlen := int(old[pos+7])
|
||||
+ if reclen < oldHeader || pos+reclen > len(old) {
|
||||
+ break
|
||||
+ }
|
||||
+ if fileno != 0 && pos+oldHeader+namlen <= len(old) {
|
||||
+ newlen := (newHeader + namlen + 1 + 7) &^ 7
|
||||
+ start := len(out)
|
||||
+ out = append(out, make([]byte, newlen)...)
|
||||
+ rec := out[start:]
|
||||
+ rec[0], rec[1], rec[2], rec[3] = byte(fileno), byte(fileno>>8), byte(fileno>>16), byte(fileno>>24)
|
||||
+ rec[16], rec[17] = byte(newlen), byte(newlen>>8)
|
||||
+ rec[18] = typ
|
||||
+ rec[20], rec[21] = byte(namlen), byte(namlen>>8)
|
||||
+ copy(rec[newHeader:], old[pos+oldHeader:pos+oldHeader+namlen])
|
||||
+ }
|
||||
+ pos += reclen
|
||||
+ }
|
||||
+ return out
|
||||
+}
|
||||
+
|
||||
+// ps5Kevent implements the FreeBSD 12 kevent, whose events carry four extra
|
||||
+// words, with the older call.
|
||||
+func ps5Kevent(call ps5Call, kq, changes, nchanges, events, nevents, timeout uintptr) (r1, r2 uintptr, err Errno) {
|
||||
+ type newKevent struct {
|
||||
+ Kevent_t
|
||||
+ Ext [4]uint64
|
||||
+ }
|
||||
+ var oldChanges, oldEvents []Kevent_t
|
||||
+ var pc, pe unsafe.Pointer
|
||||
+ if nchanges > 0 {
|
||||
+ in := unsafe.Slice((*newKevent)(unsafe.Pointer(changes)), int(nchanges))
|
||||
+ oldChanges = make([]Kevent_t, len(in))
|
||||
+ for i := range in {
|
||||
+ oldChanges[i] = in[i].Kevent_t
|
||||
+ }
|
||||
+ pc = unsafe.Pointer(&oldChanges[0])
|
||||
+ }
|
||||
+ if nevents > 0 {
|
||||
+ oldEvents = make([]Kevent_t, int(nevents))
|
||||
+ pe = unsafe.Pointer(&oldEvents[0])
|
||||
+ }
|
||||
+ r1, r2, err = call(ps5SysKevent, kq, uintptr(pc), nchanges, uintptr(pe), nevents, timeout)
|
||||
+ runtime.KeepAlive(oldChanges)
|
||||
+ if err == 0 && nevents > 0 {
|
||||
+ out := unsafe.Slice((*newKevent)(unsafe.Pointer(events)), int(nevents))
|
||||
+ for i := 0; i < int(r1) && i < len(out); i++ {
|
||||
+ out[i] = newKevent{Kevent_t: oldEvents[i]}
|
||||
+ }
|
||||
+ }
|
||||
+ return
|
||||
+}
|
||||
+
|
||||
+// ps5Getrandom fills buf from the kernel's kern.arandom sysctl, which hands
|
||||
+// out at most 256 bytes per call.
|
||||
+func ps5Getrandom(buf []byte) (int, Errno) {
|
||||
+ mib := [2]_C_int{1 /* CTL_KERN */, 37 /* KERN_ARND */}
|
||||
+ for done := 0; done < len(buf); {
|
||||
+ n := uintptr(len(buf) - done)
|
||||
+ if n > 256 {
|
||||
+ n = 256
|
||||
+ }
|
||||
+ _, _, e := rawSysDirect6(ps5SysSysctl, uintptr(unsafe.Pointer(&mib[0])), 2, uintptr(unsafe.Pointer(&buf[done])), uintptr(unsafe.Pointer(&n)), 0, 0)
|
||||
+ if e != 0 {
|
||||
+ return done, e
|
||||
+ }
|
||||
+ if n == 0 {
|
||||
+ return done, EIO
|
||||
+ }
|
||||
+ done += int(n)
|
||||
+ }
|
||||
+ return len(buf), 0
|
||||
+}
|
||||
diff --git a/src/internal/routebsd/interface_freebsd.go b/src/internal/routebsd/interface_freebsd.go
|
||||
index 7e10554..71f00ec 100644
|
||||
--- a/src/internal/routebsd/interface_freebsd.go
|
||||
+++ b/src/internal/routebsd/interface_freebsd.go
|
||||
@@ -20,6 +20,11 @@ func (w *wireFormat) parseInterfaceMessage(b []byte) (Message, error) {
|
||||
if attrs&syscall.RTA_IFP == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
+ // PS5: Sony's if_data is larger than FreeBSD's, so the link address is
|
||||
+ // not at the usual offset. if_data records its own length; use it.
|
||||
+ if dl := int(nativeEndian.Uint16(b[extOff+6 : extOff+8])); dl != 0 && extOff+dl+8 <= l {
|
||||
+ bodyOff = extOff + dl
|
||||
+ }
|
||||
m := &InterfaceMessage{
|
||||
Version: int(b[2]),
|
||||
Type: int(b[3]),
|
||||
diff --git a/src/os/executable_sysctl.go b/src/os/executable_sysctl.go
|
||||
index 8b52e92..fc91280 100644
|
||||
--- a/src/os/executable_sysctl.go
|
||||
+++ b/src/os/executable_sysctl.go
|
||||
@@ -12,6 +12,24 @@ import (
|
||||
)
|
||||
|
||||
func executable() (string, error) {
|
||||
+ p, err := executableSysctl()
|
||||
+ if err == nil && p != "" {
|
||||
+ return p, nil
|
||||
+ }
|
||||
+ // PS5: a payload is loaded from memory into a host process, so the
|
||||
+ // kernel has no path to report. Programs mostly want a name to derive
|
||||
+ // defaults from; give them one based on argv[0] rather than an error.
|
||||
+ name := "payload.elf"
|
||||
+ if len(Args) > 0 && Args[0] != "" {
|
||||
+ name = Args[0]
|
||||
+ }
|
||||
+ if name[0] != '/' {
|
||||
+ name = "/" + name
|
||||
+ }
|
||||
+ return name, nil
|
||||
+}
|
||||
+
|
||||
+func executableSysctl() (string, error) {
|
||||
n := uintptr(0)
|
||||
// get length
|
||||
_, _, err := syscall.Syscall6(syscall.SYS___SYSCTL, uintptr(unsafe.Pointer(&executableMIB[0])), 4, 0, uintptr(unsafe.Pointer(&n)), 0, 0)
|
||||
@@ -0,0 +1,80 @@
|
||||
/* Find out which buffer sizes getdirentries(196)/getdents(272) accept on the
|
||||
* PS5's filesystems, and what the returned records look like. */
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <sys/mount.h>
|
||||
#include <sys/param.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
static long
|
||||
raw(long n, long a, long b, long c, long d) {
|
||||
long ret;
|
||||
unsigned char cf;
|
||||
register long r10 __asm__("r10") = d;
|
||||
__asm__ volatile("syscall; setc %1" : "+a"(n), "=q"(cf) : "D"(a), "S"(b), "d"(c), "r"(r10) : "rcx", "r11", "r8", "r9", "memory");
|
||||
ret = n;
|
||||
return cf ? -ret : ret;
|
||||
}
|
||||
|
||||
int
|
||||
main(void) {
|
||||
static const char *dirs[] = {"/", "/data", "/dev", "/system", "/user", "/system_data", "/mnt", 0};
|
||||
static const int sizes[] = {256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536};
|
||||
static char buf[65536];
|
||||
|
||||
setvbuf(stdout, 0, _IONBF, 0);
|
||||
for (int d = 0; dirs[d]; d++) {
|
||||
struct statfs sfs;
|
||||
struct stat st;
|
||||
memset(&sfs, 0, sizeof(sfs));
|
||||
if (statfs(dirs[d], &sfs)) {
|
||||
printf("%s: statfs failed\n", dirs[d]);
|
||||
continue;
|
||||
}
|
||||
stat(dirs[d], &st);
|
||||
printf("%s: fstype=%s bsize=%lu iosize=%lu st_blksize=%d sizeof(statfs)=%zu\n", dirs[d], sfs.f_fstypename,
|
||||
(unsigned long)sfs.f_bsize, (unsigned long)sfs.f_iosize, (int)st.st_blksize, sizeof(sfs));
|
||||
for (size_t s = 0; s < sizeof(sizes) / sizeof(sizes[0]); s++) {
|
||||
int fd = open(dirs[d], O_RDONLY | O_DIRECTORY);
|
||||
if (fd < 0) {
|
||||
printf(" open failed\n");
|
||||
break;
|
||||
}
|
||||
long base = 0;
|
||||
long n1 = raw(196, fd, (long)buf, sizes[s], (long)&base);
|
||||
long off1 = lseek(fd, 0, SEEK_CUR);
|
||||
long n2 = raw(196, fd, (long)buf, sizes[s], (long)&base);
|
||||
close(fd);
|
||||
fd = open(dirs[d], O_RDONLY | O_DIRECTORY);
|
||||
long n3 = raw(272, fd, (long)buf, sizes[s], 0);
|
||||
close(fd);
|
||||
printf(" size %-6d getdirentries=%ld (off after=%ld, next=%ld) getdents=%ld\n", sizes[s], n1, off1, n2, n3);
|
||||
}
|
||||
}
|
||||
|
||||
/* Dump the first records of /data to confirm the dirent layout. */
|
||||
int fd = open("/data", O_RDONLY | O_DIRECTORY);
|
||||
long base = 0;
|
||||
long n = raw(196, fd, (long)buf, sizeof(buf), (long)&base);
|
||||
printf("/data records (n=%ld):\n", n);
|
||||
for (long pos = 0; pos + 8 <= n && pos < 400;) {
|
||||
uint32_t fileno;
|
||||
uint16_t reclen;
|
||||
memcpy(&fileno, buf + pos, 4);
|
||||
memcpy(&reclen, buf + pos + 4, 2);
|
||||
printf(" +%ld fileno=%u reclen=%u type=%u namlen=%u name=%.*s\n", pos, fileno, reclen, (uint8_t)buf[pos + 6],
|
||||
(uint8_t)buf[pos + 7], (uint8_t)buf[pos + 7], buf + pos + 8);
|
||||
if (!reclen) {
|
||||
break;
|
||||
}
|
||||
pos += reclen;
|
||||
}
|
||||
close(fd);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/* Exercise the daemon's local forwards from the console: an HTTP GET through
|
||||
* the TCP forward on 127.0.0.1:47989, and a UDP round trip through the
|
||||
* forward on 127.0.0.1:47998. Build with -DHTTP_PATH="/serverinfo" etc. */
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <netinet/in.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/time.h>
|
||||
|
||||
#ifndef HTTP_PATH
|
||||
#define HTTP_PATH "/hello.txt"
|
||||
#endif
|
||||
|
||||
static struct sockaddr_in
|
||||
local(int port) {
|
||||
struct sockaddr_in addr = {0};
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(port);
|
||||
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||
return addr;
|
||||
}
|
||||
|
||||
static double
|
||||
now(void) {
|
||||
struct timeval tv;
|
||||
gettimeofday(&tv, 0);
|
||||
return tv.tv_sec + tv.tv_usec / 1e6;
|
||||
}
|
||||
|
||||
int
|
||||
main(void) {
|
||||
struct timeval tv = {10, 0};
|
||||
struct sockaddr_in addr;
|
||||
char buf[2048];
|
||||
ssize_t n;
|
||||
int fd;
|
||||
|
||||
setvbuf(stdout, 0, _IONBF, 0);
|
||||
|
||||
/* TCP */
|
||||
addr = local(47989);
|
||||
fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||
double t0 = now();
|
||||
if (connect(fd, (struct sockaddr *)&addr, sizeof(addr))) {
|
||||
printf("tcp 127.0.0.1:47989: cannot connect (no forward listening)\n");
|
||||
} else {
|
||||
const char *req = "GET " HTTP_PATH " HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n";
|
||||
size_t total = 0;
|
||||
write(fd, req, strlen(req));
|
||||
while (total < sizeof(buf) - 1 && (n = read(fd, buf + total, sizeof(buf) - 1 - total)) > 0) {
|
||||
total += n;
|
||||
}
|
||||
buf[total] = 0;
|
||||
char *body = strstr(buf, "\r\n\r\n");
|
||||
char *eol = strstr(buf, "\r\n");
|
||||
if (!total) {
|
||||
printf("tcp 127.0.0.1:47989: connected but no response\n");
|
||||
} else {
|
||||
printf("tcp 127.0.0.1:47989: %.*s (%.0f ms)\n", eol ? (int)(eol - buf) : 60, buf, (now() - t0) * 1000);
|
||||
if (body) {
|
||||
printf(" body: %.400s\n", body + 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
close(fd);
|
||||
|
||||
#ifndef SKIP_UDP
|
||||
/* UDP */
|
||||
addr = local(47998);
|
||||
fd = socket(AF_INET, SOCK_DGRAM, 0);
|
||||
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||
int ok = 0;
|
||||
double worst = 0;
|
||||
for (int i = 0; i < 20; i++) {
|
||||
char msg[1300];
|
||||
memset(msg, 'a' + i, sizeof(msg));
|
||||
t0 = now();
|
||||
sendto(fd, msg, sizeof(msg), 0, (struct sockaddr *)&addr, sizeof(addr));
|
||||
n = recv(fd, buf, sizeof(buf), 0);
|
||||
double ms = (now() - t0) * 1000;
|
||||
if (n == sizeof(msg) && !memcmp(buf, msg, sizeof(msg))) {
|
||||
ok++;
|
||||
if (ms > worst) {
|
||||
worst = ms;
|
||||
}
|
||||
}
|
||||
}
|
||||
printf("udp 127.0.0.1:47998: %d/20 datagrams of 1300 bytes echoed, worst round trip %.1f ms\n", ok, worst);
|
||||
close(fd);
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/* List a few directories under /data (two levels) and print small text
|
||||
* files, to learn how the console's payload autoloader is configured.
|
||||
* Read-only. */
|
||||
|
||||
#include <dirent.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
static int
|
||||
is_text_name(const char *name) {
|
||||
const char *ext = strrchr(name, '.');
|
||||
return ext && (!strcmp(ext, ".txt") || !strcmp(ext, ".json") || !strcmp(ext, ".ini") || !strcmp(ext, ".cfg") ||
|
||||
!strcmp(ext, ".conf") || !strcmp(ext, ".lst"));
|
||||
}
|
||||
|
||||
static void
|
||||
print_file(const char *path, long size) {
|
||||
char buf[3000];
|
||||
FILE *f;
|
||||
size_t n;
|
||||
|
||||
if (size > 20000 || !(f = fopen(path, "rb"))) {
|
||||
return;
|
||||
}
|
||||
n = fread(buf, 1, sizeof(buf) - 1, f);
|
||||
fclose(f);
|
||||
buf[n] = 0;
|
||||
printf("----- %s\n%s\n-----\n", path, buf);
|
||||
}
|
||||
|
||||
static void
|
||||
list(const char *dir, int depth) {
|
||||
struct dirent *e;
|
||||
DIR *d = opendir(dir);
|
||||
|
||||
if (!d) {
|
||||
printf("%*s(cannot open %s)\n", depth * 2, "", dir);
|
||||
return;
|
||||
}
|
||||
while ((e = readdir(d))) {
|
||||
char path[1024];
|
||||
struct stat st;
|
||||
|
||||
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) {
|
||||
continue;
|
||||
}
|
||||
snprintf(path, sizeof(path), "%s/%s", dir, e->d_name);
|
||||
if (stat(path, &st)) {
|
||||
continue;
|
||||
}
|
||||
printf("%*s%s%s %ld\n", depth * 2, "", e->d_name, S_ISDIR(st.st_mode) ? "/" : "", (long)st.st_size);
|
||||
if (S_ISDIR(st.st_mode) && depth < 2) {
|
||||
list(path, depth + 1);
|
||||
} else if (!S_ISDIR(st.st_mode) && is_text_name(e->d_name)) {
|
||||
print_file(path, (long)st.st_size);
|
||||
}
|
||||
}
|
||||
closedir(d);
|
||||
}
|
||||
|
||||
int
|
||||
main(void) {
|
||||
static const char *dirs[] = {"/data/pldmgr", "/data/homebrew", "/data/ps5_autoloader", "/data/etaHEN", "/mnt/usb0", 0};
|
||||
|
||||
setvbuf(stdout, 0, _IONBF, 0);
|
||||
for (int i = 0; dirs[i]; i++) {
|
||||
printf("== %s\n", dirs[i]);
|
||||
list(dirs[i], 1);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/* Find out which scheduling class/priority changes the PS5 kernel accepts
|
||||
* for a payload thread, and whether new threads inherit them. */
|
||||
|
||||
#include <pthread.h>
|
||||
#include <sched.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
struct rtprio {
|
||||
unsigned short type;
|
||||
unsigned short prio;
|
||||
};
|
||||
|
||||
static long
|
||||
raw3(long n, long a, long b, long c) {
|
||||
unsigned char failed;
|
||||
__asm__ volatile("syscall; setc %1" : "+a"(n), "=q"(failed) : "D"(a), "S"(b), "d"(c) : "rcx", "r11", "memory");
|
||||
return failed ? -n : n;
|
||||
}
|
||||
|
||||
#define SYS_rtprio_thread 466
|
||||
|
||||
static struct rtprio
|
||||
lookup(void) {
|
||||
struct rtprio r = {0};
|
||||
raw3(SYS_rtprio_thread, 0, 0, (long)&r);
|
||||
return r;
|
||||
}
|
||||
|
||||
static void *
|
||||
child(void *arg) {
|
||||
struct rtprio r = lookup();
|
||||
printf(" new raw-inherited pthread: %u/%u\n", r.type, r.prio);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
main(void) {
|
||||
static const struct rtprio tries[] = {
|
||||
{3, 0}, {3, 20}, {4, 0}, {4, 31}, {2, 767}, {2, 31}, {10, 767}, {10, 760}, {10, 768}, {10, 700},
|
||||
{2, 700}, {3, 767}, {6, 767},
|
||||
};
|
||||
struct sched_param sp;
|
||||
int policy = -1;
|
||||
struct rtprio r = lookup();
|
||||
|
||||
setvbuf(stdout, 0, _IONBF, 0);
|
||||
printf("start: %u/%u\n", r.type, r.prio);
|
||||
if (!pthread_getschedparam(pthread_self(), &policy, &sp)) {
|
||||
printf("pthread policy=%d prio=%d (FIFO=%d RR=%d OTHER=%d)\n", policy, sp.sched_priority, SCHED_FIFO, SCHED_RR,
|
||||
SCHED_OTHER);
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < sizeof(tries) / sizeof(tries[0]); i++) {
|
||||
struct rtprio want = tries[i];
|
||||
long err = raw3(SYS_rtprio_thread, 1, 0, (long)&want);
|
||||
r = lookup();
|
||||
printf("set %2u/%-3u -> ret=%ld now %u/%u\n", tries[i].type, tries[i].prio, err, r.type, r.prio);
|
||||
}
|
||||
|
||||
/* Leave it at the lowest setting that stuck and see what a new thread gets. */
|
||||
struct rtprio low = {10, 767};
|
||||
raw3(SYS_rtprio_thread, 1, 0, (long)&low);
|
||||
r = lookup();
|
||||
printf("final: %u/%u\n", r.type, r.prio);
|
||||
pthread_t t;
|
||||
if (!pthread_create(&t, 0, child, 0)) {
|
||||
pthread_join(t, 0);
|
||||
}
|
||||
|
||||
for (int pol = 0; pol <= 3; pol++) {
|
||||
sp.sched_priority = 767;
|
||||
int e = pthread_setschedparam(pthread_self(), pol, &sp);
|
||||
r = lookup();
|
||||
printf("pthread_setschedparam(policy=%d, 767) -> %d, now %u/%u\n", pol, e, r.type, r.prio);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/* Exercise the daemon's HTTP proxy from the console itself: one plain request
|
||||
* to an Internet host, one to a tailnet address, and one CONNECT. */
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <netinet/in.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/time.h>
|
||||
|
||||
#ifndef TAILNET_PEER
|
||||
#define TAILNET_PEER "100.64.0.1:8000" /* a web server on one of your tailnet devices */
|
||||
#endif
|
||||
|
||||
static void
|
||||
request(const char *label, const char *req) {
|
||||
struct sockaddr_in addr = {0};
|
||||
struct timeval tv = {15, 0};
|
||||
char buf[600];
|
||||
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
ssize_t n;
|
||||
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(8118);
|
||||
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||
if (connect(fd, (struct sockaddr *)&addr, sizeof(addr))) {
|
||||
printf("%s: cannot connect to the proxy\n", label);
|
||||
close(fd);
|
||||
return;
|
||||
}
|
||||
write(fd, req, strlen(req));
|
||||
n = read(fd, buf, sizeof(buf) - 1);
|
||||
if (n <= 0) {
|
||||
printf("%s: no response\n", label);
|
||||
} else {
|
||||
buf[n] = 0;
|
||||
char *eol = strstr(buf, "\r\n");
|
||||
char *body = strstr(buf, "\r\n\r\n");
|
||||
if (eol) {
|
||||
*eol = 0;
|
||||
}
|
||||
printf("%s: %s", label, buf);
|
||||
if (body && body[4]) {
|
||||
body += 4;
|
||||
body[strcspn(body, "\r\n")] = 0;
|
||||
printf(" | body: %.80s", body);
|
||||
}
|
||||
printf("\n");
|
||||
}
|
||||
close(fd);
|
||||
}
|
||||
|
||||
int
|
||||
main(void) {
|
||||
setvbuf(stdout, 0, _IONBF, 0);
|
||||
request("internet GET ", "GET http://controlplane.tailscale.com/key?v=100 HTTP/1.1\r\n"
|
||||
"Host: controlplane.tailscale.com\r\nConnection: close\r\n\r\n");
|
||||
request("tailnet GET ", "GET http://" TAILNET_PEER "/hello.txt HTTP/1.1\r\n"
|
||||
"Host: " TAILNET_PEER "\r\nConnection: close\r\n\r\n");
|
||||
request("CONNECT ", "CONNECT controlplane.tailscale.com:443 HTTP/1.1\r\n"
|
||||
"Host: controlplane.tailscale.com:443\r\n\r\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/* List processes and kill stray test payloads.
|
||||
*
|
||||
* Only processes whose main thread is named "payload.elf" (what the ELF
|
||||
* loader calls anything sent to it as raw bytes) and whose pid is at least
|
||||
* MIN_PID are killed, so payloads the user started before this development
|
||||
* session are left alone. */
|
||||
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <sys/sysctl.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/user.h>
|
||||
|
||||
#ifndef MIN_PID
|
||||
#define MIN_PID 104
|
||||
#endif
|
||||
|
||||
int
|
||||
main(void) {
|
||||
int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PROC, 0};
|
||||
size_t size = 0;
|
||||
pid_t self = getpid();
|
||||
char *buf;
|
||||
|
||||
setvbuf(stdout, 0, _IONBF, 0);
|
||||
if (sysctl(mib, 4, 0, &size, 0, 0) || !(buf = malloc(size)) || sysctl(mib, 4, buf, &size, 0, 0)) {
|
||||
perror("sysctl");
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (char *p = buf; p < buf + size;) {
|
||||
struct kinfo_proc *ki = (struct kinfo_proc *)p;
|
||||
const char *tdname = p + 447;
|
||||
p += ki->ki_structsize;
|
||||
|
||||
#ifdef LIST_ONLY
|
||||
int stray = 0;
|
||||
{
|
||||
#elif defined(KILL_PID)
|
||||
/* Kill exactly one process, and only if it is a payload. */
|
||||
int stray = ki->ki_pid == KILL_PID && ki->ki_pid != self && !strcmp(tdname, "payload.elf");
|
||||
if (stray) {
|
||||
#else
|
||||
int stray = ki->ki_pid >= MIN_PID && ki->ki_pid != self && !strcmp(tdname, "payload.elf");
|
||||
if (ki->ki_pid >= MIN_PID - 40 || stray) {
|
||||
#endif
|
||||
printf("%6d %-20s %-20s rss=%ldMB threads=%d cpu=%.2fs stat=%d wait=%.8s%s\n", ki->ki_pid, ki->ki_comm,
|
||||
tdname, (long)(ki->ki_rssize * 16384L >> 20), ki->ki_numthreads, ki->ki_runtime / 1e6, (int)ki->ki_stat,
|
||||
ki->ki_wmesg, stray ? " <- killing" : "");
|
||||
}
|
||||
if (stray) {
|
||||
if (kill(ki->ki_pid, SIGKILL)) {
|
||||
perror(" kill");
|
||||
}
|
||||
}
|
||||
}
|
||||
free(buf);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
/* Probe which raw FreeBSD syscalls the PS5 kernel accepts, and how the
|
||||
* payload process looks (page size, fds, memory behaviour). Output goes to
|
||||
* stdout, which the ELF loader pipes back over the socket. */
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <signal.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/sysctl.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/utsname.h>
|
||||
|
||||
/* Raw syscall issued from payload memory (not from libkernel), the way the Go
|
||||
* runtime will do it. Returns the kernel result, or -errno when carry is set. */
|
||||
static long
|
||||
raw(long n, long a, long b, long c, long d, long e, long f) {
|
||||
long ret;
|
||||
unsigned char cf;
|
||||
register long r10 __asm__("r10") = d;
|
||||
register long r8 __asm__("r8") = e;
|
||||
register long r9 __asm__("r9") = f;
|
||||
__asm__ volatile("syscall; setc %1"
|
||||
: "+a"(n), "=q"(cf)
|
||||
: "D"(a), "S"(b), "d"(c), "r"(r10), "r"(r8), "r"(r9)
|
||||
: "rcx", "r11", "memory");
|
||||
ret = n;
|
||||
return cf ? -ret : ret;
|
||||
}
|
||||
|
||||
#define R0(n) raw(n, 0, 0, 0, 0, 0, 0)
|
||||
|
||||
static void
|
||||
report(const char *name, long ret) {
|
||||
if (ret < 0) {
|
||||
printf(" %-28s FAIL errno=%ld (%s)\n", name, -ret, strerror((int)-ret));
|
||||
} else {
|
||||
printf(" %-28s ok ret=%ld (0x%lx)\n", name, ret, ret);
|
||||
}
|
||||
}
|
||||
|
||||
static volatile int got_sig;
|
||||
static void
|
||||
on_sig(int sig, siginfo_t *info, void *ctx) {
|
||||
got_sig = sig;
|
||||
}
|
||||
|
||||
int
|
||||
main(int argc, char **argv) {
|
||||
setvbuf(stdout, 0, _IONBF, 0);
|
||||
printf("== sysprobe ==\n");
|
||||
printf("argc=%d argv0=%s pid=%d uid=%d\n", argc, argc ? argv[0] : "-", getpid(), getuid());
|
||||
|
||||
struct utsname un;
|
||||
if (!uname(&un)) {
|
||||
printf("uname: %s %s %s %s\n", un.sysname, un.release, un.version, un.machine);
|
||||
}
|
||||
|
||||
/* sysctls */
|
||||
{
|
||||
int mib[2] = {CTL_HW, HW_PAGESIZE};
|
||||
int v = 0;
|
||||
size_t l = sizeof(v);
|
||||
long r = raw(202, (long)mib, 2, (long)&v, (long)&l, 0, 0);
|
||||
printf("sysctl hw.pagesize -> r=%ld v=%d\n", r, v);
|
||||
mib[1] = HW_NCPU;
|
||||
v = 0;
|
||||
l = sizeof(v);
|
||||
r = raw(202, (long)mib, 2, (long)&v, (long)&l, 0, 0);
|
||||
printf("sysctl hw.ncpu -> r=%ld v=%d\n", r, v);
|
||||
int mib2[2] = {CTL_KERN, 37 /* KERN_ARND */};
|
||||
unsigned char rnd[16] = {0};
|
||||
l = sizeof(rnd);
|
||||
r = raw(202, (long)mib2, 2, (long)rnd, (long)&l, 0, 0);
|
||||
printf("sysctl kern.arandom -> r=%ld len=%zu first=%02x%02x%02x%02x\n", r, l, rnd[0], rnd[1], rnd[2], rnd[3]);
|
||||
int osrel = 0;
|
||||
l = sizeof(osrel);
|
||||
if (!sysctlbyname("kern.osreldate", &osrel, &l, 0, 0)) {
|
||||
printf("kern.osreldate=%d\n", osrel);
|
||||
} else {
|
||||
printf("kern.osreldate: errno=%d\n", errno);
|
||||
}
|
||||
int maxcpus = 0;
|
||||
l = sizeof(maxcpus);
|
||||
if (!sysctlbyname("kern.smp.maxcpus", &maxcpus, &l, 0, 0)) {
|
||||
printf("kern.smp.maxcpus=%d\n", maxcpus);
|
||||
} else {
|
||||
printf("kern.smp.maxcpus: errno=%d\n", errno);
|
||||
}
|
||||
}
|
||||
|
||||
printf("raw syscalls from payload text:\n");
|
||||
report("getpid(20)", R0(20));
|
||||
report("issetugid(253)", R0(253));
|
||||
report("sched_yield(331)", R0(331));
|
||||
{
|
||||
long tid = 0;
|
||||
report("thr_self(432)", raw(432, (long)&tid, 0, 0, 0, 0, 0));
|
||||
printf(" tid=%ld\n", tid);
|
||||
}
|
||||
{
|
||||
struct timespec ts = {0};
|
||||
report("clock_gettime(232,MONO=4)", raw(232, 4, (long)&ts, 0, 0, 0, 0));
|
||||
printf(" mono=%ld.%09ld\n", (long)ts.tv_sec, ts.tv_nsec);
|
||||
report("clock_gettime(232,REAL=0)", raw(232, 0, (long)&ts, 0, 0, 0, 0));
|
||||
printf(" real=%ld.%09ld\n", (long)ts.tv_sec, ts.tv_nsec);
|
||||
}
|
||||
{
|
||||
/* Numbers >= 532 are Sony syscalls on this kernel (pipe2, accept4 and
|
||||
* friends do not exist), so they are deliberately not probed. */
|
||||
long r = raw(42, 0, 0, 0, 0, 0, 0); /* pipe: fds in rax/rdx */
|
||||
report("pipe(42)", r);
|
||||
if (r >= 0) {
|
||||
close((int)r);
|
||||
}
|
||||
}
|
||||
{
|
||||
long kq = R0(362);
|
||||
report("kqueue(362)", kq);
|
||||
struct timespec zero = {0};
|
||||
char evbuf[256];
|
||||
report("kevent(363) poll", raw(363, kq, 0, 0, (long)evbuf, 1, (long)&zero));
|
||||
close((int)kq);
|
||||
}
|
||||
{
|
||||
unsigned char mask[64] = {0};
|
||||
long r = raw(487, 1 /* CPU_LEVEL_WHICH */, 2 /* CPU_WHICH_PID */, -1, 8, (long)mask, 0);
|
||||
report("cpuset_getaffinity(487)", r);
|
||||
printf(" mask=%02x%02x\n", mask[1], mask[0]);
|
||||
}
|
||||
{
|
||||
stack_t ss = {0};
|
||||
report("sigaltstack(53) query", raw(53, 0, (long)&ss, 0, 0, 0, 0));
|
||||
printf(" ss_sp=%p size=%zu flags=%d\n", ss.ss_sp, ss.ss_size, ss.ss_flags);
|
||||
sigset_t set;
|
||||
report("sigprocmask(340) query", raw(340, 1, 0, (long)&set, 0, 0, 0));
|
||||
struct sigaction old;
|
||||
report("sigaction(416) query URG", raw(416, SIGURG, 0, (long)&old, 0, 0, 0));
|
||||
report("sigaction(416) query SEGV", raw(416, SIGSEGV, 0, (long)&old, 0, 0, 0));
|
||||
printf(" SEGV handler=%p flags=%x\n", (void *)old.sa_sigaction, old.sa_flags);
|
||||
}
|
||||
{
|
||||
char cwd[256] = {0};
|
||||
report("__getcwd(326)", raw(326, (long)cwd, sizeof(cwd), 0, 0, 0, 0));
|
||||
printf(" cwd=%s\n", cwd);
|
||||
}
|
||||
{
|
||||
struct stat st;
|
||||
report("stat(188) /data", raw(188, (long)"/data", (long)&st, 0, 0, 0, 0));
|
||||
printf(" sizeof(struct stat)=%zu mode=%o\n", sizeof(st), st.st_mode);
|
||||
report("lstat(190) /data", raw(190, (long)"/data", (long)&st, 0, 0, 0, 0));
|
||||
report("fstatat(493) /data", raw(493, AT_FDCWD, (long)"/data", (long)&st, 0, 0, 0));
|
||||
long fd = raw(499, AT_FDCWD, (long)"/data", O_RDONLY, 0, 0, 0);
|
||||
report("openat(499) /data", fd);
|
||||
if (fd >= 0) {
|
||||
close((int)fd);
|
||||
}
|
||||
fd = raw(5, (long)"/data", O_RDONLY | 0x00020000 /* O_DIRECTORY */, 0, 0, 0, 0);
|
||||
report("open(5) /data O_DIRECTORY", fd);
|
||||
if (fd >= 0) {
|
||||
char dents[1024];
|
||||
long base = 0;
|
||||
report("fstat(189)", raw(189, fd, (long)&st, 0, 0, 0, 0));
|
||||
report("getdirentries(196)", raw(196, fd, (long)dents, sizeof(dents), (long)&base, 0, 0));
|
||||
report("getdents(272)", raw(272, fd, (long)dents, sizeof(dents), 0, 0, 0));
|
||||
report("fstatfs(397)", raw(397, fd, (long)dents, 0, 0, 0, 0));
|
||||
close((int)fd);
|
||||
}
|
||||
char link[256] = {0};
|
||||
report("readlink(58) /dev/stdout", raw(58, (long)"/dev/stdout", (long)link, sizeof(link), 0, 0, 0));
|
||||
report("access(33) /dev/urandom", raw(33, (long)"/dev/urandom", 0, 0, 0, 0, 0));
|
||||
report("access(33) /dev/random", raw(33, (long)"/dev/random", 0, 0, 0, 0, 0));
|
||||
report("access(33) /etc/resolv.conf", raw(33, (long)"/etc/resolv.conf", 0, 0, 0, 0, 0));
|
||||
report("access(33) /data/tailscale", raw(33, (long)"/data/tailscale", 0, 0, 0, 0, 0));
|
||||
}
|
||||
{
|
||||
long s = raw(97, 2, 1, 0, 0, 0, 0);
|
||||
report("socket(97) TCP", s);
|
||||
if (s >= 0) {
|
||||
close((int)s);
|
||||
}
|
||||
s = raw(97, 2, 1 | 0x10000000 | 0x20000000, 0, 0, 0, 0);
|
||||
report("socket TCP|CLOEXEC|NONBLOCK", s);
|
||||
if (s >= 0) {
|
||||
close((int)s);
|
||||
}
|
||||
s = raw(97, 28, 2, 0, 0, 0, 0);
|
||||
report("socket(97) UDP6", s);
|
||||
if (s >= 0) {
|
||||
close((int)s);
|
||||
}
|
||||
s = raw(97, 17 /* AF_ROUTE */, 3, 0, 0, 0, 0);
|
||||
report("socket(97) AF_ROUTE", s);
|
||||
if (s >= 0) {
|
||||
close((int)s);
|
||||
}
|
||||
s = raw(97, 1, 1, 0, 0, 0, 0);
|
||||
report("socket(97) AF_UNIX", s);
|
||||
if (s >= 0) {
|
||||
close((int)s);
|
||||
}
|
||||
int mib[6] = {CTL_NET, 17 /* AF_ROUTE */, 0, 0, 3 /* NET_RT_IFLIST */, 0};
|
||||
size_t l = 0;
|
||||
long r = raw(202, (long)mib, 6, 0, (long)&l, 0, 0);
|
||||
report("sysctl NET_RT_IFLIST size", r);
|
||||
printf(" need=%zu\n", l);
|
||||
mib[4] = 1; /* NET_RT_DUMP */
|
||||
l = 0;
|
||||
r = raw(202, (long)mib, 6, 0, (long)&l, 0, 0);
|
||||
report("sysctl NET_RT_DUMP size", r);
|
||||
printf(" need=%zu\n", l);
|
||||
}
|
||||
|
||||
printf("memory:\n");
|
||||
{
|
||||
long p = raw(477, 0, 64 << 20, PROT_NONE, MAP_ANON | MAP_PRIVATE, -1, 0);
|
||||
report("mmap 64MB PROT_NONE", p);
|
||||
if (p > 0) {
|
||||
long q = raw(477, p, 1 << 20, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE | MAP_FIXED, -1, 0);
|
||||
report(" remap 1MB RW MAP_FIXED", q);
|
||||
if (q > 0) {
|
||||
memset((void *)q, 0x5a, 1 << 20);
|
||||
printf(" touched ok\n");
|
||||
report(" madvise FREE(5)", raw(75, q, 1 << 20, 5, 0, 0, 0));
|
||||
}
|
||||
report(" munmap", raw(73, p, 64 << 20, 0, 0, 0, 0));
|
||||
}
|
||||
p = raw(477, 0x00c000000000, 64 << 20, PROT_NONE, MAP_ANON | MAP_PRIVATE, -1, 0);
|
||||
report("mmap hint 0xc000000000", p);
|
||||
if (p > 0) {
|
||||
raw(73, p, 64 << 20, 0, 0, 0, 0);
|
||||
}
|
||||
p = raw(477, 0, 4L << 30, PROT_NONE, MAP_ANON | MAP_PRIVATE, -1, 0);
|
||||
report("mmap 4GB PROT_NONE", p);
|
||||
if (p > 0) {
|
||||
raw(73, p, 4L << 30, 0, 0, 0, 0);
|
||||
}
|
||||
p = raw(477, 0, 512 << 20, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE, -1, 0);
|
||||
report("mmap 512MB RW (untouched)", p);
|
||||
if (p > 0) {
|
||||
raw(73, p, 512 << 20, 0, 0, 0, 0);
|
||||
}
|
||||
p = raw(477, 0, 1 << 20, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANON | MAP_PRIVATE, -1, 0);
|
||||
report("mmap 1MB RWX", p);
|
||||
if (p > 0) {
|
||||
raw(73, p, 1 << 20, 0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
printf("signals:\n");
|
||||
{
|
||||
struct sigaction sa = {0};
|
||||
sa.sa_sigaction = on_sig;
|
||||
sa.sa_flags = SA_SIGINFO | SA_RESTART;
|
||||
sigfillset(&sa.sa_mask);
|
||||
report("sigaction(416) set USR1", raw(416, SIGUSR1, (long)&sa, 0, 0, 0, 0));
|
||||
long tid = 0;
|
||||
raw(432, (long)&tid, 0, 0, 0, 0, 0);
|
||||
report("thr_kill(433) USR1", raw(433, tid, SIGUSR1, 0, 0, 0, 0));
|
||||
printf(" handler ran: got_sig=%d\n", got_sig);
|
||||
}
|
||||
|
||||
printf("fds:\n");
|
||||
for (int fd = 0; fd < 16; fd++) {
|
||||
struct stat st;
|
||||
if (!fstat(fd, &st)) {
|
||||
printf(" fd %d mode=%o\n", fd, st.st_mode);
|
||||
}
|
||||
}
|
||||
|
||||
printf("== done ==\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
module ps5probe
|
||||
|
||||
go 1.26
|
||||
@@ -0,0 +1,391 @@
|
||||
// Command probe exercises the parts of the Go runtime and standard library
|
||||
// that Tailscale depends on, to find out what works on the PS5.
|
||||
//
|
||||
// Every test is bounded in time by construction: nothing here may spin
|
||||
// forever, because a runaway thread can starve the whole console.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
var failed int
|
||||
|
||||
func step(name string, fn func() (string, error)) {
|
||||
done := make(chan struct{})
|
||||
var res string
|
||||
var err error
|
||||
go func() {
|
||||
defer close(done)
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("panic: %v", r)
|
||||
}
|
||||
}()
|
||||
res, err = fn()
|
||||
}()
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(30 * time.Second):
|
||||
err = fmt.Errorf("timed out")
|
||||
}
|
||||
if err != nil {
|
||||
failed++
|
||||
fmt.Printf("FAIL %-22s %v\n", name, err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("ok %-22s %s\n", name, res)
|
||||
}
|
||||
|
||||
// spin burns CPU without ever calling a function, so only asynchronous
|
||||
// (signal based) preemption can interrupt it. It stops by itself after a
|
||||
// fixed number of iterations, a few seconds at most.
|
||||
//
|
||||
//go:noinline
|
||||
func spin(iterations uint64, stop *atomic.Bool) uint64 {
|
||||
var x uint64 = 88172645463325252
|
||||
for i := uint64(0); i < iterations; i++ {
|
||||
x ^= x << 13
|
||||
x ^= x >> 7
|
||||
x ^= x << 17
|
||||
if i&0xfffff == 0 && stop.Load() {
|
||||
break
|
||||
}
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
func main() {
|
||||
// The process must die on its own if anything goes badly wrong.
|
||||
go func() {
|
||||
time.Sleep(150 * time.Second)
|
||||
fmt.Println("probe: watchdog expired, exiting")
|
||||
os.Exit(3)
|
||||
}()
|
||||
|
||||
fmt.Printf("== go probe: %s %s/%s cpus=%d gomaxprocs=%d pagesize=%d pid=%d GODEBUG=%q ==\n",
|
||||
runtime.Version(), runtime.GOOS, runtime.GOARCH, runtime.NumCPU(), runtime.GOMAXPROCS(0),
|
||||
os.Getpagesize(), os.Getpid(), os.Getenv("GODEBUG"))
|
||||
|
||||
step("goroutines+timers", func() (string, error) {
|
||||
var wg sync.WaitGroup
|
||||
var n atomic.Int64
|
||||
start := time.Now()
|
||||
for i := 0; i < 200; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
n.Add(1)
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
return fmt.Sprintf("%d goroutines in %v", n.Load(), time.Since(start).Round(time.Millisecond)), nil
|
||||
})
|
||||
|
||||
step("os threads", func() (string, error) {
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 24; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
ts := syscall.Timespec{Nsec: 30e6}
|
||||
syscall.Nanosleep(&ts, nil)
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
return "24 locked threads slept in the kernel", nil
|
||||
})
|
||||
|
||||
step("nil deref -> panic", func() (res string, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
res = fmt.Sprintf("recovered: %v", r)
|
||||
}
|
||||
}()
|
||||
var p *int
|
||||
*p = 1
|
||||
return "", fmt.Errorf("no panic")
|
||||
})
|
||||
|
||||
// One spinner per P plus garbage collections that have to stop them.
|
||||
// With working async preemption each GC takes milliseconds; without it
|
||||
// each GC waits for the spinners to finish.
|
||||
step("async preemption+GC", func() (string, error) {
|
||||
var stop atomic.Bool
|
||||
var wg sync.WaitGroup
|
||||
n := runtime.GOMAXPROCS(0)
|
||||
for i := 0; i < n; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
spin(3_000_000_000, &stop) // about 3 seconds
|
||||
}()
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
start := time.Now()
|
||||
var worst time.Duration
|
||||
for i := 0; i < 5; i++ {
|
||||
t := time.Now()
|
||||
runtime.GC()
|
||||
if d := time.Since(t); d > worst {
|
||||
worst = d
|
||||
}
|
||||
}
|
||||
total := time.Since(start)
|
||||
stop.Store(true)
|
||||
wg.Wait()
|
||||
res := fmt.Sprintf("%d spinners, 5 GCs in %v (worst %v)", n, total.Round(time.Millisecond), worst.Round(time.Millisecond))
|
||||
if worst > time.Second {
|
||||
return "", fmt.Errorf("preemption is not working: %s", res)
|
||||
}
|
||||
return res, nil
|
||||
})
|
||||
|
||||
step("heap 256MB", func() (string, error) {
|
||||
bufs := make([][]byte, 0, 64)
|
||||
for i := 0; i < 64; i++ {
|
||||
b := make([]byte, 4<<20)
|
||||
for j := 0; j < len(b); j += 4096 {
|
||||
b[j] = byte(i)
|
||||
}
|
||||
bufs = append(bufs, b)
|
||||
}
|
||||
var ms runtime.MemStats
|
||||
runtime.ReadMemStats(&ms)
|
||||
bufs = nil
|
||||
debug.FreeOSMemory()
|
||||
return fmt.Sprintf("sys=%dMB heap=%dMB", ms.Sys>>20, ms.HeapAlloc>>20), nil
|
||||
})
|
||||
|
||||
step("crypto/rand", func() (string, error) {
|
||||
b := make([]byte, 600)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(b[:8]) + "...", nil
|
||||
})
|
||||
|
||||
step("time", func() (string, error) {
|
||||
return time.Now().Format(time.RFC3339Nano), nil
|
||||
})
|
||||
|
||||
step("hostname/cwd/exe", func() (string, error) {
|
||||
h, herr := os.Hostname()
|
||||
wd, werr := os.Getwd()
|
||||
exe, eerr := os.Executable()
|
||||
return fmt.Sprintf("host=%q(%v) cwd=%q(%v) exe=%q(%v)", h, herr, wd, werr, exe, eerr), nil
|
||||
})
|
||||
|
||||
dir := "/data/tailscale/probe"
|
||||
step("mkdir+write+read", func() (string, error) {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
p := filepath.Join(dir, "a.txt")
|
||||
if err := os.WriteFile(p, []byte("hello ps5\n"), 0o644); err != nil {
|
||||
return "", err
|
||||
}
|
||||
b, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%q", b), nil
|
||||
})
|
||||
|
||||
step("stat+chtimes", func() (string, error) {
|
||||
p := filepath.Join(dir, "a.txt")
|
||||
when := time.Date(2024, 2, 3, 4, 5, 6, 0, time.UTC)
|
||||
if err := os.Chtimes(p, when, when); err != nil {
|
||||
return "", fmt.Errorf("chtimes: %w", err)
|
||||
}
|
||||
fi, err := os.Stat(p)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
li, err := os.Lstat(dir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !fi.ModTime().Equal(when) {
|
||||
return "", fmt.Errorf("mtime %v, want %v", fi.ModTime(), when)
|
||||
}
|
||||
return fmt.Sprintf("size=%d mode=%v mtime ok; dir=%v", fi.Size(), fi.Mode(), li.Mode()), nil
|
||||
})
|
||||
|
||||
step("rename+remove", func() (string, error) {
|
||||
a, b := filepath.Join(dir, "a.txt"), filepath.Join(dir, "b.txt")
|
||||
if err := os.Rename(a, b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
f, err := os.OpenFile(b, os.O_RDWR|os.O_APPEND, 0)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err := f.WriteString("more\n"); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := f.Sync(); err != nil {
|
||||
return "", fmt.Errorf("sync: %w", err)
|
||||
}
|
||||
f.Close()
|
||||
if err := os.Remove(b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "ok", nil
|
||||
})
|
||||
|
||||
step("readdir", func() (string, error) {
|
||||
for i := 0; i < 300; i++ {
|
||||
if err := os.WriteFile(filepath.Join(dir, fmt.Sprintf("file-with-a-longish-name-%03d", i)), nil, 0o644); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
ents, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
data, err := os.ReadDir("/data")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("/data: %w", err)
|
||||
}
|
||||
names := ""
|
||||
for _, e := range data {
|
||||
names += e.Name() + " "
|
||||
}
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
return "", fmt.Errorf("removeall: %w", err)
|
||||
}
|
||||
if len(ents) != 300 {
|
||||
return "", fmt.Errorf("probe dir has %d entries, want 300", len(ents))
|
||||
}
|
||||
return fmt.Sprintf("probe=%d; /data: %s", len(ents), names), nil
|
||||
})
|
||||
|
||||
step("autoloader dir", func() (string, error) {
|
||||
b, err := os.ReadFile("/data/ps5_autoloader/autoload.txt")
|
||||
if err != nil {
|
||||
return fmt.Sprintf("no autoload.txt: %v", err), nil
|
||||
}
|
||||
ents, _ := os.ReadDir("/data/ps5_autoloader")
|
||||
names := ""
|
||||
for _, e := range ents {
|
||||
names += e.Name() + " "
|
||||
}
|
||||
return fmt.Sprintf("files: %s\n%s", names, b), nil
|
||||
})
|
||||
|
||||
step("notification", func() (string, error) {
|
||||
var req [0xC30]byte
|
||||
copy(req[0x2D:], "Tailscale probe: notification test")
|
||||
fd, err := syscall.Open("/dev/notification0", syscall.O_WRONLY|syscall.O_NONBLOCK, 0)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("open: %w", err)
|
||||
}
|
||||
defer syscall.Close(fd)
|
||||
n, err := syscall.Write(fd, req[:])
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("write: %w", err)
|
||||
}
|
||||
return fmt.Sprintf("wrote %d bytes", n), nil
|
||||
})
|
||||
|
||||
step("net.Interfaces", func() (string, error) {
|
||||
ifs, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
s := ""
|
||||
for _, ifc := range ifs {
|
||||
addrs, aerr := ifc.Addrs()
|
||||
s += fmt.Sprintf("\n %d %s %v mtu=%d %v addrs=%v err=%v", ifc.Index, ifc.Name, ifc.HardwareAddr, ifc.MTU, ifc.Flags, addrs, aerr)
|
||||
}
|
||||
return s, nil
|
||||
})
|
||||
|
||||
step("tcp listen+accept", func() (string, error) {
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer ln.Close()
|
||||
go func() {
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
io.Copy(c, c)
|
||||
c.Close()
|
||||
}()
|
||||
c, err := net.DialTimeout("tcp", ln.Addr().String(), 5*time.Second)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer c.Close()
|
||||
c.Write([]byte("ping"))
|
||||
c.(*net.TCPConn).CloseWrite()
|
||||
b, err := io.ReadAll(c)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("echo %q via %v", b, ln.Addr()), nil
|
||||
})
|
||||
|
||||
step("udp v4+v6 sockets", func() (string, error) {
|
||||
c4, err := net.ListenPacket("udp4", ":0")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("udp4: %w", err)
|
||||
}
|
||||
defer c4.Close()
|
||||
c6, err6 := net.ListenPacket("udp6", ":0")
|
||||
if err6 == nil {
|
||||
defer c6.Close()
|
||||
}
|
||||
return fmt.Sprintf("udp4=%v udp6 err=%v", c4.LocalAddr(), err6), nil
|
||||
})
|
||||
|
||||
step("dns default resolver", func() (string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
|
||||
defer cancel()
|
||||
ips, err := net.DefaultResolver.LookupHost(ctx, "controlplane.tailscale.com")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%d addresses, first %s", len(ips), ips[0]), nil
|
||||
})
|
||||
|
||||
step("https", func() (string, error) {
|
||||
c := &http.Client{Timeout: 15 * time.Second, Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}}
|
||||
resp, err := c.Get("https://controlplane.tailscale.com/key?v=100")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
b, _ := io.ReadAll(io.LimitReader(resp.Body, 60))
|
||||
return fmt.Sprintf("%s %s %q", resp.Proto, resp.Status, b), nil
|
||||
})
|
||||
|
||||
fmt.Printf("== done, %d failed ==\n", failed)
|
||||
if failed > 0 {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
# 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
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
# Build a Go program for the PS5 and wrap it in the launcher payload.
|
||||
# .\tools\build-payload.ps1 -GoDir probe-go -Name probe [-DebugLoader] [-Watchdog 120] [-Send]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$GoDir,
|
||||
[Parameter(Mandatory = $true)][string]$Name,
|
||||
[string]$Package = '.',
|
||||
[string]$Tags = '',
|
||||
[string]$Version = '', # sets main.version in the Go program
|
||||
[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]$DebugLoader, # print loader details and early crash registers
|
||||
[switch]$KeepSymbols,
|
||||
[switch]$Send,
|
||||
[int]$Seconds = 60
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
. (Join-Path $PSScriptRoot 'env.ps1')
|
||||
|
||||
$out = Join-Path $DevRoot 'out'
|
||||
New-Item -ItemType Directory -Force $out | Out-Null
|
||||
$bin = Join-Path $out "$Name.bin"
|
||||
$elf = Join-Path $out "$Name.elf"
|
||||
|
||||
$ldflags = @()
|
||||
if (-not $KeepSymbols) { $ldflags += '-s', '-w' }
|
||||
if ($Version) { $ldflags += "-X main.version=$Version" }
|
||||
$goArgs = @('build', '-buildmode=pie', '-trimpath', "-ldflags=$($ldflags -join ' ')", '-o', $bin)
|
||||
if ($Tags) { $goArgs += "-tags=$Tags" }
|
||||
$goArgs += $Package
|
||||
|
||||
Push-Location (Join-Path $DevRoot $GoDir)
|
||||
try { Invoke-PS5Go @goArgs } finally { Pop-Location }
|
||||
|
||||
$img = $bin -replace '\\', '/'
|
||||
$ccArgs = @('-O2', '-Wall', "-DGO_IMAGE=`"$img`"")
|
||||
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'))
|
||||
Invoke-PS5CC @ccArgs
|
||||
|
||||
Write-Host ("built {0} ({1:N1} MB)" -f $elf, ((Get-Item $elf).Length / 1MB))
|
||||
if ($Send) {
|
||||
& (Join-Path $PSScriptRoot 'ps5send.ps1') -File $elf -Seconds $Seconds
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
# Dot-source this file to get the PS5 cross-build helpers.
|
||||
# . .\tools\env.ps1
|
||||
|
||||
$script:DevRoot = Split-Path -Parent $PSScriptRoot
|
||||
$script:SdkRoot = Join-Path $DevRoot 'toolchain\ps5-payload-sdk'
|
||||
$script:LlvmBin = Join-Path $DevRoot 'toolchain\llvm\bin'
|
||||
$script:GoRoot = Join-Path $DevRoot 'goroot'
|
||||
|
||||
$env:PS5_PAYLOAD_SDK = $SdkRoot
|
||||
if ($env:PATH -notlike "*$LlvmBin*") {
|
||||
$env:PATH = "$LlvmBin;$SdkRoot\win;$env:PATH"
|
||||
}
|
||||
|
||||
# Compile and link C sources into a PS5 payload ELF (same flags as the SDK's
|
||||
# prospero-clang wrapper).
|
||||
function Invoke-PS5CC {
|
||||
# Plain $args on purpose: a param() block would make PowerShell try to bind
|
||||
# compiler flags such as -o to common parameters.
|
||||
$CcArgs = $args
|
||||
# clang >= 20 links the crt objects itself and finds them through this
|
||||
# variable, so crt1.o must not be passed explicitly.
|
||||
$env:SCE_PROSPERO_SDK_DIR = $SdkRoot
|
||||
& "$LlvmBin\clang.exe" `
|
||||
--start-no-unused-arguments `
|
||||
-target x86_64-sie-ps5 `
|
||||
-fvisibility-nodllstorageclass=default `
|
||||
-isysroot $SdkRoot `
|
||||
-isystem "$SdkRoot\target\include" `
|
||||
-L "$SdkRoot\target\lib" `
|
||||
-fno-stack-protector -fno-plt -femulated-tls `
|
||||
-lc `
|
||||
--end-no-unused-arguments `
|
||||
@CcArgs `
|
||||
--start-no-unused-arguments `
|
||||
--sysroot $SdkRoot `
|
||||
'-Wl,--hash-style=gnu' `
|
||||
-lkernel_web -lSceLibcInternal -lSceNet `
|
||||
--end-no-unused-arguments
|
||||
if ($LASTEXITCODE -ne 0) { throw "clang failed ($LASTEXITCODE)" }
|
||||
}
|
||||
|
||||
# Run the patched Go toolchain for the PS5 (FreeBSD/amd64 ABI).
|
||||
function Invoke-PS5Go {
|
||||
$GoArgs = $args
|
||||
$old = @{ GOROOT = $env:GOROOT; GOOS = $env:GOOS; GOARCH = $env:GOARCH; CGO_ENABLED = $env:CGO_ENABLED
|
||||
GOTOOLCHAIN = $env:GOTOOLCHAIN; GOTMPDIR = $env:GOTMPDIR; GOCACHE = $env:GOCACHE; GOFLAGS = $env:GOFLAGS }
|
||||
try {
|
||||
$env:GOROOT = $GoRoot
|
||||
$env:GOOS = 'freebsd'
|
||||
$env:GOARCH = 'amd64'
|
||||
$env:CGO_ENABLED = '0'
|
||||
$env:GOTOOLCHAIN = 'local'
|
||||
$env:GOTMPDIR = Join-Path $DevRoot '.gotmp'
|
||||
$env:GOCACHE = Join-Path $DevRoot '.gocache'
|
||||
New-Item -ItemType Directory -Force $env:GOTMPDIR, $env:GOCACHE | Out-Null
|
||||
& "$GoRoot\bin\go.exe" @GoArgs
|
||||
if ($LASTEXITCODE -ne 0) { throw "go failed ($LASTEXITCODE)" }
|
||||
} finally {
|
||||
foreach ($k in $old.Keys) { Set-Item "env:$k" $old[$k] }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
# Send an ELF payload to the PS5 ELF loader and print whatever it writes back.
|
||||
# $env:PS5_HOST = '192.168.1.50'
|
||||
# .\tools\ps5send.ps1 -File out\hello.elf [-Seconds 20]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$File,
|
||||
[string]$PS5Host = $env:PS5_HOST, # the console's address
|
||||
[int]$Port = 9021,
|
||||
[int]$Seconds = 20, # stop listening after this long
|
||||
[int]$IdleSeconds = 0 # if > 0, also stop after this long without output
|
||||
)
|
||||
|
||||
if (-not $PS5Host) { throw 'Set $env:PS5_HOST or pass -PS5Host with the console''s address.' }
|
||||
$bytes = [IO.File]::ReadAllBytes((Resolve-Path $File))
|
||||
$client = [Net.Sockets.TcpClient]::new()
|
||||
$client.NoDelay = $true
|
||||
$client.Connect($PS5Host, $Port)
|
||||
$stream = $client.GetStream()
|
||||
$stream.Write($bytes, 0, $bytes.Length)
|
||||
$stream.Flush()
|
||||
Write-Host ("[sent {0:N0} bytes to {1}:{2}]" -f $bytes.Length, $PS5Host, $Port)
|
||||
|
||||
$buf = [byte[]]::new(65536)
|
||||
$deadline = [DateTime]::UtcNow.AddSeconds($Seconds)
|
||||
$lastData = [DateTime]::UtcNow
|
||||
$closed = $false
|
||||
while ([DateTime]::UtcNow -lt $deadline) {
|
||||
if ($client.Client.Poll(200000, [Net.Sockets.SelectMode]::SelectRead)) {
|
||||
$n = 0
|
||||
try { $n = $stream.Read($buf, 0, $buf.Length) } catch { $closed = $true; break }
|
||||
if ($n -le 0) { $closed = $true; break }
|
||||
Write-Host -NoNewline ([Text.Encoding]::UTF8.GetString($buf, 0, $n))
|
||||
$lastData = [DateTime]::UtcNow
|
||||
} elseif ($IdleSeconds -gt 0 -and ([DateTime]::UtcNow - $lastData).TotalSeconds -gt $IdleSeconds) {
|
||||
break
|
||||
}
|
||||
}
|
||||
Write-Host ''
|
||||
if ($closed) { Write-Host '[connection closed by PS5]' } else { Write-Host '[stopped listening]' }
|
||||
$client.Close()
|
||||
@@ -0,0 +1,114 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// The installer registers the daemon with whichever payload autoloader the
|
||||
// console uses: it copies the payload where the autoloader looks for files
|
||||
// and adds its file name to the autoloader's load order (one payload per
|
||||
// line, "!N" lines are delays). This file is the daemon's side of that:
|
||||
// reporting the registration and undoing it. Keep the table in sync with
|
||||
// installer/main.c.
|
||||
|
||||
const daemonFileName = "tailscale.elf"
|
||||
|
||||
// An autoloader is one place a payload autoloader keeps its load order.
|
||||
type autoloader struct {
|
||||
name string // for messages
|
||||
list string // the load order file
|
||||
payloadDir string // where the daemon payload is stored for it
|
||||
}
|
||||
|
||||
var autoloaders = func() []autoloader {
|
||||
al := []autoloader{
|
||||
// Payload Manager finds entries by file name anywhere below
|
||||
// /data/pldmgr and keeps each payload in a folder of its own.
|
||||
{"Payload Manager", "/data/pldmgr/autoload.txt", "/data/pldmgr/payloads/Tailscale"},
|
||||
{"PS5 autoloader", "/data/ps5_autoloader/autoload.txt", "/data/ps5_autoloader"},
|
||||
}
|
||||
for _, usb := range []string{"usb0", "usb1", "usb2", "usb3"} {
|
||||
dir := "/mnt/" + usb + "/ps5_autoloader"
|
||||
al = append(al, autoloader{"PS5 autoloader (" + usb + ")", dir + "/autoload.txt", dir})
|
||||
}
|
||||
return al
|
||||
}()
|
||||
|
||||
// autostartNames returns the autoloaders whose load order includes the daemon.
|
||||
func autostartNames() []string {
|
||||
names := []string{}
|
||||
for _, al := range autoloaders {
|
||||
b, err := os.ReadFile(al.list)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if _, found := removeAutoloadEntry(string(b), daemonFileName); found {
|
||||
names = append(names, al.name)
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// removeAutoloadEntry returns text without the lines that name the payload,
|
||||
// and whether there were any. Everything else, including line endings, is
|
||||
// kept as it was.
|
||||
func removeAutoloadEntry(text, name string) (string, bool) {
|
||||
var out strings.Builder
|
||||
found := false
|
||||
for len(text) > 0 {
|
||||
line := text
|
||||
if i := strings.IndexByte(text, '\n'); i >= 0 {
|
||||
line = text[:i+1]
|
||||
}
|
||||
text = text[len(line):]
|
||||
if strings.TrimSpace(line) == name {
|
||||
found = true
|
||||
continue
|
||||
}
|
||||
out.WriteString(line)
|
||||
}
|
||||
return out.String(), found
|
||||
}
|
||||
|
||||
// uninstall removes the daemon from every autoloader and deletes the
|
||||
// installed payload. With purge it also deletes the Tailscale state and
|
||||
// config, which forgets the login. The running process is not affected.
|
||||
func uninstall(purge bool, logf func(string, ...any)) error {
|
||||
var errs []error
|
||||
remove := func(path string) {
|
||||
if err := os.Remove(path); err != nil && !errors.Is(err, fs.ErrNotExist) {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
for _, al := range autoloaders {
|
||||
b, err := os.ReadFile(al.list)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if text, found := removeAutoloadEntry(string(b), daemonFileName); found {
|
||||
if err := os.WriteFile(al.list, []byte(text), 0o644); err != nil {
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
logf("removed %s from %s", daemonFileName, al.list)
|
||||
}
|
||||
remove(filepath.Join(al.payloadDir, daemonFileName))
|
||||
if filepath.Base(al.payloadDir) == "Tailscale" {
|
||||
// A folder the installer created just for this payload.
|
||||
os.Remove(al.payloadDir)
|
||||
}
|
||||
}
|
||||
remove(filepath.Join(dataDir, daemonFileName))
|
||||
if purge {
|
||||
for _, name := range []string{"state", "config.json", ".cache"} {
|
||||
if err := os.RemoveAll(filepath.Join(dataDir, name)); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRemoveAutoloadEntry(t *testing.T) {
|
||||
tests := []struct {
|
||||
name, in, want string
|
||||
found bool
|
||||
}{
|
||||
{"absent", "kstuff.elf\n!500\nftpsrv.elf\n", "kstuff.elf\n!500\nftpsrv.elf\n", false},
|
||||
{"last line", "kstuff.elf\ntailscale.elf\n", "kstuff.elf\n", true},
|
||||
{"no trailing newline", "kstuff.elf\ntailscale.elf", "kstuff.elf\n", true},
|
||||
{"middle, CRLF kept", "a.elf\r\ntailscale.elf\r\nb.elf\r\n", "a.elf\r\nb.elf\r\n", true},
|
||||
{"listed twice", "tailscale.elf\na.elf\n tailscale.elf \n", "a.elf\n", true},
|
||||
{"similar names stay", "# tailscale.elf\nmy-tailscale.elf\ntailscale.elf.bak\n", "# tailscale.elf\nmy-tailscale.elf\ntailscale.elf.bak\n", false},
|
||||
{"empty", "", "", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got, found := removeAutoloadEntry(tt.in, daemonFileName)
|
||||
if got != tt.want || found != tt.found {
|
||||
t.Errorf("%s: got %q, %v; want %q, %v", tt.name, got, found, tt.want, tt.found)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// pipe must pass a half-close through: the ELF loader protocol and FTP data
|
||||
// connections both rely on the reader seeing EOF while the other direction
|
||||
// stays open.
|
||||
func TestPipeHalfClose(t *testing.T) {
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
// "Local service": reads everything, then answers.
|
||||
go func() {
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer c.Close()
|
||||
b, _ := io.ReadAll(c)
|
||||
c.Write(append([]byte("got "), b...))
|
||||
}()
|
||||
|
||||
tcpClient, tcpProxy := tcpPair(t)
|
||||
|
||||
local, err := net.Dial("tcp", ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
go func() {
|
||||
pipe(tcpProxy, local)
|
||||
tcpProxy.Close()
|
||||
local.Close()
|
||||
}()
|
||||
|
||||
tcpClient.Write([]byte("payload"))
|
||||
tcpClient.(*net.TCPConn).CloseWrite()
|
||||
b, err := io.ReadAll(tcpClient)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(b) != "got payload" {
|
||||
t.Fatalf("got %q", b)
|
||||
}
|
||||
}
|
||||
|
||||
func tcpPair(t *testing.T) (net.Conn, net.Conn) {
|
||||
t.Helper()
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ln.Close()
|
||||
ch := make(chan net.Conn, 1)
|
||||
go func() {
|
||||
c, _ := ln.Accept()
|
||||
ch <- c
|
||||
}()
|
||||
a, err := net.Dial("tcp", ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return a, <-ch
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
)
|
||||
|
||||
// config is read from /data/tailscale/config.json. Every field is optional.
|
||||
type config struct {
|
||||
// Hostname is the name this console gets on the tailnet.
|
||||
Hostname string `json:"hostname"`
|
||||
// AuthKey, if set, logs the console in without the browser step.
|
||||
AuthKey string `json:"authKey,omitempty"`
|
||||
// WebAddr is where the status page listens.
|
||||
WebAddr string `json:"webAddr"`
|
||||
// HTTPProxyAddr is where the outbound HTTP proxy listens. Pointing the
|
||||
// PS5's proxy setting at it lets the console reach tailnet hosts. Empty
|
||||
// disables the proxy.
|
||||
HTTPProxyAddr string `json:"httpProxyAddr"`
|
||||
// ControlURL selects a coordination server other than Tailscale's.
|
||||
ControlURL string `json:"controlURL,omitempty"`
|
||||
// SunshineHost is a tailnet device running Sunshine. When set, its
|
||||
// streaming ports are forwarded from 127.0.0.1, so a Moonlight client on
|
||||
// the console can use 127.0.0.1 as the host.
|
||||
SunshineHost string `json:"sunshineHost,omitempty"`
|
||||
// Forwards are extra local forwards: a localhost port on the console
|
||||
// relayed to a host on the tailnet.
|
||||
Forwards []forwardRule `json:"forwards,omitempty"`
|
||||
// BlockedPorts lists local TCP ports that are never exposed to the tailnet.
|
||||
BlockedPorts []uint16 `json:"blockedPorts,omitempty"`
|
||||
// Verbose turns on Tailscale's own (very chatty) logging.
|
||||
Verbose bool `json:"verbose,omitempty"`
|
||||
}
|
||||
|
||||
func defaultConfig() config {
|
||||
return config{
|
||||
Hostname: "ps5",
|
||||
WebAddr: ":8090",
|
||||
HTTPProxyAddr: "127.0.0.1:8118",
|
||||
}
|
||||
}
|
||||
|
||||
// loadConfig reads the config file, writing one with the defaults if it does
|
||||
// not exist yet so that there is something to edit.
|
||||
func loadConfig(path string) (config, error) {
|
||||
cfg := defaultConfig()
|
||||
b, err := os.ReadFile(path)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return cfg, saveConfig(path, cfg)
|
||||
}
|
||||
if err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
if err := json.Unmarshal(b, &cfg); err != nil {
|
||||
return defaultConfig(), err
|
||||
}
|
||||
if cfg.Hostname == "" {
|
||||
cfg.Hostname = "ps5"
|
||||
}
|
||||
if cfg.WebAddr == "" {
|
||||
cfg.WebAddr = ":8090"
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func saveConfig(path string, cfg config) error {
|
||||
b, err := json.MarshalIndent(cfg, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(path, append(b, '\n'), 0o600)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//go:build !freebsd
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// On a development machine the console is just the terminal and PS5
|
||||
// notifications are printed.
|
||||
|
||||
type console struct{}
|
||||
|
||||
func newConsole() *console { return &console{} }
|
||||
|
||||
func (c *console) Printf(format string, args ...any) { fmt.Printf(format, args...) }
|
||||
|
||||
func redirectStdio(f *os.File) {}
|
||||
|
||||
func notify(format string, args ...any) {
|
||||
fmt.Printf("[notification] "+format+"\n", args...)
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
//go:build freebsd
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// console writes to whoever sent the payload. The ELF loader hands a payload
|
||||
// its TCP connection as stdin/stdout/stderr, and that connection goes away as
|
||||
// soon as the sender disconnects.
|
||||
//
|
||||
// Go kills the process when a write to fd 1 or 2 fails with EPIPE, so the
|
||||
// connection is moved to another descriptor and the first failed write turns
|
||||
// the console off for good.
|
||||
type console struct {
|
||||
mu sync.Mutex
|
||||
fd int
|
||||
}
|
||||
|
||||
func newConsole() *console {
|
||||
fd, err := syscall.Dup(1)
|
||||
if err != nil {
|
||||
return &console{fd: -1}
|
||||
}
|
||||
syscall.CloseOnExec(fd)
|
||||
return &console{fd: fd}
|
||||
}
|
||||
|
||||
func (c *console) Printf(format string, args ...any) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.fd < 0 {
|
||||
return
|
||||
}
|
||||
b := []byte(fmt.Sprintf(format, args...))
|
||||
for len(b) > 0 {
|
||||
n, err := syscall.Write(c.fd, b)
|
||||
if err != nil || n <= 0 {
|
||||
syscall.Close(c.fd)
|
||||
c.fd = -1
|
||||
return
|
||||
}
|
||||
b = b[n:]
|
||||
}
|
||||
}
|
||||
|
||||
// redirectStdio points stdout and stderr at the log file.
|
||||
func redirectStdio(f *os.File) {
|
||||
syscall.Dup2(int(f.Fd()), 1)
|
||||
syscall.Dup2(int(f.Fd()), 2)
|
||||
}
|
||||
|
||||
// notify shows a pop-up on the PS5 screen.
|
||||
//
|
||||
// It does what libkernel's sceKernelSendNotificationRequest does: write a
|
||||
// fixed-size request to the notification device. The request is 0xC30 bytes
|
||||
// with the UTF-8 message at offset 0x2D.
|
||||
func notify(format string, args ...any) {
|
||||
const (
|
||||
requestSize = 0xC30
|
||||
messageOffset = 0x2D
|
||||
)
|
||||
var req [requestSize]byte
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
if max := requestSize - messageOffset - 1; len(msg) > max {
|
||||
msg = msg[:max]
|
||||
}
|
||||
copy(req[messageOffset:], msg)
|
||||
|
||||
fd, err := syscall.Open("/dev/notification0", syscall.O_WRONLY|syscall.O_NONBLOCK, 0)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer syscall.Close(fd)
|
||||
syscall.Write(fd, req[:])
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// debugLog keeps Tailscale's own verbose log on disk, in a file of bounded
|
||||
// size next to the main log.
|
||||
//
|
||||
// The main log only has the daemon's few messages, which is not enough to
|
||||
// tell what Tailscale was doing when something goes wrong, and the things
|
||||
// that go wrong on a console (a freeze, a crash) take the process down
|
||||
// before anyone can ask it. So the detail is always recorded and synced to
|
||||
// disk every couple of seconds.
|
||||
type debugLog struct {
|
||||
path string
|
||||
max int64
|
||||
|
||||
mu sync.Mutex
|
||||
f *os.File
|
||||
size int64
|
||||
dirty bool
|
||||
}
|
||||
|
||||
func openDebugLog(path string, max int64) *debugLog {
|
||||
l := &debugLog{path: path, max: max}
|
||||
l.open()
|
||||
go l.syncLoop()
|
||||
return l
|
||||
}
|
||||
|
||||
func (l *debugLog) open() {
|
||||
f, err := os.OpenFile(l.path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
l.f = f
|
||||
if fi, err := f.Stat(); err == nil {
|
||||
l.size = fi.Size()
|
||||
}
|
||||
}
|
||||
|
||||
func (l *debugLog) Printf(format string, args ...any) {
|
||||
line := time.Now().Format("2006-01-02 15:04:05.000 ") + fmt.Sprintf(format, args...) + "\n"
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
if l.f == nil {
|
||||
return
|
||||
}
|
||||
if l.size+int64(len(line)) > l.max {
|
||||
// Keep one previous file, so there is always between one and two
|
||||
// times max of history.
|
||||
l.f.Close()
|
||||
os.Rename(l.path, l.path+".old")
|
||||
l.f, l.size = nil, 0
|
||||
l.open()
|
||||
if l.f == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
n, _ := l.f.WriteString(line)
|
||||
l.size += int64(n)
|
||||
l.dirty = true
|
||||
}
|
||||
|
||||
func (l *debugLog) syncLoop() {
|
||||
for range time.Tick(2 * time.Second) {
|
||||
l.mu.Lock()
|
||||
if l.dirty && l.f != nil {
|
||||
l.f.Sync()
|
||||
l.dirty = false
|
||||
}
|
||||
l.mu.Unlock()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// forwardToLocalhost is tsnet's fallback TCP handler: it is asked about every
|
||||
// tailnet connection to a port nothing in this process listens on. If a
|
||||
// service on the console listens on that port, the connection is accepted and
|
||||
// piped to it. That is what makes FTP, the payload loader and the like
|
||||
// reachable over the tailnet.
|
||||
//
|
||||
// The local connection is made before answering, while the tailnet peer is
|
||||
// still waiting for its SYN-ACK. If nothing listens on the port the
|
||||
// connection is declined, so the peer sees an ordinary "connection refused"
|
||||
// rather than a connection that opens and closes.
|
||||
func (d *daemon) forwardToLocalhost(src, dst netip.AddrPort) (handler func(net.Conn), intercept bool) {
|
||||
port := dst.Port()
|
||||
if slices.Contains(d.cfg.BlockedPorts, port) || port == d.proxyPort || d.fwd.listensOnTCP(port) {
|
||||
// The outbound proxy and the local forwards are for the console's
|
||||
// own apps. Exposing them would let any tailnet device use the
|
||||
// console as a relay.
|
||||
return nil, false
|
||||
}
|
||||
local, err := net.DialTimeout("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(int(port))), 2*time.Second)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
// If the tailnet side never completes its handshake the handler is not
|
||||
// called; do not keep the local connection open for it forever.
|
||||
abandoned := time.AfterFunc(30*time.Second, func() { local.Close() })
|
||||
return func(c net.Conn) {
|
||||
defer c.Close()
|
||||
defer local.Close()
|
||||
if !abandoned.Stop() {
|
||||
return
|
||||
}
|
||||
if port != d.webPort {
|
||||
// The status page polls every few seconds; logging its own
|
||||
// requests would bury everything else.
|
||||
d.logf("forward %v -> localhost:%d", src, port)
|
||||
}
|
||||
pipe(c, local)
|
||||
}, true
|
||||
}
|
||||
|
||||
// pipe copies in both directions until both sides are done.
|
||||
func pipe(a, b net.Conn) {
|
||||
done := make(chan struct{}, 2)
|
||||
cp := func(dst, src net.Conn) {
|
||||
io.Copy(dst, src)
|
||||
// Pass the end of the stream on, so that protocols relying on a
|
||||
// half-close (such as sending a payload to the ELF loader) work.
|
||||
if cw, ok := dst.(interface{ CloseWrite() error }); ok {
|
||||
cw.CloseWrite()
|
||||
} else {
|
||||
dst.Close()
|
||||
}
|
||||
done <- struct{}{}
|
||||
}
|
||||
go cp(a, b)
|
||||
go cp(b, a)
|
||||
<-done
|
||||
<-done
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
module ps5tailscale
|
||||
|
||||
go 1.27.1
|
||||
|
||||
require (
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20260929172509-b39ff6d641ec
|
||||
tailscale.com v1.104.0
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.2.0 // indirect
|
||||
github.com/akutz/memconn v0.1.0 // indirect
|
||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e // indirect
|
||||
github.com/coder/websocket v1.8.15 // indirect
|
||||
github.com/creachadair/msync v0.10.1 // indirect
|
||||
github.com/dblohm7/wingoes v0.0.0-20260526185140-fb298caac7ca // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.3 // indirect
|
||||
github.com/gaissmai/bart v0.29.0 // indirect
|
||||
github.com/go-json-experiment/json v0.0.0-20260820222146-c27c302e5fc3 // indirect
|
||||
github.com/godbus/dbus/v5 v5.2.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/hdevalence/ed25519consensus v0.2.0 // indirect
|
||||
github.com/huin/goupnp v1.3.0 // indirect
|
||||
github.com/jsimonetti/rtnetlink v1.4.2 // indirect
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
github.com/mdlayher/netlink v1.11.2 // indirect
|
||||
github.com/mdlayher/socket v0.7.0 // indirect
|
||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||
github.com/pires/go-proxyproto v0.15.0 // indirect
|
||||
github.com/safchain/ethtool v0.7.0 // indirect
|
||||
github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d // indirect
|
||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
|
||||
github.com/tailscale/hujson v0.0.0-20260727124030-b80ff77dac4f // indirect
|
||||
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect
|
||||
github.com/tailscale/web-client-prebuilt v0.0.0-20260917222731-e0ed2d0d0fea // indirect
|
||||
github.com/tailscale/wireguard-go v0.0.0-20260928213032-417aef361226 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect
|
||||
go4.org/netipx v0.0.0-20260823151212-3075585bcbeb // indirect
|
||||
golang.org/x/crypto v0.57.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba // indirect
|
||||
golang.org/x/net v0.59.0 // indirect
|
||||
golang.org/x/oauth2 v0.37.0 // indirect
|
||||
golang.org/x/sync v0.23.0 // indirect
|
||||
golang.org/x/sys v0.48.0 // indirect
|
||||
golang.org/x/term v0.46.0 // indirect
|
||||
golang.org/x/text v0.42.0 // indirect
|
||||
golang.org/x/time v0.16.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||
golang.zx2c4.com/wireguard/windows v1.0.1 // indirect
|
||||
gvisor.dev/gvisor v0.0.0-20260915211658-a6f909f08a72 // indirect
|
||||
)
|
||||
+243
@@ -0,0 +1,243 @@
|
||||
9fans.net/go v0.0.8-0.20250307142834-96bdba94b63f h1:1C7nZuxUMNz7eiQALRfiqNOm04+m3edWlRff/BYHf0Q=
|
||||
9fans.net/go v0.0.8-0.20250307142834-96bdba94b63f/go.mod h1:hHyrZRryGqVdqrknjq5OWDLGCTJ2NeEvtrpR96mjraM=
|
||||
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||
filippo.io/mkcert v1.4.4 h1:8eVbbwfVlaqUM7OwuftKc2nuYOoTDQWqsoXmzoXZdbc=
|
||||
filippo.io/mkcert v1.4.4/go.mod h1:VyvOchVuAye3BoUsPUOOofKygVwLV2KQMVFJNRq+1dA=
|
||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A=
|
||||
github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw=
|
||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
|
||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/aws/aws-sdk-go-v2 v1.46.0 h1:1kt7m/EKcEHt5mlyyxx9cSlMddRPIKbjb6DIQsu4HPk=
|
||||
github.com/aws/aws-sdk-go-v2 v1.46.0/go.mod h1:bttEH6JqnUL8LepvDVfdrds/fZ5bCIxzpe3abyUrhDU=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.33.3 h1:h090b3O5S17bF87/0ysHZuIT/7DCb4EBRFQX2PMVPCw=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.33.3/go.mod h1:YYDB1kTejxbfAbEVUqgCtkVp26xvNCHev9cLKABMGAk=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.20.3 h1:tToOYM/LXev4NpfWlIYGDvBvjHmJ3HXpRU9ppl+pM6k=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.20.3/go.mod h1:wfGneWyncO7p67wqXV2IQhPk14JqIc25woKlaArT3WI=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.19.2 h1:Ldv7RPHs7qwwTscRjAl3YBud32f3BvdAGRmSvAx5L38=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.19.2/go.mod h1:XyK6UV8xbo66ysVqLd2783C09pBYHOm8aKTRV5DVJ30=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.2 h1:q/PSLGuRWCChWg+dLnb9dWOnrCxJtnboXbBtFoqqRrI=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.2/go.mod h1:TD1jvU2LvXkJexct5vBqcd8QlNXh5EmRUeL/Z32p0n4=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.2 h1:6fl86IPqKEXoySqiOWdfgbEp9OVbn44zTfEICNEBDhY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.2/go.mod h1:63HDfhFkdzBpI8WGXTSKUHPKS6mqldj4u3LJW7RZtSU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.2 h1:XMgIRS+uW9F3yFKnXGRrI9pkHi99CXTmoz2kz2/TGBA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.2/go.mod h1:vorxDzK+n3jiv9a5ST/LG0Eu9cSv1CRdKTpG6pDMs+M=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19 h1:bAdDl/HkGCcGPoe25ToSHEw23VIxt6CT5fLcg111BKg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19/go.mod h1:KaUzbLxv4CeSxh6ZCl9B4m7CuFenS8kUEaDs+f/DQr4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.2 h1:ZtHYnumr6QyxhzEzNZwzQTFJEXOswrZqTTkRxthwvr4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.2/go.mod h1:a1NXrYpBd311gBzn1UI5UzJyyvXktM4xNh/ydPiPpqY=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.9.0 h1:c3k+k/CS4L+sAIH6fxikL+g5g2LpeNczaoyjjw1iMKI=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.9.0/go.mod h1:AGIoQg99fBrOIQnF78TLx4lj18mc4gZ0hJx1UaLIFM4=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.77.0 h1:JqKSBJlS68F4nxoPLwD7tB3lJ3gWEK9p+7eOiBE/lsM=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.77.0/go.mod h1:5TMMHmQGjCODMeIopSNfxkan11jG0SmFpyjcMvCLzHA=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.37.0 h1:+rqBaOq7jzInjY8M12hr+zEe85JpRll9BjMx38r33Ok=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.37.0/go.mod h1:XFlVwUsw3sYh8Hw37umYVJnrcWrwXWRxbNw/JY0bblw=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.42.0 h1:hzM3GslEAOBcLn3DHH6ENToFi+vXP+n02W+x6zejAIM=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.42.0/go.mod h1:588e7skMkYIYkSUseT8E3WKFfbAfrA1bj3Zf+qxJtJY=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.49.0 h1:N7Ey8obY3uSui+cxl0OUzFlFmkxSucoJnrniFhw+cLc=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.49.0/go.mod h1:zMBwjSf4Pt8a1OHYiZ5rPD0PJRK1kQrUaxhS/Dbld8E=
|
||||
github.com/aws/smithy-go v1.28.1 h1:R/nXH00c8qcfCzQVELtRw+eLQWtzv+VAIEFJ1/xxXlQ=
|
||||
github.com/aws/smithy-go v1.28.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/axiomhq/hyperloglog v0.2.6 h1:sRhvvF3RIXWQgAXaTphLp4yJiX4S0IN3MWTaAgZoRJw=
|
||||
github.com/axiomhq/hyperloglog v0.2.6/go.mod h1:YjX/dQqCR/7QYX0g8mu8UZAjpIenz1FKM71UEsjFoTo=
|
||||
github.com/benbjohnson/immutable v0.4.3 h1:GYHcksoJ9K6HyAUpGxwZURrbTkXA0Dh4otXGqbhdrjA=
|
||||
github.com/benbjohnson/immutable v0.4.3/go.mod h1:qJIKKSmdqz1tVzNtst1DZzvaqOU1onk1rc03IeM3Owk=
|
||||
github.com/bradfitz/reco v0.0.0-20260929154613-b883fbd17e3f h1:jdXGSp5Ge5/kzikuxGOw9+6HSStUjkNBJZjGNtpItek=
|
||||
github.com/bradfitz/reco v0.0.0-20260929154613-b883fbd17e3f/go.mod h1:oiCPXxBoDKSkb/RGEZwAZk8y/fwDlQ1vYlBbNRxPysw=
|
||||
github.com/cilium/ebpf v0.22.0 h1:v2ktp0roffpMOj2MMf3idtCQZOsAoC4BJbAJN+ke2bY=
|
||||
github.com/cilium/ebpf v0.22.0/go.mod h1:CDzZbe2hC5JjlDC+CY3KFCzlYwN4gbxppYM+Z10bQt4=
|
||||
github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA=
|
||||
github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||
github.com/coreos/go-iptables v0.8.0 h1:MPc2P89IhuVpLI7ETL/2tx3XZ61VeICZjYqDEgNsPRc=
|
||||
github.com/coreos/go-iptables v0.8.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
|
||||
github.com/creachadair/mds v0.31.0 h1:h4SXXgallvZfD8CFQnQNAOtBkUGpZjPwNBRQpHpHM/g=
|
||||
github.com/creachadair/mds v0.31.0/go.mod h1:IZ3oLffgVNZ4O5WXTPxx7EX5ilSl1ypdv5FekYyynyI=
|
||||
github.com/creachadair/msync v0.10.1 h1:14jT5Ujob64Zp2D3ZcUidayDMoXikhmYN8xBxEuPKPk=
|
||||
github.com/creachadair/msync v0.10.1/go.mod h1:dFf4Z0cxE2nibFmAdq5OqTuGn++GgEqA9JuKH+7ceSA=
|
||||
github.com/creachadair/taskgroup v0.14.4 h1:ttR8StLWmYA1O6x96YlTpQLXjKyRpO4RBo+OcO6W6C0=
|
||||
github.com/creachadair/taskgroup v0.14.4/go.mod h1:uhCtIEsa7zpeMAFixddhCYjbJi7e4JMyU7OXUygkSsg=
|
||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/dblohm7/wingoes v0.0.0-20260526185140-fb298caac7ca h1:h1Awca4lQOspNR/2eeo04Ricn5NixDX9mb17WSAgLhQ=
|
||||
github.com/dblohm7/wingoes v0.0.0-20260526185140-fb298caac7ca/go.mod h1:2TGl1jRJrRpbzykmg7asHm3h08TqutUgQqY5v9k/g3c=
|
||||
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 h1:ucRHb6/lvW/+mTEIGbvhcYU3S8+uSNkuMjx/qZFfhtM=
|
||||
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
|
||||
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q=
|
||||
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A=
|
||||
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fxamacker/cbor/v2 v2.9.3 h1:oQBnFATpNdY8gJHTndDDv5Xl4QqNaz51G5LLEPhng3Q=
|
||||
github.com/fxamacker/cbor/v2 v2.9.3/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/gaissmai/bart v0.29.0 h1:wO6HGE8g9YE0Wm0bCpYxwRzfQ4+fbJKOhL64e5ACGCI=
|
||||
github.com/gaissmai/bart v0.29.0/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c=
|
||||
github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I=
|
||||
github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo=
|
||||
github.com/go-json-experiment/json v0.0.0-20260820222146-c27c302e5fc3 h1:UADEEmDKgfXbtnGJZ97beY5XLo9ZechG1nlU4KnRrkE=
|
||||
github.com/go-json-experiment/json v0.0.0-20260820222146-c27c302e5fc3/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go4org/hashtriemap v0.0.0-20260925222741-44e5305f85d9 h1:J8S+Ms89nvHX1F9Ma2m3KhdTl41ZhbFKMyAJqvDA+iE=
|
||||
github.com/go4org/hashtriemap v0.0.0-20260925222741-44e5305f85d9/go.mod h1:OGmRfY/9QEK2P5zCRtmqfbCF283xPkU2dvVA4MvbvpI=
|
||||
github.com/go4org/plan9netshell v0.0.0-20250324183649-788daa080737 h1:cf60tHxREO3g1nroKr2osU3JWZsJzkfi7rEg+oAB0Lo=
|
||||
github.com/go4org/plan9netshell v0.0.0-20250324183649-788daa080737/go.mod h1:MIS0jDzbU/vuM9MC4YnBITCv+RYuTRq8dJzmCrFsK9g=
|
||||
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
||||
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
github.com/google/nftables v0.3.0 h1:bkyZ0cbpVeMHXOrtlFc8ISmfVqq5gPJukoYieyVmITg=
|
||||
github.com/google/nftables v0.3.0/go.mod h1:BCp9FsrbF1Fn/Yu6CLUc9GGZFw/+hsxfluNXXmxBfRM=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU=
|
||||
github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo=
|
||||
github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=
|
||||
github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
|
||||
github.com/illarion/gonotify/v3 v3.0.2 h1:O7S6vcopHexutmpObkeWsnzMJt/r1hONIEogeVNmJMk=
|
||||
github.com/illarion/gonotify/v3 v3.0.2/go.mod h1:HWGPdPe817GfvY3w7cx6zkbzNZfi3QjcBm/wgVvEL1U=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20260901064844-234b97448fae h1:nXGg65fXsylSUTNNWwvHuQsXev7mhIzQTnZREPTbWzs=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20260901064844-234b97448fae/go.mod h1:tGfUTcnFYGYvVNCaZZhwlJySU/fQQxh9TmpsFzWXnnY=
|
||||
github.com/jellydator/ttlcache/v3 v3.4.1 h1:bOdXmXiycyK6E6Qjyuj5vl+/vU3SCOoDs8a86NbHjAQ=
|
||||
github.com/jellydator/ttlcache/v3 v3.4.1/go.mod h1:j7LO12PNghFg5+0v9budMAT4rDK4JY969jb9vOdOBBk=
|
||||
github.com/jsimonetti/rtnetlink v1.4.2 h1:Df9w9TZ3npHTyDn0Ev9e1uzmN2odmXd0QX+J5GTEn90=
|
||||
github.com/jsimonetti/rtnetlink v1.4.2/go.mod h1:92s6LJdE+1iOrw+F2/RO7LYI2Qd8pPpFNNUYW06gcoM=
|
||||
github.com/kamstrup/intmap v0.5.2 h1:qnwBm1mh4XAnW9W9Ue9tZtTff8pS6+s6iKF6JRIV2Dk=
|
||||
github.com/kamstrup/intmap v0.5.2/go.mod h1:gWUVWHKzWj8xpJVFf5GC0O26bWmv3GqdnIX/LMT6Aq4=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ=
|
||||
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk=
|
||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mdlayher/genetlink v1.4.0 h1:f/Xs7Y2T+GyX9b3dbiUhnLE9InGs5F9RxJ2JwBMl71o=
|
||||
github.com/mdlayher/genetlink v1.4.0/go.mod h1:d1hrKr8fwZU2JkcAtQUAzeTrI7nbgQSl+5k1cC0biSA=
|
||||
github.com/mdlayher/netlink v1.11.2 h1:HKh2jqe+omdSWcQ88nrT7INE61B0NXfiSPFdgL4YbNI=
|
||||
github.com/mdlayher/netlink v1.11.2/go.mod h1:uT2Yc/QLaZubzDpZIBi9d4GoeLwtp3x1AMeqSRrK2sA=
|
||||
github.com/mdlayher/sdnotify v1.0.0 h1:Ma9XeLVN/l0qpyx1tNeMSeTjCPH6NtuD6/N9XdTlQ3c=
|
||||
github.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE=
|
||||
github.com/mdlayher/socket v0.7.0 h1:qVREPVwtUMg17pwvveQxpurq0PVisMxi3FGgpsorMYQ=
|
||||
github.com/mdlayher/socket v0.7.0/go.mod h1:f7iKql2EK/rfsWYDKofKjk2Ig7I+6HQWdMIdn1tO4A4=
|
||||
github.com/miekg/dns v1.1.73 h1:uhT8nJxmTrPJYClxVxTCX+CVn6qnzSiybRk72Z6DgrE=
|
||||
github.com/miekg/dns v1.1.73/go.mod h1:RW2Obtfd5NZHvOFe3zYG0W8koWOQtAzyHaLo8vASBuQ=
|
||||
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
|
||||
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||
github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY=
|
||||
github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
||||
github.com/pires/go-proxyproto v0.15.0 h1:dTshmNbFm/D+0+sbrxUuddPOZ5Y0B7c5NhtsBkm6LqI=
|
||||
github.com/pires/go-proxyproto v0.15.0/go.mod h1:OXsCrKwrK2tXS9YrI5tkHx5xaQlO8FH3lFW76orFh24=
|
||||
github.com/pkg/sftp v1.13.11 h1:0N92SLTB8JqASJB14ZLHHzFnBV8mG9zw4K7jghEFWuE=
|
||||
github.com/pkg/sftp v1.13.11/go.mod h1:uNkH9roSXglNJqM+glJJi+TQXQUm0fXFWqCFmT8hsN0=
|
||||
github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo=
|
||||
github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM=
|
||||
github.com/prometheus/common v0.71.0 h1:9KDAKb7Mj3HEVKyFCK6Dc/HIwlBzZIN2l7/lrHl3KK8=
|
||||
github.com/prometheus/common v0.71.0/go.mod h1:CLJ5H8TEsGX8bl31BdMkfhIZ+QmZ9tBPPotUxUbfcmk=
|
||||
github.com/rogpeppe/go-internal v1.16.0 h1:O9DK+vNMDVGLr2BeZqmpLeMjiMNkuXfcqntWbZV6S5g=
|
||||
github.com/rogpeppe/go-internal v1.16.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs=
|
||||
github.com/safchain/ethtool v0.7.0 h1:rlJzfDetsVvT61uz8x1YIcFn12akMfuPulHtZjtb7Is=
|
||||
github.com/safchain/ethtool v0.7.0/go.mod h1:MenQKEjXdfkjD3mp2QdCk8B/hwvkrlOTm/FD4gTpFxQ=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||
github.com/studio-b12/gowebdav v0.13.0 h1:OcwSg6IQHOFNdYHn3bPOHwSE8looG8N56Y5xTT1asqQ=
|
||||
github.com/studio-b12/gowebdav v0.13.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
|
||||
github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d h1:JcGKBZAL7ePLwOhUdN8qGQZlP5GueEiIZwY7R62pejE=
|
||||
github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4=
|
||||
github.com/tailscale/gliderssh v0.3.4-0.20260716005906-1a0f895faf28 h1:Azz5ILxxVsHN/KjIu3wkJPAmmtiijucZw4Ax5Ye8n+s=
|
||||
github.com/tailscale/gliderssh v0.3.4-0.20260716005906-1a0f895faf28/go.mod h1:wn16Km1EZOX4UEAyaZa3dBwfFGOJ7neck40NcwosJUw=
|
||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4=
|
||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg=
|
||||
github.com/tailscale/golang-x-crypto v0.0.0-20260720160339-c86ca1284b96 h1:m9EAsCu/afUkg9YkknXaRqMPl/omkUNRol5yB7hbG0Y=
|
||||
github.com/tailscale/golang-x-crypto v0.0.0-20260720160339-c86ca1284b96/go.mod h1:NC3xRCu4UR+m4n6ix8b6oLLbHa820Y0StbOQEdWTDo0=
|
||||
github.com/tailscale/hujson v0.0.0-20260727124030-b80ff77dac4f h1:9hiVElpCmKzsBKQHkBqZ8LGzt82iLfM8egxr4sew+Ys=
|
||||
github.com/tailscale/hujson v0.0.0-20260727124030-b80ff77dac4f/go.mod h1:8/zr1Tv0+cKpVtGCEB/7YfRXr2TszsMxMXLaT8YuBgU=
|
||||
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 h1:uFsXVBE9Qr4ZoF094vE6iYTLDl0qCiKzYXlL6UeWObU=
|
||||
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
|
||||
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc h1:24heQPtnFR+yfntqhI3oAu9i27nEojcQ4NuBQOo5ZFA=
|
||||
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc/go.mod h1:f93CXfllFsO9ZQVq+Zocb1Gp4G5Fz0b0rXHLOzt/Djc=
|
||||
github.com/tailscale/web-client-prebuilt v0.0.0-20260917222731-e0ed2d0d0fea h1:ah5jt6ZnxMvNclt6kBBoLcMpLLjMA44PTsnov2y7gpU=
|
||||
github.com/tailscale/web-client-prebuilt v0.0.0-20260917222731-e0ed2d0d0fea/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
|
||||
github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6 h1:l10Gi6w9jxvinoiq15g8OToDdASBni4CyJOdHY1Hr8M=
|
||||
github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6/go.mod h1:ZXRML051h7o4OcI0d3AaILDIad/Xw0IkXaHM17dic1Y=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20260928213032-417aef361226 h1:v3Lpj2iHPWQDqeCwemQPz4fWweIEMLqBkwJqCjRyJQc=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20260928213032-417aef361226/go.mod h1:rUelGmuK4UnSJYM5gl5Mknp6YbwwcL8+VAPMhNYe+jg=
|
||||
github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e h1:zOGKqN5D5hHhiYUp091JqK7DPCqSARyUfduhGUY8Bek=
|
||||
github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg=
|
||||
github.com/tc-hib/winres v0.3.1 h1:CwRjEGrKdbi5CvZ4ID+iyVhgyfatxFoizjPhzez9Io4=
|
||||
github.com/tc-hib/winres v0.3.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk=
|
||||
github.com/u-root/u-root v0.16.0 h1:wY40O83MBVks97+Is0WlFlOPSwKQMIrWP9R1IsrExg8=
|
||||
github.com/u-root/u-root v0.16.0/go.mod h1:yL/XdSSW27PdGLgUh4MNRBy54mKM+TBLzpwiB4nwj90=
|
||||
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=
|
||||
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=
|
||||
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
|
||||
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek=
|
||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
|
||||
go4.org/netipx v0.0.0-20260823151212-3075585bcbeb h1:XBM4hvfwGAttkkiTIFfeigdfcL1xIfdKXqFdgiHGtDs=
|
||||
go4.org/netipx v0.0.0-20260823151212-3075585bcbeb/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
||||
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20260929172509-b39ff6d641ec h1:6gLejUU4OPMnmDEz8DkDkWMoyDj2Ny5iUy1Ze3raJhs=
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20260929172509-b39ff6d641ec/go.mod h1:HPze8vhfG6fO06AM+VSvxRm4E3+5Yk375mgrJ5M2z1E=
|
||||
golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba h1:Ck8QetSgk912qxWLMCKxd0in+aiyBQyDSMae6e/xmpU=
|
||||
golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba/go.mod h1:50RgIsmK7OwqzTTeqcSXQW8SswW0o8fRcDxmqGluJ8E=
|
||||
golang.org/x/exp/typeparams v0.0.0-20260811152304-ee035b5b010f h1:+lI8cDJ4uceLipg2f1ODay7fEuLkk0BIHXd6PB8icxo=
|
||||
golang.org/x/exp/typeparams v0.0.0-20260811152304-ee035b5b010f/go.mod h1:PqrXSW65cXDZH0k4IeUbhmg/bcAZDbzNz3byBpKCsXo=
|
||||
golang.org/x/image v0.46.0 h1:b1+oYj0Jbp6K5MDT4i4/eZpYlk3V8SJhhDKh6LBHAyQ=
|
||||
golang.org/x/image v0.46.0/go.mod h1:3B3W05VGVQyuXucLINLjXKrqISASfi4Xj+iCVkLMwew=
|
||||
golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
|
||||
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
|
||||
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
|
||||
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
|
||||
golang.org/x/oauth2 v0.37.0 h1:JUlcxA8oAtauLfiH8FX2/FkAWHAdi0QtGCGc+hofE98=
|
||||
golang.org/x/oauth2 v0.37.0/go.mod h1:IxwZNxUULJmpBFf9K/9NTMSIfZZuvuTy1gGxhigP/58=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
|
||||
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
|
||||
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
|
||||
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
|
||||
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
|
||||
golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
|
||||
golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
|
||||
golang.org/x/tools v0.50.0 h1:c2ifzfcuY7L90lZ2aKd8S4K2NpASF08SZx9ZuJkHmSU=
|
||||
golang.org/x/tools v0.50.0/go.mod h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20260522210424-ecfc5a8d5446 h1:cqHQ3AycTHvM2R7ikgyX57D+XvtcSnGylsLkOVhta/w=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20260522210424-ecfc5a8d5446/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
|
||||
golang.zx2c4.com/wireguard/windows v1.0.1 h1:eOxiDVbywPC+ZQqvdCK7x+ZwWXKbYv50TtH8ysFIbw8=
|
||||
golang.zx2c4.com/wireguard/windows v1.0.1/go.mod h1:+fbT3FFdX4zzYDLwJh5+HPEcNN/3HyNdzhNSVsQM+zs=
|
||||
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
|
||||
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gvisor.dev/gvisor v0.0.0-20260915211658-a6f909f08a72 h1:EytKYr5WrVs+Aah/0xDO8ZAZZzS/iTEVC8ln4ipDbL0=
|
||||
gvisor.dev/gvisor v0.0.0-20260915211658-a6f909f08a72/go.mod h1:8aLQqUBHDH8fY5y60lzmwDpMMbQCcT3EBfoSwhfaGCY=
|
||||
honnef.co/go/tools v0.8.1 h1:+JKf3xJ1ni4CwrhVg4/pqsfPGP6vNAXcKbMXJodYx3w=
|
||||
honnef.co/go/tools v0.8.1/go.mod h1:XA+OnlRA9EDh/ukGvXMNSZNKGwFQJ+5dER0ioUkOxks=
|
||||
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
|
||||
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||
software.sslmate.com/src/go-pkcs12 v0.7.3 h1:JBQD3FDqYjTeyDAeZQklj2ar88ykBLtALloPJHyAauU=
|
||||
software.sslmate.com/src/go-pkcs12 v0.7.3/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
|
||||
tailscale.com v1.104.0 h1:7LgglawekeutAexqXUzSxP/Kqo3HHwF/SkcbX5HpiU4=
|
||||
tailscale.com v1.104.0/go.mod h1:Cb1XjScRgSOeRD7NW6uCEznme782WiOU9IxqyTWoRf0=
|
||||
@@ -0,0 +1,9 @@
|
||||
//go:build !freebsd
|
||||
|
||||
package main
|
||||
|
||||
// On a development machine there is no stale-instance handling.
|
||||
|
||||
func stopRecordedInstance(pidFile string) int { return 0 }
|
||||
|
||||
func recordInstance(pidFile string) error { return nil }
|
||||
@@ -0,0 +1,74 @@
|
||||
//go:build freebsd
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Only one daemon may run: two would share one Tailscale identity and fight
|
||||
// over it. A new instance first asks the old one to stop through its status
|
||||
// page, but that page can be unreachable, so the daemon also records its
|
||||
// process id and a new instance stops whatever process holds it.
|
||||
//
|
||||
// Process ids start over after a reboot, and a stale file could then name an
|
||||
// unrelated process. The file therefore also holds the kernel's boot time, and
|
||||
// the pid is only trusted when that still matches.
|
||||
|
||||
// bootID identifies the current boot, or is empty if the kernel will not say.
|
||||
func bootID() string {
|
||||
raw, err := syscall.Sysctl("kern.boottime")
|
||||
if err != nil || raw == "" {
|
||||
return ""
|
||||
}
|
||||
return hex.EncodeToString([]byte(raw))
|
||||
}
|
||||
|
||||
// stopRecordedInstance stops the daemon named in the pid file, if it is
|
||||
// still running. It reports the pid it stopped, or 0.
|
||||
func stopRecordedInstance(pidFile string) int {
|
||||
b, err := os.ReadFile(pidFile)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
fields := strings.Fields(string(b))
|
||||
if len(fields) != 2 {
|
||||
return 0
|
||||
}
|
||||
pid, err := strconv.Atoi(fields[0])
|
||||
boot := bootID()
|
||||
if err != nil || pid <= 1 || pid == os.Getpid() || boot == "" || fields[1] != boot {
|
||||
return 0
|
||||
}
|
||||
if syscall.Kill(pid, 0) != nil {
|
||||
return 0 // already gone
|
||||
}
|
||||
|
||||
syscall.Kill(pid, syscall.SIGTERM)
|
||||
for i := 0; i < 40; i++ {
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
if syscall.Kill(pid, 0) != nil {
|
||||
return pid
|
||||
}
|
||||
}
|
||||
syscall.Kill(pid, syscall.SIGKILL)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
return pid
|
||||
}
|
||||
|
||||
// recordInstance writes this process to the pid file.
|
||||
func recordInstance(pidFile string) error {
|
||||
boot := bootID()
|
||||
if boot == "" {
|
||||
// Without a boot id the pid could not be trusted later.
|
||||
os.Remove(pidFile)
|
||||
return fmt.Errorf("kern.boottime is not available")
|
||||
}
|
||||
return os.WriteFile(pidFile, []byte(fmt.Sprintf("%d %s\n", os.Getpid(), boot)), 0o644)
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// resilientListener is a TCP listener that reopens itself when accepting
|
||||
// fails.
|
||||
//
|
||||
// On the PS5 a listening socket dies when the network is reconfigured, for
|
||||
// example when the connection settings are changed: accept returns the
|
||||
// Sony-specific errno 163 and never works again. Servers treat that as fatal
|
||||
// and stop. This listener closes the dead socket, listens on the same address
|
||||
// again and carries on, so the server on top of it never notices.
|
||||
type resilientListener struct {
|
||||
network string
|
||||
addr string
|
||||
logf func(format string, args ...any)
|
||||
|
||||
mu sync.Mutex
|
||||
ln net.Listener
|
||||
closed bool
|
||||
}
|
||||
|
||||
func listenResilient(network, addr string, logf func(string, ...any)) (*resilientListener, error) {
|
||||
ln, err := net.Listen(network, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Remember the concrete address, so that ":0" reopens on the same port.
|
||||
return &resilientListener{network: network, addr: ln.Addr().String(), logf: logf, ln: ln}, nil
|
||||
}
|
||||
|
||||
func (l *resilientListener) current() (net.Listener, bool) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
return l.ln, l.closed
|
||||
}
|
||||
|
||||
func (l *resilientListener) Accept() (net.Conn, error) {
|
||||
for {
|
||||
ln, closed := l.current()
|
||||
if closed {
|
||||
return nil, net.ErrClosed
|
||||
}
|
||||
c, err := ln.Accept()
|
||||
if err == nil {
|
||||
return c, nil
|
||||
}
|
||||
if _, closed := l.current(); closed {
|
||||
return nil, net.ErrClosed
|
||||
}
|
||||
l.logf("listener %s: %v; reopening", l.addr, err)
|
||||
ln.Close()
|
||||
if !l.reopen() {
|
||||
return nil, net.ErrClosed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// reopen listens again, retrying until it works or the listener is closed.
|
||||
func (l *resilientListener) reopen() bool {
|
||||
for delay := 250 * time.Millisecond; ; delay = min(2*delay, 5*time.Second) {
|
||||
time.Sleep(delay)
|
||||
if _, closed := l.current(); closed {
|
||||
return false
|
||||
}
|
||||
ln, err := net.Listen(l.network, l.addr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
l.mu.Lock()
|
||||
if l.closed {
|
||||
l.mu.Unlock()
|
||||
ln.Close()
|
||||
return false
|
||||
}
|
||||
l.ln = ln
|
||||
l.mu.Unlock()
|
||||
l.logf("listener %s: reopened", l.addr)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func (l *resilientListener) Close() error {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
l.closed = true
|
||||
return l.ln.Close()
|
||||
}
|
||||
|
||||
func (l *resilientListener) Addr() net.Addr {
|
||||
ln, _ := l.current()
|
||||
return ln.Addr()
|
||||
}
|
||||
|
||||
// port returns the TCP port the listener is bound to.
|
||||
func (l *resilientListener) port() uint16 {
|
||||
if a, ok := l.Addr().(*net.TCPAddr); ok {
|
||||
return uint16(a.Port)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Local forwards go the opposite way from forwardToLocalhost: they let apps
|
||||
// on the console reach a host on the tailnet.
|
||||
//
|
||||
// Tailscale runs inside this process, not in the PS5's network stack, so
|
||||
// other apps cannot open connections to tailnet addresses. A forward listens
|
||||
// on a localhost port and relays to a tailnet host; the app connects to
|
||||
// 127.0.0.1 instead. TCP and UDP are both supported, which is what game
|
||||
// streaming (Moonlight to a Sunshine host) needs.
|
||||
|
||||
// forwardRule is one local forward.
|
||||
type forwardRule struct {
|
||||
Proto string `json:"proto"` // "tcp" or "udp"
|
||||
Listen string `json:"listen"` // local address, e.g. "127.0.0.1:47989"
|
||||
Target string `json:"target"` // tailnet host and port, e.g. "my-pc:47989"
|
||||
}
|
||||
|
||||
func (r forwardRule) String() string {
|
||||
return fmt.Sprintf("%s %s -> %s", r.Proto, r.Listen, r.Target)
|
||||
}
|
||||
|
||||
// Ports a Sunshine host uses with its default base port (47989).
|
||||
var (
|
||||
sunshineTCPPorts = []int{47984, 47989, 48010} // HTTPS, HTTP, RTSP
|
||||
sunshineUDPPorts = []int{47998, 47999, 48000, 48002} // video, control, audio, microphone
|
||||
)
|
||||
|
||||
// sunshineRules returns the forwards that make the Sunshine host on the
|
||||
// tailnet appear on 127.0.0.1 to a Moonlight client on the console.
|
||||
func sunshineRules(host string) []forwardRule {
|
||||
if host == "" {
|
||||
return nil
|
||||
}
|
||||
var rules []forwardRule
|
||||
for _, p := range sunshineTCPPorts {
|
||||
port := strconv.Itoa(p)
|
||||
rules = append(rules, forwardRule{"tcp", net.JoinHostPort("127.0.0.1", port), net.JoinHostPort(host, port)})
|
||||
}
|
||||
for _, p := range sunshineUDPPorts {
|
||||
port := strconv.Itoa(p)
|
||||
rules = append(rules, forwardRule{"udp", net.JoinHostPort("127.0.0.1", port), net.JoinHostPort(host, port)})
|
||||
}
|
||||
return rules
|
||||
}
|
||||
|
||||
type dialFunc func(ctx context.Context, network, addr string) (net.Conn, error)
|
||||
|
||||
// forwarder runs a set of local forwards.
|
||||
type forwarder struct {
|
||||
dial dialFunc
|
||||
logf func(format string, args ...any)
|
||||
|
||||
mu sync.Mutex
|
||||
active []forwardRule
|
||||
closers []func()
|
||||
tcpPorts map[uint16]bool
|
||||
}
|
||||
|
||||
func newForwarder(dial dialFunc, logf func(string, ...any)) *forwarder {
|
||||
return &forwarder{dial: dial, logf: logf, tcpPorts: map[uint16]bool{}}
|
||||
}
|
||||
|
||||
// set replaces the running forwards with rules. Rules that cannot be started
|
||||
// are skipped and reported.
|
||||
func (f *forwarder) set(rules []forwardRule) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
for _, c := range f.closers {
|
||||
c()
|
||||
}
|
||||
f.closers, f.active, f.tcpPorts = nil, nil, map[uint16]bool{}
|
||||
|
||||
var errs []error
|
||||
for _, r := range rules {
|
||||
var stop func()
|
||||
var err error
|
||||
switch r.Proto {
|
||||
case "tcp":
|
||||
stop, err = f.startTCP(r)
|
||||
case "udp":
|
||||
stop, err = f.startUDP(r)
|
||||
default:
|
||||
err = fmt.Errorf("unknown protocol %q", r.Proto)
|
||||
}
|
||||
if err != nil {
|
||||
f.logf("forward %v: %v", r, err)
|
||||
errs = append(errs, fmt.Errorf("%v: %w", r, err))
|
||||
continue
|
||||
}
|
||||
f.closers = append(f.closers, stop)
|
||||
f.active = append(f.active, r)
|
||||
}
|
||||
if len(f.active) > 0 {
|
||||
f.logf("local forwards active: %d", len(f.active))
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
func (f *forwarder) rules() []forwardRule {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
return append([]forwardRule(nil), f.active...)
|
||||
}
|
||||
|
||||
// listensOnTCP reports whether a local forward owns the TCP port.
|
||||
func (f *forwarder) listensOnTCP(port uint16) bool {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
return f.tcpPorts[port]
|
||||
}
|
||||
|
||||
func (f *forwarder) startTCP(r forwardRule) (stop func(), err error) {
|
||||
ln, err := listenResilient("tcp", r.Listen, f.logf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f.tcpPorts[ln.port()] = true
|
||||
go func() {
|
||||
for {
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go f.serveTCP(c, r)
|
||||
}
|
||||
}()
|
||||
return func() { ln.Close() }, nil
|
||||
}
|
||||
|
||||
func (f *forwarder) serveTCP(c net.Conn, r forwardRule) {
|
||||
defer c.Close()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
up, err := f.dial(ctx, "tcp", r.Target)
|
||||
cancel()
|
||||
if err != nil {
|
||||
f.logf("forward %v: %v", r, err)
|
||||
return
|
||||
}
|
||||
defer up.Close()
|
||||
pipe(c, up)
|
||||
}
|
||||
|
||||
// UDP flows that have been silent this long are forgotten.
|
||||
const udpIdleTimeout = 2 * time.Minute
|
||||
|
||||
// udpFlow is the relay state for one local client address.
|
||||
type udpFlow struct {
|
||||
out chan []byte // datagrams from the client waiting to go upstream
|
||||
lastSeen atomic.Int64
|
||||
}
|
||||
|
||||
func (fl *udpFlow) touch() { fl.lastSeen.Store(time.Now().UnixNano()) }
|
||||
|
||||
func (fl *udpFlow) idle() bool {
|
||||
return time.Since(time.Unix(0, fl.lastSeen.Load())) > udpIdleTimeout
|
||||
}
|
||||
|
||||
func (f *forwarder) startUDP(r forwardRule) (stop func(), err error) {
|
||||
pc, err := net.ListenPacket("udp", r.Listen)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var (
|
||||
mu sync.Mutex
|
||||
current = pc
|
||||
closed bool
|
||||
flows = map[string]*udpFlow{}
|
||||
)
|
||||
socket := func() (net.PacketConn, bool) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
return current, closed
|
||||
}
|
||||
|
||||
go func() {
|
||||
buf := make([]byte, 65535)
|
||||
for {
|
||||
sock, stopped := socket()
|
||||
if stopped {
|
||||
return
|
||||
}
|
||||
n, from, err := sock.ReadFrom(buf)
|
||||
if err != nil {
|
||||
if _, stopped := socket(); stopped {
|
||||
return
|
||||
}
|
||||
// Like TCP listeners, a UDP socket can die when the
|
||||
// PS5's network is reconfigured. Open a new one.
|
||||
f.logf("forward %v: %v; reopening", r, err)
|
||||
sock.Close()
|
||||
time.Sleep(time.Second)
|
||||
if reopened, err := net.ListenPacket("udp", r.Listen); err == nil {
|
||||
mu.Lock()
|
||||
if closed {
|
||||
reopened.Close()
|
||||
} else {
|
||||
current = reopened
|
||||
}
|
||||
mu.Unlock()
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
key := from.String()
|
||||
mu.Lock()
|
||||
fl := flows[key]
|
||||
if fl == nil {
|
||||
fl = &udpFlow{out: make(chan []byte, 256)}
|
||||
flows[key] = fl
|
||||
go f.serveUDPFlow(r, fl, from, socket, func() {
|
||||
mu.Lock()
|
||||
if flows[key] == fl {
|
||||
delete(flows, key)
|
||||
}
|
||||
mu.Unlock()
|
||||
})
|
||||
}
|
||||
mu.Unlock()
|
||||
|
||||
fl.touch()
|
||||
select {
|
||||
case fl.out <- append([]byte(nil), buf[:n]...):
|
||||
default: // upstream is not keeping up; UDP may drop
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return func() {
|
||||
mu.Lock()
|
||||
closed = true
|
||||
current.Close()
|
||||
mu.Unlock()
|
||||
}, nil
|
||||
}
|
||||
|
||||
// serveUDPFlow relays one client's datagrams to the target and the replies
|
||||
// back, until the flow goes quiet or the forward is stopped.
|
||||
func (f *forwarder) serveUDPFlow(r forwardRule, fl *udpFlow, client net.Addr, socket func() (net.PacketConn, bool), done func()) {
|
||||
defer done()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
up, err := f.dial(ctx, "udp", r.Target)
|
||||
cancel()
|
||||
if err != nil {
|
||||
f.logf("forward %v: %v", r, err)
|
||||
return
|
||||
}
|
||||
defer up.Close()
|
||||
|
||||
// Replies: target -> client.
|
||||
go func() {
|
||||
buf := make([]byte, 65535)
|
||||
for {
|
||||
up.SetReadDeadline(time.Now().Add(udpIdleTimeout))
|
||||
n, err := up.Read(buf)
|
||||
if err != nil {
|
||||
var ne net.Error
|
||||
if errors.As(err, &ne) && ne.Timeout() && !fl.idle() {
|
||||
continue
|
||||
}
|
||||
return
|
||||
}
|
||||
fl.touch()
|
||||
if pc, closed := socket(); !closed {
|
||||
pc.WriteTo(buf[:n], client)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
idle := time.NewTicker(udpIdleTimeout / 4)
|
||||
defer idle.Stop()
|
||||
for {
|
||||
select {
|
||||
case b := <-fl.out:
|
||||
if _, err := up.Write(b); err != nil {
|
||||
return
|
||||
}
|
||||
case <-idle.C:
|
||||
if _, closed := socket(); closed || fl.idle() {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// startEcho runs a TCP and a UDP echo server on the same port and returns it.
|
||||
func startEcho(t *testing.T) string {
|
||||
t.Helper()
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { ln.Close() })
|
||||
go func() {
|
||||
for {
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go func() { io.Copy(c, c); c.Close() }()
|
||||
}
|
||||
}()
|
||||
pc, err := net.ListenPacket("udp", ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { pc.Close() })
|
||||
go func() {
|
||||
buf := make([]byte, 65535)
|
||||
for {
|
||||
n, from, err := pc.ReadFrom(buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
pc.WriteTo(append([]byte("echo:"), buf[:n]...), from)
|
||||
}
|
||||
}()
|
||||
return ln.Addr().String()
|
||||
}
|
||||
|
||||
// freePort returns a localhost address nothing listens on.
|
||||
func freePort(t *testing.T) string {
|
||||
t.Helper()
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ln.Close()
|
||||
return ln.Addr().String()
|
||||
}
|
||||
|
||||
func TestLocalForward(t *testing.T) {
|
||||
echo := startEcho(t)
|
||||
listen := freePort(t)
|
||||
|
||||
// The "tailnet" is the loopback interface; record what gets dialed.
|
||||
var dialed []string
|
||||
dial := func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
dialed = append(dialed, network+" "+addr)
|
||||
var d net.Dialer
|
||||
return d.DialContext(ctx, network, echo)
|
||||
}
|
||||
f := newForwarder(dial, t.Logf)
|
||||
err := f.set([]forwardRule{
|
||||
{"tcp", listen, "sunshine-host:47989"},
|
||||
{"udp", listen, "sunshine-host:47998"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.set(nil)
|
||||
|
||||
_, port, _ := net.SplitHostPort(listen)
|
||||
if got := f.rules(); len(got) != 2 {
|
||||
t.Fatalf("active rules: %v", got)
|
||||
}
|
||||
var portNum uint16
|
||||
for _, c := range port {
|
||||
portNum = portNum*10 + uint16(c-'0')
|
||||
}
|
||||
if !f.listensOnTCP(portNum) {
|
||||
t.Errorf("listensOnTCP(%d) = false", portNum)
|
||||
}
|
||||
|
||||
// TCP: data and the half-close go through.
|
||||
c, err := net.Dial("tcp", listen)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c.Write([]byte("hello"))
|
||||
c.(*net.TCPConn).CloseWrite()
|
||||
c.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||
b, err := io.ReadAll(c)
|
||||
c.Close()
|
||||
if err != nil || string(b) != "hello" {
|
||||
t.Fatalf("tcp: got %q, %v", b, err)
|
||||
}
|
||||
|
||||
// UDP: several datagrams from one client share a flow and come back to it.
|
||||
u, err := net.Dial("udp", listen)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer u.Close()
|
||||
for _, msg := range []string{"one", "two", "three"} {
|
||||
u.Write([]byte(msg))
|
||||
buf := make([]byte, 100)
|
||||
u.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||
n, err := u.Read(buf)
|
||||
if err != nil || string(buf[:n]) != "echo:"+msg {
|
||||
t.Fatalf("udp %q: got %q, %v", msg, buf[:n], err)
|
||||
}
|
||||
}
|
||||
|
||||
want := "tcp sunshine-host:47989,udp sunshine-host:47998"
|
||||
if got := strings.Join(dialed, ","); got != want {
|
||||
t.Errorf("dialed %q, want %q (one dial per TCP connection and per UDP client)", got, want)
|
||||
}
|
||||
|
||||
// Replacing the rules frees the ports.
|
||||
if err := f.set(nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c, err := net.DialTimeout("tcp", listen, time.Second); err == nil {
|
||||
c.Close()
|
||||
t.Errorf("TCP forward still accepting after it was removed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSunshineRules(t *testing.T) {
|
||||
if got := sunshineRules(""); got != nil {
|
||||
t.Errorf("no host: got %v", got)
|
||||
}
|
||||
rules := sunshineRules("gaming-pc")
|
||||
if len(rules) != 7 {
|
||||
t.Fatalf("got %d rules, want 7", len(rules))
|
||||
}
|
||||
if got, want := rules[0].String(), "tcp 127.0.0.1:47984 -> gaming-pc:47984"; got != want {
|
||||
t.Errorf("first rule %q, want %q", got, want)
|
||||
}
|
||||
for _, r := range rules {
|
||||
if !strings.HasPrefix(r.Listen, "127.0.0.1:") {
|
||||
t.Errorf("%v does not listen on localhost only", r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsLocalDestination(t *testing.T) {
|
||||
for addr, want := range map[string]bool{
|
||||
"127.0.0.1:8080": true,
|
||||
"localhost:80": true,
|
||||
"192.168.1.50:2121": true,
|
||||
"10.0.0.5:443": true,
|
||||
"[::1]:80": true,
|
||||
"100.64.0.5:8090": false, // tailnet address
|
||||
"my-pc.tailnet.ts.net:80": false,
|
||||
"example.com:443": false,
|
||||
} {
|
||||
if got := isLocalDestination(addr); got != want {
|
||||
t.Errorf("isLocalDestination(%q) = %v, want %v", addr, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A listener that dies must be reopened without the server noticing.
|
||||
func TestResilientListenerReopens(t *testing.T) {
|
||||
ln, err := listenResilient("tcp", "127.0.0.1:0", t.Logf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ln.Close()
|
||||
addr := ln.Addr().String()
|
||||
|
||||
accepted := make(chan net.Conn, 4)
|
||||
go func() {
|
||||
for {
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
accepted <- c
|
||||
}
|
||||
}()
|
||||
|
||||
connect := func() {
|
||||
t.Helper()
|
||||
var c net.Conn
|
||||
var err error
|
||||
for i := 0; i < 50; i++ {
|
||||
if c, err = net.DialTimeout("tcp", addr, time.Second); err == nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("dial: %v", err)
|
||||
}
|
||||
c.Close()
|
||||
select {
|
||||
case s := <-accepted:
|
||||
s.Close()
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("connection was not accepted")
|
||||
}
|
||||
}
|
||||
|
||||
connect()
|
||||
// Kill the socket underneath, as a network reconfiguration would.
|
||||
inner, _ := ln.current()
|
||||
inner.Close()
|
||||
connect()
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const maxLogSize = 2 << 20
|
||||
|
||||
// openLog opens the log file and points stdout and stderr at it, so that Go
|
||||
// runtime crash output is kept too. A large log is rotated once at startup.
|
||||
func openLog(path string) (*os.File, error) {
|
||||
if fi, err := os.Stat(path); err == nil && fi.Size() > maxLogSize {
|
||||
os.Rename(path, path+".old")
|
||||
}
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
redirectStdio(f)
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// newLogger returns a logf that writes timestamped lines to the log file and
|
||||
// keeps the most recent ones for the status page.
|
||||
func newLogger(f *os.File) func(format string, args ...any) {
|
||||
var mu sync.Mutex
|
||||
return func(format string, args ...any) {
|
||||
line := time.Now().Format("2006-01-02 15:04:05 ") + fmt.Sprintf(format, args...)
|
||||
mu.Lock()
|
||||
fmt.Fprintln(f, line)
|
||||
mu.Unlock()
|
||||
recentLogs.add(line)
|
||||
}
|
||||
}
|
||||
|
||||
// logRing keeps the last lines of the log in memory.
|
||||
type logRing struct {
|
||||
mu sync.Mutex
|
||||
lines []string
|
||||
}
|
||||
|
||||
var recentLogs logRing
|
||||
|
||||
func (r *logRing) add(line string) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.lines = append(r.lines, line)
|
||||
if len(r.lines) > 200 {
|
||||
r.lines = r.lines[len(r.lines)-200:]
|
||||
}
|
||||
}
|
||||
|
||||
func (r *logRing) snapshot() []string {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return append([]string(nil), r.lines...)
|
||||
}
|
||||
+395
@@ -0,0 +1,395 @@
|
||||
// Command ps5tailscale runs Tailscale on a jailbroken PS5.
|
||||
//
|
||||
// The PS5 kernel has no tunnel device, so Tailscale runs entirely in
|
||||
// userspace (tsnet + netstack). The console joins the tailnet as a node, and
|
||||
// every TCP connection that arrives for it over the tailnet is handed to the
|
||||
// matching port on localhost. That makes whatever is listening on the console
|
||||
// (FTP, the payload loader, web servers, ...) reachable from the tailnet.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
_ "golang.org/x/crypto/x509roots/fallback" // the PS5 has no system CA bundle
|
||||
|
||||
"tailscale.com/client/local"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tsnet"
|
||||
)
|
||||
|
||||
// version is shown on the status page. Set at build time with -ldflags -X.
|
||||
var version = "dev"
|
||||
|
||||
// forceVerbose turns on Tailscale's own logging regardless of the config
|
||||
// file. Set to "1" at build time with -ldflags -X for debugging builds.
|
||||
var forceVerbose = ""
|
||||
|
||||
// dataDir holds the config, the log and Tailscale's state. PS5TS_DATA overrides
|
||||
// it when testing on a development machine.
|
||||
var dataDir = func() string {
|
||||
if d := os.Getenv("PS5TS_DATA"); d != "" {
|
||||
return d
|
||||
}
|
||||
return "/data/tailscale"
|
||||
}()
|
||||
|
||||
func main() {
|
||||
console := newConsole()
|
||||
if err := os.MkdirAll(dataDir, 0o755); err != nil {
|
||||
console.Printf("tailscale: cannot create %s: %v\n", dataDir, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
logFile, err := openLog(filepath.Join(dataDir, "tailscale.log"))
|
||||
if err != nil {
|
||||
console.Printf("tailscale: cannot open log: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
logf := newLogger(logFile)
|
||||
|
||||
cfg, err := loadConfig(filepath.Join(dataDir, "config.json"))
|
||||
if err != nil {
|
||||
logf("config: %v (using defaults)", err)
|
||||
}
|
||||
logf("ps5-tailscale %s starting, hostname %q, web UI on %s", version, cfg.Hostname, cfg.WebAddr)
|
||||
|
||||
// A payload that is sent again replaces the running instance, which is
|
||||
// how an upgrade or a restart is done.
|
||||
if stopRunningInstance(cfg.WebAddr) {
|
||||
logf("stopped the instance that was already running")
|
||||
}
|
||||
// The status page of the old instance may have been unreachable, so
|
||||
// also go by the process it recorded.
|
||||
pidFile := filepath.Join(dataDir, "tailscale.pid")
|
||||
if pid := stopRecordedInstance(pidFile); pid != 0 {
|
||||
logf("stopped a previous instance that was still running (pid %d)", pid)
|
||||
}
|
||||
if err := recordInstance(pidFile); err != nil {
|
||||
logf("pid file: %v", err)
|
||||
}
|
||||
|
||||
// Everything in the main log also goes to the debug log, so that it
|
||||
// reads as one timeline with Tailscale's own messages.
|
||||
debug := openDebugLog(filepath.Join(dataDir, "tailscale-debug.log"), 4<<20)
|
||||
mainLogf := logf
|
||||
logf = func(format string, args ...any) {
|
||||
mainLogf(format, args...)
|
||||
debug.Printf(format, args...)
|
||||
}
|
||||
|
||||
d := &daemon{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)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
type daemon struct {
|
||||
cfg config // guarded by mu once the web UI is up
|
||||
cfgPath string
|
||||
debug *debugLog
|
||||
logf func(format string, args ...any)
|
||||
console *console
|
||||
started time.Time
|
||||
|
||||
srv *tsnet.Server
|
||||
lc *local.Client
|
||||
fwd *forwarder
|
||||
|
||||
mu sync.Mutex
|
||||
state string // ipn backend state, e.g. "NeedsLogin", "Running"
|
||||
authURL string
|
||||
lastErr string
|
||||
notified string // last state the user was notified about
|
||||
|
||||
lastTsnetMsg string
|
||||
proxyPort uint16 // port of the outbound HTTP proxy, 0 if disabled
|
||||
webPort uint16 // port of the status page
|
||||
lastRelogin time.Time
|
||||
|
||||
quit chan struct{}
|
||||
quitOnce sync.Once
|
||||
}
|
||||
|
||||
func (d *daemon) run() error {
|
||||
d.quit = make(chan struct{})
|
||||
|
||||
d.srv = &tsnet.Server{
|
||||
Dir: filepath.Join(dataDir, "state"),
|
||||
Hostname: d.cfg.Hostname,
|
||||
AuthKey: d.cfg.AuthKey,
|
||||
UserLogf: d.tsnetLogf,
|
||||
}
|
||||
if d.cfg.ControlURL != "" {
|
||||
d.srv.ControlURL = d.cfg.ControlURL
|
||||
}
|
||||
// Tailscale's own log always goes to the debug log; the main log only
|
||||
// gets it when asked.
|
||||
if d.cfg.Verbose || forceVerbose == "1" {
|
||||
d.srv.Logf = func(format string, args ...any) { d.logf("ts: "+format, args...) }
|
||||
} else {
|
||||
d.srv.Logf = func(format string, args ...any) { d.debug.Printf("ts: "+format, args...) }
|
||||
}
|
||||
|
||||
// The web UI comes up first so that it can show progress and errors even
|
||||
// if Tailscale itself has trouble starting.
|
||||
d.fwd = newForwarder(d.dialTailnet, d.logf)
|
||||
webLn, err := listenResilient("tcp", d.cfg.WebAddr, d.logf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("web UI: %w", err)
|
||||
}
|
||||
d.webPort = webLn.port()
|
||||
go d.serveWeb(webLn)
|
||||
|
||||
if err := d.srv.Start(); err != nil {
|
||||
return fmt.Errorf("starting tailscale: %w", err)
|
||||
}
|
||||
d.lc, err = d.srv.LocalClient()
|
||||
if err != nil {
|
||||
return fmt.Errorf("local client: %w", err)
|
||||
}
|
||||
|
||||
if d.cfg.HTTPProxyAddr != "" {
|
||||
if ln, err := listenResilient("tcp", d.cfg.HTTPProxyAddr, d.logf); err != nil {
|
||||
d.logf("http proxy: %v", err)
|
||||
} else {
|
||||
d.proxyPort = ln.port()
|
||||
go d.serveProxy(ln)
|
||||
}
|
||||
}
|
||||
|
||||
d.fwd.set(d.localForwardRules())
|
||||
|
||||
d.srv.RegisterFallbackTCPHandler(d.forwardToLocalhost)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
go d.watch(ctx)
|
||||
go d.recoverLogin(ctx)
|
||||
|
||||
sigc := make(chan os.Signal, 1)
|
||||
signal.Notify(sigc, syscall.SIGTERM, syscall.SIGINT)
|
||||
select {
|
||||
case <-d.quit:
|
||||
d.logf("stop requested")
|
||||
case s := <-sigc:
|
||||
d.logf("received %v", s)
|
||||
}
|
||||
cancel()
|
||||
webLn.Close()
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
d.srv.Close()
|
||||
close(done)
|
||||
}()
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(5 * time.Second):
|
||||
d.logf("shutdown timed out")
|
||||
}
|
||||
d.logf("stopped")
|
||||
return nil
|
||||
}
|
||||
|
||||
// dialTailnet opens a connection through Tailscale.
|
||||
func (d *daemon) dialTailnet(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return d.srv.Dial(ctx, network, addr)
|
||||
}
|
||||
|
||||
// localForwardRules returns the local forwards the config asks for.
|
||||
func (d *daemon) localForwardRules() []forwardRule {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
return append(sunshineRules(d.cfg.SunshineHost), d.cfg.Forwards...)
|
||||
}
|
||||
|
||||
// tsnetLogf receives tsnet's messages for the user. While it waits for a
|
||||
// login it repeats the same line every few seconds, which would drown the
|
||||
// log, so a message is only logged again when it changes.
|
||||
func (d *daemon) tsnetLogf(format string, args ...any) {
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
d.mu.Lock()
|
||||
repeat := msg == d.lastTsnetMsg
|
||||
d.lastTsnetMsg = msg
|
||||
d.mu.Unlock()
|
||||
if !repeat {
|
||||
d.logf("tsnet: %s", msg)
|
||||
}
|
||||
}
|
||||
|
||||
// watch follows the backend state and tells the user, on screen, when
|
||||
// something needs their attention.
|
||||
func (d *daemon) watch(ctx context.Context) {
|
||||
for ctx.Err() == nil {
|
||||
err := d.watchOnce(ctx)
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
d.logf("state watcher: %v; retrying", err)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-time.After(3 * time.Second):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *daemon) watchOnce(ctx context.Context) error {
|
||||
w, err := d.lc.WatchIPNBus(ctx, ipn.NotifyInitialState)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer w.Close()
|
||||
for {
|
||||
n, err := w.Next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n.ErrMessage != nil {
|
||||
d.logf("backend error: %s", *n.ErrMessage)
|
||||
d.mu.Lock()
|
||||
d.lastErr = *n.ErrMessage
|
||||
d.mu.Unlock()
|
||||
}
|
||||
if n.BrowseToURL != nil && *n.BrowseToURL != "" {
|
||||
d.setAuthURL(*n.BrowseToURL)
|
||||
}
|
||||
if n.State != nil {
|
||||
d.setState(ctx, n.State.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// freshLogin abandons whatever login is pending and starts a new one, which
|
||||
// produces a new login link.
|
||||
func (d *daemon) freshLogin(ctx context.Context) error {
|
||||
d.mu.Lock()
|
||||
d.authURL = ""
|
||||
d.lastRelogin = time.Now()
|
||||
d.mu.Unlock()
|
||||
if err := d.lc.Logout(ctx); err != nil {
|
||||
d.logf("fresh login: logout: %v", err)
|
||||
}
|
||||
return d.lc.StartLoginInteractive(ctx)
|
||||
}
|
||||
|
||||
// recoverLogin gets the daemon out of a dead-end seen on the console: the
|
||||
// user completes the login in the browser, but the confirmation never
|
||||
// arrives and Tailscale's retry is answered with "auth path not found". The
|
||||
// backend then keeps offering the used-up link forever. When that error
|
||||
// shows up, start over with a new link.
|
||||
func (d *daemon) recoverLogin(ctx context.Context) {
|
||||
ticker := time.NewTicker(15 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
}
|
||||
d.mu.Lock()
|
||||
waiting := d.state == "NeedsLogin" && time.Since(d.lastRelogin) > time.Minute
|
||||
d.mu.Unlock()
|
||||
if !waiting {
|
||||
continue
|
||||
}
|
||||
st, err := d.status(ctx)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, h := range st.Health {
|
||||
if strings.Contains(h, "auth path not found") {
|
||||
d.logf("the pending login link is no longer valid; requesting a new one")
|
||||
if err := d.freshLogin(ctx); err != nil {
|
||||
d.logf("fresh login: %v", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *daemon) setAuthURL(u string) {
|
||||
d.mu.Lock()
|
||||
changed := d.authURL != u
|
||||
d.authURL = u
|
||||
d.mu.Unlock()
|
||||
if !changed {
|
||||
return
|
||||
}
|
||||
d.logf("login URL: %s", u)
|
||||
d.console.Printf("\nTo connect this PS5 to your tailnet, open:\n\n %s\n\n(also shown at %s)\n", u, d.webURL())
|
||||
notify("Tailscale needs you to log in.\nOpen %s on a phone or PC.", d.webURL())
|
||||
}
|
||||
|
||||
func (d *daemon) setState(ctx context.Context, state string) {
|
||||
d.mu.Lock()
|
||||
prev := d.state
|
||||
d.state = state
|
||||
if state == "Running" {
|
||||
d.authURL = ""
|
||||
d.lastErr = ""
|
||||
}
|
||||
already := d.notified == state
|
||||
d.notified = state
|
||||
d.mu.Unlock()
|
||||
if prev != state {
|
||||
d.logf("state: %s -> %s", prev, state)
|
||||
}
|
||||
if already {
|
||||
return
|
||||
}
|
||||
|
||||
switch state {
|
||||
case "Running":
|
||||
name, ip := d.cfg.Hostname, ""
|
||||
if st, err := d.status(ctx); err == nil && st.Self != nil {
|
||||
if st.Self.DNSName != "" {
|
||||
name = strings.TrimSuffix(st.Self.DNSName, ".")
|
||||
}
|
||||
if len(st.Self.TailscaleIPs) > 0 {
|
||||
ip = st.Self.TailscaleIPs[0].String()
|
||||
}
|
||||
}
|
||||
d.console.Printf("Tailscale is connected: %s %s\n", name, ip)
|
||||
notify("Tailscale connected\n%s\n%s", name, ip)
|
||||
case "NeedsMachineAuth":
|
||||
notify("Tailscale: this PS5 is waiting for approval in the admin console.")
|
||||
}
|
||||
}
|
||||
|
||||
func (d *daemon) status(ctx context.Context) (*ipnstate.Status, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
return d.lc.Status(ctx)
|
||||
}
|
||||
|
||||
// webURL is the address of the status page as seen from the local network.
|
||||
func (d *daemon) webURL() string {
|
||||
_, port, _ := net.SplitHostPort(d.cfg.WebAddr)
|
||||
return "http://" + net.JoinHostPort(lanIP(), port)
|
||||
}
|
||||
|
||||
// lanIP returns the console's address on the local network.
|
||||
func lanIP() string {
|
||||
// No packet is sent; connecting a UDP socket only selects a route.
|
||||
c, err := net.Dial("udp4", "192.0.2.1:9")
|
||||
if err != nil {
|
||||
return "127.0.0.1"
|
||||
}
|
||||
defer c.Close()
|
||||
if a, ok := c.LocalAddr().(*net.UDPAddr); ok && !a.IP.IsUnspecified() {
|
||||
return a.IP.String()
|
||||
}
|
||||
return "127.0.0.1"
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"time"
|
||||
)
|
||||
|
||||
// serveProxy runs a plain HTTP proxy whose outbound connections go through
|
||||
// Tailscale's dialer: tailnet addresses and MagicDNS names are reached over
|
||||
// the tailnet, everything else goes out directly. Setting it as the proxy
|
||||
// server in the PS5's network settings lets the console's own apps, such as
|
||||
// the browser, open tailnet hosts.
|
||||
func (d *daemon) serveProxy(ln net.Listener) {
|
||||
d.logf("http proxy listening on %s", ln.Addr())
|
||||
transport := &http.Transport{
|
||||
DialContext: d.proxyDial,
|
||||
ForceAttemptHTTP2: false,
|
||||
MaxIdleConns: 32,
|
||||
IdleConnTimeout: 60 * time.Second,
|
||||
ResponseHeaderTimeout: 60 * time.Second,
|
||||
}
|
||||
srv := &http.Server{
|
||||
ReadHeaderTimeout: 15 * time.Second,
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodConnect {
|
||||
d.proxyConnect(w, r)
|
||||
return
|
||||
}
|
||||
if !r.URL.IsAbs() {
|
||||
http.Error(w, "this is a proxy; requests must use an absolute URL", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
r.RequestURI = ""
|
||||
r.Header.Del("Proxy-Connection")
|
||||
r.Header.Del("Proxy-Authorization")
|
||||
resp, err := transport.RoundTrip(r)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
for k, vs := range resp.Header {
|
||||
for _, v := range vs {
|
||||
w.Header().Add(k, v)
|
||||
}
|
||||
}
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
io.Copy(w, resp.Body)
|
||||
}),
|
||||
}
|
||||
if err := srv.Serve(ln); err != nil && err != http.ErrServerClosed && !d.stopping() {
|
||||
d.logf("http proxy stopped: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// proxyDial connects on behalf of a proxy client. Tailscale's dialer handles
|
||||
// tailnet addresses and names but waits until Tailscale is connected, so it
|
||||
// is only used once that is the case, and never for the console's own or
|
||||
// LAN addresses: those must keep working whatever state Tailscale is in.
|
||||
func (d *daemon) proxyDial(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
d.mu.Lock()
|
||||
running := d.state == "Running"
|
||||
d.mu.Unlock()
|
||||
if running && !isLocalDestination(addr) {
|
||||
return d.srv.Dial(ctx, network, addr)
|
||||
}
|
||||
var direct net.Dialer
|
||||
return direct.DialContext(ctx, network, addr)
|
||||
}
|
||||
|
||||
// isLocalDestination reports whether addr is a literal loopback, private or
|
||||
// link-local address, or "localhost".
|
||||
func isLocalDestination(addr string) bool {
|
||||
host, _, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
host = addr
|
||||
}
|
||||
if host == "localhost" {
|
||||
return true
|
||||
}
|
||||
ip, err := netip.ParseAddr(host)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || ip.IsUnspecified()
|
||||
}
|
||||
|
||||
func (d *daemon) proxyConnect(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
upstream, err := d.proxyDial(ctx, "tcp", r.Host)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
defer upstream.Close()
|
||||
|
||||
hj, ok := w.(http.Hijacker)
|
||||
if !ok {
|
||||
http.Error(w, "hijacking not supported", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
client, buf, err := hj.Hijack()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer client.Close()
|
||||
io.WriteString(client, "HTTP/1.1 200 Connection established\r\n\r\n")
|
||||
// Anything the client sent right after its CONNECT request is already
|
||||
// sitting in the server's read buffer.
|
||||
if n := buf.Reader.Buffered(); n > 0 {
|
||||
io.CopyN(upstream, buf.Reader, int64(n))
|
||||
}
|
||||
pipe(client, upstream)
|
||||
}
|
||||
+253
@@ -0,0 +1,253 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Tailscale on PS5</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #f4f5f7; --panel: #ffffff; --text: #1c1e21; --muted: #646a73;
|
||||
--line: #dfe2e6; --accent: #2f5fd0; --ok: #1f8a4c; --warn: #b26a00; --bad: #c0362c;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg: #14161a; --panel: #1e2126; --text: #e8eaed; --muted: #9aa1ab;
|
||||
--line: #30353c; --accent: #7fa4ff; --ok: #56c488; --warn: #e0a84a; --bad: #f0796f;
|
||||
}
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0; padding: 24px 16px 48px; background: var(--bg); color: var(--text);
|
||||
font: 16px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||
}
|
||||
main { max-width: 720px; margin: 0 auto; }
|
||||
h1 { font-size: 22px; margin: 0 0 4px; }
|
||||
h2 { font-size: 15px; margin: 0 0 12px; color: var(--muted); font-weight: 600; text-transform: uppercase; letter-spacing: .04em; }
|
||||
.sub { color: var(--muted); margin: 0 0 20px; font-size: 14px; }
|
||||
.panel { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 18px; margin-bottom: 16px; }
|
||||
.state { display: flex; align-items: center; gap: 10px; font-size: 20px; font-weight: 600; }
|
||||
.dot { width: 12px; height: 12px; border-radius: 50%; background: var(--muted); flex: none; }
|
||||
.dot.ok { background: var(--ok); } .dot.warn { background: var(--warn); } .dot.bad { background: var(--bad); }
|
||||
dl { display: grid; grid-template-columns: max-content 1fr; gap: 6px 16px; margin: 14px 0 0; }
|
||||
dt { color: var(--muted); } dd { margin: 0; overflow-wrap: anywhere; }
|
||||
code { font: 14px ui-monospace, SFMono-Regular, Consolas, monospace; }
|
||||
a { color: var(--accent); }
|
||||
.login { text-align: center; }
|
||||
.login img { width: 240px; height: 240px; image-rendering: pixelated; background: #fff; padding: 8px; border-radius: 8px; }
|
||||
.login .url { display: block; margin: 12px 0 4px; font-size: 18px; overflow-wrap: anywhere; }
|
||||
.msg { color: var(--bad); margin: 12px 0 0; overflow-wrap: anywhere; }
|
||||
.health { color: var(--warn); margin: 12px 0 0; padding-left: 18px; }
|
||||
table { width: 100%; border-collapse: collapse; font-size: 15px; }
|
||||
th { text-align: left; color: var(--muted); font-weight: 500; padding: 4px 8px 8px 0; }
|
||||
td { padding: 7px 8px 7px 0; border-top: 1px solid var(--line); overflow-wrap: anywhere; }
|
||||
td.off { color: var(--muted); }
|
||||
.actions { display: flex; flex-wrap: wrap; gap: 10px; }
|
||||
button {
|
||||
font: inherit; padding: 9px 16px; border-radius: 8px; border: 1px solid var(--line);
|
||||
background: var(--panel); color: var(--text); cursor: pointer;
|
||||
}
|
||||
button:hover { border-color: var(--accent); }
|
||||
button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||
button.danger { color: var(--bad); }
|
||||
pre { margin: 0; max-height: 320px; overflow: auto; font: 12.5px/1.45 ui-monospace, Consolas, monospace; white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||
details summary { cursor: pointer; color: var(--muted); }
|
||||
.note { color: var(--muted); font-size: 14px; margin: 0 0 12px; }
|
||||
.field { display: flex; flex-direction: column; gap: 4px; font-size: 14px; color: var(--muted); }
|
||||
select {
|
||||
font: inherit; padding: 8px 10px; border-radius: 8px; border: 1px solid var(--line);
|
||||
background: var(--panel); color: var(--text); min-width: 220px;
|
||||
}
|
||||
.actions { align-items: flex-end; }
|
||||
.hidden { display: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>Tailscale on PS5</h1>
|
||||
<p class="sub" id="version"></p>
|
||||
|
||||
<section class="panel">
|
||||
<div class="state"><span class="dot" id="dot"></span><span id="state">Loading…</span></div>
|
||||
<dl id="facts"></dl>
|
||||
<ul class="health hidden" id="health"></ul>
|
||||
<p class="msg hidden" id="error"></p>
|
||||
</section>
|
||||
|
||||
<section class="panel login hidden" id="login">
|
||||
<h2>Log in</h2>
|
||||
<p>Scan this with your phone, or open the link on any device, to add this PS5 to your tailnet.</p>
|
||||
<img id="qr" alt="QR code for the login link">
|
||||
<a class="url" id="authurl" target="_blank" rel="noopener"></a>
|
||||
</section>
|
||||
|
||||
<section class="panel hidden" id="peerpanel">
|
||||
<h2>Devices on your tailnet</h2>
|
||||
<table>
|
||||
<thead><tr><th>Name</th><th>Address</th><th>OS</th><th>Status</th></tr></thead>
|
||||
<tbody id="peers"></tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section class="panel hidden" id="streampanel">
|
||||
<h2>Game streaming (Moonlight to Sunshine)</h2>
|
||||
<p class="note">Apps on the PS5 cannot reach tailnet addresses directly. Pick the device that runs Sunshine and its
|
||||
streaming ports are made available on this console. Then, in your Moonlight client on the PS5 (for example
|
||||
ProsperoLight), add the host <code>127.0.0.1</code>.</p>
|
||||
<div class="actions">
|
||||
<label class="field">Sunshine host
|
||||
<select id="sunshine-select"></select>
|
||||
</label>
|
||||
<button id="btn-sunshine">Save</button>
|
||||
</div>
|
||||
<p class="note" id="sunshine-state"></p>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2>Actions</h2>
|
||||
<div class="actions">
|
||||
<button id="btn-login">Log in again</button>
|
||||
<button id="btn-logout" class="danger">Log out</button>
|
||||
<button id="btn-quit" class="danger">Stop Tailscale</button>
|
||||
<button id="btn-uninstall" class="danger">Uninstall</button>
|
||||
</div>
|
||||
<p class="msg hidden" id="actionmsg"></p>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<details id="logbox">
|
||||
<summary>Recent log</summary>
|
||||
<pre id="logs"></pre>
|
||||
</details>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
const $ = id => document.getElementById(id);
|
||||
const labels = {
|
||||
Starting: ['Starting…', 'warn'],
|
||||
NoState: ['Starting…', 'warn'],
|
||||
NeedsLogin: ['Not logged in', 'warn'],
|
||||
NeedsMachineAuth: ['Waiting for approval in the admin console', 'warn'],
|
||||
Stopped: ['Stopped', 'bad'],
|
||||
Running: ['Connected', 'ok'],
|
||||
};
|
||||
let shownQR = '';
|
||||
|
||||
function row(dl, name, value, mono) {
|
||||
const dt = document.createElement('dt'); dt.textContent = name;
|
||||
const dd = document.createElement('dd');
|
||||
if (mono) { const c = document.createElement('code'); c.textContent = value; dd.append(c); }
|
||||
else dd.textContent = value;
|
||||
dl.append(dt, dd);
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
let s;
|
||||
try {
|
||||
s = await (await fetch('/api/status', {cache: 'no-store'})).json();
|
||||
} catch (e) {
|
||||
$('state').textContent = 'Not responding';
|
||||
$('dot').className = 'dot bad';
|
||||
return;
|
||||
}
|
||||
const [label, cls] = labels[s.state] || [s.state, 'warn'];
|
||||
$('state').textContent = label;
|
||||
$('dot').className = 'dot ' + cls;
|
||||
$('version').textContent = 'ps5-tailscale ' + s.version;
|
||||
|
||||
const dl = $('facts');
|
||||
dl.replaceChildren();
|
||||
row(dl, 'Name', s.dnsName || s.hostname);
|
||||
if (s.ips.length) row(dl, 'Tailnet address', s.ips.join(', '), true);
|
||||
if (s.tailnet) row(dl, 'Tailnet', s.tailnet);
|
||||
if (s.proxy) row(dl, 'HTTP proxy', s.proxy, true);
|
||||
row(dl, 'Starts with the console', s.autostart.length ? 'Yes, via ' + s.autostart.join(', ') : 'No');
|
||||
|
||||
const health = $('health');
|
||||
health.replaceChildren(...(s.health || []).map(h => { const li = document.createElement('li'); li.textContent = h; return li; }));
|
||||
health.classList.toggle('hidden', !(s.health || []).length);
|
||||
|
||||
$('error').textContent = s.error || '';
|
||||
$('error').classList.toggle('hidden', !s.error);
|
||||
|
||||
const needLogin = !!s.authURL;
|
||||
$('login').classList.toggle('hidden', !needLogin);
|
||||
if (needLogin) {
|
||||
$('authurl').textContent = s.authURL;
|
||||
$('authurl').href = s.authURL;
|
||||
if (shownQR !== s.authURL) { shownQR = s.authURL; $('qr').src = '/qr.png?' + Date.now(); }
|
||||
}
|
||||
|
||||
const tbody = $('peers');
|
||||
tbody.replaceChildren(...s.peers.map(p => {
|
||||
const tr = document.createElement('tr');
|
||||
for (const [v, mono] of [[p.name], [p.ip, true], [p.os], [p.online ? 'online' : 'offline']]) {
|
||||
const td = document.createElement('td');
|
||||
if (!p.online) td.className = 'off';
|
||||
if (mono) { const c = document.createElement('code'); c.textContent = v; td.append(c); } else td.textContent = v;
|
||||
tr.append(td);
|
||||
}
|
||||
return tr;
|
||||
}));
|
||||
$('peerpanel').classList.toggle('hidden', !s.peers.length);
|
||||
|
||||
// Game streaming: offer the tailnet's devices, keep the user's selection
|
||||
// while they are choosing.
|
||||
$('streampanel').classList.toggle('hidden', s.state !== 'Running' && !s.sunshineHost);
|
||||
const sel = $('sunshine-select');
|
||||
const options = ['', ...s.peers.map(p => p.name)];
|
||||
if (s.sunshineHost && !options.includes(s.sunshineHost)) options.push(s.sunshineHost);
|
||||
const signature = options.join('|') + '#' + s.sunshineHost;
|
||||
if (sel.dataset.signature !== signature && document.activeElement !== sel) {
|
||||
sel.replaceChildren(...options.map(name => {
|
||||
const o = document.createElement('option');
|
||||
o.value = name;
|
||||
const peer = s.peers.find(p => p.name === name);
|
||||
o.textContent = name === '' ? 'None (off)' : name + (peer && !peer.online ? ' (offline)' : '');
|
||||
return o;
|
||||
}));
|
||||
sel.value = s.sunshineHost;
|
||||
sel.dataset.signature = signature;
|
||||
}
|
||||
$('sunshine-state').textContent = s.sunshineHost
|
||||
? 'Forwarding 127.0.0.1 to ' + s.sunshineHost + ' (' + s.forwards.length + ' ports).'
|
||||
: 'Off.';
|
||||
|
||||
if ($('logbox').open) {
|
||||
try { $('logs').textContent = await (await fetch('/api/logs', {cache: 'no-store'})).text(); } catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
async function act(path, confirmText) {
|
||||
if (confirmText && !confirm(confirmText)) return;
|
||||
const msg = $('actionmsg');
|
||||
msg.classList.add('hidden');
|
||||
try {
|
||||
const r = await fetch(path, {method: 'POST', headers: {'X-PS5-Tailscale': '1'}});
|
||||
if (!r.ok) throw new Error((await r.text()).trim() || r.statusText);
|
||||
} catch (e) {
|
||||
msg.textContent = e.message;
|
||||
msg.classList.remove('hidden');
|
||||
}
|
||||
refresh();
|
||||
}
|
||||
|
||||
$('btn-login').onclick = () => act('/api/login');
|
||||
$('btn-logout').onclick = () => act('/api/logout', 'Log this PS5 out of your tailnet?');
|
||||
$('btn-quit').onclick = () => act('/api/quit', 'Stop Tailscale on this PS5? Send the payload again to start it.');
|
||||
$('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 will stop now and no longer start with the console.')) 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' : ''));
|
||||
};
|
||||
$('logbox').addEventListener('toggle', refresh);
|
||||
|
||||
refresh();
|
||||
setInterval(refresh, 3000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+358
@@ -0,0 +1,358 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
qrcode "github.com/skip2/go-qrcode"
|
||||
)
|
||||
|
||||
//go:embed status.html
|
||||
var statusHTML []byte
|
||||
|
||||
// The status page has no login: like the other services on a jailbroken
|
||||
// console it trusts the local network. State-changing requests must carry
|
||||
// this header, which a web page on another origin cannot send, so a stray
|
||||
// link or image tag cannot log the console out.
|
||||
const apiHeader = "X-PS5-Tailscale"
|
||||
|
||||
type peerInfo struct {
|
||||
Name string `json:"name"`
|
||||
IP string `json:"ip"`
|
||||
OS string `json:"os"`
|
||||
Online bool `json:"online"`
|
||||
}
|
||||
|
||||
type statusInfo struct {
|
||||
Version string `json:"version"`
|
||||
State string `json:"state"`
|
||||
AuthURL string `json:"authURL,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Hostname string `json:"hostname"`
|
||||
DNSName string `json:"dnsName,omitempty"`
|
||||
IPs []string `json:"ips"`
|
||||
Tailnet string `json:"tailnet,omitempty"`
|
||||
Health []string `json:"health,omitempty"`
|
||||
Peers []peerInfo `json:"peers"`
|
||||
Proxy string `json:"proxy,omitempty"`
|
||||
// Autostart lists the autoloaders that start the daemon.
|
||||
Autostart []string `json:"autostart"`
|
||||
// SunshineHost and Forwards describe the local forwards.
|
||||
SunshineHost string `json:"sunshineHost"`
|
||||
Forwards []string `json:"forwards"`
|
||||
Uptime int64 `json:"uptimeSeconds"`
|
||||
}
|
||||
|
||||
func (d *daemon) serveWeb(ln net.Listener) {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
w.Write(statusHTML)
|
||||
})
|
||||
mux.HandleFunc("GET /api/ping", func(w http.ResponseWriter, r *http.Request) {
|
||||
io.WriteString(w, "ps5-tailscale "+version+"\n")
|
||||
})
|
||||
mux.HandleFunc("GET /api/status", d.handleStatus)
|
||||
mux.HandleFunc("GET /api/logs", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
switch {
|
||||
case r.URL.Query().Get("full") == "1":
|
||||
// The end of the log file itself.
|
||||
writeFileTail(w, filepath.Join(dataDir, "tailscale.log"), 512<<10)
|
||||
return
|
||||
case r.URL.Query().Get("debug") == "1":
|
||||
// The end of the debug log, which includes Tailscale's own messages.
|
||||
writeFileTail(w, filepath.Join(dataDir, "tailscale-debug.log"), 1<<20)
|
||||
return
|
||||
case r.URL.Query().Get("debug") == "old":
|
||||
writeFileTail(w, filepath.Join(dataDir, "tailscale-debug.log.old"), 1<<20)
|
||||
return
|
||||
}
|
||||
io.WriteString(w, strings.Join(recentLogs.snapshot(), "\n")+"\n")
|
||||
})
|
||||
mux.HandleFunc("GET /qr.png", d.handleQR)
|
||||
mux.HandleFunc("POST /api/login", d.guard(d.handleLogin))
|
||||
mux.HandleFunc("POST /api/logout", d.guard(d.handleLogout))
|
||||
mux.HandleFunc("POST /api/quit", d.guard(d.handleQuit))
|
||||
mux.HandleFunc("POST /api/uninstall", d.guard(d.handleUninstall))
|
||||
mux.HandleFunc("POST /api/sunshine", d.guard(d.handleSunshine))
|
||||
|
||||
srv := &http.Server{Handler: mux, ReadHeaderTimeout: 10 * time.Second}
|
||||
if err := srv.Serve(ln); err != nil && err != http.ErrServerClosed && !d.stopping() {
|
||||
d.logf("web UI stopped: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// stopping reports whether a shutdown has been requested.
|
||||
func (d *daemon) stopping() bool {
|
||||
select {
|
||||
case <-d.quit:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// writeFileTail copies the last max bytes of a file to w.
|
||||
func writeFileTail(w io.Writer, path string, max int64) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
io.WriteString(w, err.Error()+"\n")
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
if fi, err := f.Stat(); err == nil && fi.Size() > max {
|
||||
f.Seek(fi.Size()-max, io.SeekStart)
|
||||
}
|
||||
io.Copy(w, f)
|
||||
}
|
||||
|
||||
func (d *daemon) guard(h http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get(apiHeader) == "" {
|
||||
http.Error(w, "missing "+apiHeader+" header", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
h(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *daemon) handleStatus(w http.ResponseWriter, r *http.Request) {
|
||||
d.mu.Lock()
|
||||
info := statusInfo{
|
||||
Version: version,
|
||||
State: d.state,
|
||||
AuthURL: d.authURL,
|
||||
Error: d.lastErr,
|
||||
Hostname: d.cfg.Hostname,
|
||||
Proxy: d.cfg.HTTPProxyAddr,
|
||||
Uptime: int64(time.Since(d.started).Seconds()),
|
||||
IPs: []string{},
|
||||
Peers: []peerInfo{},
|
||||
}
|
||||
info.SunshineHost = d.cfg.SunshineHost
|
||||
d.mu.Unlock()
|
||||
info.Autostart = autostartNames()
|
||||
info.Forwards = []string{}
|
||||
for _, r := range d.fwd.rules() {
|
||||
info.Forwards = append(info.Forwards, r.String())
|
||||
}
|
||||
if info.State == "" {
|
||||
info.State = "Starting"
|
||||
}
|
||||
|
||||
if d.lc != nil {
|
||||
if st, err := d.status(r.Context()); err == nil {
|
||||
info.State = st.BackendState
|
||||
info.Health = st.Health
|
||||
if info.AuthURL == "" {
|
||||
info.AuthURL = st.AuthURL
|
||||
}
|
||||
if st.CurrentTailnet != nil {
|
||||
info.Tailnet = st.CurrentTailnet.Name
|
||||
}
|
||||
if st.Self != nil {
|
||||
info.DNSName = strings.TrimSuffix(st.Self.DNSName, ".")
|
||||
for _, ip := range st.Self.TailscaleIPs {
|
||||
info.IPs = append(info.IPs, ip.String())
|
||||
}
|
||||
}
|
||||
for _, p := range st.Peer {
|
||||
pi := peerInfo{Name: p.HostName, OS: p.OS, Online: p.Online}
|
||||
if p.DNSName != "" {
|
||||
pi.Name = strings.SplitN(p.DNSName, ".", 2)[0]
|
||||
}
|
||||
if len(p.TailscaleIPs) > 0 {
|
||||
pi.IP = p.TailscaleIPs[0].String()
|
||||
}
|
||||
info.Peers = append(info.Peers, pi)
|
||||
}
|
||||
sort.Slice(info.Peers, func(i, j int) bool {
|
||||
if info.Peers[i].Online != info.Peers[j].Online {
|
||||
return info.Peers[i].Online
|
||||
}
|
||||
return info.Peers[i].Name < info.Peers[j].Name
|
||||
})
|
||||
}
|
||||
}
|
||||
if info.State == "Running" {
|
||||
info.AuthURL = ""
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
json.NewEncoder(w).Encode(info)
|
||||
}
|
||||
|
||||
// handleQR renders the current login URL as a QR code. It only ever encodes
|
||||
// the URL the daemon holds, never one supplied by the request.
|
||||
func (d *daemon) handleQR(w http.ResponseWriter, r *http.Request) {
|
||||
d.mu.Lock()
|
||||
u := d.authURL
|
||||
d.mu.Unlock()
|
||||
if u == "" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
png, err := qrcode.Encode(u, qrcode.Medium, 320)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "image/png")
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
w.Write(png)
|
||||
}
|
||||
|
||||
func (d *daemon) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
if d.lc == nil {
|
||||
http.Error(w, "tailscale is still starting", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
d.mu.Lock()
|
||||
loggedOut := d.state == "NeedsLogin"
|
||||
d.mu.Unlock()
|
||||
var err error
|
||||
if loggedOut {
|
||||
// The pending link may be used up or expired; get a new one.
|
||||
err = d.freshLogin(r.Context())
|
||||
} else {
|
||||
err = d.lc.StartLoginInteractive(r.Context())
|
||||
}
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
io.WriteString(w, "ok\n")
|
||||
}
|
||||
|
||||
func (d *daemon) handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
if d.lc == nil {
|
||||
http.Error(w, "tailscale is still starting", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
if err := d.lc.Logout(r.Context()); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
d.mu.Lock()
|
||||
d.notified = ""
|
||||
d.mu.Unlock()
|
||||
d.logf("logged out via the status page")
|
||||
io.WriteString(w, "ok\n")
|
||||
}
|
||||
|
||||
// handleSunshine sets (or with an empty host, clears) the Sunshine host whose
|
||||
// streaming ports are forwarded from 127.0.0.1, saves the config and applies
|
||||
// it without a restart.
|
||||
func (d *daemon) handleSunshine(w http.ResponseWriter, r *http.Request) {
|
||||
host := strings.TrimSpace(r.URL.Query().Get("host"))
|
||||
if !validHostName(host) {
|
||||
http.Error(w, "that does not look like a host name or address", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
d.mu.Lock()
|
||||
d.cfg.SunshineHost = host
|
||||
cfg := d.cfg
|
||||
d.mu.Unlock()
|
||||
if err := saveConfig(d.cfgPath, cfg); err != nil {
|
||||
d.logf("saving config: %v", err)
|
||||
}
|
||||
d.logf("sunshine host set to %q", host)
|
||||
if err := d.fwd.set(d.localForwardRules()); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
io.WriteString(w, "ok\n")
|
||||
}
|
||||
|
||||
// validHostName accepts an empty string, a DNS name or an IP address.
|
||||
func validHostName(s string) bool {
|
||||
if len(s) > 253 {
|
||||
return false
|
||||
}
|
||||
for _, c := range s {
|
||||
switch {
|
||||
case c >= 'a' && c <= 'z', c >= 'A' && c <= 'Z', c >= '0' && c <= '9', c == '.', c == '-', c == ':':
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (d *daemon) handleQuit(w http.ResponseWriter, r *http.Request) {
|
||||
io.WriteString(w, "stopping\n")
|
||||
d.stop()
|
||||
}
|
||||
|
||||
func (d *daemon) stop() {
|
||||
d.quitOnce.Do(func() { close(d.quit) })
|
||||
}
|
||||
|
||||
// handleUninstall removes the autostart entry and the installed payload, then
|
||||
// stops. With ?purge=1 it first logs the console out of the tailnet and
|
||||
// deletes the saved state as well.
|
||||
func (d *daemon) handleUninstall(w http.ResponseWriter, r *http.Request) {
|
||||
purge := r.URL.Query().Get("purge") == "1"
|
||||
if purge && d.lc != nil {
|
||||
if err := d.lc.Logout(r.Context()); err != nil {
|
||||
d.logf("uninstall: logout: %v", err)
|
||||
}
|
||||
}
|
||||
if err := uninstall(purge, d.logf); err != nil {
|
||||
d.logf("uninstall: %v", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
d.logf("uninstalled (purge=%v)", purge)
|
||||
notify("Tailscale was removed from this PS5.")
|
||||
io.WriteString(w, "uninstalled\n")
|
||||
d.stop()
|
||||
}
|
||||
|
||||
// stopRunningInstance asks an instance that is already serving the status
|
||||
// page to exit and waits for the port to become free. It reports whether
|
||||
// there was one.
|
||||
func stopRunningInstance(webAddr string) bool {
|
||||
_, port, err := net.SplitHostPort(webAddr)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
base := "http://" + net.JoinHostPort("127.0.0.1", port)
|
||||
client := &http.Client{Timeout: 3 * time.Second}
|
||||
|
||||
resp, err := client.Get(base + "/api/ping")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 100))
|
||||
resp.Body.Close()
|
||||
if !strings.HasPrefix(string(body), "ps5-tailscale") {
|
||||
return false
|
||||
}
|
||||
|
||||
req, _ := http.NewRequest("POST", base+"/api/quit", nil)
|
||||
req.Header.Set(apiHeader, "1")
|
||||
if resp, err := client.Do(req); err == nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
for i := 0; i < 40; i++ {
|
||||
c, err := net.DialTimeout("tcp", net.JoinHostPort("127.0.0.1", port), time.Second)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
c.Close()
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
}
|
||||
return true
|
||||
}
|
||||
Reference in new issue
Block a user