File: README.md
Hysteria is a powerful, lightning-fast, and censorship-resistant proxy.
[Get Started](https://v2.hysteria.network/)
[中文文档](https://v2.hysteria.network/zh/)
[Hysteria 1.x (legacy)](https://v1.hysteria.network/)
<h3>🛠️ Jack of all trades</h3>
Wide range of modes including SOCKS5, HTTP Proxy, TCP/UDP Forwarding, Linux TProxy, TUN - with more features being added constantly.
<h3>⚡ Blazing fast</h3>
Powered by a customized QUIC protocol, Hysteria is designed to deliver unparalleled performance over unreliable and lossy networks.
<h3>✊ Censorship resistant</h3>
The protocol masquerades as standard HTTP/3 traffic, making it very difficult for censors to detect and block without widespread collateral damage.
<h3>💻 Cross-platform</h3>
We have builds for every major platform and architecture. Deploy anywhere & use everywhere. Not to mention the long list of 3rd party apps.
<h3>🔗 Easy integration</h3>
With built-in support for custom authentication, traffic statistics & access control, Hysteria is easy to integrate into your infrastructure.
<h3>🤗 Chill and supportive</h3>
We have well-documented specifications and code for developers to contribute and/or build their own apps. And a helpful community, too.If you find Hysteria useful, consider giving it a ⭐️!
File: CHANGELOG.md
Changelog
https://v2.hysteria.network/docs/Changelog/
File: LICENSE.md
Copyright 2023 Toby
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
File: platforms.txt
This file controls what platform/architecture combinations we build for a release.
Windows
windows/amd64
windows/amd64-avx
windows/386
windows/arm64
macOS
darwin/amd64
darwin/amd64-avx
darwin/arm64
Linux
linux/amd64
linux/amd64-avx
linux/386
linux/arm
linux/armv5
linux/arm64
linux/s390x
linux/mipsle
linux/mipsle-sf
linux/riscv64
linux/loong64
Android
android/386
android/amd64
android/armv7
android/arm64
FreeBSD
freebsd/amd64
freebsd/amd64-avx
freebsd/386
freebsd/arm
freebsd/arm64
File: PROTOCOL.md
Hysteria 2 Protocol Specification
Hysteria is a TCP & UDP proxy based on QUIC, designed for speed, security and censorship resistance. This document describes the protocol used by Hysteria starting with version 2.0.0, sometimes internally referred to as the "v4" protocol. From here on, we will call it "the protocol" or "the Hysteria protocol".
Requirements Language
The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.
Underlying Protocol & Wire Format
The Hysteria protocol MUST be implemented on top of the standard QUIC transport protocol RFC 9000 with Unreliable Datagram Extension.
All multibyte numbers use Big Endian format.
All variable-length integers ("varints") are encoded/decoded as defined in QUIC (RFC 9000).
Authentication & HTTP/3 masquerading
One of the key features of the Hysteria protocol is that to a third party without proper authentication credentials (whether it's a middleman or an active prober), a Hysteria proxy server behaves just like a standard HTTP/3 web server. Additionally, the encrypted traffic between the client and the server appears indistinguishable from normal HTTP/3 traffic.
Therefore, a Hysteria server MUST implement an HTTP/3 server (as defined by RFC 9114) and handle HTTP requests as any standard web server would. To prevent active probers from detecting common response patterns in Hysteria servers, implementations SHOULD advise users to either host actual content or set it up as a reverse proxy for other sites.
An actual Hysteria client, upon connection, MUST send the following HTTP/3 request to the server:
:method: POST
:path: /auth
:host: hysteria
Hysteria-Auth: [string]
Hysteria-CC-RX: [uint]
Hysteria-Padding: [string]Hysteria-Auth: Authentication credentials.
Hysteria-CC-RX: Client's maximum receive rate in bytes per second. A value of 0 indicates unknown.
Hysteria-Padding: A random padding string of variable length.
The Hysteria server MUST identify this special request, and, instead of attempting to serve content or forwarding it to an upstream site, it MUST authenticate the client using the provided information. If authentication is successful, the server MUST send the following response (HTTP status code 233):
:status: 233 HyOK
Hysteria-UDP: [true/false]
Hysteria-CC-RX: [uint/"auto"]
Hysteria-Padding: [string]Hysteria-UDP: Whether the server supports UDP relay.
Hysteria-CC-RX: Server's maximum receive rate in bytes per second. A value of 0 indicates unlimited; "auto" indicates the server refuses to provide a value and ask the client to use congestion control to determine the rate on its own.
Hysteria-Padding: A random padding string of variable length.
See the Congestion Control section for more information on how to use the Hysteria-CC-RX values.
Hysteria-Padding is optional and is only intended to obfuscate the request/response pattern. It SHOULD be ignored by both sides.
If authentication fails, the server MUST either act like a standard web server that does not understand the request, or in the case of being a reverse proxy, forward the request to the upstream site and return the response to the client.
The client MUST check the status code to determine if the authentication was successful. If the status code is anything other than 233, the client MUST consider authentication to have failed and disconnect from the server.
After (and only after) a client passes authentication, the server MUST consider this QUIC connection to be a Hysteria proxy connection. It MUST then start processing proxy requests from the client as described in the next section.
Proxy Requests
TCP
For each TCP connection, the client MUST create a new QUIC bidirectional stream and send the following TCPRequest message:
[varint] 0x401 (TCPRequest ID)
[varint] Address length
[bytes] Address string (host:port)
[varint] Padding length
[bytes] Random paddingThe server MUST respond with a TCPResponse message:
[uint8] Status (0x00 = OK, 0x01 = Error)
[varint] Message length
[bytes] Message string
[varint] Padding length
[bytes] Random paddingIf the status is OK, the server MUST then begin forwarding data between the client and the specified TCP address until either side closes the connection. If the status is Error, the server MUST close the QUIC stream.
UDP
UDP packets MUST be encapsulated in the following UDPMessage format and sent over QUIC's unreliable datagram (for both client-to-server and server-to-client):
[uint32] Session ID
[uint16] Packet ID
[uint8] Fragment ID
[uint8] Fragment count
[varint] Address length
[bytes] Address string (host:port)
[bytes] PayloadThe client MUST use a unique Session ID for each UDP session. The server SHOULD assign a unique UDP port to each Session ID, unless it has another mechanism to differentiate packets from different sessions (e.g., symmetric NAT, varying outbound IP addresses, etc.).
The protocol does not provide an explicit way to close a UDP session. While a client can retain and reuse a Session ID indefinitely, the server SHOULD release and reassign the port associated with the Session ID after a period of inactivity or some other criteria. If the client sends a UDP packet to a Session ID that is no longer recognized by the server, the server MUST treat it as a new session and assign a new port.
If a server does not support UDP relay, it SHOULD silently discard all UDP messages received from the client.
Fragmentation
Due to the limit imposed by QUIC's unreliable datagram channel, any UDP packet that exceeds QUIC's maximum datagram size MUST either be fragmented or discarded.
For fragmented packets, each fragment MUST carry the same unique Packet ID. The Fragment ID, starting from 0, indicates the index out of the total Fragment Count. Both the server and client MUST wait for all fragments of a fragmented packet to arrive before processing them. If one or more fragments of a packet are lost, the entire packet MUST be discarded.
For packets that are not fragmented, the Fragment Count MUST be set to 1. In this case, the values of Packet ID and Fragment ID are irrelevant.
Congestion Control
A unique feature of Hysteria is the ability to set the tx/rx (upload/download) rate on the client side. During authentication, the client sends its rx rate to the server via the Hysteria-CC-RX header. The server can use this to determine its transmission rate to the client, and vice versa by returning its rx rate to the client through the same header.
Three special cases are:
- If the client sends 0, it doesn't know its own rx rate. The server MUST use a congestion control algorithm (e.g., BBR, Cubic) to adjust its transmission rate.
- If the server responds with 0, it has no bandwidth limit. The client MAY transmit at any rate it wants.
- If the server responds with "auto", it chooses not to specify a rate. The client MUST use a congestion control algorithm to adjust its transmission rate.
"Salamander" Obfuscation
The Hysteria protocol supports an optional obfuscation layer codenamed "Salamander".
"Salamander" encapsulates all QUIC packets in the following format:
[8 bytes] Salt
[bytes] PayloadFor each QUIC packet, the obfuscator MUST calculate the BLAKE2b-256 hash of a randomly generated 8-byte salt appended to a user-provided pre-shared key.
hash = BLAKE2b-256(key + salt)The hash is then used to obfuscate the payload using the following algorithm:
for i in range(0, len(payload)):
payload[i] ^= hash[i % 32]The deobfuscator MUST use the same algorithms to calculate the salted hash and deobfuscate the payload. Any invalid packet MUST be discarded.
File: app/LICENSE.md
Copyright 2023 Toby
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
File: app/cmd/client_test.yaml
server: example.com
auth: weak_ahh_password
realm:
stunServers:
- stun1.example.com:3478
- stun2.example.com:3478
stunTimeout: 6s
punchTimeout: 12s
insecure: true
ipMode: v4
portMapping:
enabled: true
timeout: 3s
lifetime: 2h
transport:
type: udp
udp:
hopInterval: 30s
minHopInterval: 10s
maxHopInterval: 50s
obfs:
type: salamander
salamander:
password: cry_me_a_r1ver
gecko:
password: g3ck0_in_the_wall
minPacketSize: 100
maxPacketSize: 1200
tls:
sni: another.example.com
insecure: true
pinSHA256: 114515DEADBEEF
ca: custom_ca.crt
clientCertificate: client.crt
clientKey: client.key
ech: AEv+DQBHAAAgACB3rc0Q
quic:
initStreamReceiveWindow: 1145141
maxStreamReceiveWindow: 1145142
initConnReceiveWindow: 1145143
maxConnReceiveWindow: 1145144
maxIdleTimeout: 10s
keepAlivePeriod: 4s
disablePathMTUDiscovery: true
disableChromeParrot: true
sockopts:
bindInterface: eth0
fwmark: 1234
fdControlUnixSocket: test.sock
mimic:
enabled: true
interface: eth0
xdpMode: skb
path: /usr/bin/mimic
extraArgs:
- --padding
- random
congestion:
type: bbr
bbrProfile: aggressive
bandwidth:
up: 200 mbps
down: 1 gbps
disableLossCompensation: true
fastOpen: true
lazy: true
socks5:
listen: 127.0.0.1:1080
username: anon
password: bro
disableUDP: true
http:
listen: 127.0.0.1:8080
username: qqq
password: bruh
realm: martian
tcpForwarding:
- listen: 127.0.0.1:8088
remote: internal.example.com:80
udpForwarding:
- listen: 127.0.0.1:5353
remote: internal.example.com:53
timeout: 50s
tcpTProxy:
listen: 127.0.0.1:2500
udpTProxy:
listen: 127.0.0.1:2501
timeout: 20s
tcpRedirect:
listen: 127.0.0.1:3500
tun:
name: "hytun"
mtu: 1500
timeout: 1m
address:
ipv4: 100.100.100.101/30
ipv6: 2001::ffff:ffff:ffff:fff1/126
route:
ipv4: [ 0.0.0.0/0 ]
ipv6: [ "2000::/3" ]
ipv4Exclude: [ 192.0.2.1/32 ]
ipv6Exclude: [ "2001:db8::1/128" ]
File: app/cmd/server_test.yaml
listen: :8443
realm:
stunServers:
- stun1.example.com:3478
- stun2.example.com:3478
stunTimeout: 6s
punchTimeout: 12s
heartbeatInterval: 25s
insecure: true
ipMode: v6
portMapping:
enabled: true
timeout: 3s
lifetime: 2h
obfs:
type: salamander
salamander:
password: cry_me_a_r1ver
gecko:
password: g3ck0_in_the_wall
minPacketSize: 100
maxPacketSize: 1200
tls:
cert: some.crt
key: some.key
sniGuard: strict
clientCA: some_ca.crt
ech:
keyPath: some_ech.pem
acme:
domains:
- sub1.example.com
- sub2.example.com
email: [email protected]
ca: zero
listenHost: 127.0.0.9
dir: random_dir
type: dns
http:
altPort: 8888
tls:
altPort: 44333
dns:
name: gomommy
config:
key1: value1
key2: value2
disableHTTP: true
disableTLSALPN: true
altHTTPPort: 8080
altTLSALPNPort: 4433
quic:
initStreamReceiveWindow: 77881
maxStreamReceiveWindow: 77882
initConnReceiveWindow: 77883
maxConnReceiveWindow: 77884
maxIdleTimeout: 999s
maxIncomingStreams: 256
disablePathMTUDiscovery: true
disableStatelessReset: true
mimic:
enabled: true
interface: eth0
xdpMode: skb
path: /usr/bin/mimic
extraArgs:
- --padding
- random
congestion:
type: reno
bbrProfile: aggressive
bandwidth:
up: 500 mbps
down: 100 mbps
disableLossCompensation: true
ignoreClientBandwidth: true
speedTest: true
disableUDP: true
udpIdleTimeout: 120s
auth:
type: password
password: goofy_ahh_password
userpass:
yolo: swag
lol: kek
foo: bar
http:
url: http://127.0.0.1:5000/auth
insecure: true
command: /etc/some_command
resolver:
type: udp
tcp:
addr: 123.123.123.123:5353
timeout: 4s
udp:
addr: 4.6.8.0:53
timeout: 2s
tls:
addr: dot.yolo.com:8853
timeout: 10s
sni: server1.yolo.net
insecure: true
https:
addr: cringe.ahh.cc
timeout: 5s
sni: real.stuff.net
insecure: true
sniff:
enable: true
timeout: 1s
rewriteDomain: true
tcpPorts: 80,443,1000-2000
udpPorts: 443
acl:
file: chnroute.txt
inline:
- lmao(ok)
- kek(cringe,boba,tea)
geoip: some.dat
geosite: some_site.dat
geoUpdateInterval: 168h
outbounds:
- name: goodstuff
type: direct
direct:
mode: 64
bindIPv4: 2.4.6.8
bindIPv6: 0:0:0:0:0:ffff:0204:0608
bindDevice: eth233
fastOpen: true - name: badstuff
type: socks5
socks5:
addr: shady.proxy.ru:1080
username: hackerman
password: Elliot Alderson - name: weirdstuff
type: http
http:
url: https://eyy.lmao:4443/goofy
insecure: true
trafficStats:
listen: :9999
secret: its_me_mario
masquerade:
type: proxy
file:
dir: /www/masq
proxy:
url: https://some.site.net
rewriteHost: true
xForwarded: true
insecure: true
string:
content: aint nothin here
headers:
content-type: text/plain
custom-haha: lol
statusCode: 418
listenHTTP: :80
listenHTTPS: :443
forceHTTPS: true
File: app/internal/proxymux/.mockery.yaml
with-expecter: true
dir: internal/mocks
outpkg: mocks
packages:
net:
interfaces:
Listener:
config:
mockname: MockListener
Conn:
config:
mockname: MockConn
File: core/LICENSE.md
Copyright 2023 Toby
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
File: core/client/.mockery.yaml
with-expecter: true
inpackage: true
dir: .
packages:
github.com/apernet/hysteria/core/v2/client:
interfaces:
udpIO:
config:
mockname: mockUDPIO
File: core/internal/integration_tests/.mockery.yaml
with-expecter: true
dir: mocks
outpkg: mocks
packages:
net:
interfaces:
Conn:
config:
mockname: MockConn
github.com/apernet/hysteria/core/v2/server:
interfaces:
Outbound:
config:
mockname: MockOutbound
UDPConn:
config:
mockname: MockUDPConn
Authenticator:
config:
mockname: MockAuthenticator
EventLogger:
config:
mockname: MockEventLogger
TrafficLogger:
config:
mockname: MockTrafficLogger
RequestHook:
config:
mockname: MockRequestHook
File: core/server/.mockery.yaml
with-expecter: true
inpackage: true
dir: .
packages:
github.com/apernet/hysteria/core/v2/server:
interfaces:
udpIO:
config:
mockname: mockUDPIO
udpEventLogger:
config:
mockname: mockUDPEventLogger
UDPConn:
config:
mockname: mockUDPConn
File: extras/LICENSE.md
Copyright 2023 Toby
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
File: extras/outbounds/.mockery.yaml
with-expecter: true
inpackage: true
dir: .
packages:
github.com/apernet/hysteria/extras/v2/outbounds:
interfaces:
PluggableOutbound:
config:
mockname: mockPluggableOutbound
UDPConn:
config:
mockname: mockUDPConn
File: extras/sniff/testdata/http-chrome153.txt
GET / HTTP/1.1
Host: chrome.sniff.test
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/153.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
File: .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "daily" - package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily" - package-ecosystem: "uv"
directory: "/"
schedule:
interval: "daily"
File: .github/FUNDING.yml
github: tobyxdd
custom: [ 'https://v2.hysteria.network/docs/Donation/' ]
File: .github/ISSUE_TEMPLATE/bug_report.md
name: Bug report
about: Report anything you think is a bug and needs to be fixed.
title: ''
labels: bug
assignees: ''
Describe the bug
A clear and concise description of what the bug is.
To Reproduce
Steps to reproduce the behavior.
Expected behavior
A clear and concise description of what you expected to happen.
Logs
Attach logs from the client/server when the error occurs.
Device and Operating System
What are you using it on.
Additional context
Add any other context about the problem here.
File: .github/ISSUE_TEMPLATE/bug_report.zh.md
name: Bug 反馈
about: 反馈任何你认为是 bug 需要修复的问题。
title: ''
labels: bug
assignees: ''
描述问题
请尽量清晰精准地描述你遇到的问题。
如何复现
复现问题的步骤。
预期行为
你认为修复后的行为应该是怎样的。
日志
附上客户端/服务器端在错误发生前后的日志。
设备和操作系统
你在用什么设备和操作系统。
额外信息
其他你认为有助于解决问题的信息。
File: .github/ISSUE_TEMPLATE/feature_request.md
name: Feature request
about: Suggest an idea for this project.
title: ''
labels: enhancement
assignees: ''
Is your feature request related to a problem? Please describe.
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
Describe the solution you'd like
A clear and concise description of what you want to happen.
Describe alternatives you've considered
A clear and concise description of any alternative solutions or features you've considered.
Additional context
Add any other context or screenshots about the feature request here.
File: .github/ISSUE_TEMPLATE/feature_request.zh.md
name: 功能请求
about: 为这个项目提出改进意见。
title: ''
labels: enhancement
assignees: ''
你的功能请求是否与某个问题有关?
请尽量清晰精准地描述你遇到的问题。例如:我家运营商限制 UDP 协议速度,导致 Hysteria 很慢,希望增加 FakeTCP 支持。
描述你希望的解决方案
请尽量清晰精准地描述你希望的解决方案。
有没有其他替代方案
请尽量清晰精准地描述你认为可能的替代方案。
额外信息
其他你认为有助于开发者了解你需求的信息。
File: .github/workflows/autotag.yaml
name: "Create release tags for nested modules"
on:
push:
tags:
- app/v*..
permissions:
contents: write
jobs:
tag:
name: "Create tags"
runs-on: ubuntu-latest
steps:
- name: "Extract tagbase"
id: extract_tagbase
uses: actions/github-script@v9
with:
script: |
const ref = context.ref;
core.info(context.ref: ${ref});
const refPrefix = 'refs/tags/app/';
if (!ref.startsWith(refPrefix)) {
core.setFailed(context.ref does not start with ${refPrefix}: ${ref});
return;
}
const tagbase = ref.slice(refPrefix.length);
core.info(tagbase: ${tagbase});
core.setOutput('tagbase', tagbase);
- name: "Tagging core/*"
uses: actions/github-script@v9
env:
INPUT_TAGPREFIX: "core/"
INPUT_TAGBASE: ${{ steps.extract_tagbase.outputs.tagbase }}
with:
script: |
const tagbase = core.getInput('tagbase', { required: true });
const tagprefix = core.getInput('tagprefix', { required: true });
const refname = `tags/${tagprefix}${tagbase}`;
core.info(`creating ref ${refname}`);
try {
await github.rest.git.createRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: `refs/${refname}`,
sha: context.sha
});
core.info(`created ref ${refname}`);
return;
} catch (error) {
core.info(`failed to create ref ${refname}: ${error}`);
}
core.info(`updating ref ${refname}`)
try {
await github.rest.git.updateRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: refname,
sha: context.sha
});
core.info(`updated ref ${refname}`);
return;
} catch (error) {
core.setFailed(`failed to update ref ${refname}: ${error}`);
}
- name: "Tagging extras/*"
uses: actions/github-script@v9
env:
INPUT_TAGPREFIX: "extras/"
INPUT_TAGBASE: ${{ steps.extract_tagbase.outputs.tagbase }}
with:
script: |
const tagbase = core.getInput('tagbase', { required: true });
const tagprefix = core.getInput('tagprefix', { required: true });
const refname = `tags/${tagprefix}${tagbase}`;
core.info(`creating ref ${refname}`);
try {
await github.rest.git.createRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: `refs/${refname}`,
sha: context.sha
});
core.info(`created ref ${refname}`);
return;
} catch (error) {
core.info(`failed to create ref ${refname}: ${error}`);
}
core.info(`updating ref ${refname}`)
try {
await github.rest.git.updateRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: refname,
sha: context.sha
});
core.info(`updated ref ${refname}`);
return;
} catch (error) {
core.setFailed(`failed to update ref ${refname}: ${error}`);
}File: .github/workflows/build-common.yml
name: "Build common"
on:
workflow_call:
jobs:
build:
name: Build
runs-on: ubuntu-latest
env:
ACTIONS_ALLOW_UNSECURE_COMMANDS: true
outputs:
files: ${{ steps.list-files.outputs.files }}
steps:
- name: Check out
uses: actions/checkout@v7
- name: Setup Go
uses: actions/setup-go@v7
with:
go-version: "1.26"
- name: Setup uv # This is for the build script
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
- uses: nttld/setup-ndk@v1
id: setup-ndk
with:
ndk-version: r29
add-to-path: false
- name: Run build script
env:
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
run: |
export HY_APP_PLATFORMS=$(sed 's/\r$//' platforms.txt | awk '!/^#/ && !/^$/' | paste -sd ",")
uv run python hyperbole.py build -r
- name: Generate hashes
run: |
for file in build/*; do
sha256sum $file >> build/hashes.txt
done
- name: List build files
id: list-files
run: |
files=$(ls -1 build/ | jq -R -s -c 'split("\n") | map(select(length > 0))')
echo "files=$files" >> $GITHUB_OUTPUT
- name: Upload build directory
uses: actions/upload-artifact@v7
with:
name: build-temp-${{ github.sha }}
path: build/ upload:
name: Upload ${{ matrix.file }}
needs: build
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
file: ${{ fromJson(needs.build.outputs.files) }}
steps:
- name: Download build
uses: actions/download-artifact@v8
with:
name: build-temp-${{ github.sha }}
- name: Upload ${{ matrix.file }}
uses: actions/upload-artifact@v7
with:
name: ${{ matrix.file }}
path: ${{ matrix.file }}File: .github/workflows/docker.yml
name: "Build Docker Image"
on:
push:
tags:
- app/v*..
jobs:
docker:
runs-on: ubuntu-latest
env:
ACTIONS_ALLOW_UNSECURE_COMMANDS: true
steps:
- name: Check out
uses: actions/checkout@v7
- name: Get version
id: get_version
run: echo "version=$(git describe --tags --always --match 'app/v*' | sed -n 's|app/\([^/-]*\)\(-.*\)\{0,1\}|\1|p')" >> $GITHUB_OUTPUT
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Login to DockerHub
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v7
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: tobyxdd/hysteria:latest,tobyxdd/hysteria:v2,tobyxdd/hysteria:${{ steps.get_version.outputs.version }}
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}File: .github/workflows/experimental.yml
name: "Build experimental branches"
on:
push:
branches:
- exp/**
jobs:
build:
uses: ./.github/workflows/build-common.yml
File: .github/workflows/master.yml
name: "Build master branch"
on:
push:
branches:
- master
jobs:
build:
uses: ./.github/workflows/build-common.yml
File: .github/workflows/release.yml
name: "Build release"
on:
push:
tags:
- app/v*..
jobs:
build:
uses: ./.github/workflows/build-common.yml
publish:
name: Publish
needs: build
runs-on: ubuntu-latest
steps:
- name: Check out
uses: actions/checkout@v7
- name: Get version
id: get_version
run: echo "version=$(git describe --tags --always --match 'app/v*' | sed -n 's|app/\([^/-]*\)\(-.*\)\{0,1\}|\1|p')" >> $GITHUB_OUTPUT
- name: Setup uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
- name: Download build
uses: actions/download-artifact@v8
with:
name: build-temp-${{ github.sha }}
path: build
- name: Upload GitHub
uses: softprops/action-gh-release@v3
with:
files: build/*
- name: Upload CF bucket
uses: shallwefootball/[email protected]
with:
aws_key_id: ${{ secrets.CF_KEY_ID }}
aws_secret_access_key: ${{ secrets.CF_KEY }}
aws_bucket: "hydownload"
endpoint: "https://bea223c61d5a41250d127bd67f51dfec.r2.cloudflarestorage.com/"
source_dir: "build"
destination_dir: "app/${{ steps.get_version.outputs.version }}"
- name: Publish to API
run: |
export HY_API_POST_KEY=${{ secrets.HY2_API_POST_KEY }}
uv run python hyperbole.py publishFile: .github/workflows/scripts.yml
name: "Publish scripts"
on:
push:
branches:
- master
paths:
- scripts/**
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
deployments: write
name: Publish scripts to Cloudflare Pages
steps:
- name: Check out
uses: actions/checkout@v7
- name: Publish to Cloudflare Pages
uses: cloudflare/pages-action@v1
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: hy2scripts
directory: scripts
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
branch: mainFile: .github/workflows/test.yml
name: "Tests"
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
test:
name: Test
runs-on: ubuntu-latest
steps:
- name: Check out
uses: actions/checkout@v7
- name: Setup Go
uses: actions/setup-go@v7
with:
go-version: "1.26"
- name: Setup uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
- name: Install Python dependencies
# Precompile bytecode (as pip did by default) so the Python test
# helpers start fast enough for the Go tests' server-startup wait.
run: uv sync
env:
UV_COMPILE_BYTECODE: "1"
- name: Activate Python environment
# The Go tests spawn the Python helpers via bare `python`, so put the
# uv-managed virtualenv on PATH for the steps that follow.
run: |
echo "$PWD/.venv/bin" >> "$GITHUB_PATH"
echo "VIRTUAL_ENV=$PWD/.venv" >> "$GITHUB_ENV"
- name: Install gofumpt
run: go install mvdan.cc/gofumpt@latest
- name: Check format
run: python hyperbole.py format-check
- name: Test core
working-directory: core
run: go test -v -count=1 -timeout=5m ./...
- name: Test extras
working-directory: extras
run: go test -v -count=1 ./...
- name: Test app
working-directory: app
run: go test -v -count=1 ./...