Przeglądaj źródła

webUI: update code.

Nick Peng 7 miesięcy temu
rodzic
commit
d61bbb5177

+ 26 - 2
Makefile

@@ -17,16 +17,34 @@ PKG_CONFIG := pkg-config
 DESTDIR :=
 PREFIX := /usr
 SBINDIR := $(PREFIX)/sbin
+SLIBDIR := $(PREFIX)/lib
 SYSCONFDIR := /etc
 RUNSTATEDIR := /run
 SYSTEMDSYSTEMUNITDIR := $(shell ${PKG_CONFIG} --variable=systemdsystemunitdir systemd)
 SMARTDNS_SYSTEMD = systemd/smartdns.service
 
-.PHONY: all clean install SMARTDNS_BIN
+ifneq ($(strip $(DESTDIR)),)
+$(shell mkdir -p $(DESTDIR) -m 0755)
+override DESTDIR := $(realpath $(DESTDIR))
+endif
+
+PLUGINS := 
+WITH_UI ?= 0
+
+ifeq ($(WITH_UI), 1)
+PLUGINS += plugin/smartdns-ui
+endif
+
+define PLUGINS_TARGETS
+    $(foreach plugin,$(PLUGINS),$(MAKE) $(MFLAGS) DESTDIR=$(DESTDIR) -C $(plugin) $(1);)
+endef
+
+.PHONY: all clean install help SMARTDNS_BIN 
 all: SMARTDNS_BIN 
 
 SMARTDNS_BIN: $(SMARTDNS_SYSTEMD)
-	$(MAKE) $(MFLAGS) -C src all 
+	$(MAKE) $(MFLAGS) -C src all
+	$(call PLUGINS_TARGETS, all)
 
 $(SMARTDNS_SYSTEMD): systemd/smartdns.service.in
 	cp $< $@
@@ -34,9 +52,14 @@ $(SMARTDNS_SYSTEMD): systemd/smartdns.service.in
 	sed -i 's|@SYSCONFDIR@|$(SYSCONFDIR)|' $@
 	sed -i 's|@RUNSTATEDIR@|$(RUNSTATEDIR)|' $@
 
+help:
+	@echo "Options:"
+	@echo "  WITH_UI=1: Build with smartdns-ui plugin"
+
 clean:
 	$(MAKE) $(MFLAGS) -C src clean  
 	$(RM) $(SMARTDNS_SYSTEMD)
+	$(call PLUGINS_TARGETS, clean)
 
 install: SMARTDNS_BIN 
 	install -v -m 0640 -D -t $(DESTDIR)$(SYSCONFDIR)/default etc/default/smartdns
@@ -44,4 +67,5 @@ install: SMARTDNS_BIN
 	install -v -m 0640 -D -t $(DESTDIR)$(SYSCONFDIR)/smartdns etc/smartdns/smartdns.conf
 	install -v -m 0755 -D -t $(DESTDIR)$(SBINDIR) src/smartdns
 	install -v -m 0644 -D -t $(DESTDIR)$(SYSTEMDSYSTEMUNITDIR) systemd/smartdns.service
+	$(call PLUGINS_TARGETS, install)
 

+ 1 - 1
ReadMe.md

@@ -72,7 +72,7 @@ PING 14.215.177.39 (14.215.177.39) 56(84) bytes of data.
 rtt min/avg/max/mdev = 5.954/6.133/6.313/0.195 ms
 ```
 
-从对比看出,SmartDNS 找到了访问 www.baidu.com 最快的 IP 地址,比阿里 DNS 速度快了 5 倍。
+从对比看出,SmartDNS 找到了访问 `www.baidu.com` 最快的 IP 地址,比阿里 DNS 速度快了 5 倍。
 
 ## 特性
 

+ 10 - 1
etc/smartdns/smartdns.conf

@@ -406,4 +406,13 @@ log-level info
 
 # load plugin
 # plugin [path/to/file] [args]
-# plugin /usr/lib/smartdns/libsmartdns-ui.so --p 8080 -i 0.0.0.0 -r /usr/share/smartdns/wwwroot
+# plugin /usr/libsmartdns-ui.so --p 8080 -i 0.0.0.0 -r /usr/share/smartdns/wwwroot
+# smartdns-ui.www-root /usr/share/smartdns/wwwroot
+# smartdns-ui.ip http://0.0.0.0:6080
+# smartdns-ui.token-expire 3600000000
+# smartdns-ui.token-secret 123456
+# smartdns-ui.enable-terminal yes
+# smartdns-ui.enable-cors yes
+# smartdns-ui.user admin
+# smartdns-ui.password password
+

+ 18 - 18
plugin/smartdns-ui/Cargo.toml

@@ -7,27 +7,27 @@ edition = "2021"
 crate-type = ["cdylib", "lib"]
 
 [dependencies]
-ctor = "0.2.8"
-bytes = "1.6.1"
-rusqlite = { version = "0.32.0", features = ["bundled"] }
-hyper = { version = "1.4.1", features = ["full"] }
-hyper-util = { version = "0.1.6", features = ["full"] }
+ctor = "0.2.9"
+bytes = "1.10.0"
+rusqlite = { version = "0.32.1", features = ["bundled"] }
+hyper = { version = "1.6.0", features = ["full"] }
+hyper-util = { version = "0.1.10", features = ["full"] }
 hyper-tungstenite = "0.14.0"
-tokio = { version = "1.38.1", features = ["full"] }
-serde = { version = "1.0.204", features = ["derive"] }
-tokio-rustls = { version = "0.26.0", optional = true}
-rustls-pemfile = { version = "2.1.2", optional = true}
-serde_json = "1.0.120"
+tokio = { version = "1.43.0", features = ["full"] }
+serde = { version = "1.0.217", features = ["derive"] }
+tokio-rustls = { version = "0.26.1", optional = true}
+rustls-pemfile = { version = "2.2.0", optional = true}
+serde_json = "1.0.138"
 http-body-util = "0.1.2"
 getopts = "0.2.21"
-url = "2.5.2"
+url = "2.5.4"
 jsonwebtoken = "9"
-matchit = "0.8.4"
-futures = "0.3.30"
-socket2 = "0.5.7"
+matchit = "0.8.6"
+futures = "0.3.31"
+socket2 = "0.5.8"
 cfg-if = "1.0.0"
 urlencoding = "2.1.3"
-chrono = "0.4.38"
+chrono = "0.4.39"
 nix = "0.29.0"
 tokio-fd = "0.3.0"
 pbkdf2 = { version = "0.12", features = ["simple"] }
@@ -38,10 +38,10 @@ https = ["tokio-rustls", "rustls-pemfile"]
 default = ["https"]
 
 [dev-dependencies]
-reqwest = {version = "0.12.5", features = ["blocking"]}
+reqwest = {version = "0.12.12", features = ["blocking"]}
 tungstenite = "0.23.0"
 tokio-tungstenite = "0.23.1"
-tempfile = "3.10.0"
+tempfile = "3.16.0"
 
 [build-dependencies]
-bindgen = "0.69.4"
+bindgen = "0.69.5"

+ 9 - 5
plugin/smartdns-ui/Makefile

@@ -15,6 +15,10 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 BIN=smartdns-ui
+PREFIX := /usr
+SBINDIR := $(PREFIX)/sbin
+SLIBDIR := $(PREFIX)/lib
+DESTDIR :=
 SMARTDNS_SRC_DIR=../../src
 
 ifdef DEBUG
@@ -27,20 +31,20 @@ CARGO_BUILD_PATH=target/release
 SMARTDNS_BUILD_TYPE=
 endif
 
-.PHONY: all clean $(BIN)
+.PHONY: all clean install $(BIN)
 
 all: $(BIN)
 
 test-prepare:
 	$(MAKE) -C $(SMARTDNS_SRC_DIR) libsmartdns-test.a
 
-smartdns:
-	$(MAKE) -C $(SMARTDNS_SRC_DIR) $(SMARTDNS_BUILD_TYPE)
-
-$(BIN): smartdns
+$(BIN):
 	MAKEFLAGS= cargo build $(CARGO_BUILD_TYPE) --features "build-release"
 	cp $(CARGO_BUILD_PATH)/libsmartdns_ui.so target/ 
 
+install: $(BIN)
+	install -v -m 0644 -D -t $(DESTDIR)$(SLIBDIR)/smartdns target/libsmartdns_ui.so
+
 test: test-prepare
 	MAKEFLAGS= cargo test
 

+ 15 - 0
plugin/smartdns-ui/build.rs

@@ -15,6 +15,20 @@ impl bindgen::callbacks::ParseCallbacks for IgnoreMacros {
     }
 }
 
+fn get_git_commit_version() {
+    let result = std::process::Command::new("git")
+        .args(&["describe", "--tags", "--always", "--dirty"])
+        .output();
+
+    let git_version = match result {
+        Ok(output) => output.stdout,
+        Err(_) => Vec::new(),
+    };
+
+    let git_version = String::from_utf8(git_version).expect("Invalid UTF-8 sequence");
+    println!("cargo:rustc-env=GIT_VERSION={}", git_version.trim());
+}
+
 fn link_smartdns_lib() {
     let curr_source_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
     let smartdns_src_dir = format!("{}/../../src", curr_source_dir);
@@ -65,5 +79,6 @@ fn link_smartdns_lib() {
 }
 
 fn main() {
+    get_git_commit_version();
     link_smartdns_lib();
 }

+ 70 - 28
plugin/smartdns-ui/src/data_server.rs

@@ -35,7 +35,8 @@ use std::sync::atomic::AtomicBool;
 use std::sync::{Arc, Mutex, RwLock};
 use tokio::sync::mpsc;
 use tokio::task::JoinHandle;
-use tokio::time::{Duration, Instant};
+use tokio::time::Duration;
+use tokio::time::Instant;
 
 pub const DEFAULT_MAX_LOG_AGE: u64 = 30 * 24 * 60 * 60;
 pub const DEFAULT_MAX_LOG_AGE_MS: u64 = DEFAULT_MAX_LOG_AGE * 1000;
@@ -190,7 +191,7 @@ impl DataServerControl {
         *self.is_run.lock().unwrap() = false;
     }
 
-    pub fn send_request(&self, request: &mut DnsRequest) -> Result<(), Box<dyn Error>> {
+    pub fn send_request(&self, request: Box<dyn DnsRequest>) -> Result<(), Box<dyn Error>> {
         if request.is_prefetch_request() {
             return Ok(());
         }
@@ -202,7 +203,7 @@ impl DataServerControl {
         }
 
         if let Some(tx) = self.data_server.data_tx.as_ref() {
-            let ret = tx.try_send(request.clone());
+            let ret = tx.try_send(request);
             if let Err(e) = ret {
                 self.data_server.get_stat().add_request_drop(1);
                 return Err(e.to_string().into());
@@ -226,8 +227,8 @@ pub struct DataServer {
     conf: Arc<RwLock<DataServerConfig>>,
     notify_tx: Option<mpsc::Sender<()>>,
     notify_rx: Mutex<Option<mpsc::Receiver<()>>>,
-    data_tx: Option<mpsc::Sender<DnsRequest>>,
-    data_rx: Mutex<Option<mpsc::Receiver<DnsRequest>>>,
+    data_tx: Option<mpsc::Sender<Box<dyn DnsRequest>>>,
+    data_rx: Mutex<Option<mpsc::Receiver<Box<dyn DnsRequest>>>>,
     db: Arc<DB>,
     disable_handle_request: AtomicBool,
     stat: Arc<DataStats>,
@@ -235,6 +236,7 @@ pub struct DataServer {
     plugin: Mutex<Option<Arc<SmartdnsPlugin>>>,
     whois: whois::WhoIs,
     startup_timestamp: u64,
+    recv_in_batch: Mutex<bool>,
 }
 
 impl DataServer {
@@ -254,6 +256,7 @@ impl DataServer {
             whois: whois::WhoIs::new(),
             startup_timestamp: get_utc_time_ms(),
             disable_handle_request: AtomicBool::new(false),
+            recv_in_batch: Mutex::new(true),
         };
 
         let (tx, rx) = mpsc::channel(100);
@@ -267,6 +270,14 @@ impl DataServer {
         plugin
     }
 
+    pub fn get_recv_in_batch(&self) -> bool {
+        *self.recv_in_batch.lock().unwrap()
+    }
+
+    pub fn set_recv_in_batch(&self, recv_in_batch: bool) {
+        *self.recv_in_batch.lock().unwrap() = recv_in_batch;
+    }
+
     fn init_server(&self, conf: &DataServerConfig) -> Result<(), Box<dyn Error>> {
         let mut conf_clone = self.conf.write().unwrap();
         *conf_clone = conf.clone();
@@ -369,6 +380,10 @@ impl DataServer {
         self.db.delete_domain_before_timestamp(timestamp)
     }
 
+    pub fn delete_client_by_id(&self, id: u64) -> Result<u64, Box<dyn Error>> {
+        self.db.delete_client_by_id(id)
+    }
+
     pub fn get_client_list(&self) -> Result<Vec<ClientData>, Box<dyn Error>> {
         self.db.get_client_list()
     }
@@ -442,18 +457,15 @@ impl DataServer {
         Ok(overview)
     }
 
+    pub fn insert_client_by_list(&self, data: &Vec<ClientData>) -> Result<(), Box<dyn Error>> {
+        self.db.insert_client(data)
+    }
+
     pub fn insert_domain_by_list(&self, data: &Vec<DomainData>) -> Result<(), Box<dyn Error>> {
-        let mut client_ip = Vec::new();
-        for item in data {
-            client_ip.push(item.client.clone());
-        }
-        self.db.insert_client(&client_ip)?;
         self.db.insert_domain(data)
     }
 
     pub fn insert_domain(&self, data: &DomainData) -> Result<(), Box<dyn Error>> {
-        let client_ip = vec![data.client.clone()];
-        self.db.insert_client(&client_ip)?;
         let list = vec![data.clone()];
         self.stat.add_total_request(1);
         if data.is_blocked {
@@ -463,9 +475,14 @@ impl DataServer {
         self.db.insert_domain(&list)
     }
 
-    async fn data_server_handle_dns_request(this: Arc<DataServer>, req_list: &Vec<DnsRequest>) {
+    async fn data_server_handle_dns_request(
+        this: Arc<DataServer>,
+        req_list: &Vec<Box<dyn DnsRequest>>,
+    ) {
         let mut domain_data_list = Vec::new();
+        let mut client_data_list = Vec::new();
         let mut blocked_num = 0;
+        let timestamp_now = get_utc_time_ms();
 
         for req in req_list {
             if req.is_prefetch_request() {
@@ -501,6 +518,23 @@ impl DataServer {
             );
 
             domain_data_list.push(domain_data);
+
+            let mac_str = req
+                .get_remote_mac()
+                .iter()
+                .map(|byte| format!("{:02x}", byte))
+                .collect::<Vec<String>>()
+                .join(":");
+
+            let client_data = ClientData {
+                id: 0,
+                client_ip: req.get_remote_addr(),
+                hostname: "".to_string(),
+                mac: mac_str,
+                last_query_timestamp: timestamp_now,
+            };
+
+            client_data_list.push(client_data);
         }
 
         this.stat.add_total_request(domain_data_list.len() as u64);
@@ -513,12 +547,16 @@ impl DataServer {
         );
 
         let ret = DataServer::call_blocking(this.clone(), move || {
-            let ret = this.insert_domain_by_list(&domain_data_list);
-            if let Err(e) = ret {
-                return Err(e.to_string());
-            }
+            let _ = match this.insert_domain_by_list(&domain_data_list) {
+                Ok(v) => v,
+                Err(e) => return Err(e.to_string()),
+            };
+
+            let ret = match this.insert_client_by_list(&client_data_list) {
+                Ok(v) => v,
+                Err(e) => return Err(e.to_string()),
+            };
 
-            let ret = ret.unwrap();
             Ok(ret)
         })
         .await;
@@ -578,7 +616,8 @@ impl DataServer {
 
     async fn data_server_loop(this: Arc<DataServer>) -> Result<(), Box<dyn Error>> {
         let mut rx: mpsc::Receiver<()>;
-        let mut data_rx: mpsc::Receiver<DnsRequest>;
+        let mut data_rx: mpsc::Receiver<Box<dyn DnsRequest>>;
+        let batch_mode  = *this.recv_in_batch.lock().unwrap();
 
         {
             let mut _rx = this.notify_rx.lock().unwrap();
@@ -589,7 +628,7 @@ impl DataServer {
 
         this.stat.clone().start_worker()?;
 
-        let mut req_list: Vec<DnsRequest> = Vec::new();
+        let mut req_list: Vec<Box<dyn DnsRequest>> = Vec::new();
         let mut batch_timer: Option<tokio::time::Interval> = None;
         let mut check_timer = tokio::time::interval(Duration::from_secs(60));
         let is_check_timer_running = Arc::new(AtomicBool::new(false));
@@ -630,15 +669,18 @@ impl DataServer {
                     match res {
                         Some(req) => {
                             req_list.push(req);
-                            if req_list.len() == 1 {
-                                batch_timer = Some(tokio::time::interval_at(
-                                    Instant::now() + Duration::from_millis(500),
-                                    Duration::from_secs(1),
-                                ));
-                            }
 
-                            if req_list.len() <= 65535 {
-                                continue;
+                            if batch_mode {
+                                if req_list.len() == 1 {
+                                    batch_timer = Some(tokio::time::interval_at(
+                                        Instant::now() + Duration::from_millis(500),
+                                        Duration::from_secs(1),
+                                    ));
+                                }
+
+                                if req_list.len() <= 65535 {
+                                    continue;
+                                }
                             }
 
                             DataServer::data_server_handle_dns_request(this.clone(), &req_list).await;

+ 17 - 2
plugin/smartdns-ui/src/data_stats.rs

@@ -269,8 +269,23 @@ impl DataStats {
             );
         }
 
-        let _ = self.db.refresh_client_top_list(now - 7 * 24 * 3600 * 1000);
-        let _ = self.db.refresh_domain_top_list(now - 7 * 24 * 3600 * 1000);
+        let ret = self.db.refresh_client_top_list(now - 7 * 24 * 3600 * 1000);
+        if let Err(e) = ret {
+            dns_log!(
+                LogLevel::WARN,
+                "refresh client top list error: {}",
+                e
+            );
+        }
+
+        let ret = self.db.refresh_domain_top_list(now - 7 * 24 * 3600 * 1000);
+        if let Err(e) = ret {
+            dns_log!(
+                LogLevel::WARN,
+                "refresh domain top list error: {}",
+                e
+            );
+        }
         let _ = self
             .db
             .delete_hourly_query_count_before_timestamp(30 * 24 * 3600 * 1000);

+ 96 - 32
plugin/smartdns-ui/src/db.rs

@@ -42,19 +42,23 @@ pub struct ClientData {
     pub hostname: String,
     pub client_ip: String,
     pub mac: String,
-    pub last_query_time: u64,
+    pub last_query_timestamp: u64,
 }
 
 #[derive(Debug, Clone)]
 pub struct ClientQueryCount {
     pub client_ip: String,
     pub count: u32,
+    pub timestamp_start: u64,
+    pub timestamp_end: u64,
 }
 
 #[derive(Debug, Clone)]
 pub struct DomainQueryCount {
     pub domain: String,
     pub count: u32,
+    pub timestamp_start: u64,
+    pub timestamp_end: u64,
 }
 
 #[derive(Debug, Clone)]
@@ -207,7 +211,9 @@ impl DB {
         conn.execute(
             "CREATE TABLE IF NOT EXISTS top_domain_list (
                 domain TEXT PRIMARY KEY,
-                count INTEGER DEFAULT 0
+                count INTEGER DEFAULT 0,
+                timestamp_start BIGINT DEFAULT 0,
+                timestamp_end BIGINT DEFAULT 0
             );",
             [],
         )?;
@@ -215,7 +221,9 @@ impl DB {
         conn.execute(
             "CREATE TABLE IF NOT EXISTS top_client_list (
                 client TEXT PRIMARY KEY,
-                count INTEGER DEFAULT 0
+                count INTEGER DEFAULT 0,
+                timestamp_start BIGINT DEFAULT 0,
+                timestamp_end BIGINT DEFAULT 0
             );",
             [],
         )?;
@@ -223,8 +231,12 @@ impl DB {
         conn.execute(
             "
         CREATE TABLE IF NOT EXISTS client (
-            id INTEGER PRIMARY KEY AUTOINCREMENT,
-            client_ip TEXT NOT NULL UNIQUE
+            id INTEGER PRIMARY KEY,
+            client_ip TEXT NOT NULL,
+            mac TEXT NOT NULL,
+            hostname TEXT NOT NULL,
+            last_query_timestamp BIGINT NOT NULL,
+            UNIQUE(client_ip, mac)
         )",
             [],
         )?;
@@ -887,19 +899,21 @@ impl DB {
 
     pub fn refresh_client_top_list(&self, timestamp: u64) -> Result<(), Box<dyn Error>> {
         let mut client_count_list = Vec::new();
-        let conn = self.get_readonly_conn();
-        if conn.as_ref().is_none() {
-            return Err("db is not open".into());
-        }
+        let conn = match self.get_readonly_conn() {
+            Some(v) => v,
+            None => return Err("db is not open".into()),
+        };
 
-        let conn = conn.as_ref().unwrap();
+        let timestamp_now = smartdns::get_utc_time_ms();
         let sql = "SELECT client, COUNT(*) FROM domain WHERE timestamp >= ? GROUP BY client ORDER BY COUNT(*) DESC LIMIT 20";
-        self.debug_query_plan(conn, sql.to_string(), &vec![timestamp.to_string()]);
+        self.debug_query_plan(&conn, sql.to_string(), &vec![timestamp.to_string()]);
         let mut stmt = conn.prepare(sql)?;
         let rows = stmt.query_map([timestamp.to_string()], |row| {
             Ok(ClientQueryCount {
                 client_ip: row.get(0)?,
                 count: row.get(1)?,
+                timestamp_start: timestamp,
+                timestamp_end: timestamp_now,
             })
         });
 
@@ -923,9 +937,22 @@ impl DB {
         stmt.execute([])?;
         stmt.finalize()?;
         let mut stmt =
-            tx.prepare("INSERT INTO top_client_list (client, count) VALUES ( ?1, ?2)")?;
+            tx.prepare("INSERT INTO top_client_list (client, count, timestamp_start, timestamp_end) VALUES ( ?1, ?2, $3, $4)")?;
         for client in &client_count_list {
-            stmt.execute(rusqlite::params![client.client_ip, client.count])?;
+            stmt.execute(rusqlite::params![
+                client.client_ip,
+                client.count,
+                client.timestamp_start,
+                client.timestamp_end
+            ])?;
+            dns_log!(
+                LogLevel::DEBUG,
+                "client: {}, count: {}, timestamp_start: {}, timestamp_end: {}",
+                client.client_ip,
+                client.count,
+                client.timestamp_start,
+                client.timestamp_end
+            );
         }
         stmt.finalize()?;
         tx.commit()?;
@@ -942,11 +969,13 @@ impl DB {
 
         let conn = conn.as_ref().unwrap();
         let mut stmt =
-            conn.prepare("SELECT client, count FROM top_client_list ORDER BY count DESC LIMIT ?")?;
+            conn.prepare("SELECT client, count, timestamp_start, timestamp_end FROM top_client_list ORDER BY count DESC LIMIT ?")?;
         let rows = stmt.query_map([count.to_string()], |row| {
             Ok(ClientQueryCount {
                 client_ip: row.get(0)?,
                 count: row.get(1)?,
+                timestamp_start: row.get(2)?,
+                timestamp_end: row.get(3)?,
             })
         });
 
@@ -1104,19 +1133,21 @@ impl DB {
 
     pub fn refresh_domain_top_list(&self, timestamp: u64) -> Result<(), Box<dyn Error>> {
         let mut domain_count_list = Vec::new();
-        let conn = self.get_readonly_conn();
-        if conn.as_ref().is_none() {
-            return Err("db is not open".into());
-        }
+        let conn = match self.get_readonly_conn() {
+            Some(v) => v,
+            None => return Err("db is not open".into()),
+        };
 
-        let conn = conn.as_ref().unwrap();
+        let timestamp_now = smartdns::get_utc_time_ms();
         let sql = "SELECT domain, COUNT(*) FROM domain WHERE timestamp >= ? GROUP BY domain ORDER BY COUNT(*) DESC LIMIT 20";
-        self.debug_query_plan(conn, sql.to_string(), &vec![timestamp.to_string()]);
+        self.debug_query_plan(&conn, sql.to_string(), &vec![timestamp.to_string()]);
         let mut stmt = conn.prepare(sql)?;
         let rows = stmt.query_map([timestamp.to_string()], |row| {
             Ok(DomainQueryCount {
                 domain: row.get(0)?,
                 count: row.get(1)?,
+                timestamp_start: timestamp,
+                timestamp_end: timestamp_now,
             })
         });
 
@@ -1139,9 +1170,14 @@ impl DB {
         stmt.execute([])?;
         stmt.finalize()?;
         let mut stmt =
-            tx.prepare("INSERT INTO top_domain_list (domain, count) VALUES ( ?1, ?2)")?;
+            tx.prepare("INSERT INTO top_domain_list (domain, count, timestamp_start, timestamp_end) VALUES ( ?1, ?2, ?3, ?4)")?;
         for domain in &domain_count_list {
-            stmt.execute(rusqlite::params![domain.domain, domain.count])?;
+            stmt.execute(rusqlite::params![
+                domain.domain,
+                domain.count,
+                domain.timestamp_start,
+                domain.timestamp_end
+            ])?;
         }
         stmt.finalize()?;
         tx.commit()?;
@@ -1158,11 +1194,13 @@ impl DB {
 
         let conn = conn.as_ref().unwrap();
 
-        let mut stmt = conn.prepare("SELECT domain, count FROM top_domain_list DESC LIMIT ?")?;
+        let mut stmt = conn.prepare("SELECT domain, count, timestamp_start, timestamp_end FROM top_domain_list DESC LIMIT ?")?;
         let rows = stmt.query_map([count.to_string()], |row| {
             Ok(DomainQueryCount {
                 domain: row.get(0)?,
                 count: row.get(1)?,
+                timestamp_start: row.get(2)?,
+                timestamp_end: row.get(3)?,
             })
         });
 
@@ -1290,17 +1328,26 @@ impl DB {
         Ok(ret)
     }
 
-    pub fn insert_client(&self, client_ip: &Vec<String>) -> Result<(), Box<dyn Error>> {
+    pub fn insert_client(&self, client_data: &Vec<ClientData>) -> Result<(), Box<dyn Error>> {
         let mut conn = self.conn.lock().unwrap();
         if conn.as_ref().is_none() {
             return Err("db is not open".into());
         }
-
+        
         let conn = conn.as_mut().unwrap();
         let tx = conn.transaction()?;
-        let mut stmt = tx.prepare("INSERT OR IGNORE INTO client (client_ip) VALUES (?1)")?;
-        for ip in client_ip {
-            let ret = stmt.execute(rusqlite::params![ip]);
+        let mut stmt = tx.prepare("INSERT INTO client (id, client_ip, mac, hostname, last_query_timestamp) VALUES (
+            (SELECT MAX(rowid) FROM client) + 1,
+            ?1, ?2, ?3, ?4)
+            ON CONFLICT(client_ip, mac) DO UPDATE SET last_query_timestamp = excluded.last_query_timestamp;
+            ")?;
+        for d in client_data {
+            let ret = stmt.execute(rusqlite::params![
+                d.client_ip,
+                d.mac,
+                d.hostname,
+                d.last_query_timestamp
+            ]);
 
             if let Err(e) = ret {
                 stmt.finalize()?;
@@ -1322,14 +1369,14 @@ impl DB {
 
         let conn = conn.as_ref().unwrap();
         let mut ret = Vec::new();
-        let mut stmt = conn.prepare("SELECT id, client_ip FROM client").unwrap();
+        let mut stmt = conn.prepare("SELECT id, client_ip, mac, hostname, last_query_timestamp FROM client").unwrap();
         let rows = stmt.query_map([], |row| {
             Ok(ClientData {
                 id: row.get(0)?,
                 client_ip: row.get(1)?,
-                mac: String::new(),
-                hostname: String::new(),
-                last_query_time: 0,
+                mac: row.get(2)?,
+                hostname: row.get(3)?,
+                last_query_timestamp: row.get(4)?,
             })
         });
 
@@ -1344,6 +1391,23 @@ impl DB {
         Ok(ret)
     }
 
+    pub fn delete_client_by_id(&self, id: u64) -> Result<u64, Box<dyn Error>> {
+        let conn = self.conn.lock().unwrap();
+        if conn.as_ref().is_none() {
+            return Err("db is not open".into());
+        }
+
+        let conn = conn.as_ref().unwrap();
+        
+        let ret = conn.execute("DELETE FROM client WHERE id = ?", &[&id]);
+
+        if let Err(e) = ret {
+            return Err(Box::new(e));
+        }
+
+        Ok(ret.unwrap() as u64)
+    }
+
     pub fn get_db_size(&self) -> u64 {
         let db_file = self.get_db_file_path();
         let mut total_size = 0;

+ 38 - 8
plugin/smartdns-ui/src/http_api_msg.rs

@@ -252,10 +252,11 @@ pub fn api_msg_parse_client_list(data: &str) -> Result<Vec<ClientData>, Box<dyn
     if list_count.is_none() {
         return Err("list_count not found".into());
     }
+
     let list_count = list_count.unwrap();
     let mut client_list = Vec::new();
     for i in 0..list_count {
-        let client_object = &v["client-list"][i as usize];
+        let client_object = &v["client_list"][i as usize];
         let id = client_object["id"].as_u64();
         if id.is_none() {
             return Err("id not found".into());
@@ -276,9 +277,9 @@ pub fn api_msg_parse_client_list(data: &str) -> Result<Vec<ClientData>, Box<dyn
             return Err("hostname not found".into());
         }
 
-        let last_query_time = client_object["last_query_time"].as_u64();
-        if last_query_time.is_none() {
-            return Err("last_query_time not found".into());
+        let last_query_timestamp = client_object["last_query_timestamp"].as_u64();
+        if last_query_timestamp.is_none() {
+            return Err("last_query_timestamp not found".into());
         }
 
         client_list.push(ClientData {
@@ -286,17 +287,18 @@ pub fn api_msg_parse_client_list(data: &str) -> Result<Vec<ClientData>, Box<dyn
             client_ip: client_ip.unwrap().to_string(),
             mac: mac.unwrap().to_string(),
             hostname: hostname.unwrap().to_string(),
-            last_query_time: last_query_time.unwrap(),
+            last_query_timestamp: last_query_timestamp.unwrap(),
         });
     }
 
     Ok(client_list)
 }
 
-pub fn api_msg_gen_client_list(client_list: &Vec<ClientData>) -> String {
+pub fn api_msg_gen_client_list(client_list: &Vec<ClientData>, total_count: u32) -> String {
     let json_str = json!({
         "list_count": client_list.len(),
-        "client-list":
+        "total_count": total_count,
+        "client_list":
             client_list
                 .iter()
                 .map(|x| {
@@ -305,7 +307,7 @@ pub fn api_msg_gen_client_list(client_list: &Vec<ClientData>) -> String {
                         "client_ip": x.client_ip,
                         "mac": x.mac,
                         "hostname": x.hostname,
-                        "last_query_time": x.last_query_time,
+                        "last_query_timestamp": x.last_query_timestamp,
                     });
                     s
                 })
@@ -564,6 +566,8 @@ pub fn api_msg_gen_top_client_list(client_list: &Vec<ClientQueryCount>) -> Strin
                     let s = json!({
                         "client_ip": x.client_ip,
                         "query_count": x.count,
+                        "timestamp_start": x.timestamp_start,
+                        "timestamp_end": x.timestamp_end,
                     });
                     s
                 })
@@ -592,9 +596,21 @@ pub fn api_msg_parse_top_client_list(data: &str) -> Result<Vec<ClientQueryCount>
             return Err("query_count not found".into());
         }
 
+        let timestamp_start = item["timestamp_start"].as_u64();
+        if timestamp_start.is_none() {
+            return Err("timestamp_start not found".into());
+        }
+
+        let timestamp_end = item["timestamp_end"].as_u64();
+        if timestamp_end.is_none() {
+            return Err("timestamp_end not found".into());
+        }
+
         client_list.push(ClientQueryCount {
             client_ip: client_ip.unwrap().to_string(),
             count: query_count.unwrap() as u32,
+            timestamp_start: timestamp_start.unwrap(),
+            timestamp_end: timestamp_end.unwrap(),
         });
     }
 
@@ -610,6 +626,8 @@ pub fn api_msg_gen_top_domain_list(domain_list: &Vec<DomainQueryCount>) -> Strin
                     let s = json!({
                         "domain": x.domain,
                         "query_count": x.count,
+                        "timestamp_start": x.timestamp_start,
+                        "timestamp_end": x.timestamp_end,
                     });
                     s
                 })
@@ -638,9 +656,21 @@ pub fn api_msg_parse_top_domain_list(data: &str) -> Result<Vec<DomainQueryCount>
             return Err("query_count not found".into());
         }
 
+        let timestamp_start = item["timestamp_start"].as_u64();
+        if timestamp_start.is_none() {
+            return Err("timestamp_start not found".into());
+        }
+
+        let timestamp_end = item["timestamp_end"].as_u64();
+        if timestamp_end.is_none() {
+            return Err("timestamp_end not found".into());
+        }
+
         domain_list.push(DomainQueryCount {
             domain: domain.unwrap().to_string(),
             count: query_count.unwrap() as u32,
+            timestamp_start: timestamp_start.unwrap(),
+            timestamp_end: timestamp_end.unwrap(),
         });
     }
 

+ 23 - 1
plugin/smartdns-ui/src/http_server.rs

@@ -71,6 +71,7 @@ pub struct HttpServerConfig {
     pub password: String,
     pub token_expired_time: u32,
     pub enable_cors: bool,
+    pub enable_terminal: bool,
 }
 
 impl HttpServerConfig {
@@ -82,9 +83,20 @@ impl HttpServerConfig {
             password: utils::hash_password(HTTP_SERVER_DEFAULT_PASSWORD, Some(1000)).unwrap(),
             token_expired_time: 600,
             enable_cors: false,
+            enable_terminal: false,
         }
     }
 
+    pub fn settings_map(&self) -> std::collections::HashMap<String, String> {
+        let mut map = std::collections::HashMap::new();
+        map.insert("http_ip".to_string(), self.http_ip.clone());
+        map.insert("username".to_string(), self.username.clone());
+        map.insert("token_expired_time".to_string(), self.token_expired_time.to_string());
+        map.insert("enable_cors".to_string(), self.enable_cors.to_string());
+        map.insert("enable_terminal".to_string(), self.enable_terminal.to_string());
+        map
+    }
+
     pub fn load_config(&mut self, data_server: Arc<DataServer>) -> Result<(), Box<dyn Error>> {
         if let Some(password) = data_server.get_config("smartdns-ui.password") {
             self.password = password;
@@ -101,7 +113,7 @@ impl HttpServerConfig {
             self.username = username;
         }
 
-        if let Some(enable_cors) = data_server.get_server_config("smartdns-ui.cors-enable") {
+        if let Some(enable_cors) = data_server.get_server_config("smartdns-ui.enable-cors") {
             if enable_cors.eq_ignore_ascii_case("yes") || enable_cors.eq_ignore_ascii_case("true") {
                 self.enable_cors = true;
             } else {
@@ -109,6 +121,16 @@ impl HttpServerConfig {
             }
         }
 
+        if let Some(enable_terminal) = data_server.get_server_config("smartdns-ui.enable-terminal") {
+            if enable_terminal.eq_ignore_ascii_case("yes")
+                || enable_terminal.eq_ignore_ascii_case("true")
+            {
+                self.enable_terminal = true;
+            } else {
+                self.enable_terminal = false;
+            }
+        }
+
         Ok(())
     }
 }

+ 83 - 4
plugin/smartdns-ui/src/http_server_api.rs

@@ -88,6 +88,7 @@ impl API {
         api.register(Method::GET, "/api/domain/{id}",  true, APIRoute!(API::api_domain_get_by_id));
         api.register(Method::DELETE, "/api/domain/{id}",  true, APIRoute!(API::api_domain_delete_by_id));
         api.register(Method::GET, "/api/client", true, APIRoute!(API::api_client_get_list));
+        api.register(Method::DELETE, "/api/client/{id}",  true, APIRoute!(API::api_client_delete_by_id));
         api.register(Method::GET, "/api/log/stream", true, APIRoute!(API::api_log_stream));
         api.register(Method::PUT, "/api/log/level", true, APIRoute!(API::api_log_set_level));
         api.register(Method::GET, "/api/log/level", true, APIRoute!(API::api_log_get_level));
@@ -510,6 +511,37 @@ impl API {
         API::response_build(StatusCode::OK, body)
     }
 
+    /// Delete the client by id <br>
+    /// API: DELETE /api/client/{id}
+    ///  parameter: <br>
+    async fn api_client_delete_by_id(
+        this: Arc<HttpServer>,
+        param: APIRouteParam,
+        _req: Request<body::Incoming>,
+    ) -> Result<Response<Full<Bytes>>, HttpError> {
+        let id = match API::params_parser_value(param.get("id")) {
+            Some(v) => v,
+            None => return API::response_error(StatusCode::BAD_REQUEST, "Invalid parameter."),
+        };
+
+        let data_server = this.get_data_server();
+        let ret = match data_server.delete_client_by_id(id) {
+            Ok(v) => v,
+            Err(e) => {
+                return API::response_error(
+                    StatusCode::INTERNAL_SERVER_ERROR,
+                    e.to_string().as_str(),
+                )
+            }
+        };
+
+        if ret == 0 {
+            return API::response_error(StatusCode::NOT_FOUND, "Not found");
+        }
+
+        API::response_build(StatusCode::NO_CONTENT, "".to_string())
+    }
+
     /// Delete the domain by id <br>
     /// API: DELETE /api/domain/{id}
     ///
@@ -688,8 +720,30 @@ impl API {
         _req: Request<body::Incoming>,
     ) -> Result<Response<Full<Bytes>>, HttpError> {
         let data_server = this.get_data_server();
-        let client_list: Vec<ClientData> = data_server.get_client_list()?;
-        let body = api_msg_gen_client_list(&client_list);
+        let ret = API::call_blocking(this, move || {
+            let ret = data_server.get_client_list();
+            if let Err(e) = ret {
+                return Err(e.to_string());
+            }
+
+            let ret = ret.unwrap();
+
+            return Ok(ret);
+        }).await;
+
+        let ret = match ret {
+            Ok(v) => v,
+            Err(e) => {
+                return API::response_error(StatusCode::INTERNAL_SERVER_ERROR, e.to_string().as_str());
+            },
+        };
+    
+        if let Err(e) = ret {
+            return API::response_error(StatusCode::INTERNAL_SERVER_ERROR, e.to_string().as_str());
+        }
+
+        let client_list = ret.unwrap();
+        let body = api_msg_gen_client_list(&client_list, client_list.len() as u32);
 
         API::response_build(StatusCode::OK, body)
     }
@@ -768,9 +822,11 @@ impl API {
 
     async fn api_config_get_settings(
         this: Arc<HttpServer>,
-        _param: APIRouteParam,
+        param: APIRouteParam,
         _req: Request<body::Incoming>,
     ) -> Result<Response<Full<Bytes>>, HttpError> {
+        let key = API::params_get_value(&param, "key");
+
         let data_server = this.get_data_server();
         let settings = data_server.get_config_list();
         if settings.is_err() {
@@ -778,11 +834,30 @@ impl API {
         }
 
         let mut settings = settings.unwrap();
+        this.get_conf().settings_map().iter().for_each(|(k, v)| {
+            if settings.get(k).is_none() {
+                settings.insert(k.to_string(), v.to_string());
+            }
+        });
         let pass = settings.get(PASSWORD_CONFIG_KEY);
         if pass.is_some() {
             let pass = "********".to_string();
             settings.insert(PASSWORD_CONFIG_KEY.to_string(), pass);
         }
+
+        if key.is_some() {
+            let key : String = key.unwrap();
+            let value = settings.get(key.as_str());
+            if value.is_none() {
+                return API::response_error(StatusCode::NOT_FOUND, "Not found");
+            }
+
+            let mut map = std::collections::HashMap::new();
+            map.insert(key, value.unwrap().clone());
+            let msg = api_msg_gen_key_value(&map);
+            return API::response_build(StatusCode::OK, msg);
+        }
+
         let msg = api_msg_gen_key_value(&settings);
         API::response_build(StatusCode::OK, msg)
     }
@@ -1027,10 +1102,14 @@ impl API {
     }
 
     async fn api_tool_term(
-        _this: Arc<HttpServer>,
+        this: Arc<HttpServer>,
         _param: APIRouteParam,
         mut req: Request<body::Incoming>,
     ) -> Result<Response<Full<Bytes>>, HttpError> {
+        if this.get_conf().enable_terminal != true {
+            return API::response_error(StatusCode::FORBIDDEN, "Terminal is disabled.");
+        }
+
         if hyper_tungstenite::is_upgrade_request(&req) {
             let (response, websocket) = hyper_tungstenite::upgrade(&mut req, None)
                 .map_err(|e| HttpError::new(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;

+ 1 - 0
plugin/smartdns-ui/src/http_server_stream.rs

@@ -222,6 +222,7 @@ pub async fn serve_term(websocket: HyperWebsocket) -> Result<(), Error> {
                 close(i);
             }
             use std::ffi::CString;
+            std::env::set_var("TERM", "xterm-256color");
 
             let find_cmd = |cmd: &str| -> Result<String, Box<dyn std::error::Error>> {
                 let env_path = std::env::var("PATH")?;

+ 8 - 5
plugin/smartdns-ui/src/plugin.rs

@@ -162,11 +162,14 @@ impl SmartdnsPlugin {
         self.data_server_ctl.stop_data_server();
     }
 
-    pub fn query_complete(&self, request: &mut DnsRequest) {
+    pub fn query_complete(&self, request: Box<dyn DnsRequest>) -> Result<(), Box<dyn Error>> {
         let ret = self.data_server_ctl.send_request(request);
-        if let Err(_) = ret {
-            return;
+        if let Err(e) = ret {
+            dns_log!(LogLevel::ERROR, "send request error: {}", e.to_string());
+            return Err(e);
         }
+
+        Ok(())
     }
 
     pub fn server_log(&self, level: LogLevel, msg: &str, msg_len: i32) {
@@ -199,8 +202,8 @@ impl Drop for SmartdnsPluginImpl {
 }
 
 impl SmartdnsOperations for SmartdnsPluginImpl {
-    fn server_query_complete(&self, request: &mut DnsRequest) {
-        self.plugin.query_complete(request);
+    fn server_query_complete(&self, request: Box<dyn DnsRequest>) {
+        let _ = self.plugin.query_complete(request);
     }
 
     fn server_log(&self, level: LogLevel, msg: &str, msg_len: i32) {

+ 61 - 29
plugin/smartdns-ui/src/smartdns.rs

@@ -206,7 +206,17 @@ pub fn smartdns_version() -> String {
 }
 
 pub fn smartdns_ui_version() -> String {
-    env!("CARGO_PKG_VERSION").to_string()
+    let mut ver = env!("CARGO_PKG_VERSION").to_string();
+
+    if env!("GIT_VERSION").is_empty() {
+        return ver;
+    }
+
+    ver.push_str(" (");
+    ver.push_str(env!("GIT_VERSION"));
+    ver.push_str(")");
+
+    ver
 }
 
 pub fn smartdns_get_server_name() -> String {
@@ -274,8 +284,8 @@ extern "C" fn dns_request_complete(request: *mut smartdns_c::dns_request) {
         }
 
         let ops = ops.unwrap();
-        let mut req = DnsRequest::new(request);
-        ops.server_query_complete(&mut req);
+        let req = DnsRequest_C::new(request);
+        ops.server_query_complete(Box::new(req));
     }
 }
 
@@ -333,18 +343,37 @@ extern "C" fn dns_plugin_exit(_plugin: *mut smartdns_c::dns_plugin) -> i32 {
     return 0;
 }
 
-pub struct DnsRequest {
+pub trait DnsRequest: Send + Sync {
+    fn get_group_name(&self) -> String;
+    fn get_domain(&self) -> String;
+    fn get_qtype(&self) -> u32;
+    fn get_qclass(&self) -> i32;
+    fn get_id(&self) -> u16;
+    fn get_rcode(&self) -> u16;
+    fn get_query_time(&self) -> i32;
+    fn get_query_timestamp(&self) -> u64;
+    fn get_ping_time(&self) -> f64;
+    fn get_is_blocked(&self) -> bool;
+    fn get_is_cached(&self) -> bool;
+    fn get_remote_mac(&self) -> [u8; 6];
+    fn get_remote_addr(&self) -> String;
+    fn get_local_addr(&self) -> String;
+    fn is_prefetch_request(&self) -> bool;
+    fn is_dualstack_request(&self) -> bool;
+}
+
+pub struct DnsRequest_C {
     request: *mut smartdns_c::dns_request,
 }
 
 #[allow(dead_code)]
-impl DnsRequest {
-    fn new(request: *mut smartdns_c::dns_request) -> DnsRequest {
+impl DnsRequest_C {
+    fn new(request: *mut smartdns_c::dns_request) -> DnsRequest_C {
         unsafe {
             smartdns_c::dns_server_request_get(request);
         }
 
-        DnsRequest { request }
+        DnsRequest_C { request }
     }
 
     fn put_ref(&mut self) {
@@ -353,8 +382,11 @@ impl DnsRequest {
             self.request = std::ptr::null_mut();
         }
     }
+}
 
-    pub fn get_group_name(&self) -> String {
+#[allow(dead_code)]
+impl DnsRequest for DnsRequest_C {
+    fn get_group_name(&self) -> String {
         unsafe {
             let group_name = smartdns_c::dns_server_request_get_group_name(self.request);
             std::ffi::CStr::from_ptr(group_name)
@@ -363,7 +395,7 @@ impl DnsRequest {
         }
     }
 
-    pub fn get_domain(&self) -> String {
+    fn get_domain(&self) -> String {
         unsafe {
             let domain = smartdns_c::dns_server_request_get_domain(self.request);
             std::ffi::CStr::from_ptr(domain)
@@ -372,46 +404,46 @@ impl DnsRequest {
         }
     }
 
-    pub fn get_qtype(&self) -> u32 {
+    fn get_qtype(&self) -> u32 {
         unsafe { smartdns_c::dns_server_request_get_qtype(self.request) as u32 }
     }
 
-    pub fn get_qclass(&self) -> i32 {
+    fn get_qclass(&self) -> i32 {
         unsafe { smartdns_c::dns_server_request_get_qclass(self.request) }
     }
 
-    pub fn get_id(&self) -> u16 {
+    fn get_id(&self) -> u16 {
         unsafe { smartdns_c::dns_server_request_get_id(self.request) as u16 }
     }
 
-    pub fn get_rcode(&self) -> u16 {
+    fn get_rcode(&self) -> u16 {
         unsafe { smartdns_c::dns_server_request_get_rcode(self.request) as u16 }
     }
 
-    pub fn get_query_time(&self) -> i32 {
+    fn get_query_time(&self) -> i32 {
         unsafe { smartdns_c::dns_server_request_get_query_time(self.request) }
     }
 
-    pub fn get_query_timestamp(&self) -> u64 {
+    fn get_query_timestamp(&self) -> u64 {
         unsafe { smartdns_c::dns_server_request_get_query_timestamp(self.request) }
     }
 
-    pub fn get_ping_time(&self) -> f64 {
+    fn get_ping_time(&self) -> f64 {
         let v = unsafe { smartdns_c::dns_server_request_get_ping_time(self.request) };
         let mut ping_time = v as f64;
         ping_time = (ping_time * 10.0).round() / 10.0;
         ping_time
     }
 
-    pub fn get_is_blocked(&self) -> bool {
+    fn get_is_blocked(&self) -> bool {
         unsafe { smartdns_c::dns_server_request_is_blocked(self.request) != 0 }
     }
 
-    pub fn get_is_cached(&self) -> bool {
+    fn get_is_cached(&self) -> bool {
         unsafe { smartdns_c::dns_server_request_is_cached(self.request) != 0 }
     }
 
-    pub fn get_remote_mac(&self) -> [u8; 6] {
+    fn get_remote_mac(&self) -> [u8; 6] {
         unsafe {
             let _mac_ptr = smartdns_c::dns_server_request_get_remote_mac(self.request);
             if _mac_ptr.is_null() {
@@ -423,7 +455,7 @@ impl DnsRequest {
         }
     }
 
-    pub fn get_remote_addr(&self) -> String {
+    fn get_remote_addr(&self) -> String {
         unsafe {
             let addr = smartdns_c::dns_server_request_get_remote_addr(self.request);
             if addr.is_null() {
@@ -446,7 +478,7 @@ impl DnsRequest {
         }
     }
 
-    pub fn get_local_addr(&self) -> String {
+    fn get_local_addr(&self) -> String {
         unsafe {
             let addr = smartdns_c::dns_server_request_get_local_addr(self.request);
             let mut buf = [0u8; 1024];
@@ -466,35 +498,35 @@ impl DnsRequest {
         }
     }
 
-    pub fn is_prefetch_request(&self) -> bool {
+    fn is_prefetch_request(&self) -> bool {
         unsafe { smartdns_c::dns_server_request_is_prefetch(self.request) != 0 }
     }
 
-    pub fn is_dualstack_request(&self) -> bool {
+    fn is_dualstack_request(&self) -> bool {
         unsafe { smartdns_c::dns_server_request_is_dualstack(self.request) != 0 }
     }
 }
 
-impl Drop for DnsRequest {
+impl Drop for DnsRequest_C {
     fn drop(&mut self) {
         self.put_ref();
     }
 }
 
-impl Clone for DnsRequest {
+impl Clone for DnsRequest_C {
     fn clone(&self) -> Self {
         unsafe {
             smartdns_c::dns_server_request_get(self.request);
         }
 
-        DnsRequest {
+        DnsRequest_C {
             request: self.request,
         }
     }
 }
 
-unsafe impl Send for DnsRequest {}
-unsafe impl Sync for DnsRequest {}
+unsafe impl Send for DnsRequest_C {}
+unsafe impl Sync for DnsRequest_C {}
 
 pub struct DnsServerStats {
     stats: *mut smartdns_c::dns_server_stats,
@@ -668,7 +700,7 @@ impl Clone for DnsUpstreamServer {
 unsafe impl Send for DnsUpstreamServer {}
 
 pub trait SmartdnsOperations {
-    fn server_query_complete(&self, request: &mut DnsRequest);
+    fn server_query_complete(&self, request: Box<dyn DnsRequest>);
     fn server_log(&self, level: LogLevel, msg: &str, msg_len: i32);
     fn server_init(&mut self, args: &Vec<String>) -> Result<(), Box<dyn Error>>;
     fn server_exit(&mut self);

+ 151 - 1
plugin/smartdns-ui/tests/common/server.rs

@@ -48,6 +48,129 @@ impl<'a> InstanceLockGuard<'a> {
     }
 }
 
+pub struct TestDnsRequest {
+    pub domain: String,
+    pub group_name: String,
+    pub qtype: u32,
+    pub qclass: i32,
+    pub id: u16,
+    pub rcode: u16,
+    pub query_time: i32,
+    pub query_timestamp: u64,
+    pub ping_time: f64,
+    pub is_blocked: bool,
+    pub is_cached: bool,
+    pub remote_mac: [u8; 6],
+    pub remote_addr: String,
+    pub local_addr: String,
+    pub prefetch_request: bool,
+    pub dualstack_request: bool,
+    pub drop_callback: Option<Box<dyn Fn() + Send + Sync>>,
+}
+
+#[allow(dead_code)]
+impl TestDnsRequest {
+    pub fn new() -> Self {
+        TestDnsRequest {
+            domain: "".to_string(),
+            group_name: "default".to_string(),
+            qtype: 1,
+            qclass: 1,
+            id: 0,
+            rcode: 2,
+            query_time: 0,
+            query_timestamp: get_utc_time_ms(),
+            ping_time: -0.1 as f64,
+            is_blocked: false,
+            is_cached: false,
+            remote_mac: [0; 6],
+            remote_addr: "127.0.0.1".to_string(),
+            local_addr: "127.0.0.1".to_string(),
+            prefetch_request: false,
+            dualstack_request: false,
+            drop_callback: None,
+        }
+    }
+}
+
+#[allow(dead_code)]
+impl DnsRequest for TestDnsRequest {
+    fn get_group_name(&self) -> String {
+        self.group_name.clone()
+    }
+
+    fn get_domain(&self) -> String {
+        self.domain.clone()
+    }
+
+    fn get_qtype(&self) -> u32 {
+        self.qtype
+    }
+
+    fn get_qclass(&self) -> i32 {
+        self.qclass
+    }
+
+    fn get_id(&self) -> u16 {
+        self.id
+    }
+
+    fn get_rcode(&self) -> u16 {
+        self.rcode
+    }
+
+    fn get_query_time(&self) -> i32 {
+        self.query_time
+    }
+
+    fn get_query_timestamp(&self) -> u64 {
+        self.query_timestamp
+    }
+
+    fn get_ping_time(&self) -> f64 {
+        self.ping_time
+    }
+
+    fn get_is_blocked(&self) -> bool {
+        self.is_blocked
+    }
+
+    fn get_is_cached(&self) -> bool {
+        self.is_cached
+    }
+
+    fn get_remote_mac(&self) -> [u8; 6] {
+        self.remote_mac
+    }
+
+    fn get_remote_addr(&self) -> String {
+        self.remote_addr.clone()
+    }
+
+    fn get_local_addr(&self) -> String {
+        self.local_addr.clone()
+    }
+
+    fn is_prefetch_request(&self) -> bool {
+        self.prefetch_request
+    }
+
+    fn is_dualstack_request(&self) -> bool {
+        self.dualstack_request
+    }
+}
+
+impl Drop for TestDnsRequest {
+    fn drop(&mut self) {
+        if let Some(f) = &self.drop_callback {
+            f();
+        }
+    }
+}
+
+unsafe impl Send for TestDnsRequest {}
+unsafe impl Sync for TestDnsRequest {}
+
 #[allow(dead_code)]
 struct TestSmartDnsConfigItem {
     pub key: String,
@@ -168,6 +291,7 @@ impl TestServer {
 
         server.workdir = server.temp_dir.path().to_str().unwrap().to_string();
         server.dns_server.set_workdir(&server.workdir);
+        server.get_data_server().set_recv_in_batch(false);
         server
     }
 
@@ -229,6 +353,33 @@ impl TestServer {
         }
     }
 
+    #[allow(dead_code)]
+    pub fn send_test_dnsrequest(
+        &mut self,
+        mut request: TestDnsRequest,
+    ) -> Result<(), Box<dyn std::error::Error>> {
+        let batch_mode = self.get_data_server().get_recv_in_batch();
+        let (tx, rx) = std::sync::mpsc::channel();
+        let request_drop_callback = move || {
+            tx.send(()).unwrap();
+        };
+
+        if batch_mode == false {
+            request.drop_callback = Some(Box::new(request_drop_callback));
+        }
+
+        let ret = self.plugin.query_complete(Box::new(request));
+        if let Err(e) = ret {
+            dns_log!(LogLevel::ERROR, "send_test_dnsrequest error: {:?}", e);
+            return Err(e);
+        }
+
+        if batch_mode == false {
+            rx.recv().unwrap();
+        }
+        Ok(())
+    }
+
     #[allow(dead_code)]
     pub fn new_mock_domain_record(&self) -> DomainData {
         DomainData {
@@ -246,7 +397,6 @@ impl TestServer {
         }
     }
 
-
     #[allow(dead_code)]
     pub fn get_data_server(&self) -> Arc<DataServer> {
         self.plugin.get_data_server()

+ 48 - 40
plugin/smartdns-ui/tests/restapi_test.rs

@@ -18,6 +18,7 @@
 
 mod common;
 
+use common::TestDnsRequest;
 use nix::libc::c_char;
 use reqwest;
 use serde_json::json;
@@ -291,11 +292,11 @@ fn test_rest_api_get_domain() {
     server.set_log_level(LogLevel::DEBUG);
     assert!(server.start().is_ok());
 
-    let record = server.new_mock_domain_record();
     for i in 0..1024 {
-        let mut record = record.clone();
-        record.domain = format!("{}.com", i);
-        assert!(server.add_domain_record(&record).is_ok());
+        let mut request = TestDnsRequest::new();
+        request.domain = format!("{}.com", i);
+        request.id = i as u16;
+        assert!(server.send_test_dnsrequest(request).is_ok());
     }
 
     let mut client = common::TestClient::new(&server.get_host());
@@ -328,11 +329,11 @@ fn test_rest_api_get_by_id() {
     server.set_log_level(LogLevel::DEBUG);
     assert!(server.start().is_ok());
 
-    let record = server.new_mock_domain_record();
     for i in 0..1024 {
-        let mut record = record.clone();
-        record.domain = format!("{}.com", i);
-        assert!(server.add_domain_record(&record).is_ok());
+        let mut request = TestDnsRequest::new();
+        request.domain = format!("{}.com", i);
+        request.id = i as u16;
+        assert!(server.send_test_dnsrequest(request).is_ok());
     }
 
     let mut client = common::TestClient::new(&server.get_host());
@@ -356,11 +357,11 @@ fn test_rest_api_delete_domain_by_id() {
     server.set_log_level(LogLevel::DEBUG);
     assert!(server.start().is_ok());
 
-    let record = server.new_mock_domain_record();
     for i in 0..1024 {
-        let mut record = record.clone();
-        record.domain = format!("{}.com", i);
-        assert!(server.add_domain_record(&record).is_ok());
+        let mut request = TestDnsRequest::new();
+        request.domain = format!("{}.com", i);
+        request.id = i as u16;
+        assert!(server.send_test_dnsrequest(request).is_ok());
     }
 
     let mut client = common::TestClient::new(&server.get_host());
@@ -402,7 +403,12 @@ fn test_rest_api_server_version() {
     assert!(version.is_ok());
     let version = version.unwrap();
     assert_eq!(version.0, smartdns_ui::smartdns::smartdns_version());
-    assert_eq!(version.1, env!("CARGO_PKG_VERSION"));
+    if env!("GIT_VERSION").is_empty() {
+        assert_eq!(version.1, env!("CARGO_PKG_VERSION"));
+        return;
+    }
+    let check_version = std::format!("{} ({})", env!("CARGO_PKG_VERSION"), env!("GIT_VERSION"));
+    assert_eq!(version.1, check_version);
 }
 
 #[test]
@@ -423,7 +429,12 @@ fn test_rest_api_https_server() {
     assert!(version.is_ok());
     let version = version.unwrap();
     assert_eq!(version.0, smartdns_ui::smartdns::smartdns_version());
-    assert_eq!(version.1, env!("CARGO_PKG_VERSION"));
+    if env!("GIT_VERSION").is_empty() {
+        assert_eq!(version.1, env!("CARGO_PKG_VERSION"));
+        return;
+    }
+    let check_version = std::format!("{} ({})", env!("CARGO_PKG_VERSION"), env!("GIT_VERSION"));
+    assert_eq!(version.1, check_version);
 }
 
 #[test]
@@ -467,15 +478,15 @@ fn test_rest_api_settings() {
 #[test]
 fn test_rest_api_get_client() {
     let mut server = common::TestServer::new();
-    server.set_log_level(LogLevel::DEBUG);
+    server.set_log_level(LogLevel::INFO);
     assert!(server.start().is_ok());
 
-    let record = server.new_mock_domain_record();
     for i in 0..1024 {
-        let mut record = record.clone();
-        record.domain = format!("{}.com", i);
-        record.client = format!("client-{}", i);
-        assert!(server.add_domain_record(&record).is_ok());
+        let mut request = TestDnsRequest::new();
+        request.domain = format!("{}.com", i);
+        request.remote_addr = format!("client-{}", i);
+        request.id = i as u16;
+        assert!(server.send_test_dnsrequest(request).is_ok());
     }
 
     let mut client = common::TestClient::new(&server.get_host());
@@ -498,20 +509,19 @@ fn test_rest_api_stats_top() {
     server.set_log_level(LogLevel::DEBUG);
     assert!(server.start().is_ok());
 
-    let record = server.new_mock_domain_record();
     for i in 0..1024 {
-        let mut record = record.clone();
+        let mut request = TestDnsRequest::new();
         if i < 512 {
-            record.domain = format!("a.com");
-            record.client = format!("192.168.1.1");
+            request.domain = format!("a.com");
+            request.remote_addr = format!("192.168.1.1");
         } else if i < 512 + 256 + 128 {
-            record.domain = format!("b.com");
-            record.client = format!("192.168.1.2");
+            request.domain = format!("b.com");
+            request.remote_addr = format!("192.168.1.2");
         } else {
-            record.domain = format!("c.com");
-            record.client = format!("192.168.1.3");
+            request.domain = format!("c.com");
+            request.remote_addr = format!("192.168.1.3");
         }
-        assert!(server.add_domain_record(&record).is_ok());
+        assert!(server.send_test_dnsrequest(request).is_ok());
     }
 
     server.get_data_server().get_stat().refresh();
@@ -603,13 +613,12 @@ fn test_rest_api_stats_metrics() {
         smartdns_ui::smartdns::smartdns_c::dns_stats.request.total = 15;
     }
 
-    let record = server.new_mock_domain_record();
     for i in 0..1024 {
-        let mut record = record.clone();
-        record.domain = format!("{}.com", i);
-        record.client = format!("client-{}", i);
-        record.is_blocked = i % 2 == 0;
-        assert!(server.add_domain_record(&record).is_ok());
+        let mut request = TestDnsRequest::new();
+        request.domain = format!("{}.com", i);
+        request.remote_addr = format!("client-{}", i);
+        request.is_blocked = i % 2 == 0;
+        assert!(server.send_test_dnsrequest(request).is_ok());
     }
 
     let c = client.get("/api/stats/metrics");
@@ -631,12 +640,11 @@ fn test_rest_api_get_hourly_query_count() {
     server.set_log_level(LogLevel::DEBUG);
     assert!(server.start().is_ok());
 
-    let record = server.new_mock_domain_record();
     for i in 0..1024 {
-        let mut record = record.clone();
-        record.domain = format!("{}.com", i);
-        record.client = format!("client-{}", i);
-        assert!(server.add_domain_record(&record).is_ok());
+        let mut request = TestDnsRequest::new();
+        request.domain = format!("{}.com", i);
+        request.remote_addr = format!("client-{}", i);
+        assert!(server.send_test_dnsrequest(request).is_ok());
     }
 
     let mut client = common::TestClient::new(&server.get_host());

+ 4 - 0
src/dns_client.c

@@ -4419,6 +4419,10 @@ int dns_client_query(const char *domain, int qtype, dns_client_callback callback
 		goto errout;
 	}
 
+	if (atomic_read(&client.run) == 0) {
+		goto errout;
+	}
+
 	query = malloc(sizeof(*query));
 	if (query == NULL) {
 		goto errout;

+ 58 - 4
src/util.c

@@ -1046,6 +1046,7 @@ int netlink_get_neighbors(int family,
 	if (netlink_neighbor_fd <= 0) {
 		netlink_neighbor_fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC | SOCK_NONBLOCK, NETLINK_ROUTE);
 		if (netlink_neighbor_fd < 0) {
+			errno = EINVAL;
 			return -1;
 		}
 	}
@@ -1058,6 +1059,7 @@ int netlink_get_neighbors(int family,
 	struct msghdr msg;
 	int len;
 	int ret = 0;
+	int send_count = 0;
 
 	memset(&msg, 0, sizeof(msg));
 	msg.msg_name = &sa;
@@ -1075,15 +1077,60 @@ int netlink_get_neighbors(int family,
 	ndm = NLMSG_DATA(nlh);
 	ndm->ndm_family = family;
 
-	if (send(netlink_neighbor_fd, buf, NLMSG_SPACE(sizeof(struct ndmsg)), 0) < 0) {
-		return -1;
+	while (true) {
+		if (send_count > 5) {
+			errno = ETIMEDOUT;
+			return -1;
+		}
+
+		send_count++;
+		if (send(netlink_neighbor_fd, buf, NLMSG_SPACE(sizeof(struct ndmsg)), 0) < 0) {
+			if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) {
+				struct timespec waiter;
+				waiter.tv_sec = 0;
+				waiter.tv_nsec = 500000;
+				nanosleep(&waiter, NULL);
+				continue;
+			}
+
+			close(netlink_neighbor_fd);
+			netlink_neighbor_fd = -1;
+			return -1;
+		}
+
+		break;
 	}
 
-	while ((len = recvmsg(netlink_neighbor_fd, &msg, 0)) > 0) {
+	int is_received = 0;
+	int recv_count = 0;
+	while (true) {
+		recv_count++;
+		len = recvmsg(netlink_neighbor_fd, &msg, 0);
+		if (len < 0) {
+			if (recv_count > 5 && is_received == 0) {
+				errno = ETIMEDOUT;
+				return -1;
+			}
+
+			if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) {
+				if (is_received) {
+					break;
+				}
+				struct timespec waiter;
+				waiter.tv_sec = 0;
+				waiter.tv_nsec = 500000;
+				nanosleep(&waiter, NULL);
+				continue;
+			}
+
+			return -1;
+		}
+
 		if (ret != 0) {
 			continue;
 		}
 
+		is_received = 1;
 		uint32_t nlh_len = len;
 		for (nlh = (struct nlmsghdr *)buf; NLMSG_OK(nlh, nlh_len); nlh = NLMSG_NEXT(nlh, nlh_len)) {
 			ndm = NLMSG_DATA(nlh);
@@ -1091,7 +1138,11 @@ int netlink_get_neighbors(int family,
 			const uint8_t *mac = NULL;
 			const uint8_t *net_addr = NULL;
 			int net_addr_len = 0;
-			int rta_len = RTM_PAYLOAD(nlh);
+			unsigned int rta_len = RTM_PAYLOAD(nlh);
+
+			if (rta_len > (sizeof(buf) - ((char *)rta - buf))) {
+				continue;
+			}
 
 			for (; RTA_OK(rta, rta_len); rta = RTA_NEXT(rta, rta_len)) {
 				if (rta->rta_type == NDA_DST) {
@@ -1128,6 +1179,9 @@ int netlink_get_neighbors(int family,
 					}
 				} else if (rta->rta_type == NDA_LLADDR) {
 					mac = RTA_DATA(rta);
+					if (mac[0] == 0 && mac[1] == 0 && mac[2] == 0 && mac[3] == 0 && mac[4] == 0 && mac[5] == 0) {
+						continue;
+					}
 				}
 			}