Skip to content

Commit d70f576

Browse files
committed
feat(server): enables TLS mode
This change is made to support environments that require HTTPS. The server can now run over HTTPS with either user-provided certs or self-signed ones. This is done through flags aligned with vLLM (`--ssl-certfile`, `--ssl-keyfile`). Additionally `--self-signed-certs` has been provided for self-signed certs. Signed-off-by: Bartosz Majsak <[email protected]>
1 parent a6c16ab commit d70f576

File tree

6 files changed

+309
-7
lines changed

6 files changed

+309
-7
lines changed

pkg/common/config.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,13 @@ type Configuration struct {
174174

175175
// DPSize is data parallel size - a number of ranks to run, minimum is 1, maximum is 8, default is 1
176176
DPSize int `yaml:"data-parallel-size" json:"data-parallel-size"`
177+
178+
// SSLCertFile is the path to the SSL certificate file for HTTPS
179+
SSLCertFile string `yaml:"ssl-certfile" json:"ssl-certfile"`
180+
// SSLKeyFile is the path to the SSL private key file for HTTPS
181+
SSLKeyFile string `yaml:"ssl-keyfile" json:"ssl-keyfile"`
182+
// SelfSignedCerts enables automatic generation of self-signed certificates for HTTPS
183+
SelfSignedCerts bool `yaml:"self-signed-certs" json:"self-signed-certs"`
177184
}
178185

179186
type Metrics struct {
@@ -469,9 +476,23 @@ func (c *Configuration) validate() error {
469476
if c.DPSize < 1 || c.DPSize > 8 {
470477
return errors.New("data parallel size must be between 1 ans 8")
471478
}
479+
480+
if (c.SSLCertFile == "") != (c.SSLKeyFile == "") {
481+
return errors.New("both ssl-certfile and ssl-keyfile must be provided together")
482+
}
483+
484+
if c.SelfSignedCerts && (c.SSLCertFile != "" || c.SSLKeyFile != "") {
485+
return errors.New("cannot use both self-signed-certs and explicit ssl-certfile/ssl-keyfile")
486+
}
487+
472488
return nil
473489
}
474490

491+
// SSLEnabled returns true if SSL is enabled either via certificate files or self-signed certificates
492+
func (c *Configuration) SSLEnabled() bool {
493+
return (c.SSLCertFile != "" && c.SSLKeyFile != "") || c.SelfSignedCerts
494+
}
495+
475496
func (c *Configuration) Copy() (*Configuration, error) {
476497
var dst Configuration
477498
data, err := json.Marshal(c)
@@ -552,6 +573,10 @@ func ParseCommandParamsAndLoadConfig() (*Configuration, error) {
552573
f.Var(&dummyFailureTypes, "failure-types", failureTypesDescription)
553574
f.Lookup("failure-types").NoOptDefVal = dummy
554575

576+
f.StringVar(&config.SSLCertFile, "ssl-certfile", config.SSLCertFile, "Path to SSL certificate file for HTTPS (optional)")
577+
f.StringVar(&config.SSLKeyFile, "ssl-keyfile", config.SSLKeyFile, "Path to SSL private key file for HTTPS (optional)")
578+
f.BoolVar(&config.SelfSignedCerts, "self-signed-certs", config.SelfSignedCerts, "Enable automatic generation of self-signed certificates for HTTPS")
579+
555580
// These values were manually parsed above in getParamValueFromArgs, we leave this in order to get these flags in --help
556581
var dummyString string
557582
f.StringVar(&dummyString, "config", "", "The path to a yaml configuration file. The command line values overwrite the configuration file values")

pkg/llm-d-inference-sim/server.go

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ func (s *VllmSimulator) newListener() (net.Listener, error) {
4242
return listener, nil
4343
}
4444

45-
// startServer starts http server on port defined in command line
45+
// startServer starts http/https server on port defined in command line
4646
func (s *VllmSimulator) startServer(ctx context.Context, listener net.Listener) error {
4747
r := fasthttprouter.New()
4848

@@ -61,36 +61,45 @@ func (s *VllmSimulator) startServer(ctx context.Context, listener net.Listener)
6161
r.GET("/ready", s.HandleReady)
6262
r.POST("/tokenize", s.HandleTokenize)
6363

64-
server := fasthttp.Server{
64+
server := &fasthttp.Server{
6565
ErrorHandler: s.HandleError,
6666
Handler: r.Handler,
6767
Logger: s,
6868
}
6969

70+
if err := s.configureSSL(server); err != nil {
71+
return err
72+
}
73+
7074
// Start server in a goroutine
7175
serverErr := make(chan error, 1)
7276
go func() {
73-
s.logger.Info("HTTP server starting")
74-
serverErr <- server.Serve(listener)
77+
if s.config.SSLEnabled() {
78+
s.logger.Info("Server starting", "protocol", "HTTPS", "port", s.config.Port)
79+
serverErr <- server.ServeTLS(listener, "", "")
80+
} else {
81+
s.logger.Info("Server starting", "protocol", "HTTP", "port", s.config.Port)
82+
serverErr <- server.Serve(listener)
83+
}
7584
}()
7685

7786
// Wait for either context cancellation or server error
7887
select {
7988
case <-ctx.Done():
80-
s.logger.Info("Shutdown signal received, shutting down HTTP server gracefully")
89+
s.logger.Info("Shutdown signal received, shutting down server gracefully")
8190

8291
// Gracefully shutdown the server
8392
if err := server.Shutdown(); err != nil {
8493
s.logger.Error(err, "Error during server shutdown")
8594
return err
8695
}
8796

88-
s.logger.Info("HTTP server stopped")
97+
s.logger.Info("Server stopped")
8998
return nil
9099

91100
case err := <-serverErr:
92101
if err != nil {
93-
s.logger.Error(err, "HTTP server failed")
102+
s.logger.Error(err, "Server failed")
94103
}
95104
return err
96105
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
Copyright 2025 The llm-d-inference-sim Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package llmdinferencesim
18+
19+
import (
20+
"os"
21+
"path/filepath"
22+
)
23+
24+
// GenerateTempCerts creates temporary SSL certificate and key files for testing
25+
func GenerateTempCerts(tempDir string) (certFile, keyFile string, err error) {
26+
certPEM, keyPEM, err := CreateSelfSignedTLSCertificatePEM()
27+
if err != nil {
28+
return "", "", err
29+
}
30+
31+
certFile = filepath.Join(tempDir, "cert.pem")
32+
if err := os.WriteFile(certFile, certPEM, 0644); err != nil {
33+
return "", "", err
34+
}
35+
36+
keyFile = filepath.Join(tempDir, "key.pem")
37+
if err := os.WriteFile(keyFile, keyPEM, 0600); err != nil {
38+
return "", "", err
39+
}
40+
41+
return certFile, keyFile, nil
42+
}

pkg/llm-d-inference-sim/server_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
)
3232

3333
var _ = Describe("Server", func() {
34+
3435
It("Should respond to /health", func() {
3536
ctx := context.TODO()
3637
client, err := startServer(ctx, common.ModeRandom)
@@ -116,4 +117,95 @@ var _ = Describe("Server", func() {
116117
Expect(tokenizeResp.MaxModelLen).To(Equal(2048))
117118
})
118119
})
120+
121+
Context("SSL/HTTPS Configuration", func() {
122+
It("Should parse SSL certificate configuration correctly", func() {
123+
tempDir := GinkgoT().TempDir()
124+
certFile, keyFile, err := GenerateTempCerts(tempDir)
125+
Expect(err).NotTo(HaveOccurred())
126+
127+
oldArgs := os.Args
128+
defer func() {
129+
os.Args = oldArgs
130+
}()
131+
132+
os.Args = []string{"cmd", "--model", model, "--ssl-certfile", certFile, "--ssl-keyfile", keyFile}
133+
config, err := common.ParseCommandParamsAndLoadConfig()
134+
Expect(err).NotTo(HaveOccurred())
135+
Expect(config.SSLEnabled()).To(BeTrue())
136+
Expect(config.SSLCertFile).To(Equal(certFile))
137+
Expect(config.SSLKeyFile).To(Equal(keyFile))
138+
})
139+
140+
It("Should parse self-signed certificate configuration correctly", func() {
141+
oldArgs := os.Args
142+
defer func() {
143+
os.Args = oldArgs
144+
}()
145+
146+
os.Args = []string{"cmd", "--model", model, "--self-signed-certs"}
147+
config, err := common.ParseCommandParamsAndLoadConfig()
148+
Expect(err).NotTo(HaveOccurred())
149+
Expect(config.SSLEnabled()).To(BeTrue())
150+
Expect(config.SelfSignedCerts).To(BeTrue())
151+
})
152+
153+
It("Should create self-signed TLS certificate successfully", func() {
154+
cert, err := CreateSelfSignedTLSCertificate()
155+
Expect(err).NotTo(HaveOccurred())
156+
Expect(cert.Certificate).To(HaveLen(1))
157+
Expect(cert.PrivateKey).NotTo(BeNil())
158+
})
159+
160+
It("Should validate SSL configuration - both cert and key required", func() {
161+
tempDir := GinkgoT().TempDir()
162+
163+
oldArgs := os.Args
164+
defer func() {
165+
os.Args = oldArgs
166+
}()
167+
168+
certFile, _, err := GenerateTempCerts(tempDir)
169+
Expect(err).NotTo(HaveOccurred())
170+
171+
os.Args = []string{"cmd", "--model", model, "--ssl-certfile", certFile}
172+
_, err = common.ParseCommandParamsAndLoadConfig()
173+
Expect(err).To(HaveOccurred())
174+
Expect(err.Error()).To(ContainSubstring("both ssl-certfile and ssl-keyfile must be provided together"))
175+
176+
_, keyFile, err := GenerateTempCerts(tempDir)
177+
Expect(err).NotTo(HaveOccurred())
178+
179+
os.Args = []string{"cmd", "--model", model, "--ssl-keyfile", keyFile}
180+
_, err = common.ParseCommandParamsAndLoadConfig()
181+
Expect(err).To(HaveOccurred())
182+
Expect(err.Error()).To(ContainSubstring("both ssl-certfile and ssl-keyfile must be provided together"))
183+
})
184+
185+
It("Should start HTTPS server with provided SSL certificates", func(ctx SpecContext) {
186+
tempDir := GinkgoT().TempDir()
187+
certFile, keyFile, err := GenerateTempCerts(tempDir)
188+
Expect(err).NotTo(HaveOccurred())
189+
190+
args := []string{"cmd", "--model", model, "--mode", common.ModeRandom,
191+
"--ssl-certfile", certFile, "--ssl-keyfile", keyFile}
192+
client, err := startServerWithArgs(ctx, common.ModeRandom, args, nil)
193+
Expect(err).NotTo(HaveOccurred())
194+
195+
resp, err := client.Get("https://localhost/health")
196+
Expect(err).NotTo(HaveOccurred())
197+
Expect(resp.StatusCode).To(Equal(http.StatusOK))
198+
})
199+
200+
It("Should start HTTPS server with self-signed certificates", func(ctx SpecContext) {
201+
args := []string{"cmd", "--model", model, "--mode", common.ModeRandom, "--self-signed-certs"}
202+
client, err := startServerWithArgs(ctx, common.ModeRandom, args, nil)
203+
Expect(err).NotTo(HaveOccurred())
204+
205+
resp, err := client.Get("https://localhost/health")
206+
Expect(err).NotTo(HaveOccurred())
207+
Expect(resp.StatusCode).To(Equal(http.StatusOK))
208+
})
209+
210+
})
119211
})
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/*
2+
Copyright 2025 The llm-d-inference-sim Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package llmdinferencesim
18+
19+
import (
20+
"crypto/rand"
21+
"crypto/rsa"
22+
"crypto/tls"
23+
"crypto/x509"
24+
"crypto/x509/pkix"
25+
"encoding/pem"
26+
"fmt"
27+
"math/big"
28+
"net"
29+
"time"
30+
31+
"github.com/valyala/fasthttp"
32+
)
33+
34+
// Based on: https://github.com/kubernetes-sigs/gateway-api-inference-extension/blob/8d01161ec48d6b49cd371f179551b35da46e6fd6/internal/tls/tls.go
35+
func (s *VllmSimulator) configureSSL(server *fasthttp.Server) error {
36+
if !s.config.SSLEnabled() {
37+
return nil
38+
}
39+
40+
var cert tls.Certificate
41+
var err error
42+
43+
if s.config.SSLCertFile != "" && s.config.SSLKeyFile != "" {
44+
s.logger.Info("HTTPS server starting with certificate files", "cert", s.config.SSLCertFile, "key", s.config.SSLKeyFile)
45+
cert, err = tls.LoadX509KeyPair(s.config.SSLCertFile, s.config.SSLKeyFile)
46+
if err == nil {
47+
s.logger.Info("Certificate loaded successfully from files")
48+
}
49+
} else if s.config.SelfSignedCerts {
50+
s.logger.Info("HTTPS server starting with self-signed certificate")
51+
cert, err = CreateSelfSignedTLSCertificate()
52+
if err == nil {
53+
s.logger.Info("Self-signed certificate generated successfully")
54+
}
55+
}
56+
57+
if err != nil {
58+
s.logger.Error(err, "failed to create TLS certificate")
59+
return err
60+
}
61+
62+
server.TLSConfig = &tls.Config{
63+
Certificates: []tls.Certificate{cert},
64+
MinVersion: tls.VersionTLS12,
65+
CipherSuites: []uint16{
66+
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
67+
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
68+
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
69+
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
70+
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
71+
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
72+
},
73+
}
74+
75+
return nil
76+
}
77+
78+
// CreateSelfSignedTLSCertificatePEM creates a self-signed cert and returns the PEM-encoded certificate and key bytes
79+
func CreateSelfSignedTLSCertificatePEM() (certPEM, keyPEM []byte, err error) {
80+
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
81+
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
82+
if err != nil {
83+
return nil, nil, fmt.Errorf("error creating serial number: %v", err)
84+
}
85+
now := time.Now()
86+
notBefore := now.UTC()
87+
template := x509.Certificate{
88+
SerialNumber: serialNumber,
89+
Subject: pkix.Name{
90+
Organization: []string{"llm-d Inference Simulator"},
91+
},
92+
NotBefore: notBefore,
93+
NotAfter: now.Add(time.Hour * 24 * 365 * 10).UTC(), // 10 years
94+
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
95+
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
96+
BasicConstraintsValid: true,
97+
IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback},
98+
DNSNames: []string{"localhost"},
99+
}
100+
101+
priv, err := rsa.GenerateKey(rand.Reader, 4096)
102+
if err != nil {
103+
return nil, nil, fmt.Errorf("error generating key: %v", err)
104+
}
105+
106+
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
107+
if err != nil {
108+
return nil, nil, fmt.Errorf("error creating certificate: %v", err)
109+
}
110+
111+
certBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
112+
113+
privBytes, err := x509.MarshalPKCS8PrivateKey(priv)
114+
if err != nil {
115+
return nil, nil, fmt.Errorf("error marshalling private key: %v", err)
116+
}
117+
keyBytes := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privBytes})
118+
119+
return certBytes, keyBytes, nil
120+
}
121+
122+
// CreateSelfSignedTLSCertificate creates a self-signed cert the server can use to serve TLS.
123+
// Original code: https://github.com/kubernetes-sigs/gateway-api-inference-extension/blob/8d01161ec48d6b49cd371f179551b35da46e6fd6/internal/tls/tls.go
124+
func CreateSelfSignedTLSCertificate() (tls.Certificate, error) {
125+
certPEM, keyPEM, err := CreateSelfSignedTLSCertificatePEM()
126+
if err != nil {
127+
return tls.Certificate{}, err
128+
}
129+
return tls.X509KeyPair(certPEM, keyPEM)
130+
}

0 commit comments

Comments
 (0)