From 3dd21382bad5d43604850e257cfd1b8d35e16fb9 Mon Sep 17 00:00:00 2001 From: bbedward Date: Wed, 7 Jan 2026 14:13:03 -0500 Subject: [PATCH] network: support hidden SSIDs --- .../network/backend_networkmanager_wifi.go | 175 ++++++++++++++---- core/internal/server/network/types.go | 2 + quickshell/Modals/WifiPasswordModal.qml | 74 +++++++- quickshell/Modules/Settings/NetworkTab.qml | 29 +++ quickshell/Services/DMSNetworkService.qml | 8 +- quickshell/Services/PopoutService.qml | 8 +- 6 files changed, 249 insertions(+), 47 deletions(-) diff --git a/core/internal/server/network/backend_networkmanager_wifi.go b/core/internal/server/network/backend_networkmanager_wifi.go index 9eb42c66..1a8ef422 100644 --- a/core/internal/server/network/backend_networkmanager_wifi.go +++ b/core/internal/server/network/backend_networkmanager_wifi.go @@ -357,31 +357,51 @@ func (b *NetworkManagerBackend) updateWiFiNetworks() ([]WiFiNetwork, error) { savedSSIDs := make(map[string]bool) autoconnectMap := make(map[string]bool) + hiddenSSIDs := make(map[string]bool) for _, conn := range connections { connSettings, err := conn.GetSettings() if err != nil { continue } - if connMeta, ok := connSettings["connection"]; ok { - if connType, ok := connMeta["type"].(string); ok && connType == "802-11-wireless" { - if wifiSettings, ok := connSettings["802-11-wireless"]; ok { - if ssidBytes, ok := wifiSettings["ssid"].([]byte); ok { - ssid := string(ssidBytes) - savedSSIDs[ssid] = true - autoconnect := true - if ac, ok := connMeta["autoconnect"].(bool); ok { - autoconnect = ac - } - autoconnectMap[ssid] = autoconnect - } - } - } + connMeta, ok := connSettings["connection"] + if !ok { + continue + } + + connType, ok := connMeta["type"].(string) + if !ok || connType != "802-11-wireless" { + continue + } + + wifiSettings, ok := connSettings["802-11-wireless"] + if !ok { + continue + } + + ssidBytes, ok := wifiSettings["ssid"].([]byte) + if !ok { + continue + } + + ssid := string(ssidBytes) + savedSSIDs[ssid] = true + autoconnect := true + if ac, ok := connMeta["autoconnect"].(bool); ok { + autoconnect = ac + } + autoconnectMap[ssid] = autoconnect + + if hidden, ok := wifiSettings["hidden"].(bool); ok && hidden { + hiddenSSIDs[ssid] = true } } b.stateMutex.RLock() currentSSID := b.state.WiFiSSID + wifiConnected := b.state.WiFiConnected + wifiSignal := b.state.WiFiSignal + wifiBSSID := b.state.WiFiBSSID b.stateMutex.RUnlock() seenSSIDs := make(map[string]*WiFiNetwork) @@ -444,6 +464,7 @@ func (b *NetworkManagerBackend) updateWiFiNetworks() ([]WiFiNetwork, error) { Connected: ssid == currentSSID, Saved: savedSSIDs[ssid], Autoconnect: autoconnectMap[ssid], + Hidden: hiddenSSIDs[ssid], Frequency: freq, Mode: modeStr, Rate: maxBitrate / 1000, @@ -454,6 +475,23 @@ func (b *NetworkManagerBackend) updateWiFiNetworks() ([]WiFiNetwork, error) { networks = append(networks, network) } + if wifiConnected && currentSSID != "" { + if _, exists := seenSSIDs[currentSSID]; !exists { + hiddenNetwork := WiFiNetwork{ + SSID: currentSSID, + BSSID: wifiBSSID, + Signal: wifiSignal, + Secured: true, + Connected: true, + Saved: savedSSIDs[currentSSID], + Autoconnect: autoconnectMap[currentSSID], + Hidden: true, + Mode: "infrastructure", + } + networks = append(networks, hiddenNetwork) + } + } + sortWiFiNetworks(networks) b.stateMutex.Lock() @@ -515,40 +553,53 @@ func (b *NetworkManagerBackend) createAndConnectWiFiOnDevice(req ConnectionReque nm := b.nmConn.(gonetworkmanager.NetworkManager) dev := devInfo.device w := devInfo.wireless - apPaths, err := w.GetAccessPoints() - if err != nil { - return fmt.Errorf("failed to get access points: %w", err) - } var targetAP gonetworkmanager.AccessPoint - for _, ap := range apPaths { - ssid, err := ap.GetPropertySSID() - if err != nil || ssid != req.SSID { - continue + var flags, wpaFlags, rsnFlags uint32 + + if !req.Hidden { + apPaths, err := w.GetAccessPoints() + if err != nil { + return fmt.Errorf("failed to get access points: %w", err) } - targetAP = ap - break - } - if targetAP == nil { - return fmt.Errorf("access point not found: %s", req.SSID) - } + for _, ap := range apPaths { + ssid, err := ap.GetPropertySSID() + if err != nil || ssid != req.SSID { + continue + } + targetAP = ap + break + } - flags, _ := targetAP.GetPropertyFlags() - wpaFlags, _ := targetAP.GetPropertyWPAFlags() - rsnFlags, _ := targetAP.GetPropertyRSNFlags() + if targetAP == nil { + return fmt.Errorf("access point not found: %s", req.SSID) + } + + flags, _ = targetAP.GetPropertyFlags() + wpaFlags, _ = targetAP.GetPropertyWPAFlags() + rsnFlags, _ = targetAP.GetPropertyRSNFlags() + } const KeyMgmt8021x = uint32(512) const KeyMgmtPsk = uint32(256) const KeyMgmtSae = uint32(1024) - isEnterprise := (wpaFlags&KeyMgmt8021x) != 0 || (rsnFlags&KeyMgmt8021x) != 0 - isPsk := (wpaFlags&KeyMgmtPsk) != 0 || (rsnFlags&KeyMgmtPsk) != 0 - isSae := (wpaFlags&KeyMgmtSae) != 0 || (rsnFlags&KeyMgmtSae) != 0 + var isEnterprise, isPsk, isSae, secured bool - secured := flags != uint32(gonetworkmanager.Nm80211APFlagsNone) || - wpaFlags != uint32(gonetworkmanager.Nm80211APSecNone) || - rsnFlags != uint32(gonetworkmanager.Nm80211APSecNone) + switch { + case req.Hidden: + secured = req.Password != "" || req.Username != "" + isEnterprise = req.Username != "" + isPsk = req.Password != "" && !isEnterprise + default: + isEnterprise = (wpaFlags&KeyMgmt8021x) != 0 || (rsnFlags&KeyMgmt8021x) != 0 + isPsk = (wpaFlags&KeyMgmtPsk) != 0 || (rsnFlags&KeyMgmtPsk) != 0 + isSae = (wpaFlags&KeyMgmtSae) != 0 || (rsnFlags&KeyMgmtSae) != 0 + secured = flags != uint32(gonetworkmanager.Nm80211APFlagsNone) || + wpaFlags != uint32(gonetworkmanager.Nm80211APSecNone) || + rsnFlags != uint32(gonetworkmanager.Nm80211APSecNone) + } if isEnterprise { log.Infof("[createAndConnectWiFi] Enterprise network detected (802.1x) - SSID: %s, interactive: %v", @@ -567,11 +618,15 @@ func (b *NetworkManagerBackend) createAndConnectWiFiOnDevice(req ConnectionReque settings["ipv6"] = map[string]any{"method": "auto"} if secured { - settings["802-11-wireless"] = map[string]any{ + wifiSettings := map[string]any{ "ssid": []byte(req.SSID), "mode": "infrastructure", "security": "802-11-wireless-security", } + if req.Hidden { + wifiSettings["hidden"] = true + } + settings["802-11-wireless"] = wifiSettings switch { case isEnterprise || req.Username != "": @@ -658,10 +713,14 @@ func (b *NetworkManagerBackend) createAndConnectWiFiOnDevice(req ConnectionReque return fmt.Errorf("secured network but not SAE/PSK/802.1X (rsn=0x%x wpa=0x%x)", rsnFlags, wpaFlags) } } else { - settings["802-11-wireless"] = map[string]any{ + wifiSettings := map[string]any{ "ssid": []byte(req.SSID), "mode": "infrastructure", } + if req.Hidden { + wifiSettings["hidden"] = true + } + settings["802-11-wireless"] = wifiSettings } if req.Interactive { @@ -685,14 +744,23 @@ func (b *NetworkManagerBackend) createAndConnectWiFiOnDevice(req ConnectionReque log.Infof("[createAndConnectWiFi] Enterprise connection added, activating (secret agent will be called)") } - _, err = nm.ActivateWirelessConnection(conn, dev, targetAP) + if req.Hidden { + _, err = nm.ActivateConnection(conn, dev, nil) + } else { + _, err = nm.ActivateWirelessConnection(conn, dev, targetAP) + } if err != nil { return fmt.Errorf("failed to activate connection: %w", err) } log.Infof("[createAndConnectWiFi] Connection activation initiated, waiting for NetworkManager state changes...") } else { - _, err = nm.AddAndActivateWirelessConnection(settings, dev, targetAP) + var err error + if req.Hidden { + _, err = nm.AddAndActivateConnection(settings, dev) + } else { + _, err = nm.AddAndActivateWirelessConnection(settings, dev, targetAP) + } if err != nil { return fmt.Errorf("failed to connect: %w", err) } @@ -813,6 +881,7 @@ func (b *NetworkManagerBackend) updateAllWiFiDevices() { savedSSIDs := make(map[string]bool) autoconnectMap := make(map[string]bool) + hiddenSSIDs := make(map[string]bool) for _, conn := range connections { connSettings, err := conn.GetSettings() if err != nil { @@ -846,6 +915,10 @@ func (b *NetworkManagerBackend) updateAllWiFiDevices() { autoconnect = ac } autoconnectMap[ssid] = autoconnect + + if hidden, ok := wifiSettings["hidden"].(bool); ok && hidden { + hiddenSSIDs[ssid] = true + } } var devices []WiFiDevice @@ -939,6 +1012,7 @@ func (b *NetworkManagerBackend) updateAllWiFiDevices() { Connected: connected && apSSID == ssid, Saved: savedSSIDs[apSSID], Autoconnect: autoconnectMap[apSSID], + Hidden: hiddenSSIDs[apSSID], Frequency: freq, Mode: modeStr, Rate: maxBitrate / 1000, @@ -949,6 +1023,25 @@ func (b *NetworkManagerBackend) updateAllWiFiDevices() { seenSSIDs[apSSID] = &network networks = append(networks, network) } + + if connected && ssid != "" { + if _, exists := seenSSIDs[ssid]; !exists { + hiddenNetwork := WiFiNetwork{ + SSID: ssid, + BSSID: bssid, + Signal: signal, + Secured: true, + Connected: true, + Saved: savedSSIDs[ssid], + Autoconnect: autoconnectMap[ssid], + Hidden: true, + Mode: "infrastructure", + Device: name, + } + networks = append(networks, hiddenNetwork) + } + } + sortWiFiNetworks(networks) } diff --git a/core/internal/server/network/types.go b/core/internal/server/network/types.go index 8957f761..93448cfb 100644 --- a/core/internal/server/network/types.go +++ b/core/internal/server/network/types.go @@ -33,6 +33,7 @@ type WiFiNetwork struct { Connected bool `json:"connected"` Saved bool `json:"saved"` Autoconnect bool `json:"autoconnect"` + Hidden bool `json:"hidden"` Frequency uint32 `json:"frequency"` Mode string `json:"mode"` Rate uint32 `json:"rate"` @@ -127,6 +128,7 @@ type ConnectionRequest struct { AnonymousIdentity string `json:"anonymousIdentity,omitempty"` DomainSuffixMatch string `json:"domainSuffixMatch,omitempty"` Interactive bool `json:"interactive,omitempty"` + Hidden bool `json:"hidden,omitempty"` Device string `json:"device,omitempty"` EAPMethod string `json:"eapMethod,omitempty"` Phase2Auth string `json:"phase2Auth,omitempty"` diff --git a/quickshell/Modals/WifiPasswordModal.qml b/quickshell/Modals/WifiPasswordModal.qml index dafc15ef..19921850 100644 --- a/quickshell/Modals/WifiPasswordModal.qml +++ b/quickshell/Modals/WifiPasswordModal.qml @@ -11,6 +11,7 @@ FloatingWindow { property string wifiPasswordInput: "" property string wifiUsernameInput: "" property bool requiresEnterprise: false + property bool isHiddenNetwork: false property string wifiAnonymousIdentityInput: "" property string wifiDomainInput: "" @@ -44,6 +45,8 @@ FloatingWindow { property int calculatedHeight: { let h = headerHeight + buttonRowHeight + Theme.spacingL * 2; h += fieldsInfo.length * inputFieldWithSpacing; + if (isHiddenNetwork) + h += inputFieldWithSpacing; if (showUsernameField) h += inputFieldWithSpacing; if (showPasswordField) @@ -68,6 +71,10 @@ FloatingWindow { } return; } + if (isHiddenNetwork) { + ssidInput.forceActiveFocus(); + return; + } if (requiresEnterprise && !isVpnPrompt) { usernameInput.forceActiveFocus(); return; @@ -82,6 +89,7 @@ FloatingWindow { wifiAnonymousIdentityInput = ""; wifiDomainInput = ""; isPromptMode = false; + isHiddenNetwork = false; promptToken = ""; promptReason = ""; promptFields = []; @@ -100,6 +108,30 @@ FloatingWindow { Qt.callLater(focusFirstField); } + function showHidden() { + wifiPasswordSSID = ""; + wifiPasswordInput = ""; + wifiUsernameInput = ""; + wifiAnonymousIdentityInput = ""; + wifiDomainInput = ""; + isPromptMode = false; + isHiddenNetwork = true; + promptToken = ""; + promptReason = ""; + promptFields = []; + promptSetting = ""; + isVpnPrompt = false; + connectionName = ""; + vpnServiceType = ""; + connectionType = ""; + fieldsInfo = []; + secretValues = {}; + requiresEnterprise = false; + + visible = true; + Qt.callLater(focusFirstField); + } + function showFromPrompt(token, ssid, setting, fields, hints, reason, connType, connName, vpnService, fInfo) { isPromptMode = true; promptToken = token; @@ -184,8 +216,9 @@ FloatingWindow { } NetworkService.submitCredentials(promptToken, secrets, savePasswordCheckbox.checked); } else { + const ssid = isHiddenNetwork ? ssidInput.text : wifiPasswordSSID; const username = requiresEnterprise ? usernameInput.text : ""; - NetworkService.connectToWifi(wifiPasswordSSID, passwordInput.text, username, wifiAnonymousIdentityInput, wifiDomainInput); + NetworkService.connectToWifi(ssid, passwordInput.text, username, wifiAnonymousIdentityInput, wifiDomainInput, isHiddenNetwork); } hide(); @@ -196,6 +229,8 @@ FloatingWindow { passwordInput.text = ""; if (requiresEnterprise) usernameInput.text = ""; + if (isHiddenNetwork) + ssidInput.text = ""; } function clearAndClose() { @@ -215,6 +250,8 @@ FloatingWindow { return I18n.tr("Smartcard PIN"); if (isVpnPrompt) return I18n.tr("VPN Password"); + if (isHiddenNetwork) + return I18n.tr("Hidden Network"); return I18n.tr("Wi-Fi Password"); } minimumSize: Qt.size(420, calculatedHeight) @@ -236,6 +273,7 @@ FloatingWindow { usernameInput.text = ""; anonInput.text = ""; domainMatchInput.text = ""; + ssidInput.text = ""; for (var i = 0; i < dynamicFieldsRepeater.count; i++) { const item = dynamicFieldsRepeater.itemAt(i); if (item?.children[0]) @@ -296,6 +334,8 @@ FloatingWindow { return I18n.tr("Smartcard Authentication"); if (isVpnPrompt) return I18n.tr("Connect to VPN"); + if (isHiddenNetwork) + return I18n.tr("Connect to Hidden Network"); return I18n.tr("Connect to Wi-Fi"); } font.pixelSize: Theme.fontSizeLarge @@ -315,6 +355,8 @@ FloatingWindow { return I18n.tr("Enter credentials for ") + wifiPasswordSSID; if (isVpnPrompt) return I18n.tr("Enter password for ") + wifiPasswordSSID; + if (isHiddenNetwork) + return I18n.tr("Enter network name and password"); const prefix = requiresEnterprise ? I18n.tr("Enter credentials for ") : I18n.tr("Enter password for "); return prefix + wifiPasswordSSID; } @@ -357,6 +399,34 @@ FloatingWindow { } } + Rectangle { + width: parent.width + height: inputFieldHeight + radius: Theme.cornerRadius + color: Theme.surfaceHover + border.color: ssidInput.activeFocus ? Theme.primary : Theme.outlineStrong + border.width: ssidInput.activeFocus ? 2 : 1 + visible: isHiddenNetwork + + MouseArea { + anchors.fill: parent + onClicked: ssidInput.forceActiveFocus() + } + + DankTextField { + id: ssidInput + + anchors.fill: parent + font.pixelSize: Theme.fontSizeMedium + textColor: Theme.surfaceText + placeholderText: I18n.tr("Network Name (SSID)") + backgroundColor: "transparent" + enabled: root.visible + keyNavigationTab: passwordInput + onAccepted: passwordInput.forceActiveFocus() + } + } + Repeater { id: dynamicFieldsRepeater model: fieldsInfo @@ -696,6 +766,8 @@ FloatingWindow { } if (isVpnPrompt) return passwordInput.text.length > 0; + if (isHiddenNetwork) + return ssidInput.text.length > 0; return requiresEnterprise ? (usernameInput.text.length > 0 && passwordInput.text.length > 0) : passwordInput.text.length > 0; } opacity: enabled ? 1 : 0.5 diff --git a/quickshell/Modules/Settings/NetworkTab.qml b/quickshell/Modules/Settings/NetworkTab.qml index f13d1cce..7f99ced4 100644 --- a/quickshell/Modules/Settings/NetworkTab.qml +++ b/quickshell/Modules/Settings/NetworkTab.qml @@ -768,6 +768,13 @@ Item { anchors.verticalCenter: parent.verticalCenter spacing: Theme.spacingS + DankActionButton { + iconName: "wifi_find" + buttonSize: 32 + visible: NetworkService.backend === "networkmanager" && NetworkService.wifiEnabled && !NetworkService.wifiToggling + onClicked: PopoutService.showHiddenNetworkModal() + } + DankActionButton { iconName: "refresh" buttonSize: 32 @@ -1102,6 +1109,14 @@ Item { visible: isPinned anchors.verticalCenter: parent.verticalCenter } + + DankIcon { + name: "visibility_off" + size: 14 + color: Theme.surfaceVariantText + visible: modelData.hidden || false + anchors.verticalCenter: parent.verticalCenter + } } Row { @@ -1127,6 +1142,20 @@ Item { visible: modelData.saved } + StyledText { + text: "•" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + visible: modelData.hidden || false + } + + StyledText { + text: I18n.tr("Hidden") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + visible: modelData.hidden || false + } + StyledText { text: "•" font.pixelSize: Theme.fontSizeSmall diff --git a/quickshell/Services/DMSNetworkService.qml b/quickshell/Services/DMSNetworkService.qml index 56958f59..1a0ebbb0 100644 --- a/quickshell/Services/DMSNetworkService.qml +++ b/quickshell/Services/DMSNetworkService.qml @@ -413,7 +413,7 @@ Singleton { scanWifi(); } - function connectToWifi(ssid, password = "", username = "", anonymousIdentity = "", domainSuffixMatch = "") { + function connectToWifi(ssid, password = "", username = "", anonymousIdentity = "", domainSuffixMatch = "", hidden = false) { if (!networkAvailable || isConnecting) return; pendingConnectionSSID = ssid; @@ -427,6 +427,8 @@ Singleton { }; if (effectiveWifiDevice) params.device = effectiveWifiDevice; + if (hidden) + params.hidden = true; if (DMSService.apiVersion >= 7) { if (password || username) { @@ -611,8 +613,8 @@ Singleton { } } - function connectToWifiAndSetPreference(ssid, password, username = "", anonymousIdentity = "", domainSuffixMatch = "") { - connectToWifi(ssid, password, username, anonymousIdentity, domainSuffixMatch); + function connectToWifiAndSetPreference(ssid, password, username = "", anonymousIdentity = "", domainSuffixMatch = "", hidden = false) { + connectToWifi(ssid, password, username, anonymousIdentity, domainSuffixMatch, hidden); setNetworkPreference("wifi"); } diff --git a/quickshell/Services/PopoutService.qml b/quickshell/Services/PopoutService.qml index e9096d82..7ad41af1 100644 --- a/quickshell/Services/PopoutService.qml +++ b/quickshell/Services/PopoutService.qml @@ -415,8 +415,12 @@ Singleton { notificationModal?.close(); } - function showWifiPasswordModal() { - wifiPasswordModal?.show(); + function showWifiPasswordModal(ssid) { + wifiPasswordModal?.show(ssid); + } + + function showHiddenNetworkModal() { + wifiPasswordModal?.showHidden(); } function hideWifiPasswordModal() {