@@ -14,6 +14,7 @@ use std::io;
1414use std:: path:: PathBuf ;
1515
1616mod bpf_user;
17+ mod dns;
1718mod models;
1819mod policy;
1920mod state;
@@ -142,6 +143,8 @@ async fn run_firewall(interface: &str, policy: Option<PathBuf>) -> Result<()> {
142143 let _loader = BpfLoader :: new ( interface) ?;
143144 println ! ( "✓ XDP program attached to {}" , interface) ;
144145
146+ let mut fqdn_rules = Vec :: new ( ) ;
147+
145148 if let Some ( policy_path) = policy {
146149 println ! (
147150 "\n Loading rules from policy file: {}" ,
@@ -155,6 +158,15 @@ async fn run_firewall(interface: &str, policy: Option<PathBuf>) -> Result<()> {
155158 let maps = open_pinned_maps ( ) ?;
156159
157160 for policy_rule in & policy. rules {
161+ if policy_rule. is_fqdn_rule ( ) {
162+ println ! (
163+ "✓ Registered FQDN rule '{}': domain={} (DNS monitor will resolve)" ,
164+ policy_rule. name,
165+ policy_rule. domain. as_deref( ) . unwrap_or( "?" )
166+ ) ;
167+ continue ;
168+ }
169+
158170 RulesState :: add_rule ( & maps, policy_rule) ?;
159171
160172 let rule = policy_rule. to_rule ( ) ?;
@@ -169,7 +181,21 @@ async fn run_firewall(interface: &str, policy: Option<PathBuf>) -> Result<()> {
169181 ) ;
170182 }
171183
172- println ! ( "\n Successfully loaded {} rules!" , policy. rules. len( ) ) ;
184+ fqdn_rules = dns:: monitor:: extract_fqdn_rules ( & policy. rules ) ;
185+ let ip_rule_count = policy. rules . iter ( ) . filter ( |r| !r. is_fqdn_rule ( ) ) . count ( ) ;
186+ println ! ( "\n Successfully loaded {} rules!" , ip_rule_count) ;
187+
188+ if !fqdn_rules. is_empty ( ) {
189+ println ! (
190+ " + {} FQDN rule(s) registered (resolving domains now...)" ,
191+ fqdn_rules. len( )
192+ ) ;
193+ dns:: monitor:: pre_resolve_fqdn_rules ( & fqdn_rules, & maps) ;
194+ }
195+ }
196+
197+ if !fqdn_rules. is_empty ( ) {
198+ start_dns_monitor ( fqdn_rules) ;
173199 }
174200
175201 println ! ( "\n XDP firewall is running. Use 'firebee ui' to view the TUI." ) ;
@@ -178,6 +204,10 @@ async fn run_firewall(interface: &str, policy: Option<PathBuf>) -> Result<()> {
178204 // Keep the loader alive to prevent XDP detachment
179205 std:: mem:: forget ( _loader) ;
180206
207+ // The XDP/TC programs remain attached even after this process exits.
208+ tokio:: signal:: ctrl_c ( ) . await . ok ( ) ;
209+ println ! ( "\n Exiting. XDP firewall remains attached to the interface." ) ;
210+
181211 Ok ( ( ) )
182212}
183213
@@ -272,6 +302,81 @@ async fn run_tui() -> Result<()> {
272302 Ok ( ( ) )
273303}
274304
305+ /// Start a background thread that monitors DNS responses from the BPF ring buffer,
306+ /// matches them against FQDN rules, and installs/removes BPF rules dynamically.
307+ fn start_dns_monitor ( fqdn_rules : Vec < dns:: monitor:: FqdnRule > ) {
308+ use crate :: dns:: monitor:: { handle_dns_event, sweep_expired_rules, DnsMonitorState } ;
309+ use libbpf_rs:: RingBufferBuilder ;
310+ use std:: sync:: { Arc , Mutex } ;
311+
312+ let state = Arc :: new ( Mutex :: new ( DnsMonitorState :: new ( fqdn_rules) ) ) ;
313+
314+ let dns_state = Arc :: clone ( & state) ;
315+ std:: thread:: spawn ( move || {
316+ let maps = match open_pinned_maps ( ) {
317+ Ok ( m) => m,
318+ Err ( e) => {
319+ eprintln ! ( "[dns] Failed to open maps: {}" , e) ;
320+ return ;
321+ }
322+ } ;
323+
324+ let dns_map = match & maps. dns_events {
325+ Some ( m) => m,
326+ None => {
327+ eprintln ! ( "[dns] dns_events map not found — rebuild BPF programs with 'make bpf'" ) ;
328+ return ;
329+ }
330+ } ;
331+
332+ let mut rb_builder = RingBufferBuilder :: new ( ) ;
333+ let event_state = Arc :: clone ( & dns_state) ;
334+ let event_maps = match open_pinned_maps ( ) {
335+ Ok ( m) => m,
336+ Err ( e) => {
337+ eprintln ! ( "[dns] Failed to open maps for events: {}" , e) ;
338+ return ;
339+ }
340+ } ;
341+
342+ // Leak event_maps so it lives for the ring buffer callback's 'static lifetime
343+ let event_maps: & ' static BpfMaps < ' static > = Box :: leak ( Box :: new ( event_maps) ) ;
344+
345+ rb_builder
346+ . add ( dns_map, move |data| {
347+ handle_dns_event ( data, & event_state, event_maps) ;
348+ 0
349+ } )
350+ . expect ( "Failed to add dns_events to ring buffer" ) ;
351+
352+ let rb = rb_builder. build ( ) . expect ( "Failed to build DNS ring buffer" ) ;
353+
354+ println ! ( "[dns] Monitor started — watching for DNS responses..." ) ;
355+
356+ let mut sweep_counter = 0u32 ;
357+ loop {
358+ if let Err ( e) = rb. poll ( std:: time:: Duration :: from_millis ( 100 ) ) {
359+ log:: error!( "DNS ring buffer poll error: {}" , e) ;
360+ std:: thread:: sleep ( std:: time:: Duration :: from_millis ( 100 ) ) ;
361+ }
362+
363+ // Sweep expired entries every ~10 seconds
364+ sweep_counter += 1 ;
365+ if sweep_counter >= 100 {
366+ sweep_counter = 0 ;
367+ let sweep_maps = match open_pinned_maps ( ) {
368+ Ok ( m) => m,
369+ Err ( e) => {
370+ log:: error!( "DNS monitor: failed to open maps for sweep: {}" , e) ;
371+ continue ;
372+ }
373+ } ;
374+ sweep_expired_rules ( & dns_state, & sweep_maps) ;
375+ }
376+ }
377+ } ) ;
378+ }
379+
275380async fn add_rules_from_policy (
276381 interface : Option < String > ,
277382 policy_path : PathBuf ,
@@ -304,6 +409,15 @@ async fn add_rules_from_policy(
304409 } ;
305410
306411 for policy_rule in & policy. rules {
412+ if policy_rule. is_fqdn_rule ( ) {
413+ println ! (
414+ "✓ Registered FQDN rule '{}': domain={} (DNS monitor will resolve)" ,
415+ policy_rule. name,
416+ policy_rule. domain. as_deref( ) . unwrap_or( "?" )
417+ ) ;
418+ continue ;
419+ }
420+
307421 RulesState :: add_rule ( & maps, policy_rule) ?;
308422
309423 let rule = policy_rule. to_rule ( ) ?;
@@ -318,7 +432,20 @@ async fn add_rules_from_policy(
318432 ) ;
319433 }
320434
321- println ! ( "\n Successfully added {} rules!" , policy. rules. len( ) ) ;
435+ let fqdn_rules = dns:: monitor:: extract_fqdn_rules ( & policy. rules ) ;
436+ let has_fqdn = !fqdn_rules. is_empty ( ) ;
437+ let ip_rule_count = policy. rules . iter ( ) . filter ( |r| !r. is_fqdn_rule ( ) ) . count ( ) ;
438+ println ! ( "\n Successfully added {} rules!" , ip_rule_count) ;
439+
440+ if has_fqdn {
441+ println ! (
442+ " + {} FQDN rule(s) registered (resolving domains now...)" ,
443+ fqdn_rules. len( )
444+ ) ;
445+ dns:: monitor:: pre_resolve_fqdn_rules ( & fqdn_rules, & maps) ;
446+ start_dns_monitor ( fqdn_rules) ;
447+ }
448+
322449 println ! ( "Rules are now active in the eBPF firewall." ) ;
323450
324451 if _loader. is_some ( ) {
@@ -327,6 +454,12 @@ async fn add_rules_from_policy(
327454 std:: mem:: forget ( _loader) ; // Intentionally leak to keep XDP attached
328455 }
329456
457+ if has_fqdn {
458+ println ! ( "\n DNS monitor is running for FQDN rules." ) ;
459+ tokio:: signal:: ctrl_c ( ) . await . ok ( ) ;
460+ println ! ( "\n Exiting. FQDN rules installed in BPF maps remain active." ) ;
461+ }
462+
330463 Ok ( ( ) )
331464}
332465
0 commit comments