Refactor endpoint in started interface

This commit is contained in:
世界
2026-07-18 00:28:16 +08:00
parent 414fec5371
commit 60f3012444
20 changed files with 326 additions and 110 deletions
+2 -2
View File
@@ -63,8 +63,8 @@ func runWorker() error {
}
defer listener.Close()
server := grpc.NewServer(
grpc.ChainUnaryInterceptor(unaryLocaleInterceptor),
grpc.ChainStreamInterceptor(streamLocaleInterceptor),
grpc.ChainUnaryInterceptor(daemon.UnaryLocaleInterceptor),
grpc.ChainStreamInterceptor(daemon.StreamLocaleInterceptor),
)
RegisterApplicationServiceServer(server, &applicationService{
startedService: daemon.NewStartedService(daemon.ServiceOptions{Context: include.Context(context.Background())}),
-29
View File
@@ -1,29 +0,0 @@
package main
import (
"context"
"slices"
"github.com/sagernet/sing-box/experimental/locale"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)
func setLocaleFromContext(ctx context.Context) {
requestMetadata, loaded := metadata.FromIncomingContext(ctx)
if !loaded {
return
}
slices.ContainsFunc(requestMetadata.Get("accept-language"), locale.Set)
}
func unaryLocaleInterceptor(ctx context.Context, request any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
setLocaleFromContext(ctx)
return handler(ctx, request)
}
func streamLocaleInterceptor(server any, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
setLocaleFromContext(stream.Context())
return handler(server, stream)
}
+2 -2
View File
@@ -66,8 +66,8 @@ func newDaemon() (*Daemon, error) {
})
authorizer := newAuthorizer(d)
serverOptions := []grpc.ServerOption{
grpc.ChainUnaryInterceptor(newUnaryAuthorizeInterceptor(authorizer), unaryLocaleInterceptor),
grpc.ChainStreamInterceptor(newStreamAuthorizeInterceptor(authorizer), streamLocaleInterceptor),
grpc.ChainUnaryInterceptor(newUnaryAuthorizeInterceptor(authorizer), daemon.UnaryLocaleInterceptor),
grpc.ChainStreamInterceptor(newStreamAuthorizeInterceptor(authorizer), daemon.StreamLocaleInterceptor),
}
platformOptions, err := platformServerOptions(d)
if err != nil {
+2 -2
View File
@@ -151,8 +151,8 @@ func networkConnectionFromFileDescriptor(fileDescriptor int32) (net.Conn, error)
func localDialOptions(contextDialer func(context.Context, string) (net.Conn, error)) []grpc.DialOption {
options := []grpc.DialOption{
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithUnaryInterceptor(unaryClientAuthInterceptor),
grpc.WithStreamInterceptor(streamClientAuthInterceptor),
grpc.WithChainUnaryInterceptor(daemon.UnaryClientLocaleInterceptor, unaryClientAuthInterceptor),
grpc.WithChainStreamInterceptor(daemon.StreamClientLocaleInterceptor, streamClientAuthInterceptor),
}
if contextDialer != nil {
options = append(options, grpc.WithContextDialer(contextDialer))
+17 -11
View File
@@ -8,6 +8,7 @@ import (
"strings"
"time"
"github.com/sagernet/sing-box/daemon"
E "github.com/sagernet/sing/common/exceptions"
"google.golang.org/grpc"
@@ -62,19 +63,24 @@ func newRemoteConnection(options *RemoteConnectionOptions) (*remoteConnection, e
if port == "" {
port = defaultPort
}
authorization := ""
if options.Secret != "" {
authorization = "Bearer " + options.Secret
}
dialOptions := []grpc.DialOption{
grpc.WithTransportCredentials(transportCredentials),
}
if options.Secret != "" {
authorization := "Bearer " + options.Secret
dialOptions = append(dialOptions,
grpc.WithUnaryInterceptor(func(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
return invoker(metadata.AppendToOutgoingContext(ctx, "authorization", authorization), method, req, reply, cc, opts...)
}),
grpc.WithStreamInterceptor(func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
return streamer(metadata.AppendToOutgoingContext(ctx, "authorization", authorization), desc, cc, method, opts...)
}),
)
grpc.WithChainUnaryInterceptor(daemon.UnaryClientLocaleInterceptor, func(ctx context.Context, method string, request, reply any, connection *grpc.ClientConn, invoker grpc.UnaryInvoker, options ...grpc.CallOption) error {
if authorization != "" {
ctx = metadata.AppendToOutgoingContext(ctx, "authorization", authorization)
}
return invoker(ctx, method, request, reply, connection, options...)
}),
grpc.WithChainStreamInterceptor(daemon.StreamClientLocaleInterceptor, func(ctx context.Context, description *grpc.StreamDesc, connection *grpc.ClientConn, method string, streamer grpc.Streamer, options ...grpc.CallOption) (grpc.ClientStream, error) {
if authorization != "" {
ctx = metadata.AppendToOutgoingContext(ctx, "authorization", authorization)
}
return streamer(ctx, description, connection, method, options...)
}),
}
return &remoteConnection{
target: net.JoinHostPort(host, port),
+2 -2
View File
@@ -161,8 +161,8 @@ func (s *CommandServer) Start() error {
}
s.listener = listener
serverOptions := []grpc.ServerOption{
grpc.UnaryInterceptor(unaryAuthInterceptor),
grpc.StreamInterceptor(streamAuthInterceptor),
grpc.ChainUnaryInterceptor(unaryAuthInterceptor, daemon.UnaryLocaleInterceptor),
grpc.ChainStreamInterceptor(streamAuthInterceptor, daemon.StreamLocaleInterceptor),
}
s.grpcServer = grpc.NewServer(serverOptions...)
daemon.RegisterStartedServiceServer(s.grpcServer, s.StartedService)
@@ -21,6 +21,7 @@ type OpenConnectEndpointStatusIterator interface {
type OpenConnectEndpointStatus struct {
EndpointTag string
State string
StateText string
AuthForm *OpenConnectAuthForm
Error string
TunnelInfo *OpenConnectTunnelInfo
@@ -121,6 +122,7 @@ func openConnectEndpointStatusFromGRPC(status *daemon.OpenConnectEndpointStatus)
result := &OpenConnectEndpointStatus{
EndpointTag: status.EndpointTag,
State: status.State,
StateText: status.StateText,
Error: status.Error,
}
if status.AuthForm != nil {
@@ -21,6 +21,7 @@ type OpenVPNEndpointStatusIterator interface {
type OpenVPNEndpointStatus struct {
EndpointTag string
State string
StateText string
Challenge *OpenVPNChallenge
Error string
TunnelInfo *OpenVPNTunnelInfo
@@ -86,6 +87,7 @@ func openVPNEndpointStatusFromGRPC(status *daemon.OpenVPNEndpointStatus) *OpenVP
result := &OpenVPNEndpointStatus{
EndpointTag: status.EndpointTag,
State: status.State,
StateText: status.StateText,
Error: status.Error,
}
if status.Challenge != nil {
@@ -18,6 +18,7 @@ type TailscaleEndpointStatusIterator interface {
type TailscaleEndpointStatus struct {
EndpointTag string
BackendState string
StateText string
AuthURL string
NetworkName string
MagicDNSSuffix string
@@ -105,6 +106,7 @@ func tailscaleEndpointStatusFromGRPC(status *daemon.TailscaleEndpointStatus) *Ta
result := &TailscaleEndpointStatus{
EndpointTag: status.EndpointTag,
BackendState: status.BackendState,
StateText: status.StateText,
AuthURL: status.AuthURL,
NetworkName: status.NetworkName,
MagicDNSSuffix: status.MagicDNSSuffix,
+99 -7
View File
@@ -1,6 +1,7 @@
package locale
import (
"context"
"strings"
"sync/atomic"
@@ -31,6 +32,18 @@ type Locale struct {
DeprecatedMessageNoLink string
InsecureFeatureMessage string
ExternalPathFeature string
TailscaleInitializing string
TailscaleInUse string
TailscaleNeedsLogin string
TailscaleNeedsApproval string
TailscaleStopped string
TailscaleStarting string
TailscaleRunning string
VPNConnecting string
VPNAuthentication string
VPNConnected string
VPNError string
Unknown string
}
var defaultLocale = &Locale{
@@ -39,8 +52,22 @@ var defaultLocale = &Locale{
DeprecatedMessageNoLink: "%s is deprecated in sing-box %s and will be removed in sing-box %s.",
InsecureFeatureMessage: "%s is considered insecure in the graphical client for sing-box on Windows. Enable Insecure Mode in `Settings - Core - Insecure Mode` to use it.",
ExternalPathFeature: "Access to %s (outside of the working directory) is considered insecure in the graphical client for sing-box on Windows. Enable Insecure Mode in `Settings - Core - Insecure Mode` to use it.",
TailscaleInitializing: "Initializing",
TailscaleInUse: "In use by another user",
TailscaleNeedsLogin: "Needs login",
TailscaleNeedsApproval: "Needs approval",
TailscaleStopped: "Stopped",
TailscaleStarting: "Starting",
TailscaleRunning: "Running",
VPNConnecting: "Connecting",
VPNAuthentication: "Authentication required",
VPNConnected: "Connected",
VPNError: "Error",
Unknown: "Unknown",
}
type contextKey struct{}
func init() {
current.Store(defaultLocale)
}
@@ -49,7 +76,41 @@ func Current() *Locale {
return current.Load()
}
func ContextWithLocale(ctx context.Context, localeID string) (context.Context, bool) {
selectedLocale, loaded := selectLocale(localeID)
if !loaded {
return ctx, false
}
return context.WithValue(ctx, contextKey{}, selectedLocale), true
}
func FromContext(ctx context.Context) *Locale {
selectedLocale, loaded := ctx.Value(contextKey{}).(*Locale)
if loaded {
return selectedLocale
}
return Current()
}
func selectLocale(localeID string) (*Locale, bool) {
localeName, loaded := match(localeID)
if !loaded {
return nil, false
}
selectedLocale, loaded := localeRegistry[localeName]
return selectedLocale, loaded
}
func Set(localeID string) bool {
selectedLocale, loaded := selectLocale(localeID)
if !loaded {
return false
}
current.Store(selectedLocale)
return true
}
func match(localeID string) (string, bool) {
localeEntries := strings.Split(localeID, ",")
for i, localeEntry := range localeEntries {
languageID, options, hasOptions := strings.Cut(localeEntry, ";")
@@ -74,7 +135,7 @@ func Set(localeID string) bool {
localeID = strings.Join(localeEntries, ",")
tags, _, err := language.ParseAcceptLanguage(localeID)
if err != nil || len(tags) == 0 {
return false
return "", false
}
for i, tag := range tags {
base, script, region := tag.Raw()
@@ -94,10 +155,41 @@ func Set(localeID string) bool {
tags[i] = language.MustParse(languageID)
}
_, localeIndex, _ := localeMatcher.Match(tags...)
selectedLocale, loaded := localeRegistry[localeNames[localeIndex]]
if !loaded {
return false
}
current.Store(selectedLocale)
return true
return localeNames[localeIndex], true
}
func (l *Locale) TailscaleStateText(state string) string {
switch state {
case "NoState":
return l.TailscaleInitializing
case "InUseOtherUser":
return l.TailscaleInUse
case "NeedsLogin":
return l.TailscaleNeedsLogin
case "NeedsMachineAuth":
return l.TailscaleNeedsApproval
case "Stopped":
return l.TailscaleStopped
case "Starting":
return l.TailscaleStarting
case "Running":
return l.TailscaleRunning
default:
return l.Unknown
}
}
func (l *Locale) VPNStateText(state string) string {
switch state {
case "connecting":
return l.VPNConnecting
case "auth-pending":
return l.VPNAuthentication
case "connected":
return l.VPNConnected
case "error":
return l.VPNError
default:
return l.Unknown
}
}
+12
View File
@@ -7,5 +7,17 @@ func init() {
DeprecatedMessageNoLink: "%s از sing-box %s منسوخ شده است و در sing-box %s حذف خواهد شد.",
InsecureFeatureMessage: "%s در کلاینت گرافیکی sing-box برای Windows ناامن تلقی می\u200cشود. برای استفاده، `حالت ناامن` را در `تنظیمات - هسته - حالت ناامن` فعال کنید.",
ExternalPathFeature: "دسترسی به %s (خارج از پوشهٔ کاری) در کلاینت گرافیکی sing-box برای Windows ناامن تلقی می\u200cشود. برای استفاده، `حالت ناامن` را در `تنظیمات - هسته - حالت ناامن` فعال کنید.",
TailscaleInitializing: "در حال راه\u200cاندازی",
TailscaleInUse: "در حال استفاده توسط کاربر دیگری",
TailscaleNeedsLogin: "نیاز به ورود",
TailscaleNeedsApproval: "نیاز به تأیید",
TailscaleStopped: "متوقف\u200cشده",
TailscaleStarting: "در حال شروع",
TailscaleRunning: "در حال اجرا",
VPNConnecting: "در حال اتصال",
VPNAuthentication: "نیاز به احراز هویت",
VPNConnected: "متصل",
VPNError: "خطا",
Unknown: "ناشناخته",
}
}
+12
View File
@@ -7,5 +7,17 @@ func init() {
DeprecatedMessageNoLink: "Использование %s устарело в sing-box %s, и эта возможность будет удалена в sing-box %s.",
InsecureFeatureMessage: "%s считается небезопасным в графическом клиенте sing-box для Windows. Чтобы использовать эту возможность, включите `Небезопасный режим` в разделе `Настройки — Ядро — Небезопасный режим`.",
ExternalPathFeature: "Доступ к %s (за пределами рабочего каталога) считается небезопасным в графическом клиенте sing-box для Windows. Чтобы использовать эту возможность, включите `Небезопасный режим` в разделе `Настройки — Ядро — Небезопасный режим`.",
TailscaleInitializing: "Инициализация",
TailscaleInUse: "Используется другим пользователем",
TailscaleNeedsLogin: "Требуется вход",
TailscaleNeedsApproval: "Требуется подтверждение",
TailscaleStopped: "Остановлено",
TailscaleStarting: "Запуск",
TailscaleRunning: "Работает",
VPNConnecting: "Подключение",
VPNAuthentication: "Требуется аутентификация",
VPNConnected: "Подключено",
VPNError: "Ошибка",
Unknown: "Неизвестно",
}
}
+12
View File
@@ -9,5 +9,17 @@ func init() {
DeprecatedMessageNoLink: "%s 已在 sing-box %s 中被弃用,且将在 sing-box %s 中被移除。" + warningMessageForEndUsers,
InsecureFeatureMessage: "%s 在 sing-box 的 Windows 图形客户端中被视为不安全。请在 `设置 - 核心 - 不安全模式` 中启用不安全模式后使用。",
ExternalPathFeature: "访问 %s(位于工作目录之外)在 sing-box 的 Windows 图形客户端中是不安全的。请在 `设置 - 核心 - 不安全模式` 中启用不安全模式后使用。",
TailscaleInitializing: "正在初始化",
TailscaleInUse: "正由其他用户使用",
TailscaleNeedsLogin: "需要登录",
TailscaleNeedsApproval: "需要批准",
TailscaleStopped: "已停止",
TailscaleStarting: "启动中",
TailscaleRunning: "运行中",
VPNConnecting: "正在连接",
VPNAuthentication: "需要认证",
VPNConnected: "已连接",
VPNError: "错误",
Unknown: "未知",
}
}
+12
View File
@@ -7,5 +7,17 @@ func init() {
DeprecatedMessageNoLink: "%s 已在 sing-box %s 中棄用,且將在 sing-box %s 中移除。",
InsecureFeatureMessage: "%s 在 sing-box 的 Windows 圖形用戶端中被視為不安全。請在 `設置 - 核心 - 不安全模式` 中啟用不安全模式後使用。",
ExternalPathFeature: "存取 %s(位於工作目錄之外)在 sing-box 的 Windows 圖形用戶端中被視為不安全。請在 `設置 - 核心 - 不安全模式` 中啟用不安全模式後使用。",
TailscaleInitializing: "正在初始化",
TailscaleInUse: "正由其他使用者使用",
TailscaleNeedsLogin: "需要登入",
TailscaleNeedsApproval: "需要核准",
TailscaleStopped: "已停止",
TailscaleStarting: "啟動中",
TailscaleRunning: "執行中",
VPNConnecting: "正在連線",
VPNAuthentication: "需要認證",
VPNConnected: "已連線",
VPNError: "錯誤",
Unknown: "未知",
}
}