Skip to content

Commit 76ca7d4

Browse files
Merge pull request #26 from appdevforall/feat/network-dns-slice-part1
network-dns-slice (part 1/2): DNS Clean-Arch slice + single injection point
2 parents 933552c + b3fb0e3 commit 76ca7d4

15 files changed

Lines changed: 661 additions & 42 deletions

‎controller/app/src/main/java/org/iiab/controller/DeployFragment.java‎

Lines changed: 4 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1096,9 +1096,8 @@ public void onComplete(String destDir) {
10961096
}
10971097
}
10981098

1099-
// Freshly fast-installed rootfs ships without resolv.conf; give it
1100-
// DNS before the companion-data (maps/kiwix) steps that need network.
1101-
ensureRuntimeNetworkConfig(debianRootfs);
1099+
// DNS is written at the single chokepoint (PRootEngine.executeInContainer),
1100+
// so the companion-data proot steps below get a working resolv.conf for free.
11021101

11031102
if (chkCompanionData.isChecked()) {
11041103
editLocalVarsForMaps(debianRootfs, safeTier);
@@ -1307,16 +1306,8 @@ public void onComplete(String downloadPath) {
13071306
// 4. BOOTSTRAP IIAB
13081307
mainAct.runOnUiThread(() -> btnAdvancedReset.setText(getString(R.string.install_status_bootstrapping)));
13091308

1310-
File resolvConf = new File(debianRootfs, "etc/resolv.conf");
1311-
if (resolvConf.exists()) resolvConf.delete();
1312-
java.io.FileOutputStream fos = new java.io.FileOutputStream(resolvConf);
1313-
fos.write("nameserver 1.1.1.1\nnameserver 8.8.8.8\n".getBytes());
1314-
fos.close();
1315-
1316-
File hostsFile = new File(debianRootfs, "etc/hosts");
1317-
java.io.FileOutputStream fosH = new java.io.FileOutputStream(hostsFile);
1318-
fosH.write("127.0.0.1 localhost\n".getBytes());
1319-
fosH.close();
1309+
// DNS is written at the chokepoint (PRootEngine) before the
1310+
// bootstrap proot run below; no inline write needed here.
13201311

13211312
if (prootEngine == null)
13221313
prootEngine = new PRootEngine();
@@ -2147,8 +2138,6 @@ private void refreshRestoreButtonLogic() {
21472138
tarExtractor.startExtraction(requireContext(), backupFile.getAbsolutePath(), iiabRootDir.getAbsolutePath(), new TarExtractor.ExtractionListener() {
21482139
@Override
21492140
public void onComplete(String destDir) {
2150-
// Restored artifact ships without resolv.conf; the app owns runtime DNS.
2151-
ensureRuntimeNetworkConfig(new File(iiabRootDir, "installed-rootfs/iiab"));
21522141
mainAct.runOnUiThread(() -> {
21532142
isRestoring = false;
21542143
disableSystemProtection();
@@ -2175,33 +2164,6 @@ public void onError(String error) {
21752164
}
21762165
}
21772166

2178-
// Runtime network config for the proot guest: proot has no resolver daemon, and our build
2179-
// artifact ships WITHOUT /etc/resolv.conf on purpose (the app is the single owner of runtime
2180-
// network state). Mirrors the reset path. Called from fast-install and restore.
2181-
// TODO [tech-debt]: imperative side-effect inside DeployFragment (god-object); not Clean
2182-
// Architecture. Fold into the rootfs domain/data slice during the strangler refactor.
2183-
private void ensureRuntimeNetworkConfig(File debianRootfs) {
2184-
try {
2185-
File etc = new File(debianRootfs, "etc");
2186-
if (!etc.isDirectory()) {
2187-
Log.w(TAG, "ensureRuntimeNetworkConfig: missing " + etc.getAbsolutePath() + " - skipping DNS write");
2188-
return;
2189-
}
2190-
File resolvConf = new File(etc, "resolv.conf");
2191-
if (resolvConf.exists()) resolvConf.delete();
2192-
try (java.io.FileOutputStream fos = new java.io.FileOutputStream(resolvConf)) {
2193-
fos.write("nameserver 1.1.1.1\nnameserver 8.8.8.8\n".getBytes());
2194-
}
2195-
File hostsFile = new File(etc, "hosts");
2196-
try (java.io.FileOutputStream fosH = new java.io.FileOutputStream(hostsFile)) {
2197-
fosH.write("127.0.0.1 localhost\n".getBytes());
2198-
}
2199-
Log.i(TAG, "ensureRuntimeNetworkConfig: wrote resolv.conf + hosts under " + etc.getAbsolutePath());
2200-
} catch (Exception e) {
2201-
Log.w(TAG, "ensureRuntimeNetworkConfig failed", e);
2202-
}
2203-
}
2204-
22052167
private void importBackupSafely(Uri sourceUri) {
22062168
isImporting = true;
22072169
updateDynamicButtons();

‎controller/app/src/main/java/org/iiab/controller/PRootEngine.java‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@
2020
import java.util.ArrayList;
2121
import java.util.List;
2222

23+
import org.iiab.controller.network.data.FileResolvConfWriter;
24+
import org.iiab.controller.network.data.PrefsDnsConfigRepository;
25+
import org.iiab.controller.network.domain.ApplyDnsUseCase;
26+
2327
public class PRootEngine {
2428
private static final String TAG = "IIAB-PRootEngine";
2529
private Process currentProcess;
@@ -44,6 +48,19 @@ public void executeInContainer(Context context, String rootfsDir, String command
4448
throw new Exception("libproot.so not found in native library directory!");
4549
}
4650

51+
// Single DNS injection point: every proot launch passes through here, so we write
52+
// the effective DNS (user's custom config when enabled, else defaults) into the
53+
// guest's resolv.conf BEFORE launching. Replaces the scattered writes that used to
54+
// live in DeployFragment / MainActivity.
55+
try {
56+
new ApplyDnsUseCase(
57+
new PrefsDnsConfigRepository(context),
58+
new FileResolvConfWriter()
59+
).execute(new File(rootfsDir));
60+
} catch (Exception dnsEx) {
61+
Log.w(TAG, "DNS apply skipped: " + dnsEx.getMessage());
62+
}
63+
4764
// =========================================================
4865
// THE W^X TROJAN HORSE: FAKE TERMUX PREFIX
4966
// =========================================================
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* ============================================================================
3+
* Name : FileResolvConfWriter.java
4+
* Author : AppDevForAll
5+
* Copyright : Copyright (c) 2026 AppDevForAll
6+
* Description : Writes resolv.conf (+ minimal hosts) into a rootfs; migrates the #25 helper logic.
7+
* ============================================================================
8+
*/
9+
package org.iiab.controller.network.data;
10+
11+
import android.util.Log;
12+
13+
import org.iiab.controller.network.domain.DnsConfig;
14+
import org.iiab.controller.network.domain.ResolvConfWriter;
15+
16+
import java.io.File;
17+
import java.io.FileOutputStream;
18+
import java.nio.charset.StandardCharsets;
19+
20+
/**
21+
* {@link ResolvConfWriter} that writes {@code etc/resolv.conf} (and a minimal
22+
* {@code etc/hosts} if absent) into a guest rootfs. This is the single home for the
23+
* DNS-write logic previously duplicated in {@code DeployFragment}/{@code MainActivity}.
24+
* Guards on {@code etc/} existence and never throws.
25+
*/
26+
public final class FileResolvConfWriter implements ResolvConfWriter {
27+
28+
private static final String TAG = "IIAB-DNS";
29+
30+
@Override
31+
public boolean write(DnsConfig config, File rootfsDir) {
32+
try {
33+
File etc = new File(rootfsDir, "etc");
34+
if (!etc.isDirectory()) {
35+
Log.w(TAG, "resolv.conf skipped: missing " + etc.getAbsolutePath());
36+
return false;
37+
}
38+
File resolv = new File(etc, "resolv.conf");
39+
if (resolv.exists() && !resolv.delete()) {
40+
Log.w(TAG, "could not remove old resolv.conf at " + resolv.getAbsolutePath());
41+
}
42+
try (FileOutputStream fos = new FileOutputStream(resolv)) {
43+
fos.write(config.toResolvConf().getBytes(StandardCharsets.UTF_8));
44+
}
45+
File hosts = new File(etc, "hosts");
46+
if (!hosts.exists()) {
47+
try (FileOutputStream fos = new FileOutputStream(hosts)) {
48+
fos.write("127.0.0.1 localhost\n".getBytes(StandardCharsets.UTF_8));
49+
}
50+
}
51+
Log.i(TAG, "wrote resolv.conf (" + config + ") under " + etc.getAbsolutePath());
52+
return true;
53+
} catch (Exception e) {
54+
Log.w(TAG, "FileResolvConfWriter.write failed", e);
55+
return false;
56+
}
57+
}
58+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* ============================================================================
3+
* Name : PrefsDnsConfigRepository.java
4+
* Author : AppDevForAll
5+
* Copyright : Copyright (c) 2026 AppDevForAll
6+
* Description : SharedPreferences-backed DnsConfigRepository (new keys; not the vestigial VPN ones).
7+
* ============================================================================
8+
*/
9+
package org.iiab.controller.network.data;
10+
11+
import android.content.Context;
12+
import android.content.SharedPreferences;
13+
14+
import org.iiab.controller.network.domain.DnsConfig;
15+
import org.iiab.controller.network.domain.DnsConfigRepository;
16+
17+
/**
18+
* {@link DnsConfigRepository} backed by a dedicated SharedPreferences file. Uses
19+
* fresh keys (NOT the vestigial VPN-era {@code DnsIpv4}/{@code DnsIpv6} in
20+
* {@code Preferences}, which this feature will retire). {@code MODE_MULTI_PROCESS}
21+
* matches the rest of the app.
22+
*/
23+
public final class PrefsDnsConfigRepository implements DnsConfigRepository {
24+
25+
private static final String PREFS = "NetworkDnsPrefs";
26+
private static final String K_CUSTOM = "dns.custom.enabled";
27+
private static final String K_PRIMARY = "dns.primary";
28+
private static final String K_SECONDARY = "dns.secondary";
29+
30+
private final SharedPreferences prefs;
31+
32+
public PrefsDnsConfigRepository(Context context) {
33+
this.prefs = context.getSharedPreferences(PREFS, Context.MODE_MULTI_PROCESS);
34+
}
35+
36+
@Override
37+
public DnsConfig loadEffective() {
38+
return isCustomEnabled() ? loadCustom() : DnsConfig.defaults();
39+
}
40+
41+
@Override
42+
public DnsConfig loadCustom() {
43+
DnsConfig d = DnsConfig.defaults();
44+
String primary = prefs.getString(K_PRIMARY, d.primary());
45+
String secondary = prefs.getString(K_SECONDARY, d.secondary());
46+
return new DnsConfig(primary, secondary);
47+
}
48+
49+
@Override
50+
public boolean isCustomEnabled() {
51+
return prefs.getBoolean(K_CUSTOM, false);
52+
}
53+
54+
@Override
55+
public void saveCustom(DnsConfig config) {
56+
prefs.edit()
57+
.putBoolean(K_CUSTOM, true)
58+
.putString(K_PRIMARY, config.primary())
59+
.putString(K_SECONDARY, config.secondary())
60+
.commit();
61+
}
62+
63+
@Override
64+
public void disableCustom() {
65+
prefs.edit().putBoolean(K_CUSTOM, false).commit();
66+
}
67+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* ============================================================================
3+
* Name : ApplyDnsUseCase.java
4+
* Author : AppDevForAll
5+
* Copyright : Copyright (c) 2026 AppDevForAll
6+
* Description : Use case: write the effective DNS config into a rootfs (boot-time apply).
7+
* ============================================================================
8+
*/
9+
package org.iiab.controller.network.domain;
10+
11+
import java.io.File;
12+
13+
/**
14+
* Writes the effective DNS config (custom or defaults) into a guest rootfs. Called
15+
* at server boot (and wherever a rootfs is about to run network operations) so the
16+
* proot guest always has a working {@code resolv.conf} without a resolver daemon.
17+
*/
18+
public final class ApplyDnsUseCase {
19+
20+
private final DnsConfigRepository repository;
21+
private final ResolvConfWriter writer;
22+
23+
public ApplyDnsUseCase(DnsConfigRepository repository, ResolvConfWriter writer) {
24+
this.repository = repository;
25+
this.writer = writer;
26+
}
27+
28+
/** @return {@code true} if resolv.conf was written into {@code rootfsDir}. */
29+
public boolean execute(File rootfsDir) {
30+
return writer.write(repository.loadEffective(), rootfsDir);
31+
}
32+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* ============================================================================
3+
* Name : DnsConfig.java
4+
* Author : AppDevForAll
5+
* Copyright : Copyright (c) 2026 AppDevForAll
6+
* Description : DNS configuration: two mixed slots (primary/secondary), each one IPv4 or IPv6.
7+
* ============================================================================
8+
*/
9+
package org.iiab.controller.network.domain;
10+
11+
/**
12+
* Immutable DNS configuration: a {@code primary} and an optional {@code secondary}
13+
* nameserver. Each slot holds a SINGLE address that may be IPv4 <em>or</em> IPv6
14+
* (mixed allowed in any order); no comma-separated lists. This keeps the model and
15+
* validation simple while covering every combination the user might want.
16+
*
17+
* <p>Pure domain value object (no Android, no I/O). {@link #defaults()} is the
18+
* preconfigured set the app ships with, so the user never has to configure anything.
19+
*/
20+
public final class DnsConfig {
21+
22+
private final String primary;
23+
private final String secondary;
24+
25+
public DnsConfig(String primary, String secondary) {
26+
this.primary = norm(primary);
27+
this.secondary = norm(secondary);
28+
}
29+
30+
/** Preconfigured defaults: Cloudflare (primary) + Google (secondary), both IPv4. */
31+
public static DnsConfig defaults() {
32+
return new DnsConfig("1.1.1.1", "8.8.8.8");
33+
}
34+
35+
/** The primary nameserver (IPv4 or IPv6); "" if unset. */
36+
public String primary() { return primary; }
37+
38+
/** The secondary nameserver (IPv4 or IPv6); "" if unset. */
39+
public String secondary() { return secondary; }
40+
41+
public boolean hasSecondary() { return !secondary.isEmpty(); }
42+
43+
public boolean isEmpty() { return primary.isEmpty() && secondary.isEmpty(); }
44+
45+
/** Body for {@code /etc/resolv.conf}: one {@code nameserver X} line per non-empty slot. */
46+
public String toResolvConf() {
47+
StringBuilder sb = new StringBuilder();
48+
if (!primary.isEmpty()) sb.append("nameserver ").append(primary).append("\n");
49+
if (!secondary.isEmpty()) sb.append("nameserver ").append(secondary).append("\n");
50+
return sb.toString();
51+
}
52+
53+
private static String norm(String s) { return s == null ? "" : s.trim(); }
54+
55+
@Override public boolean equals(Object o) {
56+
if (this == o) return true;
57+
if (!(o instanceof DnsConfig)) return false;
58+
DnsConfig d = (DnsConfig) o;
59+
return primary.equals(d.primary) && secondary.equals(d.secondary);
60+
}
61+
62+
@Override public int hashCode() { return 31 * primary.hashCode() + secondary.hashCode(); }
63+
64+
@Override public String toString() { return "DnsConfig{primary=" + primary + ", secondary=" + secondary + "}"; }
65+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* ============================================================================
3+
* Name : DnsConfigRepository.java
4+
* Author : AppDevForAll
5+
* Copyright : Copyright (c) 2026 AppDevForAll
6+
* Description : Domain port: persistence of the user's DNS choice.
7+
* ============================================================================
8+
*/
9+
package org.iiab.controller.network.domain;
10+
11+
/**
12+
* Domain port for persisting the user's DNS choice. The Data layer backs this with
13+
* SharedPreferences. Semantics support the "Setup DNS" checkbox:
14+
* <ul>
15+
* <li>custom disabled (default) &rarr; effective config is {@link DnsConfig#defaults()};</li>
16+
* <li>custom enabled &rarr; effective config is the saved custom config.</li>
17+
* </ul>
18+
*/
19+
public interface DnsConfigRepository {
20+
21+
/** Config to actually apply: saved custom when enabled, otherwise {@link DnsConfig#defaults()}. */
22+
DnsConfig loadEffective();
23+
24+
/** The saved custom config (or {@link DnsConfig#defaults()} if none saved yet). */
25+
DnsConfig loadCustom();
26+
27+
/** Whether the user enabled custom DNS (the "Setup DNS" check). */
28+
boolean isCustomEnabled();
29+
30+
/** Persist {@code config} as the custom config and mark custom DNS enabled. */
31+
void saveCustom(DnsConfig config);
32+
33+
/** Turn custom DNS off (revert to defaults). Used on test failure / unchecking. */
34+
void disableCustom();
35+
}

0 commit comments

Comments
 (0)