mirror of
https://github.com/vee1e/runtimeclass-debugger.git
synced 2026-09-01 18:27:58 +00:00
The TLS retry was gated on hasCerts (cert-file and key-file both set), so a TLS-only MetaServer with a CA-signed server certificate was never tried when the caller passed only --ca-file. The tool then reported AuthRequiredError and recommended the cert flags that would not help. Run the TLS retry whenever a CA file or client certificates are provided, and report a distinct reason when the handshake fails with a CA-only setup.
307 lines
8.6 KiB
Go
307 lines
8.6 KiB
Go
package metaserver
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"errors"
|
|
"math/big"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func runtimeClassList(names ...string) []byte {
|
|
items := []map[string]interface{}{}
|
|
for _, n := range names {
|
|
items = append(items, map[string]interface{}{
|
|
"apiVersion": "node.k8s.io/v1",
|
|
"kind": "RuntimeClass",
|
|
"metadata": map[string]interface{}{"name": n},
|
|
"handler": n,
|
|
})
|
|
}
|
|
b, _ := json.Marshal(map[string]interface{}{
|
|
"apiVersion": "node.k8s.io/v1",
|
|
"kind": "RuntimeClassList",
|
|
"metadata": map[string]interface{}{},
|
|
"items": items,
|
|
})
|
|
return b
|
|
}
|
|
|
|
func newClient(serverURL string) *Client {
|
|
return &Client{BaseURL: serverURL, Timeout: 5 * time.Second}
|
|
}
|
|
|
|
func TestListRuntimeClasses(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/apis/node.k8s.io/v1/runtimeclasses" {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
_, _ = w.Write(runtimeClassList("runsc", "kata"))
|
|
}))
|
|
defer server.Close()
|
|
|
|
classes, err := newClient(server.URL).ListRuntimeClasses(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(classes) != 2 || classes[0].Name != "kata" || classes[1].Name != "runsc" {
|
|
t.Fatalf("expected sorted [kata runsc], got %+v", classes)
|
|
}
|
|
}
|
|
|
|
func TestListEmptyList(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write(runtimeClassList())
|
|
}))
|
|
defer server.Close()
|
|
|
|
classes, err := newClient(server.URL).ListRuntimeClasses(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(classes) != 0 {
|
|
t.Fatalf("expected no classes, got %+v", classes)
|
|
}
|
|
}
|
|
|
|
func TestServerErrorNotAuthNotUnreachable(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
_, _ = w.Write([]byte("boom"))
|
|
}))
|
|
defer server.Close()
|
|
|
|
_, err := newClient(server.URL).ListRuntimeClasses(context.Background())
|
|
if err == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
var authErr *AuthRequiredError
|
|
var unreachErr *UnreachableError
|
|
if errors.As(err, &authErr) || errors.As(err, &unreachErr) {
|
|
t.Fatalf("expected plain error, got %T: %v", err, err)
|
|
}
|
|
}
|
|
|
|
func TestAuthRequiredOnHTTPStatus(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusForbidden)
|
|
}))
|
|
defer server.Close()
|
|
|
|
_, err := newClient(server.URL).ListRuntimeClasses(context.Background())
|
|
var authErr *AuthRequiredError
|
|
if !errors.As(err, &authErr) {
|
|
t.Fatalf("expected AuthRequiredError, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestUnreachable(t *testing.T) {
|
|
l, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
addr := l.Addr().String()
|
|
l.Close()
|
|
|
|
_, err = newClient("http://" + addr).ListRuntimeClasses(context.Background())
|
|
var unreachErr *UnreachableError
|
|
if !errors.As(err, &unreachErr) {
|
|
t.Fatalf("expected UnreachableError, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestMalformedResponse(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write([]byte("not json"))
|
|
}))
|
|
defer server.Close()
|
|
|
|
_, err := newClient(server.URL).ListRuntimeClasses(context.Background())
|
|
if err == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
}
|
|
|
|
type testPKI struct {
|
|
caPath string
|
|
clientCertPath string
|
|
clientKeyPath string
|
|
serverCert tls.Certificate
|
|
}
|
|
|
|
func generateTestPKI(t *testing.T) *testPKI {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
now := time.Now()
|
|
|
|
caKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
caTmpl := &x509.Certificate{
|
|
SerialNumber: big.NewInt(1),
|
|
Subject: pkix.Name{CommonName: "runtimeclass-debugger-test-ca"},
|
|
NotBefore: now.Add(-time.Hour),
|
|
NotAfter: now.Add(24 * time.Hour),
|
|
IsCA: true,
|
|
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature,
|
|
BasicConstraintsValid: true,
|
|
}
|
|
caDER, err := x509.CreateCertificate(rand.Reader, caTmpl, caTmpl, &caKey.PublicKey, caKey)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
caCert, err := x509.ParseCertificate(caDER)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
caPath := filepath.Join(dir, "ca.crt")
|
|
if err := os.WriteFile(caPath, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caDER}), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
signLeaf := func(cn string, ip net.IP, usage []x509.ExtKeyUsage) (tls.Certificate, string, string) {
|
|
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tmpl := &x509.Certificate{
|
|
SerialNumber: big.NewInt(2),
|
|
Subject: pkix.Name{CommonName: cn},
|
|
NotBefore: now.Add(-time.Hour),
|
|
NotAfter: now.Add(24 * time.Hour),
|
|
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
|
ExtKeyUsage: usage,
|
|
}
|
|
if ip != nil {
|
|
tmpl.IPAddresses = []net.IP{ip}
|
|
}
|
|
der, err := x509.CreateCertificate(rand.Reader, tmpl, caCert, &key.PublicKey, caKey)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
|
|
keyDER, err := x509.MarshalPKCS8PrivateKey(key)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER})
|
|
certPath := filepath.Join(dir, cn+".crt")
|
|
keyPath := filepath.Join(dir, cn+".key")
|
|
if err := os.WriteFile(certPath, certPEM, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(keyPath, keyPEM, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cert, err := tls.X509KeyPair(certPEM, keyPEM)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return cert, certPath, keyPath
|
|
}
|
|
|
|
serverCert, _, _ := signLeaf("metaserver", net.ParseIP("127.0.0.1"), []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth})
|
|
_, clientCertPath, clientKeyPath := signLeaf("client", nil, []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth})
|
|
return &testPKI{caPath: caPath, clientCertPath: clientCertPath, clientKeyPath: clientKeyPath, serverCert: serverCert}
|
|
}
|
|
|
|
func newTLSMetaServer(t *testing.T, pki *testPKI) *httptest.Server {
|
|
t.Helper()
|
|
caPEM, err := os.ReadFile(pki.caPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pool := x509.NewCertPool()
|
|
pool.AppendCertsFromPEM(caPEM)
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if len(r.TLS.PeerCertificates) == 0 {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
_, _ = w.Write(runtimeClassList("kata"))
|
|
})
|
|
server := httptest.NewUnstartedServer(handler)
|
|
server.TLS = &tls.Config{
|
|
Certificates: []tls.Certificate{pki.serverCert},
|
|
ClientCAs: pool,
|
|
ClientAuth: tls.VerifyClientCertIfGiven,
|
|
MinVersion: tls.VersionTLS12,
|
|
}
|
|
server.StartTLS()
|
|
return server
|
|
}
|
|
|
|
func TestAuthTLSWithoutCertsWarns(t *testing.T) {
|
|
server := newTLSMetaServer(t, generateTestPKI(t))
|
|
defer server.Close()
|
|
|
|
_, err := newClient(server.URL).ListRuntimeClasses(context.Background())
|
|
var authErr *AuthRequiredError
|
|
if !errors.As(err, &authErr) {
|
|
t.Fatalf("expected AuthRequiredError, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAuthTLSWithCertsSucceeds(t *testing.T) {
|
|
pki := generateTestPKI(t)
|
|
server := newTLSMetaServer(t, pki)
|
|
defer server.Close()
|
|
|
|
client := &Client{
|
|
BaseURL: server.URL,
|
|
Timeout: 5 * time.Second,
|
|
CertFile: pki.clientCertPath,
|
|
KeyFile: pki.clientKeyPath,
|
|
CAFile: pki.caPath,
|
|
}
|
|
classes, err := client.ListRuntimeClasses(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(classes) != 1 || classes[0].Name != "kata" {
|
|
t.Fatalf("expected [kata], got %+v", classes)
|
|
}
|
|
}
|
|
|
|
// TestTLSWithCAOnlySucceeds covers a TLS-only MetaServer whose certificate
|
|
// is signed by a CA the caller trusts: no client certificate is needed, and
|
|
// passing --ca-file alone must trigger the TLS retry.
|
|
func TestTLSWithCAOnlySucceeds(t *testing.T) {
|
|
pki := generateTestPKI(t)
|
|
server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write(runtimeClassList("kata"))
|
|
}))
|
|
server.TLS = &tls.Config{
|
|
Certificates: []tls.Certificate{pki.serverCert},
|
|
MinVersion: tls.VersionTLS12,
|
|
}
|
|
server.StartTLS()
|
|
defer server.Close()
|
|
|
|
client := &Client{
|
|
BaseURL: server.URL,
|
|
Timeout: 5 * time.Second,
|
|
CAFile: pki.caPath,
|
|
}
|
|
classes, err := client.ListRuntimeClasses(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(classes) != 1 || classes[0].Name != "kata" {
|
|
t.Fatalf("expected [kata], got %+v", classes)
|
|
}
|
|
}
|