diff --git a/api/apimodel.go b/api/apimodel.go index 7274731..3db79b3 100644 --- a/api/apimodel.go +++ b/api/apimodel.go @@ -42,6 +42,7 @@ type NodeInfo struct { TLSType string EnableVless bool CypherMethod string + ServerKey string ServiceName string Header json.RawMessage } diff --git a/api/newV2board/model.go b/api/newV2board/model.go new file mode 100644 index 0000000..f236d1e --- /dev/null +++ b/api/newV2board/model.go @@ -0,0 +1,7 @@ +package newV2board + +type UserTraffic struct { + UID int `json:"user_id"` + Upload int64 `json:"u"` + Download int64 `json:"d"` +} diff --git a/api/newV2board/v2board.go b/api/newV2board/v2board.go new file mode 100644 index 0000000..9cefea7 --- /dev/null +++ b/api/newV2board/v2board.go @@ -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 +} diff --git a/api/newV2board/v2board_test.go b/api/newV2board/v2board_test.go new file mode 100644 index 0000000..bb18eb1 --- /dev/null +++ b/api/newV2board/v2board_test.go @@ -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) +} diff --git a/api/v2board/model.go b/api/v2board/model.go index 36f1507..d50d825 100644 --- a/api/v2board/model.go +++ b/api/v2board/model.go @@ -1,3 +1,4 @@ +// Deprecated: after 2023.6.1 package v2board type UserTraffic struct { diff --git a/go.mod b/go.mod index aad0edb..ede50c6 100644 --- a/go.mod +++ b/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 diff --git a/panel/panel.go b/panel/panel.go index a4f0e6b..653309d 100644 --- a/panel/panel.go +++ b/panel/panel.go @@ -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": diff --git a/service/controller/controller.go b/service/controller/controller.go index 3b2c61c..2ee57d0 100644 --- a/service/controller/controller.go +++ b/service/controller/controller.go @@ -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,10 +185,16 @@ func (c *Controller) nodeInfoMonitor() (err error) { } // Update User + var usersChanged = true newUserInfo, err := c.apiClient.GetUserList() if err != nil { - log.Print(err) - return nil + if err.Error() == "users no change" { + usersChanged = false + newUserInfo = c.userList + } else { + log.Print(err) + return nil + } } var nodeInfoChanged = false @@ -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,28 +253,31 @@ func (c *Controller) nodeInfoMonitor() (err error) { return nil } } else { - deleted, added := compareUserList(c.userList, newUserInfo) - if len(deleted) > 0 { - deletedEmail := make([]string, len(deleted)) - for i, u := range deleted { - deletedEmail[i] = fmt.Sprintf("%s|%s|%d", c.Tag, u.Email, u.UID) + 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 { + deletedEmail[i] = fmt.Sprintf("%s|%s|%d", c.Tag, u.Email, u.UID) + } + err := c.removeUsers(deletedEmail, c.Tag) + if err != nil { + log.Print(err) + } } - err := c.removeUsers(deletedEmail, c.Tag) - if err != nil { - log.Print(err) + if len(added) > 0 { + err = c.addNewUser(&added, c.nodeInfo) + if err != nil { + log.Print(err) + } + // Update Limiter + if err := c.UpdateInboundLimiter(c.Tag, &added); err != nil { + log.Print(err) + } } } - if len(added) > 0 { - err = c.addNewUser(&added, c.nodeInfo) - if err != nil { - log.Print(err) - } - // Update Limiter - if err := c.UpdateInboundLimiter(c.Tag, &added); err != nil { - 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 +} diff --git a/service/controller/inboundbuilder.go b/service/controller/inboundbuilder.go index c382984..d2bb6f2 100644 --- a/service/controller/inboundbuilder.go +++ b/service/controller/inboundbuilder.go @@ -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() } diff --git a/service/controller/userbuilder.go b/service/controller/userbuilder.go index 4c807cc..e2f9483 100644 --- a/service/controller/userbuilder.go +++ b/service/controller/userbuilder.go @@ -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, + 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(&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), + }), + } } - users = append(users, &protocol.User{ - Level: 0, - Email: c.buildUserTag(&user), - Account: serial.ToTypedMessage(ssAccount), - }) } 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, + 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(&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, + }), } - users = append(users, &protocol.User{ - Level: 0, - Email: c.buildUserTag(&user), - Account: serial.ToTypedMessage(ssAccount), - }) } } - } return users }