Merge main into ui-refactor; port MDM support to the Wails UI

Integrates main's MDM configuration-profile feature and adapts it to the
Wails UI (this branch had already replaced the Fyne UI).

Conflict resolution:
- go.mod/go.sum: take main's deps; howett.net/plist pinned to v1.0.2-... (tidy)
- client/proto/daemon.pb.go: regenerated from the merged daemon.proto
- client/internal/peer/status.go: union of ipToKey (main) + sessionExpiresAt (HEAD)
- client/server/server.go: main's intent/liveness model (connectionGoroutineRunning,
  clientRunning no longer cleared by the goroutine) + empty-PSK guard
- client/ui/client_ui.go, client/ui/profile.go: removed (dead Fyne UI)

MDM port (backend + tray):
- services/settings.go: expose MDMManagedFields plus a managedFields map keyed
  by Config field names so the settings form can gate a control without
  translating mdm.Key* names
- tray: gate Profiles / Exit Node menus on DisableProfiles / DisableNetworks via
  GetFeatures, refreshed on the config_changed system event (replaces the legacy
  2s poll); localized MDM policy-applied toast in all shipped locales
- client/proto/metadata.go: shared constants for the config_changed /
  policy_applied event markers

PreSharedKey: GetConfig now returns preSharedKeySet (bool) instead of the masked
value; the settings form provides its own placeholder and sends a new key only
when the user types one.
This commit is contained in:
Zoltan Papp
2026-06-12 15:27:23 +02:00
56 changed files with 3639 additions and 314 deletions
@@ -26,7 +26,10 @@ type SettingsContextValue = {
guiVersion: string;
setField: <K extends keyof Config>(k: K, v: Config[K]) => void;
saveField: <K extends keyof Config>(k: K, v: Config[K]) => Promise<void>;
saveFields: (partial: Partial<Config>) => Promise<void>;
// opts.preSharedKey carries a new PSK to write. Config no longer exposes the
// PSK value (only preSharedKeySet), so it rides alongside the Config fields
// here and is sent only when non-empty.
saveFields: (partial: Partial<Config>, opts?: { preSharedKey?: string }) => Promise<void>;
saveNow: () => Promise<void>;
};
@@ -104,13 +107,14 @@ const useSettingsState = () => {
);
const save = useCallback(
async (profileName: string, next: Config) => {
// Sending the "**********" PSK mask back corrupts the stored PSK (wgtypes.ParseKey fails next connect).
const { preSharedKey, ...rest } = next;
async (profileName: string, next: Config, preSharedKey?: string) => {
try {
await SettingsSvc.SetConfig({
...rest,
...(preSharedKey === "**********" ? {} : { preSharedKey }),
...next,
// The daemon never returns the PSK value (only preSharedKeySet),
// so send one only when the user actually typed a new key; an
// empty field means "leave unchanged", never "clear".
...(preSharedKey ? { preSharedKey } : {}),
profileName,
username,
});
@@ -163,7 +167,7 @@ const useSettingsState = () => {
);
const saveFields = useCallback(
async (partial: Partial<Config>) => {
async (partial: Partial<Config>, opts?: { preSharedKey?: string }) => {
if (!loaded) return;
if (saveTimer.current) {
clearTimeout(saveTimer.current);
@@ -171,7 +175,7 @@ const useSettingsState = () => {
}
const next = { ...loaded.data, ...partial };
setLoaded({ profileName: loaded.profileName, data: next });
await save(loaded.profileName, next);
await save(loaded.profileName, next, opts?.preSharedKey);
},
[loaded, save],
);