mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-09-15 21:00:27 +00:00
Add Tailscale certificate API commands
This commit is contained in:
@@ -3,6 +3,7 @@ package adapter
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TailscaleEndpoint interface {
|
||||
@@ -10,6 +11,7 @@ type TailscaleEndpoint interface {
|
||||
StartTailscalePing(ctx context.Context, peerIP string, fn func(*TailscalePingResult)) error
|
||||
SetTailscaleExitNode(ctx context.Context, stableID string) error
|
||||
Logout(ctx context.Context) error
|
||||
GetTailscaleCertificate(ctx context.Context, domain string, minValidity time.Duration) (certificatePEM []byte, privateKeyPEM []byte, err error)
|
||||
SubscribeTaildropInbox(ctx context.Context, fn func(*TaildropInbox)) error
|
||||
MarkTaildropInboxRead() error
|
||||
SendTaildropFile(ctx context.Context, peerStableID string, fileName string, size int64, content io.Reader, progress func(sentBytes int64)) error
|
||||
@@ -61,6 +63,7 @@ type TailscaleEndpointStatus struct {
|
||||
WaitingFileCount int32
|
||||
ReceivingFileCount int32
|
||||
UnreadFileCount int32
|
||||
CertDomains []string
|
||||
}
|
||||
|
||||
type TailscaleUserGroup struct {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var commandAPITailscaleCertificate = &cobra.Command{
|
||||
Use: "certificate",
|
||||
Short: "Manage Tailscale HTTPS certificates",
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandAPITailscaleCertificate.PersistentFlags().StringVar(&commandAPITailscaleFlagEndpoint, "endpoint", "", commandAPITailscaleEndpointUsage)
|
||||
commandAPITailscale.AddCommand(commandAPITailscaleCertificate)
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/daemon"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
commandAPITailscaleCertificateExportFlagCertificateFile string
|
||||
commandAPITailscaleCertificateExportFlagKeyFile string
|
||||
commandAPITailscaleCertificateExportFlagMinValidity time.Duration
|
||||
)
|
||||
|
||||
var commandAPITailscaleCertificateExport = &cobra.Command{
|
||||
Use: "export <domain>",
|
||||
Short: "Export the HTTPS certificate and private key for a domain",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runAPITailscaleCertificateExport(args[0])
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandAPITailscaleCertificateExport.Flags().StringVar(&commandAPITailscaleCertificateExportFlagCertificateFile, "cert-file", "", "Output certificate file (default: <domain>.crt, \"-\" for stdout)")
|
||||
commandAPITailscaleCertificateExport.Flags().StringVar(&commandAPITailscaleCertificateExportFlagKeyFile, "key-file", "", "Output private key file (default: <domain>.key, \"-\" for stdout)")
|
||||
commandAPITailscaleCertificateExport.Flags().DurationVar(&commandAPITailscaleCertificateExportFlagMinValidity, "min-validity", 0, "Renew the certificate if it expires within this duration")
|
||||
commandAPITailscaleCertificate.AddCommand(commandAPITailscaleCertificateExport)
|
||||
}
|
||||
|
||||
func runAPITailscaleCertificateExport(domain string) error {
|
||||
clientConn, client, err := createAPIClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer clientConn.Close()
|
||||
endpoint, err := fetchTailscaleEndpoint(client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
certDomains := endpoint.GetCertDomains()
|
||||
if len(certDomains) == 0 {
|
||||
return E.New("no certificate domains, enable HTTPS in the Tailscale admin console")
|
||||
}
|
||||
if !slices.Contains(certDomains, domain) {
|
||||
return E.New("unknown certificate domain: ", domain, "\nknown domains:\n", formatTailscaleCertificateDomains(certDomains))
|
||||
}
|
||||
certificateFile := commandAPITailscaleCertificateExportFlagCertificateFile
|
||||
if certificateFile == "" {
|
||||
certificateFile = domain + ".crt"
|
||||
}
|
||||
keyFile := commandAPITailscaleCertificateExportFlagKeyFile
|
||||
if keyFile == "" {
|
||||
keyFile = domain + ".key"
|
||||
}
|
||||
writeStderrLine("fetching certificate for " + domain)
|
||||
certificate, err := client.GetTailscaleCertificate(globalCtx, &daemon.TailscaleCertificateRequest{
|
||||
EndpointTag: endpoint.GetEndpointTag(),
|
||||
Domain: domain,
|
||||
MinValiditySeconds: int64(commandAPITailscaleCertificateExportFlagMinValidity / time.Second),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if certificateFile == "-" && keyFile == "-" {
|
||||
_, err = os.Stdout.Write(certificate.GetCertificatePEM())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = os.Stdout.Write(certificate.GetPrivateKeyPEM())
|
||||
return err
|
||||
}
|
||||
err = writeTailscaleCertificateFile(certificateFile, certificate.GetCertificatePEM(), 0o644)
|
||||
if err != nil {
|
||||
return E.Cause(err, "write certificate")
|
||||
}
|
||||
err = writeTailscaleCertificateFile(keyFile, certificate.GetPrivateKeyPEM(), 0o600)
|
||||
if err != nil {
|
||||
return E.Cause(err, "write private key")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeTailscaleCertificateFile(path string, content []byte, mode os.FileMode) error {
|
||||
if path == "-" {
|
||||
_, err := os.Stdout.Write(content)
|
||||
return err
|
||||
}
|
||||
err := os.WriteFile(path, content, mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
writeStderrLine("wrote " + path)
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatTailscaleCertificateDomains(domains []string) string {
|
||||
return strings.Join(common.Map(domains, func(it string) string {
|
||||
return " " + it
|
||||
}), "\n")
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var commandAPITailscaleCertificateList = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List domains that certificates can be issued for",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runAPITailscaleCertificateList()
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandAPITailscaleCertificate.AddCommand(commandAPITailscaleCertificateList)
|
||||
}
|
||||
|
||||
func runAPITailscaleCertificateList() error {
|
||||
clientConn, client, err := createAPIClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer clientConn.Close()
|
||||
endpoint, err := fetchTailscaleEndpoint(client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
table := tableWriter{
|
||||
header: []string{"DOMAIN"},
|
||||
emptyMessage: "no certificate domains, enable HTTPS in the Tailscale admin console",
|
||||
}
|
||||
for _, domain := range endpoint.GetCertDomains() {
|
||||
table.addRow(domain)
|
||||
}
|
||||
table.flush()
|
||||
return nil
|
||||
}
|
||||
@@ -1667,6 +1667,7 @@ func tailscaleEndpointStatusToProto(tag string, s *adapter.TailscaleEndpointStat
|
||||
WaitingFileCount: s.WaitingFileCount,
|
||||
ReceivingFileCount: s.ReceivingFileCount,
|
||||
UnreadFileCount: s.UnreadFileCount,
|
||||
CertDomains: s.CertDomains,
|
||||
}
|
||||
if s.Self != nil {
|
||||
result.Self = tailscalePeerToProto(s.Self)
|
||||
@@ -1777,6 +1778,33 @@ func (s *StartedService) TailscaleLogout(ctx context.Context, request *Tailscale
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (s *StartedService) GetTailscaleCertificate(ctx context.Context, request *TailscaleCertificateRequest) (*TailscaleCertificate, error) {
|
||||
err := s.waitForStarted(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.serviceAccess.RLock()
|
||||
boxService := s.instance
|
||||
s.serviceAccess.RUnlock()
|
||||
|
||||
endpoint, err := resolveTailscaleEndpoint(boxService, request.EndpointTag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tsEndpoint, loaded := endpoint.(adapter.TailscaleEndpoint)
|
||||
if !loaded {
|
||||
return nil, status.Error(codes.FailedPrecondition, "endpoint does not support tailscale")
|
||||
}
|
||||
certificatePEM, privateKeyPEM, err := tsEndpoint.GetTailscaleCertificate(ctx, request.Domain, time.Duration(request.MinValiditySeconds)*time.Second)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &TailscaleCertificate{
|
||||
CertificatePEM: certificatePEM,
|
||||
PrivateKeyPEM: privateKeyPEM,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *StartedService) SubscribeOpenConnectStatus(
|
||||
_ *emptypb.Empty,
|
||||
server grpc.ServerStreamingServer[OpenConnectStatusUpdate],
|
||||
|
||||
+529
-396
File diff suppressed because it is too large
Load Diff
@@ -35,6 +35,7 @@ service StartedService {
|
||||
rpc StartTailscalePing(TailscalePingRequest) returns (stream TailscalePingResponse) {}
|
||||
rpc SetTailscaleExitNode(SetTailscaleExitNodeRequest) returns (google.protobuf.Empty) {}
|
||||
rpc TailscaleLogout(TailscaleLogoutRequest) returns (google.protobuf.Empty) {}
|
||||
rpc GetTailscaleCertificate(TailscaleCertificateRequest) returns (TailscaleCertificate) {}
|
||||
rpc StartTailscaleSSHSession(stream TailscaleSSHClientMessage) returns (stream TailscaleSSHServerMessage) {}
|
||||
rpc SubscribeTaildropInbox(SubscribeTaildropInboxRequest) returns (stream TaildropInbox) {}
|
||||
rpc MarkTaildropInboxRead(MarkTaildropInboxReadRequest) returns (google.protobuf.Empty) {}
|
||||
@@ -293,6 +294,7 @@ message TailscaleEndpointStatus {
|
||||
int32 waitingFileCount = 12;
|
||||
int32 receivingFileCount = 13;
|
||||
int32 unreadFileCount = 14;
|
||||
repeated string certDomains = 15;
|
||||
}
|
||||
|
||||
message TailscaleUserGroup {
|
||||
@@ -347,6 +349,17 @@ message TailscaleLogoutRequest {
|
||||
string endpointTag = 1;
|
||||
}
|
||||
|
||||
message TailscaleCertificateRequest {
|
||||
string endpointTag = 1;
|
||||
string domain = 2;
|
||||
int64 minValiditySeconds = 3;
|
||||
}
|
||||
|
||||
message TailscaleCertificate {
|
||||
bytes certificatePEM = 1;
|
||||
bytes privateKeyPEM = 2;
|
||||
}
|
||||
|
||||
message TailscaleSSHClientMessage {
|
||||
oneof message {
|
||||
TailscaleSSHStart start = 1;
|
||||
|
||||
@@ -40,6 +40,7 @@ const (
|
||||
StartedService_StartTailscalePing_FullMethodName = "/daemon.StartedService/StartTailscalePing"
|
||||
StartedService_SetTailscaleExitNode_FullMethodName = "/daemon.StartedService/SetTailscaleExitNode"
|
||||
StartedService_TailscaleLogout_FullMethodName = "/daemon.StartedService/TailscaleLogout"
|
||||
StartedService_GetTailscaleCertificate_FullMethodName = "/daemon.StartedService/GetTailscaleCertificate"
|
||||
StartedService_StartTailscaleSSHSession_FullMethodName = "/daemon.StartedService/StartTailscaleSSHSession"
|
||||
StartedService_SubscribeTaildropInbox_FullMethodName = "/daemon.StartedService/SubscribeTaildropInbox"
|
||||
StartedService_MarkTaildropInboxRead_FullMethodName = "/daemon.StartedService/MarkTaildropInboxRead"
|
||||
@@ -87,6 +88,7 @@ type StartedServiceClient interface {
|
||||
StartTailscalePing(ctx context.Context, in *TailscalePingRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[TailscalePingResponse], error)
|
||||
SetTailscaleExitNode(ctx context.Context, in *SetTailscaleExitNodeRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
TailscaleLogout(ctx context.Context, in *TailscaleLogoutRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
GetTailscaleCertificate(ctx context.Context, in *TailscaleCertificateRequest, opts ...grpc.CallOption) (*TailscaleCertificate, error)
|
||||
StartTailscaleSSHSession(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[TailscaleSSHClientMessage, TailscaleSSHServerMessage], error)
|
||||
SubscribeTaildropInbox(ctx context.Context, in *SubscribeTaildropInboxRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[TaildropInbox], error)
|
||||
MarkTaildropInboxRead(ctx context.Context, in *MarkTaildropInboxReadRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
@@ -462,6 +464,16 @@ func (c *startedServiceClient) TailscaleLogout(ctx context.Context, in *Tailscal
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *startedServiceClient) GetTailscaleCertificate(ctx context.Context, in *TailscaleCertificateRequest, opts ...grpc.CallOption) (*TailscaleCertificate, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(TailscaleCertificate)
|
||||
err := c.cc.Invoke(ctx, StartedService_GetTailscaleCertificate_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *startedServiceClient) StartTailscaleSSHSession(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[TailscaleSSHClientMessage, TailscaleSSHServerMessage], error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[11], StartedService_StartTailscaleSSHSession_FullMethodName, cOpts...)
|
||||
@@ -714,6 +726,7 @@ type StartedServiceServer interface {
|
||||
StartTailscalePing(*TailscalePingRequest, grpc.ServerStreamingServer[TailscalePingResponse]) error
|
||||
SetTailscaleExitNode(context.Context, *SetTailscaleExitNodeRequest) (*emptypb.Empty, error)
|
||||
TailscaleLogout(context.Context, *TailscaleLogoutRequest) (*emptypb.Empty, error)
|
||||
GetTailscaleCertificate(context.Context, *TailscaleCertificateRequest) (*TailscaleCertificate, error)
|
||||
StartTailscaleSSHSession(grpc.BidiStreamingServer[TailscaleSSHClientMessage, TailscaleSSHServerMessage]) error
|
||||
SubscribeTaildropInbox(*SubscribeTaildropInboxRequest, grpc.ServerStreamingServer[TaildropInbox]) error
|
||||
MarkTaildropInboxRead(context.Context, *MarkTaildropInboxReadRequest) (*emptypb.Empty, error)
|
||||
@@ -840,6 +853,10 @@ func (UnimplementedStartedServiceServer) TailscaleLogout(context.Context, *Tails
|
||||
return nil, status.Error(codes.Unimplemented, "method TailscaleLogout not implemented")
|
||||
}
|
||||
|
||||
func (UnimplementedStartedServiceServer) GetTailscaleCertificate(context.Context, *TailscaleCertificateRequest) (*TailscaleCertificate, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GetTailscaleCertificate not implemented")
|
||||
}
|
||||
|
||||
func (UnimplementedStartedServiceServer) StartTailscaleSSHSession(grpc.BidiStreamingServer[TailscaleSSHClientMessage, TailscaleSSHServerMessage]) error {
|
||||
return status.Error(codes.Unimplemented, "method StartTailscaleSSHSession not implemented")
|
||||
}
|
||||
@@ -1297,6 +1314,24 @@ func _StartedService_TailscaleLogout_Handler(srv interface{}, ctx context.Contex
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _StartedService_GetTailscaleCertificate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(TailscaleCertificateRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(StartedServiceServer).GetTailscaleCertificate(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: StartedService_GetTailscaleCertificate_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(StartedServiceServer).GetTailscaleCertificate(ctx, req.(*TailscaleCertificateRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _StartedService_StartTailscaleSSHSession_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||
return srv.(StartedServiceServer).StartTailscaleSSHSession(&grpc.GenericServerStream[TailscaleSSHClientMessage, TailscaleSSHServerMessage]{ServerStream: stream})
|
||||
}
|
||||
@@ -1573,6 +1608,10 @@ var StartedService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "TailscaleLogout",
|
||||
Handler: _StartedService_TailscaleLogout_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetTailscaleCertificate",
|
||||
Handler: _StartedService_GetTailscaleCertificate_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "MarkTaildropInboxRead",
|
||||
Handler: _StartedService_MarkTaildropInboxRead_Handler,
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
//go:build with_gvisor
|
||||
|
||||
package tailscale
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
func (t *Endpoint) GetTailscaleCertificate(ctx context.Context, domain string, minValidity time.Duration) ([]byte, []byte, error) {
|
||||
if !t.started.Load() {
|
||||
return nil, nil, E.New("Tailscale is not ready yet")
|
||||
}
|
||||
certificatePEM, privateKeyPEM, err := common.Must1(t.server.LocalClient()).CertPairWithValidity(ctx, domain, minValidity)
|
||||
if err != nil {
|
||||
return nil, nil, E.Cause(err, "tailscale certificate")
|
||||
}
|
||||
return certificatePEM, privateKeyPEM, nil
|
||||
}
|
||||
@@ -42,6 +42,7 @@ func (t *Endpoint) SubscribeTailscaleStatus(ctx context.Context, fn func(*adapte
|
||||
result.WaitingFileCount = t.taildrop.waitingFileCount()
|
||||
result.ReceivingFileCount = t.taildrop.receivingFileCount()
|
||||
result.UnreadFileCount = t.taildrop.unreadFileCount()
|
||||
result.CertDomains = t.server.CertDomains()
|
||||
if len(taildropTargets) > 0 {
|
||||
for _, group := range result.UserGroups {
|
||||
for _, peer := range group.Peers {
|
||||
|
||||
Reference in New Issue
Block a user