vmnet-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
vmnet_cli/commands.py ADDED
@@ -0,0 +1,383 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """vmnet_cli.commands - see vmnet_cli.cli for the tool overview."""
3
+
4
+ import sys
5
+ import time
6
+ import winreg
7
+
8
+ from .adapters import (
9
+ _adapter_present,
10
+ classify_interfaces,
11
+ install_adapter,
12
+ install_adapter_start,
13
+ physical_interfaces,
14
+ uninstall_adapter,
15
+ )
16
+ from .batch import batch_apply, batch_new, remove_stage
17
+ from .conf import ensure_nat, regen_nat_conf, set_dhcp_param
18
+ from .registry import CFG, reg_bridge_bindings, reg_bridge_del, reg_bridge_set, reg_exists, reg_flag
19
+ from .services import cycle_bridge, restart_services
20
+ from .vnl import FEATURE_BITS, pick_vnet, require_vnet, vnet_in_use
21
+
22
+
23
+ def cmd_adapters(vl):
24
+ iface = physical_interfaces()
25
+ bound = reg_bridge_bindings()
26
+ for name, guid in sorted(iface.items(), key=lambda kv: kv[1]):
27
+ v = bound.get(guid)
28
+ print(f"{name:30} {guid} -> {'vmnet' + str(v) if v is not None else 'vmnet0 (auto)'}")
29
+
30
+
31
+ def cmd_list(vl):
32
+ bindings = reg_bridge_bindings()
33
+ for n in range(0, vl.count()):
34
+ f = vl.features(f"vmnet{n}") or 0
35
+ if not f:
36
+ # VNL_GetVNetFeatures can transiently return 0 while PnP or the
37
+ # services settle; fall back to registry truth so a configured
38
+ # network never vanishes from the list mid-flight.
39
+ vnet = f"vmnet{n}"
40
+ if reg_exists(CFG + "\\" + vnet):
41
+ if reg_flag(vnet, "DHCP"):
42
+ f |= 0x1000000
43
+ if reg_flag(vnet, "NAT"):
44
+ f |= 0x10000
45
+ if reg_flag(vnet, "Adapter"):
46
+ f |= 0x100
47
+ if not f:
48
+ f = 1
49
+ if any(v == n for v in bindings.values()):
50
+ f |= 1
51
+ if not f:
52
+ continue
53
+ bits = ", ".join(v for b, v in FEATURE_BITS.items() if f & b)
54
+ name = vl.get_str("VNL_GetVnetDisplayName", f"vmnet{n}")
55
+ addr = vl.get_str("VNL_GetVnetSubnetAddr", f"vmnet{n}")
56
+ pins = [g for g, v in bindings.items() if v == n]
57
+ extra = f" pinned={','.join(pins)}" if pins else ""
58
+ print(f"vmnet{n:<2} {bits:40} name={name!r} subnet={addr}{extra}")
59
+
60
+
61
+ def cmd_add(vl, args):
62
+ if args.vnet is not None:
63
+ require_vnet(vl, args.vnet)
64
+ if args.vnet == 0:
65
+ sys.exit(
66
+ "vmnet0 is the built-in bridge segment; it cannot be re-typed "
67
+ "(segment 0 lives in the VMnetBridge driver, not in config)"
68
+ )
69
+ if args.vnet is not None and vnet_in_use(vl, args.vnet):
70
+ sys.exit(f"vmnet{args.vnet} is already in use; pick another --vnet or remove it first")
71
+ n = pick_vnet(vl, args.vnet)
72
+ vnet = f"vmnet{n}"
73
+ is_nat = args.kind == "nat"
74
+ if args.subnet:
75
+ vl.set_str("VNL_SetVnetSubnetAddr", vnet, args.subnet)
76
+ vl.set_str("VNL_SetVnetSubnetMask", vnet, args.mask or "255.255.255.0")
77
+ else:
78
+ if vl._c("VNL_AssignSubnetAddressToVnet", vnet.encode(), 0) != 1:
79
+ sys.exit("could not auto-assign a free subnet; pass --subnet")
80
+ subnet = vl.get_str("VNL_GetVnetSubnetAddr", vnet)
81
+ dhcp = True if is_nat else args.dhcp != "off"
82
+ print(
83
+ f"* add {args.kind}: {vnet} name={args.name!r} subnet={subnet}"
84
+ f" dhcp={'on' if dhcp else 'off'} nat={'on' if is_nat else 'off'}"
85
+ )
86
+ vl.set_str("VNL_SetVnetDisplayName", vnet, args.name)
87
+ ensure_nat(vnet, is_nat)
88
+ vl.set_use_dhcp(vnet, 1 if dhcp else 0)
89
+ ok = install_adapter(vnet)
90
+ print(f" - host adapter device: {'ok' if ok else 'pending/failed (PnP is async)'}")
91
+ # write adapter config regardless: the device may still arrive after this call
92
+ with winreg.CreateKey(winreg.HKEY_LOCAL_MACHINE, f"{CFG}\\{vnet}\\Adapter") as k:
93
+ winreg.SetValueEx(k, "UseAdapter", 0, winreg.REG_DWORD, 1) # no VNL setter exists
94
+ hostip = subnet.rsplit(".", 1)[0] + ".1" if subnet else "0.0.0.1"
95
+ vl._c("VNL_SetVnetAdapterAddr", vnet.encode(), hostip.encode())
96
+ if is_nat:
97
+ regen_nat_conf(vl)
98
+ vl.update_dhcp(vnet)
99
+ vl.update_adapter()
100
+ restart_services(dhcp=dhcp, nat=is_nat)
101
+ want = 0x100 | (0x10000 if is_nat else 0) | (0x1000000 if dhcp else 0)
102
+ f = 0
103
+ for _ in range(10): # adapter/NAT bits settle asynchronously (RECON §7)
104
+ f = vl.features(vnet) or 0
105
+ if f & want == want:
106
+ break
107
+ time.sleep(2)
108
+ print(f" {'OK' if f & want == want else 'WARNING'}: features=0x{f:08x}")
109
+
110
+
111
+ def cmd_edit(vl, args):
112
+ vnet = f"vmnet{args.vnet}"
113
+ n = args.vnet
114
+ require_vnet(vl, n)
115
+ if n == 0 and args.type:
116
+ sys.exit("vmnet0 is the built-in bridge segment; it cannot be re-typed")
117
+ if vl.features(vnet) is None:
118
+ sys.exit(f"{vnet} not readable")
119
+ dhcp_changed = nat_changed = subnet_changed = False
120
+ if args.type:
121
+ if args.type == "nat":
122
+ ensure_nat(vnet, True)
123
+ vl.set_use_dhcp(vnet, 1)
124
+ dhcp_changed = nat_changed = True
125
+ if not _adapter_present(vnet):
126
+ install_adapter(vnet)
127
+ with winreg.CreateKey(winreg.HKEY_LOCAL_MACHINE, f"{CFG}\\{vnet}\\Adapter") as k:
128
+ winreg.SetValueEx(k, "UseAdapter", 0, winreg.REG_DWORD, 1)
129
+ elif args.type == "hostonly":
130
+ ensure_nat(vnet, False)
131
+ nat_changed = True
132
+ if not _adapter_present(vnet):
133
+ install_adapter(vnet)
134
+ with winreg.CreateKey(winreg.HKEY_LOCAL_MACHINE, f"{CFG}\\{vnet}\\Adapter") as k:
135
+ winreg.SetValueEx(k, "UseAdapter", 0, winreg.REG_DWORD, 1)
136
+ else: # bridged: strip NAT/DHCP/adapter; bind a NIC with the `bridged` command
137
+ ensure_nat(vnet, False)
138
+ vl.set_use_dhcp(vnet, 0)
139
+ nat_changed = dhcp_changed = True
140
+ if _adapter_present(vnet) or reg_flag(vnet, "Adapter"):
141
+ uninstall_adapter(vnet, n)
142
+ print(f"* {vnet} connection type -> {args.type}")
143
+ if args.adapter:
144
+ if args.adapter == "on":
145
+ install_adapter(vnet)
146
+ with winreg.CreateKey(winreg.HKEY_LOCAL_MACHINE, f"{CFG}\\{vnet}\\Adapter") as k:
147
+ winreg.SetValueEx(k, "UseAdapter", 0, winreg.REG_DWORD, 1)
148
+ subnet = vl.get_str("VNL_GetVnetSubnetAddr", vnet)
149
+ if subnet:
150
+ vl._c(
151
+ "VNL_SetVnetAdapterAddr",
152
+ vnet.encode(),
153
+ (subnet.rsplit(".", 1)[0] + ".1").encode(),
154
+ )
155
+ else:
156
+ uninstall_adapter(vnet, n)
157
+ print(f"* {vnet} host adapter {args.adapter}")
158
+ if args.name is not None:
159
+ vl.set_str("VNL_SetVnetDisplayName", vnet, args.name)
160
+ print(f"* {vnet} DisplayName={args.name!r}")
161
+ if args.subnet or args.mask:
162
+ if args.subnet:
163
+ vl.set_str("VNL_SetVnetSubnetAddr", vnet, args.subnet)
164
+ vl.set_str(
165
+ "VNL_SetVnetSubnetMask", vnet, args.mask or vl.get_str("VNL_GetVnetSubnetMask", vnet)
166
+ )
167
+ subnet_changed = True
168
+ print(
169
+ f"* {vnet} subnet={vl.get_str('VNL_GetVnetSubnetAddr', vnet)}/{vl.get_str('VNL_GetVnetSubnetMask', vnet)}"
170
+ )
171
+ for key, val in (
172
+ ("begin", args.dhcp_begin),
173
+ ("end", args.dhcp_end),
174
+ ("default_lease", args.lease),
175
+ ("max_lease", args.max_lease),
176
+ ):
177
+ if val is not None:
178
+ rc = set_dhcp_param(vl, vnet, key, val)
179
+ print(f"* {vnet} DHCP {key}={val}: {'ok' if rc == 1 else 'rc=' + str(rc)}")
180
+ dhcp_changed = True
181
+ if args.dhcp:
182
+ vl.set_use_dhcp(vnet, 1 if args.dhcp == "on" else 0)
183
+ dhcp_changed = True
184
+ print(f"* {vnet} UseDHCP={args.dhcp}")
185
+ if args.nat:
186
+ ensure_nat(vnet, args.nat == "on")
187
+ nat_changed = True
188
+ print(f"* {vnet} UseNAT={args.nat}")
189
+ if subnet_changed or dhcp_changed:
190
+ vl.update_dhcp(vnet)
191
+ if subnet_changed or nat_changed:
192
+ regen_nat_conf(vl)
193
+ if subnet_changed or dhcp_changed or nat_changed:
194
+ restart_services(dhcp=subnet_changed or dhcp_changed, nat=subnet_changed or nat_changed)
195
+ print(f" features=0x{(vl.features(vnet) or 0):08x}")
196
+
197
+
198
+ def cmd_restore(vl, args):
199
+ if not args.force:
200
+ sys.exit("restore-defaults removes ALL custom networks; pass --force")
201
+ print("* restoring default networks (vmnet0 auto-bridge, vmnet1 host-only, vmnet8 NAT)")
202
+ ctx = batch_new()
203
+ for n in range(2, vl.count()):
204
+ if n in (1, 8):
205
+ continue
206
+ if reg_exists(f"{CFG}\\vmnet{n}"):
207
+ remove_stage(vl, n, ctx)
208
+ for n, is_nat, subnet in ((1, False, "192.168.14.0"), (8, True, "192.168.251.0")):
209
+ vnet = f"vmnet{n}"
210
+ if vl.features(vnet):
211
+ continue
212
+ vl.set_str("VNL_SetVnetSubnetAddr", vnet, subnet)
213
+ vl.set_str("VNL_SetVnetSubnetMask", vnet, "255.255.255.0")
214
+ vl.set_use_dhcp(vnet, 1)
215
+ if is_nat:
216
+ ensure_nat(vnet, True)
217
+ install_adapter_start(vnet) # kicked now, awaited together in apply
218
+ ctx["added"].append(n)
219
+ with winreg.CreateKey(winreg.HKEY_LOCAL_MACHINE, f"{CFG}\\{vnet}\\Adapter") as k:
220
+ winreg.SetValueEx(k, "UseAdapter", 0, winreg.REG_DWORD, 1)
221
+ vl._c("VNL_SetVnetAdapterAddr", vnet.encode(), (subnet.rsplit(".", 1)[0] + ".1").encode())
222
+ print(f" - recreated {vnet} ({'nat' if is_nat else 'hostonly'}) {subnet}")
223
+ vl.set_str("VNL_SetVnetDisplayName", "vmnet0", "VMnet0")
224
+ # UpdateDHCPFromConfig edits each segment's block in place — regen EVERY vnet
225
+ # (including 0) so conf blocks for removed/stale segments are purged too.
226
+ for n in range(vl.count()):
227
+ vl.update_dhcp(f"vmnet{n}")
228
+ ctx["dhcp"] = ctx["nat"] = True # one restart for the whole restore
229
+ batch_apply(vl, ctx)
230
+
231
+
232
+ def cmd_bridged(vl, args):
233
+ iface = physical_interfaces()
234
+ if args.adapter not in iface:
235
+ sys.exit(
236
+ f"ERROR: no network interface named {args.adapter!r} on this host. "
237
+ f"Available: {', '.join(sorted(iface))}"
238
+ )
239
+ guid = iface[args.adapter]
240
+ if args.vnet is not None:
241
+ require_vnet(vl, args.vnet)
242
+ if args.vnet == 0:
243
+ sys.exit("vmnet0 is the built-in bridge segment; it cannot be pinned")
244
+ n = args.vnet
245
+ else:
246
+ taken = set(reg_bridge_bindings().values())
247
+ c = vl.count()
248
+ n = next((k for k in range(2, c) if k not in taken and not vnet_in_use(vl, k)), None)
249
+ if n is None:
250
+ raise SystemExit(f"no free vmnet segment (2..{c - 1})")
251
+ vnet = f"vmnet{n}"
252
+ print(f"* {args.name}: {vnet} bridged to interface {args.adapter!r} ({guid})")
253
+ reg_bridge_set(guid, n)
254
+ vl.set_str("VNL_SetVnetDisplayName", vnet, args.name)
255
+ vl.set_use_dhcp(vnet, 0)
256
+ vl.set_use_nat(vnet, 0)
257
+ cycle_bridge()
258
+ f = vl.features(vnet)
259
+ ok = f is not None and f & 0x1
260
+ print(f" {'OK' if ok else 'WARNING: bridged bit not set'}: {vnet} features=0x{(f or 0):08x}")
261
+
262
+
263
+ def cmd_unpin(vl, args):
264
+ require_vnet(vl, args.vnet)
265
+ removed = False
266
+ for guid, v in reg_bridge_bindings().items():
267
+ if v == args.vnet:
268
+ reg_bridge_del(guid)
269
+ print(f"* removed bridge binding {guid} -> vmnet{v}")
270
+ removed = True
271
+ if not removed:
272
+ print(f"* no bridge binding on vmnet{args.vnet}")
273
+ if removed:
274
+ cycle_bridge()
275
+
276
+
277
+ def cmd_autobridge(vl, args):
278
+ """Bridge every physical NIC to its own segment: Ethernet -> LAN<n>,
279
+ Wi-Fi -> WLAN<n> (n = per-class index in ifIndex order). Convergent and
280
+ idempotent: existing pins are kept and renamed, clean segments are reused.
281
+ vmnet0 (the driver's auto-bridge default) is left untouched."""
282
+ nics = classify_interfaces()
283
+ if not nics:
284
+ print("* no physical NICs found (every adapter looks virtual/VPN)")
285
+ return
286
+ pins = reg_bridge_bindings()
287
+ named = {} # display name -> vnet, so a segment from an earlier run is
288
+ for k in range(1, vl.count()): # adopted instead of duplicating the name
289
+ dn = vl.get_str("VNL_GetVnetDisplayName", f"vmnet{k}")
290
+ if dn:
291
+ named.setdefault(dn, k)
292
+ counts = {"ethernet": 0, "wifi": 0}
293
+ planned = set()
294
+ changed = False
295
+ for guid, ifname, kind, desc in nics:
296
+ counts[kind] += 1
297
+ want = ("LAN" if kind == "ethernet" else "WLAN") + str(counts[kind])
298
+ if guid in pins:
299
+ n = pins[guid]
300
+ vnet = f"vmnet{n}"
301
+ if (vl.features(vnet) or 0) & ~0x1:
302
+ print(
303
+ f"* {ifname} already pinned to {vnet} with host network "
304
+ "config; left alone (use unpin/remove to reclaim it)"
305
+ )
306
+ continue
307
+ if vl.get_str("VNL_GetVnetDisplayName", vnet) != want:
308
+ vl.set_str("VNL_SetVnetDisplayName", vnet, want)
309
+ changed = True
310
+ print(f"* {vnet} renamed to {want} ({ifname})")
311
+ else:
312
+ print(f"* {vnet} already bridges {ifname} as {want}")
313
+ continue
314
+ n = named.get(want)
315
+ if n is not None and (
316
+ n in pins.values() or n in planned or (vl.features(f"vmnet{n}") or 0) & ~0x1
317
+ ):
318
+ n = None # name taken by a real network elsewhere
319
+ if n is None:
320
+ taken = set(pins.values()) | planned
321
+ cmax = vl.count()
322
+ n = next((k for k in range(2, cmax) if k not in taken and not vnet_in_use(vl, k)), None)
323
+ if n is None:
324
+ print(f"ERROR: no free vmnet segment (2..{cmax - 1}) for {ifname}")
325
+ continue
326
+ vnet = f"vmnet{n}"
327
+ print(
328
+ f"* autobridge {ifname} ({desc}; {kind}) -> {vnet} as {want}"
329
+ + (" [dry-run]" if args.dry_run else "")
330
+ )
331
+ planned.add(n)
332
+ changed = True
333
+ if not args.dry_run:
334
+ reg_bridge_set(guid, n)
335
+ pins[guid] = n
336
+ vl.set_str("VNL_SetVnetDisplayName", vnet, want)
337
+ vl.set_use_dhcp(vnet, 0)
338
+ vl.set_use_nat(vnet, 0)
339
+ named.setdefault(want, n)
340
+ if args.dry_run:
341
+ print("(dry run: nothing changed)")
342
+ return
343
+ if changed:
344
+ cycle_bridge()
345
+ for guid, n in sorted(pins.items(), key=lambda kv: kv[1]):
346
+ f = vl.features(f"vmnet{n}")
347
+ ok = f is not None and f & 0x1
348
+ print(f" {'OK' if ok else 'WARNING'}: vmnet{n} pinned to {guid} features=0x{(f or 0):08x}")
349
+
350
+
351
+ def cmd_clear(vl, args):
352
+ """Remove EVERY vmnet network — custom AND stock vmnet1/vmnet8. vmnet0 (the
353
+ VMnetBridge driver's auto-bridge segment) is not removable and always stays."""
354
+ if not args.force:
355
+ sys.exit("clear removes ALL networks including stock vmnet1/vmnet8; pass --force")
356
+ print("* clearing all vmnet networks (vmnet0 remains)")
357
+ bindings = reg_bridge_bindings()
358
+ ctx = batch_new()
359
+ for n in range(1, vl.count()):
360
+ vnet = f"vmnet{n}"
361
+ f = vl.features(vnet) or 0
362
+ try:
363
+ winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, CFG + "\\" + vnet).Close()
364
+ has = True
365
+ except OSError:
366
+ has = False
367
+ if f == 0 and not has and not any(v == n for v in bindings.values()):
368
+ continue # silent segment: nothing to do
369
+ remove_stage(vl, n, ctx)
370
+ batch_apply(vl, ctx)
371
+
372
+
373
+ def cmd_remove(vl, args):
374
+ for n in args.vnet: # validate everything before touching anything
375
+ require_vnet(vl, n)
376
+ if n == 0:
377
+ sys.exit("vmnet0 is the built-in bridge segment and cannot be removed")
378
+ if n in (1, 8) and not args.force:
379
+ sys.exit(f"vmnet{n} is a stock Workstation network; pass --force to remove it anyway")
380
+ ctx = batch_new()
381
+ for n in args.vnet:
382
+ remove_stage(vl, n, ctx)
383
+ batch_apply(vl, ctx)
@@ -0,0 +1,132 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """vmnet_cli.commands_nat - see vmnet_cli.cli for the tool overview."""
3
+
4
+ import sys
5
+ import winreg
6
+
7
+ from .conf import NATVAL, regen_nat_conf
8
+ from .registry import CFG, reg_flag
9
+ from .services import restart_services
10
+ from .vnl import require_vnet
11
+
12
+
13
+ def cmd_natfw(vl, args):
14
+ require_vnet(vl, args.vnet)
15
+ vnet = f"vmnet{args.vnet}"
16
+ if args.action != "list" and not args.proto:
17
+ sys.exit("natfw add/remove need --proto tcp|udp")
18
+ if args.action != "list" and not args.hport:
19
+ sys.exit("natfw add/remove need --hport")
20
+ proto = 1 if args.proto == "tcp" else 0 # VNL_SetNATPortForward arg2: 1=TCP 0=UDP
21
+ if args.action == "list":
22
+ for sub, label in (("TCPForward", "tcp"), ("UDPForward", "udp")):
23
+ try:
24
+ with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, f"{CFG}\\{vnet}\\NAT\\{sub}") as k:
25
+ vals = {
26
+ winreg.EnumValue(k, i)[0]: winreg.EnumValue(k, i)[1]
27
+ for i in range(winreg.QueryInfoKey(k)[1])
28
+ }
29
+ for port, target in sorted(vals.items()):
30
+ if port.endswith("Description"):
31
+ continue
32
+ desc = vals.get(port + "Description", "")
33
+ if isinstance(desc, bytes):
34
+ desc = desc.decode("utf-16-le", "replace").strip("\x00")
35
+ print(
36
+ f" {vnet} {label} host:{port} -> {target}"
37
+ + (f" ({desc})" if desc else "")
38
+ )
39
+ except OSError:
40
+ pass
41
+ return
42
+ if args.action == "add":
43
+ if not (args.vm_ip and args.vm_port):
44
+ sys.exit("natfw add needs --vm-ip and --vm-port")
45
+ rc = vl._c(
46
+ "VNL_SetNATPortForward",
47
+ vnet.encode(),
48
+ proto,
49
+ args.hport,
50
+ args.vm_ip.encode(),
51
+ args.vm_port,
52
+ args.desc,
53
+ 0,
54
+ ) # desc: str -> wchar_t*
55
+ print(
56
+ f"* {vnet} map {args.proto} host:{args.hport} -> {args.vm_ip}:{args.vm_port}: "
57
+ f"{'ok' if rc == 1 else 'rc=' + str(rc)}"
58
+ )
59
+ else: # remove: same call with vmIP NULL (GUI-proven delete semantics)
60
+ rc = vl._c("VNL_SetNATPortForward", vnet.encode(), proto, args.hport, None, 0, None, 0)
61
+ print(
62
+ f"* {vnet} unmap {args.proto} host:{args.hport}: {'ok' if rc == 1 else 'rc=' + str(rc)}"
63
+ )
64
+ # UpdateNATFromConfig rewrites the WHOLE conf from the given vnet's config —
65
+ # regen the target last so its forwards/sections are the ones in the file.
66
+ vl._c("VNL_UpdateNATFromConfig", vnet.encode())
67
+ restart_services(dhcp=False, nat=True)
68
+
69
+
70
+ def cmd_natcfg(vl, args):
71
+ require_vnet(vl, args.vnet)
72
+ vnet = f"vmnet{args.vnet}"
73
+ if not reg_flag(vnet, "NAT") and not (vl.features(vnet) or 0) & 0x10000:
74
+ sys.exit(f"{vnet} is not a NAT network")
75
+ key = f"{CFG}\\{vnet}\\NAT"
76
+ if args.action == "show":
77
+ try:
78
+ k = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, key)
79
+ except FileNotFoundError:
80
+ sys.exit(f"no NAT config key for {vnet}")
81
+ with k:
82
+ for i in range(winreg.QueryInfoKey(k)[1]):
83
+ name, val, _ = winreg.EnumValue(k, i)
84
+ print(f" {name} = {val}")
85
+ return
86
+ changed = False
87
+ with winreg.CreateKey(winreg.HKEY_LOCAL_MACHINE, key) as k:
88
+ for flag, (rname, rtype) in NATVAL.items():
89
+ val = getattr(args, flag, None)
90
+ if val is None:
91
+ continue
92
+ if rtype == "bool":
93
+ val = 1 if val == "on" else 0
94
+ winreg.SetValueEx(k, rname, 0, rtype if rtype != "bool" else winreg.REG_DWORD, val)
95
+ print(f" {vnet} NAT {rname} = {val}")
96
+ changed = True
97
+ if changed:
98
+ vl._c("VNL_UpdateNATFromConfig", vnet.encode())
99
+ restart_services(dhcp=False, nat=True)
100
+ else:
101
+ print("nothing to change")
102
+
103
+
104
+ def cmd_setip(vl, args):
105
+ require_vnet(vl, args.vnet)
106
+ vnet = f"vmnet{args.vnet}"
107
+ rc = vl._c("VNL_SetVnetAdapterAddr", vnet.encode(), args.addr.encode())
108
+ print(f"* {vnet} adapter IP {args.addr}: {'ok' if rc == 1 else 'rc=' + str(rc)}")
109
+ vl.update_adapter(vnet) # apply to the live device, not just the config
110
+ cur = vl.get_str("VNL_GetVnetAdapterAddr", vnet)
111
+ print(f" configured adapter addr: {cur!r}")
112
+
113
+
114
+ def cmd_export(vl, args):
115
+ import os
116
+
117
+ if os.path.exists(args.file):
118
+ os.remove(args.file) # export refuses existing files; match the C tool
119
+ rc = vl._c("VNL_ExportDatabaseToFile", args.file)
120
+ print(f"export -> {args.file}: {'ok' if rc == 1 else 'rc=' + str(rc)}")
121
+ if rc != 1:
122
+ sys.exit(1)
123
+
124
+
125
+ def cmd_import(vl, args):
126
+ rc = vl._c("VNL_ImportDatabaseFromFile", args.file)
127
+ print(f"import <- {args.file}: {'ok' if rc == 1 else 'rc=' + str(rc)}")
128
+ if rc != 1:
129
+ sys.exit(1)
130
+ vl.update_dhcp("vmnet1")
131
+ regen_nat_conf(vl)
132
+ restart_services(dhcp=True, nat=True)
vmnet_cli/conf.py ADDED
@@ -0,0 +1,86 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """vmnet_cli.conf - see vmnet_cli.cli for the tool overview."""
3
+
4
+ import re
5
+ import sys
6
+ import time
7
+ import winreg
8
+
9
+ from .registry import CFG, reg_flag
10
+ from .vnl import VNetLib
11
+
12
+
13
+ def _nat_conf_device():
14
+ """The `device = vmnetN` the current vmnetnat.conf renders (last wins)."""
15
+ dev = None
16
+ try:
17
+ with open(r"C:\ProgramData\VMware\vmnetnat.conf", errors="replace") as f:
18
+ for line in f:
19
+ m = re.match(r"\s*device\s*=\s*(\S+)", line)
20
+ if m:
21
+ dev = m.group(1)
22
+ except OSError:
23
+ pass
24
+ return dev
25
+
26
+
27
+ def regen_nat_conf(vl):
28
+ """vmnetnat.conf has ONE [host] section; regenerate for every NAT vnet,
29
+ ascending, so the file stays consistent with config. The LAST vnet wins
30
+ (each call rewrites the whole file) — but vnetlib intermittently no-ops
31
+ a call (internal-lock serialization), leaving the conf rendering a
32
+ removed vnet; verify the final render and retry it if stale."""
33
+ last = None
34
+ for n in range(2, vl.count()):
35
+ v = f"vmnet{n}"
36
+ if reg_flag(v, "NAT") or (vl.features(v) or 0) & 0x10000:
37
+ vl._c("VNL_UpdateNATFromConfig", v.encode())
38
+ last = v
39
+ if last:
40
+ for _ in range(3):
41
+ if _nat_conf_device() == last:
42
+ return
43
+ time.sleep(1)
44
+ vl._c("VNL_UpdateNATFromConfig", last.encode())
45
+ print(
46
+ f"warning: vmnetnat.conf still renders {_nat_conf_device()}, expected {last}",
47
+ file=sys.stderr,
48
+ )
49
+
50
+
51
+ def ensure_nat(vnet, on):
52
+ vl = VNetLib()
53
+ vl._c("VNL_SetVnetUseNAT", vnet.encode(), 1 if on else 0)
54
+ if on and not reg_flag(vnet, "NAT"):
55
+ # VNL_SetVnetUseNAT does not create the NAT subkey for a fresh vnet
56
+ with winreg.CreateKey(winreg.HKEY_LOCAL_MACHINE, f"{CFG}\\{vnet}\\NAT") as k:
57
+ winreg.SetValueEx(k, "UseNAT", 0, winreg.REG_DWORD, 1)
58
+
59
+
60
+ DHCP_PARAM = {"begin": 0x3426, "end": 0x3427, "default_lease": 0x342A, "max_lease": 0x342B}
61
+ # vmnetcfg.exe call sites proved these paramIds (0x342a/0x342b in its dhcpDlg) +
62
+ # live registry sweep mapped each to its DHCP subkey value name.
63
+
64
+
65
+ def set_dhcp_param(vl, vnet, key, value):
66
+ v = str(value).encode()
67
+ return vl._c("VNL_SetDHCPParam", vnet.encode(), DHCP_PARAM[key], v, len(v))
68
+
69
+
70
+ NATVAL = { # cli flag -> (registry value name, type) — UpdateNATFromConfig reads these
71
+ "gateway_ip": ("ip", winreg.REG_SZ),
72
+ "host_ip": ("hostIp", winreg.REG_SZ),
73
+ "configport": ("ConfigPort", winreg.REG_DWORD),
74
+ "ipv6": ("natIp6Enable", "bool"),
75
+ "active_ftp": ("activeFTP", "bool"),
76
+ "allow_any_oui": ("allowAnyOUI", "bool"),
77
+ "udp_timeout": ("udpTimeout", winreg.REG_DWORD),
78
+ "dns_auto": ("DNSAutodetect", "bool"),
79
+ "dns1": ("DNSNameServer1", winreg.REG_SZ),
80
+ "dns2": ("DNSNameServer2", winreg.REG_SZ),
81
+ "dns3": ("DNSNameServer3", winreg.REG_SZ),
82
+ "dns_policy": ("DNSPolicy", winreg.REG_SZ),
83
+ "dns_retries": ("DNSRetries", winreg.REG_DWORD),
84
+ "dns_timeout": ("DNSTimeout", winreg.REG_DWORD),
85
+ "nbns_timeout": ("NBNSTimeout", winreg.REG_DWORD),
86
+ }
vmnet_cli/registry.py ADDED
@@ -0,0 +1,66 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """vmnet_cli.registry - see vmnet_cli.cli for the tool overview."""
3
+
4
+ import winreg
5
+
6
+ BRIDGE_TABLE = r"SYSTEM\CurrentControlSet\Services\VMnetBridge\Parameters\Adapters"
7
+ CFG = r"SOFTWARE\VMware, Inc.\VMnetLib\VMnetConfig"
8
+
9
+
10
+ def reg_bridge_bindings():
11
+ """{adapter-guid: vnet-number} from the driver's Adapters subkeys (VMnet value)."""
12
+ out = {}
13
+ try:
14
+ with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, BRIDGE_TABLE) as k:
15
+ for i in range(winreg.QueryInfoKey(k)[0]):
16
+ sub = winreg.EnumKey(k, i)
17
+ try:
18
+ with winreg.OpenKey(k, sub) as sk:
19
+ v, _ = winreg.QueryValueEx(sk, "VMnet")
20
+ out[sub.upper()] = v
21
+ except OSError:
22
+ pass
23
+ except FileNotFoundError:
24
+ pass
25
+ return out
26
+
27
+
28
+ def reg_bridge_set(guid, vnet):
29
+ with winreg.CreateKey(winreg.HKEY_LOCAL_MACHINE, BRIDGE_TABLE + "\\" + guid) as sk:
30
+ winreg.SetValueEx(sk, "VMnet", 0, winreg.REG_DWORD, vnet)
31
+
32
+
33
+ def reg_bridge_del(guid):
34
+ try:
35
+ winreg.DeleteKey(winreg.HKEY_LOCAL_MACHINE, BRIDGE_TABLE + "\\" + guid)
36
+ except OSError:
37
+ pass # C tool ignores the rc too
38
+
39
+
40
+ def reg_exists(path):
41
+ try:
42
+ winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, path).Close()
43
+ return True
44
+ except OSError:
45
+ return False
46
+
47
+
48
+ def reg_flag(vnet, sub):
49
+ """True if VMnetConfig\\vnet\\sub\\Use<sub> == 1 (registry is the config truth)."""
50
+ try:
51
+ with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, f"{CFG}\\{vnet}\\{sub}") as k:
52
+ return winreg.QueryValueEx(k, "Use" + sub)[0] == 1
53
+ except OSError:
54
+ return False
55
+
56
+
57
+ def reg_delete_tree(key_path):
58
+ """Recursively delete an HKLM key and all subkeys (winreg.DeleteTree fallback)."""
59
+ try:
60
+ with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, key_path) as k:
61
+ subs = [winreg.EnumKey(k, i) for i in range(winreg.QueryInfoKey(k)[0])]
62
+ except FileNotFoundError:
63
+ return
64
+ for s in subs:
65
+ reg_delete_tree(key_path + "\\" + s)
66
+ winreg.DeleteKey(winreg.HKEY_LOCAL_MACHINE, key_path)