package main import ( "crypto/sha256" "encoding/hex" "os" "path/filepath" "github.com/sagernet/sing/common/json" "github.com/sagernet/tailscale/atomicfile" ) const ( serviceConfigFileName = "config.json" startOptionsFileName = "start_options.json" ownerFileName = "owner.json" usersDirectoryName = "users" ) type startOptions struct { WasRunning bool `json:"was_running"` OOMKillerEnabled bool `json:"oom_killer_enabled"` OOMKillerDisabled bool `json:"oom_killer_disabled"` OOMMemoryLimit int64 `json:"oom_memory_limit"` SystemProxyEnabled *bool `json:"system_proxy_enabled,omitempty"` } type ownerState struct { UserID string `json:"user_id"` SessionID uint32 `json:"session_id,omitempty"` } func userWorkingDirectory(userID string) string { digest := sha256.Sum256([]byte(userID)) return filepath.Join(workingDirectory, usersDirectoryName, hex.EncodeToString(digest[:])) } func loadOwner() (string, error) { state, err := loadOwnerState() if err != nil { return "", err } return state.UserID, nil } func loadOwnerState() (ownerState, error) { content, err := os.ReadFile(filepath.Join(workingDirectory, ownerFileName)) if err != nil { return ownerState{}, err } state, err := json.UnmarshalExtended[ownerState](content) if err != nil { return ownerState{}, err } return state, nil } func saveOwner(userID string, sessionID uint32) error { content, err := json.Marshal(ownerState{UserID: userID, SessionID: sessionID}) if err != nil { return err } return atomicfile.WriteFile(filepath.Join(workingDirectory, ownerFileName), content, 0o600) } func (o startOptions) systemProxyEnabled() bool { return o.SystemProxyEnabled == nil || *o.SystemProxyEnabled } func loadServiceConfig(userID string) (string, error) { content, err := os.ReadFile(filepath.Join(userWorkingDirectory(userID), serviceConfigFileName)) if err != nil { return "", err } return string(content), nil } func loadStartOptions(userID string) (startOptions, error) { content, err := os.ReadFile(filepath.Join(userWorkingDirectory(userID), startOptionsFileName)) if err != nil { return startOptions{}, err } options, err := json.UnmarshalExtended[startOptions](content) if err != nil { return startOptions{}, err } return options, nil } func saveStartOptions(userID string, options startOptions) error { content, err := json.Marshal(options) if err != nil { return err } directory := userWorkingDirectory(userID) err = os.MkdirAll(directory, 0o700) if err != nil { return err } return atomicfile.WriteFile(filepath.Join(directory, startOptionsFileName), content, 0o600) }