Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 0 additions & 22 deletions controller/app/src/main/java/org/iiab/controller/Preferences.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ public class Preferences {
public static final String SOCKS_PORT = "SocksPort";
public static final String SOCKS_USER = "SocksUser";
public static final String SOCKS_PASS = "SocksPass";
public static final String DNS_IPV4 = "DnsIpv4";
public static final String DNS_IPV6 = "DnsIpv6";
public static final String IPV4 = "Ipv4";
public static final String IPV6 = "Ipv6";
public static final String GLOBAL = "Global";
Expand Down Expand Up @@ -92,26 +90,6 @@ public void setSocksPassword(String pass) {
editor.commit();
}

public String getDnsIpv4() {
return prefs.getString(DNS_IPV4, "8.8.8.8");
}

public void setDnsIpv4(String addr) {
SharedPreferences.Editor editor = prefs.edit();
editor.putString(DNS_IPV4, addr);
editor.commit();
}

public String getDnsIpv6() {
return prefs.getString(DNS_IPV6, "2001:4860:4860::8888");
}

public void setDnsIpv6(String addr) {
SharedPreferences.Editor editor = prefs.edit();
editor.putString(DNS_IPV6, addr);
editor.commit();
}

public String getMappedDns() {
return "198.18.0.2";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;

import org.iiab.controller.network.presentation.DnsSettingsUiState;
import org.iiab.controller.network.presentation.DnsSettingsViewModel;
import org.iiab.controller.network.presentation.DnsSettingsViewModelFactory;

import com.google.android.material.snackbar.Snackbar;

Expand All @@ -43,7 +48,7 @@ public class UsageFragment extends Fragment implements View.OnClickListener {

private MainActivity mainActivity;
// INTERFACE VARS
private EditText edittext_socks_addr, edittext_socks_udp_addr, edittext_socks_port, edittext_socks_user, edittext_socks_pass, edittext_dns_ipv4, edittext_dns_ipv6;
private EditText edittext_socks_addr, edittext_socks_udp_addr, edittext_socks_port, edittext_socks_user, edittext_socks_pass;
private CheckBox checkbox_udp_in_tcp, checkbox_remote_dns, checkbox_global, checkbox_maintenance, checkbox_ipv4, checkbox_ipv6;
private TextView textview_maintenance_warning, configLabel, advConfigLabel, logLabel, logWarning, logSizeText, connectionLog;
private Button button_apps, button_save, button_control, button_browse_content, btnClearLog, btnCopyLog;
Expand All @@ -53,6 +58,17 @@ public class UsageFragment extends Fragment implements View.OnClickListener {

private DashboardManager dashboardManager;

// Setup DNS (network slice, PR B)
private CheckBox setup_dns_check;
private LinearLayout dns_setup_fields;
private EditText dns_primary, dns_secondary;
private Button dns_accept;
private TextView dns_result;
private TextView dns_settings_label;
private LinearLayout dns_settings_section;
private DnsSettingsViewModel dnsViewModel;
private boolean suppressDnsToggle = false;

@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
Expand All @@ -78,8 +94,25 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
edittext_socks_port = view.findViewById(R.id.socks_port);
edittext_socks_user = view.findViewById(R.id.socks_user);
edittext_socks_pass = view.findViewById(R.id.socks_pass);
edittext_dns_ipv4 = view.findViewById(R.id.dns_ipv4);
edittext_dns_ipv6 = view.findViewById(R.id.dns_ipv6);
setup_dns_check = view.findViewById(R.id.setup_dns_check);
dns_setup_fields = view.findViewById(R.id.dns_setup_fields);
dns_primary = view.findViewById(R.id.dns_primary);
dns_secondary = view.findViewById(R.id.dns_secondary);
dns_accept = view.findViewById(R.id.dns_accept);
dns_result = view.findViewById(R.id.dns_result);
dns_settings_label = view.findViewById(R.id.dns_settings_label);
dns_settings_section = view.findViewById(R.id.dns_settings_section);
dns_settings_label.setText(String.format(getString(R.string.label_separator_up), getString(R.string.network_advanced_label)));
dns_settings_label.setOnClickListener(v -> toggleVisibility(dns_settings_section, dns_settings_label, getString(R.string.network_advanced_label)));
dnsViewModel = new ViewModelProvider(this, new DnsSettingsViewModelFactory(requireContext()))
.get(DnsSettingsViewModel.class);
dnsViewModel.state().observe(getViewLifecycleOwner(), this::renderDnsState);
setup_dns_check.setOnCheckedChangeListener((btn, checked) -> {
if (suppressDnsToggle) return;
dnsViewModel.onSetupToggled(checked);
});
dns_accept.setOnClickListener(v -> dnsViewModel.onAccept(
dns_primary.getText().toString(), dns_secondary.getText().toString()));
checkbox_ipv4 = view.findViewById(R.id.ipv4);
checkbox_ipv6 = view.findViewById(R.id.ipv6);
checkbox_global = view.findViewById(R.id.global);
Expand Down Expand Up @@ -210,8 +243,6 @@ public void updateUI() {
edittext_socks_port.setText(String.valueOf(mainActivity.prefs.getSocksPort()));
edittext_socks_user.setText(mainActivity.prefs.getSocksUsername());
edittext_socks_pass.setText(mainActivity.prefs.getSocksPassword());
edittext_dns_ipv4.setText(mainActivity.prefs.getDnsIpv4());
edittext_dns_ipv6.setText(mainActivity.prefs.getDnsIpv6());
checkbox_ipv4.setChecked(mainActivity.prefs.getIpv4());
checkbox_ipv6.setChecked(mainActivity.prefs.getIpv6());
checkbox_global.setChecked(mainActivity.prefs.getGlobal());
Expand Down Expand Up @@ -450,10 +481,42 @@ public void savePrefsFromUI() {
mainActivity.prefs.setRemoteDns(true);
mainActivity.prefs.setGlobal(true);

mainActivity.prefs.setDnsIpv4(edittext_dns_ipv4.getText().toString());
mainActivity.prefs.setDnsIpv6(edittext_dns_ipv6.getText().toString());
mainActivity.prefs.setMaintenanceMode(checkbox_maintenance.isChecked());
}

private void renderDnsState(DnsSettingsUiState st) {
if (setup_dns_check == null) return;
suppressDnsToggle = true;
setup_dns_check.setChecked(st.customEnabled);
suppressDnsToggle = false;
dns_setup_fields.setVisibility(st.customEnabled ? View.VISIBLE : View.GONE);
if (st.status == DnsSettingsUiState.Status.IDLE || st.status == DnsSettingsUiState.Status.UNREACHABLE) {
dns_primary.setText(st.primary);
dns_secondary.setText(st.secondary);
}
switch (st.status) {
case TESTING:
dns_result.setVisibility(View.VISIBLE);
dns_result.setText(getString(R.string.dns_status_testing));
dns_result.setTextColor(ContextCompat.getColor(requireContext(), R.color.text_secondary));
break;
case APPLIED:
dns_result.setVisibility(View.VISIBLE);
dns_result.setText(getString(R.string.dns_status_ok));
dns_result.setTextColor(ContextCompat.getColor(requireContext(), R.color.status_success));
break;
case INVALID:
case UNREACHABLE:
dns_result.setVisibility(View.VISIBLE);
dns_result.setText(st.message != null ? st.message : "");
dns_result.setTextColor(ContextCompat.getColor(requireContext(), R.color.status_warning));
break;
default:
dns_result.setVisibility(View.GONE);
break;
}
}

public void highlightServerButton() {
if (deckContainer == null || !isAdded()) return;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* ============================================================================
* Name : UdpDnsConnectivityProbe.java
* Author : AppDevForAll
* Copyright : Copyright (c) 2026 AppDevForAll
* Description : UDP DNS-query probe: sends a query to the server and waits for a reply.
* ============================================================================
*/
package org.iiab.controller.network.data;

import android.util.Log;

import org.iiab.controller.network.domain.DnsConnectivityProbe;

import java.io.ByteArrayOutputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;

/**
* {@link DnsConnectivityProbe} that sends a tiny DNS A-query (for a fixed, stable
* name) over UDP to the given server on port 53 and waits for a valid reply.
* Pure {@code java.net}; works for IPv4 and IPv6 servers. Never throws — any
* error (timeout, unreachable, malformed) means "not reachable".
*/
public final class UdpDnsConnectivityProbe implements DnsConnectivityProbe {

private static final String TAG = "IIAB-DNS";
private static final String PROBE_NAME = "example.com";
private static final int DNS_PORT = 53;

@Override
public boolean isReachable(String dnsServer, long timeoutMs) {
if (dnsServer == null || dnsServer.trim().isEmpty()) return false;
DatagramSocket socket = null;
try {
byte[] query = buildQuery(PROBE_NAME);
InetAddress addr = InetAddress.getByName(dnsServer.trim());
socket = new DatagramSocket();
socket.setSoTimeout((int) Math.max(1, Math.min(timeoutMs, Integer.MAX_VALUE)));
socket.send(new DatagramPacket(query, query.length, addr, DNS_PORT));

byte[] buf = new byte[512];
DatagramPacket response = new DatagramPacket(buf, buf.length);
socket.receive(response);

if (response.getLength() < 12) return false;
boolean idMatches = buf[0] == query[0] && buf[1] == query[1];
boolean isResponse = (buf[2] & 0x80) != 0;
int rcode = buf[3] & 0x0F;
return idMatches && isResponse && rcode == 0;
} catch (Exception e) {
Log.i(TAG, "DNS probe to " + dnsServer + " failed: " + e.getMessage());
return false;
} finally {
if (socket != null) socket.close();
}
}

private static byte[] buildQuery(String name) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
int id = (int) (System.nanoTime() & 0xFFFF);
out.write((id >> 8) & 0xFF);
out.write(id & 0xFF);
out.write(0x01);
out.write(0x00);
out.write(0x00);
out.write(0x01);
out.write(0x00);
out.write(0x00);
out.write(0x00);
out.write(0x00);
out.write(0x00);
out.write(0x00);
for (String label : name.split("\\.")) {
byte[] b = label.getBytes(StandardCharsets.US_ASCII);
out.write(b.length);
out.write(b, 0, b.length);
}
out.write(0x00);
out.write(0x00);
out.write(0x01);
out.write(0x00);
out.write(0x01);
return out.toByteArray();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* ============================================================================
* Name : ConfigureDnsUseCase.java
* Author : AppDevForAll
* Copyright : Copyright (c) 2026 AppDevForAll
* Description : Validate, netplan-try probe, then save (or revert to defaults).
* ============================================================================
*/
package org.iiab.controller.network.domain;

/**
* Orchestrates applying a user-entered DNS config, netplan-try style:
* <ol>
* <li>validate (fail-closed) — invalid input is never saved;</li>
* <li>probe the server(s) for an actual DNS reply;</li>
* <li>if reachable, persist as custom; otherwise revert to defaults and report.</li>
* </ol>
* Pure domain logic over the {@link DnsConfigRepository} and {@link DnsConnectivityProbe}
* ports — unit-testable with fakes.
*/
public final class ConfigureDnsUseCase {

public enum Outcome { APPLIED, INVALID, UNREACHABLE }

public static final class Result {
public final Outcome outcome;
public final String message;
private Result(Outcome outcome, String message) { this.outcome = outcome; this.message = message; }
static Result applied() { return new Result(Outcome.APPLIED, null); }
static Result invalid(String reason) { return new Result(Outcome.INVALID, reason); }
static Result unreachable() { return new Result(Outcome.UNREACHABLE, "DNS did not respond"); }
}

private static final long DEFAULT_TIMEOUT_MS = 4000L;

private final DnsConfigRepository repository;
private final DnsConnectivityProbe probe;
private final long timeoutMs;

public ConfigureDnsUseCase(DnsConfigRepository repository, DnsConnectivityProbe probe) {
this(repository, probe, DEFAULT_TIMEOUT_MS);
}

public ConfigureDnsUseCase(DnsConfigRepository repository, DnsConnectivityProbe probe, long timeoutMs) {
this.repository = repository;
this.probe = probe;
this.timeoutMs = timeoutMs;
}

public Result execute(DnsConfig candidate) {
DnsValidator.Result validation = DnsValidator.validate(candidate);
if (!validation.valid) {
return Result.invalid(validation.reason);
}
boolean reachable = probe.isReachable(candidate.primary(), timeoutMs);
if (!reachable && candidate.hasSecondary()) {
reachable = probe.isReachable(candidate.secondary(), timeoutMs);
}
if (reachable) {
repository.saveCustom(candidate);
return Result.applied();
}
repository.disableCustom();
return Result.unreachable();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* ============================================================================
* Name : DnsConnectivityProbe.java
* Author : AppDevForAll
* Copyright : Copyright (c) 2026 AppDevForAll
* Description : Domain port: can a DNS server actually answer (netplan-try style).
* ============================================================================
*/
package org.iiab.controller.network.domain;

/**
* Domain port that probes whether a DNS server is a working resolver reachable
* from the device. The Data layer implements it (a small UDP DNS query). Used for
* the netplan-try style verification before committing a user-chosen DNS.
*/
public interface DnsConnectivityProbe {

/**
* @param dnsServer IPv4/IPv6 literal of the server to test
* @param timeoutMs how long to wait for a reply
* @return {@code true} if the server answered a DNS query within the timeout
*/
boolean isReachable(String dnsServer, long timeoutMs);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* ============================================================================
* Name : DnsSettingsUiState.java
* Author : AppDevForAll
* Copyright : Copyright (c) 2026 AppDevForAll
* Description : Immutable UI state for the Setup DNS panel.
* ============================================================================
*/
package org.iiab.controller.network.presentation;

/**
* Immutable UI state for the Setup DNS panel, exposed by {@link DnsSettingsViewModel}.
* {@code customEnabled} drives the checkbox; {@code primary}/{@code secondary} prefill
* the fields; {@code status} + {@code message} drive the result indicator.
*/
public final class DnsSettingsUiState {

public enum Status { IDLE, TESTING, APPLIED, INVALID, UNREACHABLE }

public final boolean customEnabled;
public final String primary;
public final String secondary;
public final Status status;
public final String message;

private DnsSettingsUiState(boolean customEnabled, String primary, String secondary, Status status, String message) {
this.customEnabled = customEnabled;
this.primary = primary == null ? "" : primary;
this.secondary = secondary == null ? "" : secondary;
this.status = status;
this.message = message;
}

public static DnsSettingsUiState idle(boolean customEnabled, String primary, String secondary) {
return new DnsSettingsUiState(customEnabled, primary, secondary, Status.IDLE, null);
}

public static DnsSettingsUiState testing(String primary, String secondary) {
return new DnsSettingsUiState(true, primary, secondary, Status.TESTING, null);
}

public static DnsSettingsUiState applied(String primary, String secondary) {
return new DnsSettingsUiState(true, primary, secondary, Status.APPLIED, null);
}

public static DnsSettingsUiState invalid(String primary, String secondary, String message) {
return new DnsSettingsUiState(true, primary, secondary, Status.INVALID, message);
}

/** Probe failed: the use case reverted to defaults, so the panel shows defaults again. */
public static DnsSettingsUiState unreachable(String defaultPrimary, String defaultSecondary) {
return new DnsSettingsUiState(false, defaultPrimary, defaultSecondary, Status.UNREACHABLE,
"DNS did not respond — reverted to defaults");
}
}
Loading