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/__init__.py +2 -0
- vmnet_cli/adapters.py +236 -0
- vmnet_cli/batch.py +138 -0
- vmnet_cli/cli.py +196 -0
- vmnet_cli/commands.py +383 -0
- vmnet_cli/commands_nat.py +132 -0
- vmnet_cli/conf.py +86 -0
- vmnet_cli/registry.py +66 -0
- vmnet_cli/services.py +42 -0
- vmnet_cli/vnl.py +116 -0
- vmnet_cli-0.1.0.dist-info/METADATA +298 -0
- vmnet_cli-0.1.0.dist-info/RECORD +16 -0
- vmnet_cli-0.1.0.dist-info/WHEEL +5 -0
- vmnet_cli-0.1.0.dist-info/entry_points.txt +2 -0
- vmnet_cli-0.1.0.dist-info/licenses/LICENSE +202 -0
- vmnet_cli-0.1.0.dist-info/top_level.txt +1 -0
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)
|