diff --git a/src/backend_win/asb_core.c b/src/backend_win/asb_core.c index a5cfe04..463d48c 100644 --- a/src/backend_win/asb_core.c +++ b/src/backend_win/asb_core.c @@ -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(); diff --git a/src/backend_win/hcn_network.c b/src/backend_win/hcn_network.c index 552c3b1..d3333a7 100644 --- a/src/backend_win/hcn_network.c +++ b/src/backend_win/hcn_network.c @@ -4,6 +4,7 @@ #include #include #include +#include #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; diff --git a/src/backend_win/hcn_network.h b/src/backend_win/hcn_network.h index e8b09d2..42030fe 100644 --- a/src/backend_win/hcn_network.h +++ b/src/backend_win/hcn_network.h @@ -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); diff --git a/src/backend_win/hcs_vm.c b/src/backend_win/hcs_vm.c index 2c8de26..d70c101 100644 --- a/src/backend_win/hcs_vm.c +++ b/src/backend_win/hcs_vm.c @@ -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 diff --git a/src/backend_win/hcs_vm.h b/src/backend_win/hcs_vm.h index 87c1595..5555d4b 100644 --- a/src/backend_win/hcs_vm.h +++ b/src/backend_win/hcs_vm.h @@ -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) */ diff --git a/src/backend_win/vm_agent.c b/src/backend_win/vm_agent.c index a7a04bb..f809c95 100644 --- a/src/backend_win/vm_agent.c +++ b/src/backend_win/vm_agent.c @@ -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 @@ -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). diff --git a/tools/agent/agent.c b/tools/agent/agent.c index adfa174..b6fda1b 100644 --- a/tools/agent/agent.c +++ b/tools/agent/agent.c @@ -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 */ diff --git a/tools/linux/agent/appsandbox-agent.c b/tools/linux/agent/appsandbox-agent.c index 462a01b..91d5fbc 100644 --- a/tools/linux/agent/appsandbox-agent.c +++ b/tools/linux/agent/appsandbox-agent.c @@ -483,121 +483,44 @@ static void *heartbeat_thread(void *arg) /* ---- Command handlers ---- */ -/* set_ip:/: - * 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 < /etc/netplan/99-appsandbox.yaml <&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);