diff --git a/daemon/client.go b/daemon/client.go index c585a818b..c7313b257 100644 --- a/daemon/client.go +++ b/daemon/client.go @@ -61,8 +61,8 @@ func NewRemoteClient(options RemoteClientOptions) (*grpc.ClientConn, error) { } return grpc.NewClient(target, grpc.WithTransportCredentials(transportCredentials), - grpc.WithUnaryInterceptor(NewClientAuthUnaryInterceptor(options.Secret)), - grpc.WithStreamInterceptor(NewClientAuthStreamInterceptor(options.Secret)), + grpc.WithChainUnaryInterceptor(UnaryClientLocaleInterceptor, NewClientAuthUnaryInterceptor(options.Secret)), + grpc.WithChainStreamInterceptor(StreamClientLocaleInterceptor, NewClientAuthStreamInterceptor(options.Secret)), ) } diff --git a/daemon/locale.go b/daemon/locale.go new file mode 100644 index 000000000..58e9bd8f3 --- /dev/null +++ b/daemon/locale.go @@ -0,0 +1,54 @@ +package daemon + +import ( + "context" + + "github.com/sagernet/sing-box/experimental/locale" + + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" +) + +func contextWithLocale(ctx context.Context) context.Context { + requestMetadata, loaded := metadata.FromIncomingContext(ctx) + if !loaded { + return ctx + } + for _, localeID := range requestMetadata.Get("accept-language") { + localizedContext, matched := locale.ContextWithLocale(ctx, localeID) + if matched { + return localizedContext + } + } + return ctx +} + +func UnaryLocaleInterceptor(ctx context.Context, request any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { + return handler(contextWithLocale(ctx), request) +} + +func StreamLocaleInterceptor(server any, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { + return handler(server, &localeServerStream{ + ServerStream: stream, + ctx: contextWithLocale(stream.Context()), + }) +} + +func UnaryClientLocaleInterceptor(ctx context.Context, method string, request, reply any, connection *grpc.ClientConn, invoker grpc.UnaryInvoker, options ...grpc.CallOption) error { + ctx = metadata.AppendToOutgoingContext(ctx, "accept-language", locale.FromContext(ctx).Locale) + return invoker(ctx, method, request, reply, connection, options...) +} + +func StreamClientLocaleInterceptor(ctx context.Context, description *grpc.StreamDesc, connection *grpc.ClientConn, method string, streamer grpc.Streamer, options ...grpc.CallOption) (grpc.ClientStream, error) { + ctx = metadata.AppendToOutgoingContext(ctx, "accept-language", locale.FromContext(ctx).Locale) + return streamer(ctx, description, connection, method, options...) +} + +type localeServerStream struct { + grpc.ServerStream + ctx context.Context +} + +func (s *localeServerStream) Context() context.Context { + return s.ctx +} diff --git a/daemon/server.go b/daemon/server.go index 2c6ab4f93..539a3b2ba 100644 --- a/daemon/server.go +++ b/daemon/server.go @@ -15,8 +15,8 @@ import ( func NewServer(startedService *StartedService, secret string) *grpc.Server { server := grpc.NewServer( - grpc.ChainUnaryInterceptor(newUnaryAuthInterceptor(secret)), - grpc.ChainStreamInterceptor(newStreamAuthInterceptor(secret)), + grpc.ChainUnaryInterceptor(newUnaryAuthInterceptor(secret), UnaryLocaleInterceptor), + grpc.ChainStreamInterceptor(newStreamAuthInterceptor(secret), StreamLocaleInterceptor), ) healthServer := health.NewServer() RegisterStartedServiceServer(server, startedService) diff --git a/daemon/started_service.go b/daemon/started_service.go index cad6f5606..9e9f43f89 100644 --- a/daemon/started_service.go +++ b/daemon/started_service.go @@ -17,6 +17,7 @@ import ( "github.com/sagernet/sing-box/common/urltest" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/experimental/deprecated" + "github.com/sagernet/sing-box/experimental/locale" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/protocol/group" "github.com/sagernet/sing/common" @@ -1410,6 +1411,7 @@ func (s *StartedService) SubscribeTailscaleStatus( var tags []string statuses := make(map[string]*adapter.TailscaleEndpointStatus, len(endpoints)) + selectedLocale := locale.FromContext(server.Context()) for update := range updates { if _, exists := statuses[update.tag]; !exists { tags = append(tags, update.tag) @@ -1417,7 +1419,7 @@ func (s *StartedService) SubscribeTailscaleStatus( statuses[update.tag] = update.status protoEndpoints := make([]*TailscaleEndpointStatus, 0, len(statuses)) for _, tag := range tags { - protoEndpoints = append(protoEndpoints, tailscaleEndpointStatusToProto(tag, statuses[tag])) + protoEndpoints = append(protoEndpoints, tailscaleEndpointStatusToProto(tag, statuses[tag], selectedLocale)) } sendErr := server.Send(&TailscaleStatusUpdate{ Endpoints: protoEndpoints, @@ -1429,7 +1431,7 @@ func (s *StartedService) SubscribeTailscaleStatus( return nil } -func tailscaleEndpointStatusToProto(tag string, s *adapter.TailscaleEndpointStatus) *TailscaleEndpointStatus { +func tailscaleEndpointStatusToProto(tag string, s *adapter.TailscaleEndpointStatus, selectedLocale *locale.Locale) *TailscaleEndpointStatus { userGroups := make([]*TailscaleUserGroup, len(s.UserGroups)) for i, group := range s.UserGroups { peers := make([]*TailscalePeer, len(group.Peers)) @@ -1447,6 +1449,7 @@ func tailscaleEndpointStatusToProto(tag string, s *adapter.TailscaleEndpointStat result := &TailscaleEndpointStatus{ EndpointTag: tag, BackendState: s.BackendState, + StateText: selectedLocale.TailscaleStateText(s.BackendState), AuthURL: s.AuthURL, NetworkName: s.NetworkName, MagicDNSSuffix: s.MagicDNSSuffix, @@ -1599,19 +1602,21 @@ func (s *StartedService) SubscribeOpenConnectStatus( s.serviceAccess.RUnlock() endpointManager := service.FromContext[adapter.EndpointManager](boxService.ctx) + selectedLocale := locale.FromContext(server.Context()) return subscribeEndpointStatus(server.Context(), endpointManager, C.TypeOpenConnect, "OpenConnect client", func(endpoints []adapter.OpenConnectEndpoint) error { return server.Send(&OpenConnectStatusUpdate{ Endpoints: common.Map(endpoints, func(endpoint adapter.OpenConnectEndpoint) *OpenConnectEndpointStatus { - return openConnectEndpointStatusToProto(endpoint.Tag(), endpoint.OpenConnectStatus()) + return openConnectEndpointStatusToProto(endpoint.Tag(), endpoint.OpenConnectStatus(), selectedLocale) }), }) }) } -func openConnectEndpointStatusToProto(tag string, endpointStatus adapter.OpenConnectStatus) *OpenConnectEndpointStatus { +func openConnectEndpointStatusToProto(tag string, endpointStatus adapter.OpenConnectStatus, selectedLocale *locale.Locale) *OpenConnectEndpointStatus { result := &OpenConnectEndpointStatus{ EndpointTag: tag, State: endpointStatus.State, + StateText: selectedLocale.VPNStateText(endpointStatus.State), Error: endpointStatus.Error, TunnelInfo: openConnectTunnelInfoToProto(endpointStatus.TunnelInfo), } @@ -1696,19 +1701,21 @@ func (s *StartedService) SubscribeOpenVPNStatus( s.serviceAccess.RUnlock() endpointManager := service.FromContext[adapter.EndpointManager](boxService.ctx) + selectedLocale := locale.FromContext(server.Context()) return subscribeEndpointStatus(server.Context(), endpointManager, C.TypeOpenVPNClient, "OpenVPN client", func(endpoints []adapter.OpenVPNEndpoint) error { return server.Send(&OpenVPNStatusUpdate{ Endpoints: common.Map(endpoints, func(endpoint adapter.OpenVPNEndpoint) *OpenVPNEndpointStatus { - return openVPNEndpointStatusToProto(endpoint.Tag(), endpoint.OpenVPNStatus()) + return openVPNEndpointStatusToProto(endpoint.Tag(), endpoint.OpenVPNStatus(), selectedLocale) }), }) }) } -func openVPNEndpointStatusToProto(tag string, endpointStatus adapter.OpenVPNStatus) *OpenVPNEndpointStatus { +func openVPNEndpointStatusToProto(tag string, endpointStatus adapter.OpenVPNStatus, selectedLocale *locale.Locale) *OpenVPNEndpointStatus { result := &OpenVPNEndpointStatus{ EndpointTag: tag, State: endpointStatus.State, + StateText: selectedLocale.VPNStateText(endpointStatus.State), Error: endpointStatus.Error, TunnelInfo: openVPNTunnelInfoToProto(endpointStatus.TunnelInfo), } diff --git a/daemon/started_service.pb.go b/daemon/started_service.pb.go index 4ea5a778c..039db91fb 100644 --- a/daemon/started_service.pb.go +++ b/daemon/started_service.pb.go @@ -2222,13 +2222,14 @@ type TailscaleEndpointStatus struct { state protoimpl.MessageState `protogen:"open.v1"` EndpointTag string `protobuf:"bytes,1,opt,name=endpointTag,proto3" json:"endpointTag,omitempty"` BackendState string `protobuf:"bytes,2,opt,name=backendState,proto3" json:"backendState,omitempty"` - AuthURL string `protobuf:"bytes,3,opt,name=authURL,proto3" json:"authURL,omitempty"` - NetworkName string `protobuf:"bytes,4,opt,name=networkName,proto3" json:"networkName,omitempty"` - MagicDNSSuffix string `protobuf:"bytes,5,opt,name=magicDNSSuffix,proto3" json:"magicDNSSuffix,omitempty"` - Self *TailscalePeer `protobuf:"bytes,6,opt,name=self,proto3" json:"self,omitempty"` - UserGroups []*TailscaleUserGroup `protobuf:"bytes,7,rep,name=userGroups,proto3" json:"userGroups,omitempty"` - ExitNode *TailscalePeer `protobuf:"bytes,8,opt,name=exitNode,proto3" json:"exitNode,omitempty"` - KeyAuth bool `protobuf:"varint,9,opt,name=keyAuth,proto3" json:"keyAuth,omitempty"` + StateText string `protobuf:"bytes,3,opt,name=stateText,proto3" json:"stateText,omitempty"` + AuthURL string `protobuf:"bytes,4,opt,name=authURL,proto3" json:"authURL,omitempty"` + NetworkName string `protobuf:"bytes,5,opt,name=networkName,proto3" json:"networkName,omitempty"` + MagicDNSSuffix string `protobuf:"bytes,6,opt,name=magicDNSSuffix,proto3" json:"magicDNSSuffix,omitempty"` + Self *TailscalePeer `protobuf:"bytes,7,opt,name=self,proto3" json:"self,omitempty"` + UserGroups []*TailscaleUserGroup `protobuf:"bytes,8,rep,name=userGroups,proto3" json:"userGroups,omitempty"` + ExitNode *TailscalePeer `protobuf:"bytes,9,opt,name=exitNode,proto3" json:"exitNode,omitempty"` + KeyAuth bool `protobuf:"varint,10,opt,name=keyAuth,proto3" json:"keyAuth,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -2277,6 +2278,13 @@ func (x *TailscaleEndpointStatus) GetBackendState() string { return "" } +func (x *TailscaleEndpointStatus) GetStateText() string { + if x != nil { + return x.StateText + } + return "" +} + func (x *TailscaleEndpointStatus) GetAuthURL() string { if x != nil { return x.AuthURL @@ -4666,9 +4674,10 @@ type OpenConnectEndpointStatus struct { state protoimpl.MessageState `protogen:"open.v1"` EndpointTag string `protobuf:"bytes,1,opt,name=endpointTag,proto3" json:"endpointTag,omitempty"` State string `protobuf:"bytes,2,opt,name=state,proto3" json:"state,omitempty"` - AuthForm *OpenConnectAuthForm `protobuf:"bytes,3,opt,name=authForm,proto3" json:"authForm,omitempty"` - Error string `protobuf:"bytes,4,opt,name=error,proto3" json:"error,omitempty"` - TunnelInfo *OpenConnectTunnelInfo `protobuf:"bytes,5,opt,name=tunnelInfo,proto3" json:"tunnelInfo,omitempty"` + StateText string `protobuf:"bytes,3,opt,name=stateText,proto3" json:"stateText,omitempty"` + AuthForm *OpenConnectAuthForm `protobuf:"bytes,4,opt,name=authForm,proto3" json:"authForm,omitempty"` + Error string `protobuf:"bytes,5,opt,name=error,proto3" json:"error,omitempty"` + TunnelInfo *OpenConnectTunnelInfo `protobuf:"bytes,6,opt,name=tunnelInfo,proto3" json:"tunnelInfo,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -4717,6 +4726,13 @@ func (x *OpenConnectEndpointStatus) GetState() string { return "" } +func (x *OpenConnectEndpointStatus) GetStateText() string { + if x != nil { + return x.StateText + } + return "" +} + func (x *OpenConnectEndpointStatus) GetAuthForm() *OpenConnectAuthForm { if x != nil { return x.AuthForm @@ -5218,9 +5234,10 @@ type OpenVPNEndpointStatus struct { state protoimpl.MessageState `protogen:"open.v1"` EndpointTag string `protobuf:"bytes,1,opt,name=endpointTag,proto3" json:"endpointTag,omitempty"` State string `protobuf:"bytes,2,opt,name=state,proto3" json:"state,omitempty"` - Challenge *OpenVPNChallenge `protobuf:"bytes,3,opt,name=challenge,proto3" json:"challenge,omitempty"` - Error string `protobuf:"bytes,4,opt,name=error,proto3" json:"error,omitempty"` - TunnelInfo *OpenVPNTunnelInfo `protobuf:"bytes,5,opt,name=tunnelInfo,proto3" json:"tunnelInfo,omitempty"` + StateText string `protobuf:"bytes,3,opt,name=stateText,proto3" json:"stateText,omitempty"` + Challenge *OpenVPNChallenge `protobuf:"bytes,4,opt,name=challenge,proto3" json:"challenge,omitempty"` + Error string `protobuf:"bytes,5,opt,name=error,proto3" json:"error,omitempty"` + TunnelInfo *OpenVPNTunnelInfo `protobuf:"bytes,6,opt,name=tunnelInfo,proto3" json:"tunnelInfo,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -5269,6 +5286,13 @@ func (x *OpenVPNEndpointStatus) GetState() string { return "" } +func (x *OpenVPNEndpointStatus) GetStateText() string { + if x != nil { + return x.StateText + } + return "" +} + func (x *OpenVPNEndpointStatus) GetChallenge() *OpenVPNChallenge { if x != nil { return x.Challenge @@ -5844,19 +5868,21 @@ const file_daemon_started_service_proto_rawDesc = "" + "\x05error\x18\a \x01(\tR\x05error\x12*\n" + "\x10natTypeSupported\x18\b \x01(\bR\x10natTypeSupported\"V\n" + "\x15TailscaleStatusUpdate\x12=\n" + - "\tendpoints\x18\x01 \x03(\v2\x1f.daemon.TailscaleEndpointStatusR\tendpoints\"\xf7\x02\n" + + "\tendpoints\x18\x01 \x03(\v2\x1f.daemon.TailscaleEndpointStatusR\tendpoints\"\x95\x03\n" + "\x17TailscaleEndpointStatus\x12 \n" + "\vendpointTag\x18\x01 \x01(\tR\vendpointTag\x12\"\n" + - "\fbackendState\x18\x02 \x01(\tR\fbackendState\x12\x18\n" + - "\aauthURL\x18\x03 \x01(\tR\aauthURL\x12 \n" + - "\vnetworkName\x18\x04 \x01(\tR\vnetworkName\x12&\n" + - "\x0emagicDNSSuffix\x18\x05 \x01(\tR\x0emagicDNSSuffix\x12)\n" + - "\x04self\x18\x06 \x01(\v2\x15.daemon.TailscalePeerR\x04self\x12:\n" + + "\fbackendState\x18\x02 \x01(\tR\fbackendState\x12\x1c\n" + + "\tstateText\x18\x03 \x01(\tR\tstateText\x12\x18\n" + + "\aauthURL\x18\x04 \x01(\tR\aauthURL\x12 \n" + + "\vnetworkName\x18\x05 \x01(\tR\vnetworkName\x12&\n" + + "\x0emagicDNSSuffix\x18\x06 \x01(\tR\x0emagicDNSSuffix\x12)\n" + + "\x04self\x18\a \x01(\v2\x15.daemon.TailscalePeerR\x04self\x12:\n" + "\n" + - "userGroups\x18\a \x03(\v2\x1a.daemon.TailscaleUserGroupR\n" + + "userGroups\x18\b \x03(\v2\x1a.daemon.TailscaleUserGroupR\n" + "userGroups\x121\n" + - "\bexitNode\x18\b \x01(\v2\x15.daemon.TailscalePeerR\bexitNode\x12\x18\n" + - "\akeyAuth\x18\t \x01(\bR\akeyAuth\"\xbf\x01\n" + + "\bexitNode\x18\t \x01(\v2\x15.daemon.TailscalePeerR\bexitNode\x12\x18\n" + + "\akeyAuth\x18\n" + + " \x01(\bR\akeyAuth\"\xbf\x01\n" + "\x12TailscaleUserGroup\x12\x16\n" + "\x06userID\x18\x01 \x01(\x03R\x06userID\x12\x1c\n" + "\tloginName\x18\x02 \x01(\tR\tloginName\x12 \n" + @@ -6040,14 +6066,15 @@ const file_daemon_started_service_proto_rawDesc = "" + "\abackend\x18\x04 \x01(\x0e2\x12.daemon.USBBackendR\abackend\x12,\n" + "\x05state\x18\x05 \x01(\x0e2\x16.daemon.USBDeviceStateR\x05state\"Z\n" + "\x17OpenConnectStatusUpdate\x12?\n" + - "\tendpoints\x18\x01 \x03(\v2!.daemon.OpenConnectEndpointStatusR\tendpoints\"\xe1\x01\n" + + "\tendpoints\x18\x01 \x03(\v2!.daemon.OpenConnectEndpointStatusR\tendpoints\"\xff\x01\n" + "\x19OpenConnectEndpointStatus\x12 \n" + "\vendpointTag\x18\x01 \x01(\tR\vendpointTag\x12\x14\n" + - "\x05state\x18\x02 \x01(\tR\x05state\x127\n" + - "\bauthForm\x18\x03 \x01(\v2\x1b.daemon.OpenConnectAuthFormR\bauthForm\x12\x14\n" + - "\x05error\x18\x04 \x01(\tR\x05error\x12=\n" + + "\x05state\x18\x02 \x01(\tR\x05state\x12\x1c\n" + + "\tstateText\x18\x03 \x01(\tR\tstateText\x127\n" + + "\bauthForm\x18\x04 \x01(\v2\x1b.daemon.OpenConnectAuthFormR\bauthForm\x12\x14\n" + + "\x05error\x18\x05 \x01(\tR\x05error\x12=\n" + "\n" + - "tunnelInfo\x18\x05 \x01(\v2\x1d.daemon.OpenConnectTunnelInfoR\n" + + "tunnelInfo\x18\x06 \x01(\v2\x1d.daemon.OpenConnectTunnelInfoR\n" + "tunnelInfo\"\xd9\x01\n" + "\x15OpenConnectTunnelInfo\x12\x16\n" + "\x06server\x18\x01 \x01(\tR\x06server\x12\x16\n" + @@ -6086,14 +6113,15 @@ const file_daemon_started_service_proto_rawDesc = "" + "\vendpointTag\x18\x01 \x01(\tR\vendpointTag\x12\x16\n" + "\x06formID\x18\x02 \x01(\tR\x06formID\"R\n" + "\x13OpenVPNStatusUpdate\x12;\n" + - "\tendpoints\x18\x01 \x03(\v2\x1d.daemon.OpenVPNEndpointStatusR\tendpoints\"\xd8\x01\n" + + "\tendpoints\x18\x01 \x03(\v2\x1d.daemon.OpenVPNEndpointStatusR\tendpoints\"\xf6\x01\n" + "\x15OpenVPNEndpointStatus\x12 \n" + "\vendpointTag\x18\x01 \x01(\tR\vendpointTag\x12\x14\n" + - "\x05state\x18\x02 \x01(\tR\x05state\x126\n" + - "\tchallenge\x18\x03 \x01(\v2\x18.daemon.OpenVPNChallengeR\tchallenge\x12\x14\n" + - "\x05error\x18\x04 \x01(\tR\x05error\x129\n" + + "\x05state\x18\x02 \x01(\tR\x05state\x12\x1c\n" + + "\tstateText\x18\x03 \x01(\tR\tstateText\x126\n" + + "\tchallenge\x18\x04 \x01(\v2\x18.daemon.OpenVPNChallengeR\tchallenge\x12\x14\n" + + "\x05error\x18\x05 \x01(\tR\x05error\x129\n" + "\n" + - "tunnelInfo\x18\x05 \x01(\v2\x19.daemon.OpenVPNTunnelInfoR\n" + + "tunnelInfo\x18\x06 \x01(\v2\x19.daemon.OpenVPNTunnelInfoR\n" + "tunnelInfo\"\xd7\x01\n" + "\x11OpenVPNTunnelInfo\x12\x16\n" + "\x06server\x18\x01 \x01(\tR\x06server\x12\x18\n" + @@ -6284,6 +6312,7 @@ var ( (*emptypb.Empty)(nil), // 82: google.protobuf.Empty } ) + var file_daemon_started_service_proto_depIdxs = []int32{ 4, // 0: daemon.ServiceStatus.status:type_name -> daemon.ServiceStatus.Type 80, // 1: daemon.Log.messages:type_name -> daemon.Log.Message diff --git a/daemon/started_service.proto b/daemon/started_service.proto index 94da556a9..dd524275b 100644 --- a/daemon/started_service.proto +++ b/daemon/started_service.proto @@ -274,13 +274,14 @@ message TailscaleStatusUpdate { message TailscaleEndpointStatus { string endpointTag = 1; string backendState = 2; - string authURL = 3; - string networkName = 4; - string magicDNSSuffix = 5; - TailscalePeer self = 6; - repeated TailscaleUserGroup userGroups = 7; - TailscalePeer exitNode = 8; - bool keyAuth = 9; + string stateText = 3; + string authURL = 4; + string networkName = 5; + string magicDNSSuffix = 6; + TailscalePeer self = 7; + repeated TailscaleUserGroup userGroups = 8; + TailscalePeer exitNode = 9; + bool keyAuth = 10; } message TailscaleUserGroup { @@ -530,9 +531,10 @@ message OpenConnectStatusUpdate { message OpenConnectEndpointStatus { string endpointTag = 1; string state = 2; - OpenConnectAuthForm authForm = 3; - string error = 4; - OpenConnectTunnelInfo tunnelInfo = 5; + string stateText = 3; + OpenConnectAuthForm authForm = 4; + string error = 5; + OpenConnectTunnelInfo tunnelInfo = 6; } message OpenConnectTunnelInfo { @@ -587,9 +589,10 @@ message OpenVPNStatusUpdate { message OpenVPNEndpointStatus { string endpointTag = 1; string state = 2; - OpenVPNChallenge challenge = 3; - string error = 4; - OpenVPNTunnelInfo tunnelInfo = 5; + string stateText = 3; + OpenVPNChallenge challenge = 4; + string error = 5; + OpenVPNTunnelInfo tunnelInfo = 6; } message OpenVPNTunnelInfo { diff --git a/experimental/boxdd/cmd_worker.go b/experimental/boxdd/cmd_worker.go index 734b8b3b8..ce43d9fa8 100644 --- a/experimental/boxdd/cmd_worker.go +++ b/experimental/boxdd/cmd_worker.go @@ -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())}), diff --git a/experimental/boxdd/locale.go b/experimental/boxdd/locale.go deleted file mode 100644 index 7c6c2518e..000000000 --- a/experimental/boxdd/locale.go +++ /dev/null @@ -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) -} diff --git a/experimental/boxdd/server.go b/experimental/boxdd/server.go index 4f70e0e38..817d87500 100644 --- a/experimental/boxdd/server.go +++ b/experimental/boxdd/server.go @@ -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 { diff --git a/experimental/libbox/command_client.go b/experimental/libbox/command_client.go index 77adf75ee..4d7b54d4d 100644 --- a/experimental/libbox/command_client.go +++ b/experimental/libbox/command_client.go @@ -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)) diff --git a/experimental/libbox/command_client_remote.go b/experimental/libbox/command_client_remote.go index bd6689fff..e8ebceac1 100644 --- a/experimental/libbox/command_client_remote.go +++ b/experimental/libbox/command_client_remote.go @@ -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), diff --git a/experimental/libbox/command_server.go b/experimental/libbox/command_server.go index dc09fa31e..1c42d7e84 100644 --- a/experimental/libbox/command_server.go +++ b/experimental/libbox/command_server.go @@ -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) diff --git a/experimental/libbox/command_types_openconnect.go b/experimental/libbox/command_types_openconnect.go index cca27ce2c..1f6658798 100644 --- a/experimental/libbox/command_types_openconnect.go +++ b/experimental/libbox/command_types_openconnect.go @@ -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 { diff --git a/experimental/libbox/command_types_openvpn.go b/experimental/libbox/command_types_openvpn.go index 202d07c3b..1de1dc1dc 100644 --- a/experimental/libbox/command_types_openvpn.go +++ b/experimental/libbox/command_types_openvpn.go @@ -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 { diff --git a/experimental/libbox/command_types_tailscale.go b/experimental/libbox/command_types_tailscale.go index 1d8e1a834..8f6e183e3 100644 --- a/experimental/libbox/command_types_tailscale.go +++ b/experimental/libbox/command_types_tailscale.go @@ -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, diff --git a/experimental/locale/locale.go b/experimental/locale/locale.go index 122fbfd64..fdb23a02c 100644 --- a/experimental/locale/locale.go +++ b/experimental/locale/locale.go @@ -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 + } } diff --git a/experimental/locale/locale_fa.go b/experimental/locale/locale_fa.go index edd952faa..c6d2e5b3b 100644 --- a/experimental/locale/locale_fa.go +++ b/experimental/locale/locale_fa.go @@ -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: "ناشناخته", } } diff --git a/experimental/locale/locale_ru.go b/experimental/locale/locale_ru.go index e7c7f7e80..1667c6a34 100644 --- a/experimental/locale/locale_ru.go +++ b/experimental/locale/locale_ru.go @@ -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: "Неизвестно", } } diff --git a/experimental/locale/locale_zh_CN.go b/experimental/locale/locale_zh_CN.go index a63450101..15ab99a04 100644 --- a/experimental/locale/locale_zh_CN.go +++ b/experimental/locale/locale_zh_CN.go @@ -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: "未知", } } diff --git a/experimental/locale/locale_zh_TW.go b/experimental/locale/locale_zh_TW.go index 3c5202eb6..7432fa35e 100644 --- a/experimental/locale/locale_zh_TW.go +++ b/experimental/locale/locale_zh_TW.go @@ -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: "未知", } }