mirror of
https://github.com/XrayR-project/XrayR.git
synced 2025-06-08 05:19:54 +00:00
feature: add support v2board new API
update: add eTag on fetch users for save resources update: split cert monitor service update: refactor log format update: add support shadowsocks_2022 The cert monitor period = UpdatePeriodic * 60 The xray-core official is not implementation of proxy.UserManager feature. So it may change in the future.
This commit is contained in:
parent
2f10c3f6b8
commit
183b1be519
@ -42,6 +42,7 @@ type NodeInfo struct {
|
||||
TLSType string
|
||||
EnableVless bool
|
||||
CypherMethod string
|
||||
ServerKey string
|
||||
ServiceName string
|
||||
Header json.RawMessage
|
||||
}
|
||||
|
7
api/newV2board/model.go
Normal file
7
api/newV2board/model.go
Normal file
@ -0,0 +1,7 @@
|
||||
package newV2board
|
||||
|
||||
type UserTraffic struct {
|
||||
UID int `json:"user_id"`
|
||||
Upload int64 `json:"u"`
|
||||
Download int64 `json:"d"`
|
||||
}
|
371
api/newV2board/v2board.go
Normal file
371
api/newV2board/v2board.go
Normal file
@ -0,0 +1,371 @@
|
||||
package newV2board
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/bitly/go-simplejson"
|
||||
"github.com/go-resty/resty/v2"
|
||||
|
||||
"github.com/XrayR-project/XrayR/api"
|
||||
)
|
||||
|
||||
// APIClient create an api client to the panel.
|
||||
type APIClient struct {
|
||||
client *resty.Client
|
||||
APIHost string
|
||||
NodeID int
|
||||
Key string
|
||||
NodeType string
|
||||
EnableVless bool
|
||||
EnableXTLS bool
|
||||
SpeedLimit float64
|
||||
DeviceLimit int
|
||||
LocalRuleList []api.DetectRule
|
||||
resp atomic.Value
|
||||
eTag string
|
||||
}
|
||||
|
||||
// New create an api instance
|
||||
func New(apiConfig *api.Config) *APIClient {
|
||||
client := resty.New()
|
||||
client.SetRetryCount(3)
|
||||
if apiConfig.Timeout > 0 {
|
||||
client.SetTimeout(time.Duration(apiConfig.Timeout) * time.Second)
|
||||
} else {
|
||||
client.SetTimeout(5 * time.Second)
|
||||
}
|
||||
client.OnError(func(req *resty.Request, err error) {
|
||||
if v, ok := err.(*resty.ResponseError); ok {
|
||||
// v.Response contains the last response from the server
|
||||
// v.Err contains the original error
|
||||
log.Print(v.Err)
|
||||
}
|
||||
})
|
||||
client.SetBaseURL(apiConfig.APIHost)
|
||||
// Create Key for each requests
|
||||
client.SetQueryParams(map[string]string{
|
||||
"node_id": strconv.Itoa(apiConfig.NodeID),
|
||||
"node_type": strings.ToLower(apiConfig.NodeType),
|
||||
"token": apiConfig.Key,
|
||||
})
|
||||
// Read local rule list
|
||||
localRuleList := readLocalRuleList(apiConfig.RuleListPath)
|
||||
apiClient := &APIClient{
|
||||
client: client,
|
||||
NodeID: apiConfig.NodeID,
|
||||
Key: apiConfig.Key,
|
||||
APIHost: apiConfig.APIHost,
|
||||
NodeType: apiConfig.NodeType,
|
||||
EnableVless: apiConfig.EnableVless,
|
||||
EnableXTLS: apiConfig.EnableXTLS,
|
||||
SpeedLimit: apiConfig.SpeedLimit,
|
||||
DeviceLimit: apiConfig.DeviceLimit,
|
||||
LocalRuleList: localRuleList,
|
||||
}
|
||||
return apiClient
|
||||
}
|
||||
|
||||
// readLocalRuleList reads the local rule list file
|
||||
func readLocalRuleList(path string) (LocalRuleList []api.DetectRule) {
|
||||
LocalRuleList = make([]api.DetectRule, 0)
|
||||
|
||||
if path != "" {
|
||||
// open the file
|
||||
file, err := os.Open(path)
|
||||
|
||||
// handle errors while opening
|
||||
if err != nil {
|
||||
log.Printf("Error when opening file: %s", err)
|
||||
return LocalRuleList
|
||||
}
|
||||
|
||||
fileScanner := bufio.NewScanner(file)
|
||||
|
||||
// read line by line
|
||||
for fileScanner.Scan() {
|
||||
LocalRuleList = append(LocalRuleList, api.DetectRule{
|
||||
ID: -1,
|
||||
Pattern: regexp.MustCompile(fileScanner.Text()),
|
||||
})
|
||||
}
|
||||
// handle first encountered error while reading
|
||||
if err := fileScanner.Err(); err != nil {
|
||||
log.Fatalf("Error while reading file: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
file.Close()
|
||||
}
|
||||
|
||||
return LocalRuleList
|
||||
}
|
||||
|
||||
// Describe return a description of the client
|
||||
func (c *APIClient) Describe() api.ClientInfo {
|
||||
return api.ClientInfo{APIHost: c.APIHost, NodeID: c.NodeID, Key: c.Key, NodeType: c.NodeType}
|
||||
}
|
||||
|
||||
// Debug set the client debug for client
|
||||
func (c *APIClient) Debug() {
|
||||
c.client.SetDebug(true)
|
||||
}
|
||||
|
||||
func (c *APIClient) assembleURL(path string) string {
|
||||
return c.APIHost + path
|
||||
}
|
||||
|
||||
func (c *APIClient) parseResponse(res *resty.Response, path string, err error) (*simplejson.Json, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request %s failed: %s", c.assembleURL(path), err)
|
||||
}
|
||||
|
||||
if res.StatusCode() > 399 {
|
||||
body := res.Body()
|
||||
return nil, fmt.Errorf("request %s failed: %s, %s", c.assembleURL(path), string(body), err)
|
||||
}
|
||||
rtn, err := simplejson.NewJson(res.Body())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ret %s invalid", res.String())
|
||||
}
|
||||
return rtn, nil
|
||||
}
|
||||
|
||||
// GetNodeInfo will pull NodeInfo Config from panel
|
||||
func (c *APIClient) GetNodeInfo() (nodeInfo *api.NodeInfo, err error) {
|
||||
path := "/api/v1/server/UniProxy/config"
|
||||
|
||||
res, err := c.client.R().
|
||||
ForceContentType("application/json").
|
||||
Get(path)
|
||||
|
||||
response, err := c.parseResponse(res, path, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.resp.Store(response)
|
||||
|
||||
switch c.NodeType {
|
||||
case "V2ray":
|
||||
nodeInfo, err = c.parseV2rayNodeResponse(response)
|
||||
case "Trojan":
|
||||
nodeInfo, err = c.parseTrojanNodeResponse(response)
|
||||
case "Shadowsocks":
|
||||
nodeInfo, err = c.parseSSNodeResponse(response)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported Node type: %s", c.NodeType)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
res, _ := response.MarshalJSON()
|
||||
return nil, fmt.Errorf("Parse node info failed: %s, \nError: %s", string(res), err)
|
||||
}
|
||||
|
||||
return nodeInfo, nil
|
||||
}
|
||||
|
||||
// GetUserList will pull user form panel
|
||||
func (c *APIClient) GetUserList() (UserList *[]api.UserInfo, err error) {
|
||||
path := "/api/v1/server/UniProxy/user"
|
||||
|
||||
switch c.NodeType {
|
||||
case "V2ray", "Trojan", "Shadowsocks":
|
||||
break
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported Node type: %s", c.NodeType)
|
||||
}
|
||||
|
||||
res, err := c.client.R().
|
||||
SetHeader("If-None-Match", c.eTag).
|
||||
ForceContentType("application/json").
|
||||
Get(path)
|
||||
|
||||
// Etag identifier for a specific version of a resource. StatusCode = 304 means no changed
|
||||
if res.StatusCode() == 304 {
|
||||
return nil, errors.New("users no change")
|
||||
}
|
||||
// update etag
|
||||
if res.Header().Get("Etag") != "" && res.Header().Get("Etag") != c.eTag {
|
||||
c.eTag = res.Header().Get("Etag")
|
||||
}
|
||||
|
||||
response, err := c.parseResponse(res, path, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
numOfUsers := len(response.Get("users").MustArray())
|
||||
userList := make([]api.UserInfo, numOfUsers)
|
||||
for i := 0; i < numOfUsers; i++ {
|
||||
user := response.Get("users").GetIndex(i)
|
||||
u := api.UserInfo{}
|
||||
u.UID = user.Get("id").MustInt()
|
||||
u.SpeedLimit = uint64(c.SpeedLimit * 1000000 / 8) // todo waiting v2board send configuration
|
||||
u.DeviceLimit = c.DeviceLimit // todo waiting v2board send configuration
|
||||
u.UUID = user.Get("uuid").MustString()
|
||||
u.Email = u.UUID + "@v2board.user"
|
||||
if c.NodeType == "Shadowsocks" {
|
||||
u.Passwd = u.UUID
|
||||
}
|
||||
userList[i] = u
|
||||
}
|
||||
|
||||
return &userList, nil
|
||||
}
|
||||
|
||||
// ReportUserTraffic reports the user traffic
|
||||
func (c *APIClient) ReportUserTraffic(userTraffic *[]api.UserTraffic) error {
|
||||
path := "/api/v1/server/UniProxy/push"
|
||||
|
||||
// json structure: {uid1: [u, d], uid2: [u, d], uid1: [u, d], uid3: [u, d]}
|
||||
data := make(map[int][]int64, len(*userTraffic))
|
||||
for _, traffic := range *userTraffic {
|
||||
data[traffic.UID] = []int64{traffic.Upload, traffic.Download}
|
||||
}
|
||||
|
||||
res, err := c.client.R().
|
||||
SetBody(data).
|
||||
ForceContentType("application/json").
|
||||
Post(path)
|
||||
_, err = c.parseResponse(res, path, err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetNodeRule implements the API interface
|
||||
func (c *APIClient) GetNodeRule() (*[]api.DetectRule, error) {
|
||||
ruleList := c.LocalRuleList
|
||||
if c.NodeType != "V2ray" {
|
||||
return &ruleList, nil
|
||||
}
|
||||
|
||||
// V2board only support the rule for v2ray
|
||||
nodeInfoResponse := c.resp.Load().(*simplejson.Json) // todo waiting v2board send configuration
|
||||
for i, rule := range nodeInfoResponse.Get("rules").MustStringArray() {
|
||||
rule = strings.TrimPrefix(rule, "regexp:")
|
||||
ruleListItem := api.DetectRule{
|
||||
ID: i,
|
||||
Pattern: regexp.MustCompile(rule),
|
||||
}
|
||||
ruleList = append(ruleList, ruleListItem)
|
||||
}
|
||||
|
||||
return &ruleList, nil
|
||||
}
|
||||
|
||||
// ReportNodeStatus implements the API interface
|
||||
func (c *APIClient) ReportNodeStatus(nodeStatus *api.NodeStatus) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReportNodeOnlineUsers implements the API interface
|
||||
func (c *APIClient) ReportNodeOnlineUsers(onlineUserList *[]api.OnlineUser) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReportIllegal implements the API interface
|
||||
func (c *APIClient) ReportIllegal(detectResultList *[]api.DetectResult) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseTrojanNodeResponse parse the response for the given nodeInfo format
|
||||
func (c *APIClient) parseTrojanNodeResponse(nodeInfoResponse *simplejson.Json) (*api.NodeInfo, error) {
|
||||
var TLSType = "tls"
|
||||
if c.EnableXTLS {
|
||||
TLSType = "xtls"
|
||||
}
|
||||
|
||||
// Create GeneralNodeInfo
|
||||
nodeInfo := &api.NodeInfo{
|
||||
NodeType: c.NodeType,
|
||||
NodeID: c.NodeID,
|
||||
Port: uint32(nodeInfoResponse.Get("server_port").MustUint64()),
|
||||
TransportProtocol: "tcp",
|
||||
EnableTLS: true,
|
||||
TLSType: TLSType,
|
||||
Host: nodeInfoResponse.Get("host").MustString(),
|
||||
ServiceName: nodeInfoResponse.Get("server_name").MustString(),
|
||||
}
|
||||
return nodeInfo, nil
|
||||
}
|
||||
|
||||
// parseSSNodeResponse parse the response for the given nodeInfo format
|
||||
func (c *APIClient) parseSSNodeResponse(nodeInfoResponse *simplejson.Json) (*api.NodeInfo, error) {
|
||||
// Create GeneralNodeInfo
|
||||
return &api.NodeInfo{
|
||||
NodeType: c.NodeType,
|
||||
NodeID: c.NodeID,
|
||||
Port: uint32(nodeInfoResponse.Get("server_port").MustUint64()),
|
||||
TransportProtocol: "tcp",
|
||||
CypherMethod: nodeInfoResponse.Get("cipher").MustString(),
|
||||
ServerKey: nodeInfoResponse.Get("server_key").MustString(), // shadowsocks2022 share key
|
||||
}, nil
|
||||
}
|
||||
|
||||
// parseV2rayNodeResponse parse the response for the given nodeInfo format
|
||||
func (c *APIClient) parseV2rayNodeResponse(nodeInfoResponse *simplejson.Json) (*api.NodeInfo, error) {
|
||||
var (
|
||||
TLSType = "tls"
|
||||
path, host, serviceName string
|
||||
header json.RawMessage
|
||||
enableTLS bool
|
||||
alterID uint16 = 0
|
||||
)
|
||||
|
||||
if c.EnableXTLS {
|
||||
TLSType = "xtls"
|
||||
}
|
||||
|
||||
transportProtocol := nodeInfoResponse.Get("network").MustString()
|
||||
|
||||
switch transportProtocol {
|
||||
case "ws":
|
||||
path = nodeInfoResponse.Get("networkSettings").Get("path").MustString()
|
||||
host = nodeInfoResponse.Get("networkSettings").Get("headers").Get("Host").MustString()
|
||||
case "grpc":
|
||||
if data, ok := nodeInfoResponse.Get("networkSettings").CheckGet("serviceName"); ok {
|
||||
serviceName = data.MustString()
|
||||
}
|
||||
case "tcp":
|
||||
if data, ok := nodeInfoResponse.Get("networkSettings").CheckGet("headers"); ok {
|
||||
if httpHeader, err := data.MarshalJSON(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
header = httpHeader
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if nodeInfoResponse.Get("tls").MustInt() == 1 {
|
||||
enableTLS = true
|
||||
}
|
||||
|
||||
// Create GeneralNodeInfo
|
||||
return &api.NodeInfo{
|
||||
NodeType: c.NodeType,
|
||||
NodeID: c.NodeID,
|
||||
Port: uint32(nodeInfoResponse.Get("server_port").MustUint64()),
|
||||
AlterID: alterID,
|
||||
TransportProtocol: transportProtocol,
|
||||
EnableTLS: enableTLS,
|
||||
TLSType: TLSType,
|
||||
Path: path,
|
||||
Host: host,
|
||||
EnableVless: c.EnableVless,
|
||||
ServiceName: serviceName,
|
||||
Header: header,
|
||||
}, nil
|
||||
}
|
101
api/newV2board/v2board_test.go
Normal file
101
api/newV2board/v2board_test.go
Normal file
@ -0,0 +1,101 @@
|
||||
package newV2board_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/XrayR-project/XrayR/api"
|
||||
"github.com/XrayR-project/XrayR/api/newV2board"
|
||||
)
|
||||
|
||||
func CreateClient() api.API {
|
||||
apiConfig := &api.Config{
|
||||
APIHost: "http://localhost:9897",
|
||||
Key: "qwertyuiopasdfghjkl",
|
||||
NodeID: 1,
|
||||
NodeType: "V2ray",
|
||||
}
|
||||
client := newV2board.New(apiConfig)
|
||||
return client
|
||||
}
|
||||
|
||||
func TestGetV2rayNodeInfo(t *testing.T) {
|
||||
client := CreateClient()
|
||||
nodeInfo, err := client.GetNodeInfo()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
t.Log(nodeInfo)
|
||||
}
|
||||
|
||||
func TestGetSSNodeInfo(t *testing.T) {
|
||||
apiConfig := &api.Config{
|
||||
APIHost: "http://127.0.0.1:668",
|
||||
Key: "qwertyuiopasdfghjkl",
|
||||
NodeID: 1,
|
||||
NodeType: "Shadowsocks",
|
||||
}
|
||||
client := newV2board.New(apiConfig)
|
||||
nodeInfo, err := client.GetNodeInfo()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
t.Log(nodeInfo)
|
||||
}
|
||||
|
||||
func TestGetTrojanNodeInfo(t *testing.T) {
|
||||
apiConfig := &api.Config{
|
||||
APIHost: "http://127.0.0.1:668",
|
||||
Key: "qwertyuiopasdfghjkl",
|
||||
NodeID: 1,
|
||||
NodeType: "Trojan",
|
||||
}
|
||||
client := newV2board.New(apiConfig)
|
||||
nodeInfo, err := client.GetNodeInfo()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
t.Log(nodeInfo)
|
||||
}
|
||||
|
||||
func TestGetUserList(t *testing.T) {
|
||||
client := CreateClient()
|
||||
|
||||
userList, err := client.GetUserList()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
t.Log(userList)
|
||||
}
|
||||
|
||||
func TestReportReportUserTraffic(t *testing.T) {
|
||||
client := CreateClient()
|
||||
userList, err := client.GetUserList()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
generalUserTraffic := make([]api.UserTraffic, len(*userList))
|
||||
for i, userInfo := range *userList {
|
||||
generalUserTraffic[i] = api.UserTraffic{
|
||||
UID: userInfo.UID,
|
||||
Upload: 114514,
|
||||
Download: 114514,
|
||||
}
|
||||
}
|
||||
// client.Debug()
|
||||
err = client.ReportUserTraffic(&generalUserTraffic)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetNodeRule(t *testing.T) {
|
||||
client := CreateClient()
|
||||
client.Debug()
|
||||
ruleList, err := client.GetNodeRule()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
t.Log(ruleList)
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
// Deprecated: after 2023.6.1
|
||||
package v2board
|
||||
|
||||
type UserTraffic struct {
|
||||
|
4
go.mod
4
go.mod
@ -11,6 +11,8 @@ require (
|
||||
github.com/go-resty/resty/v2 v2.7.0
|
||||
github.com/imdario/mergo v0.3.13
|
||||
github.com/r3labs/diff/v2 v2.15.1
|
||||
github.com/sagernet/sing v0.0.0-20220801112236-1bb95f9661fc
|
||||
github.com/sagernet/sing-shadowsocks v0.0.0-20220801112336-a91eacdd01e1
|
||||
github.com/shirou/gopsutil/v3 v3.22.10
|
||||
github.com/spf13/viper v1.14.0
|
||||
github.com/stretchr/testify v1.8.1
|
||||
@ -129,8 +131,6 @@ require (
|
||||
github.com/sacloud/go-http v0.1.2 // indirect
|
||||
github.com/sacloud/iaas-api-go v1.3.2 // indirect
|
||||
github.com/sacloud/packages-go v0.0.5 // indirect
|
||||
github.com/sagernet/sing v0.0.0-20220801112236-1bb95f9661fc // indirect
|
||||
github.com/sagernet/sing-shadowsocks v0.0.0-20220801112336-a91eacdd01e1 // indirect
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.9 // indirect
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb // indirect
|
||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/XrayR-project/XrayR/api/newV2board"
|
||||
"github.com/XrayR-project/XrayR/app/mydispatcher"
|
||||
|
||||
"github.com/imdario/mergo"
|
||||
@ -168,8 +169,11 @@ func (p *Panel) Start() {
|
||||
switch nodeConfig.PanelType {
|
||||
case "SSpanel":
|
||||
apiClient = sspanel.New(nodeConfig.ApiConfig)
|
||||
// todo Deprecated after 2023.6.1
|
||||
case "V2board":
|
||||
apiClient = v2board.New(nodeConfig.ApiConfig)
|
||||
case "NewV2board":
|
||||
apiClient = newV2board.New(nodeConfig.ApiConfig)
|
||||
case "PMpanel":
|
||||
apiClient = pmpanel.New(nodeConfig.ApiConfig)
|
||||
case "Proxypanel":
|
||||
|
@ -37,6 +37,7 @@ type Controller struct {
|
||||
userList *[]api.UserInfo
|
||||
nodeInfoMonitorPeriodic *task.Periodic
|
||||
userReportPeriodic *task.Periodic
|
||||
renewCertPeriodic *task.Periodic
|
||||
limitedUsers map[api.UserInfo]LimitInfo
|
||||
warnedUsers map[api.UserInfo]int
|
||||
panelType string
|
||||
@ -118,6 +119,10 @@ func (c *Controller) Start() error {
|
||||
Interval: time.Duration(c.config.UpdatePeriodic) * time.Second,
|
||||
Execute: c.userInfoMonitor,
|
||||
}
|
||||
c.renewCertPeriodic = &task.Periodic{
|
||||
Interval: time.Duration(c.config.UpdatePeriodic) * time.Second * 60,
|
||||
Execute: c.certMonitor,
|
||||
}
|
||||
if c.config.AutoSpeedLimitConfig == nil {
|
||||
c.config.AutoSpeedLimitConfig = &AutoSpeedLimitConfig{0, 0, 0, 0}
|
||||
}
|
||||
@ -127,13 +132,17 @@ func (c *Controller) Start() error {
|
||||
}
|
||||
|
||||
// start nodeInfoMonitor
|
||||
log.Printf("[%s: %d] Start monitor node status", c.nodeInfo.NodeType, c.nodeInfo.NodeID)
|
||||
log.Printf("%s Start monitor node status", c.logPrefix())
|
||||
go c.nodeInfoMonitorPeriodic.Start()
|
||||
|
||||
// start userReport
|
||||
log.Printf("[%s: %d] Start report node status", c.nodeInfo.NodeType, c.nodeInfo.NodeID)
|
||||
log.Printf("%s Start report user status", c.logPrefix())
|
||||
go c.userReportPeriodic.Start()
|
||||
|
||||
// start cert monitor
|
||||
log.Printf("%s Start monitor cert status", c.logPrefix())
|
||||
go c.renewCertPeriodic.Start()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -142,14 +151,21 @@ func (c *Controller) Close() error {
|
||||
if c.nodeInfoMonitorPeriodic != nil {
|
||||
err := c.nodeInfoMonitorPeriodic.Close()
|
||||
if err != nil {
|
||||
log.Panicf("node info periodic close failed: %s", err)
|
||||
log.Panicf("%s node info periodic close failed: %s", c.logPrefix(), err)
|
||||
}
|
||||
}
|
||||
|
||||
if c.nodeInfoMonitorPeriodic != nil {
|
||||
err := c.userReportPeriodic.Close()
|
||||
if err != nil {
|
||||
log.Panicf("user report periodic close failed: %s", err)
|
||||
log.Panicf("%s user report periodic close failed: %s", c.logPrefix(), err)
|
||||
}
|
||||
}
|
||||
|
||||
if c.renewCertPeriodic != nil {
|
||||
err := c.renewCertPeriodic.Close()
|
||||
if err != nil {
|
||||
log.Panicf("%s renew cert periodic close failed: %s", c.logPrefix(), err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@ -169,11 +185,17 @@ func (c *Controller) nodeInfoMonitor() (err error) {
|
||||
}
|
||||
|
||||
// Update User
|
||||
var usersChanged = true
|
||||
newUserInfo, err := c.apiClient.GetUserList()
|
||||
if err != nil {
|
||||
if err.Error() == "users no change" {
|
||||
usersChanged = false
|
||||
newUserInfo = c.userList
|
||||
} else {
|
||||
log.Print(err)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var nodeInfoChanged = false
|
||||
// If nodeInfo changed
|
||||
@ -219,19 +241,6 @@ func (c *Controller) nodeInfoMonitor() (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Check Cert
|
||||
if c.nodeInfo.EnableTLS && (c.config.CertConfig.CertMode == "dns" || c.config.CertConfig.CertMode == "http") {
|
||||
lego, err := mylego.New(c.config.CertConfig)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
// Xray-core supports the OcspStapling certification hot renew
|
||||
_, _, _, err = lego.RenewCert()
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
}
|
||||
|
||||
if nodeInfoChanged {
|
||||
err = c.addNewUser(newUserInfo, newNodeInfo)
|
||||
if err != nil {
|
||||
@ -244,7 +253,9 @@ func (c *Controller) nodeInfoMonitor() (err error) {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
deleted, added := compareUserList(c.userList, newUserInfo)
|
||||
var deleted, added []api.UserInfo
|
||||
if usersChanged {
|
||||
deleted, added = compareUserList(c.userList, newUserInfo)
|
||||
if len(deleted) > 0 {
|
||||
deletedEmail := make([]string, len(deleted))
|
||||
for i, u := range deleted {
|
||||
@ -265,7 +276,8 @@ func (c *Controller) nodeInfoMonitor() (err error) {
|
||||
log.Print(err)
|
||||
}
|
||||
}
|
||||
log.Printf("[%s: %d] %d user deleted, %d user added", c.nodeInfo.NodeType, c.nodeInfo.NodeID, len(deleted), len(added))
|
||||
}
|
||||
log.Printf("%s %d user deleted, %d user added", c.logPrefix(), len(deleted), len(added))
|
||||
}
|
||||
c.userList = newUserInfo
|
||||
return nil
|
||||
@ -365,7 +377,8 @@ func (c *Controller) addInboundForSSPlugin(newNodeInfo api.NodeInfo) (err error)
|
||||
|
||||
func (c *Controller) addNewUser(userInfo *[]api.UserInfo, nodeInfo *api.NodeInfo) (err error) {
|
||||
users := make([]*protocol.User, 0)
|
||||
if nodeInfo.NodeType == "V2ray" {
|
||||
switch nodeInfo.NodeType {
|
||||
case "V2ray":
|
||||
if nodeInfo.EnableVless {
|
||||
users = c.buildVlessUser(userInfo)
|
||||
} else {
|
||||
@ -378,20 +391,21 @@ func (c *Controller) addNewUser(userInfo *[]api.UserInfo, nodeInfo *api.NodeInfo
|
||||
}
|
||||
users = c.buildVmessUser(userInfo, alterID)
|
||||
}
|
||||
} else if nodeInfo.NodeType == "Trojan" {
|
||||
case "Trojan":
|
||||
users = c.buildTrojanUser(userInfo)
|
||||
} else if nodeInfo.NodeType == "Shadowsocks" {
|
||||
case "Shadowsocks":
|
||||
users = c.buildSSUser(userInfo, nodeInfo.CypherMethod)
|
||||
} else if nodeInfo.NodeType == "Shadowsocks-Plugin" {
|
||||
case "Shadowsocks-Plugin":
|
||||
users = c.buildSSPluginUser(userInfo)
|
||||
} else {
|
||||
default:
|
||||
return fmt.Errorf("unsupported node type: %s", nodeInfo.NodeType)
|
||||
}
|
||||
|
||||
err = c.addUsers(users, c.Tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("[%s: %d] Added %d new users", c.nodeInfo.NodeType, c.nodeInfo.NodeID, len(*userInfo))
|
||||
log.Printf("%s Added %d new users", c.logPrefix(), len(*userInfo))
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -467,7 +481,7 @@ func (c *Controller) userInfoMonitor() (err error) {
|
||||
}
|
||||
// Unlock users
|
||||
if c.config.AutoSpeedLimitConfig.Limit > 0 && len(c.limitedUsers) > 0 {
|
||||
log.Printf("Limited users:")
|
||||
log.Printf("%s Limited users:", c.logPrefix())
|
||||
toReleaseUsers := make([]api.UserInfo, 0)
|
||||
for user, limitInfo := range c.limitedUsers {
|
||||
if time.Now().Unix() > limitInfo.end {
|
||||
@ -555,7 +569,7 @@ func (c *Controller) userInfoMonitor() (err error) {
|
||||
if err = c.apiClient.ReportNodeOnlineUsers(onlineDevice); err != nil {
|
||||
log.Print(err)
|
||||
} else {
|
||||
log.Printf("[%s: %d] Report %d online users", c.nodeInfo.NodeType, c.nodeInfo.NodeID, len(*onlineDevice))
|
||||
log.Printf("%s Report %d online users", c.logPrefix(), len(*onlineDevice))
|
||||
}
|
||||
}
|
||||
// Report Illegal user
|
||||
@ -565,7 +579,7 @@ func (c *Controller) userInfoMonitor() (err error) {
|
||||
if err = c.apiClient.ReportIllegal(detectResult); err != nil {
|
||||
log.Print(err)
|
||||
} else {
|
||||
log.Printf("[%s: %d] Report %d illegal behaviors", c.nodeInfo.NodeType, c.nodeInfo.NodeID, len(*detectResult))
|
||||
log.Printf("%s Report %d illegal behaviors", c.logPrefix(), len(*detectResult))
|
||||
}
|
||||
|
||||
}
|
||||
@ -575,3 +589,26 @@ func (c *Controller) userInfoMonitor() (err error) {
|
||||
func (c *Controller) buildNodeTag() string {
|
||||
return fmt.Sprintf("%s_%s_%d", c.nodeInfo.NodeType, c.config.ListenIP, c.nodeInfo.Port)
|
||||
}
|
||||
|
||||
func (c *Controller) logPrefix() string {
|
||||
return fmt.Sprintf("[%s] %s(ID=%d)", c.clientInfo.APIHost, c.nodeInfo.NodeType, c.nodeInfo.NodeID)
|
||||
}
|
||||
|
||||
// Check Cert
|
||||
func (c *Controller) certMonitor() error {
|
||||
if c.nodeInfo.EnableTLS {
|
||||
switch c.config.CertConfig.CertMode {
|
||||
case "dns", "http", "tls":
|
||||
lego, err := mylego.New(c.config.CertConfig)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
// Xray-core supports the OcspStapling certification hot renew
|
||||
_, _, _, err = lego.RenewCert()
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -2,11 +2,15 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-shadowsocks/shadowaead_2022"
|
||||
C "github.com/sagernet/sing/common"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/common/uuid"
|
||||
"github.com/xtls/xray-core/core"
|
||||
"github.com/xtls/xray-core/infra/conf"
|
||||
|
||||
@ -49,9 +53,10 @@ func InboundBuilder(config *Config, nodeInfo *api.NodeInfo, tag string) (*core.I
|
||||
setting json.RawMessage
|
||||
)
|
||||
|
||||
var proxySetting interface{}
|
||||
var proxySetting any
|
||||
// Build Protocol and Protocol setting
|
||||
if nodeInfo.NodeType == "V2ray" {
|
||||
switch nodeInfo.NodeType {
|
||||
case "V2ray":
|
||||
if nodeInfo.EnableVless {
|
||||
protocol = "vless"
|
||||
// Enable fallback
|
||||
@ -74,7 +79,7 @@ func InboundBuilder(config *Config, nodeInfo *api.NodeInfo, tag string) (*core.I
|
||||
protocol = "vmess"
|
||||
proxySetting = &conf.VMessInboundConfig{}
|
||||
}
|
||||
} else if nodeInfo.NodeType == "Trojan" {
|
||||
case "Trojan":
|
||||
protocol = "trojan"
|
||||
// Enable fallback
|
||||
if config.EnableFallback {
|
||||
@ -89,23 +94,30 @@ func InboundBuilder(config *Config, nodeInfo *api.NodeInfo, tag string) (*core.I
|
||||
} else {
|
||||
proxySetting = &conf.TrojanServerConfig{}
|
||||
}
|
||||
} else if nodeInfo.NodeType == "Shadowsocks" || nodeInfo.NodeType == "Shadowsocks-Plugin" {
|
||||
case "Shadowsocks", "Shadowsocks-Plugin":
|
||||
protocol = "shadowsocks"
|
||||
proxySetting = &conf.ShadowsocksServerConfig{}
|
||||
randomPasswd := uuid.New()
|
||||
defaultSSuser := &conf.ShadowsocksUserConfig{
|
||||
Cipher: "aes-128-gcm",
|
||||
Password: randomPasswd.String(),
|
||||
cipher := strings.ToLower(nodeInfo.CypherMethod)
|
||||
|
||||
proxySetting = &conf.ShadowsocksServerConfig{
|
||||
Cipher: cipher,
|
||||
Password: nodeInfo.ServerKey, // shadowsocks2022 shareKey
|
||||
}
|
||||
|
||||
proxySetting, _ := proxySetting.(*conf.ShadowsocksServerConfig)
|
||||
proxySetting.Users = append(proxySetting.Users, defaultSSuser)
|
||||
// shadowsocks must have a random password
|
||||
if !C.Contains(shadowaead_2022.List, cipher) {
|
||||
b := make([]byte, 16)
|
||||
rand.Read(b)
|
||||
proxySetting.Password = hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
proxySetting.NetworkList = &conf.NetworkList{"tcp", "udp"}
|
||||
proxySetting.IVCheck = true
|
||||
if config.DisableIVCheck {
|
||||
proxySetting.IVCheck = false
|
||||
}
|
||||
|
||||
} else if nodeInfo.NodeType == "dokodemo-door" {
|
||||
case "dokodemo-door":
|
||||
protocol = "dokodemo-door"
|
||||
proxySetting = struct {
|
||||
Host string `json:"address"`
|
||||
@ -114,7 +126,7 @@ func InboundBuilder(config *Config, nodeInfo *api.NodeInfo, tag string) (*core.I
|
||||
Host: "v1.mux.cool",
|
||||
NetworkList: []string{"tcp", "udp"},
|
||||
}
|
||||
} else {
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported node type: %s, Only support: V2ray, Trojan, Shadowsocks, and Shadowsocks-Plugin", nodeInfo.NodeType)
|
||||
}
|
||||
|
||||
@ -122,6 +134,8 @@ func InboundBuilder(config *Config, nodeInfo *api.NodeInfo, tag string) (*core.I
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal proxy %s config fialed: %s", nodeInfo.NodeType, err)
|
||||
}
|
||||
inboundDetourConfig.Protocol = protocol
|
||||
inboundDetourConfig.Settings = &setting
|
||||
|
||||
// Build streamSettings
|
||||
streamSetting = new(conf.StreamConfig)
|
||||
@ -130,13 +144,15 @@ func InboundBuilder(config *Config, nodeInfo *api.NodeInfo, tag string) (*core.I
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("convert TransportProtocol failed: %s", err)
|
||||
}
|
||||
if networkType == "tcp" {
|
||||
|
||||
switch networkType {
|
||||
case "tcp":
|
||||
tcpSetting := &conf.TCPConfig{
|
||||
AcceptProxyProtocol: config.EnableProxyProtocol,
|
||||
HeaderConfig: nodeInfo.Header,
|
||||
}
|
||||
streamSetting.TCPSettings = tcpSetting
|
||||
} else if networkType == "websocket" {
|
||||
case "websocket":
|
||||
headers := make(map[string]string)
|
||||
headers["Host"] = nodeInfo.Host
|
||||
wsSettings := &conf.WebSocketConfig{
|
||||
@ -145,14 +161,14 @@ func InboundBuilder(config *Config, nodeInfo *api.NodeInfo, tag string) (*core.I
|
||||
Headers: headers,
|
||||
}
|
||||
streamSetting.WSSettings = wsSettings
|
||||
} else if networkType == "http" {
|
||||
case "http":
|
||||
hosts := conf.StringList{nodeInfo.Host}
|
||||
httpSettings := &conf.HTTPConfig{
|
||||
Host: &hosts,
|
||||
Path: nodeInfo.Path,
|
||||
}
|
||||
streamSetting.HTTPSettings = httpSettings
|
||||
} else if networkType == "grpc" {
|
||||
case "grpc":
|
||||
grpcSettings := &conf.GRPCConfig{
|
||||
ServiceName: nodeInfo.ServiceName,
|
||||
}
|
||||
@ -160,6 +176,7 @@ func InboundBuilder(config *Config, nodeInfo *api.NodeInfo, tag string) (*core.I
|
||||
}
|
||||
|
||||
streamSetting.Network = &transportProtocol
|
||||
|
||||
// Build TLS and XTLS settings
|
||||
if nodeInfo.EnableTLS && config.CertConfig.CertMode != "none" {
|
||||
streamSetting.Security = nodeInfo.TLSType
|
||||
@ -182,6 +199,7 @@ func InboundBuilder(config *Config, nodeInfo *api.NodeInfo, tag string) (*core.I
|
||||
streamSetting.XTLSSettings = xtlsSettings
|
||||
}
|
||||
}
|
||||
|
||||
// Support ProxyProtocol for any transport protocol
|
||||
if networkType != "tcp" && networkType != "ws" && config.EnableProxyProtocol {
|
||||
sockoptConfig := &conf.SocketConfig{
|
||||
@ -189,9 +207,7 @@ func InboundBuilder(config *Config, nodeInfo *api.NodeInfo, tag string) (*core.I
|
||||
}
|
||||
streamSetting.SocketSettings = sockoptConfig
|
||||
}
|
||||
inboundDetourConfig.Protocol = protocol
|
||||
inboundDetourConfig.StreamSetting = streamSetting
|
||||
inboundDetourConfig.Settings = &setting
|
||||
|
||||
return inboundDetourConfig.Build()
|
||||
}
|
||||
|
@ -4,6 +4,8 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-shadowsocks/shadowaead_2022"
|
||||
C "github.com/sagernet/sing/common"
|
||||
"github.com/xtls/xray-core/common/protocol"
|
||||
"github.com/xtls/xray-core/common/serial"
|
||||
"github.com/xtls/xray-core/infra/conf"
|
||||
@ -14,7 +16,12 @@ import (
|
||||
"github.com/XrayR-project/XrayR/api"
|
||||
)
|
||||
|
||||
var AEADMethod = []shadowsocks.CipherType{shadowsocks.CipherType_AES_128_GCM, shadowsocks.CipherType_AES_256_GCM, shadowsocks.CipherType_CHACHA20_POLY1305, shadowsocks.CipherType_XCHACHA20_POLY1305}
|
||||
var AEADMethod = map[shadowsocks.CipherType]uint8{
|
||||
shadowsocks.CipherType_AES_128_GCM: 0,
|
||||
shadowsocks.CipherType_AES_256_GCM: 0,
|
||||
shadowsocks.CipherType_CHACHA20_POLY1305: 0,
|
||||
shadowsocks.CipherType_XCHACHA20_POLY1305: 0,
|
||||
}
|
||||
|
||||
func (c *Controller) buildVmessUser(userInfo *[]api.UserInfo, serverAlterID uint16) (users []*protocol.User) {
|
||||
users = make([]*protocol.User, len(*userInfo))
|
||||
@ -66,43 +73,59 @@ func (c *Controller) buildTrojanUser(userInfo *[]api.UserInfo) (users []*protoco
|
||||
}
|
||||
|
||||
func (c *Controller) buildSSUser(userInfo *[]api.UserInfo, method string) (users []*protocol.User) {
|
||||
users = make([]*protocol.User, 0)
|
||||
users = make([]*protocol.User, len(*userInfo))
|
||||
|
||||
cypherMethod := cipherFromString(method)
|
||||
for _, user := range *userInfo {
|
||||
ssAccount := &shadowsocks.Account{
|
||||
Password: user.Passwd,
|
||||
CipherType: cypherMethod,
|
||||
}
|
||||
users = append(users, &protocol.User{
|
||||
for i, user := range *userInfo {
|
||||
// todo waiting xray-core complete proxy.UserManager in shadowsocks2022
|
||||
if C.Contains(shadowaead_2022.List, strings.ToLower(method)) {
|
||||
users[i] = &protocol.User{
|
||||
Level: 0,
|
||||
Email: c.buildUserTag(&user),
|
||||
Account: serial.ToTypedMessage(ssAccount),
|
||||
})
|
||||
Account: serial.ToTypedMessage(&shadowsocks.Account{
|
||||
Password: user.Passwd,
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
users[i] = &protocol.User{
|
||||
Level: 0,
|
||||
Email: c.buildUserTag(&user),
|
||||
Account: serial.ToTypedMessage(&shadowsocks.Account{
|
||||
Password: user.Passwd,
|
||||
CipherType: cipherFromString(method),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
return users
|
||||
}
|
||||
|
||||
func (c *Controller) buildSSPluginUser(userInfo *[]api.UserInfo) (users []*protocol.User) {
|
||||
users = make([]*protocol.User, 0)
|
||||
users = make([]*protocol.User, len(*userInfo))
|
||||
|
||||
for _, user := range *userInfo {
|
||||
// Check if the cypher method is AEAD
|
||||
cypherMethod := cipherFromString(user.Method)
|
||||
for _, aeadMethod := range AEADMethod {
|
||||
if aeadMethod == cypherMethod {
|
||||
ssAccount := &shadowsocks.Account{
|
||||
Password: user.Passwd,
|
||||
CipherType: cypherMethod,
|
||||
}
|
||||
users = append(users, &protocol.User{
|
||||
for i, user := range *userInfo {
|
||||
// todo waiting xray-core complete proxy.UserManager in shadowsocks2022
|
||||
if C.Contains(shadowaead_2022.List, strings.ToLower(user.Method)) {
|
||||
users[i] = &protocol.User{
|
||||
Level: 0,
|
||||
Email: c.buildUserTag(&user),
|
||||
Account: serial.ToTypedMessage(ssAccount),
|
||||
})
|
||||
Account: serial.ToTypedMessage(&shadowsocks.Account{
|
||||
Password: user.Passwd,
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
// Check if the cypher method is AEAD
|
||||
cypherMethod := cipherFromString(user.Method)
|
||||
if _, ok := AEADMethod[cypherMethod]; ok {
|
||||
users[i] = &protocol.User{
|
||||
Level: 0,
|
||||
Email: c.buildUserTag(&user),
|
||||
Account: serial.ToTypedMessage(&shadowsocks.Account{
|
||||
Password: user.Passwd,
|
||||
CipherType: cypherMethod,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return users
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user