Refactor scanner presets, and add iran lion and sun flag

This commit is contained in:
Sarto
2026-04-15 15:04:11 +03:30
parent 7b65d605b8
commit 4111d5115a
12 changed files with 783 additions and 56 deletions
+8 -1
View File
@@ -42,6 +42,9 @@ jobs:
goarch: amd64
- goos: android
goarch: arm64
- goos: android
goarch: arm
goarm: '7'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
@@ -71,6 +74,7 @@ jobs:
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
GOARM: ${{ matrix.goarm || '' }}
CGO_ENABLED: '0'
run: |
VERSION=${GITHUB_REF_NAME:-dev}
@@ -111,7 +115,11 @@ jobs:
- name: Stage Android client binary as JNI library
run: |
mkdir -p android/app/src/main/jniLibs/arm64-v8a
mkdir -p android/app/src/main/jniLibs/armeabi-v7a
test -f artifacts/thefeed-client-android-arm64
test -f artifacts/thefeed-client-android-arm
cp artifacts/thefeed-client-android-arm64 android/app/src/main/jniLibs/arm64-v8a/libthefeed.so
cp artifacts/thefeed-client-android-arm android/app/src/main/jniLibs/armeabi-v7a/libthefeed.so
- name: Decode signing keystore
env:
@@ -143,7 +151,6 @@ jobs:
gradle wrapper --gradle-version 8.10.2
if [ -f app/keystore.jks ]; then BT=release; TASK=assembleRelease; else BT=debug; TASK=assembleDebug; fi
cp ../artifacts/thefeed-client-android-arm64 app/src/main/jniLibs/arm64-v8a/libthefeed.so
./gradlew --no-daemon clean $TASK
APK=$(find app/build/outputs/apk/$BT -name "*.apk" | head -1)
cp "$APK" ../artifacts/thefeed-android-arm64.apk
+5 -1
View File
@@ -44,7 +44,7 @@ clean:
rm -rf $(BUILD_DIR)
# Cross-compilation targets
build-all: build-linux-amd64 build-linux-arm64 build-darwin-amd64 build-darwin-arm64 build-freebsd-amd64 build-freebsd-arm64 build-windows-amd64 build-android-arm64
build-all: build-linux-amd64 build-linux-arm64 build-darwin-amd64 build-darwin-arm64 build-freebsd-amd64 build-freebsd-arm64 build-windows-amd64 build-android-arm64 build-android-arm
build-linux-amd64:
@mkdir -p $(BUILD_DIR)
@@ -85,6 +85,10 @@ build-android-arm64:
@mkdir -p $(BUILD_DIR)
GOOS=android GOARCH=arm64 go build $(GOFLAGS) -o $(BUILD_DIR)/$(BINARY_CLIENT)-android-arm64 ./cmd/client
build-android-arm:
@mkdir -p $(BUILD_DIR)
CGO_ENABLED=0 GOOS=android GOARCH=arm GOARM=7 go build $(GOFLAGS) -o $(BUILD_DIR)/$(BINARY_CLIENT)-android-arm ./cmd/client
# UPX compression (requires upx in PATH) — only for Linux/Windows binaries
upx:
@command -v upx >/dev/null 2>&1 || { echo "upx not found, skipping compression"; exit 0; }
+29 -16
View File
@@ -116,6 +116,34 @@ sudo bash install.sh --login
sudo bash install.sh --uninstall
```
> **توجه:** سرور باید روی پورت ۵۳ پاسخ بدهد. بهتر است روی پورت غیرمحدود (`:5300`) اجرا و با iptables فوروارد کنید:
>
> نام اینترفیس شبکه خود را با `ip a` پیدا کنید و `eth0` را جایگزین کنید:
> ```bash
> sudo iptables -I INPUT -p udp --dport 5300 -j ACCEPT
> sudo iptables -t nat -I PREROUTING -i eth0 -p udp --dport 53 -j REDIRECT --to-ports 5300
> sudo ip6tables -I INPUT -p udp --dport 5300 -j ACCEPT
> sudo ip6tables -t nat -I PREROUTING -i eth0 -p udp --dport 53 -j REDIRECT --to-ports 5300
> ```
>
> برای ماندگار کردن این قوانین بعد از ریبوت:
> ```bash
> sudo apt install iptables-persistent # Debian/Ubuntu
> sudo netfilter-persistent save
> ```
**اگر مشکلی پیش آمد — حذف فوری redirect:**
```bash
# حذف قانون iptables (بازگشت به حالت اولیه)
sudo iptables -t nat -D PREROUTING -i eth0 -p udp --dport 53 -j REDIRECT --to-ports 5300
sudo iptables -D INPUT -p udp --dport 5300 -j ACCEPT
sudo netfilter-persistent save
```
## 🐳 نصب با Docker (سرور)
اجرای سرور با Docker — بدون نیاز به نصب Go.
@@ -275,21 +303,6 @@ chmod +x thefeed-client
| A | `ns.example.com` | `203.0.113.10` |
| NS | `t.example.com` | `ns.example.com` |
> **توجه:** سرور باید روی پورت ۵۳ پاسخ بدهد. بهتر است روی پورت غیرمحدود (`:5300`) اجرا و با iptables فوروارد کنید:
>
> نام اینترفیس شبکه خود را با `ip a` پیدا کنید و `eth0` را جایگزین کنید:
> ```bash
> sudo iptables -I INPUT -p udp --dport 5300 -j ACCEPT
> sudo iptables -t nat -I PREROUTING -i eth0 -p udp --dport 53 -j REDIRECT --to-ports 5300
> sudo ip6tables -I INPUT -p udp --dport 5300 -j ACCEPT
> sudo ip6tables -t nat -I PREROUTING -i eth0 -p udp --dport 53 -j REDIRECT --to-ports 5300
> ```
>
> برای ماندگار کردن این قوانین بعد از ریبوت:
> ```bash
> sudo apt install iptables-persistent # Debian/Ubuntu
> sudo netfilter-persistent save
> ```
## 🛠️ ساخت از سورس
@@ -364,7 +377,7 @@ MIT
<div align="center">
**برای ایران آزاد 🇮🇷**
**برای ایران آزاد** <img src="internal/web/static/iran-lion-sun.svg" alt="شیر و خورشید" height="20">
*هر ایرانی حق دسترسی آزاد به اطلاعات را دارد*
+29 -16
View File
@@ -82,6 +82,34 @@ sudo bash -c "$(curl -Ls https://raw.githubusercontent.com/sartoopjj/thefeed/mai
Re-login: `curl -Ls https://raw.githubusercontent.com/sartoopjj/thefeed/main/scripts/install.sh | sudo bash -s -- --login`
Uninstall: `curl -Ls https://raw.githubusercontent.com/sartoopjj/thefeed/main/scripts/install.sh | sudo bash -s -- --uninstall`
> **Note:** The server needs to receive packets on external port 53. Running on `:53` directly requires root. It's better to listen on an unprivileged port (`:5300`) and port-forward 53 to it.
>
> Replace `eth0` with your actual network interface name (check with `ip a`):
> ```bash
> sudo iptables -I INPUT -p udp --dport 5300 -j ACCEPT
> sudo iptables -t nat -I PREROUTING -i eth0 -p udp --dport 53 -j REDIRECT --to-ports 5300
> sudo ip6tables -I INPUT -p udp --dport 5300 -j ACCEPT
> sudo ip6tables -t nat -I PREROUTING -i eth0 -p udp --dport 53 -j REDIRECT --to-ports 5300
> ```
>
> To make these rules persistent across reboots:
> ```bash
> sudo apt install iptables-persistent # Debian/Ubuntu
> sudo netfilter-persistent save
> ```
**If something goes wrong — remove the redirect instantly:**
```bash
# Remove the iptables rule (restores original behavior)
sudo iptables -t nat -D PREROUTING -i eth0 -p udp --dport 53 -j REDIRECT --to-ports 5300
sudo iptables -D INPUT -p udp --dport 5300 -j ACCEPT
sudo netfilter-persistent save
```
## Docker Deployment (Server)
Run the server with Docker — no Go toolchain needed.
@@ -434,21 +462,6 @@ This points a hostname to your server IP.
This delegates all DNS queries for `t.example.com` (and its subdomains) to your server.
> **Note:** The server needs to receive packets on external port 53. Running on `:53` directly requires root. It's better to listen on an unprivileged port (`:5300`) and port-forward 53 to it.
>
> Replace `eth0` with your actual network interface name (check with `ip a`):
> ```bash
> sudo iptables -I INPUT -p udp --dport 5300 -j ACCEPT
> sudo iptables -t nat -I PREROUTING -i eth0 -p udp --dport 53 -j REDIRECT --to-ports 5300
> sudo ip6tables -I INPUT -p udp --dport 5300 -j ACCEPT
> sudo ip6tables -t nat -I PREROUTING -i eth0 -p udp --dport 53 -j REDIRECT --to-ports 5300
> ```
>
> To make these rules persistent across reboots:
> ```bash
> sudo apt install iptables-persistent # Debian/Ubuntu
> sudo netfilter-persistent save
> ```
## channels.txt Format
@@ -521,7 +534,7 @@ MIT
<div align="center">
**For FREE IRAN 🇮🇷**
**For FREE IRAN** <img src="internal/web/static/iran-lion-sun.svg" alt="Lion-and-Sun" height="20">
*Everyone deserves free access to information*
@@ -29,6 +29,7 @@ import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import java.net.HttpURLConnection
import java.net.URL
@@ -53,6 +54,10 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
// Let the app draw behind the system status bar
WindowCompat.setDecorFitsSystemWindows(window, false)
// Force light (white) status bar icons on dark background
val controller = WindowInsetsControllerCompat(window, window.decorView)
controller.isAppearanceLightStatusBars = false
controller.isAppearanceLightNavigationBars = false
setContentView(R.layout.activity_main)
// Apply top inset as padding so content isn't hidden behind the status bar
@@ -6,5 +6,6 @@
<item name="android:statusBarColor" tools:targetApi="l">@color/bg</item>
<item name="android:navigationBarColor">@color/bg</item>
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
<item name="android:windowBackground">@color/bg</item>
</style>
</resources>
+12 -6
View File
@@ -178,20 +178,26 @@ func (f *Fetcher) SetResolvers(resolvers []string) {
copy(f.activeResolvers, resolvers)
}
// UpdateResolverPool replaces the full resolver list but keeps the existing
// active pool intact (only pruning resolvers that are no longer in the bank).
// New bank entries are added to allResolvers but NOT automatically activated.
// UpdateResolverPool replaces the full resolver list and removes any active
// resolvers that are no longer in the bank.
func (f *Fetcher) UpdateResolverPool(resolvers []string) {
f.mu.Lock()
defer f.mu.Unlock()
bankSet := make(map[string]bool, len(resolvers))
for _, r := range resolvers {
bankSet[r] = true
k := r
if !strings.Contains(k, ":") {
k += ":53"
}
bankSet[k] = true
}
// Prune active resolvers that were removed from the bank.
filtered := make([]string, 0, len(f.activeResolvers))
for _, r := range f.activeResolvers {
if bankSet[r] {
k := r
if !strings.Contains(k, ":") {
k += ":53"
}
if bankSet[k] {
filtered = append(filtered, r)
}
}
+26 -3
View File
@@ -14,15 +14,32 @@ func (s *Server) handleScannerPresets(w http.ResponseWriter, r *http.Request) {
http.Error(w, "method not allowed", 405)
return
}
src := defaultScannerPresets
type preset struct {
Name string `json:"name"`
Label string `json:"label"`
Count int `json:"count"`
}
writeJSON(w, map[string]any{
"presets": []preset{
{Name: "ir", Label: "Iran", Count: parseScannerPresetCount()},
},
})
}
// parseScannerPresetLines returns the parsed non-empty, non-comment lines from the preset.
func parseScannerPresetLines() []string {
var lines []string
for _, line := range strings.Split(src, "\n") {
for _, line := range strings.Split(defaultScannerPresets, "\n") {
line = strings.TrimSpace(line)
if line != "" && !strings.HasPrefix(line, "#") {
lines = append(lines, line)
}
}
writeJSON(w, lines)
return lines
}
func parseScannerPresetCount() int {
return len(parseScannerPresetLines())
}
func (s *Server) handleScannerStart(w http.ResponseWriter, r *http.Request) {
@@ -33,6 +50,7 @@ func (s *Server) handleScannerStart(w http.ResponseWriter, r *http.Request) {
var req struct {
Targets []string `json:"targets"`
Preset string `json:"preset"` // e.g. "ir" — server-side preset, avoids sending 50K IPs
MaxIPs int `json:"maxIPs"`
RateLimit int `json:"rateLimit"`
Timeout float64 `json:"timeout"`
@@ -45,6 +63,11 @@ func (s *Server) handleScannerStart(w http.ResponseWriter, r *http.Request) {
return
}
// Resolve preset into targets server-side.
if req.Preset == "ir" && len(req.Targets) == 0 {
req.Targets = parseScannerPresetLines()
}
if len(req.Targets) == 0 {
http.Error(w, "targets required", 400)
return
+31 -10
View File
@@ -1762,10 +1762,11 @@
<label data-i18n="scanner_targets">IPs or CIDRs (one per line)</label>
<div style="display:flex;gap:4px">
<button class="btn btn-flat" onclick="document.getElementById('scanTargets').value=''" data-i18n="scanner_clear_targets" style="font-size:12px;padding:4px 10px">&#128465; Clear</button>
<button class="btn btn-flat" onclick="loadScannerPresets()" data-i18n="scanner_load_presets" style="font-size:12px;padding:4px 10px">🇮🇷 IR</button>
<button class="btn btn-flat" onclick="loadScannerPresets()" style="font-size:12px;padding:4px 10px"><img class="iran-flag-icon" src="/static/iran-lion-sun.svg" alt="IR" style="height:14px;vertical-align:middle;margin-right:2px"> <span data-i18n="scanner_load_presets">Load IR Presets</span></button>
</div>
</div>
<textarea id="scanTargets" rows="3" placeholder="5.1.0.0/16&#10;8.8.8.8&#10;1.1.1.1" style="width:100%;font-family:monospace;font-size:13px"></textarea>
<div id="scanPresetTag" style="display:none;margin-top:6px;padding:6px 10px;background:var(--surface2);border:1px solid var(--border);border-radius:6px;font-size:13px;color:var(--text)"></div>
</div>
<div class="form-group">
<label data-i18n="scanner_profile">Profile</label>
@@ -1939,7 +1940,8 @@
scanner_about_short: 'بازه‌های IP را اسکن کنید تا ریزالورهای DNS سازگار با سرور شما پیدا شوند.',
scanner_read_more: 'بیشتر بخوانید...',
scanner_read_less: 'بستن',
scanner_load_presets: '\uD83C\uDDEE\uD83C\uDDF7 بارگذاری لیست ایران',
scanner_load_presets: 'بارگذاری لیست ایران',
scanner_preset_active: 'ریزالورهای ایران بارگذاری شد',
scanner_new_scan: 'اسکن جدید',
scanner_advanced: 'تنظیمات پیشرفته',
scanner_copy_all: 'کپی همه',
@@ -2070,7 +2072,8 @@
scanner_about_short: 'Scan IP ranges to find DNS resolvers that work with your server.',
scanner_read_more: 'Read more...',
scanner_read_less: 'Show less',
scanner_load_presets: '\uD83C\uDDEE\uD83C\uDDF7 Load IR Presets',
scanner_load_presets: 'Load IR Presets',
scanner_preset_active: 'Iran resolvers loaded',
scanner_new_scan: 'New Scan',
scanner_advanced: 'Advanced options',
scanner_copy_all: 'Copy All',
@@ -2961,7 +2964,7 @@
var timeStr = ts.toLocaleTimeString(dateLocale, { hour: '2-digit', minute: '2-digit' });
var text = msg.Text || msg.text || '';
currentMsgTexts.push(text);
var mediaHtml = '', textHtml = esc(text);
var mediaHtml = '', textHtml = esc(text).replace(/\uD83C\uDDEE\uD83C\uDDF7/g, '<img src="/static/iran-lion-sun.svg" alt="\u{1F981}\u2600\uFE0F" style="height:1.1em;vertical-align:middle">');
var mediaTypes = ['[IMAGE]', '[VIDEO]', '[FILE]', '[AUDIO]', '[STICKER]', '[GIF]', '[POLL]', '[CONTACT]', '[LOCATION]', '[REPLY]'];
for (var m = 0; m < mediaTypes.length; m++) {
if (text.indexOf(mediaTypes[m]) === 0) {
@@ -3291,6 +3294,7 @@
// ===== SCANNER =====
var scanPollTimer = null;
var scanLastResults = []; // cache for selection
var scannerActivePreset = ''; // server-side preset name (e.g. 'ir')
function openScanner() {
document.getElementById('scannerModal').classList.add('active');
@@ -3318,27 +3322,44 @@
}
async function loadScannerPresets() {
if (scannerActivePreset === 'ir') {
// Toggle off
scannerActivePreset = '';
renderPresetTag();
return;
}
try {
var r = await fetch('/api/scanner/presets');
if (!r.ok) return;
var lines = await r.json();
if (lines && lines.length) {
var el = document.getElementById('scanTargets');
var existing = el.value.trim();
el.value = existing ? existing + '\n' + lines.join('\n') : lines.join('\n');
var data = await r.json();
var presets = data.presets || [];
if (presets.length > 0) {
scannerActivePreset = presets[0].name;
renderPresetTag();
}
} catch (e) { showToast(e.message) }
}
function renderPresetTag() {
var tag = document.getElementById('scanPresetTag');
if (scannerActivePreset) {
tag.style.display = '';
tag.innerHTML = '<img src="/static/iran-lion-sun.svg" alt="IR" style="height:14px;vertical-align:middle;margin-right:4px"> ' + t('scanner_preset_active');
} else {
tag.style.display = 'none';
tag.textContent = '';
}
}
async function startScan() {
var targets = document.getElementById('scanTargets').value.trim().split('\n').filter(function (s) { return s.trim() });
if (!targets.length) { showToast(t('scanner_targets')); return }
if (!targets.length && !scannerActivePreset) { showToast(t('scanner_targets')); return }
// Clear stale results from previous scan.
scanLastResults = [];
document.getElementById('scanResultsBody').innerHTML = '';
document.getElementById('scannerApplySection').style.display = 'none';
var body = {
targets: targets,
preset: scannerActivePreset || undefined,
profileId: document.getElementById('scanProfile').value,
rateLimit: parseInt(document.getElementById('scanRateLimit').value) || 50,
timeout: parseInt(document.getElementById('scanTimeout').value) || 15,
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 40 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 40 KiB

+15 -3
View File
@@ -165,11 +165,23 @@ func TestE2E_Scanner_Presets(t *testing.T) {
t.Fatalf("GET /api/scanner/presets: expected 200, got %d", resp.StatusCode)
}
defer resp.Body.Close()
var lines []string
if err := json.NewDecoder(resp.Body).Decode(&lines); err != nil {
var data struct {
Presets []struct {
Name string `json:"name"`
Label string `json:"label"`
Count int `json:"count"`
} `json:"presets"`
}
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
t.Fatalf("decode: %v", err)
}
if len(lines) == 0 {
if len(data.Presets) == 0 {
t.Error("expected non-empty presets list")
}
if data.Presets[0].Name != "ir" {
t.Errorf("first preset name = %q, want ir", data.Presets[0].Name)
}
if data.Presets[0].Count == 0 {
t.Error("expected non-zero count for ir preset")
}
}