Skip to content

Commit e21ffa5

Browse files
committed
feat: explicitly configure public IP for TLS socket bindings
1 parent 9a8894f commit e21ffa5

10 files changed

Lines changed: 141 additions & 69 deletions

File tree

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,19 @@ and this project follows versions of format `{year}.{month}.{patch_number}`.
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- `qpi-ui`: Added `--ip-addr` (or `QPI_IP_ADDR`, or `ipAddr` in config) to explicitly specify the public IP for binding TLS sockets. The provided IP is now properly encoded in the X509 certificate's SAN IP block.
13+
14+
### Changed
15+
16+
- `qpi-driver`: Updated NNG setup logic. The driver now establishes connections using the explicit NNG IP address returned by the server via `ConnectResponse`, decoupling it from the HTTP QPI address.
17+
18+
### Fixed
19+
20+
- `qpi-ui`: Removed the `fetchHostIPs()` autodiscovery logic which caused unintended behavior when deployed behind proxies.
21+
- `qpi-driver`: Fixed a race condition where the result sender process could attempt to read the CA certificate from disk before the main process had downloaded it.
22+
1023
## [0.0.32] - 2026-06-26
1124

1225
### Fixed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ The Go server can be configured via CLI flags, environment variables, or a confi
136136
|---|---|---|---|
137137
| `--config-file` | `QPI_CONFIG_FILE` | `qpi.config.yml` | Path to JSON or YAML configuration file. |
138138
| `--domain` | `QPI_DOMAIN` | | The domain name this server is running on. |
139+
| `--ip-addr` | `QPI_IP_ADDR` | "127.0.0.1" | The public IP address to include in the generated TLS certificates. |
139140
| `--server-port` | `QPI_SERVER_PORT` | `8090` | The port this server should run on. |
140141
| `--tls-ca-cert-file` | `QPI_TLS_CA_CERT_FILE` | `.qpi.ca.pem` | Path to TLS root CA certificate file. |
141142
| `--tls-ca-key-file` | `QPI_TLS_CA_KEY_FILE` | `.qpi.ca.key` | Path to TLS root CA key file. |

qpi-driver/qpi_driver/driver.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,13 @@ class HandshakeInfo:
3535
nng_command_port: Port allocated for NNG command dispatch.
3636
nng_result_port: Port allocated for NNG result collection.
3737
auth_token: Static authorization token assigned to the QPU.
38+
nng_host: Hostname or IP for NNG connections.
3839
"""
3940

4041
nng_command_port: int
4142
nng_result_port: int
4243
auth_token: str
44+
nng_host: str
4345

4446

4547
def _normalize_qpi_addr(qpi_addr: str) -> str:
@@ -106,6 +108,7 @@ def do_handshake(
106108
nng_command_port=int(data["nng_command_port"]),
107109
nng_result_port=int(data["nng_result_port"]),
108110
auth_token=data.get("auth_token", ""),
111+
nng_host=data.get("nng_host") or _extract_host(qpi_addr),
109112
)
110113

111114

@@ -195,8 +198,6 @@ def send_results(
195198
result_queue: multiprocessing.Queue,
196199
res_port: int,
197200
nng_host: str,
198-
qpi_addr: str,
199-
ca_fingerprint: str,
200201
ca_file_path: Path,
201202
) -> None:
202203
"""Result sender process: reads Qiskit-format result dicts from result_queue
@@ -206,8 +207,6 @@ def send_results(
206207
result_queue: Queue used to receive result dicts from the worker.
207208
res_port: Port allocated for the NNG PUSH socket to return results.
208209
nng_host: Hostname or IP of the Go PocketBase server (for NNG TCP connections).
209-
qpi_addr: The address of the QPI server to download the Certificate authority from
210-
ca_fingerprint: The fingerprint of the CA file for verification of the download
211210
ca_file_path: Path to the CA certificate file for TLS connections.
212211
"""
213212
logging.basicConfig(
@@ -221,8 +220,10 @@ def send_results(
221220
addr = f"tls+tcp://{nng_host}:{res_port}"
222221
rs_log.info("Connecting NNG PUSH → %s", addr)
223222

224-
tls_config = _get_tls_config(
225-
qpi_addr, ca_fingerprint=ca_fingerprint, ca_file_path=ca_file_path
223+
tls_config = TLSConfig(
224+
TLSConfig.MODE_CLIENT,
225+
server_name=nng_host,
226+
ca_files=ca_file_path.as_posix(),
226227
)
227228

228229
with pynng.Push0(tls_config=tls_config) as sock:
@@ -313,7 +314,7 @@ def run_driver(
313314
executor_type=executor_type_str,
314315
device_config=device_config,
315316
)
316-
nng_host = _extract_host(qpi_addr)
317+
nng_host = info.nng_host
317318
cmd_port = info.nng_command_port
318319
res_port = info.nng_result_port
319320

@@ -340,28 +341,27 @@ def run_driver(
340341
)
341342
worker.start()
342343

344+
# 4. Ensure TLS CA cert is downloaded before starting the result sender process
345+
tls_config = _get_tls_config(
346+
qpi_addr, ca_fingerprint=ca_fingerprint, ca_file_path=ca_file_path
347+
)
348+
343349
# Start Result Sender Process
344350
result_sender = multiprocessing.Process(
345351
target=send_results,
346352
kwargs={
347353
"result_queue": result_queue,
348354
"res_port": res_port,
349355
"nng_host": nng_host,
350-
"qpi_addr": qpi_addr,
351-
"ca_fingerprint": ca_fingerprint,
352356
"ca_file_path": ca_file_path,
353357
},
354358
name="QPI-ResultSender",
355359
daemon=True,
356360
)
357361
result_sender.start()
358362

359-
# 4. Start NNG PULL (commands) in Main Process
360363
addr = f"tls+tcp://{nng_host}:{cmd_port}"
361364
log.info("Connecting NNG PULL → %s", addr)
362-
tls_config = _get_tls_config(
363-
qpi_addr, ca_fingerprint=ca_fingerprint, ca_file_path=ca_file_path
364-
)
365365

366366
try:
367367
with pynng.Pull0(tls_config=tls_config) as sock:

qpi-ui/internal/api/api.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,7 @@ func handleQPUConnect(re *core.RequestEvent) error {
591591
NNGResultPort: qpu.NNGResultPort,
592592
TLSHash: cfg.GetTlsCaHash(),
593593
AuthToken: token,
594+
NNGHost: cfg.IpAddr,
594595
}
595596
return re.JSON(http.StatusOK, resp)
596597
}

qpi-ui/internal/api/schema.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ type ConnectResponse struct {
4343
NNGResultPort int `json:"nng_result_port"`
4444
TLSHash string `json:"tls_hash"`
4545
AuthToken string `json:"auth_token"`
46+
NNGHost string `json:"nng_host"`
4647
}
4748

4849
func (cr *ConnectResponse) SetDefaults() {
@@ -54,6 +55,7 @@ func (cr *ConnectResponse) ToMap() map[string]any {
5455
"nng_command_port": cr.NNGCommandPort,
5556
"nng_result_port": cr.NNGResultPort,
5657
"auth_token": cr.AuthToken,
58+
"nng_host": cr.NNGHost,
5759
}
5860
}
5961

qpi-ui/internal/config/config.go

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"encoding/json"
1212
"fmt"
1313
"log"
14+
"net"
1415
"os"
1516
"strconv"
1617
"strings"
@@ -60,6 +61,7 @@ var (
6061
flagTLSCaKeyFile string
6162
flagDomainName string
6263
flagServerPort int
64+
flagIpAddr string
6365
)
6466

6567
// AppConfig stores application-wide configuration parameters for the QPI server.
@@ -85,6 +87,7 @@ type AppConfig struct {
8587
TlsCaKeyFile string
8688
DomainName string
8789
ServerPort int
90+
IpAddr string
8891
tlsConfig *certKeyPair
8992
parsedTlsConfig *tls.Config
9093
tlsCaConfig *certKeyPair
@@ -191,13 +194,13 @@ func (cfg *AppConfig) StartTlsRenewalWorker(ctx context.Context) {
191194
}
192195

193196
// regenerate certificate with new CA
194-
err = generateCertAndKeyFiles(cfg.DomainName, cfg.TlsCertFile, cfg.TlsKeyFile, cfg.tlsCaConfig)
197+
err = generateCertAndKeyFiles(cfg.DomainName, cfg.TlsCertFile, cfg.TlsKeyFile, cfg.tlsCaConfig, cfg.IpAddr)
195198
if err != nil {
196199
log.Printf("[Config] Error regenerating TLS cert and key: %v\n", err)
197200
continue
198201
}
199202

200-
cfg.tlsConfig, err = getTlsCertKeyPair(cfg.TlsCertFile, cfg.TlsKeyFile, cfg.DomainName, cfg.tlsCaConfig)
203+
cfg.tlsConfig, err = getTlsCertKeyPair(cfg.TlsCertFile, cfg.TlsKeyFile, cfg.DomainName, cfg.tlsCaConfig, cfg.IpAddr)
201204
if err != nil {
202205
log.Printf("[Config] Error getting TLS config: %v\n", err)
203206
continue
@@ -224,7 +227,7 @@ func (cfg *AppConfig) StartTlsRenewalWorker(ctx context.Context) {
224227
if isCertUpForRenewal(cfg.tlsConfig, certBuffer) {
225228
log.Println("[Config] TLS certificate regenerating...")
226229

227-
err := generateCertAndKeyFiles(cfg.DomainName, cfg.TlsCertFile, cfg.TlsKeyFile, cfg.tlsCaConfig)
230+
err := generateCertAndKeyFiles(cfg.DomainName, cfg.TlsCertFile, cfg.TlsKeyFile, cfg.tlsCaConfig, cfg.IpAddr)
228231
if err != nil {
229232
log.Printf("[Config] Error regenerating TLS cert and key: %v\n", err)
230233
continue
@@ -236,7 +239,7 @@ func (cfg *AppConfig) StartTlsRenewalWorker(ctx context.Context) {
236239
continue
237240
}
238241

239-
cfg.tlsConfig, err = getTlsCertKeyPair(cfg.TlsCertFile, cfg.TlsKeyFile, cfg.DomainName, cfg.tlsCaConfig)
242+
cfg.tlsConfig, err = getTlsCertKeyPair(cfg.TlsCertFile, cfg.TlsKeyFile, cfg.DomainName, cfg.tlsCaConfig, cfg.IpAddr)
240243
if err != nil {
241244
log.Printf("[Config] Error loading TLS config: %v\n", err)
242245
continue
@@ -314,6 +317,7 @@ func BindFlags(cmd *cobra.Command) {
314317
cmd.PersistentFlags().StringVar(&flagTLSCaCertFile, "tls-ca-cert-file", DefaultTLSCaCertFile, "Path to QPI TLS certificate authority CA cert file")
315318
cmd.PersistentFlags().StringVar(&flagTLSCaKeyFile, "tls-ca-key-file", DefaultTLSCaKeyFile, "Path to QPI TLS certificate authority CA key file")
316319
cmd.PersistentFlags().StringVar(&flagDomainName, "domain", "", "The domain name this server is running on")
320+
cmd.PersistentFlags().StringVar(&flagIpAddr, "ip-addr", "127.0.0.1", "The public IP address to include in the generated TLS certificates")
317321
cmd.PersistentFlags().IntVar(&flagServerPort, "server-port", 8090, "The port this server should run on")
318322
cmd.PersistentFlags().StringVar(&flagCollectionQPUs, "qpus-collection", DefaultQpusCollection, "Collection name for QPUs")
319323
cmd.PersistentFlags().StringVar(&flagCollectionTimeSlots, "timeslots-collection", DefaultTimeSlotsCollection, "Collection name for Time Slots")
@@ -363,6 +367,7 @@ func NewFromFlags(cmd *cobra.Command) (*AppConfig, error) {
363367
cfg.TlsKeyFile = DefaultTLSKeyFile
364368
cfg.TlsCaCertFile = DefaultTLSCaCertFile
365369
cfg.TlsCaKeyFile = DefaultTLSCaKeyFile
370+
cfg.IpAddr = "127.0.0.1"
366371

367372
// Overlay Config File (if specified via env or flag)
368373
configFile := flagConfigFile
@@ -393,6 +398,7 @@ func NewFromFlags(cmd *cobra.Command) (*AppConfig, error) {
393398
TlsCaCertFile *string `json:"tlsCaCertFile" yaml:"tlsCaCertFile"`
394399
TlsCaKeyFile *string `json:"tlsCaKeyFile" yaml:"tlsCaKeyFile"`
395400
ServerPort *int `json:"serverPort" yaml:"serverPort"`
401+
IpAddr *string `json:"ipAddr" yaml:"ipAddr"`
396402
CollectionTimeSlots *string `json:"timeslotsCollection" yaml:"timeslotsCollection"`
397403
CollectionQuantumJobs *string `json:"jobsCollection" yaml:"jobsCollection"`
398404
CollectionAPITokens *string `json:"apiTokensCollection" yaml:"apiTokensCollection"`
@@ -434,6 +440,11 @@ func NewFromFlags(cmd *cobra.Command) (*AppConfig, error) {
434440
if fileCfg.ServerPort != nil {
435441
cfg.ServerPort = *fileCfg.ServerPort
436442
}
443+
if fileCfg.IpAddr != nil {
444+
if _, err := parseIpOrErr(*fileCfg.IpAddr); err == nil {
445+
cfg.IpAddr = *fileCfg.IpAddr
446+
}
447+
}
437448
if fileCfg.CollectionQPUs != nil {
438449
cfg.CollectionQPUs = *fileCfg.CollectionQPUs
439450
}
@@ -554,6 +565,7 @@ func NewFromFlags(cmd *cobra.Command) (*AppConfig, error) {
554565
cfg.TlsCaCertFile = resolveString("tls-ca-cert-file", "QPI_TLS_CA_CERT_FILE", cfg.TlsCaCertFile)
555566
cfg.TlsCaKeyFile = resolveString("tls-ca-key-file", "QPI_TLS_CA_KEY_FILE", cfg.TlsCaKeyFile)
556567
cfg.ServerPort = resolveInt("server-port", "QPI_SERVER_PORT", cfg.ServerPort)
568+
cfg.IpAddr = resolveString("ip-addr", "QPI_IP_ADDR", cfg.IpAddr)
557569
cfg.CollectionQPUs = resolveString("qpus-collection", "QPI_QPUS_COLLECTION", cfg.CollectionQPUs)
558570
cfg.CollectionTimeSlots = resolveString("timeslots-collection", "QPI_TIMESLOTS_COLLECTION", cfg.CollectionTimeSlots)
559571
cfg.CollectionQuantumJobs = resolveString("jobs-collection", "QPI_JOBS_COLLECTION", cfg.CollectionQuantumJobs)
@@ -606,7 +618,7 @@ func NewFromFlags(cmd *cobra.Command) (*AppConfig, error) {
606618
}
607619

608620
// Load the TLS
609-
cfg.tlsConfig, err = getTlsCertKeyPair(cfg.TlsCertFile, cfg.TlsKeyFile, cfg.DomainName, cfg.tlsCaConfig)
621+
cfg.tlsConfig, err = getTlsCertKeyPair(cfg.TlsCertFile, cfg.TlsKeyFile, cfg.DomainName, cfg.tlsCaConfig, cfg.IpAddr)
610622
if err != nil {
611623
return nil, fmt.Errorf("[Config] NewFromFlags error: %w", err)
612624
}
@@ -647,3 +659,13 @@ func fileExists(path string) bool {
647659

648660
return true
649661
}
662+
663+
// parseIp parsed the IP string, returning an error if invalid
664+
func parseIpOrErr(value string) (net.IP, error) {
665+
parsedIP := net.ParseIP(value)
666+
if parsedIP == nil {
667+
return nil, fmt.Errorf("invalid IP address: %s", value)
668+
}
669+
670+
return parsedIP, nil
671+
}

qpi-ui/internal/config/config_test.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ jobsCollection: "yaml_quantum_jobs"
2222
portRangeStart: 6005
2323
portRangeEnd: 7005
2424
idleThreshold: "12s"
25+
ipAddr: "127.0.0.1"
2526
disableEmailPasswordAuth: true
2627
tlsCertFile: "` + tmpDir + `/test.cert.pem"
2728
tlsKeyFile: "` + tmpDir + `/test.key"
@@ -104,6 +105,7 @@ func TestNewFromFlags_Json(t *testing.T) {
104105
jsonContent := `{
105106
"jobsCollection": "json_quantum_jobs",
106107
"portRangeStart": 8000,
108+
"ipAddr": "127.0.0.1",
107109
"tlsCertFile": "` + tmpDir + `/test.cert.pem",
108110
"tlsKeyFile": "` + tmpDir + `/test.key",
109111
"tlsCaCertFile": "` + tmpDir + `/test.ca.pem",
@@ -172,6 +174,7 @@ tlsCertFile: "` + certFile + `"
172174
tlsKeyFile: "` + keyFile + `"
173175
tlsCaCertFile: "` + caCertFile + `"
174176
tlsCaKeyFile: "` + caKeyFile + `"
177+
ipAddr: "127.0.0.1"
175178
serverPort: 8443
176179
`
177180
if err := os.WriteFile(configFile, []byte(yamlContent), 0644); err != nil {
@@ -225,6 +228,7 @@ func TestNewFromFlags_TlsFromEnv(t *testing.T) {
225228
t.Setenv("QPI_TLS_CA_CERT_FILE", caCertFile)
226229
t.Setenv("QPI_TLS_CA_KEY_FILE", caKeyFile)
227230
t.Setenv("QPI_SERVER_PORT", "9443")
231+
t.Setenv("QPI_IP_ADDR", "127.0.0.1")
228232
t.Setenv("QPI_CONFIG_FILE", emptyConfig)
229233

230234
cmd := &cobra.Command{}
@@ -279,6 +283,9 @@ func TestNewFromFlags_TlsFromFlags(t *testing.T) {
279283
if err := cmd.PersistentFlags().Set("server-port", "10443"); err != nil {
280284
t.Fatalf("failed to set server-port flag: %v", err)
281285
}
286+
if err := cmd.PersistentFlags().Set("ip-addr", "127.0.0.1"); err != nil {
287+
t.Fatalf("failed to set ip-addr flag: %v", err)
288+
}
282289

283290
cfg, err := NewFromFlags(cmd)
284291
if err != nil {
@@ -325,6 +332,9 @@ func TestNewFromFlags_TlsAutoGenerate(t *testing.T) {
325332
if err := cmd.PersistentFlags().Set("tls-ca-key-file", caKeyFile); err != nil {
326333
t.Fatalf("failed to set tls-ca-key-file flag: %v", err)
327334
}
335+
if err := cmd.PersistentFlags().Set("ip-addr", "127.0.0.1"); err != nil {
336+
t.Fatalf("failed to set ip-addr flag: %v", err)
337+
}
328338
if err := cmd.PersistentFlags().Set("domain", "test.local"); err != nil {
329339
t.Fatalf("failed to set domain flag: %v", err)
330340
}
@@ -378,7 +388,7 @@ func TestNewFromFlags_TlsReuseExisting(t *testing.T) {
378388
if err != nil {
379389
t.Fatalf("generateCA failed: %v", err)
380390
}
381-
err = generateCertAndKeyFiles("test.local", certFile, keyFile, caPair)
391+
err = generateCertAndKeyFiles("test.local", certFile, keyFile, caPair, "127.0.0.1" )
382392
if err != nil {
383393
t.Fatalf("generateCertAndKeyFiles failed: %v", err)
384394
}
@@ -402,6 +412,9 @@ func TestNewFromFlags_TlsReuseExisting(t *testing.T) {
402412
if err := cmd.PersistentFlags().Set("tls-ca-key-file", caKeyFile); err != nil {
403413
t.Fatalf("failed to set tls-ca-key-file flag: %v", err)
404414
}
415+
if err := cmd.PersistentFlags().Set("ip-addr", "127.0.0.1"); err != nil {
416+
t.Fatalf("failed to set ip-addr flag: %v", err)
417+
}
405418

406419
cfg, err := NewFromFlags(cmd)
407420
if err != nil {

0 commit comments

Comments
 (0)