• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kubevirt / kubevirt / 6327324e-b1c6-424d-a17b-49445e3b3998

25 Jul 2026 05:23PM UTC coverage: 72.531% (-0.03%) from 72.56%
6327324e-b1c6-424d-a17b-49445e3b3998

push

prow

web-flow
Merge pull request #18400 from awels/export-proxy-fd-leak-fix

virt-exportproxy: fix backend connection FD leak and inherit backend TLS config from kubevirt CR

89 of 106 new or added lines in 2 files covered. (83.96%)

8 existing lines in 2 files now uncovered.

84451 of 116435 relevant lines covered (72.53%)

510.25 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

51.65
/cmd/virt-exportproxy/virt-exportproxy.go
1
package main
2

3
/*
4
 * This file is part of the KubeVirt project
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.
8
 * You may obtain a copy of the License at
9
 *
10
 *     http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * See the License for the specific language governing permissions and
16
 * limitations under the License.
17
 *
18
 * Copyright 2022 Red Hat, Inc.
19
 *
20
 */
21

22
import (
23
        "context"
24
        "crypto/tls"
25
        "crypto/x509"
26
        "flag"
27
        "fmt"
28
        "io"
29
        "net"
30
        "net/http"
31
        "net/http/httputil"
32
        "regexp"
33
        "time"
34

35
        kvtls "kubevirt.io/kubevirt/pkg/util/tls"
36

37
        "github.com/prometheus/client_golang/prometheus/promhttp"
38
        "k8s.io/client-go/tools/cache"
39
        certificate2 "k8s.io/client-go/util/certificate"
40
        aggregatorclient "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset"
41

42
        exportv1 "kubevirt.io/api/export/v1"
43
        "kubevirt.io/client-go/kubecli"
44
        "kubevirt.io/client-go/log"
45
        clientutil "kubevirt.io/client-go/util"
46

47
        "kubevirt.io/kubevirt/pkg/certificates/bootstrap"
48
        "kubevirt.io/kubevirt/pkg/controller"
49
        "kubevirt.io/kubevirt/pkg/service"
50
)
51

52
const (
53
        defaultTlsCertFilePath = "/etc/virt-exportproxy/certificates/tls.crt"
54
        defaultTlsKeyFilePath  = "/etc/virt-exportproxy/certificates/tls.key"
55

56
        apiGroup           = "export.kubevirt.io"
57
        apiVersions        = "v1beta1|v1"
58
        exportResourceName = "virtualmachineexports"
59

60
        backendIdleConnTimeout       = 30 * time.Second
61
        backendDialTimeout           = 10 * time.Second
62
        backendDialKeepAlive         = 30 * time.Second
63
        backendResponseHeaderTimeout = 30 * time.Second
64
        serverIdleTimeout            = 60 * time.Second
65
        serverReadHeaderTimeout      = 10 * time.Second
66
)
67

68
type exportProxyApp struct {
69
        service.ServiceListen
70
        tlsCertFilePath string
71
        tlsKeyFilePath  string
72
        certManager     certificate2.Manager
73
        caManager       kvtls.ClientCAManager
74
        exportStore     cache.Store
75
        kubeVirtStore   cache.Store
76
        // reverseProxy is a shared template; proxyHandler takes a shallow copy per
77
        // request and sets a per-request Rewrite closure on the copy.
78
        reverseProxy *httputil.ReverseProxy
79
}
80

81
func NewExportProxyApp() service.Service {
×
82
        return &exportProxyApp{}
×
83
}
×
84

85
func (app *exportProxyApp) AddFlags() {
×
86
        app.InitFlags()
×
87
        app.AddCommonFlags()
×
88

×
89
        flag.StringVar(&app.tlsCertFilePath, "tls-cert-file", defaultTlsCertFilePath,
×
90
                "File containing the default x509 Certificate for HTTPS")
×
91
        flag.StringVar(&app.tlsKeyFilePath, "tls-key-file", defaultTlsKeyFilePath,
×
92
                "File containing the default x509 private key matching --tls-cert-file")
×
93
}
×
94

95
func (app *exportProxyApp) Run() {
×
96
        stopChan := make(chan struct{}, 1)
×
97
        defer close(stopChan)
×
NEW
98
        if err := app.prepareInformers(stopChan); err != nil {
×
NEW
99
                panic(err)
×
100
        }
101

102
        app.prepareCertManager()
×
103
        go app.certManager.Start()
×
104

×
NEW
105
        app.initReverseProxy()
×
NEW
106

×
107
        appTLSConfig := kvtls.SetupExportProxyTLS(app.certManager, app.kubeVirtStore)
×
108
        mux := http.NewServeMux()
×
109
        mux.HandleFunc("/", app.proxyHandler)
×
110
        mux.HandleFunc("/healthz", app.healthzHandler)
×
111
        mux.Handle("/metrics", promhttp.Handler())
×
112

×
113
        server := &http.Server{
×
NEW
114
                Addr:              app.Address(),
×
NEW
115
                Handler:           mux,
×
NEW
116
                TLSConfig:         appTLSConfig,
×
NEW
117
                ReadHeaderTimeout: serverReadHeaderTimeout,
×
NEW
118
                IdleTimeout:       serverIdleTimeout,
×
119
                // Disable HTTP/2
×
120
                // See CVE-2023-44487
×
121
                TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){},
×
122
        }
×
123

×
124
        if err := server.ListenAndServeTLS("", ""); err != nil {
×
125
                panic(err)
×
126
        }
127
}
128

129
func (app *exportProxyApp) healthzHandler(w http.ResponseWriter, r *http.Request) {
×
130
        io.WriteString(w, "OK")
×
131
}
×
132

133
var proxyPathMatcher = regexp.MustCompile(`^/api/` + apiGroup + "/" + "(" + apiVersions + ")" + `/namespaces/([^/]+)/` + exportResourceName + `/([^/]+)/(.*)$`)
134

135
func (app *exportProxyApp) proxyHandler(w http.ResponseWriter, r *http.Request) {
3✔
136
        match := proxyPathMatcher.FindStringSubmatch(r.URL.Path)
3✔
137
        if len(match) != 5 {
3✔
138
                w.WriteHeader(http.StatusBadRequest)
×
139
                return
×
140
        }
×
141

142
        key := fmt.Sprintf("%s/%s", match[2], match[3])
3✔
143
        obj, exists, err := app.exportStore.GetByKey(key)
3✔
144
        if err != nil {
3✔
145
                w.WriteHeader(http.StatusInternalServerError)
×
146
                return
×
147
        }
×
148

149
        if !exists {
4✔
150
                w.WriteHeader(http.StatusNotFound)
1✔
151
                return
1✔
152
        }
1✔
153

154
        export := obj.(*exportv1.VirtualMachineExport)
2✔
155
        if export.Status == nil || export.Status.Phase != exportv1.Ready {
3✔
156
                w.WriteHeader(http.StatusServiceUnavailable)
1✔
157
                return
1✔
158
        }
1✔
159

160
        backendHost := fmt.Sprintf("%s.%s.svc:443", export.Status.ServiceName, match[2])
1✔
161
        backendPath := "/" + match[4]
1✔
162
        log.Log.V(4).Infof("Proxying to https://%s%s", backendHost, backendPath)
1✔
163
        proxy := *app.reverseProxy
1✔
164
        proxy.Rewrite = func(pr *httputil.ProxyRequest) {
2✔
165
                // Route via Out (not SetURL) so the inbound path is not joined onto the target.
1✔
166
                pr.Out.URL.Scheme = "https"
1✔
167
                pr.Out.URL.Host = backendHost
1✔
168
                pr.Out.URL.Path = backendPath
1✔
169
                pr.Out.URL.RawPath = ""
1✔
170
                pr.Out.Host = ""
1✔
171
        }
1✔
172
        proxy.ServeHTTP(w, r)
1✔
173
}
174

175
func (app *exportProxyApp) initReverseProxy() {
3✔
176
        transport := &http.Transport{
3✔
177
                DialTLSContext:        app.dialBackendTLS,
3✔
178
                MaxIdleConns:          100,
3✔
179
                MaxIdleConnsPerHost:   20,
3✔
180
                IdleConnTimeout:       backendIdleConnTimeout,
3✔
181
                ResponseHeaderTimeout: backendResponseHeaderTimeout,
3✔
182
        }
3✔
183
        app.reverseProxy = &httputil.ReverseProxy{
3✔
184
                Transport:     transport,
3✔
185
                FlushInterval: -1, // flush immediately; avoids proxy-side buffering of large export streams
3✔
186
        }
3✔
187
}
3✔
188

189
func (app *exportProxyApp) dialBackendTLS(ctx context.Context, network, addr string) (net.Conn, error) {
4✔
190
        dialer := net.Dialer{
4✔
191
                Timeout:   backendDialTimeout,
4✔
192
                KeepAlive: backendDialKeepAlive,
4✔
193
        }
4✔
194
        conn, err := dialer.DialContext(ctx, network, addr)
4✔
195
        if err != nil {
5✔
196
                return nil, err
1✔
197
        }
1✔
198

199
        serverName, _, err := net.SplitHostPort(addr)
3✔
200
        if err != nil {
3✔
NEW
201
                _ = conn.Close()
×
NEW
202
                return nil, fmt.Errorf("could not parse backend address %q: %w", addr, err)
×
NEW
203
        }
×
204
        cfg := &tls.Config{
3✔
205
                // Neither the client nor the server should validate anything itself; VerifyConnection is still executed.
3✔
206
                InsecureSkipVerify: true, // #nosec G402 -- VerifyConnection performs certificate verification
3✔
207
                VerifyConnection:   app.verifyBackendConnection,
3✔
208
                ServerName:         serverName,
3✔
209
        }
3✔
210
        kvtls.ApplyTLSConfigurationFromKubeVirtStore(cfg, app.kubeVirtStore)
3✔
211

3✔
212
        tlsConn := tls.Client(conn, cfg)
3✔
213
        if err := tlsConn.HandshakeContext(ctx); err != nil {
5✔
214
                _ = conn.Close()
2✔
215
                return nil, err
2✔
216
        }
2✔
217
        return tlsConn, nil
1✔
218
}
219

220
func (app *exportProxyApp) verifyBackendConnection(cs tls.ConnectionState) error {
9✔
221
        if len(cs.PeerCertificates) == 0 {
10✔
222
                return fmt.Errorf("backend presented no certificate")
1✔
223
        }
1✔
224
        if cs.ServerName == "" {
9✔
225
                return fmt.Errorf("backend TLS ServerName is required")
1✔
226
        }
1✔
227

228
        certPool, err := app.caManager.GetCurrent()
7✔
229
        if err != nil {
8✔
230
                return err
1✔
231
        }
1✔
232

233
        peer := cs.PeerCertificates[0]
6✔
234
        intermediates := x509.NewCertPool()
6✔
235
        for _, intermediate := range cs.PeerCertificates[1:] {
7✔
236
                intermediates.AddCert(intermediate)
1✔
237
        }
1✔
238

239
        opts := x509.VerifyOptions{
6✔
240
                Roots:         certPool,
6✔
241
                Intermediates: intermediates,
6✔
242
                KeyUsages:     []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
6✔
243
                DNSName:       cs.ServerName,
6✔
244
        }
6✔
245

6✔
246
        _, err = peer.Verify(opts)
6✔
247
        if err != nil {
9✔
248
                return fmt.Errorf("could not verify backend certificate: %w", err)
3✔
249
        }
3✔
250
        return nil
3✔
251
}
252

NEW
253
func (app *exportProxyApp) prepareInformers(stopChan <-chan struct{}) error {
×
254
        namespace, err := clientutil.GetNamespace()
×
255
        if err != nil {
×
NEW
256
                return fmt.Errorf("failed to get namespace: %w", err)
×
257
        }
×
258

259
        clientConfig, err := kubecli.GetKubevirtClientConfig()
×
260
        if err != nil {
×
NEW
261
                return fmt.Errorf("failed to get kubevirt client config: %w", err)
×
262
        }
×
263
        virtCli, err := kubecli.GetKubevirtClientFromRESTConfig(clientConfig)
×
264
        if err != nil {
×
NEW
265
                return fmt.Errorf("failed to create kubevirt client: %w", err)
×
266
        }
×
267
        aggregatorClient := aggregatorclient.NewForConfigOrDie(clientConfig)
×
268

×
269
        kubeInformerFactory := controller.NewKubeInformerFactory(virtCli.RestClient(), virtCli, virtCli, aggregatorClient, namespace)
×
270
        caInformer := kubeInformerFactory.KubeVirtExportCAConfigMap()
×
271
        app.exportStore = kubeInformerFactory.VirtualMachineExport().GetStore()
×
272
        app.kubeVirtStore = kubeInformerFactory.KubeVirt().GetStore()
×
273
        kubeInformerFactory.Start(stopChan)
×
274
        kubeInformerFactory.WaitForCacheSync(stopChan)
×
275

×
276
        app.caManager = kvtls.NewCAManager(caInformer.GetStore(), namespace, "kubevirt-export-ca")
×
NEW
277
        return nil
×
278
}
279

280
func (app *exportProxyApp) prepareCertManager() {
×
281
        app.certManager = bootstrap.NewFileCertificateManager(app.tlsCertFilePath, app.tlsKeyFilePath)
×
282
}
×
283

284
func main() {
×
285
        log.InitializeLogging("virt-exportproxy")
×
286
        log.Log.Info("Starting export proxy")
×
287

×
288
        app := NewExportProxyApp()
×
289
        service.Setup(app)
×
290
        app.Run()
×
291
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc