From 6fd5a8f7c984cbf30cc7e3ab0c5b914f54f24446 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Mart=C3=ADnez=20Fay=C3=B3?= Date: Wed, 13 Apr 2022 18:14:53 -0300 Subject: [PATCH] Add support for Named Pipes in Windows (#190) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Introduce the WithNamedPipeName option to support named pipes (Windows only) Signed-off-by: Agustín Martínez Fayó --- v2/go.mod | 1 + v2/go.sum | 11 +++- .../fakeworkloadapi/workload_api_windows.go | 44 +++++++++++++++ v2/workloadapi/addr.go | 4 ++ v2/workloadapi/client.go | 15 +----- v2/workloadapi/client_posix.go | 29 ++++++++++ v2/workloadapi/client_windows.go | 50 +++++++++++++++++ v2/workloadapi/client_windows_test.go | 53 +++++++++++++++++++ v2/workloadapi/option.go | 7 +-- v2/workloadapi/option_windows.go | 12 +++++ 10 files changed, 207 insertions(+), 19 deletions(-) create mode 100644 v2/internal/test/fakeworkloadapi/workload_api_windows.go create mode 100644 v2/workloadapi/client_posix.go create mode 100644 v2/workloadapi/client_windows.go create mode 100644 v2/workloadapi/client_windows_test.go create mode 100644 v2/workloadapi/option_windows.go diff --git a/v2/go.mod b/v2/go.mod index 82877ce4..f9e6870e 100644 --- a/v2/go.mod +++ b/v2/go.mod @@ -3,6 +3,7 @@ module github.com/spiffe/go-spiffe/v2 go 1.13 require ( + github.com/Microsoft/go-winio v0.5.2 github.com/stretchr/testify v1.7.1 github.com/zeebo/errs v1.2.2 google.golang.org/grpc v1.33.2 diff --git a/v2/go.sum b/v2/go.sum index 34c6a28a..ff859824 100644 --- a/v2/go.sum +++ b/v2/go.sum @@ -1,11 +1,14 @@ 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/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 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/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/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= @@ -33,7 +36,9 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/zeebo/errs v1.2.2 h1:5NFypMTuSdoySVTqlNs1dEoU21QVamMQJxW/Fii5O7g= @@ -57,8 +62,10 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ 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/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/v2/internal/test/fakeworkloadapi/workload_api_windows.go b/v2/internal/test/fakeworkloadapi/workload_api_windows.go new file mode 100644 index 00000000..b06b165a --- /dev/null +++ b/v2/internal/test/fakeworkloadapi/workload_api_windows.go @@ -0,0 +1,44 @@ +//go:build windows +// +build windows + +package fakeworkloadapi + +import ( + "fmt" + "math/rand" + "testing" + "time" + + "github.com/Microsoft/go-winio" + "github.com/spiffe/go-spiffe/v2/proto/spiffe/workload" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" +) + +func NewWithNamedPipeListener(tb testing.TB) *WorkloadAPI { + w := &WorkloadAPI{ + x509Chans: make(map[chan *workload.X509SVIDResponse]struct{}), + jwtBundlesChans: make(map[chan *workload.JWTBundlesResponse]struct{}), + } + + listener, err := winio.ListenPipe(fmt.Sprintf(`\\.\pipe\go-spiffe-test-pipe-%x`, rand.Uint64()), nil) + require.NoError(tb, err) + + server := grpc.NewServer() + workload.RegisterSpiffeWorkloadAPIServer(server, &workloadAPIWrapper{w: w}) + + w.wg.Add(1) + go func() { + defer w.wg.Done() + _ = server.Serve(listener) + }() + + w.addr = listener.Addr().String() + tb.Logf("WorkloadAPI address: %s", w.addr) + w.server = server + return w +} + +func init() { + rand.Seed(time.Now().UnixNano()) +} diff --git a/v2/workloadapi/addr.go b/v2/workloadapi/addr.go index 8a200f3e..6fb58ca5 100644 --- a/v2/workloadapi/addr.go +++ b/v2/workloadapi/addr.go @@ -17,6 +17,10 @@ func GetDefaultAddress() (string, bool) { return os.LookupEnv(SocketEnv) } +// ValidateAddress validates that the provided address +// can be parsed to a gRPC target string for dialing +// a Workload API endpoint exposed as either a Unix +// Domain Socket or TCP socket. func ValidateAddress(addr string) error { _, err := parseTargetFromAddr(addr) return err diff --git a/v2/workloadapi/client.go b/v2/workloadapi/client.go index 1e5fba34..fdc467e6 100644 --- a/v2/workloadapi/client.go +++ b/v2/workloadapi/client.go @@ -218,22 +218,9 @@ func (c *Client) ValidateJWTSVID(ctx context.Context, token, audience string) (* return jwtsvid.ParseInsecure(token, []string{audience}) } -func (c *Client) setAddress() error { - if c.config.address == "" { - var ok bool - c.config.address, ok = GetDefaultAddress() - if !ok { - return errors.New("workload endpoint socket address is not configured") - } - } - - var err error - c.config.address, err = parseTargetFromAddr(c.config.address) - return err -} - func (c *Client) newConn(ctx context.Context) (*grpc.ClientConn, error) { c.config.dialOptions = append(c.config.dialOptions, grpc.WithInsecure()) + c.appendDialOptionsOS() return grpc.DialContext(ctx, c.config.address, c.config.dialOptions...) } diff --git a/v2/workloadapi/client_posix.go b/v2/workloadapi/client_posix.go new file mode 100644 index 00000000..4a0ad142 --- /dev/null +++ b/v2/workloadapi/client_posix.go @@ -0,0 +1,29 @@ +//go:build !windows +// +build !windows + +package workloadapi + +import "errors" + +// appendDialOptionsOS appends OS specific dial options +func (c *Client) appendDialOptionsOS() { + // No options to add in this platform +} +func (c *Client) setAddress() error { + if c.config.namedPipeName != "" { + // Purely defensive. This should never happen. + return errors.New("named pipes not supported in this platform") + } + + if c.config.address == "" { + var ok bool + c.config.address, ok = GetDefaultAddress() + if !ok { + return errors.New("workload endpoint socket address is not configured") + } + } + + var err error + c.config.address, err = parseTargetFromAddr(c.config.address) + return err +} diff --git a/v2/workloadapi/client_windows.go b/v2/workloadapi/client_windows.go new file mode 100644 index 00000000..0388c829 --- /dev/null +++ b/v2/workloadapi/client_windows.go @@ -0,0 +1,50 @@ +//go:build windows +// +build windows + +package workloadapi + +import ( + "errors" + "path/filepath" + + "github.com/Microsoft/go-winio" + "google.golang.org/grpc" +) + +// appendDialOptionsOS appends OS specific dial options +func (c *Client) appendDialOptionsOS() { + if c.config.namedPipeName != "" { + // Use the dialer to connect to named pipes only if a named pipe + // is defined (i.e. WithNamedPipeName is used). + c.config.dialOptions = append(c.config.dialOptions, grpc.WithContextDialer(winio.DialPipeContext)) + } +} + +func (c *Client) setAddress() error { + var err error + if c.config.namedPipeName != "" { + if c.config.address != "" { + return errors.New("only one of WithAddr or WithNamedPipeName options can be used, not both") + } + c.config.address = namedPipeTarget(c.config.namedPipeName) + return nil + } + + if c.config.address == "" { + var ok bool + c.config.address, ok = GetDefaultAddress() + if !ok { + return errors.New("workload endpoint socket address is not configured") + } + } + + c.config.address, err = parseTargetFromAddr(c.config.address) + return err +} + +// namedPipeTarget returns a target string suitable for +// dialing the endpoint address based on the provided +// pipe name. +func namedPipeTarget(pipeName string) string { + return `\\.\` + filepath.Join("pipe", pipeName) +} diff --git a/v2/workloadapi/client_windows_test.go b/v2/workloadapi/client_windows_test.go new file mode 100644 index 00000000..0a9dfc6b --- /dev/null +++ b/v2/workloadapi/client_windows_test.go @@ -0,0 +1,53 @@ +//go:build windows +// +build windows + +package workloadapi + +import ( + "context" + "strings" + "testing" + + "github.com/spiffe/go-spiffe/v2/internal/test" + "github.com/spiffe/go-spiffe/v2/internal/test/fakeworkloadapi" + "github.com/stretchr/testify/require" +) + +func TestWithNamedPipeName(t *testing.T) { + ca := test.NewCA(t, td) + wl := fakeworkloadapi.NewWithNamedPipeListener(t) + defer wl.Stop() + + pipeName := getPipeName(wl.Addr()) + c, err := New(context.Background(), WithNamedPipeName(pipeName)) + require.NoError(t, err) + defer c.Close() + require.Equal(t, pipeName, c.config.namedPipeName) + + resp := &fakeworkloadapi.X509SVIDResponse{ + Bundle: ca.X509Bundle(), + SVIDs: makeX509SVIDs(ca, fooID, barID), + } + wl.SetX509SVIDResponse(resp) + svid, err := c.FetchX509SVID(context.Background()) + require.NoError(t, err) + assertX509SVID(t, svid, fooID, resp.SVIDs[0].Certificates) +} + +func TestWithNamedPipeNameError(t *testing.T) { + wl := fakeworkloadapi.NewWithNamedPipeListener(t) + defer wl.Stop() + + c, err := New(context.Background(), WithNamedPipeName("ohno")) + require.NoError(t, err) + defer c.Close() + + wl.SetX509SVIDResponse(&fakeworkloadapi.X509SVIDResponse{}) + _, err = c.FetchX509SVID(context.Background()) + require.Error(t, err) + require.Contains(t, err.Error(), `ohno: The system cannot find the file specified`) +} + +func getPipeName(s string) string { + return strings.TrimPrefix(s, `\\.\pipe`) +} diff --git a/v2/workloadapi/option.go b/v2/workloadapi/option.go index cdfcf16a..00cab7d1 100644 --- a/v2/workloadapi/option.go +++ b/v2/workloadapi/option.go @@ -81,9 +81,10 @@ type BundleSourceOption interface { } type clientConfig struct { - address string - dialOptions []grpc.DialOption - log logger.Logger + address string + namedPipeName string + dialOptions []grpc.DialOption + log logger.Logger } type clientOption func(*clientConfig) diff --git a/v2/workloadapi/option_windows.go b/v2/workloadapi/option_windows.go new file mode 100644 index 00000000..c06e5338 --- /dev/null +++ b/v2/workloadapi/option_windows.go @@ -0,0 +1,12 @@ +//go:build windows +// +build windows + +package workloadapi + +// WithNamedPipeName provides a Pipe Name for the Workload API +// endpoint in the form \\.\pipe\. +func WithNamedPipeName(pipeName string) ClientOption { + return clientOption(func(c *clientConfig) { + c.namedPipeName = pipeName + }) +}