新增调试信息

This commit is contained in:
2025-10-27 22:15:25 +08:00
parent ae62457d8c
commit 04642cb2f0
5479 changed files with 683397 additions and 3450 deletions
+77
View File
@@ -0,0 +1,77 @@
package v2
import (
pb "github.com/hiddify/hiddify-core/hiddifyrpc"
"github.com/sagernet/sing-box/experimental/libbox"
"github.com/sagernet/sing-box/log"
)
type CommandClientHandler struct {
port int64
logger log.Logger
}
func (cch *CommandClientHandler) Connected() {
cch.logger.Debug("CONNECTED")
}
func (cch *CommandClientHandler) Disconnected(message string) {
cch.logger.Debug("DISCONNECTED: ", message)
}
func (cch *CommandClientHandler) ClearLog() {
cch.logger.Debug("clear log")
}
func (cch *CommandClientHandler) WriteLog(message string) {
cch.logger.Debug("log: ", message)
}
func (cch *CommandClientHandler) WriteStatus(message *libbox.StatusMessage) {
systemInfoObserver.Emit(pb.SystemInfo{
ConnectionsIn: message.ConnectionsIn,
ConnectionsOut: message.ConnectionsOut,
Uplink: message.Uplink,
Downlink: message.Downlink,
UplinkTotal: message.UplinkTotal,
DownlinkTotal: message.DownlinkTotal,
Memory: message.Memory,
Goroutines: message.Goroutines,
})
cch.logger.Debug("Memory: ", libbox.FormatBytes(message.Memory), ", Goroutines: ", message.Goroutines)
}
func (cch *CommandClientHandler) WriteGroups(message libbox.OutboundGroupIterator) {
if message == nil {
return
}
groups := pb.OutboundGroupList{}
for message.HasNext() {
group := message.Next()
items := group.GetItems()
groupItems := []*pb.OutboundGroupItem{}
for items.HasNext() {
item := items.Next()
groupItems = append(groupItems,
&pb.OutboundGroupItem{
Tag: item.Tag,
Type: item.Type,
UrlTestTime: item.URLTestTime,
UrlTestDelay: item.URLTestDelay,
},
)
}
groups.Items = append(groups.Items, &pb.OutboundGroup{Tag: group.Tag, Type: group.Type, Selected: group.Selected, Items: groupItems})
}
outboundsInfoObserver.Emit(groups)
mainOutboundsInfoObserver.Emit(groups)
}
func (cch *CommandClientHandler) InitializeClashMode(modeList libbox.StringIterator, currentMode string) {
cch.logger.Debug("initial clash mode: ", currentMode)
}
func (cch *CommandClientHandler) UpdateClashMode(newMode string) {
cch.logger.Debug("update clash mode: ", newMode)
}
+156
View File
@@ -0,0 +1,156 @@
package v2
import (
"context"
"time"
pb "github.com/hiddify/hiddify-core/hiddifyrpc"
"github.com/sagernet/sing-box/experimental/libbox"
"google.golang.org/grpc"
)
var (
systemInfoObserver = NewObserver[pb.SystemInfo](10)
outboundsInfoObserver = NewObserver[pb.OutboundGroupList](10)
mainOutboundsInfoObserver = NewObserver[pb.OutboundGroupList](10)
)
var (
statusClient *libbox.CommandClient
groupClient *libbox.CommandClient
groupInfoOnlyClient *libbox.CommandClient
)
func (s *CoreService) GetSystemInfo(req *pb.Empty, stream grpc.ServerStreamingServer[pb.SystemInfo]) error {
if statusClient == nil {
statusClient = libbox.NewCommandClient(
&CommandClientHandler{},
&libbox.CommandClientOptions{
Command: libbox.CommandStatus,
StatusInterval: 1000000000, // 1000ms debounce
},
)
defer func() {
statusClient.Disconnect()
statusClient = nil
}()
statusClient.Connect()
}
sub, done, _ := systemInfoObserver.Subscribe()
for {
select {
case <-stream.Context().Done():
return nil
case <-done:
return nil
case info := <-sub:
stream.Send(&info)
case <-time.After(1000 * time.Millisecond):
}
}
}
func (s *CoreService) OutboundsInfo(req *pb.Empty, stream grpc.ServerStreamingServer[pb.OutboundGroupList]) error {
if groupClient == nil {
groupClient = libbox.NewCommandClient(
&CommandClientHandler{},
&libbox.CommandClientOptions{
Command: libbox.CommandGroup,
StatusInterval: 500000000, // 500ms debounce
},
)
defer func() {
groupClient.Disconnect()
groupClient = nil
}()
groupClient.Connect()
}
sub, done, _ := outboundsInfoObserver.Subscribe()
for {
select {
case <-stream.Context().Done():
return nil
case <-done:
return nil
case info := <-sub:
stream.Send(&info)
case <-time.After(500 * time.Millisecond):
}
}
}
func (s *CoreService) MainOutboundsInfo(req *pb.Empty, stream grpc.ServerStreamingServer[pb.OutboundGroupList]) error {
if groupInfoOnlyClient == nil {
groupInfoOnlyClient = libbox.NewCommandClient(
&CommandClientHandler{},
&libbox.CommandClientOptions{
Command: libbox.CommandGroupInfoOnly,
StatusInterval: 500000000, // 500ms debounce
},
)
defer func() {
groupInfoOnlyClient.Disconnect()
groupInfoOnlyClient = nil
}()
groupInfoOnlyClient.Connect()
}
sub, stopch, _ := mainOutboundsInfoObserver.Subscribe()
for {
select {
case <-stream.Context().Done():
return nil
case <-stopch:
return nil
case info := <-sub:
stream.Send(&info)
case <-time.After(500 * time.Millisecond):
}
}
}
func (s *CoreService) SelectOutbound(ctx context.Context, in *pb.SelectOutboundRequest) (*pb.Response, error) {
return SelectOutbound(in)
}
func SelectOutbound(in *pb.SelectOutboundRequest) (*pb.Response, error) {
err := libbox.NewStandaloneCommandClient().SelectOutbound(in.GroupTag, in.OutboundTag)
if err != nil {
return &pb.Response{
ResponseCode: pb.ResponseCode_FAILED,
Message: err.Error(),
}, err
}
return &pb.Response{
ResponseCode: pb.ResponseCode_OK,
Message: "",
}, nil
}
func (s *CoreService) UrlTest(ctx context.Context, in *pb.UrlTestRequest) (*pb.Response, error) {
return UrlTest(in)
}
func UrlTest(in *pb.UrlTestRequest) (*pb.Response, error) {
err := libbox.NewStandaloneCommandClient().URLTest(in.GroupTag)
if err != nil {
return &pb.Response{
ResponseCode: pb.ResponseCode_FAILED,
Message: err.Error(),
}, err
}
return &pb.Response{
ResponseCode: pb.ResponseCode_OK,
Message: "",
}, nil
}
+25
View File
@@ -0,0 +1,25 @@
package common
import (
"net"
"net/netip"
"time"
)
func CanConnectIPv6Addr(remoteAddr netip.AddrPort) bool {
dialer := net.Dialer{
Timeout: 1 * time.Second,
}
conn, err := dialer.Dial("tcp6", remoteAddr.String())
if err != nil {
return false
}
defer conn.Close()
return true
}
func CanConnectIPv6() bool {
return CanConnectIPv6Addr(netip.MustParseAddrPort("[2001:4860:4860::8888]:80"))
}
+57
View File
@@ -0,0 +1,57 @@
package v2
import (
"encoding/json"
"fmt"
"github.com/hiddify/hiddify-core/bridge"
pb "github.com/hiddify/hiddify-core/hiddifyrpc"
"google.golang.org/grpc"
)
var (
coreInfoObserver = *NewObserver[*pb.CoreInfoResponse](1)
CoreState = pb.CoreState_STOPPED
)
func SetCoreStatus(state pb.CoreState, msgType pb.MessageType, message string) *pb.CoreInfoResponse {
msg := fmt.Sprintf("%s: %s %s", state.String(), msgType.String(), message)
if msgType == pb.MessageType_EMPTY {
msg = fmt.Sprintf("%s: %s", state.String(), message)
}
Log(pb.LogLevel_INFO, pb.LogType_CORE, msg)
CoreState = state
info := pb.CoreInfoResponse{
CoreState: state,
MessageType: msgType,
Message: message,
}
coreInfoObserver.Emit(&info)
if useFlutterBridge {
msg, _ := json.Marshal(StatusMessage{Status: convert2OldState(CoreState)})
bridge.SendStringToPort(statusPropagationPort, string(msg))
}
return &info
}
func (s *CoreService) CoreInfoListener(req *pb.Empty, stream grpc.ServerStreamingServer[pb.CoreInfoResponse]) error {
coreSub, done, err := coreInfoObserver.Subscribe()
if err != nil {
return err
}
defer coreInfoObserver.UnSubscribe(coreSub)
for {
select {
case <-stream.Context().Done():
return nil
case <-done:
return nil
case info := <-coreSub:
stream.Send(info)
// case <-time.After(500 * time.Millisecond):
// info := SetCoreStatus(pb.CoreState_STOPPED, pb.MessageType_EMPTY, "")
// stream.Send(info)
}
}
}
+357
View File
@@ -0,0 +1,357 @@
package v2
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"time"
"github.com/hiddify/hiddify-core/bridge"
"github.com/hiddify/hiddify-core/config"
pb "github.com/hiddify/hiddify-core/hiddifyrpc"
"github.com/sagernet/sing-box/experimental/libbox"
"github.com/sagernet/sing-box/log"
)
var (
Box *libbox.BoxService
HiddifyOptions *config.HiddifyOptions
activeConfigPath string
coreLogFactory log.Factory
useFlutterBridge bool = true
)
func StopAndAlert(msgType pb.MessageType, message string) {
SetCoreStatus(pb.CoreState_STOPPED, msgType, message)
config.DeactivateTunnelService()
if oldCommandServer != nil {
oldCommandServer.SetService(nil)
}
if Box != nil {
Box.Close()
Box = nil
}
if oldCommandServer != nil {
oldCommandServer.Close()
}
if useFlutterBridge {
alert := msgType.String()
msg, _ := json.Marshal(StatusMessage{Status: convert2OldState(CoreState), Alert: &alert, Message: &message})
bridge.SendStringToPort(statusPropagationPort, string(msg))
}
}
func (s *CoreService) Start(ctx context.Context, in *pb.StartRequest) (*pb.CoreInfoResponse, error) {
return Start(in)
}
func Start(in *pb.StartRequest) (*pb.CoreInfoResponse, error) {
defer config.DeferPanicToError("start", func(err error) {
Log(pb.LogLevel_FATAL, pb.LogType_CORE, err.Error())
StopAndAlert(pb.MessageType_UNEXPECTED_ERROR, err.Error())
})
Log(pb.LogLevel_INFO, pb.LogType_CORE, "Starting")
if CoreState != pb.CoreState_STOPPED {
Log(pb.LogLevel_INFO, pb.LogType_CORE, "Starting0000")
Stop()
// return &pb.CoreInfoResponse{
// CoreState: CoreState,
// MessageType: pb.MessageType_INSTANCE_NOT_STOPPED,
// }, fmt.Errorf("instance not stopped")
}
Log(pb.LogLevel_DEBUG, pb.LogType_CORE, "Starting Core")
SetCoreStatus(pb.CoreState_STARTING, pb.MessageType_EMPTY, "")
libbox.SetMemoryLimit(!in.DisableMemoryLimit)
resp, err := StartService(in)
return resp, err
}
func (s *CoreService) StartService(ctx context.Context, in *pb.StartRequest) (*pb.CoreInfoResponse, error) {
return StartService(in)
}
func StartService(in *pb.StartRequest) (*pb.CoreInfoResponse, error) {
Log(pb.LogLevel_DEBUG, pb.LogType_CORE, "Starting Core Service")
content := in.ConfigContent
if content == "" {
activeConfigPath = in.ConfigPath
fileContent, err := os.ReadFile(activeConfigPath)
if err != nil {
Log(pb.LogLevel_FATAL, pb.LogType_CORE, err.Error())
resp := SetCoreStatus(pb.CoreState_STOPPED, pb.MessageType_ERROR_READING_CONFIG, err.Error())
StopAndAlert(pb.MessageType_UNEXPECTED_ERROR, err.Error())
return resp, err
}
content = string(fileContent)
}
Log(pb.LogLevel_DEBUG, pb.LogType_CORE, "Parsing Config")
parsedContent, err := readOptions(content)
Log(pb.LogLevel_DEBUG, pb.LogType_CORE, "Parsed")
if err != nil {
Log(pb.LogLevel_FATAL, pb.LogType_CORE, err.Error())
resp := SetCoreStatus(pb.CoreState_STOPPED, pb.MessageType_ERROR_PARSING_CONFIG, err.Error())
StopAndAlert(pb.MessageType_UNEXPECTED_ERROR, err.Error())
return resp, err
}
if !in.EnableRawConfig {
Log(pb.LogLevel_DEBUG, pb.LogType_CORE, "Building config")
parsedContent_tmp, err := config.BuildConfig(*HiddifyOptions, parsedContent)
if err != nil {
Log(pb.LogLevel_FATAL, pb.LogType_CORE, err.Error())
resp := SetCoreStatus(pb.CoreState_STOPPED, pb.MessageType_ERROR_BUILDING_CONFIG, err.Error())
StopAndAlert(pb.MessageType_UNEXPECTED_ERROR, err.Error())
return resp, err
}
parsedContent = *parsedContent_tmp
}
Log(pb.LogLevel_DEBUG, pb.LogType_CORE, "Saving config")
currentBuildConfigPath := filepath.Join(sWorkingPath, "current-config.json")
config.SaveCurrentConfig(currentBuildConfigPath, parsedContent)
if activeConfigPath == "" {
activeConfigPath = currentBuildConfigPath
}
if in.EnableOldCommandServer {
Log(pb.LogLevel_DEBUG, pb.LogType_CORE, "Starting Command Server")
err = startCommandServer()
if err != nil {
Log(pb.LogLevel_FATAL, pb.LogType_CORE, err.Error())
resp := SetCoreStatus(pb.CoreState_STOPPED, pb.MessageType_START_COMMAND_SERVER, err.Error())
StopAndAlert(pb.MessageType_UNEXPECTED_ERROR, err.Error())
return resp, err
}
}
Log(pb.LogLevel_DEBUG, pb.LogType_CORE, "Stating Service ")
instance, err := NewService(parsedContent)
if err != nil {
Log(pb.LogLevel_FATAL, pb.LogType_CORE, err.Error())
resp := SetCoreStatus(pb.CoreState_STOPPED, pb.MessageType_CREATE_SERVICE, err.Error())
StopAndAlert(pb.MessageType_UNEXPECTED_ERROR, err.Error())
return resp, err
}
Log(pb.LogLevel_DEBUG, pb.LogType_CORE, "Service.. started")
if in.DelayStart {
<-time.After(250 * time.Millisecond)
}
err = instance.Start()
if err != nil {
Log(pb.LogLevel_FATAL, pb.LogType_CORE, err.Error())
resp := SetCoreStatus(pb.CoreState_STOPPED, pb.MessageType_START_SERVICE, err.Error())
StopAndAlert(pb.MessageType_UNEXPECTED_ERROR, err.Error())
return resp, err
}
Box = instance
if in.EnableOldCommandServer {
oldCommandServer.SetService(Box)
}
resp := SetCoreStatus(pb.CoreState_STARTED, pb.MessageType_EMPTY, "")
return resp, nil
}
func (s *CoreService) Parse(ctx context.Context, in *pb.ParseRequest) (*pb.ParseResponse, error) {
return Parse(in)
}
func Parse(in *pb.ParseRequest) (*pb.ParseResponse, error) {
defer config.DeferPanicToError("parse", func(err error) {
Log(pb.LogLevel_FATAL, pb.LogType_CONFIG, err.Error())
StopAndAlert(pb.MessageType_UNEXPECTED_ERROR, err.Error())
})
content := in.Content
if in.TempPath != "" {
contentBytes, err := os.ReadFile(in.TempPath)
content = string(contentBytes)
os.Chdir(filepath.Dir(in.ConfigPath))
if err != nil {
return nil, err
}
}
config, err := config.ParseConfigContent(content, true, HiddifyOptions, false)
if err != nil {
return &pb.ParseResponse{
ResponseCode: pb.ResponseCode_FAILED,
Message: err.Error(),
}, err
}
if in.ConfigPath != "" {
err = os.WriteFile(in.ConfigPath, config, 0o644)
if err != nil {
return &pb.ParseResponse{
ResponseCode: pb.ResponseCode_FAILED,
Message: err.Error(),
}, err
}
}
return &pb.ParseResponse{
ResponseCode: pb.ResponseCode_OK,
Content: string(config),
Message: "",
}, err
}
func (s *CoreService) ChangeHiddifySettings(ctx context.Context, in *pb.ChangeHiddifySettingsRequest) (*pb.CoreInfoResponse, error) {
return ChangeHiddifySettings(in)
}
func ChangeHiddifySettings(in *pb.ChangeHiddifySettingsRequest) (*pb.CoreInfoResponse, error) {
HiddifyOptions = config.DefaultHiddifyOptions()
err := json.Unmarshal([]byte(in.HiddifySettingsJson), HiddifyOptions)
if err != nil {
return nil, err
}
if HiddifyOptions.Warp.WireguardConfigStr != "" {
err := json.Unmarshal([]byte(HiddifyOptions.Warp.WireguardConfigStr), &HiddifyOptions.Warp.WireguardConfig)
if err != nil {
return nil, err
}
}
if HiddifyOptions.Warp2.WireguardConfigStr != "" {
err := json.Unmarshal([]byte(HiddifyOptions.Warp2.WireguardConfigStr), &HiddifyOptions.Warp2.WireguardConfig)
if err != nil {
return nil, err
}
}
return &pb.CoreInfoResponse{}, nil
}
func (s *CoreService) GenerateConfig(ctx context.Context, in *pb.GenerateConfigRequest) (*pb.GenerateConfigResponse, error) {
return GenerateConfig(in)
}
func GenerateConfig(in *pb.GenerateConfigRequest) (*pb.GenerateConfigResponse, error) {
defer config.DeferPanicToError("generateConfig", func(err error) {
Log(pb.LogLevel_FATAL, pb.LogType_CONFIG, err.Error())
StopAndAlert(pb.MessageType_UNEXPECTED_ERROR, err.Error())
})
if HiddifyOptions == nil {
HiddifyOptions = config.DefaultHiddifyOptions()
}
config, err := generateConfigFromFile(in.Path, *HiddifyOptions)
if err != nil {
return nil, err
}
return &pb.GenerateConfigResponse{
ConfigContent: config,
}, nil
}
func generateConfigFromFile(path string, configOpt config.HiddifyOptions) (string, error) {
os.Chdir(filepath.Dir(path))
content, err := os.ReadFile(path)
if err != nil {
return "", err
}
options, err := readOptions(string(content))
if err != nil {
return "", err
}
config, err := config.BuildConfigJson(configOpt, options)
if err != nil {
return "", err
}
return config, nil
}
func (s *CoreService) Stop(ctx context.Context, empty *pb.Empty) (*pb.CoreInfoResponse, error) {
return Stop()
}
func Stop() (*pb.CoreInfoResponse, error) {
defer config.DeferPanicToError("stop", func(err error) {
Log(pb.LogLevel_FATAL, pb.LogType_CORE, err.Error())
StopAndAlert(pb.MessageType_UNEXPECTED_ERROR, err.Error())
})
if CoreState != pb.CoreState_STARTED {
Log(pb.LogLevel_FATAL, pb.LogType_CORE, "Core is not started")
return &pb.CoreInfoResponse{
CoreState: CoreState,
MessageType: pb.MessageType_INSTANCE_NOT_STARTED,
Message: "instance is not started",
}, fmt.Errorf("instance not started")
}
if Box == nil {
return &pb.CoreInfoResponse{
CoreState: CoreState,
MessageType: pb.MessageType_INSTANCE_NOT_FOUND,
Message: "instance is not found",
}, fmt.Errorf("instance not found")
}
SetCoreStatus(pb.CoreState_STOPPING, pb.MessageType_EMPTY, "")
config.DeactivateTunnelService()
if oldCommandServer != nil {
oldCommandServer.SetService(nil)
}
err := Box.Close()
if err != nil {
return &pb.CoreInfoResponse{
CoreState: CoreState,
MessageType: pb.MessageType_UNEXPECTED_ERROR,
Message: "Error while stopping the service.",
}, fmt.Errorf("Error while stopping the service.")
}
Box = nil
if oldCommandServer != nil {
err = oldCommandServer.Close()
if err != nil {
return &pb.CoreInfoResponse{
CoreState: CoreState,
MessageType: pb.MessageType_UNEXPECTED_ERROR,
Message: "Error while Closing the comand server.",
}, fmt.Errorf("error while Closing the comand server.")
}
oldCommandServer = nil
}
resp := SetCoreStatus(pb.CoreState_STOPPED, pb.MessageType_EMPTY, "")
return resp, nil
}
func (s *CoreService) Restart(ctx context.Context, in *pb.StartRequest) (*pb.CoreInfoResponse, error) {
return Restart(in)
}
func Restart(in *pb.StartRequest) (*pb.CoreInfoResponse, error) {
defer config.DeferPanicToError("restart", func(err error) {
Log(pb.LogLevel_FATAL, pb.LogType_CORE, err.Error())
StopAndAlert(pb.MessageType_UNEXPECTED_ERROR, err.Error())
})
log.Debug("[Service] Restarting")
if CoreState != pb.CoreState_STARTED {
return &pb.CoreInfoResponse{
CoreState: CoreState,
MessageType: pb.MessageType_INSTANCE_NOT_STARTED,
Message: "instance is not started",
}, fmt.Errorf("instance not started")
}
if Box == nil {
return &pb.CoreInfoResponse{
CoreState: CoreState,
MessageType: pb.MessageType_INSTANCE_NOT_FOUND,
Message: "instance is not found",
}, fmt.Errorf("instance not found")
}
resp, err := Stop()
if err != nil {
return resp, err
}
SetCoreStatus(pb.CoreState_STARTING, pb.MessageType_EMPTY, "")
<-time.After(250 * time.Millisecond)
libbox.SetMemoryLimit(!in.DisableMemoryLimit)
resp, gErr := StartService(in)
return resp, gErr
}
+218
View File
@@ -0,0 +1,218 @@
package db
import (
"bytes"
"encoding/gob"
"fmt"
"log"
"os"
"reflect"
"time"
"github.com/syndtr/goleveldb/leveldb/opt"
tmdb "github.com/tendermint/tm-db"
)
// getDB initializes the database with retry logic. If it fails after 100 attempts, it returns nil.
func getDB(name string, readOnly bool) (tmdb.DB, error) {
// Check if the database file exists; if not, set to readOnly
dbPath := "data/" + name + ".db"
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
readOnly = false
}
const retryAttempts = 100
const retryDelay = 100 * time.Microsecond
var db tmdb.DB
var err error
for i := 0; i < retryAttempts; i++ {
// Set readOnly to true for the first 80 attempts
opts := &opt.Options{ReadOnly: readOnly && i < 80}
db, err = tmdb.NewGoLevelDBWithOpts(name, "./data", opts)
if err == nil {
return db, nil
}
log.Printf("Failed attempt %d to initialize the database: %v", i, err)
time.Sleep(retryDelay)
}
return nil, err
}
// GetTable returns a new Table instance for the generic type T, ensuring the struct has an "Id" field.
func GetTable[T any]() *Table[T] {
var t T
typeName := reflect.TypeOf(t).Name()
if !hasIdField(t) {
panic(fmt.Sprintf("Table %s must have a field named 'Id'", typeName))
}
return &Table[T]{name: typeName}
}
// hasIdField checks if the struct has a field named "Id".
func hasIdField[T any](t T) bool {
val := reflect.Indirect(reflect.ValueOf(t))
if val.Kind() != reflect.Struct {
return false
}
return val.FieldByName("Id").IsValid()
}
// getIdBytes converts an ID to its byte representation for storage in the database.
func getIdBytes(id any) []byte {
res, err := SerializeKey(id)
if err != nil {
return nil
}
return res
}
// if id == nil {
// return nil
// }
// var buf bytes.Buffer
// switch v := id.(type) {
// case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
// if err := binary.Write(&buf, binary.BigEndian, v); err == nil {
// return buf.Bytes()
// }
// case string:
// return []byte(v)
// case []byte:
// return v
// default:
// return []byte(fmt.Sprint(v))
// }
// return nil
// }
// getId retrieves the "Id" field from the struct T.
func getId[T any](t T) any {
val := reflect.Indirect(reflect.ValueOf(t))
if val.Kind() != reflect.Struct {
return nil
}
field := val.FieldByName("Id")
if field.IsValid() {
return field.Interface()
}
return nil
}
// Table represents a database table for generic type T.
type Table[T any] struct {
name string
}
// All retrieves all entries from the database and unmarshals them into a slice of T.
func (tbl *Table[T]) All() ([]*T, error) {
db, err := getDB(tbl.name, true)
if db == nil {
return nil, fmt.Errorf("failed to open database %s, error: %w", tbl.name, err)
}
defer db.Close()
var items []*T
iter, err := db.Iterator(nil, nil)
if err != nil {
return nil, err
}
defer iter.Close()
for ; iter.Valid(); iter.Next() {
item, err := Deserialize[T](iter.Value())
if err != nil {
return nil, err
}
items = append(items, item)
}
return items, nil
}
func Serialize(data any) ([]byte, error) {
var buf bytes.Buffer
enc := gob.NewEncoder(&buf)
err := enc.Encode(data)
return buf.Bytes(), err
// return json.Marshal(data)
}
func SerializeKey(data any) ([]byte, error) {
var buf bytes.Buffer
enc := gob.NewEncoder(&buf)
err := enc.Encode(data)
return buf.Bytes(), err
}
func Deserialize[T any](data []byte) (*T, error) {
var obj T
buf := bytes.NewBuffer(data)
dec := gob.NewDecoder(buf)
err := dec.Decode(&obj)
return &obj, err
// return &obj, json.Unmarshal(data, &obj)
}
// UpdateInsert inserts or updates multiple items in the database.
func (tbl *Table[T]) UpdateInsert(items ...*T) error {
db, err := getDB(tbl.name, false)
if db == nil {
return fmt.Errorf("failed to open database %s, error: %w", tbl.name, err)
}
defer db.Close()
for _, item := range items {
// b, err := json.Marshal(item)
b, err := Serialize(item)
if err != nil {
return err
}
if err := db.Set(getIdBytes(getId(item)), b); err != nil {
return err
}
}
return nil
}
// Delete removes entries by their IDs.
func (tbl *Table[T]) Delete(ids ...any) error {
db, err := getDB(tbl.name, true)
if db == nil {
return fmt.Errorf("failed to open database %s, error: %w", tbl.name, err)
}
defer db.Close()
for _, id := range ids {
if err := db.Delete(getIdBytes(id)); err != nil {
return err
}
}
return nil
}
// Get retrieves a single item by its ID.
func (tbl *Table[T]) Get(id any) (*T, error) {
db, err := getDB(tbl.name, true)
if db == nil {
return nil, fmt.Errorf("failed to open database %s, error: %w", tbl.name, err)
}
defer db.Close()
b, err := db.Get(getIdBytes(id))
if err != nil {
return nil, err
}
return Deserialize[T](b)
}
+62
View File
@@ -0,0 +1,62 @@
package main
import (
"context"
"log"
"time"
pb "github.com/hiddify/hiddify-core/hiddifyrpc"
"google.golang.org/grpc"
)
const (
address = "localhost:50051"
defaultName = "world"
)
func main() {
conn, err := grpc.Dial(address, grpc.WithInsecure())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewHelloClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
// SayHello
r, err := c.SayHello(ctx, &pb.HelloRequest{Name: defaultName})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", r.Message)
// SayHelloStream
stream, err := c.SayHelloStream(ctx)
if err != nil {
log.Fatalf("could not stream: %v", err)
}
names := []string{"Alice", "Bob", "Charlie"}
for _, name := range names {
err := stream.Send(&pb.HelloRequest{Name: name})
if err != nil {
log.Fatalf("could not send: %v", err)
}
r, err := stream.Recv()
if err != nil {
log.Fatalf("could not receive: %v", err)
}
log.Printf("Received1: %s", r.Message)
r2, err2 := stream.Recv()
if err2 != nil {
log.Fatalf("could not receive2: %v", err2)
}
log.Printf("Received: %s", r2.Message)
time.Sleep(1 * time.Second)
}
}
+18
View File
@@ -0,0 +1,18 @@
package main
import (
"os"
"os/signal"
"syscall"
v2 "github.com/hiddify/hiddify-core/v2"
)
func main() {
// defer C.free(unsafe.Pointer(port))
v2.StartGrpcServer("127.0.0.1:50051", "hello")
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
<-sigChan
}
+68
View File
@@ -0,0 +1,68 @@
package v2
/*
#include "stdint.h"
*/
import (
"log"
"net"
"github.com/hiddify/hiddify-core/extension"
pb "github.com/hiddify/hiddify-core/hiddifyrpc"
"google.golang.org/grpc"
)
type HelloService struct {
pb.UnimplementedHelloServer
}
type CoreService struct {
pb.UnimplementedCoreServer
}
type TunnelService struct {
pb.UnimplementedTunnelServiceServer
}
func StartGrpcServer(listenAddressG string, service string) (*grpc.Server, error) {
lis, err := net.Listen("tcp", listenAddressG)
if err != nil {
log.Printf("failed to listen: %v", err)
return nil, err
}
s := grpc.NewServer()
if service == "core" {
// Setup("./tmp/", "./tmp", "./tmp", 11111, false)
useFlutterBridge = false
pb.RegisterCoreServer(s, &CoreService{})
pb.RegisterExtensionHostServiceServer(s, &extension.ExtensionHostService{})
} else if service == "hello" {
pb.RegisterHelloServer(s, &HelloService{})
} else if service == "tunnel" {
pb.RegisterTunnelServiceServer(s, &TunnelService{})
}
log.Printf("Server listening on %s", listenAddressG)
go func() {
if err := s.Serve(lis); err != nil {
log.Printf("failed to serve: %v", err)
}
log.Printf("Server stopped")
// cancel()
}()
return s, nil
}
func StartCoreGrpcServer(listenAddressG string) (*grpc.Server, error) {
return StartGrpcServer(listenAddressG, "core")
}
func StartHelloGrpcServer(listenAddressG string) (*grpc.Server, error) {
return StartGrpcServer(listenAddressG, "hello")
}
func StartTunnelGrpcServer(listenAddressG string) (*grpc.Server, error) {
return StartGrpcServer(listenAddressG, "tunnel")
}
+36
View File
@@ -0,0 +1,36 @@
package v2
import (
"context"
"log"
"time"
pb "github.com/hiddify/hiddify-core/hiddifyrpc"
)
func (s *HelloService) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
return &pb.HelloResponse{Message: "Hello, " + in.Name}, nil
}
func (s *HelloService) SayHelloStream(stream pb.Hello_SayHelloStreamServer) error {
for {
req, err := stream.Recv()
if err != nil {
log.Printf("stream.Recv() failed: %v", err)
break
}
log.Printf("Received: %v", req.Name)
time.Sleep(1 * time.Second)
err = stream.Send(&pb.HelloResponse{Message: "Hello, " + req.Name})
if err != nil {
log.Printf("stream.Send() failed: %v", err)
break
}
err = stream.Send(&pb.HelloResponse{Message: "Hello again, " + req.Name})
if err != nil {
log.Printf("stream.Send() failed: %v", err)
break
}
}
return nil
}
+172
View File
@@ -0,0 +1,172 @@
package v2
import (
"fmt"
"io"
"net"
"net/http"
"time"
"github.com/hiddify/hiddify-core/config"
"golang.org/x/net/proxy"
"github.com/sagernet/sing-box/experimental/libbox"
"github.com/sagernet/sing-box/option"
)
func getRandomAvailblePort() uint16 {
// TODO: implement it
listener, err := net.Listen("tcp", ":0")
if err != nil {
panic(err)
}
defer listener.Close()
return uint16(listener.Addr().(*net.TCPAddr).Port)
}
func RunInstanceString(hiddifySettings *config.HiddifyOptions, proxiesInput string) (*HiddifyService, error) {
if hiddifySettings == nil {
hiddifySettings = config.DefaultHiddifyOptions()
}
singconfigs, err := config.ParseConfigContentToOptions(proxiesInput, true, hiddifySettings, false)
if err != nil {
return nil, err
}
return RunInstance(hiddifySettings, singconfigs)
}
func RunInstance(hiddifySettings *config.HiddifyOptions, singconfig *option.Options) (*HiddifyService, error) {
if hiddifySettings == nil {
hiddifySettings = config.DefaultHiddifyOptions()
}
hiddifySettings.EnableClashApi = false
hiddifySettings.InboundOptions.MixedPort = getRandomAvailblePort()
hiddifySettings.InboundOptions.EnableTun = false
hiddifySettings.InboundOptions.EnableTunService = false
hiddifySettings.InboundOptions.SetSystemProxy = false
hiddifySettings.InboundOptions.TProxyPort = 0
hiddifySettings.InboundOptions.LocalDnsPort = 0
hiddifySettings.Region = "other"
hiddifySettings.BlockAds = false
hiddifySettings.LogFile = "/dev/null"
finalConfigs, err := config.BuildConfig(*hiddifySettings, *singconfig)
if err != nil {
return nil, err
}
instance, err := NewService(*finalConfigs)
if err != nil {
return nil, err
}
err = instance.Start()
if err != nil {
return nil, err
}
<-time.After(250 * time.Millisecond)
hservice := &HiddifyService{libbox: instance, ListenPort: hiddifySettings.InboundOptions.MixedPort}
hservice.PingCloudflare()
return hservice, nil
}
type HiddifyService struct {
libbox *libbox.BoxService
ListenPort uint16
}
// dialer, err := s.libbox.GetInstance().Router().Dialer(context.Background())
func (s *HiddifyService) Close() error {
return s.libbox.Close()
}
func (s *HiddifyService) GetContent(url string) (string, error) {
return s.ContentFromURL("GET", url, 10*time.Second)
}
func (s *HiddifyService) ContentFromURL(method string, url string, timeout time.Duration) (string, error) {
if method == "" {
return "", fmt.Errorf("empty method")
}
if url == "" {
return "", fmt.Errorf("empty url")
}
req, err := http.NewRequest(method, url, nil)
if err != nil {
return "", err
}
dialer, err := proxy.SOCKS5("tcp", fmt.Sprintf("127.0.0.1:%d", s.ListenPort), nil, proxy.Direct)
if err != nil {
return "", err
}
transport := &http.Transport{
Dial: dialer.Dial,
}
client := &http.Client{
Transport: transport,
Timeout: timeout,
}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
return "", fmt.Errorf("request failed with status code: %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
if body == nil {
return "", fmt.Errorf("empty body")
}
return string(body), nil
}
func (s *HiddifyService) PingCloudflare() (time.Duration, error) {
return s.Ping("http://cp.cloudflare.com")
}
// func (s *HiddifyService) RawConnection(ctx context.Context, url string) (net.Conn, error) {
// return
// }
func (s *HiddifyService) PingAverage(url string, count int) (time.Duration, error) {
if count <= 0 {
return -1, fmt.Errorf("count must be greater than 0")
}
var sum int
real_count := 0
for i := 0; i < count; i++ {
delay, err := s.Ping(url)
if err == nil {
real_count++
sum += int(delay.Milliseconds())
} else if real_count == 0 && i > count/2 {
return -1, fmt.Errorf("ping average failed")
}
}
return time.Duration(sum / real_count * int(time.Millisecond)), nil
}
func (s *HiddifyService) Ping(url string) (time.Duration, error) {
startTime := time.Now()
_, err := s.ContentFromURL("HEAD", url, 4*time.Second)
if err != nil {
return -1, err
}
duration := time.Since(startTime)
return duration, nil
}
+44
View File
@@ -0,0 +1,44 @@
package v2
import (
"fmt"
"time"
pb "github.com/hiddify/hiddify-core/hiddifyrpc"
"github.com/sagernet/sing/common/observable"
"google.golang.org/grpc"
)
func NewObserver[T any](listenerBufferSize int) *observable.Observer[T] {
return observable.NewObserver(observable.NewSubscriber[T](listenerBufferSize), listenerBufferSize)
}
var logObserver = NewObserver[pb.LogMessage](10)
func Log(level pb.LogLevel, typ pb.LogType, message string) {
if level != pb.LogLevel_DEBUG {
fmt.Printf("%s %s %s\n", level, typ, message)
}
logObserver.Emit(pb.LogMessage{
Level: level,
Type: typ,
Message: message,
})
}
func (s *CoreService) LogListener(req *pb.Empty, stream grpc.ServerStreamingServer[pb.LogMessage]) error {
logSub, stopch, _ := logObserver.Subscribe()
defer logObserver.UnSubscribe(logSub)
for {
select {
case <-stream.Context().Done():
return nil
case <-stopch:
return nil
case info := <-logSub:
stream.Send(&info)
case <-time.After(500 * time.Millisecond):
}
}
}
+106
View File
@@ -0,0 +1,106 @@
package v2
import (
"encoding/json"
"fmt"
"github.com/hiddify/hiddify-core/bridge"
"github.com/sagernet/sing-box/experimental/libbox"
"github.com/sagernet/sing-box/log"
)
var (
_ libbox.CommandClientHandler = (*OldCommandClientHandler)(nil)
)
type OldCommandClientHandler struct {
port int64
logger log.Logger
}
func (cch *OldCommandClientHandler) Connected() {
cch.logger.Debug("CONNECTED")
}
func (cch *OldCommandClientHandler) Disconnected(message string) {
cch.logger.Debug("DISCONNECTED: ", message)
}
func (cch *OldCommandClientHandler) ClearLog() {
cch.logger.Debug("clear log")
}
func (cch *OldCommandClientHandler) WriteLog(message string) {
cch.logger.Debug("log: ", message)
}
func (cch *OldCommandClientHandler) WriteStatus(message *libbox.StatusMessage) {
msg, err := json.Marshal(
map[string]int64{
"connections-in": int64(message.ConnectionsIn),
"connections-out": int64(message.ConnectionsOut),
"uplink": message.Uplink,
"downlink": message.Downlink,
"uplink-total": message.UplinkTotal,
"downlink-total": message.DownlinkTotal,
},
)
cch.logger.Debug("Memory: ", libbox.FormatBytes(message.Memory), ", Goroutines: ", message.Goroutines)
if err != nil {
bridge.SendStringToPort(cch.port, fmt.Sprintf("error: %e", err))
} else {
bridge.SendStringToPort(cch.port, string(msg))
}
}
func (cch *OldCommandClientHandler) WriteGroups(message libbox.OutboundGroupIterator) {
if message == nil {
return
}
groups := []*OutboundGroup{}
for message.HasNext() {
group := message.Next()
items := group.GetItems()
groupItems := []*OutboundGroupItem{}
for items.HasNext() {
item := items.Next()
groupItems = append(groupItems,
&OutboundGroupItem{
Tag: item.Tag,
Type: item.Type,
URLTestTime: item.URLTestTime,
URLTestDelay: item.URLTestDelay,
},
)
}
groups = append(groups, &OutboundGroup{Tag: group.Tag, Type: group.Type, Selected: group.Selected, Items: groupItems})
}
response, err := json.Marshal(groups)
if err != nil {
bridge.SendStringToPort(cch.port, fmt.Sprintf("error: %e", err))
} else {
bridge.SendStringToPort(cch.port, string(response))
}
}
func (cch *OldCommandClientHandler) InitializeClashMode(modeList libbox.StringIterator, currentMode string) {
cch.logger.Debug("initial clash mode: ", currentMode)
}
func (cch *OldCommandClientHandler) UpdateClashMode(newMode string) {
cch.logger.Debug("update clash mode: ", newMode)
}
type OutboundGroup struct {
Tag string `json:"tag"`
Type string `json:"type"`
Selected string `json:"selected"`
Items []*OutboundGroupItem `json:"items"`
}
type OutboundGroupItem struct {
Tag string `json:"tag"`
Type string `json:"type"`
URLTestTime int64 `json:"url-test-time"`
URLTestDelay int32 `json:"url-test-delay"`
}
+53
View File
@@ -0,0 +1,53 @@
package v2
import (
pb "github.com/hiddify/hiddify-core/hiddifyrpc"
"github.com/sagernet/sing-box/experimental/libbox"
"github.com/sagernet/sing-box/log"
)
var oldCommandServer *libbox.CommandServer
type CommandServerHandler struct {
logger log.Logger
}
func (csh *CommandServerHandler) ServiceReload() error {
csh.logger.Trace("Reloading service")
SetCoreStatus(pb.CoreState_STARTING, pb.MessageType_EMPTY, "")
if oldCommandServer != nil {
oldCommandServer.SetService(nil)
oldCommandServer = nil
}
if Box != nil {
Box.Close()
Box = nil
}
_, err := StartService(&pb.StartRequest{
EnableOldCommandServer: true,
DelayStart: true,
})
return err
}
func (csh *CommandServerHandler) GetSystemProxyStatus() *libbox.SystemProxyStatus {
csh.logger.Trace("Getting system proxy status")
return &libbox.SystemProxyStatus{Available: true, Enabled: false}
}
func (csh *CommandServerHandler) SetSystemProxyEnabled(isEnabled bool) error {
csh.logger.Trace("Setting system proxy status, enabled? ", isEnabled)
return csh.ServiceReload()
}
func (csh *CommandServerHandler) PostServiceClose() {
}
func startCommandServer() error {
logger := coreLogFactory.NewLogger("[Command Server Handler]")
logger.Trace("Starting command server")
oldCommandServer = libbox.NewCommandServer(&CommandServerHandler{logger: logger}, 300)
return oldCommandServer.Start()
}
+71
View File
@@ -0,0 +1,71 @@
package v2
import (
"github.com/sagernet/sing-box/experimental/libbox"
)
var (
oldStatusClient *libbox.CommandClient
oldGroupClient *libbox.CommandClient
oldGroupInfoOnlyClient *libbox.CommandClient
)
func StartCommand(command int32, port int64) error {
switch command {
case libbox.CommandStatus:
oldStatusClient = libbox.NewCommandClient(
&OldCommandClientHandler{
port: port,
logger: coreLogFactory.NewLogger("[Status Command Client]"),
},
&libbox.CommandClientOptions{
Command: libbox.CommandStatus,
StatusInterval: 1000000000, //1000ms debounce
},
)
return oldStatusClient.Connect()
case libbox.CommandGroup:
oldGroupClient = libbox.NewCommandClient(
&OldCommandClientHandler{
port: port,
logger: coreLogFactory.NewLogger("[Group Command Client]"),
},
&libbox.CommandClientOptions{
Command: libbox.CommandGroup,
StatusInterval: 300000000, //300ms debounce
},
)
return oldGroupClient.Connect()
case libbox.CommandGroupInfoOnly:
oldGroupInfoOnlyClient = libbox.NewCommandClient(
&OldCommandClientHandler{
port: port,
logger: coreLogFactory.NewLogger("[GroupInfoOnly Command Client]"),
},
&libbox.CommandClientOptions{
Command: libbox.CommandGroupInfoOnly,
StatusInterval: 300000000, //300ms debounce
},
)
return oldGroupInfoOnlyClient.Connect()
}
return nil
}
func StopCommand(command int32) error {
switch command {
case libbox.CommandStatus:
err := oldStatusClient.Disconnect()
oldStatusClient = nil
return err
case libbox.CommandGroup:
err := oldGroupClient.Disconnect()
oldGroupClient = nil
return err
case libbox.CommandGroupInfoOnly:
err := oldGroupInfoOnlyClient.Disconnect()
oldGroupInfoOnlyClient = nil
return err
}
return nil
}
+38
View File
@@ -0,0 +1,38 @@
package v2
import pb "github.com/hiddify/hiddify-core/hiddifyrpc"
const (
Stopped = "Stopped"
Starting = "Starting"
Started = "Started"
Stopping = "Stopping"
)
const (
EmptyConfiguration = "EmptyConfiguration"
StartCommandServer = "StartCommandServer"
CreateService = "CreateService"
)
func convert2OldState(newStatus pb.CoreState) string {
if newStatus == pb.CoreState_STOPPED {
return Stopped
}
if newStatus == pb.CoreState_STARTED {
return Started
}
if newStatus == pb.CoreState_STARTING {
return Starting
}
if newStatus == pb.CoreState_STOPPING {
return Stopping
}
return "Invalid"
}
type StatusMessage struct {
Status string `json:"status"`
Alert *string `json:"alert"`
Message *string `json:"message"`
}
+101
View File
@@ -0,0 +1,101 @@
package v2
import (
"context"
"io"
"os"
"runtime"
runtimeDebug "runtime/debug"
"time"
"github.com/hiddify/hiddify-core/v2/service_manager"
B "github.com/sagernet/sing-box"
"github.com/sagernet/sing-box/common/urltest"
"github.com/sagernet/sing-box/experimental/libbox"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/service"
"github.com/sagernet/sing/service/filemanager"
"github.com/sagernet/sing/service/pause"
)
var (
sWorkingPath string
sTempPath string
sUserID int
sGroupID int
statusPropagationPort int64
)
func InitHiddifyService() error {
return service_manager.StartServices()
}
func Setup(basePath string, workingPath string, tempPath string, statusPort int64, debug bool) error {
statusPropagationPort = int64(statusPort)
tcpConn := runtime.GOOS == "windows" // TODO add TVOS
libbox.Setup(basePath, workingPath, tempPath, tcpConn)
sWorkingPath = workingPath
os.Chdir(sWorkingPath)
sTempPath = tempPath
sUserID = os.Getuid()
sGroupID = os.Getgid()
var defaultWriter io.Writer
if !debug {
defaultWriter = io.Discard
}
factory, err := log.New(
log.Options{
DefaultWriter: defaultWriter,
BaseTime: time.Now(),
Observable: true,
// Options: option.LogOptions{
// Disabled: false,
// Level: "trace",
// Output: "stdout",
// },
})
coreLogFactory = factory
if err != nil {
return E.Cause(err, "create logger")
}
return InitHiddifyService()
}
func NewService(options option.Options) (*libbox.BoxService, error) {
runtimeDebug.FreeOSMemory()
ctx, cancel := context.WithCancel(context.Background())
ctx = filemanager.WithDefault(ctx, sWorkingPath, sTempPath, sUserID, sGroupID)
urlTestHistoryStorage := urltest.NewHistoryStorage()
ctx = service.ContextWithPtr(ctx, urlTestHistoryStorage)
instance, err := B.New(B.Options{
Context: ctx,
Options: options,
})
if err != nil {
cancel()
return nil, E.Cause(err, "create service")
}
runtimeDebug.FreeOSMemory()
service := libbox.NewBoxService(
ctx,
cancel,
instance,
service.FromContext[pause.Manager](ctx),
urlTestHistoryStorage,
)
return &service, nil
}
func readOptions(configContent string) (option.Options, error) {
var options option.Options
err := options.UnmarshalJSON([]byte(configContent))
if err != nil {
return option.Options{}, E.Cause(err, "decode config")
}
return options, nil
}
+47
View File
@@ -0,0 +1,47 @@
package service_manager
import (
"github.com/sagernet/sing-box/adapter"
)
var (
services = []adapter.Service{}
preservices = []adapter.Service{}
)
func RegisterPreservice(service adapter.Service) {
preservices = append(services, service)
}
func Register(service adapter.Service) {
services = append(services, service)
}
func StartServices() error {
CloseServices()
for _, service := range preservices {
if err := service.Start(); err != nil {
return err
}
}
for _, service := range services {
if err := service.Start(); err != nil {
return err
}
}
return nil
}
func CloseServices() error {
for _, service := range services {
if err := service.Close(); err != nil {
return err
}
}
for _, service := range preservices {
if err := service.Close(); err != nil {
return err
}
}
return nil
}
+248
View File
@@ -0,0 +1,248 @@
package v2
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"os/signal"
"runtime"
"strconv"
"strings"
"syscall"
"time"
"github.com/hiddify/hiddify-core/config"
pb "github.com/hiddify/hiddify-core/hiddifyrpc"
"github.com/sagernet/sing-box/option"
)
func RunStandalone(hiddifySettingPath string, configPath string, defaultConfig config.HiddifyOptions) error {
fmt.Println("Running in standalone mode")
useFlutterBridge = false
current, err := readAndBuildConfig(hiddifySettingPath, configPath, &defaultConfig)
if err != nil {
fmt.Printf("Error in read and build config %v", err)
return err
}
go StartService(&pb.StartRequest{
ConfigContent: current.Config,
EnableOldCommandServer: false,
DelayStart: false,
EnableRawConfig: true,
})
go updateConfigInterval(current, hiddifySettingPath, configPath)
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
fmt.Printf("Waiting for CTRL+C to stop\n")
<-sigChan
fmt.Printf("CTRL+C recived-->stopping\n")
_, err = Stop()
return err
}
type ConfigResult struct {
Config string
RefreshInterval int
HiddifyHiddifyOptions *config.HiddifyOptions
}
func readAndBuildConfig(hiddifySettingPath string, configPath string, defaultConfig *config.HiddifyOptions) (ConfigResult, error) {
var result ConfigResult
result, err := readConfigContent(configPath)
if err != nil {
return result, err
}
hiddifyconfig := config.DefaultHiddifyOptions()
if defaultConfig != nil {
hiddifyconfig = defaultConfig
}
if hiddifySettingPath != "" {
hiddifyconfig, err = ReadHiddifyOptionsAt(hiddifySettingPath)
if err != nil {
return result, err
}
}
result.HiddifyHiddifyOptions = hiddifyconfig
result.Config, err = buildConfig(result.Config, *result.HiddifyHiddifyOptions)
if err != nil {
return result, err
}
return result, nil
}
func readConfigContent(configPath string) (ConfigResult, error) {
var content string
var refreshInterval int
if strings.HasPrefix(configPath, "http://") || strings.HasPrefix(configPath, "https://") {
client := &http.Client{}
// Create a new request
req, err := http.NewRequest("GET", configPath, nil)
if err != nil {
fmt.Println("Error creating request:", err)
return ConfigResult{}, err
}
req.Header.Set("User-Agent", "HiddifyNext/2.3.1 ("+runtime.GOOS+") like ClashMeta v2ray sing-box")
resp, err := client.Do(req)
if err != nil {
fmt.Println("Error making GET request:", err)
return ConfigResult{}, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return ConfigResult{}, fmt.Errorf("failed to read config body: %w", err)
}
content = string(body)
refreshInterval, _ = extractRefreshInterval(resp.Header, content)
fmt.Printf("Refresh interval: %d\n", refreshInterval)
} else {
data, err := ioutil.ReadFile(configPath)
if err != nil {
return ConfigResult{}, fmt.Errorf("failed to read config file: %w", err)
}
content = string(data)
}
return ConfigResult{
Config: content,
RefreshInterval: refreshInterval,
}, nil
}
func extractRefreshInterval(header http.Header, bodyStr string) (int, error) {
refreshIntervalStr := header.Get("profile-update-interval")
if refreshIntervalStr != "" {
refreshInterval, err := strconv.Atoi(refreshIntervalStr)
if err != nil {
return 0, fmt.Errorf("failed to parse refresh interval from header: %w", err)
}
return refreshInterval, nil
}
lines := strings.Split(bodyStr, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "//profile-update-interval:") || strings.HasPrefix(line, "#profile-update-interval:") {
parts := strings.SplitN(line, ":", 2)
str := strings.TrimSpace(parts[1])
refreshInterval, err := strconv.Atoi(str)
if err != nil {
return 0, fmt.Errorf("failed to parse refresh interval from body: %w", err)
}
return refreshInterval, nil
}
}
return 0, nil
}
func buildConfig(configContent string, options config.HiddifyOptions) (string, error) {
parsedContent, err := config.ParseConfigContent(configContent, true, &options, false)
if err != nil {
return "", fmt.Errorf("failed to parse config content: %w", err)
}
singconfigs, err := readConfigBytes([]byte(parsedContent))
if err != nil {
return "", err
}
finalconfig, err := config.BuildConfig(options, *singconfigs)
if err != nil {
return "", fmt.Errorf("failed to build config: %w", err)
}
finalconfig.Log.Output = ""
finalconfig.Experimental.ClashAPI.ExternalUI = "webui"
if options.AllowConnectionFromLAN {
finalconfig.Experimental.ClashAPI.ExternalController = "0.0.0.0:6756"
} else {
finalconfig.Experimental.ClashAPI.ExternalController = "127.0.0.1:6756"
}
fmt.Printf("Open http://localhost:6756/ui/?secret=%s in your browser\n", finalconfig.Experimental.ClashAPI.Secret)
if err := Setup("./", "./", "./tmp", 0, false); err != nil {
return "", fmt.Errorf("failed to set up global configuration: %w", err)
}
configStr, err := config.ToJson(*finalconfig)
if err != nil {
return "", fmt.Errorf("failed to convert config to JSON: %w", err)
}
return configStr, nil
}
func updateConfigInterval(current ConfigResult, hiddifySettingPath string, configPath string) {
if current.RefreshInterval <= 0 {
return
}
for {
<-time.After(time.Duration(current.RefreshInterval) * time.Hour)
new, err := readAndBuildConfig(hiddifySettingPath, configPath, current.HiddifyHiddifyOptions)
if err != nil {
continue
}
if new.Config != current.Config {
go Stop()
go StartService(&pb.StartRequest{
ConfigContent: new.Config,
DelayStart: false,
EnableOldCommandServer: false,
DisableMemoryLimit: false,
EnableRawConfig: true,
})
}
current = new
}
}
func readConfigBytes(content []byte) (*option.Options, error) {
var options option.Options
err := options.UnmarshalJSON(content)
if err != nil {
return nil, err
}
return &options, nil
}
func ReadHiddifyOptionsAt(path string) (*config.HiddifyOptions, error) {
content, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var options config.HiddifyOptions
err = json.Unmarshal(content, &options)
if err != nil {
return nil, err
}
if options.Warp.WireguardConfigStr != "" {
err := json.Unmarshal([]byte(options.Warp.WireguardConfigStr), &options.Warp.WireguardConfig)
if err != nil {
return nil, err
}
}
if options.Warp2.WireguardConfigStr != "" {
err := json.Unmarshal([]byte(options.Warp2.WireguardConfigStr), &options.Warp2.WireguardConfig)
if err != nil {
return nil, err
}
}
return &options, nil
}
+44
View File
@@ -0,0 +1,44 @@
package v2
import (
"context"
pb "github.com/hiddify/hiddify-core/hiddifyrpc"
"github.com/sagernet/sing-box/experimental/libbox"
)
func (s *CoreService) GetSystemProxyStatus(ctx context.Context, empty *pb.Empty) (*pb.SystemProxyStatus, error) {
return GetSystemProxyStatus(ctx, empty)
}
func GetSystemProxyStatus(ctx context.Context, empty *pb.Empty) (*pb.SystemProxyStatus, error) {
status, err := libbox.NewStandaloneCommandClient().GetSystemProxyStatus()
if err != nil {
return nil, err
}
return &pb.SystemProxyStatus{
Available: status.Available,
Enabled: status.Enabled,
}, nil
}
func (s *CoreService) SetSystemProxyEnabled(ctx context.Context, in *pb.SetSystemProxyEnabledRequest) (*pb.Response, error) {
return SetSystemProxyEnabled(ctx, in)
}
func SetSystemProxyEnabled(ctx context.Context, in *pb.SetSystemProxyEnabledRequest) (*pb.Response, error) {
err := libbox.NewStandaloneCommandClient().SetSystemProxyEnabled(in.IsEnabled)
if err != nil {
return &pb.Response{
ResponseCode: pb.ResponseCode_FAILED,
Message: err.Error(),
}, err
}
return &pb.Response{
ResponseCode: pb.ResponseCode_OK,
Message: "",
}, nil
}
+147
View File
@@ -0,0 +1,147 @@
package v2
import (
"fmt"
"log"
"os"
"path/filepath"
"github.com/kardianos/service"
)
var logger service.Logger
type hiddifyNext struct{}
var port int = 18020
func (m *hiddifyNext) Start(s service.Service) error {
_, err := StartTunnelGrpcServer(fmt.Sprintf("127.0.0.1:%d", port))
return err
}
func (m *hiddifyNext) Stop(s service.Service) error {
_, err := Stop()
if err != nil {
return nil
}
// Stop should not block. Return with a few seconds.
// <-time.After(time.Second * 1)
return nil
}
func getCurrentExecutableDirectory() string {
executablePath, err := os.Executable()
if err != nil {
return ""
}
// Extract the directory (folder) containing the executable
executableDirectory := filepath.Dir(executablePath)
return executableDirectory
}
func StartTunnelService(goArg string) (int, string) {
svcConfig := &service.Config{
Name: "HiddifyTunnelService",
DisplayName: "Hiddify Tunnel Service",
Arguments: []string{"tunnel", "run"},
Description: "This is a bridge for tunnel",
Option: map[string]interface{}{
"RunAtLoad": true,
"WorkingDirectory": getCurrentExecutableDirectory(),
},
}
prg := &hiddifyNext{}
s, err := service.New(prg, svcConfig)
if err != nil {
// log.Printf("Error: %v", err)
return 1, fmt.Sprintf("Error: %v", err)
}
if len(goArg) > 0 && goArg != "run" {
return control(s, goArg)
}
logger, err = s.Logger(nil)
if err != nil {
log.Printf("Error: %v", err)
}
err = s.Run()
if err != nil {
logger.Error(err)
return 3, fmt.Sprintf("Error: %v", err)
}
return 0, ""
}
func control(s service.Service, goArg string) (int, string) {
dolog := false
var err error
status, serr := s.Status()
if dolog {
fmt.Printf("Current Status: %+v %+v!\n", status, serr)
}
switch goArg {
case "uninstall":
if status == service.StatusRunning {
s.Stop()
}
if dolog {
fmt.Printf("Tunnel Service Uninstalled Successfully.\n")
}
err = s.Uninstall()
case "start":
if status == service.StatusRunning {
if dolog {
fmt.Printf("Tunnel Service Already Running.\n")
}
return 0, "Tunnel Service Already Running."
} else if status == service.StatusUnknown {
s.Uninstall()
s.Install()
status, serr = s.Status()
if dolog {
fmt.Printf("Check status again: %+v %+v!", status, serr)
}
}
if status != service.StatusRunning {
err = s.Start()
}
case "install":
s.Uninstall()
err = s.Install()
status, serr = s.Status()
if dolog {
fmt.Printf("Check Status Again: %+v %+v", status, serr)
}
if status != service.StatusRunning {
err = s.Start()
}
case "stop":
if status == service.StatusStopped {
if dolog {
fmt.Printf("Tunnel Service Already Stopped.\n")
}
return 0, "Tunnel Service Already Stopped."
}
err = s.Stop()
default:
err = service.Control(s, goArg)
}
if err == nil {
out := fmt.Sprintf("Tunnel Service %sed Successfully.", goArg)
if dolog {
fmt.Printf(out)
}
return 0, out
} else {
out := fmt.Sprintf("Error: %v", err)
if dolog {
log.Printf(out)
}
return 2, out
}
}
+114
View File
@@ -0,0 +1,114 @@
package v2
import (
"context"
"fmt"
"log"
"os"
pb "github.com/hiddify/hiddify-core/hiddifyrpc"
)
func (s *TunnelService) Start(ctx context.Context, in *pb.TunnelStartRequest) (*pb.TunnelResponse, error) {
if in.ServerPort == 0 {
in.ServerPort = 12334
}
useFlutterBridge = false
res, err := Start(&pb.StartRequest{
ConfigContent: makeTunnelConfig(in.Ipv6, in.ServerPort, in.StrictRoute, in.EndpointIndependentNat, in.Stack),
EnableOldCommandServer: false,
DisableMemoryLimit: true,
EnableRawConfig: true,
})
fmt.Printf("Start Result: %+v\n", res)
if err != nil {
return &pb.TunnelResponse{
Message: err.Error(),
}, err
}
return &pb.TunnelResponse{
Message: "OK",
}, err
}
func makeTunnelConfig(Ipv6 bool, ServerPort int32, StrictRoute bool, EndpointIndependentNat bool, Stack string) string {
var ipv6 string
if Ipv6 {
ipv6 = ` "inet6_address": "fdfe:dcba:9876::1/126",`
} else {
ipv6 = ""
}
base := `{
"log":{
"level": "warn"
},
"inbounds": [
{
"type": "tun",
"tag": "tun-in",
"interface_name": "HiddifyTunnel",
"inet4_address": "172.19.0.1/30",
` + ipv6 + `
"auto_route": true,
"strict_route": ` + fmt.Sprintf("%t", StrictRoute) + `,
"endpoint_independent_nat": ` + fmt.Sprintf("%t", EndpointIndependentNat) + `,
"stack": "` + Stack + `"
}
],
"outbounds": [
{
"type": "socks",
"tag": "socks-out",
"server": "127.0.0.1",
"server_port": ` + fmt.Sprintf("%d", ServerPort) + `,
"version": "5"
},
{
"type": "direct",
"tag": "direct-out"
}
],
"route": {
"rules": [
{
"process_name":[
"Hiddify.exe",
"Hiddify",
"HiddifyCli",
"HiddifyCli.exe"
],
"outbound": "direct-out"
}
]
}
}`
return base
}
func (s *TunnelService) Stop(ctx context.Context, _ *pb.Empty) (*pb.TunnelResponse, error) {
res, err := Stop()
log.Printf("Stop Result: %+v\n", res)
if err != nil {
return &pb.TunnelResponse{
Message: err.Error(),
}, err
}
return &pb.TunnelResponse{
Message: "OK",
}, err
}
func (s *TunnelService) Status(ctx context.Context, _ *pb.Empty) (*pb.TunnelResponse, error) {
return &pb.TunnelResponse{
Message: "Not Implemented",
}, nil
}
func (s *TunnelService) Exit(ctx context.Context, _ *pb.Empty) (*pb.TunnelResponse, error) {
Stop()
os.Exit(0)
return &pb.TunnelResponse{
Message: "OK",
}, nil
}
+32
View File
@@ -0,0 +1,32 @@
package v2
import (
"context"
"github.com/hiddify/hiddify-core/config"
pb "github.com/hiddify/hiddify-core/hiddifyrpc"
)
func (s *CoreService) GenerateWarpConfig(ctx context.Context, in *pb.GenerateWarpConfigRequest) (*pb.WarpGenerationResponse, error) {
return GenerateWarpConfig(in)
}
func GenerateWarpConfig(in *pb.GenerateWarpConfigRequest) (*pb.WarpGenerationResponse, error) {
identity, log, wg, err := config.GenerateWarpInfo(in.LicenseKey, in.AccountId, in.AccessToken)
if err != nil {
return nil, err
}
return &pb.WarpGenerationResponse{
Account: &pb.WarpAccount{
AccountId: identity.ID,
AccessToken: identity.Token,
},
Config: &pb.WarpWireguardConfig{
PrivateKey: wg.PrivateKey,
LocalAddressIpv4: wg.LocalAddressIPv4,
LocalAddressIpv6: wg.LocalAddressIPv6,
PeerPublicKey: wg.PeerPublicKey,
ClientId: wg.ClientID,
},
Log: log,
}, nil
}