From d810eedc181d1b721928663d2967278663a6eec1 Mon Sep 17 00:00:00 2001 From: lateautumn233 Date: Sun, 17 May 2026 01:37:35 +0800 Subject: [PATCH 1/3] Dnsmasq: add dhcp router/dns options and disable dns port Set DHCP router option based on the bridge IPv4 network containing the range start, advertise public DNS servers, and disable the DNS listener since dnsmasq is only used for DHCP here. --- .../daemon/network/backend/Dnsmasq.java | 54 ++++++++++++++----- 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/Dnsmasq.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/Dnsmasq.java index cc20aef..c621198 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/Dnsmasq.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/Dnsmasq.java @@ -13,9 +13,12 @@ import java.io.File; import java.io.InputStreamReader; import java.io.InterruptedIOException; +import java.util.ArrayList; import java.util.concurrent.TimeUnit; import cn.classfun.droidvm.daemon.network.NetworkInstance; +import cn.classfun.droidvm.lib.network.IPv4Address; +import cn.classfun.droidvm.lib.network.IPv4Network; import cn.classfun.droidvm.lib.store.network.NetworkState; import cn.classfun.droidvm.lib.utils.ProcessUtils; @@ -36,22 +39,28 @@ private void startDnsmasqProcess( var bridge = inst.item.optString("bridge_name", ""); var rangeStart = inst.item.optString("dhcp_range_start", ""); var rangeEnd = inst.item.optString("dhcp_range_end", ""); + var router = findRouterAddress(rangeStart); Log.i(TAG, fmt("Starting dnsmasq on %s (%s - %s)", bridge, rangeStart, rangeEnd)); var pidFile = getDnsmasqPidFile(); var leaseFile = getDnsmasqLeaseFile(); try { - var pb = new ProcessBuilder( - "dnsmasq", - fmt("--interface=%s", bridge), - "--bind-interfaces", - fmt("--dhcp-range=%s,%s,12h", rangeStart, rangeEnd), - "--no-daemon", - "--keep-in-foreground", - fmt("--pid-file=%s", pidFile), - fmt("--dhcp-leasefile=%s", leaseFile), - "--log-queries", - "--log-dhcp" - ); + var args = new ArrayList(); + args.add("dnsmasq"); + args.add(fmt("--interface=%s", bridge)); + args.add("--bind-interfaces"); + args.add(fmt("--dhcp-range=%s,%s,12h", rangeStart, rangeEnd)); + if (router != null) + args.add(fmt("--dhcp-option=option:router,%s", router)); + args.add("--dhcp-option=option:dns-server,8.8.8.8,1.1.1.1"); + args.add("--port=0"); + args.add("--no-resolv"); + args.add("--no-daemon"); + args.add("--keep-in-foreground"); + args.add(fmt("--pid-file=%s", pidFile)); + args.add(fmt("--dhcp-leasefile=%s", leaseFile)); + args.add("--log-queries"); + args.add("--log-dhcp"); + var pb = new ProcessBuilder(args); pb.redirectErrorStream(true); dnsmasqExitCode = 0; dnsmasqProcess = pb.start(); @@ -61,6 +70,27 @@ private void startDnsmasqProcess( } } + @Nullable + private String findRouterAddress(@NonNull String rangeStart) { + IPv4Address dhcpStart = null; + try { + if (!rangeStart.isEmpty()) dhcpStart = IPv4Address.parse(rangeStart); + } catch (Exception ignored) { + } + + IPv4Network fallback = null; + for (var iter : inst.item.get("ipv4_addresses")) { + try { + var cidr = IPv4Network.parse(iter.getValue().asString()); + if (fallback == null) fallback = cidr; + if (dhcpStart != null && cidr.contains(dhcpStart)) + return cidr.address().toString(); + } catch (Exception ignored) { + } + } + return fallback != null ? fallback.address().toString() : null; + } + private void stopDnsmasqProcess(@Nullable Process process) { var bridge = inst.item.optString("bridge_name", ""); Log.i(TAG, fmt("Stopping dnsmasq on %s", bridge)); From 29a224d6a755df62233b83028ae0a69e5b66da08 Mon Sep 17 00:00:00 2001 From: lateautumn233 Date: Sun, 17 May 2026 01:37:40 +0800 Subject: [PATCH 2/3] Dnsmasq: prefer prebuilt binary when available Resolve the dnsmasq executable to the prebuilt path when present and executable, falling back to PATH lookup otherwise. --- .../droidvm/daemon/network/backend/Dnsmasq.java | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/Dnsmasq.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/Dnsmasq.java index c621198..f7d9435 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/Dnsmasq.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/Dnsmasq.java @@ -1,6 +1,7 @@ package cn.classfun.droidvm.daemon.network.backend; import static cn.classfun.droidvm.lib.Constants.DATA_DIR; +import static cn.classfun.droidvm.lib.utils.AssetUtils.getPrebuiltBinaryPath; import static cn.classfun.droidvm.lib.utils.StringUtils.fmt; import static cn.classfun.droidvm.lib.utils.StringUtils.pathJoin; @@ -34,18 +35,29 @@ public Dnsmasq(NetworkInstance inst) { this.inst = inst; } + @NonNull + private static String resolveDnsmasqBinary() { + var prebuilt = getPrebuiltBinaryPath("dnsmasq"); + var file = new File(prebuilt); + if (file.isFile() && file.canExecute()) + return prebuilt; + return "dnsmasq"; + } + private void startDnsmasqProcess( ) { var bridge = inst.item.optString("bridge_name", ""); var rangeStart = inst.item.optString("dhcp_range_start", ""); var rangeEnd = inst.item.optString("dhcp_range_end", ""); var router = findRouterAddress(rangeStart); - Log.i(TAG, fmt("Starting dnsmasq on %s (%s - %s)", bridge, rangeStart, rangeEnd)); + var dnsmasq = resolveDnsmasqBinary(); + Log.i(TAG, fmt("Starting dnsmasq on %s (%s - %s) using %s", + bridge, rangeStart, rangeEnd, dnsmasq)); var pidFile = getDnsmasqPidFile(); var leaseFile = getDnsmasqLeaseFile(); try { var args = new ArrayList(); - args.add("dnsmasq"); + args.add(dnsmasq); args.add(fmt("--interface=%s", bridge)); args.add("--bind-interfaces"); args.add(fmt("--dhcp-range=%s,%s,12h", rangeStart, rangeEnd)); From 5173797eb4e29e73f46bf493a3f51796d72b7b9f Mon Sep 17 00:00:00 2001 From: lateautumn233 Date: Sun, 17 May 2026 19:37:25 +0800 Subject: [PATCH 3/3] Dnsmasq: support customizable dns servers Add a DNS Servers section in NetworkEditActivity reusing the IPv4/IPv6 address row pattern. Defaults to 8.8.8.8 and 1.1.1.1, requires at least one entry. Dnsmasq now reads dns_servers from network config instead of the previously hardcoded list. --- .../daemon/network/backend/Dnsmasq.java | 8 +- .../ui/network/edit/NetworkEditActivity.java | 97 ++++++++++++++++--- app/src/main/res/drawable/ic_dns.xml | 9 ++ .../main/res/layout/activity_network_edit.xml | 36 +++++++ app/src/main/res/values-zh-rCN/strings.xml | 5 + app/src/main/res/values/strings.xml | 5 + 6 files changed, 147 insertions(+), 13 deletions(-) create mode 100644 app/src/main/res/drawable/ic_dns.xml diff --git a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/Dnsmasq.java b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/Dnsmasq.java index f7d9435..0c41b71 100644 --- a/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/Dnsmasq.java +++ b/app/src/main/java/cn/classfun/droidvm/daemon/network/backend/Dnsmasq.java @@ -63,7 +63,13 @@ private void startDnsmasqProcess( args.add(fmt("--dhcp-range=%s,%s,12h", rangeStart, rangeEnd)); if (router != null) args.add(fmt("--dhcp-option=option:router,%s", router)); - args.add("--dhcp-option=option:dns-server,8.8.8.8,1.1.1.1"); + var dnsServers = new ArrayList(); + for (var iter : inst.item.get("dns_servers")) { + var s = iter.getValue().asString(); + if (s != null && !s.isEmpty()) dnsServers.add(s); + } + if (!dnsServers.isEmpty()) + args.add(fmt("--dhcp-option=option:dns-server,%s", String.join(",", dnsServers))); args.add("--port=0"); args.add("--no-resolv"); args.add("--no-daemon"); diff --git a/app/src/main/java/cn/classfun/droidvm/ui/network/edit/NetworkEditActivity.java b/app/src/main/java/cn/classfun/droidvm/ui/network/edit/NetworkEditActivity.java index 6bc05ac..42745bb 100644 --- a/app/src/main/java/cn/classfun/droidvm/ui/network/edit/NetworkEditActivity.java +++ b/app/src/main/java/cn/classfun/droidvm/ui/network/edit/NetworkEditActivity.java @@ -31,7 +31,6 @@ import java.util.UUID; import cn.classfun.droidvm.R; -import cn.classfun.droidvm.lib.network.IPNetwork; import cn.classfun.droidvm.lib.network.IPv4Address; import cn.classfun.droidvm.lib.network.IPv4Network; import cn.classfun.droidvm.lib.network.IPv6Network; @@ -47,14 +46,15 @@ public final class NetworkEditActivity extends AppCompatActivity { public static final String EXTRA_NETWORK_ID = "network_id"; private final List ipv4Addresses = new ArrayList<>(); private final List ipv6Addresses = new ArrayList<>(); + private final List dnsServers = new ArrayList<>(); private boolean editMode = false; private UUID editNetworkId = null; private CollapsingToolbarLayout collapsingToolbar; private TextInputRowWidget inputName, inputBridgeName, inputMac; private SwitchRowWidget swAutoUp, swStp, swNat, swDhcp; - private TextInputRowWidget inputIPv4, inputIPv6; - private LinearLayout layoutIPv4, layoutIPv6, groupDhcp; - private TextView tvIPv4Empty, tvIPv6Empty; + private TextInputRowWidget inputIPv4, inputIPv6, inputDns; + private LinearLayout layoutIPv4, layoutIPv6, layoutDns, groupDhcp; + private TextView tvIPv4Empty, tvIPv6Empty, tvDnsEmpty; private TextInputRowWidget inputDhcpStart, inputDhcpEnd; private FloatingActionButton fab; private NetworkStore store; @@ -87,10 +87,13 @@ protected void onCreate(Bundle savedInstanceState) { swDhcp = findViewById(R.id.sw_dhcp); inputIPv4 = findViewById(R.id.input_ipv4); inputIPv6 = findViewById(R.id.input_ipv6); + inputDns = findViewById(R.id.input_dns); layoutIPv4 = findViewById(R.id.layout_ipv4); layoutIPv6 = findViewById(R.id.layout_ipv6); + layoutDns = findViewById(R.id.layout_dns); tvIPv4Empty = findViewById(R.id.tv_ipv4_empty); tvIPv6Empty = findViewById(R.id.tv_ipv6_empty); + tvDnsEmpty = findViewById(R.id.tv_dns_empty); groupDhcp = findViewById(R.id.group_dhcp); inputDhcpStart = findViewById(R.id.input_dhcp_start); inputDhcpEnd = findViewById(R.id.input_dhcp_end); @@ -104,6 +107,7 @@ private void initialize() { store.load(this); inputIPv4.setEndIconOnClickListener(v -> onAddIPv4()); inputIPv6.setEndIconOnClickListener(v -> onAddIPv6()); + inputDns.setEndIconOnClickListener(v -> onAddDns()); inputIPv4.addTextChangedListener(new SimpleTextWatcher() { @Override public void onTextChanged(CharSequence s, int start, int before, int count) { @@ -116,6 +120,12 @@ public void onTextChanged(CharSequence s, int start, int before, int count) { inputIPv6.setError(null); } }); + inputDns.addTextChangedListener(new SimpleTextWatcher() { + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + inputDns.setError(null); + } + }); swDhcp.setOnCheckedChangeListener((btn, checked) -> groupDhcp.setVisibility(checked ? VISIBLE : GONE)); fab.setOnClickListener(v -> onSaveClicked()); @@ -130,8 +140,9 @@ public void onTextChanged(CharSequence s, int start, int before, int count) { collapsingToolbar.setTitle(getString(R.string.network_create_title)); generateDefaults(); } - buildAddressList(layoutIPv4, tvIPv4Empty, ipv4Addresses, true); - buildAddressList(layoutIPv6, tvIPv6Empty, ipv6Addresses, false); + buildAddressList(layoutIPv4, tvIPv4Empty, ipv4Addresses, this::updateDhcpFromIPv4); + buildAddressList(layoutIPv6, tvIPv6Empty, ipv6Addresses, null); + buildAddressList(layoutDns, tvDnsEmpty, dnsServers, null); } private void installManualEditWatchers() { @@ -177,10 +188,18 @@ private void generateDefaults() { setTextProgrammatic(inputBridgeName, bridgeNameFromMac(mac)); var ipv4Cidr = generateRandomIPv4(); ipv4Addresses.add(ipv4Cidr); + addDefaultDnsServers(); swDhcp.setChecked(true); updateDhcpFromIPv4(); } + private void addDefaultDnsServers() { + var a = IPv4Address.parse("8.8.8.8"); + var b = IPv4Address.parse("1.1.1.1"); + if (a != null) dnsServers.add(a); + if (b != null) dnsServers.add(b); + } + @NonNull private String bridgeNameFromMac(@NonNull String mac) { return fmt("br%s", mac.replace(":", "").toLowerCase()); @@ -268,6 +287,9 @@ private void loadExistingConfig() { parseIPv4Addresses(config, ipv4Addresses); ipv6Addresses.clear(); parseIPv6Addresses(config, ipv6Addresses); + dnsServers.clear(); + parseDnsServers(config, dnsServers); + if (dnsServers.isEmpty()) addDefaultDnsServers(); macManuallyEdited = true; bridgeManuallyEdited = true; ipv4ManuallyEdited = true; @@ -293,7 +315,7 @@ private void onAddIPv4() { ipv4Addresses.add(ip); ipv4ManuallyEdited = true; inputIPv4.setText(""); - buildAddressList(layoutIPv4, tvIPv4Empty, ipv4Addresses, true); + buildAddressList(layoutIPv4, tvIPv4Empty, ipv4Addresses, this::updateDhcpFromIPv4); updateDhcpFromIPv4(); } @@ -314,14 +336,38 @@ private void onAddIPv6() { inputIPv6.setError(null); ipv6Addresses.add(ip); inputIPv6.setText(""); - buildAddressList(layoutIPv6, tvIPv6Empty, ipv6Addresses, false); + buildAddressList(layoutIPv6, tvIPv6Empty, ipv6Addresses, null); + } + + private void onAddDns() { + var addr = inputDns.getText().trim(); + if (addr.isEmpty()) return; + if (!IPv4Address.isValid(addr)) { + inputDns.setError(getString(R.string.network_edit_error_dns_invalid)); + return; + } + var ip = IPv4Address.parse(addr); + if (ip == null) { + inputDns.setError(getString(R.string.network_edit_error_dns_invalid)); + return; + } + for (var existing : dnsServers) { + if (existing.value() == ip.value()) { + inputDns.setText(""); + return; + } + } + inputDns.setError(null); + dnsServers.add(ip); + inputDns.setText(""); + buildAddressList(layoutDns, tvDnsEmpty, dnsServers, null); } private void buildAddressList( @NonNull LinearLayout container, @NonNull TextView emptyView, - @NonNull List> list, - boolean isIPv4 + @NonNull List list, + @Nullable Runnable onRemoved ) { container.removeAllViews(); if (list.isEmpty()) { @@ -347,8 +393,8 @@ private void buildAddressList( btn.setContentDescription(getString(R.string.network_edit_remove_address)); btn.setOnClickListener(v -> { list.remove(idx); - buildAddressList(container, emptyView, list, isIPv4); - if (isIPv4) updateDhcpFromIPv4(); + buildAddressList(container, emptyView, list, onRemoved); + if (onRemoved != null) onRemoved.run(); }); row.addView(btn); container.addView(row); @@ -362,6 +408,7 @@ private void onSaveClicked() { inputMac.setError(null); inputIPv4.setError(null); inputIPv6.setError(null); + inputDns.setError(null); inputDhcpStart.setError(null); inputDhcpEnd.setError(null); if (!inputIPv4.getText().trim().isEmpty()) { @@ -372,6 +419,14 @@ private void onSaveClicked() { inputIPv6.setError(getString(R.string.network_edit_error_ipv6_not_added)); return; } + if (!inputDns.getText().trim().isEmpty()) { + inputDns.setError(getString(R.string.network_edit_error_dns_not_added)); + return; + } + if (dnsServers.isEmpty()) { + inputDns.setError(getString(R.string.network_edit_error_dns_empty)); + return; + } var name = inputName.getText().trim(); var bridgeName = inputBridgeName.getText().trim(); var mac = inputMac.getText().trim(); @@ -445,6 +500,7 @@ private void onSaveClicked() { config.item.set("dhcp_enabled", swDhcp.isChecked()); setIPv4Addresses(config, ipv4Addresses); setIPv6Addresses(config, ipv6Addresses); + setDnsServers(config, dnsServers); config.item.set("dhcp_range_start", inputDhcpStart.getText().trim()); config.item.set("dhcp_range_end", inputDhcpEnd.getText().trim()); if (editMode) { @@ -544,6 +600,16 @@ private static void parseIPv6Addresses(@NonNull NetworkConfig cfg, @NonNull List }); } + private static void parseDnsServers(@NonNull NetworkConfig cfg, @NonNull List out) { + cfg.item.get("dns_servers").forEachArray(a -> { + try { + var ip = IPv4Address.parse(a.asString()); + if (ip != null) out.add(ip); + } catch (Exception ignored) { + } + }); + } + private static void setIPv4Addresses(@NonNull NetworkConfig cfg, @NonNull List addresses) { var arr = DataItem.newArray(); for (var addr : addresses) @@ -557,4 +623,11 @@ private static void setIPv6Addresses(@NonNull NetworkConfig cfg, @NonNull List dns) { + var arr = DataItem.newArray(); + for (var ip : dns) + arr.append(DataItem.newString(ip.toString())); + cfg.item.set("dns_servers", arr); + } } diff --git a/app/src/main/res/drawable/ic_dns.xml b/app/src/main/res/drawable/ic_dns.xml new file mode 100644 index 0000000..717ed89 --- /dev/null +++ b/app/src/main/res/drawable/ic_dns.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_network_edit.xml b/app/src/main/res/layout/activity_network_edit.xml index 4eb7bc0..eb2ca8e 100644 --- a/app/src/main/res/layout/activity_network_edit.xml +++ b/app/src/main/res/layout/activity_network_edit.xml @@ -168,6 +168,42 @@ + + + + + + + + + + NAT(网络地址转换) IPv4 地址 IPv4 CIDR(例如 192.168.1.1/24) + DNS 服务器 + DNS 服务器(例如 8.8.8.8) IPv6 地址 IPv6 CIDR(例如 fd00::1/64) 未配置地址。 @@ -628,6 +630,9 @@ 无效的 DHCP 地址池结束地址 IPv4 地址输入框不为空,请先添加或清空 IPv6 地址输入框不为空,请先添加或清空 + DNS 服务器输入框不为空,请先添加或清空 + 无效的 DNS 服务器地址 + 至少需要一个 DNS 服务器 启动 停止 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7f83b1c..87c928b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -641,6 +641,8 @@ NAT (Network Address Translate) IPv4 Addresses IPv4 CIDR (e.g. 192.168.1.1/24) + DNS Servers + DNS server (e.g. 8.8.8.8) IPv6 Addresses IPv6 CIDR (e.g. fd00::1/64) No addresses configured. @@ -666,6 +668,9 @@ Invalid DHCP pool end address IPv4 address input is not empty, please add it first or clear the field IPv6 address input is not empty, please add it first or clear the field + DNS server input is not empty, please add it first or clear the field + Invalid DNS server address + At least one DNS server is required Start Stop