This commit is contained in:
Xboard 2024-11-02 04:49:43 +08:00 committed by xboard
parent 78598bfd1b
commit 2d396d3789
8 changed files with 433 additions and 83 deletions

View File

@ -1,44 +1,89 @@
name: "Build Docker Image" name: Docker
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
on: on:
push: push:
tags: branches: [ "master" ]
- app/v*.*.* # Publish semver tags as releases.
tags: [ 'v*.*.*' ]
env:
# Use docker.io for Docker Hub if empty
REGISTRY: ghcr.io
# github.repository as <account>/<repo>
IMAGE_NAME: ${{ github.repository }}
jobs: jobs:
docker: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: permissions:
ACTIONS_ALLOW_UNSECURE_COMMANDS: true contents: read
packages: write
# This is used to complete the identity challenge
# with sigstore/fulcio when running outside of PRs.
id-token: write
steps: steps:
- name: Check out - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v3
- 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 - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Install cosign
uses: docker/setup-buildx-action@v3 uses: sigstore/cosign-installer@v3.4.0
- name: Login to DockerHub
uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} cosign-release: 'v2.2.2'
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push - name: Set up Docker Buildx
id: docker_build uses: docker/setup-buildx-action@v3.2.0
uses: docker/build-push-action@v5
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@v3.1.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5.5.1
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Get version
id: get_version
run: echo "version=$(git describe --tags --always)" >> $GITHUB_OUTPUT
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v5.3.0
with: with:
context: . context: .
push: true push: true
platforms: linux/amd64,linux/arm64 platforms: linux/amd64
tags: tobyxdd/hysteria:latest,tobyxdd/hysteria:v2,tobyxdd/hysteria:${{ steps.get_version.outputs.version }} tags: |
${{ env.REGISTRY }}/${{ github.repository_owner }}/hysteria
- name: Image digest ${{ env.REGISTRY }}/${{ github.repository_owner }}/hysteria:latest
run: echo ${{ steps.docker_build.outputs.digest }} ${{ env.REGISTRY }}/${{ github.repository_owner }}/hysteria:${{ steps.get_version.outputs.version }}
# Sign the resulting Docker image digest except on PRs.
# This will only write to the public Rekor transparency log when the Docker
# repository is public to avoid leaking data. If you would like to publish
# transparency data even for private images, pass --force to cosign below.
# https://github.com/sigstore/cosign
- name: Sign the published Docker image
env:
# https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable
TAGS: ${{ steps.meta.outputs.tags }}
DIGEST: ${{ steps.build-and-push.outputs.digest }}
# This step uses the identity token to provision an ephemeral certificate
# against the sigstore community Fulcio instance.
run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST}

View File

@ -29,11 +29,12 @@ RUN if [ ! -e /etc/nsswitch.conf ]; then echo 'hosts: files dns' > /etc/nsswitch
# #
# Do not try to add the "--no-cache" option when there are multiple "apk" # Do not try to add the "--no-cache" option when there are multiple "apk"
# commands, this will cause the build process to become very slow. # commands, this will cause the build process to become very slow.
COPY ./entrypoint /usr/local/bin/entrypoint
RUN set -ex \ RUN set -ex \
&& apk upgrade \ && apk upgrade \
&& apk add bash tzdata ca-certificates \ && apk add bash tzdata ca-certificates \
&& rm -rf /var/cache/apk/* && rm -rf /var/cache/apk/* \
&& chmod +x /usr/local/bin/entrypoint
COPY --from=builder /go/bin/hysteria /usr/local/bin/hysteria COPY --from=builder /go/bin/hysteria /usr/local/bin/hysteria
CMD ["entrypoint"]
ENTRYPOINT ["hysteria"]

View File

@ -1,60 +1,49 @@
# ![Hysteria 2](logo.svg) # ![Hysteria 2](logo.svg)
[![License][1]][2] [![Release][3]][4] [![Telegram][5]][6] [![Discussions][7]][8] # 支持对接V2board面板的Hysteria2后端
[1]: https://img.shields.io/badge/license-MIT-blue ### 项目说明
[2]: LICENSE.md 本项目基于hysteria官方内核二次开发添加了从v2b获取节点信息、用户鉴权信息与上报用户流量的功能。
[3]: https://img.shields.io/github/v/release/apernet/hysteria?style=flat-square 性能方面已经由hysteria2内核作者亲自指导优化过了。
[4]: https://github.com/apernet/hysteria/releases
[5]: https://img.shields.io/badge/chat-Telegram-blue?style=flat-square
[6]: https://t.me/hysteria_github
[7]: https://img.shields.io/github/discussions/apernet/hysteria?style=flat-square
[8]: https://github.com/apernet/hysteria/discussions
<h2 style="text-align: center;">Hysteria is a powerful, lightning fast and censorship resistant proxy.</h2> ### TG交流群
欢迎加入交流群 [点击加入](https://t.me/+DcRt8AB2VbI2Yzc1)
### [Get Started](https://v2.hysteria.network/)
### [中文文档](https://v2.hysteria.network/zh/) ### 示例配置
```
v2board:
apiHost: https://面板地址
apiKey: 面板节点密钥
nodeID: 节点ID
tls:
type: tls
cert: /etc/hysteria/tls.crt
key: /etc/hysteria/tls.key
auth:
type: v2board
trafficStats:
listen: 127.0.0.1:7653
acl:
inline:
- reject(10.0.0.0/8)
- reject(172.16.0.0/12)
- reject(192.168.0.0/16)
- reject(127.0.0.0/8)
- reject(fc00::/7)
```
> 其他配置完全与hysteria文档的一致可以查看hysteria2官方文档 [点击查看](https://hysteria.network/zh/docs/getting-started/Installation/)
### [Hysteria 1.x (legacy)](https://v1.hysteria.network/) ### 快速启动
```
--- docker run -itd --restart=always --network=host \
-e apiHost=https://example.com \
<div class="feature-grid"> -e apiKey=xxxxxxxxxxxxxxxxxxxxx \
<div> -e domain=hy2.example.com \
<h3>🛠️ Jack of all trades</h3> -e nodeID=1 \
<p>Wide range of modes including SOCKS5, HTTP Proxy, TCP/UDP Forwarding, Linux TProxy, TUN - with more features being added constantly.</p> ghcr.io/cedar2025/hysteria:latest
</div> ```
### docker 仓库
<div> ```
<h3>⚡ Blazing fast</h3> docker pull ghcr.io/cedar2025/hysteria:latest
<p>Powered by a customized QUIC protocol, Hysteria is designed to deliver unparalleled performance over unreliable and lossy networks.</p> ```
</div>
<div>
<h3>✊ Censorship resistant</h3>
<p>The protocol masquerades as standard HTTP/3 traffic, making it very difficult for censors to detect and block without widespread collateral damage.</p>
</div>
<div>
<h3>💻 Cross-platform</h3>
<p>We have builds for every major platform and architecture. Deploy anywhere & use everywhere. Not to mention the long list of 3rd party apps.</p>
</div>
<div>
<h3>🔗 Easy integration</h3>
<p>With built-in support for custom authentication, traffic statistics & access control, Hysteria is easy to integrate into your infrastructure.</p>
</div>
<div>
<h3>🤗 Chill and supportive</h3>
<p>We have well-documented specifications and code for developers to contribute and/or build their own apps. And a helpful community, too.</p>
</div>
</div>
---
**If you find Hysteria useful, consider giving it a ⭐️!**
[![Star History Chart](https://api.star-history.com/svg?repos=apernet/hysteria&type=Date)](https://star-history.com/#apernet/hysteria&Date)

View File

@ -6,6 +6,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io"
"net" "net"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
@ -54,6 +55,7 @@ func init() {
} }
type serverConfig struct { type serverConfig struct {
V2board *v2boardConfig `mapstructure:"v2board"`
Listen string `mapstructure:"listen"` Listen string `mapstructure:"listen"`
Obfs serverConfigObfs `mapstructure:"obfs"` Obfs serverConfigObfs `mapstructure:"obfs"`
TLS *serverConfigTLS `mapstructure:"tls"` TLS *serverConfigTLS `mapstructure:"tls"`
@ -73,6 +75,14 @@ type serverConfig struct {
Masquerade serverConfigMasquerade `mapstructure:"masquerade"` Masquerade serverConfigMasquerade `mapstructure:"masquerade"`
} }
type v2boardConfig struct {
ApiHost string `mapstructure:"apiHost"`
ApiKey string `mapstructure:"apiKey"`
NodeID uint `mapstructure:"nodeID"`
PullInterval time.Duration `mapstructure:"pullInterval"`
PushInterval time.Duration `mapstructure:"pushInterval"`
}
type serverConfigObfsSalamander struct { type serverConfigObfsSalamander struct {
Password string `mapstructure:"password"` Password string `mapstructure:"password"`
} }
@ -764,6 +774,14 @@ func (c *serverConfig) fillAuthenticator(hyConfig *server.Config) error {
return configError{Field: "auth.command", Err: errors.New("empty auth command")} return configError{Field: "auth.command", Err: errors.New("empty auth command")}
} }
hyConfig.Authenticator = &auth.CommandAuthenticator{Cmd: c.Auth.Command} hyConfig.Authenticator = &auth.CommandAuthenticator{Cmd: c.Auth.Command}
return nil
case "v2board":
v2boardConfig := c.V2board
if v2boardConfig.ApiHost == "" || v2boardConfig.ApiKey == "" || v2boardConfig.NodeID == 0 {
return configError{Field: "auth.v2board", Err: errors.New("v2board config error")}
}
hyConfig.Authenticator = &auth.V2boardApiProvider{URL: fmt.Sprintf("%s?token=%s&node_id=%d&node_type=hysteria", c.V2board.ApiHost+"/api/v1/server/UniProxy/user", c.V2board.ApiKey, c.V2board.NodeID)}
return nil return nil
default: default:
return configError{Field: "auth.type", Err: errors.New("unsupported auth type")} return configError{Field: "auth.type", Err: errors.New("unsupported auth type")}
@ -776,11 +794,28 @@ func (c *serverConfig) fillEventLogger(hyConfig *server.Config) error {
} }
func (c *serverConfig) fillTrafficLogger(hyConfig *server.Config) error { func (c *serverConfig) fillTrafficLogger(hyConfig *server.Config) error {
pullInterval := time.Second * 5
if c.V2board.PullInterval > 0 {
pullInterval = time.Duration(c.V2board.PullInterval) * time.Second
}
pushInterval := time.Second * 60
if c.V2board.PushInterval > 0 {
pushInterval = time.Duration(c.V2board.PushInterval) * time.Second
}
userURL := fmt.Sprintf("%s?token=%s&node_id=%d&node_type=hysteria", c.V2board.ApiHost+"/api/v1/server/UniProxy/user", c.V2board.ApiKey, c.V2board.NodeID)
pushURL := fmt.Sprintf("%s?token=%s&node_id=%d&node_type=hysteria", c.V2board.ApiHost+"/api/v1/server/UniProxy/push", c.V2board.ApiKey, c.V2board.NodeID)
if c.TrafficStats.Listen != "" { if c.TrafficStats.Listen != "" {
tss := trafficlogger.NewTrafficStatsServer(c.TrafficStats.Secret) tss := trafficlogger.NewTrafficStatsServer(c.TrafficStats.Secret)
hyConfig.TrafficLogger = tss hyConfig.TrafficLogger = tss
if c.V2board != nil && c.V2board.ApiHost != "" {
go auth.UpdateUsers(userURL, pullInterval, hyConfig.TrafficLogger)
go hyConfig.TrafficLogger.PushTrafficToV2boardInterval(pushURL, pushInterval)
}
go runTrafficStatsServer(c.TrafficStats.Listen, tss) go runTrafficStatsServer(c.TrafficStats.Listen, tss)
} else {
go auth.UpdateUsers(userURL, pullInterval, nil)
} }
return nil return nil
} }
@ -894,6 +929,19 @@ func (c *serverConfig) Config() (*server.Config, error) {
return hyConfig, nil return hyConfig, nil
} }
type ResponseNodeInfo struct {
Host string `json:"host"`
ServerPort uint `json:"server_port"`
ServerName string `json:"server_name"`
UpMbps uint `json:"down_mbps"`
DownMbps uint `json:"up_mbps"`
Obfs string `json:"obfs"`
BaseConfig struct {
PushInterval int `json:"push_interval"`
PullInterval int `json:"pull_interval"`
} `json:"base_config"`
}
func runServer(cmd *cobra.Command, args []string) { func runServer(cmd *cobra.Command, args []string) {
logger.Info("server mode") logger.Info("server mode")
@ -904,6 +952,48 @@ func runServer(cmd *cobra.Command, args []string) {
if err := viper.Unmarshal(&config); err != nil { if err := viper.Unmarshal(&config); err != nil {
logger.Fatal("failed to parse server config", zap.Error(err)) logger.Fatal("failed to parse server config", zap.Error(err))
} }
// 如果配置了v2board 则自动获取监听端口、obfs
if config.V2board != nil && config.V2board.ApiHost != "" {
// 创建一个url.Values来存储查询参数
queryParams := url.Values{
"token": {config.V2board.ApiKey},
"node_id": {strconv.Itoa(int(config.V2board.NodeID))},
"node_type": {"hysteria"},
}
nodeInfoUrl := config.V2board.ApiHost + "/api/v1/server/UniProxy/config?" + queryParams.Encode()
resp, err := http.Get(nodeInfoUrl)
if err != nil {
// 处理错误
fmt.Println("HTTP GET 请求出错:", err)
logger.Fatal("failed to client v2board api to get nodeInfo", zap.Error(err))
}
defer resp.Body.Close()
// 读取响应数据
body, err := io.ReadAll(resp.Body)
if err != nil {
logger.Fatal("failed to read v2board reaponse", zap.Error(err))
}
// 解析JSON数据
var responseNodeInfo ResponseNodeInfo
err = json.Unmarshal(body, &responseNodeInfo)
if err != nil {
logger.Fatal("failed to unmarshal v2board reaponse", zap.Error(err))
}
// 给 hy的端口、obfs、上行下行进行赋值
if responseNodeInfo.ServerPort != 0 {
config.Listen = ":" + strconv.Itoa(int(responseNodeInfo.ServerPort))
}
if responseNodeInfo.DownMbps != 0 {
config.Bandwidth.Down = strconv.Itoa(int(responseNodeInfo.DownMbps)) + "Mbps"
}
if responseNodeInfo.UpMbps != 0 {
config.Bandwidth.Up = strconv.Itoa(int(responseNodeInfo.UpMbps)) + "Mbps"
}
if responseNodeInfo.Obfs != "" {
config.Obfs.Type = "salamander"
config.Obfs.Salamander.Password = responseNodeInfo.Obfs
}
}
hyConfig, err := config.Config() hyConfig, err := config.Config()
if err != nil { if err != nil {
logger.Fatal("failed to load server config", zap.Error(err)) logger.Fatal("failed to load server config", zap.Error(err))

View File

@ -211,5 +211,7 @@ type EventLogger interface {
// The implementation of this interface must be thread-safe. // The implementation of this interface must be thread-safe.
type TrafficLogger interface { type TrafficLogger interface {
LogTraffic(id string, tx, rx uint64) (ok bool) LogTraffic(id string, tx, rx uint64) (ok bool)
PushTrafficToV2boardInterval(url string, interval time.Duration)
LogOnlineState(id string, online bool) LogOnlineState(id string, online bool)
NewKick(id string) (ok bool)
} }

50
entrypoint Normal file
View File

@ -0,0 +1,50 @@
#!/bin/sh
CONFIG_FILE="/etc/hysteria/server.yaml"
# 判断配置文件是否存存在,如果不存在走不存在的逻辑
if [ ! -f "$CONFIG_FILE" ]; then
echo "Creating configuration file $CONFIG_FILE"
mkdir -p /etc/hysteria
cat <<EOF >"$CONFIG_FILE"
v2board:
apiHost: ${apiHost}
apiKey: ${apiKey}
nodeID: ${nodeID}
acme:
domains:
- ${domain}
email: your@email.com
auth:
type: v2board
trafficStats:
listen: 127.0.0.1:7653
acl:
inline:
- reject(10.0.0.0/8)
- reject(172.16.0.0/12)
- reject(192.168.0.0/16)
- reject(127.0.0.0/8)
- reject(fc00::/7)
EOF
fi
hysteria server -c $CONFIG_FILE 2>&1 | tee &
# 获取HYSTERIA server命令的进程组IDProcess Group ID
HYSTERIA_PID=$!
# 定义一个函数来处理Ctrl+C信号
cleanup() {
echo "接收到Ctrl+C信号正在停止HYSTERIA..."
# 向HYSTERIA进程组发送终止信号
kill -SIGINT $HYSTERIA_PID
exit 0
}
# 捕获Ctrl+C信号并调用cleanup函数
trap cleanup INT
trap cleanup SIGTERM
# 等待HYSTERIA进程结束
wait

110
extras/auth/v2board.go Normal file
View File

@ -0,0 +1,110 @@
package auth
import (
"encoding/json"
"fmt"
"net"
"net/http"
"strconv"
"sync"
"time"
"github.com/apernet/hysteria/core/v2/server"
)
var _ server.Authenticator = &V2boardApiProvider{}
type V2boardApiProvider struct {
Client *http.Client
URL string
}
// 用户列表
var (
usersMap map[string]User
lock sync.Mutex
)
type User struct {
ID int `json:"id"`
UUID string `json:"uuid"`
SpeedLimit *uint32 `json:"speed_limit"`
}
type ResponseData struct {
Users []User `json:"users"`
}
func getUserList(url string) ([]User, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var responseData ResponseData
err = json.NewDecoder(resp.Body).Decode(&responseData)
if err != nil {
return nil, err
}
return responseData.Users, nil
}
func UpdateUsers(url string, interval time.Duration, trafficlogger server.TrafficLogger) {
fmt.Println("用户列表自动更新服务已激活,更新周期为", interval)
// 先立即执行一次更新
userList, err := getUserList(url)
if err != nil {
fmt.Println("Error:", err)
return // 如果第一次获取失败,退出函数
}
processUserList(userList, trafficlogger)
// 设置定时器
ticker := time.NewTicker(interval)
defer ticker.Stop()
for range ticker.C {
userList, err := getUserList(url)
if err != nil {
fmt.Println("Error:", err)
continue
}
processUserList(userList, trafficlogger)
}
}
// 处理用户列表的逻辑
func processUserList(userList []User, trafficlogger server.TrafficLogger) {
lock.Lock()
defer lock.Unlock()
newUsersMap := make(map[string]User)
for _, user := range userList {
newUsersMap[user.UUID] = user
}
if trafficlogger != nil {
for uuid := range usersMap {
if _, exists := newUsersMap[uuid]; !exists {
fmt.Println(usersMap[uuid].ID)
trafficlogger.NewKick(strconv.Itoa(usersMap[uuid].ID))
}
}
}
usersMap = newUsersMap
}
// 验证代码
func (v *V2boardApiProvider) Authenticate(addr net.Addr, auth string, tx uint64) (ok bool, id string) {
// 获取判断连接用户是否在用户列表内
lock.Lock()
defer lock.Unlock()
if user, exists := usersMap[auth]; exists {
return true, strconv.Itoa(user.ID)
}
return false, ""
}

View File

@ -1,10 +1,14 @@
package trafficlogger package trafficlogger
import ( import (
"bytes"
"encoding/json" "encoding/json"
"errors"
"fmt"
"net/http" "net/http"
"strconv" "strconv"
"sync" "sync"
"time"
"github.com/apernet/hysteria/core/v2/server" "github.com/apernet/hysteria/core/v2/server"
) )
@ -29,6 +33,57 @@ func NewTrafficStatsServer(secret string) TrafficStatsServer {
} }
} }
type TrafficPushRequest struct {
Data map[string][2]int64
}
// 定时提交用户流量情况
func (s *trafficStatsServerImpl) PushTrafficToV2boardInterval(url string, interval time.Duration) {
fmt.Println("用户流量情况监控已启动,提交周期为:", interval)
ticker := time.NewTicker(interval)
defer ticker.Stop()
for range ticker.C {
if err := s.PushTrafficToV2board(url); err != nil {
fmt.Println("用户流量信息提交失败:", err)
}
}
}
// 向v2board 提交用户流量使用情况
func (s *trafficStatsServerImpl) PushTrafficToV2board(url string) error {
s.Mutex.Lock() // 写锁,阻止其他操作 StatsMap 的并发访问
defer s.Mutex.Unlock() // 确保在函数退出时释放写锁
request := TrafficPushRequest{
Data: make(map[string][2]int64),
}
for id, stats := range s.StatsMap {
request.Data[id] = [2]int64{int64(stats.Tx), int64(stats.Rx)}
}
if len(request.Data) == 0 {
return nil
}
jsonData, err := json.Marshal(request.Data)
if err != nil {
return err
}
resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData))
if err != nil {
fmt.Println(resp)
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return errors.New("HTTP request failed with status code: " + resp.Status)
}
s.StatsMap = make(map[string]*trafficStatsEntry)
return nil
}
type trafficStatsServerImpl struct { type trafficStatsServerImpl struct {
Mutex sync.RWMutex Mutex sync.RWMutex
StatsMap map[string]*trafficStatsEntry StatsMap map[string]*trafficStatsEntry
@ -152,3 +207,11 @@ func (s *trafficStatsServerImpl) kick(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
} }
// 踢出用户名单
func (s *trafficStatsServerImpl) NewKick(id string) bool {
s.Mutex.Lock()
s.KickMap[id] = struct{}{}
s.Mutex.Unlock()
return true
}