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/__init__.py
ADDED
vmnet_cli/adapters.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""vmnet_cli.adapters - see vmnet_cli.cli for the tool overview."""
|
|
3
|
+
|
|
4
|
+
import csv
|
|
5
|
+
import ctypes
|
|
6
|
+
import io
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
import winreg
|
|
11
|
+
|
|
12
|
+
from .registry import CFG
|
|
13
|
+
from .vnl import VL, VNetLib
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# ---- physical interface enumeration (PowerShell Get-NetAdapter) --------------
|
|
17
|
+
def physical_interfaces():
|
|
18
|
+
"""Return {friendly name: adapter GUID} for physical (non-VMware) interfaces."""
|
|
19
|
+
ps = (
|
|
20
|
+
"Get-NetAdapter | Where-Object { $_.InterfaceDescription -notlike 'VMware*' }"
|
|
21
|
+
" | Sort-Object ifIndex | ConvertTo-Csv -NoTypeInformation"
|
|
22
|
+
)
|
|
23
|
+
try:
|
|
24
|
+
out = subprocess.run(
|
|
25
|
+
["powershell", "-NoProfile", "-Command", ps],
|
|
26
|
+
capture_output=True,
|
|
27
|
+
text=True,
|
|
28
|
+
check=True,
|
|
29
|
+
timeout=60,
|
|
30
|
+
).stdout
|
|
31
|
+
except (FileNotFoundError, subprocess.CalledProcessError) as e:
|
|
32
|
+
sys.exit(f"adapter enumeration failed: {e}")
|
|
33
|
+
import csv
|
|
34
|
+
import io
|
|
35
|
+
|
|
36
|
+
result = {}
|
|
37
|
+
for row in csv.DictReader(io.StringIO(out)):
|
|
38
|
+
guid = (row.get("InterfaceGuid") or "").strip().upper()
|
|
39
|
+
name = (row.get("Name") or "").strip('"')
|
|
40
|
+
if guid and name:
|
|
41
|
+
result[name] = guid
|
|
42
|
+
return result
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# Real-hardware PnP bus prefixes. Software adapters (TAP, Wintun/WireGuard,
|
|
46
|
+
# ZeroTier, VMware VMnet…) are ROOT\- or SWD\-enumerated and never appear here,
|
|
47
|
+
# even when their driver masquerades as an 802.3 medium.
|
|
48
|
+
PHYSICAL_BUSES = ("PCI", "VPCI", "USB", "PCMCIA", "SDIO", "SDCC", "SD", "IEEE1394", "MDIO", "ACPI")
|
|
49
|
+
NET_CLASS = r"SYSTEM\CurrentControlSet\Control\Class\{4d36e972-e325-11ce-bfc1-08002be10318}"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _adapter_medium(guid):
|
|
53
|
+
"""(*IfType, *PhysicalMediaType) from the adapter's registry class key.
|
|
54
|
+
71 = IEEE 802.11 in both fields; 6/14 = 802.3 Ethernet. (0, 0) = unknown
|
|
55
|
+
(some drivers omit the REV values; the bus gate already proved physical)."""
|
|
56
|
+
try:
|
|
57
|
+
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, NET_CLASS) as k:
|
|
58
|
+
for i in range(winreg.QueryInfoKey(k)[0]):
|
|
59
|
+
try:
|
|
60
|
+
with winreg.OpenKey(k, winreg.EnumKey(k, i)) as sk:
|
|
61
|
+
if winreg.QueryValueEx(sk, "NetCfgInstanceId")[0].upper() != guid:
|
|
62
|
+
continue
|
|
63
|
+
out = []
|
|
64
|
+
for rev, plain in (
|
|
65
|
+
("*IfType", "IfType"),
|
|
66
|
+
("*PhysicalMediaType", "PhysicalMediaType"),
|
|
67
|
+
):
|
|
68
|
+
for name in (rev, plain):
|
|
69
|
+
try:
|
|
70
|
+
out.append(winreg.QueryValueEx(sk, name)[0])
|
|
71
|
+
break
|
|
72
|
+
except OSError:
|
|
73
|
+
continue
|
|
74
|
+
else:
|
|
75
|
+
out.append(0)
|
|
76
|
+
return tuple(out)
|
|
77
|
+
except OSError:
|
|
78
|
+
continue
|
|
79
|
+
except OSError:
|
|
80
|
+
pass
|
|
81
|
+
return (0, 0)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def classify_interfaces():
|
|
85
|
+
"""Physical NICs only, in ifIndex order: [(guid, name, kind, desc)].
|
|
86
|
+
kind is 'wifi' for IEEE 802.11 (ifType/PhysicalMediaType 71) else 'ethernet'.
|
|
87
|
+
VPN/virtual adapters are filtered by requiring the adapter's PnP device to
|
|
88
|
+
sit on a real hardware bus: ROOT\\-enumerated drivers (TAP, WireGuard,
|
|
89
|
+
ZeroTier, VMware VMnet) and SWD\\ devices never qualify, whatever medium
|
|
90
|
+
their driver claims."""
|
|
91
|
+
ps = (
|
|
92
|
+
"Get-CimInstance -Namespace root/StandardCimv2 -ClassName MSFT_NetAdapter"
|
|
93
|
+
" | Sort-Object ifIndex | Select-Object Name,InterfaceGuid,PnPDeviceID,"
|
|
94
|
+
"InterfaceDescription | ConvertTo-Csv -NoTypeInformation"
|
|
95
|
+
)
|
|
96
|
+
out = subprocess.run(
|
|
97
|
+
["powershell", "-NoProfile", "-Command", ps],
|
|
98
|
+
capture_output=True,
|
|
99
|
+
text=True,
|
|
100
|
+
check=True,
|
|
101
|
+
timeout=60,
|
|
102
|
+
).stdout
|
|
103
|
+
result = []
|
|
104
|
+
for row in csv.DictReader(io.StringIO(out)):
|
|
105
|
+
guid = (row.get("InterfaceGuid") or "").strip().upper()
|
|
106
|
+
name = (row.get("Name") or "").strip('"')
|
|
107
|
+
desc = (row.get("InterfaceDescription") or "").strip('"')
|
|
108
|
+
pnp = (row.get("PnPDeviceID") or "").strip('"').upper()
|
|
109
|
+
if not guid or "VMware" in desc or not pnp:
|
|
110
|
+
continue
|
|
111
|
+
if not pnp.split("\\")[0].startswith(PHYSICAL_BUSES):
|
|
112
|
+
continue # ROOT\/SWD\… = software driver
|
|
113
|
+
iftype, pmt = _adapter_medium(guid)
|
|
114
|
+
if iftype in (24, 131, 135, 136, 148, 154, 166, 181, 209):
|
|
115
|
+
continue # ppp / tunnel / virtual ifTypes
|
|
116
|
+
kind = "wifi" if 71 in (iftype, pmt) else "ethernet"
|
|
117
|
+
result.append((guid, name, kind, desc))
|
|
118
|
+
return result
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def install_adapter_start(vnet):
|
|
122
|
+
"""Kick the VNL adapter install; wait via install_adapter_finish."""
|
|
123
|
+
vl = VNetLib()
|
|
124
|
+
# 5-arg signature: (vnet, hwnd, flags, p4, p5*) — p5 is dereferenced if not NULL,
|
|
125
|
+
# so every trailing arg MUST be passed explicitly (stale-register AV otherwise).
|
|
126
|
+
vl._c("VNL_InstallNetworkAdapter", vnet.encode(), 0, 0, 0, 0)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def install_adapter_finish(vnet, rounds=10):
|
|
130
|
+
"""Wait for the PnP install; fall back to drvInst64 installRootDriver
|
|
131
|
+
(works headless, verified)."""
|
|
132
|
+
import time
|
|
133
|
+
|
|
134
|
+
for _ in range(rounds): # PnP completes asynchronously; give it room
|
|
135
|
+
time.sleep(3)
|
|
136
|
+
if _adapter_present(vnet):
|
|
137
|
+
_name_adapter(vnet) # headless VNL path also leaves generic names
|
|
138
|
+
return True
|
|
139
|
+
exe = VL.rsplit("\\", 1)[0] + "\\drvInst64.exe"
|
|
140
|
+
inf = VL.rsplit("\\", 1)[0] + "\\netadapter.inf"
|
|
141
|
+
subprocess.run(
|
|
142
|
+
[exe, "installRootDriver", "0", f"*{vnet.replace('vmnet', 'VMnetAdapter')}", inf, "1"],
|
|
143
|
+
capture_output=True,
|
|
144
|
+
timeout=120,
|
|
145
|
+
)
|
|
146
|
+
for _ in range(4):
|
|
147
|
+
time.sleep(3)
|
|
148
|
+
if _adapter_present(vnet):
|
|
149
|
+
_name_adapter(vnet)
|
|
150
|
+
return True
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def install_adapter(vnet):
|
|
155
|
+
install_adapter_start(vnet)
|
|
156
|
+
return install_adapter_finish(vnet)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _adapter_present(vnet):
|
|
160
|
+
return int(vnet[5:]) in _adapters_present_set()
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _name_adapter(vnet):
|
|
164
|
+
"""drvInst installs with generic connection names; give it the VMnetN name."""
|
|
165
|
+
want = f"VMware Network Adapter {vnet.replace('vmnet', 'VMnet')}"
|
|
166
|
+
subprocess.run(
|
|
167
|
+
[
|
|
168
|
+
"powershell",
|
|
169
|
+
"-NoProfile",
|
|
170
|
+
"-Command",
|
|
171
|
+
f"$a = Get-NetAdapter | Where-Object {{ $_.InterfaceDescription -match 'VMnet{vnet[5:]}$' -and $_.Name -ne '{want}' }};"
|
|
172
|
+
f" if ($a) {{ Rename-NetAdapter -InputObject $a -NewName '{want}' -Confirm:$false }}",
|
|
173
|
+
],
|
|
174
|
+
capture_output=True,
|
|
175
|
+
text=True,
|
|
176
|
+
timeout=60,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _adapters_present_set():
|
|
181
|
+
"""VMnet segments that currently have an adapter device (one PS call)."""
|
|
182
|
+
r = subprocess.run(
|
|
183
|
+
[
|
|
184
|
+
"powershell",
|
|
185
|
+
"-NoProfile",
|
|
186
|
+
"-Command",
|
|
187
|
+
"(Get-NetAdapter | ForEach-Object { if ($_.InterfaceDescription -match 'VMnet(\\d+)$') { $Matches[1] } }) -join ' '",
|
|
188
|
+
],
|
|
189
|
+
capture_output=True,
|
|
190
|
+
text=True,
|
|
191
|
+
timeout=60,
|
|
192
|
+
)
|
|
193
|
+
if r.returncode != 0:
|
|
194
|
+
raise RuntimeError("Get-NetAdapter failed: " + (r.stderr or "").strip()[:200])
|
|
195
|
+
return {int(x) for x in r.stdout.split()}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def uninstall_adapter_start(vnet, n):
|
|
199
|
+
"""Kick the VNL adapter removal; clear the config flag. Wait via finish."""
|
|
200
|
+
vl = VNetLib()
|
|
201
|
+
try:
|
|
202
|
+
vl._c("VNL_UninstallNetworkAdapter", vnet.encode(), 0, ctypes.byref(ctypes.c_int(0)))
|
|
203
|
+
except OSError:
|
|
204
|
+
pass
|
|
205
|
+
with winreg.CreateKey(winreg.HKEY_LOCAL_MACHINE, f"{CFG}\\{vnet}\\Adapter") as k:
|
|
206
|
+
winreg.SetValueEx(k, "UseAdapter", 0, winreg.REG_DWORD, 0)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def uninstall_adapter_finish(n, present=None):
|
|
210
|
+
"""Wait for device removal; pnputil fallback. present = live segment set."""
|
|
211
|
+
if present is None:
|
|
212
|
+
time.sleep(3)
|
|
213
|
+
present = _adapters_present_set()
|
|
214
|
+
if n in present:
|
|
215
|
+
r = subprocess.run(
|
|
216
|
+
[
|
|
217
|
+
"powershell",
|
|
218
|
+
"-NoProfile",
|
|
219
|
+
"-Command",
|
|
220
|
+
"Get-PnpDevice -Class Net -ErrorAction SilentlyContinue |"
|
|
221
|
+
f" Where-Object {{ $_.FriendlyName -match 'VMnet{n}$' }} | Select-Object -Expand InstanceId",
|
|
222
|
+
],
|
|
223
|
+
capture_output=True,
|
|
224
|
+
text=True,
|
|
225
|
+
timeout=60,
|
|
226
|
+
)
|
|
227
|
+
for iid in r.stdout.split():
|
|
228
|
+
subprocess.run(["pnputil.exe", "/remove-device", iid], capture_output=True, timeout=120)
|
|
229
|
+
time.sleep(3)
|
|
230
|
+
present = _adapters_present_set()
|
|
231
|
+
return n not in present
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def uninstall_adapter(vnet, n):
|
|
235
|
+
uninstall_adapter_start(vnet, n)
|
|
236
|
+
return uninstall_adapter_finish(n)
|
vmnet_cli/batch.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""vmnet_cli.batch - see vmnet_cli.cli for the tool overview."""
|
|
3
|
+
|
|
4
|
+
import time
|
|
5
|
+
import winreg
|
|
6
|
+
|
|
7
|
+
from .adapters import (
|
|
8
|
+
_adapters_present_set,
|
|
9
|
+
_name_adapter,
|
|
10
|
+
uninstall_adapter_finish,
|
|
11
|
+
uninstall_adapter_start,
|
|
12
|
+
)
|
|
13
|
+
from .conf import regen_nat_conf
|
|
14
|
+
from .registry import CFG, reg_bridge_bindings, reg_bridge_del, reg_delete_tree, reg_flag
|
|
15
|
+
from .services import cycle_bridge, restart_services
|
|
16
|
+
from .vnl import FEATURE_BITS, VL
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def batch_new():
|
|
20
|
+
"""Collects multi-network changes so every expensive wait/restart happens
|
|
21
|
+
once for the whole batch instead of once per network."""
|
|
22
|
+
return {"dhcp": False, "nat": False, "bridge": False, "gone": [], "added": [], "vnets": []}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def remove_stage(vl, n, ctx):
|
|
26
|
+
"""Cheap part of a removal: pins, adapter kick, config key, conf edits."""
|
|
27
|
+
vnet = f"vmnet{n}"
|
|
28
|
+
f = vl.features(vnet) or 0
|
|
29
|
+
pinned = any(v == n for v in reg_bridge_bindings().values())
|
|
30
|
+
had_dhcp = bool(f & 0x1000000) or reg_flag(vnet, "DHCP")
|
|
31
|
+
had_nat = bool(f & 0x10000) or reg_flag(vnet, "NAT")
|
|
32
|
+
had_adapter = bool(f & 0x100) or reg_flag(vnet, "Adapter")
|
|
33
|
+
try:
|
|
34
|
+
winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, CFG + "\\" + vnet).Close()
|
|
35
|
+
has_config = True
|
|
36
|
+
except OSError:
|
|
37
|
+
has_config = False
|
|
38
|
+
if f == 0 and not (had_dhcp or had_nat or had_adapter or pinned) and not has_config:
|
|
39
|
+
print(f"* {vnet} is not configured; nothing to remove")
|
|
40
|
+
return False
|
|
41
|
+
parts = [v for b, v in FEATURE_BITS.items() if f & b]
|
|
42
|
+
print(f"* removing {vnet}" + (f" ({', '.join(parts)})" if parts else ""))
|
|
43
|
+
ctx["vnets"].append(n)
|
|
44
|
+
for guid, v in reg_bridge_bindings().items():
|
|
45
|
+
if v == n:
|
|
46
|
+
reg_bridge_del(guid)
|
|
47
|
+
print(f" - unbound {guid}")
|
|
48
|
+
ctx["bridge"] = True
|
|
49
|
+
if had_adapter:
|
|
50
|
+
uninstall_adapter_start(vnet, n)
|
|
51
|
+
ctx["gone"].append(n)
|
|
52
|
+
reg_delete_tree(CFG + "\\" + vnet)
|
|
53
|
+
print(" - deleted VMnetConfig\\" + vnet)
|
|
54
|
+
if had_dhcp:
|
|
55
|
+
rc = vl.update_dhcp(vnet)
|
|
56
|
+
print(f" - vmnetdhcp.conf regenerated: {'ok' if rc == 1 else 'rc=' + str(rc)}")
|
|
57
|
+
ctx["dhcp"] = True
|
|
58
|
+
if had_nat:
|
|
59
|
+
ctx["nat"] = True # conf regen once at apply (whole-file rewrite)
|
|
60
|
+
return True
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _present_safe():
|
|
64
|
+
"""Adapter enumeration must never abort a batch apply: a transient
|
|
65
|
+
failure degrades to 'nothing present' (matching the C tool, whose
|
|
66
|
+
enumeration just returns 0 on failure), with a warning. The service
|
|
67
|
+
and bridge phases below must always run."""
|
|
68
|
+
try:
|
|
69
|
+
return _adapters_present_set()
|
|
70
|
+
except Exception as e:
|
|
71
|
+
print(f" WARNING: adapter enumeration failed: {e}")
|
|
72
|
+
return set()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def batch_apply(vl, ctx):
|
|
76
|
+
"""One wait for all PnP operations, one conf regen, one service restart,
|
|
77
|
+
one bridge cycle — regardless of batch size."""
|
|
78
|
+
if ctx["nat"]:
|
|
79
|
+
regen_nat_conf(vl)
|
|
80
|
+
print(" - vmnetnat.conf regenerated")
|
|
81
|
+
if ctx["gone"]:
|
|
82
|
+
try:
|
|
83
|
+
time.sleep(3)
|
|
84
|
+
present = _present_safe()
|
|
85
|
+
for n in ctx["gone"]:
|
|
86
|
+
if n in present:
|
|
87
|
+
uninstall_adapter_finish(n, present=present)
|
|
88
|
+
present = _present_safe()
|
|
89
|
+
print(
|
|
90
|
+
f" - host adapter device vmnet{n}: {'STILL PRESENT' if n in present else 'gone'}"
|
|
91
|
+
)
|
|
92
|
+
except Exception as e:
|
|
93
|
+
print(f" WARNING: adapter removal step failed: {e} (continuing)")
|
|
94
|
+
if ctx["added"]:
|
|
95
|
+
try:
|
|
96
|
+
pending = list(ctx["added"])
|
|
97
|
+
for _ in range(10):
|
|
98
|
+
time.sleep(3)
|
|
99
|
+
present = _present_safe()
|
|
100
|
+
pending = [n for n in pending if n not in present]
|
|
101
|
+
if not pending:
|
|
102
|
+
break
|
|
103
|
+
import subprocess
|
|
104
|
+
|
|
105
|
+
exe = VL.rsplit("\\", 1)[0] + "\\drvInst64.exe"
|
|
106
|
+
inf = VL.rsplit("\\", 1)[0] + "\\netadapter.inf"
|
|
107
|
+
for n in pending:
|
|
108
|
+
try:
|
|
109
|
+
subprocess.run(
|
|
110
|
+
[exe, "installRootDriver", "0", f"*VMnetAdapter{n}", inf, "1"],
|
|
111
|
+
capture_output=True,
|
|
112
|
+
timeout=120,
|
|
113
|
+
)
|
|
114
|
+
except (subprocess.TimeoutExpired, OSError):
|
|
115
|
+
pass # keep waiting; do not abort the batch
|
|
116
|
+
time.sleep(3)
|
|
117
|
+
for _ in range(4):
|
|
118
|
+
if not pending:
|
|
119
|
+
break
|
|
120
|
+
time.sleep(3)
|
|
121
|
+
present = _present_safe()
|
|
122
|
+
pending = [n for n in pending if n not in present]
|
|
123
|
+
present = _present_safe()
|
|
124
|
+
for n in ctx["added"]:
|
|
125
|
+
if n in present:
|
|
126
|
+
_name_adapter(f"vmnet{n}")
|
|
127
|
+
print(
|
|
128
|
+
f" - host adapter device vmnet{n}: {'ok' if n in present else 'pending/failed (PnP is async)'}"
|
|
129
|
+
)
|
|
130
|
+
except Exception as e:
|
|
131
|
+
print(f" WARNING: adapter install step failed: {e} (continuing)")
|
|
132
|
+
if ctx["dhcp"] or ctx["nat"]:
|
|
133
|
+
restart_services(dhcp=ctx["dhcp"], nat=ctx["nat"])
|
|
134
|
+
if ctx["bridge"]:
|
|
135
|
+
cycle_bridge()
|
|
136
|
+
for n in ctx["vnets"]:
|
|
137
|
+
left = vl.features(f"vmnet{n}") or 0 # absent (None) counts as removed
|
|
138
|
+
print(f" {'OK' if left == 0 else 'WARNING'}: vmnet{n} features now 0x{left:08x}")
|
vmnet_cli/cli.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""vmnet_cli.cli - see vmnet_cli.cli for the tool overview."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
if sys.platform != "win32":
|
|
8
|
+
sys.exit(
|
|
9
|
+
"vmnet-cli must run on a Windows host with VMware Workstation "
|
|
10
|
+
"installed (it drives vnetlib.dll)."
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from .commands import (
|
|
14
|
+
cmd_adapters,
|
|
15
|
+
cmd_add,
|
|
16
|
+
cmd_autobridge,
|
|
17
|
+
cmd_bridged,
|
|
18
|
+
cmd_clear,
|
|
19
|
+
cmd_edit,
|
|
20
|
+
cmd_list,
|
|
21
|
+
cmd_remove,
|
|
22
|
+
cmd_restore,
|
|
23
|
+
cmd_unpin,
|
|
24
|
+
)
|
|
25
|
+
from .commands_nat import cmd_export, cmd_import, cmd_natcfg, cmd_natfw, cmd_setip
|
|
26
|
+
from .vnl import VNetLib
|
|
27
|
+
|
|
28
|
+
"""
|
|
29
|
+
vmnet-cli — unofficial command-line interface to the VMware Workstation
|
|
30
|
+
Virtual Network Editor, built on the reverse-engineered vnetlib.dll VNL_* API
|
|
31
|
+
(Workstation 26H1,
|
|
32
|
+
build 25688693; API v1.0). Run elevated on the Windows host running Workstation.
|
|
33
|
+
|
|
34
|
+
Usage (full vmnetcfg.exe Network Editor parity):
|
|
35
|
+
vmnet-cli.py adapters list physical interfaces
|
|
36
|
+
vmnet-cli.py list list vmnet segments + features
|
|
37
|
+
vmnet-cli.py add hostonly --name X [--subnet A [--mask M]] [--vnet N] [--dhcp on|off]
|
|
38
|
+
vmnet-cli.py add nat --name X [--subnet A [--mask M]] [--vnet N]
|
|
39
|
+
vmnet-cli.py bridged --name LAN --adapter Ethernet [--vnet N]
|
|
40
|
+
vmnet-cli.py edit --vnet N [--name X] [--subnet A] [--mask M] [--dhcp on|off] [--nat on|off]
|
|
41
|
+
[--type bridged|hostonly|nat] [--adapter on|off]
|
|
42
|
+
[--dhcp-begin A --dhcp-end A] [--lease S --max-lease S]
|
|
43
|
+
vmnet-cli.py set-ip --vnet N --addr A host adapter IP
|
|
44
|
+
vmnet-cli.py natfw list --vnet N --proto tcp|udp --hport P
|
|
45
|
+
vmnet-cli.py natfw add --vnet N --proto tcp|udp --hport P --vm-ip A --vm-port P [--desc D]
|
|
46
|
+
vmnet-cli.py natfw remove --vnet N --proto tcp|udp --hport P
|
|
47
|
+
vmnet-cli.py natcfg show|set --vnet N [--gateway-ip A] [--host-ip A] [--configport P]
|
|
48
|
+
[--ipv6 on|off] [--active-ftp on|off] [--allow-any-oui on|off]
|
|
49
|
+
[--udp-timeout S] [--dns-auto on|off] [--dns1 A --dns2 A --dns3 A]
|
|
50
|
+
[--dns-policy order|rotate|burst] [--dns-retries N] [--dns-timeout S]
|
|
51
|
+
[--nbns-timeout S]
|
|
52
|
+
vmnet-cli.py remove --vnet N [--force] delete a vmnet network entirely
|
|
53
|
+
vmnet-cli.py unpin --vnet N drop bridge binding(s), keep network
|
|
54
|
+
vmnet-cli.py autobridge [--dry-run] bridge each physical NIC:
|
|
55
|
+
Ethernet->LAN<n>, Wi-Fi->WLAN<n>
|
|
56
|
+
vmnet-cli.py restore-defaults --force wipe custom nets, recreate vmnet1+vmnet8
|
|
57
|
+
vmnet-cli.py clear|flush --force remove EVERY network (vmnet0 stays)
|
|
58
|
+
vmnet-cli.py export --file F save config (registry hive)
|
|
59
|
+
vmnet-cli.py import --file F restore config
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def parse_vnet(s):
|
|
64
|
+
"""Accept only '2' or 'vmnet2' (letters then digits, nothing else) —
|
|
65
|
+
mirrors the C tool's strict strtol parse; vnetlib's index resolution
|
|
66
|
+
clamps, so sloppy parses would silently target other segments."""
|
|
67
|
+
t = str(s)
|
|
68
|
+
i = 0
|
|
69
|
+
while i < len(t) and t[i].isascii() and t[i].isalpha():
|
|
70
|
+
i += 1
|
|
71
|
+
rest = t[i:]
|
|
72
|
+
if not rest.isascii() or not rest.isdigit() or int(rest) > 1000000:
|
|
73
|
+
raise argparse.ArgumentTypeError(
|
|
74
|
+
f"expected a segment number or name (e.g. 2 or vmnet2), got {s!r}"
|
|
75
|
+
)
|
|
76
|
+
return int(rest)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def require_elevated():
|
|
80
|
+
"""Same bar as vmnetcfg.exe: full elevated Administrator token.
|
|
81
|
+
SYSTEM is not required; Network Configuration Operators is not enough."""
|
|
82
|
+
import ctypes
|
|
83
|
+
|
|
84
|
+
if not ctypes.windll.shell32.IsUserAnAdmin():
|
|
85
|
+
sys.exit(
|
|
86
|
+
"vmnet-cli must run elevated as an Administrator "
|
|
87
|
+
"(elevated prompt or Run as administrator); SYSTEM not required"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def main():
|
|
92
|
+
ap = argparse.ArgumentParser()
|
|
93
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
94
|
+
sub.add_parser("adapters")
|
|
95
|
+
sub.add_parser("list")
|
|
96
|
+
a = sub.add_parser("add")
|
|
97
|
+
a.add_argument("kind", choices=["hostonly", "nat"])
|
|
98
|
+
a.add_argument("--name", required=True)
|
|
99
|
+
a.add_argument("--subnet")
|
|
100
|
+
a.add_argument("--mask")
|
|
101
|
+
a.add_argument("--vnet", type=parse_vnet)
|
|
102
|
+
a.add_argument("--dhcp", choices=["on", "off"], default="on", help="hostonly only")
|
|
103
|
+
b = sub.add_parser("bridged")
|
|
104
|
+
b.add_argument("--name", required=True)
|
|
105
|
+
b.add_argument("--adapter", required=True)
|
|
106
|
+
b.add_argument("--vnet", type=parse_vnet)
|
|
107
|
+
e = sub.add_parser("edit")
|
|
108
|
+
e.add_argument("--vnet", type=parse_vnet, required=True)
|
|
109
|
+
e.add_argument("--name")
|
|
110
|
+
e.add_argument("--subnet")
|
|
111
|
+
e.add_argument("--mask")
|
|
112
|
+
e.add_argument("--dhcp", choices=["on", "off"])
|
|
113
|
+
e.add_argument("--nat", choices=["on", "off"])
|
|
114
|
+
e.add_argument("--type", choices=["bridged", "hostonly", "nat"])
|
|
115
|
+
e.add_argument("--adapter", choices=["on", "off"], help="host adapter checkbox")
|
|
116
|
+
e.add_argument("--dhcp-begin")
|
|
117
|
+
e.add_argument("--dhcp-end")
|
|
118
|
+
e.add_argument("--lease", type=int, help="default lease seconds")
|
|
119
|
+
e.add_argument("--max-lease", type=int)
|
|
120
|
+
fw = sub.add_parser("natfw", help="NAT port forwarding (mapPortDlg)")
|
|
121
|
+
fw.add_argument("action", choices=["list", "add", "remove"])
|
|
122
|
+
fw.add_argument("--vnet", type=parse_vnet, required=True)
|
|
123
|
+
fw.add_argument("--proto", choices=["tcp", "udp"])
|
|
124
|
+
fw.add_argument("--hport", type=int)
|
|
125
|
+
fw.add_argument("--vm-ip")
|
|
126
|
+
fw.add_argument("--vm-port", type=int)
|
|
127
|
+
fw.add_argument("--desc")
|
|
128
|
+
nc = sub.add_parser("natcfg", help="NAT/DNS/NetBIOS advanced settings (natDlg)")
|
|
129
|
+
nc.add_argument("action", choices=["show", "set"])
|
|
130
|
+
nc.add_argument("--vnet", type=parse_vnet, required=True)
|
|
131
|
+
for flag, typ in (
|
|
132
|
+
("gateway-ip", str),
|
|
133
|
+
("host-ip", str),
|
|
134
|
+
("configport", int),
|
|
135
|
+
("udp-timeout", int),
|
|
136
|
+
("dns1", str),
|
|
137
|
+
("dns2", str),
|
|
138
|
+
("dns3", str),
|
|
139
|
+
("dns-policy", str),
|
|
140
|
+
("dns-retries", int),
|
|
141
|
+
("dns-timeout", int),
|
|
142
|
+
("nbns-timeout", int),
|
|
143
|
+
):
|
|
144
|
+
nc.add_argument("--" + flag, type=typ)
|
|
145
|
+
for flag in ("ipv6", "active-ftp", "allow-any-oui", "dns-auto"):
|
|
146
|
+
nc.add_argument("--" + flag, choices=["on", "off"])
|
|
147
|
+
rd = sub.add_parser("restore-defaults", help="wipe custom nets, recreate vmnet1+vmnet8")
|
|
148
|
+
rd.add_argument("--force", action="store_true")
|
|
149
|
+
cl = sub.add_parser(
|
|
150
|
+
"clear", aliases=["flush"], help="remove ALL vmnet networks incl. stock (vmnet0 stays)"
|
|
151
|
+
)
|
|
152
|
+
cl.add_argument("--force", action="store_true")
|
|
153
|
+
i = sub.add_parser("set-ip")
|
|
154
|
+
i.add_argument("--vnet", type=parse_vnet, required=True)
|
|
155
|
+
i.add_argument("--addr", required=True)
|
|
156
|
+
r = sub.add_parser(
|
|
157
|
+
"remove",
|
|
158
|
+
help="delete vmnet networks (adapter, config, conf, services) — batched across all --vnet args",
|
|
159
|
+
)
|
|
160
|
+
r.add_argument("--vnet", type=parse_vnet, required=True, action="append")
|
|
161
|
+
r.add_argument("--force", action="store_true", help="allow removing stock vmnet1/vmnet8")
|
|
162
|
+
u = sub.add_parser("unpin", help="only remove bridge binding(s) of a vnet, keep the network")
|
|
163
|
+
u.add_argument("--vnet", type=parse_vnet, required=True)
|
|
164
|
+
ab = sub.add_parser(
|
|
165
|
+
"autobridge", help="bridge every physical NIC: Ethernet->LAN<n>, Wi-Fi->WLAN<n>"
|
|
166
|
+
)
|
|
167
|
+
ab.add_argument("--dry-run", action="store_true", help="show the plan, change nothing")
|
|
168
|
+
x = sub.add_parser("export")
|
|
169
|
+
x.add_argument("--file", required=True)
|
|
170
|
+
m = sub.add_parser("import")
|
|
171
|
+
m.add_argument("--file", required=True)
|
|
172
|
+
args = ap.parse_args()
|
|
173
|
+
require_elevated()
|
|
174
|
+
vl = VNetLib()
|
|
175
|
+
{
|
|
176
|
+
"adapters": lambda: cmd_adapters(vl),
|
|
177
|
+
"list": lambda: cmd_list(vl),
|
|
178
|
+
"add": lambda: cmd_add(vl, args),
|
|
179
|
+
"bridged": lambda: cmd_bridged(vl, args),
|
|
180
|
+
"edit": lambda: cmd_edit(vl, args),
|
|
181
|
+
"set-ip": lambda: cmd_setip(vl, args),
|
|
182
|
+
"remove": lambda: cmd_remove(vl, args),
|
|
183
|
+
"unpin": lambda: cmd_unpin(vl, args),
|
|
184
|
+
"autobridge": lambda: cmd_autobridge(vl, args),
|
|
185
|
+
"natfw": lambda: cmd_natfw(vl, args),
|
|
186
|
+
"natcfg": lambda: cmd_natcfg(vl, args),
|
|
187
|
+
"restore-defaults": lambda: cmd_restore(vl, args),
|
|
188
|
+
"clear": lambda: cmd_clear(vl, args),
|
|
189
|
+
"flush": lambda: cmd_clear(vl, args),
|
|
190
|
+
"export": lambda: cmd_export(vl, args),
|
|
191
|
+
"import": lambda: cmd_import(vl, args),
|
|
192
|
+
}[args.cmd]()
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
if __name__ == "__main__":
|
|
196
|
+
main()
|