Skip to content

Commit 91cd4c4

Browse files
authored
DAOS-3985 control: Add ControlInterface to server config (#17367)
By default, the control plane server binds to 0.0.0.0, which means that it is listening to all addresses on all interfaces. In some cases, the admin may prefer to specify a single interface to be used for control plane traffic. When control_iface is set in daos_server.yml, the server will use the lowest IPv4 address on that interface as both the listen address and the address recorded in the management database. If the prometheus listener is configured, it will also use the same address found for the control interface. Signed-off-by: Michael MacDonald <[email protected]>
1 parent 154dbdc commit 91cd4c4

File tree

14 files changed

+399
-25
lines changed

14 files changed

+399
-25
lines changed

src/control/fault/code/codes.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//
22
// (C) Copyright 2018-2024 Intel Corporation.
3-
// (C) Copyright 2025 Hewlett Packard Enterprise Development LP
3+
// (C) Copyright 2025-2026 Hewlett Packard Enterprise Development LP
44
// (C) Copyright 2025 Google LLC
55
//
66
// SPDX-License-Identifier: BSD-2-Clause-Patent
@@ -205,6 +205,8 @@ const (
205205
ServerConfigBdevExcludeClash
206206
ServerConfigHugepagesDisabledWithNrSet
207207
ServerConfigScmHugeEnabled
208+
ServerConfigBadControlInterface
209+
ServerConfigControlInterfaceMismatch
208210
)
209211

210212
// SPDK library bindings codes

src/control/lib/telemetry/promexp/httpd.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//
22
// (C) Copyright 2021-2024 Intel Corporation.
3+
// (C) Copyright 2026 Hewlett Packard Enterprise Development LP
34
//
45
// SPDX-License-Identifier: BSD-2-Clause-Patent
56
//
@@ -29,9 +30,10 @@ type (
2930

3031
// ExporterConfig defines the configuration for the Prometheus exporter.
3132
ExporterConfig struct {
32-
Port int
33-
Title string
34-
Register RegMonFn
33+
Port int
34+
BindAddress string // optional: IP address to bind to (default: 0.0.0.0)
35+
Title string
36+
Register RegMonFn
3537
}
3638
)
3739

@@ -60,7 +62,11 @@ func StartExporter(ctx context.Context, log logging.Logger, cfg *ExporterConfig)
6062
return nil, errors.Wrap(err, "failed to register client monitor")
6163
}
6264

63-
listenAddress := fmt.Sprintf("0.0.0.0:%d", cfg.Port)
65+
bindAddr := cfg.BindAddress
66+
if bindAddr == "" {
67+
bindAddr = "0.0.0.0"
68+
}
69+
listenAddress := fmt.Sprintf("%s:%d", bindAddr, cfg.Port)
6470

6571
srv := http.Server{Addr: listenAddress}
6672
http.Handle("/metrics", promhttp.HandlerFor(

src/control/server/config/faults.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//
22
// (C) Copyright 2020-2024 Intel Corporation.
3-
// (C) Copyright 2025 Hewlett Packard Enterprise Development LP
3+
// (C) Copyright 2025-2026 Hewlett Packard Enterprise Development LP
44
//
55
// SPDX-License-Identifier: BSD-2-Clause-Patent
66
//
@@ -283,6 +283,25 @@ func FaultConfigEngineNUMAImbalance(nodeMap map[int]int) *fault.Fault {
283283
)
284284
}
285285

286+
// FaultConfigBadControlInterface creates a fault for an invalid control plane network interface.
287+
func FaultConfigBadControlInterface(iface string, err error) *fault.Fault {
288+
return serverConfigFault(
289+
code.ServerConfigBadControlInterface,
290+
fmt.Sprintf("control_iface %q is invalid: %s", iface, err),
291+
"update the 'control_iface' parameter with a valid network interface and restart",
292+
)
293+
}
294+
295+
// FaultConfigControlInterfaceMismatch creates a fault when the control interface address
296+
// doesn't match the configured MS replica address.
297+
func FaultConfigControlInterfaceMismatch(ifaceAddr, replicaAddr string) *fault.Fault {
298+
return serverConfigFault(
299+
code.ServerConfigControlInterfaceMismatch,
300+
fmt.Sprintf("control_iface address %s doesn't match configured MS replica address %s", ifaceAddr, replicaAddr),
301+
"ensure 'control_iface' specifies an interface with an address matching this server's entry in 'mgmt_svc_replicas'",
302+
)
303+
}
304+
286305
func serverConfigFault(code code.Code, desc, res string) *fault.Fault {
287306
return &fault.Fault{
288307
Domain: "serverconfig",

src/control/server/config/server.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//
22
// (C) Copyright 2020-2024 Intel Corporation.
3-
// (C) Copyright 2025 Hewlett Packard Enterprise Development LP
3+
// (C) Copyright 2025-2026 Hewlett Packard Enterprise Development LP
44
//
55
// SPDX-License-Identifier: BSD-2-Clause-Patent
66
//
@@ -58,6 +58,7 @@ type deprecatedParams struct {
5858
type Server struct {
5959
// control-specific
6060
ControlPort int `yaml:"port"`
61+
ControlInterface string `yaml:"control_iface,omitempty"`
6162
TransportConfig *security.TransportConfig `yaml:"transport_config"`
6263
Engines []*engine.Config `yaml:"engines"`
6364
BdevExclude []string `yaml:"bdev_exclude,omitempty"`
@@ -231,6 +232,12 @@ func (cfg *Server) WithControlPort(port int) *Server {
231232
return cfg
232233
}
233234

235+
// WithControlInterface sets the network interface for the control plane listener.
236+
func (cfg *Server) WithControlInterface(iface string) *Server {
237+
cfg.ControlInterface = iface
238+
return cfg
239+
}
240+
234241
// WithTransportConfig sets the gRPC transport configuration.
235242
func (cfg *Server) WithTransportConfig(cfgTransport *security.TransportConfig) *Server {
236243
cfg.TransportConfig = cfgTransport

src/control/server/config/server_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ func TestServerConfig_Constructed(t *testing.T) {
240240
// possible to construct an identical configuration with the helpers.
241241
constructed := DefaultServer().
242242
WithControlPort(10001).
243+
WithControlInterface("eth0").
243244
WithControlMetadata(storage.ControlMetadata{
244245
Path: "/home/daos_server/control_meta",
245246
DevicePath: "/dev/sdb1",

src/control/server/server.go

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//
22
// (C) Copyright 2018-2024 Intel Corporation.
3-
// (C) Copyright 2025 Hewlett Packard Enterprise Development LP
3+
// (C) Copyright 2025-2026 Hewlett Packard Enterprise Development LP
44
//
55
// SPDX-License-Identifier: BSD-2-Clause-Patent
66
//
@@ -308,21 +308,37 @@ func (srv *server) setCoreDumpFilter() error {
308308
func (srv *server) initNetwork() error {
309309
defer srv.logDuration(track("time to init network"))
310310

311-
ctlAddr, err := getControlAddr(ctlAddrParams{
311+
params := ctlAddrParams{
312312
port: srv.cfg.ControlPort,
313313
replicaAddrSrc: srv.sysdb,
314314
lookupHost: net.LookupIP,
315-
})
315+
}
316+
317+
// If a control interface is configured, look it up and pass it to getControlAddr.
318+
// Also track whether we should bind to a specific IP (only when control_iface is set).
319+
bindToCtlAddr := false
320+
if srv.cfg.ControlInterface != "" {
321+
iface, err := net.InterfaceByName(srv.cfg.ControlInterface)
322+
if err != nil {
323+
return config.FaultConfigBadControlInterface(srv.cfg.ControlInterface, err)
324+
}
325+
params.ctlIface = iface
326+
bindToCtlAddr = true
327+
srv.log.Debugf("using control interface %s for listener", srv.cfg.ControlInterface)
328+
}
329+
330+
ctlAddr, err := getControlAddr(params)
316331
if err != nil {
317332
return err
318333
}
319334

320-
listener, err := createListener(ctlAddr, net.Listen)
335+
listener, err := createListener(ctlAddr, net.Listen, bindToCtlAddr)
321336
if err != nil {
322337
return err
323338
}
324339
srv.ctlAddr = ctlAddr
325340
srv.listener = listener
341+
srv.log.Debugf("control plane listener bound to %s", ctlAddr)
326342

327343
return nil
328344
}

src/control/server/server_utils.go

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//
22
// (C) Copyright 2021-2024 Intel Corporation.
3-
// (C) Copyright 2025 Hewlett Packard Enterprise Development LP
3+
// (C) Copyright 2025-2026 Hewlett Packard Enterprise Development LP
44
//
55
// SPDX-License-Identifier: BSD-2-Clause-Patent
66
//
@@ -150,11 +150,30 @@ type ctlAddrParams struct {
150150
port int
151151
replicaAddrSrc replicaAddrGetter
152152
lookupHost ipLookupFn
153+
ctlIface netInterface // optional: if set, use this interface for bind address
153154
}
154155

155156
func getControlAddr(params ctlAddrParams) (*net.TCPAddr, error) {
156-
ipStr := "0.0.0.0"
157+
// If a control interface is configured, use its first IPv4 address.
158+
if params.ctlIface != nil {
159+
ip, err := getFirstIPv4Addr(params.ctlIface)
160+
if err != nil {
161+
return nil, errors.Wrap(err, "getting control interface address")
162+
}
163+
164+
// If this node is a replica, verify the control interface address matches
165+
// the configured replica address. A mismatch would break raft connectivity.
166+
if repAddr, err := params.replicaAddrSrc.ReplicaAddr(); err == nil {
167+
if !repAddr.IP.Equal(ip) {
168+
return nil, config.FaultConfigControlInterfaceMismatch(ip.String(), repAddr.IP.String())
169+
}
170+
}
157171

172+
return &net.TCPAddr{IP: ip, Port: params.port}, nil
173+
}
174+
175+
// Fall back to legacy behavior: use replica address if available, otherwise 0.0.0.0.
176+
ipStr := "0.0.0.0"
158177
if repAddr, err := params.replicaAddrSrc.ReplicaAddr(); err == nil {
159178
ipStr = repAddr.IP.String()
160179
}
@@ -167,11 +186,17 @@ func getControlAddr(params ctlAddrParams) (*net.TCPAddr, error) {
167186
return ctlAddr, nil
168187
}
169188

170-
func createListener(ctlAddr *net.TCPAddr, listen netListenFn) (net.Listener, error) {
189+
func createListener(ctlAddr *net.TCPAddr, listen netListenFn, bindToCtlAddr bool) (net.Listener, error) {
171190
// Create and start listener on management network.
172-
lis, err := listen("tcp4", fmt.Sprintf("0.0.0.0:%d", ctlAddr.Port))
191+
// Only bind to ctlAddr.IP if explicitly requested (i.e., control_iface is set),
192+
// otherwise bind to all interfaces (0.0.0.0) for backwards compatibility.
193+
bindAddr := fmt.Sprintf("0.0.0.0:%d", ctlAddr.Port)
194+
if bindToCtlAddr {
195+
bindAddr = ctlAddr.String()
196+
}
197+
lis, err := listen("tcp4", bindAddr)
173198
if err != nil {
174-
return nil, errors.Wrap(err, "unable to listen on management interface")
199+
return nil, errors.Wrapf(err, "unable to listen on %s", bindAddr)
175200
}
176201

177202
return lis, nil
@@ -730,9 +755,12 @@ func registerTelemetryCallbacks(ctx context.Context, srv *server) {
730755
return
731756
}
732757

758+
// Use the same bind address as the control plane listener.
759+
bindAddr := srv.ctlAddr.IP.String()
760+
733761
srv.OnEnginesStarted(func(ctxIn context.Context) error {
734762
srv.log.Debug("starting Prometheus exporter")
735-
cleanup, err := startPrometheusExporter(ctxIn, srv.log, telemPort, srv.harness.Instances())
763+
cleanup, err := startPrometheusExporter(ctxIn, srv.log, telemPort, bindAddr, srv.harness.Instances())
736764
if err != nil {
737765
return err
738766
}
@@ -875,6 +903,35 @@ type netInterface interface {
875903
Addrs() ([]net.Addr, error)
876904
}
877905

906+
// getFirstIPv4Addr returns the first (lowest) IPv4 address from the interface.
907+
// If multiple IPv4 addresses exist, the lowest one is returned for determinism.
908+
func getFirstIPv4Addr(iface netInterface) (net.IP, error) {
909+
addrs, err := iface.Addrs()
910+
if err != nil {
911+
return nil, errors.Wrap(err, "failed to get interface addresses")
912+
}
913+
914+
var ipv4s []net.IP
915+
for _, a := range addrs {
916+
if ipNet, ok := a.(*net.IPNet); ok && ipNet.IP != nil {
917+
if v4 := ipNet.IP.To4(); v4 != nil {
918+
ipv4s = append(ipv4s, v4)
919+
}
920+
}
921+
}
922+
923+
if len(ipv4s) == 0 {
924+
return nil, errors.New("no IPv4 addresses on interface")
925+
}
926+
927+
// Sort for deterministic selection (lowest address first).
928+
sort.Slice(ipv4s, func(i, j int) bool {
929+
return bytes.Compare(ipv4s[i], ipv4s[j]) < 0
930+
})
931+
932+
return ipv4s[0], nil
933+
}
934+
878935
func getSrxSetting(cfg *config.Server) (int32, error) {
879936
if len(cfg.Engines) == 0 {
880937
return -1, nil

0 commit comments

Comments
 (0)