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..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 @@ -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; @@ -13,9 +14,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; @@ -31,27 +35,50 @@ 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", ""); - Log.i(TAG, fmt("Starting dnsmasq on %s (%s - %s)", bridge, rangeStart, rangeEnd)); + var router = findRouterAddress(rangeStart); + 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 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)); + 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"); + 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 +88,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)); 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