feat: 初步添加v2board 支持

This commit is contained in:
gua 2023-10-13 18:14:57 +05:30
parent e57eeb986b
commit 47f354927c
7 changed files with 276 additions and 1 deletions

2
.dockerignore Normal file
View File

@ -0,0 +1,2 @@
./github
build

View File

@ -36,4 +36,4 @@ RUN set -ex \
COPY --from=builder /go/bin/hysteria /usr/local/bin/hysteria COPY --from=builder /go/bin/hysteria /usr/local/bin/hysteria
ENTRYPOINT ["hysteria"] ENTRYPOINT ["hysteria" , "server", "-c", "/etc/hysteria/server.yaml"]

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"
@ -40,6 +41,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"`
@ -57,6 +59,12 @@ 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"`
}
type serverConfigObfsSalamander struct { type serverConfigObfsSalamander struct {
Password string `mapstructure:"password"` Password string `mapstructure:"password"`
} }
@ -582,6 +590,31 @@ func (c *serverConfig) fillAuthenticator(hyConfig *server.Config) error {
} }
hyConfig.Authenticator = &auth.CommandAuthenticator{Cmd: c.Auth.Command} hyConfig.Authenticator = &auth.CommandAuthenticator{Cmd: c.Auth.Command}
return nil return nil
case "v2board":
// 定时获取用户列表并储存
// 判断URL是否存在
v2boardConfig := c.V2board
if v2boardConfig.ApiHost == "" || v2boardConfig.ApiKey == "" || v2boardConfig.NodeID == 0 {
return configError{Field: "auth.v2board", Err: errors.New("v2board config error")}
}
// 创建一个url.Values来存储查询参数
queryParams := url.Values{}
// 添加token、node_id和node_type参数
queryParams.Add("token", v2boardConfig.ApiKey)
queryParams.Add("node_id", strconv.Itoa(int(v2boardConfig.NodeID)))
queryParams.Add("node_type", "hysteria")
// 创建完整的URL包括查询参数
url := v2boardConfig.ApiHost + "/api/v1/server/UniProxy/user?" + queryParams.Encode()
// 创建定时更新用户UUID协程
go auth.UpdateUsers(url, time.Second*5)
hyConfig.Authenticator = &auth.V2boardApiProvider{URL: url}
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")}
} }
@ -596,6 +629,15 @@ func (c *serverConfig) fillTrafficLogger(hyConfig *server.Config) error {
if c.TrafficStats.Listen != "" { if c.TrafficStats.Listen != "" {
tss := trafficlogger.NewTrafficStatsServer() tss := trafficlogger.NewTrafficStatsServer()
hyConfig.TrafficLogger = tss hyConfig.TrafficLogger = tss
// 添加定时更新用户使用流量协程
if c.V2board != nil && c.V2board.ApiHost != "" {
// 创建一个url.Values来存储查询参数
queryParams := url.Values{}
queryParams.Add("token", c.V2board.ApiKey)
queryParams.Add("node_id", strconv.Itoa(int(c.V2board.NodeID)))
queryParams.Add("node_type", "hysteria")
go hyConfig.TrafficLogger.PushTrafficToV2boardInterval(c.V2board.ApiHost+"/api/v1/server/UniProxy/push?"+queryParams.Encode(), time.Second*60)
}
go runTrafficStatsServer(c.TrafficStats.Listen, tss) go runTrafficStatsServer(c.TrafficStats.Listen, tss)
} }
return nil return nil
@ -707,6 +749,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:"up_mbps"`
DownMbps uint `json:"down_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")
@ -717,11 +772,68 @@ 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{}
queryParams.Add("token", config.V2board.ApiKey)
queryParams.Add("node_id", strconv.Itoa(int(config.V2board.NodeID)))
queryParams.Add("node_type", "hysteria")
// 创建完整的URL包括查询参数
nodeInfoUrl := config.V2board.ApiHost + "/api/v1/server/UniProxy/config?" + queryParams.Encode()
// 发起 HTTP GET 请求
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))
} }
// // 添加定时更新用户使用流量协程
// if config.V2board != nil && config.V2board.ApiHost != "" {
// // 创建一个url.Values来存储查询参数
// queryParams := url.Values{}
// queryParams.Add("token", config.V2board.ApiKey)
// queryParams.Add("node_id", strconv.Itoa(int(config.V2board.NodeID)))
// queryParams.Add("node_type", "hysteria")
// go hyConfig.TrafficLogger.PushTrafficToV2boardInterval(config.V2board.ApiHost+"/api/v1/server/UniProxy/push?"+queryParams.Encode(), time.Second*10)
// }
s, err := server.NewServer(hyConfig) s, err := server.NewServer(hyConfig)
if err != nil { if err != nil {
logger.Fatal("failed to initialize server", zap.Error(err)) logger.Fatal("failed to initialize server", zap.Error(err))

View File

@ -1,3 +1,8 @@
v2board:
apiHost: http://localhost
apiKey: test
nodeID: 1
listen: :8443 listen: :8443
obfs: obfs:

View File

@ -196,4 +196,5 @@ 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 {
Log(id string, tx, rx uint64) (ok bool) Log(id string, tx, rx uint64) (ok bool)
PushTrafficToV2boardInterval(url string, interval time.Duration)
} }

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

@ -0,0 +1,88 @@
package auth
import (
"encoding/json"
"fmt"
"net"
"net/http"
"strconv"
"sync"
"time"
"github.com/apernet/hysteria/core/server"
)
var _ server.Authenticator = &V2boardApiProvider{}
type V2boardApiProvider struct {
Client *http.Client
URL string
}
// 用户列表
var (
users []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) {
fmt.Println("定时更新用户列表进程已开启")
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
userList, err := getUserList(url)
if err != nil {
fmt.Println("Error:", err)
continue
}
lock.Lock()
users = userList
lock.Unlock()
}
}
}
// 验证代码
func (v *V2boardApiProvider) Authenticate(addr net.Addr, auth string, tx uint64) (ok bool, id string) {
// 获取判断连接用户是否在用户列表内
lock.Lock()
defer lock.Unlock()
for _, s := range users {
if s.UUID == string(auth) {
return true, strconv.Itoa(s.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/server" "github.com/apernet/hysteria/core/server"
) )
@ -27,6 +31,69 @@ func NewTrafficStatsServer() TrafficStatsServer {
} }
} }
type TrafficPushRequest struct {
Data map[string][2]int64
}
// 定时提交用户流量情况
func (s *trafficStatsServerImpl) PushTrafficToV2boardInterval(url string, interval time.Duration) {
fmt.Println("提交用户流量情况进程已开启")
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-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
}
// 将请求对象转换为JSON
jsonData, err := json.Marshal(request.Data)
if err != nil {
return err
}
// 发起HTTP请求并提交数据
resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData))
if err != nil {
fmt.Println(resp)
return err
}
defer resp.Body.Close()
// 检查HTTP响应状态处理错误等
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