diff --git a/internal/commands/activate/activate.go b/internal/commands/activate/activate.go index 878a4c590..d9027c6a2 100644 --- a/internal/commands/activate/activate.go +++ b/internal/commands/activate/activate.go @@ -21,6 +21,7 @@ import ( "github.com/device-management-toolkit/rpc-go/v2/internal/device" "github.com/device-management-toolkit/rpc-go/v2/internal/orchestrator" "github.com/device-management-toolkit/rpc-go/v2/internal/profile" + "github.com/device-management-toolkit/rpc-go/v2/pkg/utils" log "github.com/sirupsen/logrus" ) @@ -430,6 +431,13 @@ func (cmd *ActivateCmd) addDeviceToConsole(ctx *commands.Context, consoleBaseURL useTLS, allowSelfSigned := cmd.resolveTLSFlags(cfg) + isLMSAvailable := false + if cmd.WSMan != nil { + isLMSAvailable = cmd.WSMan.IsLMSAvailable() + } else { + isLMSAvailable = utils.DetectLMS(cmd.LocalTLSEnforced) + } + payload := device.DevicePayload{ GUID: guid, Hostname: hostname, @@ -440,6 +448,7 @@ func (cmd *ActivateCmd) addDeviceToConsole(ctx *commands.Context, consoleBaseURL MEBXPassword: mebxPassword, UseTLS: useTLS, AllowSelfSigned: allowSelfSigned, + IsLMSAvailable: isLMSAvailable, } if hasCIRA { diff --git a/internal/commands/amtinfo.go b/internal/commands/amtinfo.go index 55c308b4e..5489788e7 100644 --- a/internal/commands/amtinfo.go +++ b/internal/commands/amtinfo.go @@ -379,13 +379,14 @@ type syncPayload struct { } type syncDeviceInfo struct { - FWVersion string `json:"fwVersion"` - FWBuild string `json:"fwBuild"` - FWSku string `json:"fwSku"` - CurrentMode string `json:"currentMode"` - Features string `json:"features"` - IPAddress string `json:"ipAddress"` - LastUpdated time.Time `json:"lastUpdated"` + FWVersion string `json:"fwVersion"` + FWBuild string `json:"fwBuild"` + FWSku string `json:"fwSku"` + CurrentMode string `json:"currentMode"` + Features string `json:"features"` + IPAddress string `json:"ipAddress"` + LastUpdated time.Time `json:"lastUpdated"` + LMSInstalled bool `json:"lmsInstalled"` } // SyncDeviceInfo sends a PATCH to the provided endpoint URL with the device info payload @@ -397,13 +398,14 @@ func (s *InfoService) SyncDeviceInfo(ctx *Context, result *InfoResult, urlArg st payload := syncPayload{ GUID: result.UUID, DeviceInfo: syncDeviceInfo{ - FWVersion: result.AMT, - FWBuild: result.BuildNumber, - FWSku: result.SKU, - CurrentMode: result.ControlMode, - Features: result.Features, - IPAddress: bestIPAddress(result), - LastUpdated: time.Now(), + FWVersion: result.AMT, + FWBuild: result.BuildNumber, + FWSku: result.SKU, + CurrentMode: result.ControlMode, + Features: result.Features, + IPAddress: bestIPAddress(result), + LastUpdated: time.Now(), + LMSInstalled: utils.DetectLMS(s.localTLSEnforced), }, } diff --git a/internal/device/api.go b/internal/device/api.go index 06d46564b..e0fa10a4f 100644 --- a/internal/device/api.go +++ b/internal/device/api.go @@ -120,6 +120,7 @@ type DevicePayload struct { MPSPassword string `json:"mpspassword,omitempty"` UseTLS bool `json:"useTLS"` AllowSelfSigned bool `json:"allowSelfSigned"` + IsLMSAvailable bool `json:"isLMSAvailable"` } // AddDevice registers a device via POST to the devices API endpoint. diff --git a/internal/interfaces/wsman.go b/internal/interfaces/wsman.go index 133184921..582852dfb 100644 --- a/internal/interfaces/wsman.go +++ b/internal/interfaces/wsman.go @@ -35,6 +35,7 @@ import ( type WSMANer interface { SetupWsmanClient(username, password string, useTLS, logAMTMessages bool, tlsConfig *cryptotls.Config) error + IsLMSAvailable() bool Close() error Unprovision(int) (setupandconfiguration.Response, error) PartialUnprovision() (setupandconfiguration.Response, error) diff --git a/internal/local/amt/wsman.go b/internal/local/amt/wsman.go index aba8e144a..1476580ed 100644 --- a/internal/local/amt/wsman.go +++ b/internal/local/amt/wsman.go @@ -50,6 +50,7 @@ type GoWSMANMessages struct { wsmanMessages wsman.Messages target string localTransport *LocalTransport + lmsAvailable bool } func NewGoWSMANMessages(lmsAddress string) *GoWSMANMessages { @@ -78,6 +79,8 @@ func (g *GoWSMANMessages) SetupWsmanClient(username, password string, useTLS, lo LogAMTMessages: logAMTMessages, } + g.lmsAvailable = false + probeTimeout := time.Duration(utils.LMSDialerTimeout) * time.Second if clientParams.UseTLS { @@ -97,6 +100,8 @@ func (g *GoWSMANMessages) SetupWsmanClient(username, password string, useTLS, lo } else { logrus.Debug("Successfully connected to LMS.") + g.lmsAvailable = true + if tlsConn, ok := conn.(*cryptotls.Conn); ok { state := tlsConn.ConnectionState() cert := state.PeerCertificates[0] @@ -119,6 +124,9 @@ func (g *GoWSMANMessages) SetupWsmanClient(username, password string, useTLS, lo clientParams.Transport = g.localTransport } else { logrus.Debug("Successfully connected to LMS.") + + g.lmsAvailable = true + con.Close() } } @@ -128,6 +136,11 @@ func (g *GoWSMANMessages) SetupWsmanClient(username, password string, useTLS, lo return nil } +// IsLMSAvailable returns whether LMS was reachable during the last SetupWsmanClient call. +func (g *GoWSMANMessages) IsLMSAvailable() bool { + return g.lmsAvailable +} + // Close closes any open local transport connections func (g *GoWSMANMessages) Close() error { if g.localTransport != nil { diff --git a/internal/local/amt/wsman_test.go b/internal/local/amt/wsman_test.go index f2e97177c..91ca3e291 100644 --- a/internal/local/amt/wsman_test.go +++ b/internal/local/amt/wsman_test.go @@ -4,3 +4,46 @@ **********************************************************************/ package amt + +import ( + "context" + "net" + "testing" + + "github.com/device-management-toolkit/rpc-go/v2/pkg/utils" + "github.com/stretchr/testify/assert" +) + +func TestIsLMSAvailable_DefaultFalse(t *testing.T) { + g := NewGoWSMANMessages(utils.LMSAddress) + assert.False(t, g.IsLMSAvailable(), "should be false before SetupWsmanClient is called") +} + +func TestIsLMSAvailable_TrueWhenLMSListening(t *testing.T) { + // Start a temporary TCP listener simulating LMS on port 16992. + lc := &net.ListenConfig{} + + ln, err := lc.Listen(context.Background(), "tcp4", "127.0.0.1:"+utils.LMSPort) + if err != nil { + t.Skipf("cannot bind to port %s (may be in use): %v", utils.LMSPort, err) + } + defer ln.Close() + + go func() { + for { + conn, err := ln.Accept() + if err != nil { + return + } + + conn.Close() + } + }() + + g := NewGoWSMANMessages(utils.LMSAddress) + + // SetupWsmanClient with dummy credentials — the TCP probe happens before auth. + err = g.SetupWsmanClient("admin", "password", false, false, nil) + assert.NoError(t, err) + assert.True(t, g.IsLMSAvailable(), "should be true when LMS port is reachable") +} diff --git a/internal/mocks/wsman_mock.go b/internal/mocks/wsman_mock.go index 1c5f3f20b..9ec36246a 100644 --- a/internal/mocks/wsman_mock.go +++ b/internal/mocks/wsman_mock.go @@ -183,6 +183,20 @@ func (mr *MockWSMANerMockRecorder) AddWiFiSettings(wifiEndpointSettings, ieee802 return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddWiFiSettings", reflect.TypeOf((*MockWSMANer)(nil).AddWiFiSettings), wifiEndpointSettings, ieee8021xSettings, wifiEndpoint, clientCredential, caCredential) } +// Close mocks base method. +func (m *MockWSMANer) Close() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Close") + ret0, _ := ret[0].(error) + return ret0 +} + +// Close indicates an expected call of Close. +func (mr *MockWSMANerMockRecorder) Close() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockWSMANer)(nil).Close)) +} + // CommitChanges mocks base method. func (m *MockWSMANer) CommitChanges() (setupandconfiguration.Response, error) { m.ctrl.T.Helper() @@ -643,6 +657,20 @@ func (mr *MockWSMANerMockRecorder) HostBasedSetupServiceAdmin(password, digestRe return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HostBasedSetupServiceAdmin", reflect.TypeOf((*MockWSMANer)(nil).HostBasedSetupServiceAdmin), password, digestRealm, nonce, signature, isUpgrade) } +// IsLMSAvailable mocks base method. +func (m *MockWSMANer) IsLMSAvailable() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsLMSAvailable") + ret0, _ := ret[0].(bool) + return ret0 +} + +// IsLMSAvailable indicates an expected call of IsLMSAvailable. +func (mr *MockWSMANerMockRecorder) IsLMSAvailable() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsLMSAvailable", reflect.TypeOf((*MockWSMANer)(nil).IsLMSAvailable)) +} + // PUTTLSSettings mocks base method. func (m *MockWSMANer) PUTTLSSettings(instanceID string, tlsSettingData tls0.SettingDataRequest) (tls0.Response, error) { m.ctrl.T.Helper() @@ -969,17 +997,3 @@ func (mr *MockWSMANerMockRecorder) UpdateAMTPassword(passwordBase64 any) *gomock mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAMTPassword", reflect.TypeOf((*MockWSMANer)(nil).UpdateAMTPassword), passwordBase64) } - -// Close mocks base method. -func (m *MockWSMANer) Close() error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Close") - ret0, _ := ret[0].(error) - return ret0 -} - -// Close indicates an expected call of Close. -func (mr *MockWSMANerMockRecorder) Close() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockWSMANer)(nil).Close)) -} diff --git a/internal/rps/message.go b/internal/rps/message.go index aca5bcb9e..53a4730cd 100644 --- a/internal/rps/message.go +++ b/internal/rps/message.go @@ -60,6 +60,7 @@ type MessagePayload struct { FriendlyName string `json:"friendlyName,omitempty"` TLSEnforced bool `json:"tlsEnforced,omitempty"` TLSTunnel bool `json:"tlsTunnel,omitempty"` + LMSInstalled bool `json:"lmsInstalled"` } // MethodTLSData is the method type for TLS tunnel data passthrough @@ -240,6 +241,7 @@ func (p Payload) CreateMessageRequest(req Request) (Message, error) { payload.FriendlyName = req.FriendlyName payload.TLSEnforced = req.LocalTlsEnforced payload.TLSTunnel = req.TLSTunnel + payload.LMSInstalled = utils.DetectLMS(req.LocalTlsEnforced) // convert struct to json data, err := json.Marshal(payload) diff --git a/pkg/utils/lms.go b/pkg/utils/lms.go new file mode 100644 index 000000000..7194bf47f --- /dev/null +++ b/pkg/utils/lms.go @@ -0,0 +1,32 @@ +/********************************************************************* + * Copyright (c) Intel Corporation 2026 + * SPDX-License-Identifier: Apache-2.0 + **********************************************************************/ + +package utils + +import ( + "context" + "net" + "time" +) + +// DetectLMS probes the local LMS port to determine if Intel LMS is running. +// It uses the TLS port when localTLSEnforced is true. +func DetectLMS(localTLSEnforced bool) bool { + port := LMSPort + if localTLSEnforced { + port = LMSTLSPort + } + + dialer := &net.Dialer{Timeout: time.Duration(LMSDialerTimeout) * time.Second} + + conn, err := dialer.DialContext(context.Background(), "tcp4", net.JoinHostPort(LMSAddress, port)) + if err != nil { + return false + } + + conn.Close() + + return true +}