From 9b75ddbfc45fed24d16eea1203cc7a6f347bd69f Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Tue, 28 Jul 2026 15:15:29 +0200 Subject: [PATCH 01/13] wip: set a fixed MAC for bridged test and test port forwarding issue #2360 --- lib/Ravada/Domain.pm | 1 + t/vm/93_ports_bridge.t | 154 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 154 insertions(+), 1 deletion(-) diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index 5bc079700..02b1ab5d9 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -7668,6 +7668,7 @@ sub _check_port($self, $port, $ip=$self->ip, $request=undef) { return 1 if $err =~ /succeeded!/; return 0 if $err =~ /failed/; + warn "nc -z -v -w 1 $ip $port"; warn $err; return 0; } diff --git a/t/vm/93_ports_bridge.t b/t/vm/93_ports_bridge.t index f23730fae..74342689f 100644 --- a/t/vm/93_ports_bridge.t +++ b/t/vm/93_ports_bridge.t @@ -6,6 +6,7 @@ use Data::Dumper; use IPC::Run3; use JSON::XS; use Test::More; +use YAML qw(LoadFile); use lib 't/lib'; use Test::Ravada; @@ -17,6 +18,11 @@ use_ok('Ravada'); my $BASE_NAME = "zz-test-base-alpine-q35-uefi"; my $BASE; +my $FILE_CONFIG_BRIDGE = "t/etc/bridge.conf"; + +my $CONFIG_BRIDGE; +$CONFIG_BRIDGE = LoadFile($FILE_CONFIG_BRIDGE) +if -e $FILE_CONFIG_BRIDGE; ####################################################################### @@ -43,6 +49,20 @@ sub _wait_ip($domain) { confess "Error : no ip for ".$domain->name; } +sub _set_mac_address($domain) { + return if ! $CONFIG_BRIDGE || !exists $CONFIG_BRIDGE->{mac}; + if ($domain->type eq 'KVM') { + my $doc = XML::LibXML->load_xml( string => $domain->xml_description()); + my ($dev) = $doc->findnodes('/domain/devices/interface[@type="bridge"]/mac'); + $dev->setAttribute('address' => $CONFIG_BRIDGE->{mac}); + $domain->reload_config($doc); + } elsif( $domain->type eq 'Void') { + return; + } else { + die "I don't know how to set mac for ".$domain->type; + } +} + sub _set_bridge($vm, $domain) { my @bridges = $vm->_list_bridges(); my $req = Ravada::Request->change_hardware( @@ -57,6 +77,7 @@ sub _set_bridge($vm, $domain) { } ); wait_request(); + _set_mac_address($domain); return $bridges[0]; } @@ -155,12 +176,142 @@ sub test_bridge($vm) { } +sub _get_alternate_ip { + my ($in, $out, $err); + run3(["ip","route"],\$in, \$out,\ $err); + + my ($ip) = $out =~ /^\d+\.\d+.* dev virbr.*link src (\d+\.\d+\.\d+\.\d+)/m; + + return $ip if $ip; + + warn "Warning: I can't find an alternate ip from $out. Using localhost" if !$ip; + + return '127.0.0.1'; + +} + +# Test scenario with NAT and Display_ip ######################################## +# +sub test_bridge_nat($vm) { + + my $nat_ip = '198.18.1.33'; + my $display_ip = _get_alternate_ip(); + $vm->nat_ip($nat_ip); + $vm->display_ip($display_ip); + + diag("NAT IP: ".$vm->nat_ip." , display_ip: ".$vm->display_ip); + + my $domain= $BASE->clone(name => new_domain_name, user => user_admin); + is($domain->has_nat_interfaces,1,"Expecting ".$domain->name." has nat " + .$vm->name); + _set_bridge($vm, $domain); + is($domain->has_nat_interfaces,0,"Expecting ".$domain->name." has no nat " + .$vm->name) or exit; + + my $internal_port = 22; + my $name = "foo"; + $domain->expose(port => $internal_port, restricted => 1, name => 'ssh'); + + my $remote_ip = '10.0.0.1'; + $domain->start(user => user_admin, remote_ip => $remote_ip); + _wait_ip($domain); + + Ravada::Request->start_domain(uid => user_admin->id + ,id_domain => $domain->id + ,remote_ip => $remote_ip + ); + wait_request(debug => 0); + exit; + + my $internal_ip = _wait_ip($domain); + $domain->ip; + wait_request(debug => 0); + + my $ip_info = $domain->ip_info(); + ok($ip_info->{type} eq 'bridge'); + + my $internal_net = $internal_ip; + $internal_net =~ s{(.*)\.\d+$}{$1.0/24}; + + my $local_ip = $vm->ip; + my $exposed_port = $domain->exposed_port($internal_port); + my $public_port = $exposed_port->{public_port}; + + ok($public_port) or die $domain->name; + + isnt($exposed_port->{public_port}, $internal_port) or exit; + + my ($in, $out, $err); + run3(['iptables','-t','nat','-L','PREROUTING','-n'],\($in, $out, $err)); + die $err if $err; + my @out = split /\n/,$out; + is(grep(/^DNAT.*$local_ip.*dpt:$public_port to:$internal_ip:$internal_port/,@out),1) + or die Dumper(\@out); + + run3(['iptables','-t','nat','-L','POSTROUTING','-n'],\($in, $out, $err)); + die $err if $err; + @out = split /\n/,$out; + is(grep(/^SNAT.* 0.0.0.0\/0\s+$internal_ip\s+tcp dpt\:$internal_port to\:$local_ip$/,@out),1); + + run3(['iptables-save','-t','nat'],\($in, $out, $err)); + @out = grep /SNAT/, split/\n/,$out; + warn "SNAT: ".Dumper([grep /SNAT/, @out]); + + run3(['iptables','-L','FORWARD','-n'],\($in, $out, $err)); + die $err if $err; + @out = split /\n/,$out; + is(grep(m{^ACCEPT.*$internal_net\s+state NEW},@out),1) or die $out; + + run3(['iptables','-L','FORWARD','-n'],\($in, $out, $err)); + die $err if $err; + @out = split /\n/,$out; + is(grep(m{^ACCEPT.*$remote_ip\s+$internal_ip.*dpt:$internal_port},@out),1) or die $out; + is(grep(m{^DROP.*0.0.0.0.+$internal_ip.*dpt:$internal_port},@out),1) or die $out; + + diag("Shutdown ".$domain->name); + Ravada::Request->shutdown_domain( + uid => user_admin->id + ,id_domain => $domain->id + ,timeout => 2 + ); + wait_request(); + for ( 1.. 10 ) { + run3(['iptables','-t','nat','-L','PREROUTING','-n'],\($in, $out, $err)); + die $err if $err; + @out = split /\n/,$out; + warn "DNAT 0 ".Dumper([grep /^DNAT/,@out]); + + last if(!grep(/^DNAT.*$local_ip.*dpt:$public_port to:$internal_ip:$internal_port/,@out)); + + wait_request(); + } + + run3(['iptables','-t','nat','-L','PREROUTING','-n'],\($in, $out, $err)); + die $err if $err; + @out = split /\n/,$out; + + is(grep(/^DNAT.*$local_ip.*dpt:$public_port to:$internal_ip:$internal_port/,@out),0) + or die Dumper(\@out); + + run3(['iptables','-t','nat','-L','POSTROUTING','-n'],\($in, $out, $err)); + die $err if $err; + @out = split /\n/,$out; + is(grep(/^SNAT.* 0.0.0.0\/0\s+$internal_ip\s+tcp dpt\:$internal_port to\:$local_ip$/,@out),0) + or die Dumper([ grep /^SNAT/, @out]); + + $vm->nat_ip(''); + $vm->display_ip(''); + remove_domain($domain); + diag("done"); +} + + ###################################################################### init(); clean(); -for my $vm_name ( reverse vm_names() ) { +for my $vm_name ( vm_names() ) { SKIP: { my $vm = rvd_back->search_vm($vm_name); @@ -179,6 +330,7 @@ for my $vm_name ( reverse vm_names() ) { flush_rules() if !$<; _import_base($vm); + test_bridge_nat($vm); test_bridge($vm); } } From 4e46c1e601efa8734751e2574b4c5f7b04f30646 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Wed, 29 Jul 2026 12:25:54 +0200 Subject: [PATCH 02/13] wip: use interface IP for SNAT issue #2360 --- lib/Ravada/Domain.pm | 4 ++-- lib/Ravada/VM.pm | 9 +++++++++ t/vm/93_ports_bridge.t | 29 +++++++++++++++-------------- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index 02b1ab5d9..e694247a6 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -3970,7 +3970,7 @@ sub _open_exposed_port($self, $internal_port, $name, $restricted, $remote_ip=und $public_port = $self->_set_public_port($id_port, $internal_port, $name, $restricted) if !$public_port; - my $local_ip = $self->_vm->ip; + my $local_ip = $self->_vm->interface_ip($remote_ip); $sth = $$CONNECTOR->dbh->prepare("UPDATE domain_ports set internal_ip=?" ." WHERE id_domain=? AND internal_port=?" ); @@ -6797,6 +6797,7 @@ sub _around_ip($orig, $self, @args) { uid => Ravada::Utils::user_daemon->id ,id_domain => $self->id ,retry => 20 + ,remote_ip => $self->remote_ip() ,_force => 1 ); } @@ -7668,7 +7669,6 @@ sub _check_port($self, $port, $ip=$self->ip, $request=undef) { return 1 if $err =~ /succeeded!/; return 0 if $err =~ /failed/; - warn "nc -z -v -w 1 $ip $port"; warn $err; return 0; } diff --git a/lib/Ravada/VM.pm b/lib/Ravada/VM.pm index b9bfaa1ad..b70f37ed3 100644 --- a/lib/Ravada/VM.pm +++ b/lib/Ravada/VM.pm @@ -907,6 +907,15 @@ sub _set_display_ip($self, $value) { $self->_data( display_ip => $value ); } +sub interface_ip($self, $remote_ip=undef) { + my $key = '_interface_ip_'.($remote_ip or ''); + return $self->{$key} if exists $self->{$key}; + + my $ip = $self->_interface_ip($remote_ip); + $self->{$key}=$ip; + return $ip; +} + sub _list_ip_address($self) { my @cmd = ("ip","address","show"); my ($out, $err) = $self->run_command(@cmd); diff --git a/t/vm/93_ports_bridge.t b/t/vm/93_ports_bridge.t index 74342689f..9f888d07e 100644 --- a/t/vm/93_ports_bridge.t +++ b/t/vm/93_ports_bridge.t @@ -40,9 +40,11 @@ sub _import_base($vm) { } sub _wait_ip($domain) { - $domain->start(user => user_admin, remote_ip => '1.2.3.5') unless $domain->is_active(); + my $remote_ip = '1.2.3.4.5'; + $domain->start(user => user_admin, remote_ip => $remote_ip) unless $domain->is_active(); for ( 1 .. 30 ) { - return $domain->ip if $domain->ip; + my $ip = $domain->ip(); + return $ip if $ip; diag("Waiting for ".$domain->name. " ip") if !(time % 10); sleep 1; } @@ -199,8 +201,6 @@ sub test_bridge_nat($vm) { $vm->nat_ip($nat_ip); $vm->display_ip($display_ip); - diag("NAT IP: ".$vm->nat_ip." , display_ip: ".$vm->display_ip); - my $domain= $BASE->clone(name => new_domain_name, user => user_admin); is($domain->has_nat_interfaces,1,"Expecting ".$domain->name." has nat " .$vm->name); @@ -220,8 +220,7 @@ sub test_bridge_nat($vm) { ,id_domain => $domain->id ,remote_ip => $remote_ip ); - wait_request(debug => 0); - exit; + wait_request(debug => 0 , skip => 'refresh_machine_ports'); my $internal_ip = _wait_ip($domain); $domain->ip; @@ -234,6 +233,7 @@ sub test_bridge_nat($vm) { $internal_net =~ s{(.*)\.\d+$}{$1.0/24}; my $local_ip = $vm->ip; + my $interface_ip = $vm->interface_ip($remote_ip); my $exposed_port = $domain->exposed_port($internal_port); my $public_port = $exposed_port->{public_port}; @@ -245,17 +245,19 @@ sub test_bridge_nat($vm) { run3(['iptables','-t','nat','-L','PREROUTING','-n'],\($in, $out, $err)); die $err if $err; my @out = split /\n/,$out; - is(grep(/^DNAT.*$local_ip.*dpt:$public_port to:$internal_ip:$internal_port/,@out),1) + is(grep(/^DNAT.*$interface_ip.*dpt:$public_port to:$internal_ip:$internal_port/,@out),1) or die Dumper(\@out); run3(['iptables','-t','nat','-L','POSTROUTING','-n'],\($in, $out, $err)); die $err if $err; @out = split /\n/,$out; - is(grep(/^SNAT.* 0.0.0.0\/0\s+$internal_ip\s+tcp dpt\:$internal_port to\:$local_ip$/,@out),1); + + is(grep(/^SNAT.* 0.0.0.0\/0\s+$internal_ip\s+tcp dpt\:$internal_port to\:$interface_ip$/,@out),1); run3(['iptables-save','-t','nat'],\($in, $out, $err)); @out = grep /SNAT/, split/\n/,$out; - warn "SNAT: ".Dumper([grep /SNAT/, @out]); + my @snat = grep /SNAT/, @out; + is(scalar(@snat),1); run3(['iptables','-L','FORWARD','-n'],\($in, $out, $err)); die $err if $err; @@ -268,20 +270,20 @@ sub test_bridge_nat($vm) { is(grep(m{^ACCEPT.*$remote_ip\s+$internal_ip.*dpt:$internal_port},@out),1) or die $out; is(grep(m{^DROP.*0.0.0.0.+$internal_ip.*dpt:$internal_port},@out),1) or die $out; - diag("Shutdown ".$domain->name); Ravada::Request->shutdown_domain( uid => user_admin->id ,id_domain => $domain->id ,timeout => 2 ); - wait_request(); + wait_request(debug => 0, skip => 'refresh_machine_ports'); for ( 1.. 10 ) { run3(['iptables','-t','nat','-L','PREROUTING','-n'],\($in, $out, $err)); die $err if $err; @out = split /\n/,$out; - warn "DNAT 0 ".Dumper([grep /^DNAT/,@out]); - last if(!grep(/^DNAT.*$local_ip.*dpt:$public_port to:$internal_ip:$internal_port/,@out)); + my ($dnat) = grep(/^DNAT.*$local_ip.*dpt:$public_port to:$internal_ip:$internal_port/,@out); + my ($snat) = grep(/^SNAT.* 0.0.0.0\/0\s+$internal_ip\s+tcp dpt\:$internal_port to\:$interface_ip$/,@out); + last if !$dnat && !$snat; wait_request(); } @@ -302,7 +304,6 @@ sub test_bridge_nat($vm) { $vm->nat_ip(''); $vm->display_ip(''); remove_domain($domain); - diag("done"); } From 8af565b956a7c31389eef2538b67e69e79d166aa Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Wed, 29 Jul 2026 12:37:55 +0200 Subject: [PATCH 03/13] wip: test first mock domains --- t/vm/93_ports_bridge.t | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/t/vm/93_ports_bridge.t b/t/vm/93_ports_bridge.t index 9f888d07e..87678e939 100644 --- a/t/vm/93_ports_bridge.t +++ b/t/vm/93_ports_bridge.t @@ -312,7 +312,7 @@ sub test_bridge_nat($vm) { init(); clean(); -for my $vm_name ( vm_names() ) { +for my $vm_name ( reverse vm_names() ) { SKIP: { my $vm = rvd_back->search_vm($vm_name); From b3588787d97fbc04a5fbe943e32c232fbdef0bd0 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Wed, 29 Jul 2026 12:52:11 +0200 Subject: [PATCH 04/13] doc: interface ip issue #2360 --- lib/Ravada/VM.pm | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/Ravada/VM.pm b/lib/Ravada/VM.pm index b70f37ed3..3b4998d64 100644 --- a/lib/Ravada/VM.pm +++ b/lib/Ravada/VM.pm @@ -907,6 +907,15 @@ sub _set_display_ip($self, $value) { $self->_data( display_ip => $value ); } +=head2 interface_ip + +Returns the Host IP from its network interface + +Optionally you can pass a remote IP. Then the proper interface +will be returned if you connect from a different network than default. + +=cut + sub interface_ip($self, $remote_ip=undef) { my $key = '_interface_ip_'.($remote_ip or ''); return $self->{$key} if exists $self->{$key}; From 796d1fc6a9aafb895e2eab2b8b895bc26559b554 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Wed, 29 Jul 2026 12:53:21 +0200 Subject: [PATCH 05/13] Use a correct network ip Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- t/vm/93_ports_bridge.t | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/t/vm/93_ports_bridge.t b/t/vm/93_ports_bridge.t index 87678e939..f336b7355 100644 --- a/t/vm/93_ports_bridge.t +++ b/t/vm/93_ports_bridge.t @@ -40,8 +40,7 @@ sub _import_base($vm) { } sub _wait_ip($domain) { - my $remote_ip = '1.2.3.4.5'; - $domain->start(user => user_admin, remote_ip => $remote_ip) unless $domain->is_active(); + my $remote_ip = '1.2.3.5'; for ( 1 .. 30 ) { my $ip = $domain->ip(); return $ip if $ip; From c882dd8f5c9e06215c64afcc2743ac1bf3afad48 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Wed, 29 Jul 2026 12:54:07 +0200 Subject: [PATCH 06/13] Use XML lib Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- t/vm/93_ports_bridge.t | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/t/vm/93_ports_bridge.t b/t/vm/93_ports_bridge.t index f336b7355..7c8041130 100644 --- a/t/vm/93_ports_bridge.t +++ b/t/vm/93_ports_bridge.t @@ -7,7 +7,7 @@ use IPC::Run3; use JSON::XS; use Test::More; use YAML qw(LoadFile); - +use XML::LibXML; use lib 't/lib'; use Test::Ravada; From b35d452c58a98b3d09e1e65c0a8a3c8d0896e411 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Wed, 29 Jul 2026 12:54:43 +0200 Subject: [PATCH 07/13] Show error and quit when failed Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- t/vm/93_ports_bridge.t | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/t/vm/93_ports_bridge.t b/t/vm/93_ports_bridge.t index 7c8041130..e7b1643b5 100644 --- a/t/vm/93_ports_bridge.t +++ b/t/vm/93_ports_bridge.t @@ -179,8 +179,8 @@ sub test_bridge($vm) { sub _get_alternate_ip { my ($in, $out, $err); - run3(["ip","route"],\$in, \$out,\ $err); - + run3(["ip","route"], \$in, \$out, \$err); + die $err if $err; my ($ip) = $out =~ /^\d+\.\d+.* dev virbr.*link src (\d+\.\d+\.\d+\.\d+)/m; return $ip if $ip; From a25320be49a6408d203187101f9b936a1e170cd9 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Wed, 29 Jul 2026 12:55:10 +0200 Subject: [PATCH 08/13] Show error and exit if failed Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- t/vm/93_ports_bridge.t | 1 + 1 file changed, 1 insertion(+) diff --git a/t/vm/93_ports_bridge.t b/t/vm/93_ports_bridge.t index e7b1643b5..8ed7b141b 100644 --- a/t/vm/93_ports_bridge.t +++ b/t/vm/93_ports_bridge.t @@ -254,6 +254,7 @@ sub test_bridge_nat($vm) { is(grep(/^SNAT.* 0.0.0.0\/0\s+$internal_ip\s+tcp dpt\:$internal_port to\:$interface_ip$/,@out),1); run3(['iptables-save','-t','nat'],\($in, $out, $err)); + die $err if $err; @out = grep /SNAT/, split/\n/,$out; my @snat = grep /SNAT/, @out; is(scalar(@snat),1); From 7ab8ba20ad9671c38ededf1ad0cfad5bee4e1699 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Wed, 29 Jul 2026 12:56:32 +0200 Subject: [PATCH 09/13] Return configured host ip if interface ip failed Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- lib/Ravada/VM.pm | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/Ravada/VM.pm b/lib/Ravada/VM.pm index 3b4998d64..6783aab65 100644 --- a/lib/Ravada/VM.pm +++ b/lib/Ravada/VM.pm @@ -917,11 +917,13 @@ will be returned if you connect from a different network than default. =cut sub interface_ip($self, $remote_ip=undef) { - my $key = '_interface_ip_'.($remote_ip or ''); + my $key = '_interface_ip_'.($remote_ip // ''); return $self->{$key} if exists $self->{$key}; my $ip = $self->_interface_ip($remote_ip); - $self->{$key}=$ip; + $ip = $self->ip() if !defined($ip) || $ip eq ''; + + $self->{$key} = $ip; return $ip; } From 8a841b42671eacecbff1a612edc3db65fd8daf4a Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Wed, 29 Jul 2026 13:12:47 +0200 Subject: [PATCH 10/13] wip: pass only remote ip when defined issue #2360 --- lib/Ravada/Domain.pm | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index e694247a6..a75e9f67b 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -6793,12 +6793,15 @@ sub _around_ip($orig, $self, @args) { if (!$self->readonly() && $self->list_ports()) { if ($ip && !$self->_data('ports_exposed')) { $self->_data('ports_exposed' => 1); + my @ip; + my $remote_ip = $self->remote_ip(); + @ip = ( remote_ip => $remote_ip ) if $remote_ip; my $req = Ravada::Request->open_exposed_ports( uid => Ravada::Utils::user_daemon->id ,id_domain => $self->id ,retry => 20 - ,remote_ip => $self->remote_ip() ,_force => 1 + ,@ip ); } if (!$ip && $self->_data('ports_exposed')) { From e47fda1e313e2101a6c28d2b9854873626737903 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Thu, 30 Jul 2026 10:47:03 +0200 Subject: [PATCH 11/13] wip: get bridge interface IP from Domain bridged ip issue #2360 Co-authored-by: Guillermo Miranda --- lib/Ravada/Domain.pm | 2 ++ lib/Ravada/VM.pm | 21 +++++++++++++++++++++ t/vm/93_ports_bridge.t | 9 +++++++-- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index a75e9f67b..f90c03506 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -3998,6 +3998,8 @@ sub _open_exposed_port($self, $internal_port, $name, $restricted, $remote_ip=und ); if ($internal_ip_info->{type} eq 'bridge') { + my $bridge_ip = ( $self->_vm->bridge_ip($internal_ip) or $local_ip); + my @iptables_arg = ("0.0.0.0/0" ,$internal_ip, 'nat', 'POSTROUTING', 'SNAT', ,{'protocol' => 'tcp' diff --git a/lib/Ravada/VM.pm b/lib/Ravada/VM.pm index 6783aab65..6767d68cf 100644 --- a/lib/Ravada/VM.pm +++ b/lib/Ravada/VM.pm @@ -927,6 +927,27 @@ sub interface_ip($self, $remote_ip=undef) { return $ip; } +=head2 bridge_ip + +Returns the Host IP from its bridged network interface that matches the machine IP + +Argument: The virtual machine IP + +=cut + +sub bridge_ip($self, $domain_ip) { + my $key = '_interface_ip_'.($domain_ip // ''); + return $self->{$key} if exists $self->{$key}; + + my ($out,$err)=$self->run_command("ip","r","get",$domain_ip); + my ($ip) = $out =~ /src (\d+\.\d+\.\d+\.\d+)/; + + $self->{$key} = $ip; + return $ip; +} + + + sub _list_ip_address($self) { my @cmd = ("ip","address","show"); my ($out, $err) = $self->run_command(@cmd); diff --git a/t/vm/93_ports_bridge.t b/t/vm/93_ports_bridge.t index 8ed7b141b..a8fce9432 100644 --- a/t/vm/93_ports_bridge.t +++ b/t/vm/93_ports_bridge.t @@ -47,7 +47,7 @@ sub _wait_ip($domain) { diag("Waiting for ".$domain->name. " ip") if !(time % 10); sleep 1; } - confess "Error : no ip for ".$domain->name; + confess "Error : no ip for ".$domain->name." Maybe set MAC in YAML file $FILE_CONFIG_BRIDGE"; } sub _set_mac_address($domain) { @@ -58,7 +58,12 @@ sub _set_mac_address($domain) { $dev->setAttribute('address' => $CONFIG_BRIDGE->{mac}); $domain->reload_config($doc); } elsif( $domain->type eq 'Void') { - return; + my $ip = $CONFIG_BRIDGE->{ip} + or die "Error: missing ip in $FILE_CONFIG_BRIDGE ".Dumper($CONFIG_BRIDGE); + + my $hardware = $domain->_value('hardware'); + $hardware->{network}->[0]->{address} = $ip; + $domain->_store('hardware' => $hardware); } else { die "I don't know how to set mac for ".$domain->type; } From 2ae3a5a839c95c371b16ae41527dacdc7d7a9154 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Thu, 30 Jul 2026 10:51:49 +0200 Subject: [PATCH 12/13] wip: use bridge IP for iptables rule issue #2360 --- lib/Ravada/Domain.pm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm index f90c03506..4d3077a94 100644 --- a/lib/Ravada/Domain.pm +++ b/lib/Ravada/Domain.pm @@ -4004,7 +4004,7 @@ sub _open_exposed_port($self, $internal_port, $name, $restricted, $remote_ip=und ,$internal_ip, 'nat', 'POSTROUTING', 'SNAT', ,{'protocol' => 'tcp' ,'d_port' => $internal_port - ,'to_source' => $local_ip + ,'to_source' => $bridge_ip }); $self->_log_iptable(iptables => \@iptables_arg @@ -4018,7 +4018,7 @@ sub _open_exposed_port($self, $internal_port, $name, $restricted, $remote_ip=und ,d => $internal_ip ,dport => $internal_port ,j => 'SNAT' - ,'to-source' => $local_ip + ,'to-source' => $bridge_ip ); } From 2e018c2aca852a51e1f7734eb35b8e7f0ad8f780 Mon Sep 17 00:00:00 2001 From: Francesc Guasch Date: Fri, 31 Jul 2026 14:36:45 +0200 Subject: [PATCH 13/13] Proper pass of out and err Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- t/vm/93_ports_bridge.t | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/t/vm/93_ports_bridge.t b/t/vm/93_ports_bridge.t index a8fce9432..616d2f934 100644 --- a/t/vm/93_ports_bridge.t +++ b/t/vm/93_ports_bridge.t @@ -246,7 +246,7 @@ sub test_bridge_nat($vm) { isnt($exposed_port->{public_port}, $internal_port) or exit; my ($in, $out, $err); - run3(['iptables','-t','nat','-L','PREROUTING','-n'],\($in, $out, $err)); + run3(['iptables','-t','nat','-L','PREROUTING','-n'], undef, \$out, \$err); die $err if $err; my @out = split /\n/,$out; is(grep(/^DNAT.*$interface_ip.*dpt:$public_port to:$internal_ip:$internal_port/,@out),1)