From 236e22b3c4b05c524021fd35e4b007bef5b19d55 Mon Sep 17 00:00:00 2001 From: Kevin Fox Date: Sat, 16 Nov 2024 06:03:32 -0800 Subject: [PATCH] Initial checkin Signed-off-by: Kevin Fox --- Dockerfile | 11 + cmd/main.go | 724 ++++++++++++++++++ go.mod | 50 ++ go.sum | 376 +++++++++ pkg/peertracker/conn.go | 15 + pkg/peertracker/credentials.go | 75 ++ pkg/peertracker/errors.go | 9 + pkg/peertracker/info.go | 27 + pkg/peertracker/listener.go | 97 +++ pkg/peertracker/listener_posix.go | 43 ++ pkg/peertracker/listener_test.go | 152 ++++ pkg/peertracker/listener_windows.go | 45 ++ pkg/peertracker/npipe.go | 9 + pkg/peertracker/npipe_fallback.go | 11 + pkg/peertracker/npipe_windows.go | 76 ++ pkg/peertracker/peertracker.go | 39 + pkg/peertracker/peertracker_posix_test.go | 14 + pkg/peertracker/peertracker_test.go | 238 ++++++ .../peertracker_test_child_posix.go | 67 ++ .../peertracker_test_child_windows.go | 64 ++ pkg/peertracker/peertracker_test_posix.go | 60 ++ pkg/peertracker/peertracker_test_windows.go | 66 ++ pkg/peertracker/peertracker_windows_test.go | 11 + pkg/peertracker/trace.go | 54 ++ pkg/peertracker/trace_linux.go | 43 ++ pkg/peertracker/tracker_bsd.go | 220 ++++++ pkg/peertracker/tracker_fallback.go | 11 + pkg/peertracker/tracker_linux.go | 208 +++++ pkg/peertracker/tracker_linux_test.go | 41 + pkg/peertracker/tracker_windows.go | 212 +++++ pkg/peertracker/tracker_windows_test.go | 228 ++++++ pkg/peertracker/uds.go | 32 + pkg/peertracker/uds_bsd.go | 20 + pkg/peertracker/uds_fallback.go | 7 + pkg/peertracker/uds_linux.go | 22 + 35 files changed, 3377 insertions(+) create mode 100644 Dockerfile create mode 100644 cmd/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 pkg/peertracker/conn.go create mode 100644 pkg/peertracker/credentials.go create mode 100644 pkg/peertracker/errors.go create mode 100644 pkg/peertracker/info.go create mode 100644 pkg/peertracker/listener.go create mode 100644 pkg/peertracker/listener_posix.go create mode 100644 pkg/peertracker/listener_test.go create mode 100644 pkg/peertracker/listener_windows.go create mode 100644 pkg/peertracker/npipe.go create mode 100644 pkg/peertracker/npipe_fallback.go create mode 100644 pkg/peertracker/npipe_windows.go create mode 100644 pkg/peertracker/peertracker.go create mode 100644 pkg/peertracker/peertracker_posix_test.go create mode 100644 pkg/peertracker/peertracker_test.go create mode 100644 pkg/peertracker/peertracker_test_child_posix.go create mode 100644 pkg/peertracker/peertracker_test_child_windows.go create mode 100644 pkg/peertracker/peertracker_test_posix.go create mode 100644 pkg/peertracker/peertracker_test_windows.go create mode 100644 pkg/peertracker/peertracker_windows_test.go create mode 100644 pkg/peertracker/trace.go create mode 100644 pkg/peertracker/trace_linux.go create mode 100644 pkg/peertracker/tracker_bsd.go create mode 100644 pkg/peertracker/tracker_fallback.go create mode 100644 pkg/peertracker/tracker_linux.go create mode 100644 pkg/peertracker/tracker_linux_test.go create mode 100644 pkg/peertracker/tracker_windows.go create mode 100644 pkg/peertracker/tracker_windows_test.go create mode 100644 pkg/peertracker/uds.go create mode 100644 pkg/peertracker/uds_bsd.go create mode 100644 pkg/peertracker/uds_fallback.go create mode 100644 pkg/peertracker/uds_linux.go diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..94eeaff --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM docker.io/library/golang:1.23.2 as build + +COPY * /build/ +WORKDIR /build + +RUN \ + GOPROXY=direct CGO_ENABLED=0 go build . + +FROM gcr.io/distroless/static-debian12 +COPY --from=build /build/spire-ha-agent /usr/bin/spire-ha-agent +ENTRYPOINT ["/usr/bin/spire-ha-agent"] diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..3acd4ce --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,724 @@ +package main + +import ( + "bytes" + "context" + "flag" + "log" + "net" + "time" + "errors" + "encoding/json" + "fmt" + "crypto/x509" + "reflect" + "sync" + "strconv" + "os" + + //FIXME Local tweaked copy for now. Need to break this out on its own. + "github.com/spiffe/spire-ha-agent/peertracker" + jose "github.com/go-jose/go-jose/v4" + + "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + metadata "google.golang.org/grpc/metadata" + "google.golang.org/grpc/credentials/insecure" + workload "github.com/spiffe/go-spiffe/v2/proto/spiffe/workload" + agentdelegated "github.com/spiffe/spire-api-sdk/proto/spire/api/agent/delegatedidentity/v1" + agentdebug "github.com/spiffe/spire-api-sdk/proto/spire/api/agent/debug/v1" + types "github.com/spiffe/spire-api-sdk/proto/spire/api/types" + "github.com/spiffe/spire/pkg/common/api/middleware" + //"github.com/spiffe/spire/pkg/common/peertracker" + "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" + "github.com/spiffe/go-spiffe/v2/spiffeid" +) + +type callerPIDKey struct{} + +type x509BundleUpdated struct{ + id int + bundle *x509bundle.Set +} + +type jwtBundleUpdated struct{ + id int + bundle map[string]jose.JSONWebKeySet +} + +type server struct { + x509BundleUpdate chan x509BundleUpdated + jwtBundleUpdate chan jwtBundleUpdated + rawBundles map[string][]byte + rawJwtBundles map[string][]byte + bundleChan chan struct{} + jwtBundleChan chan struct{} + bundleLock sync.RWMutex + clients [2]clientSet + workload.UnimplementedSpiffeWorkloadAPIServer + multi bool +} + +type clientSet struct { + clientOK bool + debugClient agentdebug.DebugClient + delegatedClient agentdelegated.DelegatedIdentityClient + bundle *x509bundle.Set + jwtBundles map[string]jose.JSONWebKeySet +} + +func ConcatRawCertsFromCerts(certs []*x509.Certificate) []byte { + var rawCerts []byte + for _, cert := range certs { + rawCerts = append(rawCerts, cert.Raw...) + } + return rawCerts +} + +func get_x509cert(dctx context.Context, pid int, delegatedClient agentdelegated.DelegatedIdentityClient, notify *chan*agentdelegated.SubscribeToX509SVIDsResponse) { + for { + // uctx, cancel := context.WithCancel(metadata.NewOutgoingContext(dctx, metadata.Pairs("workload.spiffe.io", "true"))) + uctx := metadata.NewOutgoingContext(dctx, metadata.Pairs("workload.spiffe.io", "true")) + //defer cancel() + upstream, err := delegatedClient.SubscribeToX509SVIDs(uctx, &agentdelegated.SubscribeToX509SVIDsRequest{Pid: int32(pid)}) + if err != nil { + log.Printf("x509cert %d upstream error: %v", pid, err) + time.Sleep(5 * time.Second) + continue + } + for { + resp, err := upstream.Recv() + if err != nil { + if errors.Is(dctx.Err(), context.Canceled) { + log.Printf("x509cert %d canceled", pid) + return + } + log.Printf("x509cert %d upstream error2: %v", pid, err) + time.Sleep(5 * time.Second) + break + } + log.Printf("x509cert %d upstream got cert", pid) + //FIXME Can squash duplicates that happen during reconnect... + *notify <- resp + } + } +} + +func delegatedResponseToWorkloadResponse (resp *agentdelegated.SubscribeToX509SVIDsResponse, rawBundles *map[string][]byte) *workload.X509SVIDResponse { + res := &workload.X509SVIDResponse{ + FederatedBundles: make(map[string][]byte), + //FIXME Crl? + } + + for _, svid := range resp.GetX509Svids() { + var x509Svid *types.X509SVID = svid.GetX509Svid() + var key []byte = svid.GetX509SvidKey() + log.Printf("Got %s\n", x509Svid) + id := x509Svid.GetId() + res.Svids = append(res.Svids, &workload.X509SVID{ + SpiffeId: fmt.Sprintf("spiffe://%s%s", id.GetTrustDomain(), id.GetPath()), + X509Svid: bytes.Join(x509Svid.GetCertChain(), []byte("")), + X509SvidKey: key, + Bundle: (*rawBundles)[id.GetTrustDomain()], + Hint: x509Svid.GetHint(), + }) + //FIXME dont forget Federated bundles + } + return res +} + +// Fetch X.509-SVIDs for all SPIFFE identities the workload is entitled to, +// as well as related information like trust bundles and CRLs. As this +// information changes, subsequent messages will be streamed from the +// server. +func (s *server) FetchX509SVID(req *workload.X509SVIDRequest, downstream workload.SpiffeWorkloadAPI_FetchX509SVIDServer) error { + var bundleChan chan struct{} + dctx := downstream.Context() + pid := dctx.Value(callerPIDKey{}).(int) + log.Printf("x509fetch calling pid: %d", pid) + + var chan1 chan*agentdelegated.SubscribeToX509SVIDsResponse = make(chan*agentdelegated.SubscribeToX509SVIDsResponse) + var chan2 chan*agentdelegated.SubscribeToX509SVIDsResponse = make(chan*agentdelegated.SubscribeToX509SVIDsResponse) + go get_x509cert(dctx, pid, s.clients[0].delegatedClient, &chan1) + if s.multi { + go get_x509cert(dctx, pid, s.clients[1].delegatedClient, &chan2) + } + + var resp *agentdelegated.SubscribeToX509SVIDsResponse + select { + case <-dctx.Done(): + log.Printf("x509fetch client disconncted\n") + return nil + case resp = <-chan1: + log.Printf("x509fetch got new certs\n") + case resp = <-chan2: + log.Printf("x509fetch got new certs2\n") + } + + s.bundleLock.RLock() + pb := delegatedResponseToWorkloadResponse(resp, &s.rawBundles) + bundleChan = s.bundleChan + s.bundleLock.RUnlock() + log.Printf("Got %s\n", resp.GetFederatesWith()) + + for { + log.Printf("Sending back cert/bundle update\n") + if err := downstream.Send(pb); err != nil { + return err + } + for { + diff := false + + select { + case <-dctx.Done(): + log.Printf("x509fetch client disconncted\n") + return nil + case resp = <-chan1: + log.Printf("x509fetch got new certs\n") + pb = delegatedResponseToWorkloadResponse(resp, &s.rawBundles) + diff = true + break + case resp = <-chan2: + log.Printf("x509fetch got new certs2\n") + pb = delegatedResponseToWorkloadResponse(resp, &s.rawBundles) + diff = true + break + case <-bundleChan: + log.Printf("x509fetch ca refreshed\n") + s.bundleLock.RLock() + for _, svid := range pb.Svids { + td, err := spiffeid.TrustDomainFromString(svid.GetSpiffeId()) + if err != nil { + log.Fatal("Aaahhhh") + } + if !bytes.Equal(svid.GetBundle(), s.rawBundles[td.Name()]) { + diff = true + svid.Bundle = s.rawBundles[td.Name()] + } + } + //FIXME also check on federated bundles + log.Printf("diff: %t", diff) + log.Printf("tds total: %d", len(s.rawBundles)) + bundleChan = s.bundleChan + s.bundleLock.RUnlock() + } + if diff { + break + } + } + } + + log.Printf("FetchX509SVID") + return status.Errorf(codes.Unimplemented, "method FetchX509SVID not implemented") +} + + +func delegatedResponseToWorkloadBundleResponse (resp *agentdelegated.SubscribeToX509SVIDsResponse, rawBundles *map[string][]byte) *workload.X509BundlesResponse { + var res *workload.X509BundlesResponse = &workload.X509BundlesResponse{}; + //FIXME crl? + res.Bundles = make(map[string][]byte, 0) + for _, trustDomain := range resp.GetFederatesWith() { + res.Bundles[trustDomain] = (*rawBundles)[trustDomain] + } + for _, svid := range resp.GetX509Svids() { + var x509Svid *types.X509SVID = svid.GetX509Svid() + id := x509Svid.GetId() + trustDomain := id.GetTrustDomain() + res.Bundles[trustDomain] = (*rawBundles)[trustDomain] + } + return res +} + +// Fetch trust bundles and CRLs. Useful for clients that only need to +// validate SVIDs without obtaining an SVID for themself. As this +// information changes, subsequent messages will be streamed from the +// server. +func (s *server) FetchX509Bundles(req *workload.X509BundlesRequest, downstream workload.SpiffeWorkloadAPI_FetchX509BundlesServer) error { + var bundleChan chan struct{} + dctx := downstream.Context() + pid := dctx.Value(callerPIDKey{}).(int) + log.Printf("Calling pid: %d", pid) + + var chan1 chan*agentdelegated.SubscribeToX509SVIDsResponse = make(chan*agentdelegated.SubscribeToX509SVIDsResponse) + var chan2 chan*agentdelegated.SubscribeToX509SVIDsResponse = make(chan*agentdelegated.SubscribeToX509SVIDsResponse) + go get_x509cert(dctx, pid, s.clients[0].delegatedClient, &chan1) + if s.multi { + go get_x509cert(dctx, pid, s.clients[1].delegatedClient, &chan2) + } + + var resp *agentdelegated.SubscribeToX509SVIDsResponse + select { + case <-dctx.Done(): + log.Printf("x509fetch client disconncted\n") + return nil + case resp = <-chan1: + log.Printf("x509fetch got new certs\n") + case resp = <-chan2: + log.Printf("x509fetch got new certs2\n") + } + + s.bundleLock.RLock() + bundles := delegatedResponseToWorkloadBundleResponse(resp, &s.rawBundles) + bundleChan = s.bundleChan + s.bundleLock.RUnlock() + log.Printf("Got %s\n", resp.GetFederatesWith()) + + for { + log.Printf("Sending back cert/bundle update\n") + if err := downstream.Send(bundles); err != nil { + return err + } + for { + diff := false + + select { + //FIXME squash duplicate sends. + case <-dctx.Done(): + log.Printf("x509fetch client disconncted\n") + return nil + case resp = <-chan1: + log.Printf("x509fetch got new certs\n") + s.bundleLock.RLock() + bundles = delegatedResponseToWorkloadBundleResponse(resp, &s.rawBundles) + s.bundleLock.RUnlock() + diff = true + break + case resp = <-chan2: + log.Printf("x509fetch got new certs2\n") + s.bundleLock.RLock() + bundles = delegatedResponseToWorkloadBundleResponse(resp, &s.rawBundles) + s.bundleLock.RUnlock() + diff = true + break + case <-bundleChan: + log.Printf("x509fetch ca refreshed\n") + s.bundleLock.RLock() + bundles = delegatedResponseToWorkloadBundleResponse(resp, &s.rawBundles) + bundleChan = s.bundleChan + s.bundleLock.RUnlock() + diff = true + break + } + if diff { + break + } + } + } + + log.Printf("FetchX509Bundles") + return status.Errorf(codes.Unimplemented, "method FetchX509Bundles not implemented") +} + +func get_jwt(dctx context.Context, pid int, audience []string, delegatedClient agentdelegated.DelegatedIdentityClient, notify *chan*agentdelegated.FetchJWTSVIDsResponse) { + uctx := metadata.NewOutgoingContext(dctx, metadata.Pairs("workload.spiffe.io", "true")) + resp, err := delegatedClient.FetchJWTSVIDs(uctx, &agentdelegated.FetchJWTSVIDsRequest{Audience: audience, Pid: int32(pid)}) + if err != nil { + log.Printf("jwt %d upstream error: %v", pid, err) + } + *notify <- resp +} + +// Fetch JWT-SVIDs for all SPIFFE identities the workload is entitled to, +// for the requested audience. If an optional SPIFFE ID is requested, only +// the JWT-SVID for that SPIFFE ID is returned. +func (s *server) FetchJWTSVID(dctx context.Context, downstream *workload.JWTSVIDRequest) (*workload.JWTSVIDResponse, error) { + log.Printf("FetchJWTSVID") + pid := dctx.Value(callerPIDKey{}).(int) + + var count int = 0 + var resp *agentdelegated.FetchJWTSVIDsResponse + var chan1 chan*agentdelegated.FetchJWTSVIDsResponse = make(chan*agentdelegated.FetchJWTSVIDsResponse) +//FIXME lots of different ways of doing this. Just do the simple thing for now. + go get_jwt(dctx, pid, downstream.Audience, s.clients[0].delegatedClient, &chan1) + if s.multi { + go get_jwt(dctx, pid, downstream.Audience, s.clients[1].delegatedClient, &chan1) + } + +//FIXME in the request, string spiffe_id = 2; +//reponse hint? no delegated api equiv. + + for { + select { + case <-dctx.Done(): + log.Printf("jwt client disconncted\n") + return nil, nil + case resp = <-chan1: + log.Printf("jwt got new token\n") + count++ + break + } + if resp != nil { + break + } + if count >= 2 { + return nil, status.Errorf(codes.Unavailable, "failed to talk to either agent") + } + } + + svids := make([]*workload.JWTSVID, 0) + for _, s := range resp.Svids { + id := fmt.Sprintf("spiffe://%s%s", resp.Svids[0].Id.TrustDomain, resp.Svids[0].Id.Path) + e := &workload.JWTSVID{SpiffeId: id, Svid: s.Token} + //FIXME how might we return a hint? Not returned from the delegated api + svids = append(svids, e) + } + res := &workload.JWTSVIDResponse{Svids: svids} + log.Printf("wark3 %s\n", res) + return res, nil +} + +// Fetches the JWT bundles, formatted as JWKS documents, keyed by the +// SPIFFE ID of the trust domain. As this information changes, subsequent +// messages will be streamed from the server. +func (s *server) FetchJWTBundles(req *workload.JWTBundlesRequest, downstream workload.SpiffeWorkloadAPI_FetchJWTBundlesServer) error { + var res *workload.JWTBundlesResponse = &workload.JWTBundlesResponse{}; + ctx := downstream.Context() + pid := ctx.Value(callerPIDKey{}).(int) + log.Printf("Calling pid: %d", pid) + log.Printf("FetchJWTBundles") +//FIXME double check. does this scope down to the caller somehow? + //ls.rawJwtBundles + + var bundleChan chan struct{} + dctx := downstream.Context() + pid = dctx.Value(callerPIDKey{}).(int) + log.Printf("Calling pid: %d", pid) + + s.bundleLock.RLock() + bundleChan = s.jwtBundleChan + bundles := s.rawJwtBundles + s.bundleLock.RUnlock() + + for { + log.Printf("Sending back jwt bundle update\n") + res.Bundles = bundles + if err := downstream.Send(res); err != nil { + return err + } + for { + diff := false + + select { + //FIXME squash duplicate sends. + case <-dctx.Done(): + log.Printf("jwtfetch client disconncted\n") + return nil + case <-bundleChan: + log.Printf("jwtfetch ca refreshed\n") + s.bundleLock.RLock() + bundles = s.rawJwtBundles + bundleChan = s.jwtBundleChan + s.bundleLock.RUnlock() + diff = true + break + } + if diff { + break + } + } + } + + log.Printf("FetchJWTBundles") + return status.Errorf(codes.Unimplemented, "method FetchJWTBundles not implemented") +} + +// Validates a JWT-SVID against the requested audience. Returns the SPIFFE +// ID of the JWT-SVID and JWT claims. +func (s *server) ValidateJWTSVID(ctx context.Context, downstream *workload.ValidateJWTSVIDRequest) (*workload.ValidateJWTSVIDResponse, error) { + //ctx = downstream.Context() + pid := ctx.Value(callerPIDKey{}).(int) + log.Printf("Calling pid: %d", pid) + log.Printf("ValidateJWTSVID") + return nil, status.Errorf(codes.Unimplemented, "method ValidateJWTSVID not implemented") +} + +func addWatcherPID(ctx context.Context, _ string, _ any) (context.Context, error) { + watcher, ok := peertracker.WatcherFromContext(ctx) + if ok { + pid := int(watcher.PID()) + ctx = context.WithValue(ctx, callerPIDKey{}, pid) + } + return ctx, nil +} + +func parseX509Bundles(bun map[string][]byte) (*x509bundle.Set, error) { + bundles := []*x509bundle.Bundle{} + + for tdID, b := range bun { + td, err := spiffeid.TrustDomainFromString(tdID) + if err != nil { + return nil, err + } + b, err := x509bundle.ParseRaw(td, b) + if err != nil { + return nil, err + } + bundles = append(bundles, b) + } + + return x509bundle.NewSet(bundles...), nil +} + +func setupClient(ls *server, clientName string, id int, mainSockName string, adminSocketName string, cs *clientSet) { + //Raw client code: https://github.com/spiffe/go-spiffe/blob/main/v2/workloadapi/client.go#L255 + var dialOptions []grpc.DialOption +// var conn *grpc.ClientConn + + dialOptions = append(dialOptions, grpc.WithTransportCredentials(insecure.NewCredentials())) + dconn, err := grpc.DialContext(context.Background(), adminSocketName, dialOptions...) + if err != nil { + log.Fatalf("Failed to dial context: %v", err) + } + + ls.x509BundleUpdate = make(chan x509BundleUpdated) + ls.jwtBundleUpdate = make(chan jwtBundleUpdated) + cs.delegatedClient = agentdelegated.NewDelegatedIdentityClient(dconn) + cs.debugClient = agentdebug.NewDebugClient(dconn) + go func() { + var lt int64 = 0 + var count = 0 + cs.clientOK = false + for { + resp, err := cs.debugClient.GetInfo(context.TODO(), nil) + if err != nil { + log.Printf("Failed getinfo: %v", err) + count++ + cs.clientOK = false + } else if lt != resp.LastSyncSuccess { + count = 0 + lt = resp.LastSyncSuccess + cs.clientOK = true + } else { + count++ + } + if count >= 3 { + cs.clientOK = false + } + if resp != nil { + log.Printf("%s: %d %d %t", clientName, resp.LastSyncSuccess, count, cs.clientOK) + } + time.Sleep(5 * time.Second) + } + }() + + go func() { + for { + ctx, cancel := context.WithCancel(metadata.NewOutgoingContext(context.Background(), metadata.Pairs("workload.spiffe.io", "true"))) + defer cancel() + stream, err := cs.delegatedClient.SubscribeToX509Bundles(ctx, &agentdelegated.SubscribeToX509BundlesRequest{}) + if err != nil { + log.Printf("Failed to build x509 client: %v", err) + time.Sleep(5 * time.Second) + continue + } + for { + resp, err := stream.Recv() + if err != nil { + log.Printf("Failed to get x509 bundles: %v", err) + time.Sleep(5 * time.Second) + break + } + bundles, err := parseX509Bundles(resp.GetCaCertificates()) + if err != nil { + log.Fatalf("Failed to parse x509 bundles: %v", err) + } + for _, bundle := range bundles.Bundles() { + log.Printf("x509 Bundle: %s %d", bundle.TrustDomain(), len(bundle.X509Authorities())) + } + log.Printf("Pushing x509 bundle") + ls.x509BundleUpdate <- x509BundleUpdated{id, bundles} + } + } + }() + + go func() { + for { + ctx, cancel := context.WithCancel(metadata.NewOutgoingContext(context.Background(), metadata.Pairs("workload.spiffe.io", "true"))) + defer cancel() + stream, err := cs.delegatedClient.SubscribeToJWTBundles(ctx, &agentdelegated.SubscribeToJWTBundlesRequest{}) + if err != nil { + log.Printf("Failed to build jwt client: %v", err) + time.Sleep(5 * time.Second) + continue + } + for { + resp, err := stream.Recv() + if err != nil { + log.Printf("Failed to get jwt bundles: %v", err) + time.Sleep(5 * time.Second) + break + } + bundles := resp.GetBundles() + jwksBundles := make(map[string]jose.JSONWebKeySet) + for td, bundle := range bundles { + log.Printf("jwt Bundle: %s %s", td, string(bundle)) + //log.Printf("jwt Bundle: %s %d", td, len(bundle)) + jwks := new(jose.JSONWebKeySet) + if err := json.NewDecoder(bytes.NewReader(bundle)).Decode(jwks); err != nil { + log.Printf("failed to decode key set: %v", err) + //FIXME whats the right thing to do here? + continue + } + jwksBundles[td] = *jwks + } + log.Printf("Pushing jwt bundle") + ls.jwtBundleUpdate <- jwtBundleUpdated{id, jwksBundles} + } + } + }() +} + +func main() { + var wg sync.WaitGroup + var jwtWg sync.WaitGroup + initBundle := true + jwtInitBundle := true + wg.Add(1) + jwtWg.Add(1) + flag.Parse() + lf := &peertracker.ListenerFactory{} + var lis *peertracker.Listener + var err error + //FIXME need to consider a config file rather then env vars. + if os.Getenv("SPIRE_HA_AGENT_VSOCK") == "enabled" { + port := os.Getenv("SPIRE_HA_AGENT_PORT") + if port == "" { + port = "997" + } + iport, err := strconv.Atoi(port) + if err != nil { + log.Fatalf("failed to parse port: %v", err) + } + lis, err = lf.ListenVSock(uint32(iport)) + } else { + lis, err = lf.ListenUnix("unix", &net.UnixAddr{Name: "/var/run/spire/agent/sockets/main/public/api.sock", Net: "unix"}) + } + if err != nil { + log.Fatalf("failed to listen: %v", err) + } + var ls = &server{ + multi: os.Getenv("SPIRE_HA_AGENT_SINGLE") != "enabled", + } + + unaryInterceptor, streamInterceptor := middleware.Interceptors(middleware.Chain( + middleware.Preprocess(addWatcherPID), + )) + s := grpc.NewServer( + grpc.Creds(peertracker.NewCredentials()), + grpc.UnaryInterceptor(unaryInterceptor), + grpc.StreamInterceptor(streamInterceptor), + ) + + setupClient(ls, "clientA", 0, "unix:///var/run/spire/agent/sockets/a/public/api.sock", "unix:///var/run/spire/agent/sockets/a/private/admin.sock", &ls.clients[0]) + setupClient(ls, "clientB", 1, "unix:///var/run/spire/agent/sockets/b/public/api.sock", "unix:///var/run/spire/agent/sockets/b/private/admin.sock", &ls.clients[1]) + + go func() { + for { + time.Sleep(5 * time.Second) + log.Printf("Clients: %t %t\n", ls.clients[0].clientOK, ls.clients[1].clientOK) + //FIXME.... maybe want jwt to happen as soon as it detects not ok. Maybe want to wait on other client on fail to see if they both fail. + } + }() + + go func() { + log.Printf("Listening for x509 bundle updates\n") + for u := range ls.x509BundleUpdate { + log.Printf("Got update for %d\n", u.id) + ls.clients[u.id].bundle = u.bundle + if ls.clients[0].bundle != nil && ls.clients[1].bundle != nil { + log.Printf("We got two bundles\n") + var rawBundles map[string][]byte = make(map[string][]byte) + for _, bundle := range ls.clients[0].bundle.Bundles() { + td := bundle.TrustDomain() + if tdb, ok := ls.clients[1].bundle.Get(td); ok { + for _, cert := range tdb.X509Authorities() { + if !bundle.HasX509Authority(cert) { + bundle.AddX509Authority(cert) + } + } + } + rawBundles[td.String()] = ConcatRawCertsFromCerts(bundle.X509Authorities()) + } + if initBundle { + wg.Done() + initBundle = false + } + if reflect.DeepEqual(ls.rawBundles, rawBundles) { + log.Printf("x509 bundles unchanged") + } else { + log.Printf("x590 bundles changed") + ls.rawBundles = rawBundles + ls.bundleLock.Lock() + if ls.bundleChan != nil { + close(ls.bundleChan) + } + ls.bundleChan = make(chan struct{}) + ls.bundleLock.Unlock() + } + } + } + }() + + go func() { + log.Printf("Listening for jwt bundle updates\n") + for u := range ls.jwtBundleUpdate { + log.Printf("Got update for %d\n", u.id) + ls.clients[u.id].jwtBundles = u.bundle + if ls.clients[0].jwtBundles != nil && ls.clients[1].jwtBundles != nil { + log.Printf("We got two jwt bundles\n") + tmpBundles := make(map[string]jose.JSONWebKeySet) + var rawBundles map[string][]byte = make(map[string][]byte) + for td, bundle := range ls.clients[0].jwtBundles { + kids := make(map[string]bool) + var set jose.JSONWebKeySet + for _, b := range bundle.Keys { + kids[b.KeyID] = true + set.Keys = append(set.Keys, b) + } + if tdb, ok := ls.clients[1].jwtBundles[td]; ok { + for _, b := range tdb.Keys { + if _, ok := kids[b.KeyID]; !ok { + set.Keys = append(set.Keys, b) + } + } + } + tmpBundles[td] = set +//FIXME td's in 1 but not 0. Maybe same with x509? + res, err := json.Marshal(tmpBundles[td]) + if err != nil { +//FIXME what is the best way to handle this + log.Printf("Failed to marchal. %v", err) + continue + } + rawBundles[td] = res + } + if jwtInitBundle { + jwtWg.Done() + jwtInitBundle = false + } + if reflect.DeepEqual(ls.rawJwtBundles, rawBundles) { + log.Printf("jwt bundles unchanged") + } else { + log.Printf("jwt bundles changed") + ls.rawJwtBundles = rawBundles + ls.bundleLock.Lock() + if ls.jwtBundleChan != nil { + close(ls.jwtBundleChan) + } + ls.jwtBundleChan = make(chan struct{}) + ls.bundleLock.Unlock() + } + } + } + }() + + wg.Wait() + jwtWg.Wait() + log.Printf("Startup settled") + + workload.RegisterSpiffeWorkloadAPIServer(s, ls) + if err := s.Serve(lis); err != nil { + log.Fatalf("failed to serve: %v", err) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3f11b2b --- /dev/null +++ b/go.mod @@ -0,0 +1,50 @@ +module github.com/spiffe/spire-ha-agent + +go 1.23.2 + +require ( + github.com/Microsoft/go-winio v0.6.2 + github.com/mdlayher/vsock v1.2.1 + github.com/sirupsen/logrus v1.9.3 + github.com/spiffe/go-spiffe/v2 v2.4.0 + github.com/spiffe/spire v1.11.0 + github.com/spiffe/spire-api-sdk v1.11.0 + github.com/stretchr/testify v1.9.0 + golang.org/x/sys v0.26.0 + google.golang.org/grpc v1.67.1 +) + +require ( + github.com/DataDog/datadog-go v3.2.0+incompatible // indirect + github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/go-jose/go-jose/v4 v4.0.4 // indirect + github.com/golang/mock v1.6.0 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/hashicorp/go-immutable-radix v1.3.1 // indirect + github.com/hashicorp/go-metrics v0.5.3 // indirect + github.com/hashicorp/golang-lru v1.0.2 // indirect + github.com/hashicorp/hcl v1.0.1-vault-5 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/mdlayher/socket v0.4.1 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang v1.20.4 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/twmb/murmur3 v1.1.8 // indirect + github.com/uber-go/tally/v4 v4.1.16 // indirect + github.com/zeebo/errs v1.3.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/text v0.19.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 // indirect + google.golang.org/protobuf v1.35.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d930bdd --- /dev/null +++ b/go.sum @@ -0,0 +1,376 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/DataDog/datadog-go v3.2.0+incompatible h1:qSG2N4FghB1He/r2mFrWKCaL7dXCilEuNEeAn20fdD4= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 h1:MzBOUgng9orim59UnfUTLRjMpd09C5uEVQ6RPGeCaVI= +github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cactus/go-statsd-client/v5 v5.0.0/go.mod h1:COEvJ1E+/E2L4q6QE5CkjWPi4eeDw9maJBMIuMPBZbY= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk= +github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= +github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E= +github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-metrics v0.5.3 h1:M5uADWMOGCTUNU1YuC4hfknOeHNaX54LDm4oYSucoNE= +github.com/hashicorp/go-metrics v0.5.3/go.mod h1:KEjodfebIOuBYSAe/bHTm+HChmKSxAOXPBieMLYozDE= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= +github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.1-vault-5 h1:kI3hhbbyzr4dldA8UdTb7ZlVVlI2DACdCfz31RPDgJM= +github.com/hashicorp/hcl v1.0.1-vault-5/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= +github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= +github.com/mdlayher/vsock v1.2.1 h1:pC1mTJTvjo1r9n9fbm7S1j04rCgCzhCOS5DY0zqHlnQ= +github.com/mdlayher/vsock v1.2.1/go.mod h1:NRfCibel++DgeMD8z/hP+PPTjlNJsdPOmxcnENvE+SE= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= +github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.51.1 h1:eIjN50Bwglz6a/c3hAgSMcofL3nD+nFQkV6Dd4DsQCw= +github.com/prometheus/common v0.51.1/go.mod h1:lrWtQx+iDfn2mbH5GUzlH9TSHyfZpHkSiG1W7y3sF2Q= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spiffe/go-spiffe/v2 v2.3.0 h1:g2jYNb/PDMB8I7mBGL2Zuq/Ur6hUhoroxGQFyD6tTj8= +github.com/spiffe/go-spiffe/v2 v2.3.0/go.mod h1:Oxsaio7DBgSNqhAO9i/9tLClaVlfRok7zvJnTV8ZyIY= +github.com/spiffe/go-spiffe/v2 v2.4.0 h1:j/FynG7hi2azrBG5cvjRcnQ4sux/VNj8FAVc99Fl66c= +github.com/spiffe/go-spiffe/v2 v2.4.0/go.mod h1:m5qJ1hGzjxjtrkGHZupoXHo/FDWwCB1MdSyBzfHugx0= +github.com/spiffe/spire v1.10.1 h1:FAGOMB95lSBWu7MceOwncRe92rCaAHwzbX/p1DOjmMQ= +github.com/spiffe/spire v1.10.1/go.mod h1:yPpwBr+iBi2c0T9/rp/RTFbfMkzDvOaXo2hNGrkAT6U= +github.com/spiffe/spire v1.11.0 h1:aUtgZxp03IdjAYk9xiKB4mutmWg5KiaY+q/r0mCq7lw= +github.com/spiffe/spire v1.11.0/go.mod h1:RqMc7c1Iev739s8ak00C6M8Xh1y4U4z0Lar8XEg4idY= +github.com/spiffe/spire-api-sdk v1.2.5-0.20240627195926-b5ac064f580b h1:k7ei1fQyt6+FbqDEAd90xaXLg52YuXueM+BRcoHZvEU= +github.com/spiffe/spire-api-sdk v1.2.5-0.20240627195926-b5ac064f580b/go.mod h1:4uuhFlN6KBWjACRP3xXwrOTNnvaLp1zJs8Lribtr4fI= +github.com/spiffe/spire-api-sdk v1.10.1 h1:w29LB8Jm8d/9oGTgXzSQj5aGzwWEdMogKMMfCyOTM6I= +github.com/spiffe/spire-api-sdk v1.10.1/go.mod h1:4uuhFlN6KBWjACRP3xXwrOTNnvaLp1zJs8Lribtr4fI= +github.com/spiffe/spire-api-sdk v1.10.3 h1:6xsCJj77M+vkdH9tlTwNFP3Wpu/D0/Rd1fwOBDb2gD4= +github.com/spiffe/spire-api-sdk v1.10.3/go.mod h1:4uuhFlN6KBWjACRP3xXwrOTNnvaLp1zJs8Lribtr4fI= +github.com/spiffe/spire-api-sdk v1.11.0 h1:9t46NLWGEaOKwWb95nhOaezAUSTBJqW5Lx7C3uK8L4M= +github.com/spiffe/spire-api-sdk v1.11.0/go.mod h1:4uuhFlN6KBWjACRP3xXwrOTNnvaLp1zJs8Lribtr4fI= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/twmb/murmur3 v1.1.5/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= +github.com/twmb/murmur3 v1.1.8 h1:8Yt9taO/WN3l08xErzjeschgZU2QSrwm1kclYq+0aRg= +github.com/twmb/murmur3 v1.1.8/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= +github.com/uber-go/tally/v4 v4.1.16 h1:by2hveWRh/cUReButk6ns1sHK/hiKry7BuOV6iY16XI= +github.com/uber-go/tally/v4 v4.1.16/go.mod h1:RW5DgqsyEPs0lA4b0YNf4zKj7DveKHd73hnO6zVlyW0= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/zeebo/errs v1.3.0 h1:hmiaKqgYZzcVgRL1Vkc1Mn2914BbzB0IBxs+ebeutGs= +github.com/zeebo/errs v1.3.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 h1:Zy9XzmMEflZ/MAaA7vNcoebnRAld7FsPW1EeBB7V0m8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade h1:oCRSWfwGXQsqlVdErcyTt4A93Y8fo0/9D4b1gnI++qo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 h1:QCqS/PdaHTSWGvupk2F/ehwHtGc0/GYkT+3GAcR1CCc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= +google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/validator.v2 v2.0.0-20200605151824-2b28d334fa05/go.mod h1:o4V0GXN9/CAmCsvJ0oXYZvrZOe7syiDZSN1GWGZTGzc= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/pkg/peertracker/conn.go b/pkg/peertracker/conn.go new file mode 100644 index 0000000..94c18ad --- /dev/null +++ b/pkg/peertracker/conn.go @@ -0,0 +1,15 @@ +package peertracker + +import ( + "net" +) + +type Conn struct { + net.Conn + Info AuthInfo +} + +func (c *Conn) Close() error { + c.Info.Watcher.Close() + return c.Conn.Close() +} diff --git a/pkg/peertracker/credentials.go b/pkg/peertracker/credentials.go new file mode 100644 index 0000000..3ad2811 --- /dev/null +++ b/pkg/peertracker/credentials.go @@ -0,0 +1,75 @@ +package peertracker + +import ( + "context" + "net" + + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/peer" +) + +type grpcCredentials struct{} + +func NewCredentials() credentials.TransportCredentials { + return &grpcCredentials{} +} + +func (c *grpcCredentials) ClientHandshake(_ context.Context, _ string, conn net.Conn) (net.Conn, credentials.AuthInfo, error) { + conn.Close() + return conn, AuthInfo{}, ErrInvalidConnection +} + +func (c *grpcCredentials) ServerHandshake(conn net.Conn) (net.Conn, credentials.AuthInfo, error) { + wrappedCon, ok := conn.(*Conn) + if !ok { + conn.Close() + return conn, AuthInfo{}, ErrInvalidConnection + } + + return wrappedCon, wrappedCon.Info, nil +} + +func (c *grpcCredentials) Info() credentials.ProtocolInfo { + return credentials.ProtocolInfo{ + SecurityProtocol: authType, + SecurityVersion: "0.2", + ServerName: "spire-agent", + } +} + +func (c *grpcCredentials) Clone() credentials.TransportCredentials { + credentialsCopy := *c + return &credentialsCopy +} + +func (c *grpcCredentials) OverrideServerName(_ string) error { + return nil +} + +func WatcherFromContext(ctx context.Context) (Watcher, bool) { + ai, ok := AuthInfoFromContext(ctx) + if !ok { + return nil, false + } + + return ai.Watcher, true +} + +func CallerFromContext(ctx context.Context) (CallerInfo, bool) { + ai, ok := AuthInfoFromContext(ctx) + if !ok { + return CallerInfo{}, false + } + + return ai.Caller, true +} + +func AuthInfoFromContext(ctx context.Context) (AuthInfo, bool) { + peer, ok := peer.FromContext(ctx) + if !ok { + return AuthInfo{}, false + } + + ai, ok := peer.AuthInfo.(AuthInfo) + return ai, ok +} diff --git a/pkg/peertracker/errors.go b/pkg/peertracker/errors.go new file mode 100644 index 0000000..6ff9311 --- /dev/null +++ b/pkg/peertracker/errors.go @@ -0,0 +1,9 @@ +package peertracker + +import "errors" + +var ( + ErrInvalidConnection = errors.New("invalid connection") + ErrUnsupportedPlatform = errors.New("unsupported platform") + ErrUnsupportedTransport = errors.New("unsupported transport") +) diff --git a/pkg/peertracker/info.go b/pkg/peertracker/info.go new file mode 100644 index 0000000..b446146 --- /dev/null +++ b/pkg/peertracker/info.go @@ -0,0 +1,27 @@ +package peertracker + +import ( + "net" +) + +const ( + authType = "spire-attestation" +) + +type CallerInfo struct { + Addr net.Addr + PID int32 + UID uint32 + GID uint32 +} + +type AuthInfo struct { + Caller CallerInfo + Watcher Watcher +} + +// AuthType returns the authentication type and allows us to +// conform to the gRPC AuthInfo interface +func (AuthInfo) AuthType() string { + return authType +} diff --git a/pkg/peertracker/listener.go b/pkg/peertracker/listener.go new file mode 100644 index 0000000..a5c14f6 --- /dev/null +++ b/pkg/peertracker/listener.go @@ -0,0 +1,97 @@ +package peertracker + +import ( + "io" + "net" + + "github.com/sirupsen/logrus" +) + +var _ net.Listener = &Listener{} + +type ListenerFactory struct { + Log logrus.FieldLogger + NewTracker func(log logrus.FieldLogger) (PeerTracker, error) + ListenerFactoryOS // OS specific +} + +type Listener struct { + l net.Listener + log logrus.FieldLogger + Tracker PeerTracker +} + +func newNoopLogger() *logrus.Logger { + logger := logrus.New() + logger.Out = io.Discard + return logger +} + +func (l *Listener) Accept() (net.Conn, error) { + for { + var caller CallerInfo + var err error + + conn, err := l.l.Accept() + if err != nil { + return conn, err + } + + // Support future Listener types + switch conn.RemoteAddr().Network() { + case "unix": + caller, err = CallerFromUDSConn(conn) + case "pipe": + caller, err = CallerFromNamedPipeConn(conn) + case "vsock": + caller, err = CallerFromVSockConn(conn) + default: + err = ErrUnsupportedTransport + } + + if err != nil { + l.log.WithError(err).Warn("Connection failed during accept") + conn.Close() + continue + } + + watcher, err := l.Tracker.NewWatcher(caller) + if err != nil { + l.log.WithError(err).Warn("Connection failed during accept") + conn.Close() + continue + } + + wrappedConn := &Conn{ + Conn: conn, + Info: AuthInfo{ + Caller: caller, + Watcher: closeOnIsAliveErr{Watcher: watcher, conn: conn}, + }, + } + + return wrappedConn, nil + } +} + +func (l *Listener) Close() error { + l.Tracker.Close() + return l.l.Close() +} + +func (l *Listener) Addr() net.Addr { + return l.l.Addr() +} + +type closeOnIsAliveErr struct { + Watcher + conn io.Closer +} + +func (w closeOnIsAliveErr) IsAlive() error { + err := w.Watcher.IsAlive() + if err != nil { + _ = w.conn.Close() + } + return err +} diff --git a/pkg/peertracker/listener_posix.go b/pkg/peertracker/listener_posix.go new file mode 100644 index 0000000..763faa7 --- /dev/null +++ b/pkg/peertracker/listener_posix.go @@ -0,0 +1,43 @@ +//go:build !windows + +package peertracker + +import "net" +import "github.com/mdlayher/vsock" + +type ListenerFactoryOS struct { + NewUnixListener func(network string, laddr *net.UnixAddr) (*net.UnixListener, error) + NewVSockListener func(port uint32, cfg *vsock.Config) (*vsock.Listener, error) +} + +func (lf *ListenerFactory) ListenUnix(network string, laddr *net.UnixAddr) (*Listener, error) { + if lf.NewUnixListener == nil { + lf.NewUnixListener = net.ListenUnix + } + if lf.NewTracker == nil { + lf.NewTracker = NewTracker + } + if lf.Log == nil { + lf.Log = newNoopLogger() + } + return lf.listenUnix(network, laddr) +} + +func (lf *ListenerFactory) listenUnix(network string, laddr *net.UnixAddr) (*Listener, error) { + l, err := lf.NewUnixListener(network, laddr) + if err != nil { + return nil, err + } + + tracker, err := lf.NewTracker(lf.Log) + if err != nil { + l.Close() + return nil, err + } + + return &Listener{ + l: l, + Tracker: tracker, + log: lf.Log, + }, nil +} diff --git a/pkg/peertracker/listener_test.go b/pkg/peertracker/listener_test.go new file mode 100644 index 0000000..6a5cea4 --- /dev/null +++ b/pkg/peertracker/listener_test.go @@ -0,0 +1,152 @@ +//go:build !windows + +package peertracker + +import ( + "context" + "errors" + "net" + "path" + "testing" + "time" + + "github.com/sirupsen/logrus" + "github.com/sirupsen/logrus/hooks/test" + "github.com/spiffe/spire/test/spiretest" + "github.com/stretchr/testify/suite" +) + +var errMockWatcherFailed = errors.New("create new watcher failed") + +type failingMockTracker struct{} + +func (failingMockTracker) Close() {} +func (failingMockTracker) NewWatcher(CallerInfo) (Watcher, error) { + return nil, errMockWatcherFailed +} + +func newFailingMockTracker(_ logrus.FieldLogger) (PeerTracker, error) { + return failingMockTracker{}, nil +} + +func TestListenerTestSuite(t *testing.T) { + suite.Run(t, new(ListenerTestSuite)) +} + +type ListenerTestSuite struct { + suite.Suite + + ul *Listener + unixAddr *net.UnixAddr +} + +func (p *ListenerTestSuite) SetupTest() { + tempDir := spiretest.TempDir(p.T()) + p.unixAddr = &net.UnixAddr{ + Net: "unix", + Name: path.Join(tempDir, "test.sock"), + } +} + +func (p *ListenerTestSuite) TearDownTest() { + // only close the listener if we haven't already + if p.ul != nil { + err := p.ul.Close() + p.NoError(err) + p.ul = nil + } +} + +func (p *ListenerTestSuite) TestAcceptDoesntFailWhenTrackerFails() { + var err error + logger, hook := test.NewNullLogger() + logger.Level = logrus.WarnLevel + lf := ListenerFactory{ + NewTracker: newFailingMockTracker, + Log: logger, + } + p.ul, err = lf.ListenUnix(p.unixAddr.Network(), p.unixAddr) + p.Require().NoError(err) + + // used to cancel the log polling below if something goes wrong with + // the test + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + clientDone := make(chan error) + peer := newFakePeer(p.T()) + + peer.connect(p.unixAddr, clientDone) + + type acceptResult struct { + conn net.Conn + err error + } + acceptCh := make(chan acceptResult, 1) + go func() { + conn, err := p.ul.Accept() + acceptCh <- acceptResult{ + conn: conn, + err: err, + } + }() + + logCh := make(chan *logrus.Entry, 1) + go func() { + for { + logEntry := hook.LastEntry() + if logEntry == nil { + select { + case <-ctx.Done(): + close(logCh) + case <-time.After(time.Millisecond * 10): + } + continue + } + logCh <- logEntry + } + }() + + // Wait for the logs to show up demonstrating the accept failure + select { + case logEntry := <-logCh: + p.Require().NotNil(logEntry) + p.Require().Equal("Connection failed during accept", logEntry.Message) + logErr := logEntry.Data["error"] + p.Require().IsType(errors.New(""), logErr) + p.Require().EqualError(logErr.(error), "create new watcher failed") + case <-time.After(time.Second): + p.Require().Fail("waited too long for logs") + } + + p.Require().NoError(p.ul.Close()) + p.ul = nil + + // Wait for the listener to stop + select { + case acceptRes := <-acceptCh: + p.Require().Error(acceptRes.err) + p.Require().Contains(acceptRes.err.Error(), "use of closed network connection") + p.Require().Nil(acceptRes.conn) + case <-time.After(time.Second): + p.Require().Fail("waited too long for listener to close") + } +} + +func (p *ListenerTestSuite) TestAcceptFailsWhenUnderlyingAcceptFails() { + lf := ListenerFactory{ + NewTracker: newFailingMockTracker, + } + lf.ListenerFactoryOS.NewUnixListener = newFailingMockListenUnix + + ul, err := lf.ListenUnix(p.unixAddr.Network(), p.unixAddr) + p.Require().NoError(err) + + _, err = ul.Accept() + p.Require().Error(err) +} + +// returns an empty unix listener that will fail any call to Accept() +func newFailingMockListenUnix(string, *net.UnixAddr) (*net.UnixListener, error) { + return &net.UnixListener{}, nil +} diff --git a/pkg/peertracker/listener_windows.go b/pkg/peertracker/listener_windows.go new file mode 100644 index 0000000..58bc94a --- /dev/null +++ b/pkg/peertracker/listener_windows.go @@ -0,0 +1,45 @@ +//go:build windows + +package peertracker + +import ( + "net" + + "github.com/Microsoft/go-winio" +) + +type ListenerFactoryOS struct { + NewPipeListener func(pipe string, pipeConfig *winio.PipeConfig) (net.Listener, error) +} + +func (lf *ListenerFactory) ListenPipe(pipe string, pipeConfig *winio.PipeConfig) (*Listener, error) { + if lf.NewPipeListener == nil { + lf.NewPipeListener = winio.ListenPipe + } + if lf.NewTracker == nil { + lf.NewTracker = NewTracker + } + if lf.Log == nil { + lf.Log = newNoopLogger() + } + return lf.listenPipe(pipe, pipeConfig) +} + +func (lf *ListenerFactory) listenPipe(pipe string, pipeConfig *winio.PipeConfig) (*Listener, error) { + l, err := lf.NewPipeListener(pipe, pipeConfig) + if err != nil { + return nil, err + } + + tracker, err := lf.NewTracker(lf.Log) + if err != nil { + l.Close() + return nil, err + } + + return &Listener{ + l: l, + Tracker: tracker, + log: lf.Log, + }, nil +} diff --git a/pkg/peertracker/npipe.go b/pkg/peertracker/npipe.go new file mode 100644 index 0000000..5be969c --- /dev/null +++ b/pkg/peertracker/npipe.go @@ -0,0 +1,9 @@ +package peertracker + +import ( + "net" +) + +func CallerFromNamedPipeConn(conn net.Conn) (CallerInfo, error) { + return getCallerInfoFromNamedPipeConn(conn) +} diff --git a/pkg/peertracker/npipe_fallback.go b/pkg/peertracker/npipe_fallback.go new file mode 100644 index 0000000..8d58f17 --- /dev/null +++ b/pkg/peertracker/npipe_fallback.go @@ -0,0 +1,11 @@ +//go:build !windows + +package peertracker + +import ( + "net" +) + +func getCallerInfoFromNamedPipeConn(net.Conn) (CallerInfo, error) { + return CallerInfo{}, ErrUnsupportedPlatform +} diff --git a/pkg/peertracker/npipe_windows.go b/pkg/peertracker/npipe_windows.go new file mode 100644 index 0000000..8dca5b8 --- /dev/null +++ b/pkg/peertracker/npipe_windows.go @@ -0,0 +1,76 @@ +//go:build windows + +package peertracker + +import ( + "errors" + "fmt" + "net" + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +var ( + kernelbase = windows.NewLazyDLL("kernelbase.dll") + kernel32 = windows.NewLazyDLL("kernel32.dll") + + procCompareObjectHandles = kernelbase.NewProc("CompareObjectHandles") + procCompareObjectHandlesErr = procCompareObjectHandles.Find() + procGetNamedPipeClientProcessID = kernel32.NewProc("GetNamedPipeClientProcessId") + procGetNamedPipeClientProcessIDErr = procGetNamedPipeClientProcessID.Find() +) + +func getCallerInfoFromNamedPipeConn(conn net.Conn) (CallerInfo, error) { + var info CallerInfo + + type Fder interface { + Fd() uintptr + } + fder, ok := conn.(Fder) + if !ok { + conn.Close() + return info, errors.New("invalid connection") + } + + var pid int32 + if err := getNamedPipeClientProcessID(windows.Handle(fder.Fd()), &pid); err != nil { + return info, fmt.Errorf("error in GetNamedPipeClientProcessId function: %w", err) + } + + return CallerInfo{ + Addr: conn.RemoteAddr(), + PID: pid, + }, nil +} + +// getNamedPipeClientProcessID retrieves the client process identifier +// for the specified handle representing a named pipe. +func getNamedPipeClientProcessID(pipe windows.Handle, clientProcessID *int32) (err error) { + if procGetNamedPipeClientProcessIDErr != nil { + return procGetNamedPipeClientProcessIDErr + } + r1, _, e1 := syscall.SyscallN(procGetNamedPipeClientProcessID.Addr(), uintptr(pipe), uintptr(unsafe.Pointer(clientProcessID))) + if r1 == 0 { + return e1 + } + return nil +} + +func isCompareObjectHandlesFound() bool { + return procCompareObjectHandlesErr == nil +} + +// compareObjectHandles compares two object handles to determine if they +// refer to the same underlying kernel object +func compareObjectHandles(firstHandle, secondHandle windows.Handle) error { + if isCompareObjectHandlesFound() { + return procCompareObjectHandlesErr + } + r1, _, e1 := syscall.SyscallN(procCompareObjectHandles.Addr(), uintptr(firstHandle), uintptr(secondHandle)) + if r1 == 0 { + return e1 + } + return nil +} diff --git a/pkg/peertracker/peertracker.go b/pkg/peertracker/peertracker.go new file mode 100644 index 0000000..505b734 --- /dev/null +++ b/pkg/peertracker/peertracker.go @@ -0,0 +1,39 @@ +// Package peertracker handles attestation security for the SPIFFE Workload +// API. It does so in part by implementing the `net.Listener` interface and +// the gRPC credential interface, the functions of which are dependent on the +// underlying platform. Currently, UNIX domain sockets are supported on Linux, +// Darwin and the BSDs. Named pipes is supported on Windows. +// +// To accomplish the attestation security required by SPIFFE and SPIRE, this +// package provides process tracking - namely, exit detection. By using the +// included listener, `net.Conn`s can be cast back into the *peertracker.Conn +// type which allows access to caller information and liveness checks. By +// further utilizing the included gRPC credentials, this information can be +// extracted directly from the context by dependent handlers. +// +// Consumers that wish to use the included PID information for additional +// process interrogation should call IsAlive() following its use to ensure +// that the original caller is still alive and that the PID has not been +// reused. +package peertracker + +import ( + "github.com/sirupsen/logrus" +) + +type PeerTracker interface { + Close() + NewWatcher(CallerInfo) (Watcher, error) +} + +type Watcher interface { + Close() + IsAlive() error + PID() int32 +} + +// NewTracker creates a new platform-specific peer tracker. Close() must +// be called when done to release associated resources. +func NewTracker(log logrus.FieldLogger) (PeerTracker, error) { + return newTracker(log) +} diff --git a/pkg/peertracker/peertracker_posix_test.go b/pkg/peertracker/peertracker_posix_test.go new file mode 100644 index 0000000..dc79dd4 --- /dev/null +++ b/pkg/peertracker/peertracker_posix_test.go @@ -0,0 +1,14 @@ +//go:build !windows + +package peertracker + +import ( + "testing" + + "github.com/stretchr/testify/require" + "golang.org/x/sys/unix" +) + +func requireCallerExitFailedDirent(tb testing.TB, actual any) { + require.Equal(tb, unix.ENOENT, actual) +} diff --git a/pkg/peertracker/peertracker_test.go b/pkg/peertracker/peertracker_test.go new file mode 100644 index 0000000..d30f775 --- /dev/null +++ b/pkg/peertracker/peertracker_test.go @@ -0,0 +1,238 @@ +package peertracker + +import ( + "fmt" + "net" + "os" + "os/exec" + "path/filepath" + "runtime" + "strconv" + "testing" + + "github.com/sirupsen/logrus" + logtest "github.com/sirupsen/logrus/hooks/test" + "github.com/stretchr/testify/require" +) + +type peertrackerTest struct { + childPath string + listener *Listener + addr net.Addr + logHook *logtest.Hook +} + +func setupTest(t *testing.T) *peertrackerTest { + childPath := filepath.Join(t.TempDir(), "child.exe") + buildOutput, err := exec.Command("go", "build", "-o", childPath, childSource).CombinedOutput() + if err != nil { + t.Logf("build output:\n%v\n", string(buildOutput)) + require.FailNow(t, "failed to build test child") + } + + log, logHook := logtest.NewNullLogger() + + listener := listener(t, log, addr(t)) + p := &peertrackerTest{ + childPath: childPath, + listener: listener, + addr: listener.Addr(), + logHook: logHook, + } + t.Cleanup(func() { + if p.listener != nil { + require.NoError(t, p.listener.Close()) + } + }) + + return p +} + +func TestTrackerClose(t *testing.T) { + test := setupTest(t) + + test.listener.Tracker.Close() + _, err := test.listener.Tracker.NewWatcher(CallerInfo{}) + require.Error(t, err) +} + +func TestListener(t *testing.T) { + test := setupTest(t) + + doneCh := make(chan error) + peer := newFakePeer(t) + + peer.connect(test.addr, doneCh) + + rawConn, err := test.listener.Accept() + require.NoError(t, err) + + // Unblock connect goroutine + require.NoError(t, <-doneCh) + + conn, ok := rawConn.(*Conn) + require.True(t, ok) + + // Ensure we resolved the PID ok + require.Equal(t, int32(os.Getpid()), conn.Info.Caller.PID) + + // Ensure watcher is set up correctly + require.NotNil(t, conn.Info.Watcher) + require.Equal(t, int32(os.Getpid()), conn.Info.Watcher.PID()) + + peer.disconnect() + conn.Close() +} + +func TestExitDetection(t *testing.T) { + test := setupTest(t) + + // First, just test against ourselves + doneCh := make(chan error) + peer := newFakePeer(t) + + peer.connect(test.addr, doneCh) + + rawConn, err := test.listener.Accept() + require.NoError(t, err) + + // Unblock connect goroutine + require.NoError(t, <-doneCh) + + conn, ok := rawConn.(*Conn) + require.True(t, ok) + + // We're connected to ourselves - we should be alive! + require.NoError(t, conn.Info.Watcher.IsAlive()) + + // Should return an error once we're no longer tracking + peer.disconnect() + conn.Close() + require.EqualError(t, conn.Info.Watcher.IsAlive(), "caller is no longer being watched") + + // Start a forking child and allow it to exit while the grandchild holds the socket + peer.connectFromForkingChild(test.addr, test.childPath, doneCh) + + rawConn, err = test.listener.Accept() + + // Unblock child connect goroutine + require.NoError(t, <-doneCh) + + // Check for Accept() error only after unblocking + // the child so we can be sure that we can + // clean up correctly + defer peer.killGrandchild() + require.NoError(t, err) + + conn, ok = rawConn.(*Conn) + require.True(t, ok) + + // We know the child has exited because we read from doneCh + // Call to IsAlive should now return an error + switch runtime.GOOS { + case "darwin": + require.EqualError(t, conn.Info.Watcher.IsAlive(), "caller exit detected via kevent notification") + require.Len(t, test.logHook.Entries, 2) + firstEntry := test.logHook.Entries[0] + require.Equal(t, logrus.WarnLevel, firstEntry.Level) + require.Equal(t, "Caller is no longer being watched", firstEntry.Message) + secondEntry := test.logHook.Entries[1] + require.Equal(t, logrus.WarnLevel, secondEntry.Level) + require.Equal(t, "Caller exit detected via kevent notification", secondEntry.Message) + case "linux": + require.EqualError(t, conn.Info.Watcher.IsAlive(), "caller exit suspected due to failed readdirent") + require.Len(t, test.logHook.Entries, 2) + firstEntry := test.logHook.Entries[0] + require.Equal(t, logrus.WarnLevel, firstEntry.Level) + require.Equal(t, "Caller is no longer being watched", firstEntry.Message) + secondEntry := test.logHook.Entries[1] + require.Equal(t, logrus.WarnLevel, secondEntry.Level) + require.Equal(t, "Caller exit suspected due to failed readdirent", secondEntry.Message) + requireCallerExitFailedDirent(t, secondEntry.Data["error"]) + case "windows": + require.EqualError(t, conn.Info.Watcher.IsAlive(), "caller exit detected: exit code: 0") + require.Len(t, test.logHook.Entries, 2) + firstEntry := test.logHook.Entries[0] + require.Equal(t, logrus.WarnLevel, firstEntry.Level) + require.Equal(t, "Caller is no longer being watched", firstEntry.Message) + secondEntry := test.logHook.Entries[1] + require.Equal(t, logrus.WarnLevel, secondEntry.Level) + require.Equal(t, "Caller is not running anymore", secondEntry.Message) + require.Equal(t, "caller exit detected: exit code: 0", fmt.Sprintf("%v", secondEntry.Data["error"])) + default: + require.FailNow(t, "missing case for OS specific failure") + } + + // IsAlive should close the underlying connection with the grandchild when + // it detects the caller has exited. + _, err = conn.Read(make([]byte, 10)) + require.Error(t, err) + + conn.Close() + + // Check that IsAlive doesn't freak out if called after + // the tracker has been closed + test.listener.Close() + test.listener = nil + require.EqualError(t, conn.Info.Watcher.IsAlive(), "caller is no longer being watched") +} + +func newFakePeer(t *testing.T) *fakePeer { + return &fakePeer{ + t: t, + } +} + +// connect to the tcp listener +func (f *fakePeer) connect(addr net.Addr, doneCh chan error) { + if f.conn != nil { + f.t.Fatal("fake peer already connected") + } + + go func() { + conn, err := dial(addr) + if err != nil { + doneCh <- fmt.Errorf("could not dial address %s: %w", addr, err) + return + } + + f.conn = conn + doneCh <- nil + }() +} + +// close a connection we opened previously +func (f *fakePeer) disconnect() { + if f.conn == nil { + f.t.Fatal("fake peer not connected") + } + + f.conn.Close() + f.conn = nil +} + +// run child to connect and fork. allows us to test stale PID data +func (f *fakePeer) connectFromForkingChild(addr net.Addr, childPath string, doneCh chan error) { + if f.grandchildPID != 0 { + f.t.Fatalf("grandchild already running with PID %v", f.grandchildPID) + } + + go func() { + // #nosec G204 test code + out, err := childExecCommand(childPath, addr).Output() + if err != nil { + doneCh <- fmt.Errorf("child process failed: %w", err) + return + } + + // Get and store the grandchild PID from our child's STDOUT + grandchildPID, err := strconv.ParseInt(string(out), 10, 0) + if err != nil { + doneCh <- fmt.Errorf("could not get grandchild pid: %w", err) + return + } + + f.grandchildPID = int(grandchildPID) + doneCh <- nil + }() +} diff --git a/pkg/peertracker/peertracker_test_child_posix.go b/pkg/peertracker/peertracker_test_child_posix.go new file mode 100644 index 0000000..b28bc0c --- /dev/null +++ b/pkg/peertracker/peertracker_test_child_posix.go @@ -0,0 +1,67 @@ +//go:build ignore + +// This file is used during testing. It is built as an external binary +// and called from the test suite in order to exercise various peer +// tracking scenarios +package main + +import ( + "flag" + "fmt" + "net" + "os" + "time" +) + +func main() { + var socketPath string + flag.StringVar(&socketPath, "socketPath", "", "path to peertracker socket") + flag.Parse() + + // We are a grandchild - send a sign then sleep forever + if socketPath == "" { + fmt.Fprintf(os.Stdout, "i'm alive!") + + time.Sleep(1 * time.Minute) + } + + if socketPath == "" { + fmt.Fprint(os.Stderr, "-socketPath or noop flag required") + os.Exit(4) + } + + addr := &net.UnixAddr{ + Name: socketPath, + Net: "unix", + } + + conn, err := net.DialUnix("unix", nil, addr) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to connect to socket: %v", err) + os.Exit(5) + } + + fd, err := conn.File() + if err != nil { + fmt.Fprintf(os.Stderr, "failed to get socket descriptor: %v", err) + os.Exit(6) + } + + // Pass our fork the socket's file descriptor + procattr := &os.ProcAttr{ + Files: []*os.File{ + os.Stdin, + fd, + }, + } + + proc, err := os.StartProcess(os.Args[0], []string{os.Args[0]}, procattr) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to produce grandchild: %v", err) + os.Exit(7) + } + + // Inform our caller of the grandchild pid + fmt.Fprintf(os.Stdout, "%v", proc.Pid) + os.Exit(0) +} diff --git a/pkg/peertracker/peertracker_test_child_windows.go b/pkg/peertracker/peertracker_test_child_windows.go new file mode 100644 index 0000000..cbdda35 --- /dev/null +++ b/pkg/peertracker/peertracker_test_child_windows.go @@ -0,0 +1,64 @@ +//go:build ignore + +// This file is used during testing. It is built as an external binary +// and called from the test suite in order to exercise various peer +// tracking scenarios +package main + +import ( + "flag" + "fmt" + "os" + + "github.com/Microsoft/go-winio" +) + +func main() { + var namedPipeName string + + flag.StringVar(&namedPipeName, "namedPipeName", "", "pipe name to peertracker named pipe") + flag.Parse() + + // We are a grandchild - send a sign then sleep forever + if namedPipeName == "" { + fmt.Fprintf(os.Stdout, "i'm alive!") + + select {} + } + + conn, err := winio.DialPipe(namedPipeName, nil) + if err != nil { + fmt.Fprintf(os.Stderr, "DialPipe failed: %v", err) + os.Exit(5) + } + + type Fder interface { + Fd() uintptr + } + fder, ok := conn.(Fder) + if !ok { + conn.Close() + fmt.Fprintf(os.Stderr, "invalid connection", err) + os.Exit(6) + } + + f := os.NewFile(fder.Fd(), "pipe") + procattr := &os.ProcAttr{ + Env: os.Environ(), + Files: []*os.File{ + os.Stdin, // Do not block on stdin + f, + os.Stdin, // Do not block on stderr + }, + } + + proc, err := os.StartProcess(os.Args[0], []string{os.Args[0]}, procattr) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to produce grandchild: %v", err) + os.Exit(7) + } + + // Inform our caller of the grandchild pid + fmt.Fprintf(os.Stdout, "%v", proc.Pid) + os.Exit(0) +} diff --git a/pkg/peertracker/peertracker_test_posix.go b/pkg/peertracker/peertracker_test_posix.go new file mode 100644 index 0000000..71ca9dc --- /dev/null +++ b/pkg/peertracker/peertracker_test_posix.go @@ -0,0 +1,60 @@ +//go:build !windows + +package peertracker + +import ( + "net" + "os/exec" + "path/filepath" + "testing" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/require" + "golang.org/x/sys/unix" +) + +const ( + childSource = "peertracker_test_child_posix.go" +) + +type fakePeer struct { + grandchildPID int + conn net.Conn + t *testing.T +} + +func (f *fakePeer) killGrandchild() { + if f.grandchildPID == 0 { + f.t.Fatal("no known grandchild") + } + + err := unix.Kill(f.grandchildPID, unix.SIGKILL) + if err != nil { + f.t.Fatalf("unable to kill grandchild: %v", err) + } + + f.grandchildPID = 0 +} + +func addr(t *testing.T) net.Addr { + return &net.UnixAddr{ + Net: "unix", + Name: filepath.Join(t.TempDir(), "test.sock"), + } +} + +func listener(t *testing.T, log *logrus.Logger, addr net.Addr) *Listener { + listener, err := (&ListenerFactory{Log: log}).ListenUnix(addr.Network(), addr.(*net.UnixAddr)) + require.NoError(t, err) + + return listener +} + +func childExecCommand(childPath string, addr net.Addr) *exec.Cmd { + // #nosec G204 test code + return exec.Command(childPath, "-socketPath", addr.(*net.UnixAddr).Name) +} + +func dial(addr net.Addr) (net.Conn, error) { + return net.Dial(addr.Network(), addr.String()) +} diff --git a/pkg/peertracker/peertracker_test_windows.go b/pkg/peertracker/peertracker_test_windows.go new file mode 100644 index 0000000..44fcbfd --- /dev/null +++ b/pkg/peertracker/peertracker_test_windows.go @@ -0,0 +1,66 @@ +//go:build windows + +package peertracker + +import ( + "net" + "os" + "os/exec" + "testing" + + "github.com/Microsoft/go-winio" + "github.com/sirupsen/logrus" + "github.com/spiffe/spire/test/spiretest" + "github.com/stretchr/testify/require" +) + +const ( + childSource = "peertracker_test_child_windows.go" +) + +type fakePeer struct { + grandchildPID int + conn net.Conn + t *testing.T +} + +func (f *fakePeer) killGrandchild() { + if f.grandchildPID == 0 { + f.t.Fatal("no known grandchild") + } + + process, err := os.FindProcess(f.grandchildPID) + if err != nil { + f.t.Fatalf("unable to find process: %v", err) + } + if err = process.Kill(); err != nil { + f.t.Fatalf("unable to kill grandchild: %v", err) + } + + // Wait for the process to exit, so we are sure that we can + // cleanup the directory containing the executable + if _, err := process.Wait(); err != nil { + f.t.Fatalf("wait failed: %v", err) + } + f.grandchildPID = 0 +} + +func addr(*testing.T) net.Addr { + return spiretest.GetRandNamedPipeAddr() +} + +func listener(t *testing.T, log *logrus.Logger, addr net.Addr) *Listener { + listener, err := (&ListenerFactory{Log: log}).ListenPipe(addr.String(), nil) + require.NoError(t, err) + + return listener +} + +func childExecCommand(childPath string, addr net.Addr) *exec.Cmd { + // #nosec G204 test code + return exec.Command(childPath, "-namedPipeName", addr.String()) +} + +func dial(addr net.Addr) (net.Conn, error) { + return winio.DialPipe(addr.String(), nil) +} diff --git a/pkg/peertracker/peertracker_windows_test.go b/pkg/peertracker/peertracker_windows_test.go new file mode 100644 index 0000000..3443e47 --- /dev/null +++ b/pkg/peertracker/peertracker_windows_test.go @@ -0,0 +1,11 @@ +//go:build windows + +package peertracker + +import ( + "testing" +) + +func requireCallerExitFailedDirent(_ testing.TB, _ any) { + // No-op on Windows, only relevant for Unix systems +} diff --git a/pkg/peertracker/trace.go b/pkg/peertracker/trace.go new file mode 100644 index 0000000..4c1b528 --- /dev/null +++ b/pkg/peertracker/trace.go @@ -0,0 +1,54 @@ +package peertracker + +import "net" +import "fmt" +import "github.com/mdlayher/vsock" + +func (lf *ListenerFactory) ListenVSock(port uint32) (*Listener, error) { + if lf.NewUnixListener == nil { + lf.NewVSockListener = vsock.Listen + } + if lf.NewTracker == nil { + lf.NewTracker = NewTracker + } + if lf.Log == nil { + lf.Log = newNoopLogger() + } + return lf.listenVSock(port) +} + +func (lf *ListenerFactory) listenVSock(port uint32) (*Listener, error) { + l, err := lf.NewVSockListener(port, nil) + if err != nil { + return nil, err + } + + tracker, err := lf.NewTracker(lf.Log) + if err != nil { + l.Close() + return nil, err + } + + return &Listener{ + l: l, + Tracker: tracker, + log: lf.Log, + }, nil +} + +func CallerFromVSockConn(conn net.Conn) (CallerInfo, error) { + var info CallerInfo + cid := int(conn.RemoteAddr().(*vsock.Addr).ContextID) + pid := CID2PID(cid) + fmt.Printf("Got PID %d for CID %d\n", pid, cid) + if pid < 0 { + return info, fmt.Errorf("Could not fetch PID from CID") + } + info = CallerInfo{ + PID: int32(pid), + } + + info.Addr = conn.RemoteAddr() + return info, nil +} + diff --git a/pkg/peertracker/trace_linux.go b/pkg/peertracker/trace_linux.go new file mode 100644 index 0000000..fd49050 --- /dev/null +++ b/pkg/peertracker/trace_linux.go @@ -0,0 +1,43 @@ +package peertracker + +import "os/exec" +import "fmt" +import "bytes" +import "gopkg.in/yaml.v3" + +// #include +//import "C" +/* +func CID2PID(cid int) int { + pid := int(C.tracefs_find_cid_pid(C.int(cid))) + return pid + +}*/ +/*func main() { + fmt.Printf("%d\n", CID2PID(12345)) +}*/ + +type cid2pid struct { + Cid int `yaml:"cid"` + Pid int `yaml:"pid"` +} + +func CID2PID(cid int) int { + buf := new(bytes.Buffer) + c2p := &cid2pid{} + cidstr := fmt.Sprintf("%d", cid) + cmd := exec.Command("cid2pid", cidstr) + cmd.Stdout = buf + if err := cmd.Run(); err != nil { + return -1 + } + err := yaml.Unmarshal(buf.Bytes(), &c2p) + if err != nil { + return -4 + } + if c2p.Cid != cid { + fmt.Printf("%d %d\n", c2p.Cid, c2p.Pid) + return -5 + } + return c2p.Pid +} diff --git a/pkg/peertracker/tracker_bsd.go b/pkg/peertracker/tracker_bsd.go new file mode 100644 index 0000000..0d1aa04 --- /dev/null +++ b/pkg/peertracker/tracker_bsd.go @@ -0,0 +1,220 @@ +//go:build darwin || freebsd || netbsd || openbsd + +package peertracker + +import ( + "context" + "errors" + "fmt" + "sync" + "time" + + "github.com/sirupsen/logrus" + "github.com/spiffe/spire/pkg/common/telemetry" + "golang.org/x/sys/unix" +) + +const ( + bsdType = "bsd" +) + +var ( + safetyDelay = 250 * time.Millisecond +) + +type bsdTracker struct { + closer func() + ctx context.Context + kqfd int + mtx sync.Mutex + watchedPIDs map[int]chan struct{} + log logrus.FieldLogger +} + +func newTracker(log logrus.FieldLogger) (*bsdTracker, error) { + kqfd, err := unix.Kqueue() + if err != nil { + return nil, err + } + + ctx, cancel := context.WithCancel(context.Background()) + tracker := &bsdTracker{ + closer: cancel, + ctx: ctx, + kqfd: kqfd, + watchedPIDs: make(map[int]chan struct{}), + log: log.WithField(telemetry.Type, bsdType), + } + + go tracker.receiveKevents(kqfd) + + return tracker, nil +} + +func (b *bsdTracker) Close() { + b.mtx.Lock() + defer b.mtx.Unlock() + + // Be sure to cancel the context before closing the + // kqueue file descriptor so the goroutine watching it + // will know that we are shutting down. + b.closer() + unix.Close(b.kqfd) +} + +func (b *bsdTracker) NewWatcher(info CallerInfo) (Watcher, error) { + // If PID == 0, something is wrong... + if info.PID == 0 { + return nil, errors.New("could not resolve caller information") + } + + if b.ctx.Err() != nil { + return nil, errors.New("tracker has been closed") + } + + b.mtx.Lock() + defer b.mtx.Unlock() + + pid := int(info.PID) + + done, ok := b.watchedPIDs[pid] + if !ok { + err := b.addKeventForWatcher(pid) + if err != nil { + return nil, fmt.Errorf("could not create watcher: %w", err) + } + + done = make(chan struct{}) + b.watchedPIDs[pid] = done + } + log := b.log.WithField(telemetry.PID, pid) + + return newBSDWatcher(info, done, log), nil +} + +func (b *bsdTracker) addKeventForWatcher(pid int) error { + kevent := unix.Kevent_t{} + flags := unix.EV_ADD | unix.EV_RECEIPT | unix.EV_ONESHOT + unix.SetKevent(&kevent, pid, unix.EVFILT_PROC, flags) + + kevent.Fflags = unix.NOTE_EXIT + + kevents := []unix.Kevent_t{kevent} + _, err := unix.Kevent(b.kqfd, kevents, nil, nil) + return err +} + +func (b *bsdTracker) receiveKevents(kqfd int) { + for { + receive := make([]unix.Kevent_t, 5) + num, err := unix.Kevent(kqfd, nil, receive, nil) + if err != nil { + // KQUEUE(2) outlines the conditions under which the Kevent call + // can return an error - they are as follows: + // + // EACCESS: The process does not have permission to register a filter. + // EFAULT: There was an error reading or writing the kevent or kevent64_s structure. + // EBADF: The specified descriptor is invalid. + // EINTR: A signal was delivered before the timeout expired and before any events were + // placed on the kqueue for return. + // EINVAL: The specified time limit or filter is invalid. + // ENOENT: The event could not be found to be modified or deleted. + // ENOMEM: No memory was available to register the event. + // ESRCH: The specified process to attach to does not exist. + // + // Given our usage, the only error that seems possible is EBADF during shutdown. + // If we encounter any other error, we really have no way to recover. This will cause + // all subsequent workload attestations to fail open. After much deliberation, it is + // decided that the safest thing to do is to panic and allow supervision to step in. + // If this is actually encountered in the wild, we can examine the conditions and try + // to do something more intelligent. For now, we will just check to see if we are + // shutting down. + if b.ctx.Err() != nil { + // Don't panic, we're just shutting down + return + } + + if errors.Is(err, unix.EINTR) { + continue + } + + panicMsg := fmt.Sprintf("unrecoverable error while reading from kqueue: %v", err) + panic(panicMsg) + } + + b.mtx.Lock() + for _, kevent := range receive[:num] { + if kevent.Filter == unix.EVFILT_PROC && (kevent.Fflags&unix.NOTE_EXIT) > 0 { + pid := int(kevent.Ident) + done, ok := b.watchedPIDs[pid] + if ok { + close(done) + delete(b.watchedPIDs, pid) + } + } + } + b.mtx.Unlock() + } +} + +type bsdWatcher struct { + closed bool + done <-chan struct{} + mtx sync.Mutex + pid int32 + log logrus.FieldLogger +} + +func newBSDWatcher(info CallerInfo, done <-chan struct{}, log logrus.FieldLogger) *bsdWatcher { + return &bsdWatcher{ + done: done, + pid: info.PID, + log: log, + } +} + +func (b *bsdWatcher) Close() { + // For simplicity, don't bother cleaning up after ourselves + // The map entry will be reaped when the process exits + // + // Other watchers are unable to track after closed (unlike + // this one), so to provide consistent behavior, set the closed + // bit and return an error on subsequent IsAlive() calls + b.mtx.Lock() + defer b.mtx.Unlock() + b.closed = true +} + +func (b *bsdWatcher) IsAlive() error { + b.mtx.Lock() + if b.closed { + b.mtx.Unlock() + b.log.Warn("Caller is no longer being watched") + return errors.New("caller is no longer being watched") + } + b.mtx.Unlock() + + // Using kqueue/kevent means we are relying on an asynchronous notification + // system for exit detection. Delays can be incurred on either side: in our + // kevent consumer or in the kernel. Typically, IsAlive() is called following + // workload attestation which can take hundreds of milliseconds, so in practice + // we will probably have been notified of an exit by now if it occurred prior to + // or during the attestation process. + // + // As an extra safety precaution, artificially delay our answer to IsAlive() in + // a blind attempt to allow "enough" time to pass for us to learn of the + // potential exit event. + time.Sleep(safetyDelay) + + select { + case <-b.done: + b.log.Warn("Caller exit detected via kevent notification") + return errors.New("caller exit detected via kevent notification") + default: + return nil + } +} + +func (b *bsdWatcher) PID() int32 { + return b.pid +} diff --git a/pkg/peertracker/tracker_fallback.go b/pkg/peertracker/tracker_fallback.go new file mode 100644 index 0000000..0929950 --- /dev/null +++ b/pkg/peertracker/tracker_fallback.go @@ -0,0 +1,11 @@ +//go:build !linux && !darwin && !freebsd && !netbsd && !openbsd && !windows + +package peertracker + +import ( + "github.com/sirupsen/logrus" +) + +func newTracker(log logrus.FieldLogger) (PeerTracker, error) { + return nil, ErrUnsupportedPlatform +} diff --git a/pkg/peertracker/tracker_linux.go b/pkg/peertracker/tracker_linux.go new file mode 100644 index 0000000..f97183c --- /dev/null +++ b/pkg/peertracker/tracker_linux.go @@ -0,0 +1,208 @@ +//go:build linux + +package peertracker + +import ( + "errors" + "fmt" + "os" + "strings" + "sync" + + "github.com/sirupsen/logrus" + "github.com/spiffe/spire/pkg/common/telemetry" + "golang.org/x/sys/unix" +) + +const ( + linuxType = "linux" +) + +type linuxTracker struct { + log logrus.FieldLogger +} + +func newTracker(log logrus.FieldLogger) (*linuxTracker, error) { + return &linuxTracker{ + log: log.WithField(telemetry.Type, linuxType), + }, nil +} + +func (l *linuxTracker) NewWatcher(info CallerInfo) (Watcher, error) { + return newLinuxWatcher(info, l.log) +} + +func (*linuxTracker) Close() { +} + +type linuxWatcher struct { + gid uint32 + pid int32 + mtx sync.Mutex + procPath string + procfd int + starttime string + uid uint32 + log logrus.FieldLogger +} + +func newLinuxWatcher(info CallerInfo, log logrus.FieldLogger) (*linuxWatcher, error) { + // If PID == 0, something is wrong... + if info.PID == 0 { + return nil, errors.New("could not resolve caller information") + } + + procPath := fmt.Sprintf("/proc/%v", info.PID) + + // Grab a handle to proc first since that's the fastest thing we can do + procfd, err := unix.Open(procPath, unix.O_RDONLY, 0) + if err != nil { + return nil, fmt.Errorf("could not open caller's proc directory: %w", err) + } + + starttime, err := getStarttime(info.PID) + if err != nil { + unix.Close(procfd) + return nil, err + } + + log = log.WithFields(logrus.Fields{ + telemetry.CallerGID: info.GID, + telemetry.PID: info.PID, + telemetry.Path: procPath, + telemetry.CallerUID: info.UID, + telemetry.StartTime: starttime, + }) + + return &linuxWatcher{ + gid: info.GID, + pid: info.PID, + procPath: procPath, + procfd: procfd, + starttime: starttime, + uid: info.UID, + log: log, + }, nil +} + +func (l *linuxWatcher) Close() { + l.mtx.Lock() + defer l.mtx.Unlock() + + if l.procfd < 0 { + return + } + + unix.Close(l.procfd) + l.procfd = -1 +} + +func (l *linuxWatcher) IsAlive() error { + l.mtx.Lock() + defer l.mtx.Unlock() + + if l.procfd < 0 { + l.log.Warn("Caller is no longer being watched") + return errors.New("caller is no longer being watched") + } + + // First we will check if we can read from the original directory handle. + // If the process has exited since we opened it, the read should fail (i.e. + // the ReadDirent syscall will return -1) + var buf [8196]byte + n, err := unix.ReadDirent(l.procfd, buf[:]) + if err != nil { + l.log.WithError(err).Warn("Caller exit suspected due to failed readdirent") + return errors.New("caller exit suspected due to failed readdirent") + } + if n < 0 { + l.log.WithField(telemetry.StatusCode, n).Warn("Caller exit suspected due to failed readdirent") + return fmt.Errorf("caller exit suspected due to failed readdirent: n=%d", n) + } + + // A successful fd read should indicate that the original process is still alive, however + // it is not clear if the original inode can be freed by Linux while it is still referenced. + // This _shouldn't_ happen, but if it does, then there might be room for a reused PID to + // collide with the original inode making the read successful. As an extra measure, ensure + // that the current `starttime` matches the one we saw originally. + // + // This is probably overkill. + // TODO: Evaluate the use of `starttime` as the primary exit detection mechanism. + currentStarttime, err := getStarttime(l.pid) + if err != nil { + l.log.WithError(err).Warn("Caller exit suspected due to failure to get starttime") + return fmt.Errorf("caller exit suspected due to failure to get starttime: %w", err) + } + if currentStarttime != l.starttime { + l.log.WithFields(logrus.Fields{ + telemetry.ExpectStartTime: l.starttime, + telemetry.ReceivedStartTime: currentStarttime, + }).Warn("New process detected: process starttime does not match original caller") + return fmt.Errorf("new process detected: process starttime %v does not match original caller %v", currentStarttime, l.starttime) + } + + // Finally, read the UID and GID off the proc directory to determine the owner. If + // we got beaten by a PID race when opening the proc handle originally, we can at + // least get to know that the race winner is running as the same user and group as + // the original caller by comparing it to the received CallerInfo. + var stat unix.Stat_t + if err := unix.Stat(l.procPath, &stat); err != nil { + l.log.WithError(err).Warn("Caller exit suspected due to failed proc stat") + return errors.New("caller exit suspected due to failed proc stat") + } + if stat.Uid != l.uid { + l.log.WithFields(logrus.Fields{ + telemetry.ExpectUID: l.uid, + telemetry.ReceivedUID: stat.Uid, + }).Warn("New process detected: process uid does not match original caller") + return fmt.Errorf("new process detected: process uid %v does not match original caller %v", stat.Uid, l.uid) + } + if stat.Gid != l.gid { + l.log.WithFields(logrus.Fields{ + telemetry.ExpectGID: l.gid, + telemetry.ReceivedGID: stat.Gid, + }).Warn("New process detected: process gid does not match original caller") + return fmt.Errorf("new process detected: process gid %v does not match original caller %v", stat.Gid, l.gid) + } + + return nil +} + +func (l *linuxWatcher) PID() int32 { + return l.pid +} + +func parseTaskStat(stat string) ([]string, error) { + b := strings.IndexByte(stat, '(') + e := strings.LastIndexByte(stat, ')') + if b == -1 || e == -1 { + return nil, errors.New("task name is not parenthesized") + } + + fields := make([]string, 0, 52) + fields = append(fields, strings.Split(stat[:b-1], " ")...) + fields = append(fields, stat[b+1:e]) + fields = append(fields, strings.Split(stat[e+2:], " ")...) + return fields, nil +} + +func getStarttime(pid int32) (string, error) { + statBytes, err := os.ReadFile(fmt.Sprintf("/proc/%v/stat", pid)) + if err != nil { + return "", fmt.Errorf("could not read caller stat: %w", err) + } + + statFields, err := parseTaskStat(string(statBytes)) + if err != nil { + return "", fmt.Errorf("bad stat data: %w", err) + } + + // starttime is the 22nd field in the proc stat data + // Field number 38 was introduced in Linux 2.1.22 + // Protect against invalid index and reject anything before 2.1.22 + if len(statFields) < 38 { + return "", errors.New("bad stat data or unsupported platform") + } + + return statFields[21], nil +} diff --git a/pkg/peertracker/tracker_linux_test.go b/pkg/peertracker/tracker_linux_test.go new file mode 100644 index 0000000..829f559 --- /dev/null +++ b/pkg/peertracker/tracker_linux_test.go @@ -0,0 +1,41 @@ +//go:build linux + +package peertracker + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseTaskStat(t *testing.T) { + tests := []struct { + data string + fields []string + err error + }{ + { + data: "1 (cmd) S 0 1 1 0 -1 4194560 30901 1011224 96 1826 185 2546 3273 2402 20 0 1 0 24 170409984 2900 18446744073709551615 1 1 0 0 0 0 671173123 4096 1260 0 0 0 17 7 0 0 12 0 0 0 0 0 0 0 0 0 0", + fields: []string{"1", "cmd", "S", "0", "1", "1", "0", "-1", "4194560", "30901", "1011224", "96", "1826", "185", "2546", "3273", "2402", "20", "0", "1", "0", "24", "170409984", "2900", "18446744073709551615", "1", "1", "0", "0", "0", "0", "671173123", "4096", "1260", "0", "0", "0", "17", "7", "0", "0", "12", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0"}, + err: nil, + }, + { + data: "1 (the cmd) S 0 1 1 0 -1 4194560 30901 1011224 96 1826 185 2546 3273 2402 20 0 1 0 24 170409984 2900 18446744073709551615 1 1 0 0 0 0 671173123 4096 1260 0 0 0 17 7 0 0 12 0 0 0 0 0 0 0 0 0 0", + fields: []string{"1", "the cmd", "S", "0", "1", "1", "0", "-1", "4194560", "30901", "1011224", "96", "1826", "185", "2546", "3273", "2402", "20", "0", "1", "0", "24", "170409984", "2900", "18446744073709551615", "1", "1", "0", "0", "0", "0", "671173123", "4096", "1260", "0", "0", "0", "17", "7", "0", "0", "12", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0"}, + err: nil, + }, + { + data: "1 cmd S 0 1 1 0 -1 4194560 30901 1011224 96 1826 185 2546 3273 2402 20 0 1 0 24 170409984 2900 18446744073709551615 1 1 0 0 0 0 671173123 4096 1260 0 0 0 17 7 0 0 12 0 0 0 0 0 0 0 0 0 0", + fields: nil, + err: errors.New("task name is not parenthesized"), + }, + } + + assert := assert.New(t) + for _, tt := range tests { + fields, err := parseTaskStat(tt.data) + assert.Equal(fields, tt.fields) + assert.Equal(err, tt.err) + } +} diff --git a/pkg/peertracker/tracker_windows.go b/pkg/peertracker/tracker_windows.go new file mode 100644 index 0000000..8b09d5e --- /dev/null +++ b/pkg/peertracker/tracker_windows.go @@ -0,0 +1,212 @@ +//go:build windows + +package peertracker + +import ( + "errors" + "fmt" + "sync" + + "github.com/sirupsen/logrus" + "github.com/spiffe/spire/pkg/common/telemetry" + "golang.org/x/sys/windows" +) + +const ( + windowsType = "windows" + stillActive = 259 // https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getexitcodeprocess +) + +type windowsTracker struct { + log logrus.FieldLogger + sc systemCaller +} + +func newTracker(log logrus.FieldLogger) (*windowsTracker, error) { + return &windowsTracker{ + log: log.WithField(telemetry.Type, windowsType), + sc: &systemCall{}, + }, nil +} + +func (t *windowsTracker) NewWatcher(info CallerInfo) (Watcher, error) { + ww, err := t.newWindowsWatcher(info, t.log) + if err != nil { + return nil, err + } + return ww, nil +} + +func (*windowsTracker) Close() { +} + +type windowsWatcher struct { + mtx sync.Mutex + procHandle windows.Handle + + pid int32 + log logrus.FieldLogger + + sc systemCaller +} + +func (t *windowsTracker) newWindowsWatcher(info CallerInfo, log logrus.FieldLogger) (*windowsWatcher, error) { + // Having an open process handle prevents the process object from being destroyed, + // keeping the process ID valid, so this is the first thing that we do. + procHandle, err := t.sc.OpenProcess(info.PID) + if err != nil { + return nil, err + } + + // Find out if the PID is a well known PID that we don't + // expect from a workload. + switch info.PID { + case 0: + // Process ID 0 is the Idle process + return nil, errors.New("caller is the Idle process") + case 4: + // Process ID 4 is the System process + return nil, errors.New("caller is the System process") + } + + // This is a mitigation for attacks that leverage opening a + // named pipe through the local SMB server that set the PID + // attribute to 0xFEFF (65279). We want to to prevent abusing + // the fact that Windows reuses PID values and an attacker could + // cycle through process creation until it has a suitable process + // meeting the security check requirements from SMB server. + // Note that 65279 is not a valid PID in Windows because is not + // a multiple of 4, but if the SMB server calls OpenProcess on + // 65279 it will round down and open the PID 65276 which could + // be created by the attacker. + // This check makes sure that the process handle obtained from + // the PID discovered through the GetNamedPipeClientProcessId + // call matches the one that is obtained from that process ID. + pid, err := t.sc.GetProcessID(procHandle) + if err != nil { + return nil, fmt.Errorf("error getting process id from handle: %w", err) + } + if int32(pid) != info.PID { + return nil, errors.New("process ID does not match with the caller") + } + + log = log.WithFields(logrus.Fields{ + telemetry.PID: info.PID, + }) + + return &windowsWatcher{ + log: log, + pid: info.PID, + procHandle: procHandle, + sc: t.sc, + }, nil +} + +func (w *windowsWatcher) Close() { + w.mtx.Lock() + defer w.mtx.Unlock() + + if err := w.sc.CloseHandle(w.procHandle); err != nil { + w.log.WithError(err).Warn("Could not close process handle") + } + w.procHandle = windows.InvalidHandle +} + +func (w *windowsWatcher) IsAlive() error { + w.mtx.Lock() + defer w.mtx.Unlock() + + if w.procHandle == windows.InvalidHandle { + w.log.Warn("Caller is no longer being watched") + return errors.New("caller is no longer being watched") + } + + // The process object remains as long as the process is still running or + // as long as there is a handle to the process object. + // GetExitCodeProcess can be called to retrieve the exit code. + var exitCode uint32 + err := w.sc.GetExitCodeProcess(w.procHandle, &exitCode) + if err != nil { + return fmt.Errorf("error getting exit code from the process: %w", err) + } + if exitCode != stillActive { + err = fmt.Errorf("caller exit detected: exit code: %d", exitCode) + w.log.WithError(err).Warnf("Caller is not running anymore") + return err + } + + h, err := w.sc.OpenProcess(w.pid) + if err != nil { + w.log.WithError(err).Warn("Caller exit suspected due to failure to open process") + return fmt.Errorf("caller exit suspected due to failure to open process: %w", err) + } + defer func() { + if err := w.sc.CloseHandle(h); err != nil { + w.log.WithError(err).Warn("Could not close process handle in liveness check") + } + }() + + if w.sc.IsCompareObjectHandlesFound() { + if err := w.sc.CompareObjectHandles(w.procHandle, h); err != nil { + w.log.WithError(err).Warn("Current process handle does not refer to the same original process: CompareObjectHandles failed") + return fmt.Errorf("current process handle does not refer to the same original process: CompareObjectHandles failed: %w", err) + } + } + + return nil +} + +func (w *windowsWatcher) PID() int32 { + return w.pid +} + +type systemCaller interface { + // CloseHandle closes an open object handle. + CloseHandle(windows.Handle) error + + // CompareObjectHandles compares two object handles to determine if they + // refer to the same underlying kernel object + CompareObjectHandles(windows.Handle, windows.Handle) error + + // OpenProcess returns an open handle to the specified process id. + OpenProcess(int32) (windows.Handle, error) + + // GetProcessID retrieves the process identifier corresponding + // to the specified process handle. + GetProcessID(windows.Handle) (uint32, error) + + // GetExitCodeProcess retrieves the termination status of the + // specified process handle. + GetExitCodeProcess(windows.Handle, *uint32) error + + // IsCompareObjectHandlesFound returns true if the CompareObjectHandles + // function could be found in this Windows instance + IsCompareObjectHandlesFound() bool +} + +type systemCall struct { +} + +func (s *systemCall) CloseHandle(h windows.Handle) error { + return windows.CloseHandle(h) +} + +func (s *systemCall) IsCompareObjectHandlesFound() bool { + return isCompareObjectHandlesFound() +} + +func (s *systemCall) CompareObjectHandles(h1, h2 windows.Handle) error { + return compareObjectHandles(h1, h2) +} + +func (s *systemCall) GetExitCodeProcess(h windows.Handle, exitCode *uint32) error { + return windows.GetExitCodeProcess(h, exitCode) +} + +func (s *systemCall) GetProcessID(h windows.Handle) (uint32, error) { + return windows.GetProcessId(h) +} + +func (s *systemCall) OpenProcess(pid int32) (handle windows.Handle, err error) { + return windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, uint32(pid)) +} diff --git a/pkg/peertracker/tracker_windows_test.go b/pkg/peertracker/tracker_windows_test.go new file mode 100644 index 0000000..5904dd7 --- /dev/null +++ b/pkg/peertracker/tracker_windows_test.go @@ -0,0 +1,228 @@ +//go:build windows + +package peertracker + +import ( + "errors" + "testing" + + "github.com/sirupsen/logrus" + logtest "github.com/sirupsen/logrus/hooks/test" + "github.com/spiffe/spire/pkg/common/telemetry" + "github.com/spiffe/spire/test/spiretest" + "github.com/stretchr/testify/require" + "golang.org/x/sys/windows" +) + +func TestWindowsTracker(t *testing.T) { + testCases := []struct { + name string + pid int32 + sc *fakeSystemCall + expectNewWatcherErr string + expectIsAliveErr string + expectLogs []spiretest.LogEntry + }{ + { + name: "success", + pid: 1000, + sc: &fakeSystemCall{ + exitCode: stillActive, + processID: 1000, + }, + }, + { + name: "idle process", + pid: 0, + expectNewWatcherErr: "caller is the Idle process", + sc: &fakeSystemCall{}, + }, + { + name: "system process", + pid: 4, + expectNewWatcherErr: "caller is the System process", + sc: &fakeSystemCall{}, + }, + { + name: "process mismatch", + pid: 65279, + expectNewWatcherErr: "process ID does not match with the caller", + sc: &fakeSystemCall{ + processID: 65276, + }, + }, + { + name: "compare object handle not found", + pid: 65279, + sc: &fakeSystemCall{ + processID: 65279, + exitCode: stillActive, + isCompareObjectHandlesNotFound: true, + }, + }, + { + name: "get process id error", + pid: 1000, + expectNewWatcherErr: "error getting process id from handle: get process id error", + sc: &fakeSystemCall{ + processID: 1000, + getProcessIDErr: errors.New("get process id error"), + }, + }, + { + name: "invalid handle", + pid: 1000, + sc: &fakeSystemCall{ + processID: 1000, + handle: windows.InvalidHandle, + }, + expectIsAliveErr: "caller is no longer being watched", + expectLogs: []spiretest.LogEntry{ + { + Level: logrus.WarnLevel, + Message: "Caller is no longer being watched", + Data: logrus.Fields{ + telemetry.PID: "1000", + }, + }, + }, + }, + { + name: "get exit code process error", + pid: 1000, + sc: &fakeSystemCall{ + exitCode: stillActive, + getExitCodeProcessErr: errors.New("get exit code process error"), + processID: 1000, + }, + expectIsAliveErr: "error getting exit code from the process: get exit code process error", + }, + { + name: "process not active", + pid: 1000, + sc: &fakeSystemCall{ + exitCode: 100, + processID: 1000, + }, + expectIsAliveErr: "caller exit detected: exit code: 100", + expectLogs: []spiretest.LogEntry{ + { + Level: logrus.WarnLevel, + Message: "Caller is not running anymore", + Data: logrus.Fields{ + logrus.ErrorKey: "caller exit detected: exit code: 100", + telemetry.PID: "1000", + }, + }, + }, + }, + { + name: "compare object handles error", + pid: 1000, + sc: &fakeSystemCall{ + compareObjectHandlesErr: errors.New("compare object handles error"), + exitCode: stillActive, + processID: 1000, + }, + expectIsAliveErr: "current process handle does not refer to the same original process: CompareObjectHandles failed: compare object handles error", + expectLogs: []spiretest.LogEntry{ + { + Level: logrus.WarnLevel, + Message: "Current process handle does not refer to the same original process: CompareObjectHandles failed", + Data: logrus.Fields{ + logrus.ErrorKey: "compare object handles error", + telemetry.PID: "1000", + }, + }, + }, + }, + { + name: "close handle error", + pid: 1000, + sc: &fakeSystemCall{ + closeHandleErr: errors.New("close handle error"), + exitCode: stillActive, + processID: 1000, + }, + expectLogs: []spiretest.LogEntry{ + { + Level: logrus.WarnLevel, + Message: "Could not close process handle in liveness check", + Data: logrus.Fields{ + logrus.ErrorKey: "close handle error", + telemetry.PID: "1000", + }, + }, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + log, logHook := logtest.NewNullLogger() + tracker := &windowsTracker{ + log: log, + sc: testCase.sc, + } + tracker.sc = testCase.sc + + // Exercise NewWatcher + w, err := tracker.NewWatcher(CallerInfo{PID: testCase.pid}) + if testCase.expectNewWatcherErr != "" { + require.Nil(t, w) + require.EqualError(t, err, testCase.expectNewWatcherErr) + return + } + require.NoError(t, err) + require.NotNil(t, w) + + // Exercise IsAlive + err = w.IsAlive() + if testCase.expectIsAliveErr != "" { + require.EqualError(t, err, testCase.expectIsAliveErr) + spiretest.AssertLogs(t, logHook.AllEntries(), testCase.expectLogs) + return + } + require.NoError(t, err) + spiretest.AssertLogs(t, logHook.AllEntries(), testCase.expectLogs) + }) + } +} + +type fakeSystemCall struct { + handle windows.Handle + exitCode uint32 + processID uint32 + + closeHandleErr error + compareObjectHandlesErr error + getExitCodeProcessErr error + getProcessIDErr error + openProcessErr error + isCompareObjectHandlesNotFound bool +} + +func (s *fakeSystemCall) CloseHandle(windows.Handle) error { + return s.closeHandleErr +} + +func (s *fakeSystemCall) CompareObjectHandles(windows.Handle, windows.Handle) error { + return s.compareObjectHandlesErr +} + +func (s *fakeSystemCall) GetExitCodeProcess(_ windows.Handle, exitCode *uint32) error { + *exitCode = s.exitCode + return s.getExitCodeProcessErr +} + +func (s *fakeSystemCall) GetProcessID(windows.Handle) (uint32, error) { + return s.processID, s.getProcessIDErr +} + +func (s *fakeSystemCall) OpenProcess(int32) (handle windows.Handle, err error) { + return s.handle, s.openProcessErr +} + +func (s *fakeSystemCall) IsCompareObjectHandlesFound() bool { + return !s.isCompareObjectHandlesNotFound +} diff --git a/pkg/peertracker/uds.go b/pkg/peertracker/uds.go new file mode 100644 index 0000000..bc05dbb --- /dev/null +++ b/pkg/peertracker/uds.go @@ -0,0 +1,32 @@ +package peertracker + +import ( + "net" +) + +func CallerFromUDSConn(conn net.Conn) (CallerInfo, error) { + var info CallerInfo + + unixConn, ok := conn.(*net.UnixConn) + if !ok { + return info, ErrInvalidConnection + } + + rawconn, err := unixConn.SyscallConn() + if err != nil { + return info, err + } + + ctrlErr := rawconn.Control(func(fd uintptr) { + info, err = getCallerInfoFromFileDescriptor(fd) + }) + if ctrlErr != nil { + return info, ctrlErr + } + if err != nil { + return info, err + } + + info.Addr = conn.RemoteAddr() + return info, nil +} diff --git a/pkg/peertracker/uds_bsd.go b/pkg/peertracker/uds_bsd.go new file mode 100644 index 0000000..8bcedc7 --- /dev/null +++ b/pkg/peertracker/uds_bsd.go @@ -0,0 +1,20 @@ +//go:build darwin || freebsd || netbsd || openbsd + +package peertracker + +import ( + "golang.org/x/sys/unix" +) + +func getCallerInfoFromFileDescriptor(fd uintptr) (CallerInfo, error) { + result, err := unix.GetsockoptInt(int(fd), 0, 0x002) // getsockopt(fd, SOL_LOCAL, LOCAL_PEERPID) + if err != nil { + return CallerInfo{}, err + } + + info := CallerInfo{ + PID: int32(result), + } + + return info, nil +} diff --git a/pkg/peertracker/uds_fallback.go b/pkg/peertracker/uds_fallback.go new file mode 100644 index 0000000..ee586c0 --- /dev/null +++ b/pkg/peertracker/uds_fallback.go @@ -0,0 +1,7 @@ +//go:build !linux && !darwin && !freebsd && !netbsd && !openbsd + +package peertracker + +func getCallerInfoFromFileDescriptor(uintptr) (CallerInfo, error) { + return CallerInfo{}, ErrUnsupportedPlatform +} diff --git a/pkg/peertracker/uds_linux.go b/pkg/peertracker/uds_linux.go new file mode 100644 index 0000000..1040362 --- /dev/null +++ b/pkg/peertracker/uds_linux.go @@ -0,0 +1,22 @@ +//go:build linux + +package peertracker + +import ( + "golang.org/x/sys/unix" +) + +func getCallerInfoFromFileDescriptor(fd uintptr) (CallerInfo, error) { + ucred, err := unix.GetsockoptUcred(int(fd), unix.SOL_SOCKET, unix.SO_PEERCRED) + if err != nil { + return CallerInfo{}, err + } + + info := CallerInfo{ + PID: ucred.Pid, + UID: ucred.Uid, + GID: ucred.Gid, + } + + return info, nil +}