Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix allocator service tls auth for C# client and add a C# sample #1514

Merged
merged 1 commit into from
May 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 31 additions & 11 deletions cmd/allocator/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,22 +185,42 @@ func (h *serviceHandler) getServerOptions() []grpc.ServerOption {
}

cfg := &tls.Config{
Certificates: []tls.Certificate{tlsCer},
ClientAuth: tls.RequireAndVerifyClientCert,
GetConfigForClient: func(*tls.ClientHelloInfo) (*tls.Config, error) {
h.certMutex.RLock()
defer h.certMutex.RUnlock()
return &tls.Config{
Certificates: []tls.Certificate{tlsCer},
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: h.caCertPool,
}, nil
},
Certificates: []tls.Certificate{tlsCer},
ClientAuth: tls.RequireAnyClientCert,
VerifyPeerCertificate: h.verifyClientCertificate,
}
// Add options for creds and OpenCensus stats handler to enable stats and tracing.
return []grpc.ServerOption{grpc.Creds(credentials.NewTLS(cfg)), grpc.StatsHandler(&ocgrpc.ServerHandler{})}
}

// verifyClientCertificate verifies that the client certificate is accepted
// This method is used as GetConfigForClient is cross lang incompatible.
func (h *serviceHandler) verifyClientCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
opts := x509.VerifyOptions{
Roots: h.caCertPool,
CurrentTime: time.Now(),
Intermediates: x509.NewCertPool(),
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
}

for _, cert := range rawCerts[1:] {
opts.Intermediates.AppendCertsFromPEM(cert)
}

c, err := x509.ParseCertificate(rawCerts[0])
if err != nil {
return errors.New("bad client certificate: " + err.Error())
}

h.certMutex.RLock()
defer h.certMutex.RUnlock()
_, err = c.Verify(opts)
if err != nil {
return errors.New("failed to verify client certificate: " + err.Error())
}
return nil
}

// Set up our client which we will use to call the API
func getClients() (*kubernetes.Clientset, *versioned.Clientset, error) {
// Create the in-cluster config
Expand Down
34 changes: 34 additions & 0 deletions cmd/allocator/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ package main

import (
"context"
"crypto/x509"
"encoding/pem"
"io/ioutil"
"net/http"
"os"
Expand Down Expand Up @@ -137,6 +139,38 @@ func TestBadReturnType(t *testing.T) {
assert.Contains(t, st.Message(), "internal server error")
}

func TestVerifyClientCertificateSucceeds(t *testing.T) {
t.Parallel()

crt := []byte(clientCert)
certPool := x509.NewCertPool()
assert.True(t, certPool.AppendCertsFromPEM(crt))

h := serviceHandler{
caCertPool: certPool,
}

block, _ := pem.Decode(crt)
input := [][]byte{block.Bytes}
assert.Nil(t, h.verifyClientCertificate(input, nil),
"verifyClientCertificate failed.")
}

func TestVerifyClientCertificateFails(t *testing.T) {
t.Parallel()

crt := []byte(clientCert)
certPool := x509.NewCertPool()
h := serviceHandler{
caCertPool: certPool,
}

block, _ := pem.Decode(crt)
input := [][]byte{block.Bytes}
assert.Error(t, h.verifyClientCertificate(input, nil),
"verifyClientCertificate() succeeded, expected error.")
}

func TestGettingCaCert(t *testing.T) {
t.Parallel()

Expand Down
45 changes: 45 additions & 0 deletions examples/allocator-client-csharp/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using System.IO;
using Grpc.Core;
using V1Alpha1;
using System.Net.Http;

namespace AllocatorClient
{
class Program
{
static async Task Main(string[] args)
{
if (args.Length < 6) {
throw new Exception("Arguments are missing. Expecting: <private key> <public key> <server CA> <external IP> <namepace> <enable multi-cluster>");
}

string clientKey = File.ReadAllText(args[0]);
string clientCert = File.ReadAllText(args[1]);
string serverCa = File.ReadAllText(args[2]);
string externalIp = args[3];
string namespaceArg = args[4];
bool multicluster = bool.Parse(args[5]);

var creds = new SslCredentials(serverCa, new KeyCertificatePair(clientCert, clientKey));
var channel = new Channel(externalIp + ":443", creds);
var client = new AllocationService.AllocationServiceClient(channel);

try {
var response = await client.AllocateAsync(new AllocationRequest {
Namespace = namespaceArg,
MultiClusterSetting = new V1Alpha1.MultiClusterSetting {
Enabled = multicluster,
}
});
Console.WriteLine(response);
}
catch(RpcException e)
{
Console.WriteLine($"gRPC error: {e}");
}
}
}
}
19 changes: 19 additions & 0 deletions examples/allocator-client-csharp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# A sample Allocator service C# client
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we link this somewhere, such as here?
https://agones.dev/site/docs/advanced/allocator-service/#send-allocation-request

(and wrapped in a feature code block)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing it out.
PTAL. I added a note.


This sample serves as a gRPC C# client sample code for agones-allocator gRPC service.

Follow instructions in [Allocator Service](https://agones.dev/site/docs/advanced/allocator-service) to set up client and server certificate.

Run the following to allocate a game server:
```
#!/bin/bash

NAMESPACE=default # replace with any namespace
EXTERNAL_IP=`kubectl get services agones-allocator -n agones-system -o jsonpath='{.status.loadBalancer.ingress[0].ip}'`
KEY_FILE=client.key
CERT_FILE=client.crt
TLS_CA_FILE=ca.crt
MULTICLUSTER_ENABLED=false

dotnet run $KEY_FILE $CERT_FILE $TLS_CA_FILE $EXTERNAL_IP $NAMESPACE $MULTICLUSTER_ENABLED
```
19 changes: 19 additions & 0 deletions examples/allocator-client-csharp/allocator-client-csharp.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<RootNamespace>AllocatorClient</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Google.Api.CommonProtos" Version="1.7.0" />
<PackageReference Include="Google.Protobuf" Version="3.11.2" />
<PackageReference Include="Grpc" Version="2.26.0" />
<PackageReference Include="Grpc.Core" Version="2.26.0" />
<PackageReference Include="Grpc.Tools" Version="2.26.0" PrivateAssets="all"/>
</ItemGroup>

<ItemGroup>
<Protobuf Include="allocation.proto" ProtoRoot="../../proto/allocation/v1alpha1;../../proto/googleapis" GrpcServices="Client"/>
</ItemGroup>
</Project>
2 changes: 1 addition & 1 deletion examples/allocator-client/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func main() {
cacertFile := flag.String("cacert", "missing cacert", "the CA cert file for server signing certificate in PEM format")
externalIP := flag.String("ip", "missing external IP", "the external IP for allocator server")
namespace := flag.String("namespace", "default", "the game server kubernetes namespace")
multicluster := flag.Bool("multicluster", false, "enabling")
multicluster := flag.Bool("multicluster", false, "set to true to enable the multi-cluster allocation")

flag.Parse()

Expand Down
7 changes: 7 additions & 0 deletions site/content/en/docs/Advanced/allocator-service.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,14 @@ kubectl get pods -n agones-system -o=name | grep agones-allocator | xargs kubect

## Send allocation request

{{% feature expiryVersion="1.6.0" %}}
Now the service is ready to accept requests from the client with the generated certificates. Create a [fleet](https://agones.dev/site/docs/getting-started/create-fleet/#1-create-a-fleet) and send a gRPC request to agones-allocator by providing the namespace to which the fleet is deployed. You can find the gRPC sample for sending allocation request at {{< ghlink href="examples/allocator-client/main.go" >}}allocator-client sample{{< /ghlink >}}.
{{% /feature %}}

{{% feature publishVersion="1.6.0" %}}
Now the service is ready to accept requests from the client with the generated certificates. Create a [fleet](https://agones.dev/site/docs/getting-started/create-fleet/#1-create-a-fleet) and send a gRPC request to agones-allocator. To start, take a look at the allocation gRPC client examples in {{< ghlink href="examples/allocator-client/main.go" >}}golang{{< /ghlink >}} and {{< ghlink href="examples/allocator-client-csharp/Program.cs" >}}C#{{< /ghlink >}} languages. In the following, the {{< ghlink href="examples/allocator-client/main.go" >}}golang gRPC client example{{< /ghlink >}} is used to allocate a Game Server in the default namespace.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want this to show up on the current documentation, or only after the next release?

If we want it after the next release, we should create publish and expire versions of this paragraph:
https://agones.dev/site/docs/contribute/#within-a-page

Other than that, this looks good to go 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion. Makes sense. I added the feature wrapper to expose the content with 1.6 release.

{{% /feature %}}


```bash
#!/bin/bash
Expand Down