mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-10-31 00:01:40 +01:00 
			
		
		
		
	cmd/containerboot: manage HA Ingress TLS certs from containerboot When ran as HA Ingress node, containerboot now can determine whether it should manage TLS certs for the HA Ingress replicas and call the LocalAPI cert endpoint to ensure initial issuance and renewal of the shared TLS certs. Updates tailscale/corp#24795 Signed-off-by: Irbe Krumina <irbe@tailscale.com>
		
			
				
	
	
		
			272 lines
		
	
	
		
			5.4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			272 lines
		
	
	
		
			5.4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright (c) Tailscale Inc & AUTHORS
 | |
| // SPDX-License-Identifier: BSD-3-Clause
 | |
| 
 | |
| //go:build linux
 | |
| 
 | |
| package main
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"os"
 | |
| 	"path/filepath"
 | |
| 	"testing"
 | |
| 
 | |
| 	"github.com/google/go-cmp/cmp"
 | |
| 	"tailscale.com/client/local"
 | |
| 	"tailscale.com/ipn"
 | |
| 	"tailscale.com/kube/kubetypes"
 | |
| )
 | |
| 
 | |
| func TestUpdateServeConfig(t *testing.T) {
 | |
| 	tests := []struct {
 | |
| 		name       string
 | |
| 		sc         *ipn.ServeConfig
 | |
| 		certDomain string
 | |
| 		wantCall   bool
 | |
| 	}{
 | |
| 		{
 | |
| 			name: "no_https_no_cert_domain",
 | |
| 			sc: &ipn.ServeConfig{
 | |
| 				TCP: map[uint16]*ipn.TCPPortHandler{
 | |
| 					80: {HTTP: true},
 | |
| 				},
 | |
| 			},
 | |
| 			certDomain: kubetypes.ValueNoHTTPS, // tailnet has HTTPS disabled
 | |
| 			wantCall:   true,                   // should set serve config as it doesn't have HTTPS endpoints
 | |
| 		},
 | |
| 		{
 | |
| 			name: "https_with_cert_domain",
 | |
| 			sc: &ipn.ServeConfig{
 | |
| 				TCP: map[uint16]*ipn.TCPPortHandler{
 | |
| 					443: {HTTPS: true},
 | |
| 				},
 | |
| 				Web: map[ipn.HostPort]*ipn.WebServerConfig{
 | |
| 					"${TS_CERT_DOMAIN}:443": {
 | |
| 						Handlers: map[string]*ipn.HTTPHandler{
 | |
| 							"/": {Proxy: "http://10.0.1.100:8080"},
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			certDomain: "test-node.tailnet.ts.net",
 | |
| 			wantCall:   true,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "https_without_cert_domain",
 | |
| 			sc: &ipn.ServeConfig{
 | |
| 				TCP: map[uint16]*ipn.TCPPortHandler{
 | |
| 					443: {HTTPS: true},
 | |
| 				},
 | |
| 			},
 | |
| 			certDomain: kubetypes.ValueNoHTTPS,
 | |
| 			wantCall:   false, // incorrect configuration- should not set serve config
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, tt := range tests {
 | |
| 		t.Run(tt.name, func(t *testing.T) {
 | |
| 			fakeLC := &fakeLocalClient{}
 | |
| 			err := updateServeConfig(context.Background(), tt.sc, tt.certDomain, fakeLC)
 | |
| 			if err != nil {
 | |
| 				t.Errorf("updateServeConfig() error = %v", err)
 | |
| 			}
 | |
| 			if fakeLC.setServeCalled != tt.wantCall {
 | |
| 				t.Errorf("SetServeConfig() called = %v, want %v", fakeLC.setServeCalled, tt.wantCall)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestReadServeConfig(t *testing.T) {
 | |
| 	tests := []struct {
 | |
| 		name       string
 | |
| 		gotSC      string
 | |
| 		certDomain string
 | |
| 		wantSC     *ipn.ServeConfig
 | |
| 		wantErr    bool
 | |
| 	}{
 | |
| 		{
 | |
| 			name: "empty_file",
 | |
| 		},
 | |
| 		{
 | |
| 			name: "valid_config_with_cert_domain_placeholder",
 | |
| 			gotSC: `{
 | |
| 				"TCP": {
 | |
| 					"443": {
 | |
| 						"HTTPS": true
 | |
| 					}
 | |
| 				},
 | |
| 				"Web": {
 | |
| 					"${TS_CERT_DOMAIN}:443": {
 | |
| 					"Handlers": {
 | |
| 						"/api": {
 | |
| 							"Proxy": "https://10.2.3.4/api"
 | |
| 						}}}}}`,
 | |
| 			certDomain: "example.com",
 | |
| 			wantSC: &ipn.ServeConfig{
 | |
| 				TCP: map[uint16]*ipn.TCPPortHandler{
 | |
| 					443: {
 | |
| 						HTTPS: true,
 | |
| 					},
 | |
| 				},
 | |
| 				Web: map[ipn.HostPort]*ipn.WebServerConfig{
 | |
| 					ipn.HostPort("example.com:443"): {
 | |
| 						Handlers: map[string]*ipn.HTTPHandler{
 | |
| 							"/api": {
 | |
| 								Proxy: "https://10.2.3.4/api",
 | |
| 							},
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "valid_config_for_http_proxy",
 | |
| 			gotSC: `{
 | |
| 				"TCP": {
 | |
| 					"80": {
 | |
| 						"HTTP": true
 | |
| 					}
 | |
| 				}}`,
 | |
| 			wantSC: &ipn.ServeConfig{
 | |
| 				TCP: map[uint16]*ipn.TCPPortHandler{
 | |
| 					80: {
 | |
| 						HTTP: true,
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "config_without_cert_domain",
 | |
| 			gotSC: `{
 | |
| 				"TCP": {
 | |
| 					"443": {
 | |
| 						"HTTPS": true
 | |
| 					}
 | |
| 				},
 | |
| 				"Web": {
 | |
| 					"localhost:443": {
 | |
| 					"Handlers": {
 | |
| 						"/api": {
 | |
| 							"Proxy": "https://10.2.3.4/api"
 | |
| 						}}}}}`,
 | |
| 			certDomain: "",
 | |
| 			wantErr:    false,
 | |
| 			wantSC: &ipn.ServeConfig{
 | |
| 				TCP: map[uint16]*ipn.TCPPortHandler{
 | |
| 					443: {
 | |
| 						HTTPS: true,
 | |
| 					},
 | |
| 				},
 | |
| 				Web: map[ipn.HostPort]*ipn.WebServerConfig{
 | |
| 					ipn.HostPort("localhost:443"): {
 | |
| 						Handlers: map[string]*ipn.HTTPHandler{
 | |
| 							"/api": {
 | |
| 								Proxy: "https://10.2.3.4/api",
 | |
| 							},
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name:    "invalid_json",
 | |
| 			gotSC:   "invalid json",
 | |
| 			wantErr: true,
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, tt := range tests {
 | |
| 		t.Run(tt.name, func(t *testing.T) {
 | |
| 			dir := t.TempDir()
 | |
| 			path := filepath.Join(dir, "serve-config.json")
 | |
| 			if err := os.WriteFile(path, []byte(tt.gotSC), 0644); err != nil {
 | |
| 				t.Fatal(err)
 | |
| 			}
 | |
| 
 | |
| 			got, err := readServeConfig(path, tt.certDomain)
 | |
| 			if (err != nil) != tt.wantErr {
 | |
| 				t.Errorf("readServeConfig() error = %v, wantErr %v", err, tt.wantErr)
 | |
| 				return
 | |
| 			}
 | |
| 			if !cmp.Equal(got, tt.wantSC) {
 | |
| 				t.Errorf("readServeConfig() diff (-got +want):\n%s", cmp.Diff(got, tt.wantSC))
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| type fakeLocalClient struct {
 | |
| 	*local.Client
 | |
| 	setServeCalled bool
 | |
| }
 | |
| 
 | |
| func (m *fakeLocalClient) SetServeConfig(ctx context.Context, cfg *ipn.ServeConfig) error {
 | |
| 	m.setServeCalled = true
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (m *fakeLocalClient) CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err error) {
 | |
| 	return nil, nil, nil
 | |
| }
 | |
| 
 | |
| func TestHasHTTPSEndpoint(t *testing.T) {
 | |
| 	tests := []struct {
 | |
| 		name string
 | |
| 		cfg  *ipn.ServeConfig
 | |
| 		want bool
 | |
| 	}{
 | |
| 		{
 | |
| 			name: "nil_config",
 | |
| 			cfg:  nil,
 | |
| 			want: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "empty_config",
 | |
| 			cfg:  &ipn.ServeConfig{},
 | |
| 			want: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "no_https_endpoints",
 | |
| 			cfg: &ipn.ServeConfig{
 | |
| 				TCP: map[uint16]*ipn.TCPPortHandler{
 | |
| 					80: {
 | |
| 						HTTPS: false,
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			want: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "has_https_endpoint",
 | |
| 			cfg: &ipn.ServeConfig{
 | |
| 				TCP: map[uint16]*ipn.TCPPortHandler{
 | |
| 					443: {
 | |
| 						HTTPS: true,
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			want: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "mixed_endpoints",
 | |
| 			cfg: &ipn.ServeConfig{
 | |
| 				TCP: map[uint16]*ipn.TCPPortHandler{
 | |
| 					80:  {HTTPS: false},
 | |
| 					443: {HTTPS: true},
 | |
| 				},
 | |
| 			},
 | |
| 			want: true,
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, tt := range tests {
 | |
| 		t.Run(tt.name, func(t *testing.T) {
 | |
| 			got := hasHTTPSEndpoint(tt.cfg)
 | |
| 			if got != tt.want {
 | |
| 				t.Errorf("hasHTTPSEndpoint() = %v, want %v", got, tt.want)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 |