Use DHCP for Windows-host NAT networking

This commit is contained in:
James Stringer committed 2026-09-07 15:44:22 -04:00
1 parent 4e2493dfbe
commit c737c292cf
8 files changed
+155 -509

No files matched your search

+8 -146
View File
@@ -501,8 +501,6 @@ static void save_vm_list(void)
fwprintf(f, L"NetAdapter=%s\n", g_vms[i].net_adapter);
if (g_vms[i].resources_iso_path[0] != L'\0')
fwprintf(f, L"ResourcesIso=%s\n", g_vms[i].resources_iso_path);
if (g_vms[i].nat_ip[0] != '\0')
fwprintf(f, L"NatIp=%S\n", g_vms[i].nat_ip);
if (g_vms[i].is_template)
fwprintf(f, L"IsTemplate=1\n");
if (g_vms[i].test_mode)
@@ -650,8 +648,6 @@ static void load_vm_list(void)
wcscpy_s(vm->net_adapter, 256, line + 11);
else if (wcsncmp(line, L"ResourcesIso=", 13) == 0)
wcscpy_s(vm->resources_iso_path, MAX_PATH, line + 13);
else if (wcsncmp(line, L"NatIp=", 6) == 0)
WideCharToMultiByte(CP_UTF8, 0, line + 6, -1, vm->nat_ip, sizeof(vm->nat_ip), NULL, NULL);
else if (wcsncmp(line, L"IsTemplate=", 11) == 0)
vm->is_template = (_wtoi(line + 11) != 0);
else if (wcsncmp(line, L"TestMode=", 9) == 0)
@@ -829,7 +825,6 @@ static void asb_hcs_state_changed(VmInstance *instance, DWORD event)
asb_log(L"VM \"%s\" exited (event=0x%08X).", instance->name, event);
asb_vm_cleanup_network(instance);
instance->nat_ip[0] = '\0';
hcs_close_vm(instance);
/* Template finalization */
@@ -920,75 +915,6 @@ ASB_API void asb_vm_cleanup_network(VmInstance *vm)
vm->network_cleaned = TRUE;
}
/* ---- NAT IP allocation ---- */
/* Allocate the next free IP in the chosen NAT /24 (see hcn_nat_subnet_base).
Scans g_vms[] for IPs already in use. Returns FALSE if pool exhausted. */
static BOOL allocate_nat_ip(VmInstance *vm)
{
BOOL used[256] = { 0 };
const char *base = hcn_nat_subnet_base(); /* e.g. "192.168.42" */
int ba, bb, bc;
int i, octet;
if (sscanf_s(base, "%d.%d.%d", &ba, &bb, &bc) != 3) return FALSE;
for (i = 0; i < g_vm_count; i++) {
int a, b, c, d;
if (&g_vms[i] == vm) continue;
if (g_vms[i].nat_ip[0] == '\0') continue;
/* Only consider entries that share our current /24. Stale entries
from a prior run with a different subnet are ignored - their
endpoints died with the previous NAT network. */
if (sscanf_s(g_vms[i].nat_ip, "%d.%d.%d.%d", &a, &b, &c, &d) == 4 &&
a == ba && b == bb && c == bc && d >= 2 && d <= 254)
used[d] = TRUE;
}
for (octet = 2; octet <= 254; octet++) {
if (!used[octet]) {
sprintf_s(vm->nat_ip, sizeof(vm->nat_ip), "%s.%d", base, octet);
return TRUE;
}
}
return FALSE;
}
/* Try hcn_create_endpoint, retrying on NAT failure by bumping the last
octet of nat_ip up to 10 times. The buffer pointed to by nat_ip is
mutated in place on a successful retry; the caller is responsible for
save_vm_list() if it cares about persistence.
Defends against HCN_E_ADDR_INVALID_OR_RESERVED (0x803B002F), which
HNS returns when an endpoint at the chosen IP was orphaned by a
previous run -- the IP is in our allocator's free list but HNS still
has a phantom reservation. Bumping past it usually wins. */
static HRESULT try_endpoint_with_retry(const GUID *net_id, GUID *ep_id,
wchar_t *ep_guid_str, size_t str_len,
char *nat_ip, size_t nat_ip_size,
BOOL is_nat)
{
HRESULT hr;
int retry;
hr = hcn_create_endpoint(net_id, ep_id, ep_guid_str, str_len,
(nat_ip && nat_ip[0]) ? nat_ip : NULL);
if (SUCCEEDED(hr) || !is_nat || !nat_ip || !nat_ip[0]) return hr;
asb_log(L"Endpoint failed for %S, trying next IP...", nat_ip);
for (retry = 0; retry < 10; retry++) {
int a, b, c, d;
if (sscanf_s(nat_ip, "%d.%d.%d.%d", &a, &b, &c, &d) != 4 || d >= 254) break;
sprintf_s(nat_ip, nat_ip_size, "%d.%d.%d.%d", a, b, c, d + 1);
asb_log(L"Retrying with %S...", nat_ip);
hr = hcn_create_endpoint(net_id, ep_id, ep_guid_str, str_len, nat_ip);
if (SUCCEEDED(hr)) return hr;
}
return hr;
}
/* ---- Background VM start thread ---- */
typedef struct {
VmInstance *vm;
VmConfig config;
@@ -1004,18 +930,6 @@ static DWORD WINAPI start_vm_thread(LPVOID param)
HRESULT hr;
wchar_t endpoint_guid_str[64] = { 0 };
/* Allocate NAT IP before endpoint creation (only for NAT mode) */
if (args->network_mode == NET_NAT) {
if (allocate_nat_ip(vm)) {
asb_log(L"Allocated NAT IP %S for \"%s\".", vm->nat_ip, vm->name);
save_vm_list();
} else {
asb_log(L"Warning: NAT IP pool exhausted.");
}
} else {
vm->nat_ip[0] = '\0';
}
if (args->network_mode != NET_NONE) {
switch (args->network_mode) {
case NET_NAT: hr = hcn_create_nat_network(&args->network_id); break;
@@ -1024,11 +938,8 @@ static DWORD WINAPI start_vm_thread(LPVOID param)
default: hr = E_INVALIDARG; break;
}
if (SUCCEEDED(hr)) {
hr = try_endpoint_with_retry(&args->network_id, &args->endpoint_id,
endpoint_guid_str, 64,
vm->nat_ip, sizeof(vm->nat_ip),
args->network_mode == NET_NAT);
if (SUCCEEDED(hr) && args->network_mode == NET_NAT) save_vm_list();
hr = hcn_create_endpoint(&args->network_id, &args->endpoint_id,
endpoint_guid_str, 64);
if (FAILED(hr)) {
asb_log(L"Error: Network endpoint failed (0x%08X).", hr);
if (g_state_cb) g_state_cb(vm_handle(vm), FALSE, g_state_ud);
@@ -1304,20 +1215,8 @@ static DWORD WINAPI vhdx_create_thread(LPVOID param)
args->vhdx_created = TRUE;
/* Allocate NAT IP before endpoint creation.
args->vm_inst isn't set yet (HCS VM not created), so allocate
into the g_vms[] entry directly via vm_index. */
if (args->config.network_mode == NET_NAT && args->vm_index >= 0 && args->vm_index < g_vm_count) {
if (allocate_nat_ip(&g_vms[args->vm_index])) {
asb_log(L"Allocated NAT IP %S for new VM.", g_vms[args->vm_index].nat_ip);
save_vm_list();
}
}
/* Network */
if (args->config.network_mode != NET_NONE) {
char *nat_ip = (args->vm_index >= 0 && args->vm_index < g_vm_count)
? g_vms[args->vm_index].nat_ip : NULL;
switch (args->config.network_mode) {
case NET_NAT: hr = hcn_create_nat_network(&args->network_id); break;
case NET_INTERNAL: hr = hcn_create_internal_network(&args->network_id); break;
@@ -1325,15 +1224,10 @@ static DWORD WINAPI vhdx_create_thread(LPVOID param)
default: hr = E_INVALIDARG; break;
}
if (SUCCEEDED(hr)) {
BOOL is_nat = (args->config.network_mode == NET_NAT);
size_t ip_size = (args->vm_index >= 0 && args->vm_index < g_vm_count)
? sizeof(g_vms[args->vm_index].nat_ip) : 0;
hr = try_endpoint_with_retry(&args->network_id, &args->endpoint_id,
args->endpoint_guid, 64,
nat_ip, ip_size, is_nat);
hr = hcn_create_endpoint(&args->network_id, &args->endpoint_id,
args->endpoint_guid, 64);
if (SUCCEEDED(hr)) {
args->has_network = TRUE;
if (is_nat) save_vm_list();
}
/* If endpoint create fails, leave the network alone - it may be
shared with other VMs. Orphan networks are cleaned up at next
@@ -2466,21 +2360,8 @@ static DWORD WINAPI linux_create_thread(LPVOID param)
runtime VM — the installed VHDX is fully self-bootable. */
args->config.image_path[0] = L'\0';
/* ---- 2. NAT IP allocation. The chosen static /24 address is baked
into the HCN endpoint policy below and pushed to the guest at
runtime by the agent (set_ip -> static netplan); the guest never
DHCPs in NAT mode. ---- */
if (args->config.network_mode == NET_NAT && args->vm_index >= 0 && args->vm_index < g_vm_count) {
if (allocate_nat_ip(&g_vms[args->vm_index])) {
asb_log(L"Allocated NAT IP %S for new VM.", g_vms[args->vm_index].nat_ip);
save_vm_list();
}
}
/* ---- 5. Network + endpoint (same as vhdx_create_thread:1114). ---- */
if (args->config.network_mode != NET_NONE) {
char *nat_ip = (args->vm_index >= 0 && args->vm_index < g_vm_count)
? g_vms[args->vm_index].nat_ip : NULL;
switch (args->config.network_mode) {
case NET_NAT: hr = hcn_create_nat_network(&args->network_id); break;
case NET_INTERNAL: hr = hcn_create_internal_network(&args->network_id); break;
@@ -2488,15 +2369,10 @@ static DWORD WINAPI linux_create_thread(LPVOID param)
default: hr = E_INVALIDARG; break;
}
if (SUCCEEDED(hr)) {
BOOL is_nat = (args->config.network_mode == NET_NAT);
size_t ip_size = (args->vm_index >= 0 && args->vm_index < g_vm_count)
? sizeof(g_vms[args->vm_index].nat_ip) : 0;
hr = try_endpoint_with_retry(&args->network_id, &args->endpoint_id,
args->endpoint_guid, 64,
nat_ip, ip_size, is_nat);
hr = hcn_create_endpoint(&args->network_id, &args->endpoint_id,
args->endpoint_guid, 64);
if (SUCCEEDED(hr)) {
args->has_network = TRUE;
if (is_nat) save_vm_list();
}
}
if (FAILED(hr))
@@ -3431,16 +3307,6 @@ ASB_API HRESULT asb_vm_create(const AsbVmConfig *config)
inst->network_cleaned = FALSE;
/* Allocate NAT IP before endpoint creation */
if (cfg.network_mode == NET_NAT) {
if (allocate_nat_ip(inst)) {
asb_log(L"Allocated NAT IP %S for \"%s\".", inst->nat_ip, inst->name);
save_vm_list();
}
} else {
inst->nat_ip[0] = '\0';
}
/* Networking */
if (cfg.network_mode != NET_NONE) {
switch (cfg.network_mode) {
@@ -3453,11 +3319,8 @@ ASB_API HRESULT asb_vm_create(const AsbVmConfig *config)
asb_log(L"Warning: Network failed (0x%08X). Continuing without.", hr);
cfg.network_mode = NET_NONE;
} else {
hr = try_endpoint_with_retry(&inst->network_id, &inst->endpoint_id,
endpoint_guid_str, 64,
inst->nat_ip, sizeof(inst->nat_ip),
cfg.network_mode == NET_NAT);
if (SUCCEEDED(hr) && cfg.network_mode == NET_NAT) save_vm_list();
hr = hcn_create_endpoint(&inst->network_id, &inst->endpoint_id,
endpoint_guid_str, 64);
if (FAILED(hr)) {
asb_log(L"Warning: Endpoint failed (0x%08X).", hr);
/* Leave the shared network alone - other VMs may be using it. */
@@ -3681,7 +3544,6 @@ ASB_API HRESULT asb_vm_stop(AsbVm vm)
hcs_close_vm(inst);
asb_vm_cleanup_network(inst);
inst->nat_ip[0] = '\0';
asb_log(L"VM \"%s\" terminated.", inst->name);
save_vm_list();
+82 -139
View File
@@ -4,6 +4,7 @@
#include <stdio.h>
#include <objbase.h>
#include <iphlpapi.h>
#include <wctype.h>
#pragma comment(lib, "iphlpapi.lib")
@@ -23,6 +24,10 @@ typedef HRESULT (WINAPI *PFN_HcnCloseNetwork)(void *network);
typedef HRESULT (WINAPI *PFN_HcnCloseEndpoint)(void *endpoint);
typedef HRESULT (WINAPI *PFN_HcnOpenNetwork)(
REFGUID id, void **network, PWSTR *errorRecord);
typedef HRESULT (WINAPI *PFN_HcnOpenEndpoint)(
REFGUID id, void **endpoint, PWSTR *errorRecord);
typedef HRESULT (WINAPI *PFN_HcnQueryEndpointProperties)(
void *endpoint, PCWSTR query, PWSTR *properties, PWSTR *errorRecord);
/* ---- Loaded function pointers ---- */
@@ -35,6 +40,8 @@ static PFN_HcnDeleteEndpoint pfnDeleteEp;
static PFN_HcnCloseNetwork pfnCloseNet;
static PFN_HcnCloseEndpoint pfnCloseEp;
static PFN_HcnOpenNetwork pfnOpenNet;
static PFN_HcnOpenEndpoint pfnOpenEp;
static PFN_HcnQueryEndpointProperties pfnQueryEp;
/* ---- Helpers ---- */
@@ -67,9 +74,12 @@ BOOL hcn_init(void)
pfnCloseNet = (PFN_HcnCloseNetwork)GetProcAddress(g_hcn_dll, "HcnCloseNetwork");
pfnCloseEp = (PFN_HcnCloseEndpoint)GetProcAddress(g_hcn_dll, "HcnCloseEndpoint");
pfnOpenNet = (PFN_HcnOpenNetwork)GetProcAddress(g_hcn_dll, "HcnOpenNetwork");
pfnOpenEp = (PFN_HcnOpenEndpoint)GetProcAddress(g_hcn_dll, "HcnOpenEndpoint");
pfnQueryEp = (PFN_HcnQueryEndpointProperties)GetProcAddress(g_hcn_dll, "HcnQueryEndpointProperties");
pfnEnumNet = (PFN_HcnEnumerateNetworks)GetProcAddress(g_hcn_dll, "HcnEnumerateNetworks");
if (!pfnCreateNet || !pfnCreateEp || !pfnCloseNet || !pfnCloseEp) {
if (!pfnCreateNet || !pfnCreateEp || !pfnCloseNet || !pfnCloseEp ||
!pfnOpenEp || !pfnQueryEp) {
FreeLibrary(g_hcn_dll);
g_hcn_dll = NULL;
return FALSE;
@@ -210,106 +220,6 @@ void hcn_cleanup(void)
}
}
/* -------------------------------------------------------------------------
* NAT subnet selection
*
* We use a small fixed list of /24 candidates and pick the first one that
* doesn't overlap any IPv4 subnet currently bound to a host adapter.
* Default Switch (172.20.48.0/20) is built-in on every Windows 11 host;
* 192.168.42.0/24 sidesteps it. The fallback 192.168.142.0/24 covers the
* unlikely case where the user's home network or VPN claims .42.
*
* Decided once on first call, stable for process lifetime. No saved-state
* coordination, no per-VM adoption -- the same probe runs every start.
* HCN's startup network-delete (hcn_cleanup_stale_networks) wipes the
* previous run's network so the freshly-picked subnet is always honored.
* ------------------------------------------------------------------------- */
static char g_nat_base[16]; /* "192.168.42" or "192.168.142", no trailing dot */
typedef struct { DWORD network; int prefix_len; } SubnetInfo;
static BOOL ranges_overlap(DWORD a_net, int a_prefix, DWORD b_net, int b_prefix)
{
DWORD a_mask = (a_prefix == 0) ? 0 : (~0u << (32 - a_prefix));
DWORD b_mask = (b_prefix == 0) ? 0 : (~0u << (32 - b_prefix));
DWORD a_start = a_net & a_mask;
DWORD a_end = a_start | ~a_mask;
DWORD b_start = b_net & b_mask;
DWORD b_end = b_start | ~b_mask;
return (a_start <= b_end) && (b_start <= a_end);
}
static int collect_inuse_subnets(SubnetInfo *out, int cap)
{
ULONG size = 0;
int n = 0;
PIP_ADAPTER_ADDRESSES buf = NULL;
PIP_ADAPTER_ADDRESSES a;
GetAdaptersAddresses(AF_INET, GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST,
NULL, NULL, &size);
if (size == 0) return 0;
buf = (PIP_ADAPTER_ADDRESSES)malloc(size);
if (!buf) return 0;
if (GetAdaptersAddresses(AF_INET, GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST,
NULL, buf, &size) != NO_ERROR) {
free(buf); return 0;
}
for (a = buf; a && n < cap; a = a->Next) {
PIP_ADAPTER_UNICAST_ADDRESS u;
for (u = a->FirstUnicastAddress; u && n < cap; u = u->Next) {
SOCKADDR_IN *sin;
if (!u->Address.lpSockaddr) continue;
if (u->Address.lpSockaddr->sa_family != AF_INET) continue;
if (u->OnLinkPrefixLength > 32) continue;
sin = (SOCKADDR_IN *)u->Address.lpSockaddr;
out[n].network = ntohl(sin->sin_addr.s_addr);
out[n].prefix_len = u->OnLinkPrefixLength;
n++;
}
}
free(buf);
return n;
}
static void pick_nat_base_once(void)
{
static const char *CANDIDATES[] = { "192.168.42", "192.168.142", NULL };
SubnetInfo used[64];
int n_used, i, j;
if (g_nat_base[0]) return;
n_used = collect_inuse_subnets(used, 64);
for (i = 0; CANDIDATES[i]; i++) {
int a, b, c;
DWORD candidate;
BOOL conflict = FALSE;
if (sscanf_s(CANDIDATES[i], "%d.%d.%d", &a, &b, &c) != 3) continue;
candidate = ((DWORD)a << 24) | ((DWORD)b << 16) | ((DWORD)c << 8);
for (j = 0; j < n_used; j++) {
if (ranges_overlap(candidate, 24, used[j].network, used[j].prefix_len)) {
conflict = TRUE; break;
}
}
if (!conflict) {
strcpy_s(g_nat_base, sizeof(g_nat_base), CANDIDATES[i]);
ui_log(L"NAT subnet: %S.0/24 (gateway %S.1)", g_nat_base, g_nat_base);
return;
}
}
strcpy_s(g_nat_base, sizeof(g_nat_base), CANDIDATES[0]);
ui_log(L"WARNING: every candidate NAT subnet (192.168.42.0/24, 192.168.142.0/24) overlaps a host adapter. "
L"Using %S.0/24 anyway; HCN create may fail. Edit hcn_network.c CANDIDATES to add another.",
g_nat_base);
}
const char *hcn_nat_subnet_base(void)
{
pick_nat_base_once();
return g_nat_base;
}
HRESULT hcn_create_nat_network(GUID *network_id)
{
wchar_t settings[1024];
@@ -327,22 +237,14 @@ HRESULT hcn_create_nat_network(GUID *network_id)
if (hcn_network_exists(&APPSANDBOX_NAT_GUID))
return S_OK;
pick_nat_base_once();
swprintf_s(settings, 1024,
L"{"
L"\"SchemaVersion\":{\"Major\":2,\"Minor\":0},"
L"\"Name\":\"AppSandboxNAT\","
L"\"Type\":\"NAT\","
L"\"Ipams\":[{"
L"\"Type\":\"Static\","
L"\"Subnets\":[{"
L"\"IpAddressPrefix\":\"%S.0/24\","
L"\"Routes\":[{\"NextHop\":\"%S.1\",\"DestinationPrefix\":\"0.0.0.0/0\"}]"
L"}]"
L"}]"
L"}",
g_nat_base, g_nat_base);
L"\"Type\":\"ICS\","
L"\"IsolateSwitch\":true,"
L"\"Flags\":11"
L"}");
hr = pfnCreateNet(network_id, settings, &network, &error_record);
@@ -436,8 +338,7 @@ HRESULT hcn_create_external_network(GUID *network_id, const wchar_t *adapter_nam
}
HRESULT hcn_create_endpoint(const GUID *network_id, GUID *endpoint_id,
wchar_t *endpoint_guid_str, size_t str_len,
const char *nat_ip)
wchar_t *endpoint_guid_str, size_t str_len)
{
wchar_t net_guid_str[64];
wchar_t ep_guid_str[64];
@@ -459,30 +360,11 @@ HRESULT hcn_create_endpoint(const GUID *network_id, GUID *endpoint_id,
guid_to_string(network_id, net_guid_str, 64);
guid_to_string(endpoint_id, ep_guid_str, 64);
/* Static IP for NAT; DHCP for Internal (ICS) and External (Transparent) */
if (IsEqualGUID(network_id, &APPSANDBOX_NAT_GUID) && nat_ip && nat_ip[0]) {
swprintf_s(settings, 1024,
L"{"
L"\"SchemaVersion\":{\"Major\":2,\"Minor\":0},"
L"\"HostComputeNetwork\":\"%s\","
L"\"IpConfigurations\":[{\"IpAddress\":\"%S\",\"PrefixLength\":24}]"
L"}", net_guid_str, nat_ip);
} else if (IsEqualGUID(network_id, &APPSANDBOX_NAT_GUID)) {
pick_nat_base_once();
swprintf_s(settings, 1024,
L"{"
L"\"SchemaVersion\":{\"Major\":2,\"Minor\":0},"
L"\"HostComputeNetwork\":\"%s\","
L"\"IpConfigurations\":[{\"IpAddress\":\"%S.2\",\"PrefixLength\":24}]"
L"}", net_guid_str, g_nat_base);
} else {
/* Internal (ICS DHCP) or External (LAN DHCP) - no static IP */
swprintf_s(settings, 1024,
L"{"
L"\"SchemaVersion\":{\"Major\":2,\"Minor\":0},"
L"\"HostComputeNetwork\":\"%s\""
L"}", net_guid_str);
}
swprintf_s(settings, 1024,
L"{"
L"\"SchemaVersion\":{\"Major\":2,\"Minor\":0},"
L"\"HostComputeNetwork\":\"%s\""
L"}", net_guid_str);
hr = pfnCreateEp(network, endpoint_id, settings, &endpoint, &error_record);
@@ -500,6 +382,67 @@ HRESULT hcn_create_endpoint(const GUID *network_id, GUID *endpoint_id,
return hr;
}
HRESULT hcn_get_endpoint_mac(const wchar_t *endpoint_guid,
wchar_t *mac_address, size_t str_len)
{
GUID endpoint_id;
wchar_t guid[40];
void *endpoint = NULL;
PWSTR properties = NULL;
PWSTR error_record = NULL;
const wchar_t *value, *end;
size_t i;
HRESULT hr;
if (!endpoint_guid || !mac_address || str_len < 18)
return E_INVALIDARG;
mac_address[0] = L'\0';
if (!g_hcn_dll || !pfnOpenEp || !pfnQueryEp || !pfnCloseEp)
return E_NOT_VALID_STATE;
if (endpoint_guid[0] == L'{') {
hr = CLSIDFromString(endpoint_guid, &endpoint_id);
} else {
if (_snwprintf_s(guid, 40, _TRUNCATE, L"{%s}", endpoint_guid) < 0)
return E_INVALIDARG;
hr = CLSIDFromString(guid, &endpoint_id);
}
if (FAILED(hr)) return hr;
hr = pfnOpenEp(&endpoint_id, &endpoint, &error_record);
if (error_record) { CoTaskMemFree(error_record); error_record = NULL; }
if (FAILED(hr)) goto done;
hr = pfnQueryEp(endpoint, L"{}", &properties, &error_record);
if (FAILED(hr)) goto done;
hr = HRESULT_FROM_WIN32(ERROR_INVALID_DATA);
value = properties ? wcsstr(properties, L"\"MacAddress\"") : NULL;
if (!value) goto done;
value += wcslen(L"\"MacAddress\"");
while (iswspace(*value)) value++;
if (*value++ != L':') goto done;
while (iswspace(*value)) value++;
if (*value++ != L'\"') goto done;
end = wcschr(value, L'\"');
if (!end || end - value != 17) goto done;
for (i = 0; i < 17; i++) {
if (i % 3 == 2) {
if (value[i] != L'-' && value[i] != L':') goto done;
} else if (!iswxdigit(value[i])) {
goto done;
}
}
wcsncpy_s(mac_address, str_len, value, 17);
hr = S_OK;
done:
if (error_record) CoTaskMemFree(error_record);
if (properties) CoTaskMemFree(properties);
if (endpoint) pfnCloseEp(endpoint);
return hr;
}
HRESULT hcn_delete_network(const GUID *network_id)
{
PWSTR error_record = NULL;
+6 -16
View File
@@ -28,18 +28,9 @@ void hcn_cleanup(void);
void hcn_cleanup_stale_networks(void);
/* Create a NAT network. Returns S_OK on success.
network_id: output GUID for the created network.
The NAT subnet is chosen on first use from a small static candidate
list (192.168.42.0/24, then 192.168.142.0/24) - the first that does
not overlap a host adapter wins. Decided once per process, stable. */
network_id: output GUID for the created network. */
HRESULT hcn_create_nat_network(GUID *network_id);
/* Returns the first 3 octets of the chosen NAT subnet ("192.168.42" or
"192.168.142"). Triggers the pick on first call. Stable for process
lifetime. Callers build full IPs via printf-style formatting. */
const char *hcn_nat_subnet_base(void);
/* Create an internal (host-only) network. Returns S_OK on success. */
HRESULT hcn_create_internal_network(GUID *network_id);
@@ -51,13 +42,12 @@ HRESULT hcn_create_external_network(GUID *network_id, const wchar_t *adapter_nam
/* Create an endpoint on a network.
network_id: the network to attach to.
endpoint_id: output GUID for the created endpoint.
endpoint_guid_str: output string representation of endpoint GUID (for HCS JSON).
nat_ip: for NAT networks, the static IP to assign (e.g. "172.20.0.2"); if
NULL/empty, NAT endpoints default to "172.20.0.2". Ignored for
Internal and External networks (which always use DHCP). */
endpoint_guid_str: output string representation of endpoint GUID (for HCS JSON). */
HRESULT hcn_create_endpoint(const GUID *network_id, GUID *endpoint_id,
wchar_t *endpoint_guid_str, size_t str_len,
const char *nat_ip);
wchar_t *endpoint_guid_str, size_t str_len);
HRESULT hcn_get_endpoint_mac(const wchar_t *endpoint_guid,
wchar_t *mac_address, size_t str_len);
/* Delete a network by GUID. */
ASB_API HRESULT hcn_delete_network(const GUID *network_id);
+6 -2
View File
@@ -1,4 +1,5 @@
#include "hcs_vm.h"
#include "hcn_network.h"
#include "vm_agent.h"
#include "ui.h"
#include "disk_util.h" /* ASB_IS_ARM64 */
@@ -935,9 +936,12 @@ BOOL hcs_build_vm_json(const VmConfig *config, const wchar_t *endpoint_guid,
/* Network adapter — skip for template VMs (no network during template creation) */
net_section[0] = L'\0';
if (endpoint_guid && endpoint_guid[0] != L'\0' && !config->is_template) {
wchar_t mac_address[18];
if (FAILED(hcn_get_endpoint_mac(endpoint_guid, mac_address, 18)))
return FALSE;
swprintf_s(net_section, 512,
L",\"NetworkAdapters\":{\"Default\":{\"EndpointId\":\"%s\"}}",
endpoint_guid);
L",\"NetworkAdapters\":{\"Default\":{\"EndpointId\":\"%s\",\"MacAddress\":\"%s\"}}",
endpoint_guid, mac_address);
}
/* Secure Boot — uses ApplySecureBootTemplate + SecureBootTemplateId
-1
View File
@@ -99,7 +99,6 @@ typedef struct {
volatile ULONGLONG last_heartbeat; /* GetTickCount64() of last agent heartbeat */
HANDLE idd_probe_thread; /* Background VDD availability probe */
volatile BOOL idd_probe_stop;
char nat_ip[32]; /* Allocated NAT IP (e.g. "172.20.0.2"), empty if not NAT */
wchar_t admin_user[128]; /* Guest local admin username */
BOOL ssh_enabled; /* TRUE = OpenSSH Server installed in guest */
DWORD ssh_port; /* Host-side TCP port for SSH tunnel (e.g. 2222) */
+7 -9
View File
@@ -2,7 +2,6 @@
#include "vm_agent.h"
#include "vm_ssh_proxy.h"
#include "asb_core.h"
#include "hcn_network.h"
#include "ui.h"
#include <stdio.h>
@@ -399,15 +398,14 @@ static DWORD WINAPI agent_thread_proc(LPVOID param)
ui_log(L"Install complete for \"%s\".", vm->name);
}
/* Send NAT IP to agent (only for NAT mode). Gateway is the chosen
subnet's .1; prefix length is always /24 for our NAT. */
if (vm->network_mode == NET_NAT && vm->nat_ip[0] != '\0') {
char ip_cmd[64];
sprintf_s(ip_cmd, sizeof(ip_cmd), "set_ip:%s/24:%s.1",
vm->nat_ip, hcn_nat_subnet_base());
n = send_tagged_cmd(s, vm, &conn->cmd_seq, ip_cmd, buf, sizeof(buf));
if (vm->network_mode == NET_NAT) {
DWORD timeout = 65000;
setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (char *)&timeout, sizeof(timeout));
n = send_tagged_cmd(s, vm, &conn->cmd_seq, "set_dhcp", buf, sizeof(buf));
if (n <= 0) goto disconnected;
ui_log(L"NAT IP config for \"%s\": %S", vm->name, buf);
timeout = 5000;
setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (char *)&timeout, sizeof(timeout));
ui_log(L"NAT DHCP config for \"%s\": %S", vm->name, buf);
}
/* Send GPU share info to agent (if GPU-PV is assigned).
+23 -96
View File
@@ -9,7 +9,7 @@
* when the host sends gpu_query_response with share metadata.
*
* Supports: ping, shutdown, restart, gpu_copy, gpu_query_response,
* gpu_none, idd_connect, set_ip.
* gpu_none, idd_connect, set_dhcp.
*
* Usage:
* appsandbox-agent.exe --install Install and start the service
@@ -1876,10 +1876,11 @@ static void disable_hyperv_video(AsbConn *notify_sock)
/* Forward declaration — defined after SSH proxy section */
static void handle_ssh_enable(AsbConn *client, const char *tag);
static ULONG primary_nic_index(void)
static ULONG primary_nic_index(BOOL *dhcp_enabled)
{
ULONG buf_len = 15000;
ULONG index = 0;
*dhcp_enabled = FALSE;
for (int attempt = 0; attempt < 3; attempt++) {
IP_ADAPTER_ADDRESSES *addrs, *cur;
@@ -1895,10 +1896,10 @@ static ULONG primary_nic_index(void)
for (cur = addrs; cur; cur = cur->Next) {
if (cur->IfType != IF_TYPE_ETHERNET_CSMACD || !cur->IfIndex)
continue;
if (!index) index = cur->IfIndex;
if (cur->OperStatus == IfOperStatusUp) {
if (!index || cur->OperStatus == IfOperStatusUp) {
index = cur->IfIndex;
break;
*dhcp_enabled = (cur->Flags & IP_ADAPTER_DHCP_ENABLED) != 0;
if (cur->OperStatus == IfOperStatusUp) break;
}
}
}
@@ -2041,100 +2042,26 @@ static void handle_client(AsbConn *client)
else if (strcmp(cmd, "ssh_enable") == 0) {
handle_ssh_enable(client, tag);
}
else if (strncmp(cmd, "set_ip:", 7) == 0) {
/* set_ip:172.20.0.X/PREFIX:GATEWAY */
char ip[32] = {0}, prefix[8] = {0}, gateway[32] = {0};
char *slash, *colon2;
char *arg = cmd + 7;
else if (strcmp(cmd, "set_dhcp") == 0) {
wchar_t wcmd[256];
wchar_t nic[32] = L"Ethernet";
BOOL dhcp_enabled;
ULONG nic_index = primary_nic_index(&dhcp_enabled);
DWORD result = ERROR_SUCCESS;
/* Parse IP/prefix:gateway */
slash = strchr(arg, '/');
colon2 = slash ? strchr(slash, ':') : NULL;
if (slash && colon2) {
int ip_len = (int)(slash - arg);
int pfx_len = (int)(colon2 - slash - 1);
if (ip_len > 0 && ip_len < (int)sizeof(ip))
strncpy_s(ip, sizeof(ip), arg, ip_len);
if (pfx_len > 0 && pfx_len < (int)sizeof(prefix))
strncpy_s(prefix, sizeof(prefix), slash + 1, pfx_len);
strncpy_s(gateway, sizeof(gateway), colon2 + 1, sizeof(gateway) - 1);
if (nic_index)
swprintf_s(nic, 32, L"%lu", nic_index);
if (!dhcp_enabled) {
swprintf_s(wcmd, 256,
L"netsh interface ipv4 set address name=\"%s\" source=dhcp", nic);
result = run_quiet(wcmd);
}
if (ip[0] && prefix[0] && gateway[0]) {
wchar_t wcmd[512];
wchar_t nic[32] = L"Ethernet";
ULONG nic_index = primary_nic_index();
STARTUPINFOW si;
PROCESS_INFORMATION pi;
DWORD exit_code = 1;
if (nic_index)
swprintf_s(nic, 32, L"%lu", nic_index);
swprintf_s(wcmd, 512,
L"netsh interface ip set address \"%s\" static %S %S %S",
nic, ip,
/* Convert prefix length to subnet mask */
atoi(prefix) == 16 ? "255.255.0.0" :
atoi(prefix) == 24 ? "255.255.255.0" :
atoi(prefix) == 8 ? "255.0.0.0" : "255.255.255.0",
gateway);
agent_log("Setting IP: %S", wcmd);
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
if (CreateProcessW(NULL, wcmd, NULL, NULL, FALSE,
CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) {
WaitForSingleObject(pi.hProcess, 10000);
GetExitCodeProcess(pi.hProcess, &exit_code);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
}
/* Set DNS to gateway (host NAT) + 8.8.8.8 fallback */
if (exit_code == 0) {
wchar_t dns_cmd[512];
STARTUPINFOW si2;
PROCESS_INFORMATION pi2;
swprintf_s(dns_cmd, 512,
L"netsh interface ip set dns \"%s\" static %S", nic, gateway);
ZeroMemory(&si2, sizeof(si2));
si2.cb = sizeof(si2);
ZeroMemory(&pi2, sizeof(pi2));
if (CreateProcessW(NULL, dns_cmd, NULL, NULL, FALSE,
CREATE_NO_WINDOW, NULL, NULL, &si2, &pi2)) {
WaitForSingleObject(pi2.hProcess, 10000);
CloseHandle(pi2.hProcess);
CloseHandle(pi2.hThread);
}
swprintf_s(dns_cmd, 512,
L"netsh interface ip add dns \"%s\" 8.8.8.8 index=2", nic);
ZeroMemory(&si2, sizeof(si2));
si2.cb = sizeof(si2);
ZeroMemory(&pi2, sizeof(pi2));
if (CreateProcessW(NULL, dns_cmd, NULL, NULL, FALSE,
CREATE_NO_WINDOW, NULL, NULL, &si2, &pi2)) {
WaitForSingleObject(pi2.hProcess, 10000);
CloseHandle(pi2.hProcess);
CloseHandle(pi2.hThread);
}
agent_log("IP configured: %s/%s gw %s dns %s,8.8.8.8",
ip, prefix, gateway, gateway);
REPLY("ok");
} else {
agent_log("netsh failed (exit %lu)", exit_code);
REPLY("error:netsh_failed");
}
} else {
agent_log("set_ip: bad format: %s", cmd);
REPLY("error:bad_format");
if (result == ERROR_SUCCESS) {
swprintf_s(wcmd, 256,
L"netsh interface ipv4 set dnsservers name=\"%s\" source=dhcp", nic);
result = run_quiet(wcmd);
}
REPLY(result == ERROR_SUCCESS ? "ok" : "error:dhcp_failed");
}
else if (strcmp(cmd, "gpu_copy") == 0) {
/* Host re-triggered GPU copy — ask for share list */
+23 -100
View File
@@ -483,121 +483,44 @@ static void *heartbeat_thread(void *arg)
/* ---- Command handlers ---- */
/* set_ip:<ip>/<prefix>:<gw>
* e.g. set_ip:192.168.42.2/24:192.168.42.1
*
* Mirrors the Windows agent's `netsh ip set static` clobber semantics:
* 1. Disable cloud-init's network module so it stops re-writing
* /etc/netplan/50-cloud-init.yaml at next boot.
* 2. Remove every other *.yaml in /etc/netplan so the merge has only
* our file to consider — no coexistence games.
* 3. Write /etc/netplan/99-appsandbox.yaml with the host-assigned
* address + gateway + DNS (gateway primary, 8.8.8.8 fallback —
* same DNS layout the Windows agent uses).
* 4. `netplan apply` then `systemctl restart systemd-networkd` so the
* kernel actually drops any stale addresses from a prior config.
*
* Reply: "ok" on success, "error:..." on failure. */
static void handle_set_ip(int fd, const char *tag, const char *args)
static void handle_set_dhcp(int fd, const char *tag)
{
char ip[64], prefix[8], gw[64];
const char *slash = strchr(args, '/');
const char *colon2 = slash ? strchr(slash, ':') : NULL;
size_t ip_len, pfx_len;
char cmd[2400];
int n, rc;
if (!slash || !colon2) {
send_reply(fd, tag, "error:bad_format");
return;
}
ip_len = (size_t)(slash - args);
pfx_len = (size_t)(colon2 - slash - 1);
if (ip_len >= sizeof(ip) || pfx_len >= sizeof(prefix)) {
send_reply(fd, tag, "error:bad_format");
return;
}
memcpy(ip, args, ip_len); ip[ip_len] = '\0';
memcpy(prefix, slash + 1, pfx_len); prefix[pfx_len] = '\0';
snprintf(gw, sizeof(gw), "%s", colon2 + 1);
n = snprintf(cmd, sizeof(cmd),
/* Pick the renderer by probing whether the NetworkManager service
* is active (Ubuntu Desktop); otherwise default to systemd-networkd
* (Server). Writing a netplan with the wrong renderer makes the
* active one stop managing the NIC entirely (which is why "no
* network in Ubuntu Settings" happened). */
const char *cmd =
"RENDERER=networkd; "
"if systemctl is-active --quiet NetworkManager; then "
" RENDERER=NetworkManager; "
"fi; "
/* Disable cloud-init's network module so it stops re-writing
* /etc/netplan/50-cloud-init.yaml on every boot. Idempotent. */
"mkdir -p /etc/cloud/cloud.cfg.d && "
"mkdir -p /etc/cloud/cloud.cfg.d /etc/netplan || exit 1; "
"printf 'network: {config: disabled}\\n' "
" > /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg && "
/* Remove every other netplan dropfile so merge can't reintroduce
* a conflicting interface key (e.g. cloud-init's `enp0s5: dhcp4`
* + our `appsbnic` both binding the same NIC). */
" > /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg || exit 1; "
"find /etc/netplan -maxdepth 1 -type f -name '*.yaml' "
" ! -name '99-appsandbox.yaml' -delete; "
/* Our authoritative config. Heredoc is unquoted so $RENDERER
* expands; nothing else in the YAML uses '$'. */
"umask 077 && cat > /etc/netplan/99-appsandbox.yaml <<EOF\n"
" ! -name '99-appsandbox.yaml' -delete || exit 1; "
"umask 077; cat > /etc/netplan/99-appsandbox.yaml <<EOF || exit 1\n"
"network:\n"
" version: 2\n"
" renderer: $RENDERER\n"
" ethernets:\n"
" appsbnic:\n"
" match: { name: \"e*\" }\n"
" dhcp4: false\n"
" dhcp4: true\n"
" dhcp6: false\n"
" addresses: [\"%s/%s\"]\n"
" routes:\n"
" - to: default\n"
" via: %s\n"
" nameservers:\n"
" addresses: [%s, 8.8.8.8]\n"
"EOF\n"
/* Apply, then restart the renderer to drop stale leases / state
* left behind by cloud-init or a previous run. netplan apply
* alone is sometimes a no-op against a NIC that already has an
* address the renderer hasn't released.
*
* IMPORTANT: reset umask BEFORE `netplan apply`. netplan's
* systemd-networkd backend writes the generated
* /run/systemd/network/10-netplan-*.network file via Python
* open() with no explicit mode, so the file ends up at
* (0666 & ~umask). With our 077 umask the generated file is 600
* (root:systemd-network), and systemd-networkd — running as
* `systemd-network` user, only in the systemd-network group —
* gets "Permission denied" trying to read it, falls back to the
* dracut catch-all `.network`, and eth0 ends up with no IP.
* 022 → generated .network is 644 → systemd-network can read it. */
"chmod 600 /etc/netplan/99-appsandbox.yaml && "
"umask 022 && "
"netplan apply 2>&1; "
"chmod 600 /etc/netplan/99-appsandbox.yaml || exit 1; "
"umask 022; "
"if [ \"$RENDERER\" = NetworkManager ]; then "
" systemctl restart NetworkManager 2>&1; "
" netplan generate 2>&1 && nmcli connection reload 2>&1 || exit 1; "
" if nmcli -t -f NAME connection show --active | grep -Fxq netplan-appsbnic && "
" nmcli -g DHCP4.OPTION connection show id netplan-appsbnic | grep -q . && "
" nmcli -g IP4.ADDRESS connection show id netplan-appsbnic | grep -q . && "
" nmcli -g IP4.GATEWAY connection show id netplan-appsbnic | grep -q .; then "
" exit 0; "
" fi; "
" nmcli --wait 10 connection up id netplan-appsbnic 2>&1; "
"else "
" systemctl restart systemd-networkd 2>&1; "
"fi; "
/* Verify the address actually materialised. netplan apply / NM
* reload are async — the host might try to SSH before the new
* IP claims the wire. Poll for up to 5 seconds. */
"for i in 1 2 3 4 5 6 7 8 9 10; do "
" ip -4 addr show | grep -q '%s/' && exit 0; "
" sleep 0.5; "
"done; "
"echo 'set_ip: address never appeared'; ip -4 addr show; exit 1",
ip, prefix, gw, gw, ip);
if (n < 0 || n >= (int)sizeof(cmd)) {
send_reply(fd, tag, "error:cmd_too_long");
return;
}
rc = run_sync(cmd);
agent_log("set_ip: %s/%s via %s → rc=%d", ip, prefix, gw, rc);
send_reply(fd, tag, rc == 0 ? "ok" : "error:netplan_failed");
" netplan apply 2>&1 && systemctl restart systemd-networkd 2>&1; "
"fi";
int rc = run_sync(cmd);
send_reply(fd, tag, rc == 0 ? "ok" : "error:dhcp_failed");
}
/* ---- SSH proxy: AF_VSOCK :7 ↔ localhost:22 ----
@@ -1109,8 +1032,8 @@ static void handle_client(int fd)
agent_log("restart requested");
spawn_detached("sleep 1 && systemctl reboot");
}
else if (strncmp(cmd, "set_ip:", 7) == 0) {
handle_set_ip(fd, tag, cmd + 7);
else if (strcmp(cmd, "set_dhcp") == 0) {
handle_set_dhcp(fd, tag);
}
else if (strcmp(cmd, "ssh_enable") == 0) {
handle_ssh_enable(fd, tag);