diff --git a/lib/Ravada.pm b/lib/Ravada.pm
index 9f416f54b..5078253fe 100644
--- a/lib/Ravada.pm
+++ b/lib/Ravada.pm
@@ -1641,6 +1641,12 @@ sub _add_indexes_generic($self) {
             ,"UNIQUE (name)"
 
         ]
+        ,virtual_networks => [
+            "unique(id_vm,internal_id)"
+            ,"unique(id_vm,name)"
+            ,"index(date_changed)"
+            ,"index(id_owner)"
+        ]
     );
     my $if_not_exists = '';
     $if_not_exists = ' IF NOT EXISTS ' if $CONNECTOR->dbh->{Driver}{Name} =~ /sqlite|mariadb/i;
@@ -1812,6 +1818,8 @@ sub _add_grants($self) {
     $self->_add_grant('view_all',0,"The user can start and access the screen of any virtual machine");
     $self->_add_grant('create_disk',0,'can create disk volumes');
     $self->_add_grant('quota_disk',0,'disk space limit',1);
+    $self->_add_grant('create_networks',0,'can create virtual networks.');
+    $self->_add_grant('manage_all_networks',0,'can manage all the virtual networks.');
 }
 
 sub _add_grant($self, $grant, $allowed, $description, $is_int = 0, $default_admin=1) {
@@ -1889,6 +1897,7 @@ sub _enable_grants($self) {
         ,'start_limit',     'start_many'
         ,'view_all'
         ,'create_disk', 'quota_disk'
+        ,'create_networks','manage_all_networks'
     );
 
     my $sth = $CONNECTOR->dbh->prepare("SELECT id,name FROM grant_types");
@@ -2362,6 +2371,24 @@ sub _sql_create_tables($self) {
 
             }
         ]
+        ,
+        [virtual_networks => {
+                id => 'integer PRIMARY KEY AUTO_INCREMENT',
+                ,id_vm => 'integer NOT NULL references `vms` (`id`) ON DELETE CASCADE',
+                ,name => 'varchar(200)'
+                ,id_owner => 'integer NOT NULL references `users` (`id`) ON DELETE CASCADE',
+                ,internal_id => 'char(80) not null'
+                ,autostart => 'integer not null'
+                ,bridge => 'char(80)'
+                ,'ip_address' => 'char(20)'
+                ,'ip_netmask' => 'char(20)'
+                ,'dhcp_start' => 'char(15)'
+                ,'dhcp_end' => 'char(15)'
+                ,'is_active' => 'integer not null default 1'
+                ,'is_public' => 'integer not null default 0'
+                ,date_changed => 'timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'
+            }
+        ]
 
     );
     for my $new_table (@tables ) {
@@ -5111,7 +5138,12 @@ sub _cmd_change_hardware {
     my $user = Ravada::Auth::SQL->search_by_id($uid);
 
     die "Error: User ".$user->name." not allowed\n"
-        if $hardware ne 'memory' && !$user->is_admin;
+    unless $user->is_admin
+    || $hardware eq 'memory'
+    || ($hardware eq 'network'
+        && $user->can_change_hardware_network($domain, $data)
+       )
+    ;
 
     $domain->change_hardware(
          $request->args('hardware')
@@ -5810,6 +5842,7 @@ sub _refresh_active_vms ($self) {
             next;
         }
         $active_vm{$vm->id} = 1;
+        $vm->list_virtual_networks();
     }
     return \%active_vm;
 }
@@ -6304,6 +6337,12 @@ sub _req_method {
     ,move_volume => \&_cmd_move_volume
     ,update_iso_urls => \&_cmd_update_iso_urls
 
+    ,list_networks => \&_cmd_list_virtual_networks
+    ,new_network => \&_cmd_new_network
+    ,create_network => \&_cmd_create_network
+    ,remove_network => \&_cmd_remove_network
+    ,change_network => \&_cmd_change_network
+
     );
     return $methods{$cmd};
 }
@@ -6723,6 +6762,106 @@ sub _cmd_remove_files($self, $request) {
     $vm->remove_file(@file);
 }
 
+sub _cmd_list_virtual_networks($self, $request) {
+    my $user=Ravada::Auth::SQL->search_by_id($request->args('uid'));
+    die "Error: ".$user->name." not authorized\n"
+    unless $user->is_admin || $user->can_manage_all_networks || $user->can_create_networks;
+
+    my $id = $request->args('id_vm') or die "Error: missing id_vm";
+    my $vm = Ravada::VM->open($id);
+    my @list = $vm->list_virtual_networks();
+
+    $request->output(encode_json(\@list));
+}
+
+
+sub _cmd_new_network($self, $request) {
+    my $user=Ravada::Auth::SQL->search_by_id($request->args('uid'));
+    die "Error: ".$user->name." not authorized\n"
+    unless $user->can_create_networks;
+
+    $request->output(encode_json({}));
+
+    my $id = $request->args('id_vm') or die "Error: missing id_vm";
+    my $vm = Ravada::VM->open($id);
+    my $name = ($request->defined_arg('name') or 'net');
+
+    my $new = $vm->new_network($name);
+    $new = {} if !$new;
+
+    $request->output(encode_json( $new));
+}
+
+sub _cmd_create_network($self, $request) {
+    my $user=Ravada::Auth::SQL->search_by_id($request->args('uid'));
+    die "Error: ".$user->name." not authorized\n"
+    unless $user->can_create_networks || $user->can_manage_all_networks;
+
+    my $id = $request->args('id_vm') or die "Error: missing id_vm";
+    my $vm = Ravada::VM->open($id);
+    $request->output(encode_json({}));
+    my $id_net = $vm->create_network($request->args('data'),$request->args('uid')
+                    , $request);
+    $request->output(encode_json({id_network => $id_net}));
+}
+
+sub _cmd_remove_network($self, $request) {
+
+    my $id = $request->args('id');
+    my $name = $request->defined_arg('name');
+    my $sth_net = $CONNECTOR->dbh->prepare(
+        "SELECT * FROM virtual_networks WHERE id=?"
+    );
+    $sth_net->execute($id);
+    my $network = $sth_net->fetchrow_hashref;
+    if  ($network && $network->{id} ) {
+        _check_user_authorized_network($request, $id);
+    }
+    my $id_vm = ( $network->{id_vm} or $request->defined_arg('id_vm') );
+    die "Error: unknown id_vm ".Dumper([$network,$request]) if !$id_vm;
+
+    die "Error: unkonwn network , id=$id, name='".($name or '')."' "
+    .Dumper($network) if ! $network->{id} && !$name;
+
+    my $user=Ravada::Auth::SQL->search_by_id($request->args('uid'));
+
+    my $vm = Ravada::VM->open($id_vm);
+    $vm->remove_network($user, ($network->{id} or $name));
+}
+
+sub _check_user_authorized_network($request, $id_network) {
+
+    my $user=Ravada::Auth::SQL->search_by_id($request->args('uid'));
+
+    my $sth = $CONNECTOR->dbh->prepare(
+        "SELECT * FROM virtual_networks WHERE id=?"
+    );
+    $sth->execute($id_network);
+    my $network = $sth->fetchrow_hashref;
+
+    confess "Error: network $id_network not found" if !$network->{id};
+
+    die "Error: ".$user->name." not authorized\n"
+    unless $user->is_admin
+    || $user->can_manage_all_networks
+    || ( $user->can_create_networks && $network->{id_owner} == $user->id);
+
+    return $network;
+}
+
+sub _cmd_change_network($self, $request) {
+
+    my $data = $request->args('data');
+    die "Error: network.id required" if !exists $data->{id} || !$data->{id};
+
+    my $network = _check_user_authorized_network($request, $data->{id});
+
+    $data->{internal_id} = $network->{internal_id} if !$data->{internal_id};
+    my $vm = Ravada::VM->open($network->{id_vm});
+
+    $vm->change_network($data, $request->args('uid'));
+}
+
 sub _cmd_active_storage_pool($self, $request) {
     my $user = Ravada::Auth::SQL->search_by_id($request->args('uid'));
     die "Error: ".$user->name." not authorized to manage storage pools"
diff --git a/lib/Ravada/Auth/SQL.pm b/lib/Ravada/Auth/SQL.pm
index fb39df37e..c9d80f6e2 100644
--- a/lib/Ravada/Auth/SQL.pm
+++ b/lib/Ravada/Auth/SQL.pm
@@ -378,6 +378,7 @@ sub is_operator {
             || $self->can_view_groups()
             || $self->can_manage_groups()
             || $self->can_view_all()
+            || $self->can_create_networks()
     ;
     return 0;
 }
@@ -635,9 +636,25 @@ sub remove($self) {
     my $sth = $$CON->dbh->prepare("DELETE FROM grants_user where id_user=?");
     $sth->execute($self->id);
 
+    $self->_remove_networks();
+
     $sth = $$CON->dbh->prepare("DELETE FROM users where id=?");
     $sth->execute($self->id);
     $sth->finish;
+
+}
+
+sub _remove_networks($self) {
+    my $sth = $$CON->dbh->prepare("SELECT id,id_vm,name FROM virtual_networks WHERE id_owner=?");
+    $sth->execute($self->id);
+    while (my ($id, $id_vm, $name) = $sth->fetchrow) {
+        Ravada::Request->remove_network(
+            uid => Ravada::Utils::user_daemon->id
+            ,id_vm => $id_vm
+            ,id => $id
+            ,name => $name
+        );
+    }
 }
 
 =head2 can_do
@@ -1235,6 +1252,46 @@ sub disk_used($self) {
     return $used;
 }
 
+sub _load_network($network) {
+    confess "Error: undefined network"
+    if !defined $network;
+
+    my $sth = $$CON->dbh->prepare(
+        "SELECT * FROM virtual_networks where name=?"
+    );
+    $sth->execute($network);
+    my $row = $sth->fetchrow_hashref;
+
+    die "Error: network '$network' not found"
+    if !$row->{id};
+
+    lock_hash(%$row);
+    return $row;
+}
+
+=head2  can_change_hardware_network
+
+Returns true if the user can change the network in a virtual machine,
+false elsewhere
+
+=cut
+
+sub can_change_hardware_network($user, $domain, $data) {
+    return 1 if $user->is_admin;
+    return 1 if $user->can_manage_all_networks()
+                && $domain->id_owner == $user->id;
+
+    confess "Error: undefined network ".Dumper($data)
+    if !exists $data->{network} || !defined $data->{network};
+
+    my $net = _load_network($data->{network});
+
+    return 1 if $user->id == $domain->id_owner
+        && ( $net->{is_public} || $user->id == $net->{id_owner});
+    return 0;
+}
+
+
 sub AUTOLOAD($self, $domain=undef) {
 
     my $name = $AUTOLOAD;
diff --git a/lib/Ravada/Domain.pm b/lib/Ravada/Domain.pm
index 03d2c252d..37e533f13 100644
--- a/lib/Ravada/Domain.pm
+++ b/lib/Ravada/Domain.pm
@@ -344,7 +344,7 @@ sub _around_start($orig, $self, @arg) {
                 if ( Ravada::setting(undef,"/backend/display_password") ) {
                     # We'll see if we set it from the network, defaults to 0 meanwhile
                     my $set_password = 0;
-                    my $network = Ravada::Network->new(address => $remote_ip);
+                    my $network = Ravada::Route->new(address => $remote_ip);
                     $set_password = 1 if $network->requires_password();
                     $arg{set_password} = $set_password;
                 }
diff --git a/lib/Ravada/Domain/Void.pm b/lib/Ravada/Domain/Void.pm
index 20f0b4303..7a07f59b2 100644
--- a/lib/Ravada/Domain/Void.pm
+++ b/lib/Ravada/Domain/Void.pm
@@ -758,6 +758,8 @@ sub _set_default_info($self, $listen_ip=undef) {
         hwaddr => $info->{mac}
         ,address => $info->{ip}
         ,type => 'nat'
+        ,driver => 'virtio'
+        ,name => "net1"
     };
     $self->_store(hardware => $hardware );
 
@@ -921,6 +923,18 @@ sub _internal_autostart {
     return $self->_value('autostart');
 }
 
+sub _new_network($self) {
+    my $hardware = $self->_value('hardware');
+    my $list = ( $hardware->{'network'} or [] );
+    my $data = {
+        hwaddr => _new_mac()
+        ,address => ''
+        ,type => 'nat'
+        ,driver => 'virtio'
+        ,name => "net".(scalar(@$list)+1)
+    };
+}
+
 sub set_controller($self, $name, $number=undef, $data=undef) {
     my $hardware = $self->_value('hardware');
 
@@ -939,12 +953,16 @@ sub set_controller($self, $name, $number=undef, $data=undef) {
     my @list2;
     if (!defined $number) {
         @list2 = @$list;
-        push @list2,($data or " $name z 1");
+        $data = $self->_new_network() if $name eq 'network' && !$data;
+        push @list2,($data or "$name z 1");
     } else {
         my $count = 0;
         for my $item ( @$list ) {
             $count++;
             if ($number == $count) {
+                $data = $self->_new_network()
+                if $name eq 'network' && (!$data || ! keys %$data);
+
                 my $data2 = ( $data or " $name a ".($count+1));
                 $data2 = " $name b ".($count+1) if defined $data2 && ref($data2) && !keys %$data2;
 
@@ -953,7 +971,7 @@ sub set_controller($self, $name, $number=undef, $data=undef) {
             }
             $item = { driver => 'spice' , port => 'auto' , listen_ip => $self->_vm->listen_ip }
             if $name eq 'display' && !defined $item;
-            push @list2,($item or " $name b ".($count+1));
+            push @list2,($item or " $name c ".($count+1));
         }
     }
     $hardware->{$name} = \@list2;
diff --git a/lib/Ravada/Front.pm b/lib/Ravada/Front.pm
index 945071d84..fd3e8639e 100644
--- a/lib/Ravada/Front.pm
+++ b/lib/Ravada/Front.pm
@@ -21,7 +21,7 @@ use Ravada;
 use Ravada::Auth::LDAP;
 use Ravada::Front::Domain;
 use Ravada::Front::Domain::KVM;
-use Ravada::Network;
+use Ravada::Route;
 
 use feature qw(signatures);
 no warnings "experimental::signatures";
@@ -1201,7 +1201,7 @@ sub list_bases_anonymous {
     my $self = shift;
     my $ip = shift or confess "Missing remote IP";
 
-    my $net = Ravada::Network->new(address => $ip);
+    my $net = Ravada::Route->new(address => $ip);
 
     my $sth = $CONNECTOR->dbh->prepare(
         "SELECT id, alias, name, id_base, is_public, file_screenshot "
@@ -1697,6 +1697,63 @@ sub list_storage_pools($self, $uid, $id_vm, $active=undef) {
     return _filter_active($pools, $active);
 }
 
+=head2 list_networks
+
+List the virtual networks for a Virtual Machine Manager
+
+Arguments: id vm , id user
+
+Returns: list ref of networks
+
+=cut
+
+sub list_networks($self, $id_vm ,$id_user) {
+    my $query = "SELECT * FROM virtual_networks "
+        ." WHERE id_vm=?";
+
+    my $user = Ravada::Auth::SQL->search_by_id($id_user);
+    my $owned = 0;
+    unless ($user->is_admin || $user->can_manage_all_networks) {
+        $query .= " AND ( id_owner=? or is_public=1) ";
+        $owned = 1;
+    }
+    $query .= " ORDER BY name";
+    my $sth = $CONNECTOR->dbh->prepare($query);
+    if ($owned) {
+        $sth->execute($id_vm, $id_user);
+    } else {
+        $sth->execute($id_vm);
+    }
+    my @networks;
+    my %owner;
+    while ( my $row = $sth->fetchrow_hashref ) {
+        $self->_search_user($row->{id_owner},\%owner);
+        $row->{_owner} = $owner{$row->{id_owner}};
+        $row->{_can_change}=0;
+
+        $row->{_can_change}=1
+        if $user->is_admin || $user->can_manage_all_networks
+        || ($user->can_create_networks && $user->id == $row->{id_owner});
+
+        push @networks,($row);
+    }
+    return \@networks;
+}
+
+sub _search_user($self,$id, $users) {
+    return if $users->{$id};
+
+    my $sth = $self->_dbh->prepare(
+        "SELECT * FROM users WHERE id=?"
+    );
+    $sth->execute($id);
+    my $row = $sth->fetchrow_hashref();
+    for my $field (keys %$row) {
+        delete $row->{$field} if $field =~ /passw/;
+    }
+    $users->{$id}=$row;
+}
+
 sub _filter_active($pools, $active) {
     return $pools if !defined $active;
 
diff --git a/lib/Ravada/Front/Domain/Void.pm b/lib/Ravada/Front/Domain/Void.pm
index beff8f98b..95440df67 100644
--- a/lib/Ravada/Front/Domain/Void.pm
+++ b/lib/Ravada/Front/Domain/Void.pm
@@ -13,6 +13,7 @@ our %GET_CONTROLLER_SUB = (
     'mock' => \&_get_controller_mock
     ,'disk' => \&_get_controller_disk
     ,'display' => \&_get_controller_display
+    ,'network' => \&_get_controller_network
 
 );
 
@@ -86,4 +87,14 @@ sub _get_controller_display(@args) {
     return Ravada::Front::Domain::_get_controller_display(@args);
 }
 
+sub _get_controller_generic($self, $item) {
+    my $hardware = $self->_value('hardware');
+    return () if !exists $hardware->{$item};
+    return @{$hardware->{$item}};
+}
+
+sub _get_controller_network($self) {
+    return $self->_get_controller_generic('network');
+}
+
 1;
diff --git a/lib/Ravada/Request.pm b/lib/Ravada/Request.pm
index c215b80f2..4040031bb 100644
--- a/lib/Ravada/Request.pm
+++ b/lib/Ravada/Request.pm
@@ -167,6 +167,13 @@ our %VALID_ARG = (
     ,list_unused_volumes => {uid => 1, id_vm => 1, start => 2, limit => 2 }
     ,remove_files => { uid => 1, id_vm => 1, files => 1 }
     ,move_volume => { uid => 1, id_domain => 1, volume => 1, storage => 1 }
+    
+    ,list_networks => { uid => 1, id_vm => 1}
+    ,new_network => { uid => 1, id_vm => 1, name => 2 }
+    ,create_network => { uid => 1, id_vm => 1, data => 1 }
+    ,remove_network => { uid => 1, id => 1, id_vm => 2, name => 2 }
+    ,change_network => { uid => 1, data => 1 }
+
 );
 
 $VALID_ARG{shutdown} = $VALID_ARG{shutdown_domain};
@@ -183,6 +190,8 @@ our %CMD_SEND_MESSAGE = map { $_ => 1 }
             shutdown_node reboot_node start_node
             compact purge
             start_domain
+
+            create_network change_network remove_network
     );
 
 our %CMD_NO_DUPLICATE = map { $_ => 1 }
diff --git a/lib/Ravada/Network.pm b/lib/Ravada/Route.pm
similarity index 96%
rename from lib/Ravada/Network.pm
rename to lib/Ravada/Route.pm
index 129eec17f..10d4e14c0 100644
--- a/lib/Ravada/Network.pm
+++ b/lib/Ravada/Route.pm
@@ -1,11 +1,11 @@
-package Ravada::Network;
+package Ravada::Route;
 
 use strict;
 use warnings;
 
 =head1 NAME
 
-Ravada::Network - Networks management library for Ravada
+Ravada::Route - Routes management library for Ravada
 
 =cut
 
@@ -31,7 +31,7 @@ our $CONNECTOR;
 
 =head1 Description
 
-    my $net = Ravada::Network->new(address => '127.0.0.1/32');
+    my $net = Ravada::Route->new(address => '127.0.0.1/32');
     if ( $net->allowed( $domain->id ) ) {
 
 =cut
diff --git a/lib/Ravada/VM.pm b/lib/Ravada/VM.pm
index eee2ebd54..7bd1d023c 100644
--- a/lib/Ravada/VM.pm
+++ b/lib/Ravada/VM.pm
@@ -25,6 +25,7 @@ use Net::OpenSSH;
 use IO::Socket;
 use IO::Interface;
 use Net::Domain qw(hostfqdn);
+use Storable qw(dclone);
 
 use Ravada::HostDevice;
 use Ravada::Utils;
@@ -129,6 +130,13 @@ has 'netssh' => (
     , lazy => 1
     , clearer => 'clear_netssh'
 );
+
+has 'has_networking' => (
+    isa => 'Bool'
+    , is => 'ro'
+    , default => 0
+);
+
 ############################################################
 #
 # Method Modifiers definition
@@ -147,6 +155,12 @@ around 'ping' => \&_around_ping;
 around 'connect' => \&_around_connect;
 after 'disconnect' => \&_post_disconnect;
 
+around 'new_network' => \&_around_new_network;
+around 'create_network' => \&_around_create_network;
+around 'remove_network' => \&_around_remove_network;
+around 'list_virtual_networks' => \&_around_list_networks;
+around 'change_network' => \&_around_change_network;
+
 around 'copy_file_storage' => \&_around_copy_file_storage;
 
 #############################################################
@@ -191,6 +205,8 @@ sub open {
     } else {
         $args{id} = shift;
     }
+    confess "Error: undefind id in ".Dumper(\%args) if !$args{id};
+
     my $class=ref($proto) || $proto;
 
     my $self = {};
@@ -606,7 +622,7 @@ sub _add_instance_db($self, $id_domain) {
 sub _define_spice_password($self, $remote_ip) {
     my $spice_password = Ravada::Utils::random_name(4);
     if ($remote_ip) {
-        my $network = Ravada::Network->new(address => $remote_ip);
+        my $network = Ravada::Route->new(address => $remote_ip);
         $spice_password = undef if !$network->requires_password;
     }
     return $spice_password;
@@ -1433,6 +1449,183 @@ sub _around_ping($orig, $self, $option=undef, $cache=1) {
     return $ping;
 }
 
+sub _insert_network($self, $net) {
+    delete $net->{id};
+    $net->{id_owner} = Ravada::Utils::user_daemon->id
+    if !exists $net->{id_owner};
+
+    $net->{id_vm} = $self->id;
+    $net->{is_public}=1 if !exists $net->{is_public};
+
+    my @fields = grep !/^_/, sort keys %$net;
+
+    my $sql = "INSERT INTO virtual_networks ("
+    .join(",",@fields).")"
+    ." VALUES(".join(",",map { '?' } @fields).")";
+    my $sth = $self->_dbh->prepare($sql);
+    $sth->execute(map { $net->{$_} } @fields);
+
+    $net->{id} = Ravada::Utils::last_insert_id($$CONNECTOR->dbh);
+}
+
+sub _update_network($self, $net) {
+    my $sth = $self->_dbh->prepare("SELECT * FROM virtual_networks "
+        ." WHERE id_vm=? AND ( internal_id=? OR name = ?)"
+    );
+    $sth->execute($self->id,$net->{internal_id}, $net->{name});
+    my $db_net = $sth->fetchrow_hashref();
+    if (!$db_net || !$db_net->{id}) {
+        $self->_insert_network($net);
+    } else {
+        $self->_update_network_db($db_net, $net);
+        $net->{id}  = $db_net->{id};
+    }
+    delete $db_net->{date_changed};
+    #    delete $db_net->{id};
+}
+
+sub _update_network_db($self, $old, $new0) {
+    my $new = dclone($new0);
+    my $id = $old->{id} or confess "Missing id";
+    my $sql = "";
+    for my $field (sort keys %$new) {
+        if (    exists $old->{$field} && defined $old->{$field}
+            &&  exists $new->{$field} && defined $new->{$field}
+            && $new->{$field} eq $old->{$field}) {
+            delete $new->{$field};
+            next;
+        }
+        $sql .= ", " if $sql;
+        $sql .=" $field=? "
+    }
+    if ($sql) {
+        $sql = "UPDATE virtual_networks set $sql WHERE id=?";
+        my $sth = $self->_dbh->prepare($sql);
+        my @values = map { $new->{$_} } sort keys %$new;
+        $sth->execute(@values, $id);
+    }
+}
+
+sub _around_new_network($orig, $self, $name) {
+    my $data = $self->$orig($name);
+    $data->{id_vm} = $self->id;
+    $data->{is_active}=1;
+    $data->{autostart}=1;
+    return $data;
+}
+
+sub _around_create_network($orig, $self,$data, $id_owner, $request=undef) {
+    my $owner = Ravada::Auth::SQL->search_by_id($id_owner) or confess "Unknown user id: $id_owner";
+    die "Error: Access denied: ".$owner->name." can not create networks"
+    unless $owner->can_create_networks();
+
+    for my $field (qw(name bridge)) {
+        next if !$data->{$field};
+        my ($found) = grep { $_->{$field} eq $data->{$field} }
+        $self->list_virtual_networks();
+
+        die "Error: network $field=$data->{$field} already exists\n"
+        if $found;
+    }
+
+    $data->{is_active}=0 if !exists $data->{is_active};
+    $data->{autostart}=0 if !exists $data->{autostart};
+    my $ip = $data->{ip_address} or die "Error: missing ip address";
+    $ip =~ s/(.*)\..*/$1/;
+    $data->{dhcp_start}="$ip.2" if !exists $data->{dhcp_start};
+    $data->{dhcp_end}="$ip.254" if !exists $data->{dhcp_end};
+    delete $data->{_update};
+    delete $data->{internal_id} if exists $data->{internal_id};
+    eval { $self->$orig($data) };
+    $request->error(''.$@) if $@ && $request;
+    $data->{id_owner} = $id_owner;
+    $data->{is_public} = 0 if !$data->{is_public};
+    $self->_insert_network($data);
+}
+
+sub _around_remove_network($orig, $self, $user, $id_net) {
+    die "Error: Access denied: ".$user->name." can not remove networks"
+    unless $user->can_create_networks();
+
+    confess "Error: undefined network" if !defined $id_net;
+
+    my $name = $id_net;
+    if ($id_net =~ /^\d+$/) {
+        my ($net) = grep { $_->{id} eq $id_net } $self->list_virtual_networks();
+        die "Error: network id $id_net not found" if !$net;
+        $name = $net->{name};
+    }
+
+
+    $self->$orig($name);
+
+    my $sth = $self->_dbh->prepare("DELETE FROM virtual_networks WHERE id=?");
+    $sth->execute($id_net);
+}
+
+sub _around_change_network($orig, $self, $data, $uid) {
+    delete $data->{date_changed};
+
+    for my $field (keys %$data) {
+        delete $data->{$field} if $field =~ /^_/;
+    }
+
+    my $data2 = dclone($data);
+
+    my $id_owner = delete $data->{id_owner};
+
+    my $id = delete $data2->{id};
+    my $sth0 = $self->_dbh->prepare("SELECT * FROM virtual_networks WHERE id=?");
+    $sth0->execute($id);
+    my $old = $sth0->fetchrow_hashref;
+
+    # this field is only in DB
+    delete $data->{is_public};
+    my $changed = $orig->($self, $data);
+
+    my $user=Ravada::Auth::SQL->search_by_id($uid);
+
+    $self->_update_network_db($old, $data2);
+
+}
+
+sub _check_networks($self) { }
+
+sub _around_list_networks($orig, $self) {
+    my $sth_previous = $self->_dbh->prepare(
+        "SELECT count(*) FROM virtual_networks "
+        ." WHERE id_vm=?"
+    );
+    $sth_previous->execute($self->id);
+    my ($count) = $sth_previous->fetchrow;
+    my $first_time = !$count;
+
+    my @list = $orig->($self);
+    for my $net ( @list ) {
+        $net->{id_vm}=$self->id;
+        $self->_update_network($net);
+    }
+    my $sth = $self->_dbh->prepare(
+        "SELECT id,name,id_owner,is_public FROM virtual_networks "
+        ." WHERE id_vm=?");
+    my $sth_delete = $self->_dbh->prepare("DELETE FROM virtual_networks where id=?");
+    $sth->execute($self->id);
+    while (my ($id, $name, $id_owner, $is_public) = $sth->fetchrow) {
+        my ($found) = grep {$_->{name} eq $name } @list;
+        if ( $found ) {
+            $found->{id_owner} = $id_owner;
+            $found->{id} = $id;
+            $found->{is_public} = $is_public;
+            next;
+        }
+        $sth_delete->execute($id);
+    }
+
+    $self->_check_networks() if $first_time;
+
+    return @list;
+}
+
 =head2 is_active
 
 Returns if the domain is active. The active state is cached for some seconds.
@@ -2343,36 +2536,20 @@ sub list_network_interfaces($self, $type) {
 
 sub _list_nat_interfaces($self) {
 
-    my @cmd = ( '/usr/bin/virsh','net-list');
-    my ($out,$err) = $self->run_command(@cmd);
-
-    my @lines = split /\n/,$out;
-    shift @lines;
-    shift @lines;
-
+    my @nets = $self->list_virtual_networks();
     my @networks;
-    for (@lines) {
-        /\s*(.*?)\s+.*/;
-        push @networks,($1) if $1;
+    for my $net (@nets) {
+        push @networks,($net->{name}) if $net->{name};
     }
     return @networks;
 }
 
-sub _get_nat_bridge($self, $net) {
-    my @cmd = ( '/usr/bin/virsh','net-info', $net);
-    my ($out,$err) = $self->run_command(@cmd);
-
-    for my $line (split /\n/, $out) {
-        my ($bridge) = $line =~ /^Bridge:\s+(.*)/;
-        return $bridge if $bridge;
-    }
-}
-
 sub _list_qemu_bridges($self) {
     my %bridge;
-    my @networks = $self->_list_nat_interfaces();
+    my @networks = $self->list_virtual_networks();
     for my $net (@networks) {
-        my $nat_bridge = $self->_get_nat_bridge($net);
+        next if !$net->{bridge};
+        my $nat_bridge = $net->{bridge};
         $bridge{$nat_bridge}++;
     }
     return keys %bridge;
@@ -2680,6 +2857,39 @@ sub _around_copy_file_storage($orig, $self, $file, $storage) {
     return $new_file;
 }
 
+sub _list_files_local($self, $dir, $pattern) {
+    opendir my $ls,$dir or die "$! $dir";
+    my @list;
+    while (my $file = readdir $ls) {
+        next if defined $pattern && $file !~ $pattern;
+        push @list,($file);
+    }
+    return @list;
+}
+
+sub _list_files_remote($self, $dir, $pattern) {
+    my @cmd=("ls",$dir);
+    my ($out, $err) = $self->run_command(@cmd);
+    my @list;
+    for my $file (split /\n/,$out) {
+        push @list,($file) if !defined $pattern || $file =~ $pattern;
+    }
+    return @list;
+}
+
+=head2 list_files
+
+List files in a Virtual Manager
+
+Arguments: dir , optional regexp pattern
+
+=cut
+
+sub list_files($self, $dir, $pattern=undef) {
+    return $self->_list_files_local($dir, $pattern) if $self->is_local;
+    return $self->_list_files_remote($dir, $pattern);
+}
+
 1;
 
 
diff --git a/lib/Ravada/VM/KVM.pm b/lib/Ravada/VM/KVM.pm
index c46d02ced..ff0dbe70f 100644
--- a/lib/Ravada/VM/KVM.pm
+++ b/lib/Ravada/VM/KVM.pm
@@ -55,6 +55,12 @@ has type => (
     ,default => 'KVM'
 );
 
+has has_networking => (
+    isa => 'Bool'
+    , is => 'ro'
+    , default => 1
+);
+
 #########################################################################3
 #
 
@@ -119,7 +125,6 @@ sub _connect {
          };
          confess $@ if $@;
     }
-    $self->_check_networks($vm);
     return $vm;
 }
 
@@ -143,17 +148,22 @@ sub _check_default_storage($self) {
     }
 }
 
-sub _check_networks {
-    my $self = shift;
-    my $vm = shift;
+sub _check_networks($self, $vm=$self->vm) {
 
+    my @found;
     for my $net ($vm->list_all_networks) {
-        next if $net->is_active;
-
-        warn "INFO: Activating KVM network ".$net->get_name."\n";
-        $net->create;
-        $net->set_autostart(1);
+        return if $net->is_active;
+        push @found,($net);
     }
+
+    my ($net) = grep {$_->get_name eq 'default' } @found;
+
+    $net = $found[0] if !$net;
+
+    return if !$net;
+    warn "INFO: Activating KVM network ".$net->get_name."\n";
+    $net->create;
+    $net->set_autostart(1);
 }
 
 =head2 disconnect
@@ -1829,6 +1839,7 @@ sub _xml_modify_options($self, $doc, $options=undef) {
     my $machine = delete $options->{machine};
     my $arch = delete $options->{arch};
     my $bios = delete $options->{'bios'};
+    my $network = delete $options->{'network'};
 
     confess "Error: bios=$bios and uefi=$uefi clash"
     if defined $uefi && defined $bios
@@ -1863,7 +1874,12 @@ sub _xml_modify_options($self, $doc, $options=undef) {
     } else {
         $self->_xml_set_pci_noe($doc);
     }
+    $self->_xml_set_network($doc, $network) if $network;
+}
 
+sub _xml_set_network($self, $doc, $network) {
+    my ($net_source) = $doc->findnodes('/domain/devices/interface/source');
+    $net_source->setAttribute('network' => $network);
 }
 
 sub _xml_set_arch($self, $doc, $arch) {
@@ -2664,13 +2680,13 @@ sub _xml_add_graphics_streaming {
     $listen->setAttribute(mode => 'filter');
 }
 
-=head2 list_networks
+=head2 list_routes
 
 Returns a list of networks known to this VM. Each element is a Ravada::NetInterface object
 
 =cut
 
-sub list_networks {
+sub list_routes {
     my $self = shift;
 
     $self->connect() if !$self->vm;
@@ -2959,4 +2975,202 @@ sub get_library_version($self) {
     return $self->vm->get_library_version();
 }
 
+sub list_virtual_networks($self) {
+    my @networks;
+    for my $net ($self->vm->list_all_networks()) {
+        my $doc = XML::LibXML->load_xml(string => $net->get_xml_description);
+        my ($ip_doc) = $doc->findnodes("/network/ip");
+        my $ip = $ip_doc->getAttribute('address');
+        my $netmask = $ip_doc->getAttribute('netmask');
+        my $data= {
+            is_active => $net->is_active()
+            ,autostart => $net->get_autostart()
+            ,bridge => $net->get_bridge_name()
+            ,id_vm => $self->id
+            ,name => $net->get_name
+            ,ip_address => $ip
+            ,ip_netmask => $netmask
+            ,internal_id => ''.$net->get_uuid_string
+        };
+        my ($dhcp_range) = $ip_doc->findnodes("dhcp/range");
+        my ($start,$end);
+        if ($dhcp_range) {
+            $start = $dhcp_range->getAttribute('start');
+            $end = $dhcp_range->getAttribute('end');
+            $data->{dhcp_start} = $start if defined $start;
+            $data->{dhcp_end} = $end if defined $end;
+        }
+        push @networks,($data);
+    }
+    return @networks;
+}
+
+sub new_network($self, $name='net') {
+    my @networks = $self->list_virtual_networks();
+
+    my %base = (
+        name => $name
+        ,ip_address => ['192.168.',122,'.1']
+        ,bridge => 'virbr'
+    );
+    my $new = {ip_netmask => '255.255.255.0'};
+    for my $field ( keys %base) {
+        my %old = map { $_->{$field} => 1 } @networks;
+        my ($last) = reverse sort keys %old;
+        my ($z,$n) = $last =~ /.*?(0*)(\d+)/;
+        $z=$last if !defined $z;
+        $n=0 if !defined $n;
+        $n++;
+        $n = "$z$n";
+
+        my $template = $base{$field};
+        if (ref($template)) {
+            $n=$template->[1];
+        }
+        my $value;
+        for ( 0 .. 255 ) {
+            if (ref($template)) {
+                $value = $template->[0].$n.$template->[2]
+            } else {
+                $value = $template.$n;
+            }
+            last if !exists $old{$value};
+            $n++;
+        }
+        $new->{$field} = $value;
+    }
+    return $new;
+}
+
+sub create_network($self, $data) {
+
+    confess if !$data->{name} || !$data->{ip_address};
+
+    my $xml = XML::LibXML->load_xml(string =>
+        "<network><name>$data->{name}</name></network>"
+    );
+    my ($xml_net) = $xml->findnodes("/network");
+
+    my $forward = $xml_net->addNewChild(undef,'forward');
+    $forward->setAttribute('mode' => 'nat');
+
+    my $ip = $xml_net->addNewChild(undef,'ip');
+    $ip->setAttribute('address' => $data->{ip_address});
+    $ip->setAttribute('netmask' => $data->{ip_netmask});
+
+    if ($data->{dhcp_start} || $data->{dhcp_end}) {
+        my $dhcp = $ip->addNewChild(undef, 'dhcp');
+        my $range = $dhcp->addNewChild(undef,'range');
+        $range->setAttribute('start' => $data->{dhcp_start});
+        $range->setAttribute('end' => $data->{dhcp_end});
+    }
+
+    if ($data->{bridge}) {
+        my ($bridge) = $xml_net->addNewChild(undef,'bridge');
+        $bridge->setAttribute(name => $data->{bridge});
+    }
+
+    $self->vm->define_network($xml->toString());
+
+    my $new_network=$self->vm->get_network_by_name($data->{name});
+    my $xml2=XML::LibXML->load_xml(string =>$new_network->get_xml_description());
+    my ($uuid) = $xml2->findnodes("/network/uuid/text()");
+    $data->{internal_id} = ''.$uuid;
+
+    $new_network->create() if $data->{is_active};
+
+    $new_network->set_autostart($data->{autostart});
+
+    $data->{is_active} = $new_network->is_active;
+
+    return { internal_uid => ''.$uuid };
+}
+
+sub remove_network($self, $name) {
+    my $net = $self->vm->get_network_by_name($name);
+
+    $net->destroy() if $net->is_active;
+    $net->undefine();
+}
+
+sub change_network($self, $data) {
+    my $network = $self->vm->get_network_by_uuid($data->{internal_id});
+    die "Error: Unknown network $data->{name}" if !$network;
+
+    my $doc = XML::LibXML->load_xml(string => $network->get_xml_description);
+    my $changed = 0;
+    my $bridge = delete $data->{bridge};
+    if (defined $bridge) {
+        my ($bridge_xml) = $doc->findnodes("/network/bridge");
+        if (!defined $bridge_xml->getAttribute('name')
+            || $bridge_xml->getAttribute('name') ne $bridge) {
+            $bridge_xml->setAttribute(name => $bridge);
+            $changed++;
+        }
+    }
+    my $dhcp_start = delete $data->{dhcp_start};
+    my $dhcp_end = delete $data->{dhcp_end};
+    my ($dhcp_xml) = $doc->findnodes("/network/ip/dhcp/range");
+    if ($dhcp_xml->getAttribute('start') ne $dhcp_start) {
+        $dhcp_xml->setAttribute('start' => $dhcp_start);
+        $changed++;
+    }
+    if ($dhcp_xml->getAttribute('end') ne $dhcp_end) {
+        $dhcp_xml->setAttribute('end' => $dhcp_end);
+        $changed++;
+    }
+    my $ip_address = delete $data->{ip_address};
+    my ($ip_xml) = $doc->findnodes("/network/ip");
+    if ($ip_address ne $ip_xml->getAttribute('address')) {
+        $ip_xml->setAttribute('address' => $ip_address);
+        $changed++;
+    }
+    my $ip_netmask = delete $data->{ip_netmask};
+    if ($ip_netmask ne $ip_xml->getAttribute('netmask')) {
+        $ip_xml->setAttribute('netmask' => $ip_netmask);
+        $changed++;
+    }
+    my $name = delete $data->{name};
+    my ($name_xml) = $doc->findnodes("/network/name/text()");
+    if (''.$name_xml ne $name) {
+        die "Error: networks can not be renamed";
+    }
+
+    if ($changed) {
+        $network->destroy() if $network->is_active;
+        $network= $self->vm->define_network($doc->toString);
+    }
+
+    my $is_active = delete $data->{is_active};
+    if (defined $is_active) {
+        if ($is_active && ! $network->is_active) {
+            $network->create();
+            $changed++;
+        }
+        if (!$is_active && $network->is_active) {
+            $network->destroy;
+            $changed++;
+        }
+    }
+
+    my $autostart = delete $data->{autostart};
+    if (defined $autostart) {
+        if ($autostart && ! $network->get_autostart) {
+            $network->set_autostart(1);
+            $changed++;
+        }
+        if (!$autostart && $network->get_autostart) {
+            $network->set_autostart(0);
+            $changed++;
+        }
+    }
+
+    for ('id_vm','internal_id','id' ,'_old_name', 'date_changed') {
+        delete $data->{$_};
+    }
+    die "Error: unexpected args ".Dumper($data) if keys %$data;
+
+    return $changed;
+}
+
 1;
diff --git a/lib/Ravada/VM/Void.pm b/lib/Ravada/VM/Void.pm
index d05269aa7..5e562c56a 100644
--- a/lib/Ravada/VM/Void.pm
+++ b/lib/Ravada/VM/Void.pm
@@ -11,7 +11,7 @@ use Moose;
 use Socket qw( inet_aton inet_ntoa );
 use Sys::Hostname;
 use URI;
-use YAML qw(Dump LoadFile);
+use YAML qw(Dump Load);
 
 use Ravada::Domain::Void;
 use Ravada::NetInterface::Void;
@@ -34,6 +34,12 @@ has 'vm' => (
     ,lazy => 1
 );
 
+has has_networking => (
+    isa => 'Bool'
+    , is => 'ro'
+    , default => 1
+);
+
 our $CONNECTOR = \$Ravada::CONNECTOR;
 
 ##########################################################################
@@ -336,10 +342,129 @@ sub search_domain {
     return;
 }
 
-sub list_networks {
+sub list_routes {
     return Ravada::NetInterface::Void->new();
 }
 
+sub list_virtual_networks($self) {
+
+    my $dir_net = $self->dir_img."/networks/";
+    if (!$self->file_exists($dir_net)) {
+        my ($out, $err) = $self->run_command("mkdir", $dir_net);
+        die $err if $err;
+    }
+    my @files = $self->list_files($dir_net,qr/.yml$/);
+    my @list;
+    for my $file(@files) {
+        my $net;
+        eval { $net = Load($self->read_file("$dir_net/$file")) };
+        confess $@ if $@;
+
+        $net->{id_vm} = $self->id if !$net->{id_vm};
+        $net->{is_active}=0 if !defined $net->{is_active};
+        push @list,($net);
+    }
+    if (!@list) {
+        my $net = {name => 'default'
+            , autostart => 1
+            , internal_id => 1
+            , bridge => 'voidbr0'
+            ,ip_address => '192.51.100.1'
+            ,is_active => 1
+        };
+
+        my $file_out = $self->dir_img."/networks/".$net->{name}.".yml";
+        $self->write_file($file_out,Dump($net));
+        push @list,($net);
+    }
+    return @list;
+}
+
+sub _new_net_id($self) {
+    my %id;
+    for my $net ( $self->list_virtual_networks() ) {
+        $id{$net->{internal_id}}++;
+    }
+    my $new_id = 0;
+    for (;;) {
+        return $new_id if !exists $id{++$new_id};
+    }
+}
+
+sub _new_net_bridge ($self) {
+    my %bridge;
+    my $n = 0;
+    for my $net ( $self->list_virtual_networks() ) {
+        $bridge{$net->{bridge}}++;
+    }
+    my $new_id = 0;
+    for (;;) {
+        my $new_bridge = 'voidbr'.$new_id;
+        return $new_bridge if !exists $bridge{$new_bridge};
+        $new_id++;
+    }
+}
+
+sub new_network($self, $name='net') {
+
+    my @networks = $self->list_virtual_networks();
+
+    my %base = (
+        name => $name
+        ,ip_address => ['192.168.','.0']
+        ,bridge => 'voidbr'
+    );
+    my $new = {ip_netmask => '255.255.255.0'};
+    for my $field ( keys %base) {
+        my %old = map { $_->{$field} => 1 } @networks;
+        my $n = 0;
+        my $base = ($base{$field} or $field);
+        my $value;
+        for ( 0 .. 255 ) {
+            if (ref($base)) {
+                $value = $base->[0].$n.$base->[1]
+            } else {
+                $value = $base.$n;
+            }
+            last if !$old{$value};
+            $n++;
+        }
+        $new->{$field} = $value;
+    }
+    return $new;
+}
+
+sub create_network($self, $data, $id_owner=undef, $request=undef) {
+
+    $data->{internal_id} = $self->_new_net_id();
+    my $file_out = $self->dir_img."/networks/".$data->{name}.".yml";
+    die "Error: network $data->{name} already created"
+    if $self->file_exists($file_out);
+
+    $data->{bridge} = $self->_new_net_bridge()
+    if !exists $data->{bridge} || ! defined $data->{bridge};
+
+    delete $data->{is_public};
+
+    for my $field ('bridge','ip_address') {
+        $self->_check_duplicated_network($field,$data);
+    }
+
+    delete $data->{is_public};
+    delete $data->{id};
+    delete $data->{id_vm};
+
+    $self->write_file($file_out,Dump($data));
+
+    return $data;
+}
+
+sub remove_network($self, $name) {
+    my $file_out = $self->dir_img."/networks/$name.yml";
+    unlink $file_out or die "$! $file_out" if $self->file_exists($file_out);
+}
+
+
 sub search_volume($self, $pattern) {
 
     return $self->_search_volume_remote($pattern)   if !$self->is_local;
@@ -470,7 +595,7 @@ sub _init_storage_pool_default($self) {
     my $config_dir = Ravada::Front::Domain::Void::_config_dir();
     my $file_sp = "$config_dir/.storage_pools.yml";
 
-    return if -e $file_sp;
+    return if $self->file_exists($file_sp);
 
     my @list = ({ name => 'default', path => $config_dir, is_active => 1 });
 
@@ -504,7 +629,7 @@ sub list_storage_pools($self, $info=0) {
     $self->_init_storage_pool_default();
 
     my $file_sp = "$config_dir/.storage_pools.yml";
-    my $extra= LoadFile($file_sp);
+    my $extra= Load($self->read_file($file_sp));
     push @list,(@$extra) if $extra;
 
     my ($default) = grep { $_->{name} eq 'default'} @list;
@@ -668,11 +793,65 @@ sub active_storage_pool($self, $name, $value) {
     $self->write_file($file_sp, Dump( \@list));
 }
 
+sub has_networking { return 1 };
+
+sub _check_duplicated_network($self, $field, $data) {
+    my @networks = $self->list_virtual_networks();
+    my ($found) = grep {$data->{name} ne $_->{name}
+        && $_->{$field} eq $data->{$field} } @networks;
+    return if !$found;
+
+    $field = 'Network' if $field eq 'ip_address';
+    die "Error: $field is already in use in $found->{name}";
+}
+
+sub change_network($self,$data) {
+    my $id = delete $data->{internal_id} or confess "Missing internal_id ".Dumper($data);
+    confess if exists $data->{is_public};
+
+    my @networks = $self->list_virtual_networks();
+    my ($net0) = grep { $_->{internal_id} eq $id } @networks;
+
+    my $file_out = $self->dir_img."/networks/".$net0->{name}.".yml";
+    my $net= {};
+    eval { $net = Load($self->read_file($file_out)) };
+    confess $@ if $@;
+
+    my $changed = 0;
+    for my $field ('name', sort keys %$data) {
+        next if $field =~ /^_/ || $field eq 'is_public';
+        if (!exists $net->{$field}) {
+            $net->{$field} = $data->{$field};
+            $changed++;
+            next;
+        }
+        next if exists $data->{$field} && exists $net->{$field}
+        && defined $data->{$field} && defined $net->{$field}
+        && $data->{$field} eq $net->{$field};
+
+        if ($field eq 'name') {
+            die "Error: network can not be renamed";
+        }
+
+        if ($field eq 'bridge' || $field eq 'ip_address') {
+            $self->_check_duplicated_network($field,$data);
+        }
+        $net->{$field} = $data->{$field};
+        $changed++;
+    }
+    return if !$changed;
+
+    delete $net->{is_public};
+    delete $net->{id};
+    delete $net->{id_vm};
+
+    $self->write_file($file_out,Dump($net));
+}
+
 sub remove_storage_pool($self, $name) {
-    die "TODO remote VM" unless $self->is_local;
 
     my $file_sp = $self->dir_img."/.storage_pools.yml";
-    my $sp_list = LoadFile($file_sp);
+    my $sp_list = Load($self->read_file($file_sp));
     my @sp2;
     for my $sp (@$sp_list) {
         push @sp2,($sp) if $sp->{name} ne $name;
diff --git a/lib/Ravada/WebSocket.pm b/lib/Ravada/WebSocket.pm
index f9b83f239..96ae5a663 100644
--- a/lib/Ravada/WebSocket.pm
+++ b/lib/Ravada/WebSocket.pm
@@ -49,6 +49,7 @@ my %SUB = (
                  ,list_next_bookings_today => \&_list_next_bookings_today
 
                  ,log_active_domains => \&_log_active_domains
+                 ,list_networks => \&_list_networks
 );
 
 our %TABLE_CHANNEL = (
@@ -60,6 +61,7 @@ our %TABLE_CHANNEL = (
     ,list_requests => 'requests'
     ,machine_info => 'domains'
     ,log_active_domains => 'log_active_domains'
+    ,list_networks => 'virtual_networks'
 );
 
 my $A_WHILE;
@@ -381,6 +383,18 @@ sub _log_active_domains($rvd, $args) {
     return Ravada::Front::Log::list_active_recent($unit,$time);
 }
 
+sub _list_networks($rvd, $args) {
+    my @networks;
+    my $sth = $rvd->_dbh->prepare(
+        "SELECT * FROM virtual_networks ORDER BY name "
+    );
+    $sth->execute();
+    while (my $row = $sth->fetchrow_hashref) {
+        push @networks,($row);
+    }
+    return \@networks;
+}
+
 sub _its_been_a_while_channel($channel) {
     if (!$A_WHILE{$channel} || time -$A_WHILE{$channel} > 5) {
         $A_WHILE{$channel} = time;
diff --git a/public/js/admin.js b/public/js/admin.js
index d7f84318b..72727a686 100644
--- a/public/js/admin.js
+++ b/public/js/admin.js
@@ -7,10 +7,12 @@ ravadaApp.directive("solShowMachine", swMach)
         .controller("usersPage", usersPageC)
         .controller("messagesPage", messagesPageC)
         .controller("manage_nodes",manage_nodes)
+        .controller("manage_routes",manage_routes)
         .controller("manage_networks",manage_networks)
+        .controller("settings_network",settings_network)
         .controller("settings_node",settings_node)
         .controller("settings_storage",settings_storage)
-        .controller("settings_network",settings_network)
+        .controller("settings_route",settings_route)
         .controller("new_node", newNodeCtrl)
         .controller("new_storage", new_storage)
         .controller("settings_global", settings_global_ctrl)
@@ -868,29 +870,93 @@ ravadaApp.directive("solShowMachine", swMach)
         $scope.list_nodes();
         $interval($scope.list_nodes,30 * 1000);
     };
-
     function manage_networks($scope, $http, $interval, $timeout) {
-        list_networks= function() {
-            $http.get('/list_networks.json').then(function(response) {
+        $scope.init = function(id_vm) {
+            $scope.list_networks(id_vm);
+            $scope.loaded_networks=false;
+        }
+        $scope.list_networks = function(id_vm) {
+            $http.get('/v2/vm/list_networks/'+id_vm).then(function(response) {
+                $scope.networks=response.data;
+                $scope.loaded_networks=true;
+                });
+        }
+    }
+
+    function manage_routes($scope, $http, $interval, $timeout) {
+        list_routes = function() {
+            $http.get('/list_routes.json').then(function(response) {
                     for (var i=0; i<response.data.length; i++) {
                         var item = response.data[i];
-                        $scope.networks[item.id] = item;
+                        $scope.routes[item.id] = item;
                     }
                 });
         }
         $scope.update_network= function(id, field) {
-            var value = $scope.networks[id][field];
+            var value = $scope.routes[id][field];
             var args = { 'id': id };
             args[field] = value;
-            $http.post('/v1/network/set'
+            $http.post('/v2/route/set'
                 , JSON.stringify( args ))
             .then(function(response) {
             });
         };
 
 
-        $scope.networks={};
-        list_networks();
+        $scope.routes={};
+        list_routes();
+    }
+
+    function settings_network($scope, $http, $interval, $timeout) {
+        $scope.init = function(id,url, id_vm) {
+            if ( id ) {
+                $scope.load_network(id);
+            } else {
+                $scope.new_network(id_vm);
+            }
+        };
+        $scope.new_network = function(id_vm) {
+            $scope.network = { };
+            $http.get('/v2/network/new/'+id_vm)
+                .then(function(response) {
+                    $scope.network=response.data;
+                    console.log(response.data);
+            });
+        };
+
+        $scope.load_network = function(id) {
+            $http.get('/v2/network/info/'+id)
+                .then(function(response) {
+                $scope.network = response.data;
+                $scope.network._old_name = $scope.network.name;
+            });
+
+        };
+
+        $scope.update_network = function() {
+
+            var update = $scope.network['id'];
+            $http.post('/v2/network/set/'
+                , JSON.stringify($scope.network))
+                .then(function(response) {
+                    $scope.error=response.data.error;
+                    if (!update) {
+                        if (response.data['id_network']) {
+                            window.location.assign('/network/settings/'
+                                +response.data['id_network']+'.html');
+                        }
+                    }
+                });
+        };
+        $scope.remove_network = function() {
+            $http.post('/request/remove_network'
+                ,JSON.stringify({'id': $scope.network.id }))
+                .then(function(response) {
+                    $scope.network._removed = true;
+                });
+        };
+
+
     }
 
 
@@ -1128,11 +1194,11 @@ ravadaApp.directive("solShowMachine", swMach)
 
     };
 
-   function settings_network($scope, $http, $timeout) {
+   function settings_route($scope, $http, $timeout) {
         var url_ws;
         $scope.init = function(id_network) {
             if (typeof id_network == 'undefined') {
-                $scope.network = {
+                $scope.route= {
                     'name': ''
                     ,'all_domains': 1
                 };
@@ -1142,30 +1208,30 @@ ravadaApp.directive("solShowMachine", swMach)
             }
         }
         $scope.check_no_domains = function() {
-            if ( $scope.network.no_domains == 1 ){
-                $scope.network.all_domains = 0;
+            if ( $scope.route.no_domains == 1 ){
+                $scope.route.all_domains = 0;
             }
         };
         $scope.check_all_domains = function() {
-            if ( $scope.network.all_domains == 1 ){
-                $scope.network.no_domains = 0;
+            if ( $scope.route.all_domains == 1 ){
+                $scope.route.no_domains = 0;
             }
         };
         $scope.update_network= function(field) {
-            var data = $scope.network;
+            var data = $scope.route;
             if (typeof field != 'undefined') {
                 var data = {};
-                data[field] = $scope.network[field];
+                data[field] = $scope.route[field];
             }
             $scope.saved = false;
             $scope.error = '';
-            $http.post('/v1/network/set/'
+            $http.post('/v2/route/set/'
                 , JSON.stringify(data))
             //                    , JSON.stringify({ value: $scope.network[field]}))
                 .then(function(response) {
                     if (response.data.ok == 1){
                         $scope.saved = true;
-                        if (!$scope.network.id) {
+                        if (!$scope.route.id) {
                             $scope.new_saved = true;
                         }
                     }
@@ -1177,19 +1243,19 @@ ravadaApp.directive("solShowMachine", swMach)
         $scope.load_network = function(id_network) {
                 $scope.error = '';
                 $scope.saved = false;
-                $http.get('/network/info/'+id_network+'.json').then(function(response) {
-                    $scope.network = response.data;
+                $http.get('/route/info/'+id_network).then(function(response) {
+                    $scope.route = response.data;
                     $scope.formNetwork.$setPristine();
-                    $scope.network._old_name = $scope.network.name;
+                    $scope.route._old_name = $scope.route.name;
                 });
         };
         $scope.list_domains_network = function(id_network) {
-                $http.get('/network/list_domains/'+id_network).then(function(response) {
+                $http.get('/route/list_domains/'+id_network).then(function(response) {
                     $scope.machines = response.data;
                 });
         };
         $scope.set_network_domain= function(id_domain, field, allowed) {
-            $http.get("/network/set/"+$scope.network.id+ "/" + field+ "/" +id_domain+"/"
+            $http.get("/v2/route/set/"+$scope.route.id+ "/" + field+ "/" +id_domain+"/"
                     +allowed)
                 .then(function(response) {
                 });
@@ -1200,26 +1266,26 @@ ravadaApp.directive("solShowMachine", swMach)
             });
         };
 
-        $scope.remove_network = function(id_network) {
-            if ($scope.network.name == 'default') {
-                $scope.error = $scope.network.name + " network can't be removed";
+        $scope.remove_route = function(id_network) {
+            if ($scope.route.name == 'default') {
+                $scope.error = $scope.route.name + " network can't be removed";
                 return;
             }
-            $http.get('/v1/network/remove/'+id_network).then(function(response) {
-                $scope.message = "Network "+$scope.network.name+" removed";
-                $scope.network ={};
+            console.log(id_network);
+            $http.get('/v2/route/remove/'+id_network).then(function(response) {
+                window.location.assign('/admin/routes');
             });
         };
         $scope.check_duplicate = function(field) {
             var args = {};
-            if (typeof ($scope.network['id']) != 'undefined') {
-                args['id'] = $scope.network['id'];
+            if (typeof ($scope.route['id']) != 'undefined') {
+                args['id'] = $scope.route['id'];
             }
-            args[field] = $scope.network[field];
+            args[field] = $scope.route[field];
 
             $http.post("/v1/exists/networks",JSON.stringify(args))
                 .then(function(response) {
-                    $scope.network["_duplicated_"+field]=response.data.id;
+                    $scope.route["_duplicated_"+field]=response.data.id;
             });
         };
         $scope.new_saved = false;
diff --git a/public/js/ravada.js b/public/js/ravada.js
index 0660dd8fb..89120e38c 100644
--- a/public/js/ravada.js
+++ b/public/js/ravada.js
@@ -596,11 +596,11 @@
                                     }
                                 });
                             }
+                            list_interfaces();
                             if (is_admin) {
                                 $scope.init_domain_access();
                                 $scope.init_ldap_access();
                                 $scope.list_ldap_attributes();
-                                list_interfaces();
                                 list_users();
                                 list_host_devices();
                                 list_access_groups();
@@ -616,9 +616,12 @@
 
           var list_interfaces = function() {
             if (! $scope.network_nats) {
-                $http.get('/network/interfaces/'+$scope.showmachine.type+'/nat')
+                $http.get('/v2/vm/list_networks/'+$scope.showmachine.id_vm)
                     .then(function(response) {
-                        $scope.network_nats = response.data;
+                        $scope.network_nats= [];
+                        for (var i=0; i<response.data.length; i++ ) {
+                            $scope.network_nats.push(response.data[i].name);
+                        }
                 });
             }
             if (! $scope.network_bridges ) {
diff --git a/script/rvd_front b/script/rvd_front
index b017b8a2f..1e4d1793a 100644
--- a/script/rvd_front
+++ b/script/rvd_front
@@ -31,7 +31,7 @@ use Ravada::Booking;
 use Ravada::Front;
 use Ravada::Front::Domain;
 use Ravada::HostDevice::Templates;
-use Ravada::Network;
+use Ravada::Route;
 use Ravada::WebSocket;
 use POSIX qw(locale_h strftime);
 
@@ -383,6 +383,24 @@ get '/admin/storage' => sub($c) {
 
 };
 
+get '/admin/networks/*id_vm' => { id_vm => undef} => sub($c) {
+
+    return access_denied($c) unless $USER->is_admin
+    || $USER->can_create_networks || $USER->can_manage_all_networks;
+
+    _add_admin_libs($c);
+
+    _select_vm($c) if !defined $c->stash('id_vm');
+
+    Ravada::Request->list_networks(
+        uid => $USER->id
+        ,id_vm => $c->stash('id_vm')
+    );
+
+    return $c->render( template => '/main/admin_networks' );
+
+};
+
 any '/admin/#type' => sub {
   my $c = shift;
 
@@ -493,46 +511,147 @@ get '/node/host_device/remove/(:id)' => sub($c) {
     return $c->render(json => { ok => 1 });
 };
 
-get '/list_networks.json' => sub {
+get '/list_routes.json' => sub {
     my $c = shift;
-    $c->render(json => [ Ravada::Network->list_networks ]);
+    $c->render(json => [ Ravada::Route->list_networks ]);
 };
 
-get '/network/info/#id' => sub($c) {
+get '/v2/vm/list_networks/(:id_vm)' => sub {
+    my $c = shift;
+
+    return access_denied($c) unless $USER->is_admin
+    || $USER->can_create_networks || $USER->can_manage_all_networks;
+
+    $c->render(json => Ravada::Front->list_networks($c->stash('id_vm'), $USER->id) );
+};
+
+get '/route/info/#id' => sub($c) {
     my $sth = $RAVADA->_dbh->prepare("SELECT * from networks WHERE id=?");
     $sth->execute($c->stash('id'));
     my $row = $sth->fetchrow_hashref;
     return $c->render(json => $row);
 };
 
-get '/v1/network/remove/#id' => sub($c) {
+get '/v2/route/remove/#id' => sub($c) {
     my $sth = $RAVADA->_dbh->prepare("DELETE from networks WHERE id=?");
     $sth->execute($c->stash('id'));
+    $USER->send_message("Network ".$c->stash('id')." removed");
     return $c->render(json => { ok => 1 });
 };
 
-get '/network/list_domains/#id' => sub($c) {
+get '/route/list_domains/#id' => sub($c) {
     return $c->render( json => $RAVADA->list_bases_network($c->stash('id')));
 };
 
+any '/network/new' => sub($c) {
+    return access_denied($c) unless $USER->is_admin
+    || $USER->can_create_networks || $USER->can_manage_all_networks;
+
+    _add_admin_libs($c);
+    _select_vm($c);
+    $c->stash(item => 'network', id => undef, tab => undef);
+
+    return $c->render( template => '/main/network_new' );
+};
+
+any '/v2/network/new/#id_vm' => sub($c) {
+
+    return access_denied($c) unless $USER->is_admin
+    || $USER->can_create_networks || $USER->can_manage_all_networks;
+
+    my $name = 'net';
+    if ( $c->req->method eq 'POST') {
+        my $arg = decode_json($c->req->body);
+        $name = $arg->{name} if $arg->{name};
+    }
+
+    my $req = Ravada::Request->new_network(
+        uid => $USER->id
+        ,id_vm => $c->stash('id_vm')
+        ,name => $name
+    );
+    $RAVADA->wait_request($req);
+    my $data = {};
+    if ($req->status eq 'done' && $req->output && $req->output =~ /^\{/) {
+        eval {
+            $data = decode_json($req->output);
+        };
+        warn $@ if $@;
+    }
+    $c->render(json => $data);
+};
+
+get '/v2/network/info/#id' => sub($c) {
+    my $sth = $RAVADA->_dbh->prepare("SELECT * from virtual_networks WHERE id=?");
+    $sth->execute($c->stash('id'));
+    my $row = $sth->fetchrow_hashref;
+    return $c->render(json => $row);
+};
+
+sub _network_id_owner($id_network) {
+
+    confess "Error: undefined id_network "if !defined $id_network;
+    my $sth = $RAVADA->_dbh->prepare("SELECT * FROM virtual_networks "
+        ." WHERE id=?");
+    $sth->execute($id_network);
+    my $row = $sth->fetchrow_hashref;
+    warn "Error: network id=$id_network not found " if !$row->{id};
+
+    return ($row->{id_owner} or 0 );
+}
+
+post '/v2/network/set' => sub($c) {
+    my $arg = decode_json($c->req->body);
+    delete $arg->{_owner};
+
+    return access_denied_json($c)
+    unless $USER->is_admin || $USER->can_manage_all_networks
+    ||  ($USER->can_create_networks
+            && ( !$arg->{id} || $USER->id == _network_id_owner($arg->{id}))
+        );
+
+    my $req;
+    my %data;
+    if ($arg->{id}) {
+        $req = Ravada::Request->change_network(
+            uid => $USER->id
+            ,data => $arg
+        );
+    } else {
+        my $id_vm = delete $arg->{id_vm};
+        $req = Ravada::Request->create_network(
+            uid => $USER->id
+            ,id_vm => $id_vm
+            ,data => $arg
+        );
+        $RAVADA->wait_request($req, 120);
+        my $out = {};
+        eval { $out = decode_json($req->output) if $req->output };
+        warn $@ if $@;
+        %data = %$out if $out && keys %$out;
+    }
+    $data{error} = $req->error;
+    return $c->render(json => \%data, output => $req->output , error => $req->error);
+};
+
 sub _add_admin_libs($c) {
     push @{$c->stash->{js}}, '/js/admin.js?v='.$RAVADA->version;
     push @{$c->stash->{css}}, '/css/admin.css';
 }
 
-any '/network/new' => sub($c) {
+any '/route/new' => sub($c) {
     _add_admin_libs($c);
-    return $c->render(template => "/main/network_new");
+    return $c->render(template => "/main/route_new");
 };
 
-post '/v1/network/set' => sub($c) {
+post '/v2/route/set' => sub($c) {
     return access_denied($c) if !$USER->is_admin;
 
     my $arg = decode_json($c->req->body);
     return _update_fields($c, "networks", $arg);
 };
 
-get '/network/set/#id/#field/#id_domain/#value' => sub ($c) {
+get '/v2/route/set/#id/#field/#id_domain/#value' => sub ($c) {
     my $id_network = $c->stash('id');
     my $field = $c->stash('field');
     my $id_domain = $c->stash('id_domain');
@@ -598,6 +717,7 @@ post '/v1/exists/:item' => sub($c) {
 
     $query .= " AND id <> $id " if defined $id;
 
+
     my $sth = $RAVADA->_dbh->prepare("SELECT id FROM $table WHERE $query ");
     $sth->execute(map { $arg->{$_} } @fields );
 
@@ -740,7 +860,7 @@ get '/list_machine_types.json' => sub {
 get '/list_cpu_models.json' => sub {
     my $c = shift;
 
-    return access_denied($c) unless _logged_in($c)
+    return $c->render(json => []) unless _logged_in($c)
         && $USER->can_create_machine();
 
     my $id_domain = $c->param('id_domain');
@@ -829,6 +949,35 @@ any '/(:item)/settings/(:id).html' => sub($c) {
 
     _add_admin_libs($c);
     $c->stash(tab => ($c->param('tab') or ''));
+    my $table = $c->stash('item')."s";
+    $table = 'virtual_networks' if $table eq 'networks';
+    $table = 'networks'         if $table eq 'routes';
+
+    return access_denied($c) if $table !~ /^[a-z][a-z_]+$/i;
+
+    if ($table eq 'virtual_networks') {
+        my $id = $c->stash('id'); # get id network
+        return access_denied_json($c)
+            unless $USER->is_admin || $USER->can_manage_all_networks
+        ||  ($USER->can_create_networks && $USER->id == _network_id_owner($id))
+        ;
+    }
+
+    my $id_vm;
+    if ($table eq 'nodes') {
+        $id_vm = $c->stash('id');
+    } elsif ($table ne 'networks') {
+        my $sth = $RAVADA->_dbh->prepare(
+            "SELECT id,id_vm FROM $table WHERE id=?"
+        );
+        $sth->execute($c->stash('id'));
+        my $id2;
+        ($id2, $id_vm) = $sth->fetchrow;
+        return not_found($c, 404, $c->stash('item')." ".$c->stash('id')
+        ." not found in $table") if !$id2;
+
+    }
+    $c->stash('id_vm' => $id_vm);
 
     return $c->render( template => '/main/settings_generic' );
 };
@@ -1847,6 +1996,7 @@ post '/request/(:name)/' => sub {
     my $name = $c->stash('name');
 
     my $args = decode_json($c->req->body);
+    my $wait = delete $args->{_wait};
 
     for (qw(remote_ip uid)) {
         confess "Error: $_ should not be provided".Dumper($args)
@@ -1889,11 +2039,19 @@ post '/request/(:name)/' => sub {
         };
         warn $@ if $@;
     }
+    my $error = ( $@ or '');
+    my $output = '';
 
+    if ($wait) {
+        $RAVADA->wait_request($req, 60);
+        $error => $req->error if !$error;
+        $output = $req->output if $req->output;
+    }
     $RAVADA->_cache_clean();
 
     return $c->render(json => { ok => 0, error => $@ }) if !$req;
-    return $c->render(json => { ok => 1, request => $req->id });
+    return $c->render(json => { ok => 1, request => $req->id
+                                ,error => $error, output => $output });
 };
 
 get '/request/(:id).(:type)' => sub {
@@ -3031,6 +3189,32 @@ sub access_denied_json($c, $msg='Access denied') {
     return access_denied($c, $msg);
 }
 
+sub not_found($c,$code=404, $msg="Not found") {
+    my $agent = $c->req->headers->user_agent;
+    if ($agent =~ /Mojolicious/) {
+        return $c->render( text => $msg , status => $code );
+    }
+    if (defined $c->stash('type') && $c->stash('type') eq 'json') {
+        return $c->render(json => { error => $msg }, status => $code);
+    }
+    my @css_snippets = ["\t.intro {\n\t\tbackground:"
+                    ." url($CONFIG_FRONT->{login_bg_file})"
+                    ." no-repeat bottom center scroll;\n\t}"];
+
+    return $c->render(
+                    template => ($CONFIG_FRONT->{not_found} or '/main/not_found')
+                    ,css => ['/css/main.css']
+                    ,csssnippets => @css_snippets
+                    ,js => ['/js/main.js?v='.$RAVADA->version]
+                    ,navbar_custom => 1
+                    ,error => $msg
+                    ,can_login => ((! $USER) || ($USER->is_temporary))
+                    ,guide => $CONFIG_FRONT->{guide}
+                    ,status => $code
+    );
+
+}
+
 sub access_denied {
     my $c = shift;
     my $msg = shift;
@@ -3043,6 +3227,11 @@ sub access_denied {
     if (defined $c->stash('type') && $c->stash('type') eq 'json') {
         return $c->render(json => { error => $msg }, status => 403);
     }
+
+    my $agent = $c->req->headers->user_agent;
+    if ($agent =~ /Mojolicious/) {
+        return $c->render(text => $msg, status => 403);
+    }
     my @css_snippets = ["\t.intro {\n\t\tbackground:"
                     ." url($CONFIG_FRONT->{login_bg_file})"
                     ." no-repeat bottom center scroll;\n\t}"];
diff --git a/t/30_request.t b/t/30_request.t
index f531ad5e3..9297b5248 100644
--- a/t/30_request.t
+++ b/t/30_request.t
@@ -310,6 +310,7 @@ sub test_force() {
     my $req = Ravada::Request->refresh_vms(uid => user_admin->id);
     ok($req);
     wait_request( debug => 0);
+    is($req->error, '') or exit;
 
     my $req3 = Ravada::Request->refresh_vms(uid => user_admin->id);
     ok($req3);
diff --git a/t/70_networks.t b/t/70_networks.t
index 194068b79..a32492c51 100644
--- a/t/70_networks.t
+++ b/t/70_networks.t
@@ -10,7 +10,7 @@ no warnings "experimental::signatures";
 use feature qw(signatures);
 
 use_ok('Ravada');
-use_ok('Ravada::Network');
+use_ok('Ravada::Route');
 
 use lib 't/lib';
 use Test::Ravada;
@@ -28,7 +28,7 @@ sub test_allow_all {
     my $domain = shift;
 
     my $ip = '192.168.1.2/32';
-    my $net = Ravada::Network->new(address => $ip);
+    my $net = Ravada::Route->new(address => $ip);
     ok(!$net->allowed($domain->id),"Expecting not allowed from unknown network");
 
     #check list bases, default allowed
@@ -53,7 +53,7 @@ sub test_allow_all {
     ok(!$net->allowed_anonymous($domain->id),"Expecting denied anonymous from known network");
     ok($net->allowed($domain->id),"Expecting allowed from known network");
 
-    my $net2 = Ravada::Network->new(address => '192.168.1.22/32');
+    my $net2 = Ravada::Route->new(address => '192.168.1.22/32');
     ok($net2->allowed($domain->id),"Expecting allowed from known network");
     ok(!$net2->allowed_anonymous($domain->id),"Expecting denied anonymous from known network");
     { # test list bases anonymous
@@ -68,7 +68,7 @@ sub test_allow_domain {
     my $domain = shift;
 
     my $ip = '10.1.1.1/32';
-    my $net = Ravada::Network->new(address => $ip);
+    my $net = Ravada::Route->new(address => $ip);
     ok(!$net->allowed($domain->id),"Expecting not allowed from unknown network");
 
     { # test list bases anonymous
@@ -197,7 +197,7 @@ sub test_deny_all {
 
     my $ip = '10.0.0.2/32';
 
-    my $net = Ravada::Network->new(address => $ip);
+    my $net = Ravada::Route->new(address => $ip);
     ok(!$net->allowed($domain->id),"Expecting not allowed from unknown network");
 
     { # test list bases anonymous
@@ -318,11 +318,11 @@ $domain->is_public(1);
 
 test_conflict_allowed();
 
-my $net = Ravada::Network->new(address => '127.0.0.1/32');
+my $net = Ravada::Route->new(address => '127.0.0.1/32');
 ok($net->allowed($domain->id));
 
 deny_everything_any();
-my $net2 = Ravada::Network->new(address => '10.0.0.0/32');
+my $net2 = Ravada::Route->new(address => '10.0.0.0/32');
 ok(!$net2->allowed($domain->id), "Address unknown should not be allowed to anything");
 
 test_allow_all($domain);
diff --git a/t/kvm/p10_password.t b/t/kvm/p10_password.t
index 4b397bac6..000354614 100644
--- a/t/kvm/p10_password.t
+++ b/t/kvm/p10_password.t
@@ -16,13 +16,15 @@ init();
 my @VMS = vm_names();
 my $USER = create_user("foo","bar", 1);
 
+use_ok('Ravada::Route');
+
 #######################################################
 
 sub test_domain_no_password {
     my $vm_name = shift;
     my $vm = rvd_back->search_vm($vm_name);
 
-    my $net = Ravada::Network->new(address => '127.0.0.1/32');
+    my $net = Ravada::Route->new(address => '127.0.0.1/32');
 
     ok(!$net->requires_password);
     my $domain_name = new_domain_name();
@@ -43,7 +45,7 @@ sub test_domain_no_password {
     }
     is($domain->is_active,0) or return;
 
-    my $net2 = Ravada::Network->new(address => '10.0.0.1/32');
+    my $net2 = Ravada::Route->new(address => '10.0.0.1/32');
     ok(!$net2->requires_password,"Expecting net requires password ");
 
     $domain->start(user => $USER, remote_ip => '10.0.0.1');
@@ -69,7 +71,7 @@ sub test_domain_password2 {
     my $vm_name = shift;
     my $vm = rvd_back->search_vm($vm_name);
 
-    my $net = Ravada::Network->new(address => '127.0.0.1/32');
+    my $net = Ravada::Route->new(address => '127.0.0.1/32');
 
     ok(!$net->requires_password) or return;
     my $domain_name = new_domain_name();
@@ -90,7 +92,7 @@ sub test_domain_password2 {
     }
     is($domain->is_active,0) or return;
 
-    my $net2 = Ravada::Network->new(address => '10.0.0.1/32');
+    my $net2 = Ravada::Route->new(address => '10.0.0.1/32');
     ok($net2->requires_password,"Expecting net requires password ")
         or return;
 
@@ -117,7 +119,7 @@ sub test_domain_password2 {
 sub test_domain_password1($vm_name, $requires_password=1) {
     my $vm = rvd_back->search_vm($vm_name);
 
-    my $net2 = Ravada::Network->new(address => '10.0.0.1/32');
+    my $net2 = Ravada::Route->new(address => '10.0.0.1/32');
 
     ok($net2->requires_password,"Expecting net requires password ")
         or return;
diff --git a/t/lib/Test/Ravada.pm b/t/lib/Test/Ravada.pm
index 4d16f095f..7ac192c18 100644
--- a/t/lib/Test/Ravada.pm
+++ b/t/lib/Test/Ravada.pm
@@ -1485,6 +1485,48 @@ sub _qemu_storage_pool {
     return $pool_name;
 }
 
+sub remove_void_networks($vm=undef) {
+    if (!defined $vm) {
+        eval { $vm = rvd_back->search_vm('Void') };
+    }
+    my $dir_net = $vm->dir_img()."/networks";
+    return if ! -e $dir_net;
+    my $base = base_domain_name();
+
+    opendir my $dir, $dir_net or die "$! $dir_net";
+    while(my $filename = readdir $dir) {
+        my $file = "$dir_net/$filename";
+        next unless $file =~ /^$base.*\.yml$/;
+        unlink $file or warn "$! $file";
+    }
+
+}
+
+sub remove_qemu_networks($vm=undef) {
+    return if !$VM_VALID{'KVM'} || $>;
+    if (!defined $vm) {
+        eval { $vm = rvd_back->search_vm('KVM') };
+        if ($@ && $@ !~ /Missing qemu-img/) {
+            warn $@;
+        }
+        if  ( !$vm ) {
+            $VM_VALID{'KVM'} = 0;
+            return;
+        }
+    }
+
+    my $base = base_domain_name();
+    $vm->connect();
+    for my $network ( $vm->vm->list_all_networks) {
+        my $name = $network->get_name;
+        next if $name !~ /^$base/;
+        diag("removing network $name");
+        $network->destroy() if $network->is_active;
+        $network->undefine();
+    }
+
+}
+
 sub remove_qemu_pools($vm=undef) {
     return if !$vm && (!$VM_VALID{'KVM'} || $>);
     return if defined $vm && $vm->type eq 'Void';
@@ -1553,6 +1595,11 @@ sub remove_old_pools {
     remove_qemu_pools();
 }
 
+sub remove_old_networks {
+    remove_qemu_networks();
+    remove_void_networks();
+}
+
 sub _remove_old_entries($table) {
     my $sth = connector()->dbh->prepare("DELETE FROM $table"
     ." WHERE name like ? "
@@ -1588,6 +1635,7 @@ sub clean($ldap=undef) {
     remove_old_domains();
     remove_old_disks();
     remove_old_pools();
+    remove_old_networks();
     _remove_old_entries('vms');
     _remove_old_entries('networks');
     _remove_old_groups_ldap();
diff --git a/t/mojo/30_settings.t b/t/mojo/30_settings.t
index d03b2ffc3..f7983bc47 100644
--- a/t/mojo/30_settings.t
+++ b/t/mojo/30_settings.t
@@ -101,8 +101,10 @@ sub test_nodes($vm_name) {
 }
 
 sub test_settings_item($id, $item) {
-    $t->get_ok('/'.$item.'/settings/'.$id.'.html');
-    is($t->tx->res->code(),200) or die $t->tx->res->body;
+    $item = 'route' if $item eq 'network';
+    my $url = '/'.$item.'/settings/'.$id.'.html';
+    $t->get_ok($url);
+    is($t->tx->res->code(),200, "Expecting $url") or die $t->tx->res->body;
 }
 
 sub test_exists_node($id_node, $name) {
@@ -147,56 +149,245 @@ sub test_exists_network($id_network, $field, $name) {
     is($result_exists->{id}, $id_network);
 }
 
-sub _remove_network($address) {
-    my @list_networks = Ravada::Network::list_networks();
+sub _remove_route($address) {
+    my @list_networks = Ravada::Route::list_networks();
 
     my ($found) = grep { $_->{address} eq $address} @list_networks;
     return if !$found;
 
-    $t->get_ok("/v1/network/remove/".$found->{id});
+    $t->get_ok("/v2/route/remove/".$found->{id});
     is($t->tx->res->code(),200) or die $t->tx->res->body;
 
 }
 
-sub test_networks($vm_name) {
+sub _remove_networks($id_vm) {
+    my $sth = connector->dbh->prepare("SELECT vn.id FROM virtual_networks vn, vms v"
+        ." WHERE vn.id_vm=v.id "
+        ."   AND v.id=? AND vn.name like ?"
+    );
+    $sth->execute($id_vm, base_domain_name."%");
+
+    while ( my ($id) = $sth->fetchrow) {
+        my $id_req = mojo_request($t, "remove_network", { id => $id});
+        if ($id_req) {
+            my $req = Ravada::Request->open($id_req);
+            die "Error in ".$req->command." id=$id" if $req->error;
+        }
+    }
+
+}
+
+sub _id_vm($vm_name) {
+    my $sth = connector->dbh->prepare("SELECT id,hostname FROM vms "
+        ." WHERE vm_type=? AND is_active=1");
+    $sth->execute($vm_name);
+    my @vm;
+    while (my $row = $sth->fetchrow_hashref ) {
+        push @vm,($row);
+    }
+    my ($vm) = grep { $_->{hostname} eq 'localhost' } @vm;
+
+    my $id_vm;
+    $id_vm = $vm->{id}      if $vm;
+    $id_vm = $vm[0]->{id}   if !$id_vm;
+
+    return $id_vm;
+}
+
+sub test_networks_access($vm_name) {
+
+    my ($name, $pass) = (new_domain_name, "$$ $$");
+    my $user = create_user($name, $pass,0 );
+    is($user->is_admin,0 );
+    mojo_login($t, $name, $pass);
+
+    my $id_vm = _id_vm($vm_name);
+
+    my @urls =(
+        "/admin/networks", "/network/new"
+        , "/v2/vm/list_networks/$id_vm","/v2/network/new/".$id_vm);
+    for my $url (@urls) {
+        $t->get_ok($url)->status_is(403);
+    }
+
+    user_admin->grant($user,'create_networks');
+    for my $url (@urls) {
+        $t->get_ok($url)->status_is(200);
+    }
+
+    user_admin->revoke($user,'create_networks');
+    user_admin->grant($user,'manage_all_networks');
+    for my $url (@urls) {
+        $t->get_ok($url)->status_is(200);
+    }
+
+    $user->remove();
+    mojo_login($t, $USERNAME, $PASSWORD);
+
+}
+
+sub test_networks_access_grant($vm_name) {
+
+    my ($name, $pass) = (new_domain_name, "$$ $$");
+    my $user = create_user($name, $pass,0 );
+    user_admin->grant($user,"create_networks");
+    mojo_login($t, $name, $pass);
+
+    my $id_vm = _id_vm($vm_name);
+
+    $t->post_ok("/v2/network/new/".$id_vm => json => { name => base_domain_name() });
+    my $data = decode_json($t->tx->res->body);
+    ok(keys %$data) or die Dumper($data);
+
+    $t->post_ok("/v2/network/set/" => json => $data );
+    my $new_ok = decode_json($t->tx->res->body);
+    ok($new_ok->{id_network}) or return;
+
+    $t->get_ok("/settings/network".$new_ok->{id_network}.".html");
+
+    $t->get_ok("/v2/vm/list_networks/".$id_vm);
+    my $networks2 = decode_json($t->tx->res->body);
+    my ($old) = grep { $_->{name} ne $data->{name} } @$networks2;
+    ok($old,"Expecting more networks for VM $vm_name [ $id_vm ]")
+        or die Dumper([map {$_->{name} } @$networks2]);
+
+    is($old->{_can_change},0) or exit;
+
+    $t->get_ok("/network/settings/".$old->{id}.".html")->status_is(403);
+    $old->{autostart}=0;
+
+    $t->post_ok("/v2/network/set/" => json => $old)->status_is(403);
+
+    my ($new) = grep { $_->{name} eq $data->{name} } @$networks2;
+    ok($new,"Expecting new network $data->{name}")
+        or die Dumper([map {$_->{name} } @$networks2]);
+    is($new->{_owner}->{id},$user->id);
+    is($new->{_can_change},1) or exit;
+
+    for ( 1 .. 2 ) {
+        $new->{is_active} = (!$new->{is_active} or 0);
+        $t->post_ok("/v2/network/set/" => json => $new)->status_is(200);
+        wait_request();
+
+        $t->get_ok("/v2/vm/list_networks/".$id_vm);
+
+        my $networks3 = decode_json($t->tx->res->body);
+        my ($net3) = grep { $_->{name} eq $new->{name}} @$networks3;
+        is($net3->{is_active}, $new->{is_active}) or exit;
+    }
+
+    for ( 1 .. 2 ) {
+        $new->{is_public} = (!$new->{is_public} or 0);
+        $t->post_ok("/v2/network/set/" => json => $new)->status_is(200);
+        wait_request();
+
+        $t->get_ok("/v2/vm/list_networks/".$id_vm);
+        my $networks4 = decode_json($t->tx->res->body);
+        my ($net4) = grep { $_->{name} eq $new->{name}} @$networks4;
+        is($net4->{is_public}, $new->{is_public}) or exit;
+    }
+
+    mojo_login($t, $USERNAME, $PASSWORD);
+
+}
+
+sub test_networks_admin($vm_name) {
+    mojo_check_login($t);
+
+    for my $url (qw( /admin/networks/ /network/new) ) {
+        $t->get_ok($url);
+        is($t->tx->res->code(),200, "Expecting access to $url");
+    }
+
+    my $id_vm = _id_vm($vm_name);
+    die "Error: I can't find if for vm type = $vm_name" if !$id_vm;
+
+    _remove_networks($id_vm);
+
+    $t->get_ok("/v2/vm/list_networks/".$id_vm);
+    my $networks = decode_json($t->tx->res->body);
+    ok(scalar(@$networks));
+
+    $t->post_ok("/v2/network/new/".$id_vm => json => { name => base_domain_name() });
+    my $data = decode_json($t->tx->res->body);
+
+    $t->post_ok("/v2/network/set/" => json => $data );
+
+    my $new_ok = decode_json($t->tx->res->body);
+    ok($new_ok->{id_network}) or die Dumper([$t->tx->res->body, $new_ok]);
+
+    $t->get_ok("/v2/vm/list_networks/".$id_vm);
+    my $networks2 = decode_json($t->tx->res->body);
+    my ($new) = grep { $_->{name} eq $data->{name} } @$networks2;
+
+    ok($new);
+    is($new->{_can_change},1) or exit;
+    is($new->{_owner}->{id},user_admin->id) or exit;
+    $new->{is_active} = 0;
+
+    $t->post_ok("/v2/network/set/" => json => $new);
+    wait_request(debug => 1);
+    $t->get_ok("/v2/vm/list_networks/".$id_vm);
+
+    my $networks3 = decode_json($t->tx->res->body);
+    my ($changed) = grep { $_->{name} eq $data->{name} } @$networks3;
+    is($changed->{is_active},0) or die $changed->{name};
+
+    $t->get_ok("/v2/network/info/".$changed->{id});
+
+    my $changed4 = decode_json($t->tx->res->body);
+    is($changed4->{is_active},0) or exit;
+
+    $new->{is_public}=1;
+    $t->post_ok("/v2/network/set/" => json => $new);
+    wait_request(debug => 1);
+    $t->get_ok("/v2/vm/list_networks/".$id_vm);
+
+    my $networks5 = decode_json($t->tx->res->body);
+    my ($changed5) = grep { $_->{name} eq $data->{name} } @$networks5;
+    is($changed5->{is_public},1) or warn Dumper($changed5);
+
+}
+
+sub test_routes($vm_name) {
     mojo_check_login($t);
     my $name = new_domain_name();
     my $address = '1.2.3.0/24';
 
-    _remove_network($address);
+    _remove_route($address);
 
-    $t->post_ok('/v1/network/set' => json => {
+    $t->post_ok('/v2/route/set' => json => {
         name => $name
         , address =>  $address    });
     is($t->tx->res->code(),200) or die $t->tx->res->to_string();
 
     exit if !$t->success;
 
-    my @list_networks = Ravada::Network::list_networks();
+    my @list_networks = Ravada::Route::list_networks();
     my ($found) = grep { $_->{name} eq $name } @list_networks;
     ok($found,"Expecting $name in list vms ".Dumper(\@list_networks)) or return;
     my $id_network = $found->{id};
 
     my $name2 = new_domain_name();
-    $t->post_ok("/v1/network/set", json => {
+    $t->post_ok("/v2/route/set", json => {
             id => $found->{id}
             , name => $name2
         }
     );
     is($t->tx->res->code(),200) or die $t->tx->res->body;
 
-    @list_networks = Ravada::Network::list_networks();
+    @list_networks = Ravada::Route::list_networks();
     ($found) = grep { $_->{id} == $id_network } @list_networks;
     is($found->{name}, $name2) or die Dumper($found);
 
     my $new_name = new_domain_name();
-    $t->post_ok("/v1/network/set", json => {
+    $t->post_ok("/v2/route/set", json => {
             id => $id_network
             , name => $new_name
         }
     );
 
-    @list_networks = Ravada::Network::list_networks();
+    @list_networks = Ravada::Route::list_networks();
 
     ($found) = grep { $_->{id} == $id_network } @list_networks;
     is($found->{name}, $new_name) or die Dumper(\@list_networks);
@@ -206,10 +397,10 @@ sub test_networks($vm_name) {
 
     test_settings_item( $id_network, 'network' );
 
-    $t->get_ok("/v1/network/remove/".$found->{id});
+    $t->get_ok("/v2/route/remove/".$found->{id});
     is($t->tx->res->code(),200) or die $t->tx->res->body;
 
-    ok(! grep { $_->{id} == $found->{id} } Ravada::Network::list_networks());
+    ok(! grep { $_->{id} == $found->{id} } Ravada::Route::list_networks());
 
 }
 
@@ -329,10 +520,7 @@ sub test_storage_pools($vm_name) {
     my $sp = decode_json($t->tx->res->body);
     ok(scalar(@$sp));
 
-    my $sth = connector->dbh->prepare("SELECT id FROM vms where vm_type=?"
-        ." AND hostname='localhost'");
-    $sth->execute($vm_name);
-    my ($id_vm) = $sth->fetchrow;
+    my $id_vm = _id_vm($vm_name);
 
     $t->get_ok("/list_storage_pools/$id_vm");
 
@@ -418,11 +606,15 @@ for my $vm_name (reverse @{rvd_front->list_vm_types} ) {
 
     diag("Testing settings in $vm_name");
 
+    test_networks_access( $vm_name );
+    test_networks_access_grant($vm_name);
+    test_networks_admin( $vm_name );
     test_storage_pools($vm_name);
     test_nodes( $vm_name );
-    test_networks( $vm_name );
+    test_routes( $vm_name );
 }
 
 clean_clones();
+remove_old_users();
 
 done_testing();
diff --git a/t/mojo/40_anonymous.t b/t/mojo/40_anonymous.t
index 88fa75bc9..cd5180ea5 100644
--- a/t/mojo/40_anonymous.t
+++ b/t/mojo/40_anonymous.t
@@ -57,7 +57,7 @@ sub _allow_anonymous_base() {
     my $id_domain = _id_domain('zz-test-base-alpine');
 
     mojo_login($t, user_admin->name,"$$ $$");
-    $t->get_ok("/network/set/$id_net/anonymous/$id_domain/1");
+    $t->get_ok("/v2/route/set/$id_net/anonymous/$id_domain/1");
 
     my $sth = connector->dbh->prepare("UPDATE domains set is_public=1"
         ." WHERE id=?");
@@ -97,6 +97,7 @@ _allow_anonymous_base();
 $t->get_ok("/anonymous");
 
 is($t->tx->res->code(), 200 ) or exit;
+
 is(list_anonymous_users(), $n_anonymous + 1);
 
 my $bases = rvd_front->list_bases_anonymous('127.0.0.1');
diff --git a/t/nodes/10_basic.t b/t/nodes/10_basic.t
index 0ceb540f4..0e5cfdeb6 100644
--- a/t/nodes/10_basic.t
+++ b/t/nodes/10_basic.t
@@ -1626,6 +1626,11 @@ sub test_displays($vm, $node, $no_builtin=0) {
     $domain->remove(user_admin);
 }
 
+sub test_network($vm, $node) {
+    my @vm_nets= $vm->list_virtual_networks();
+    my $node_nets = $node->list_virtual_networks();
+}
+
 ##################################################################################
 
 if ($>)  {
@@ -1639,7 +1644,7 @@ clean();
 $Ravada::Domain::MIN_FREE_MEMORY = 256 * 1024;
 my $tls;
 
-for my $vm_name (vm_names() ) {
+for my $vm_name (reverse vm_names() ) {
     my $vm;
     eval { $vm = rvd_back->search_vm($vm_name) };
 
@@ -1666,6 +1671,7 @@ for my $vm_name (vm_names() ) {
         $tls = 1 if check_libvirt_tls() && $vm_name eq 'KVM';
         my $node = remote_node($vm_name)  or next;
         clean_remote_node($node);
+        test_network($vm,$node);
 
         ok($node->vm,"[$vm_name] expecting a VM inside the node") or do {
             remove_node($node);
diff --git a/t/request/30_hardware.t b/t/request/30_hardware.t
index 5891af05c..2547f2458 100644
--- a/t/request/30_hardware.t
+++ b/t/request/30_hardware.t
@@ -1104,7 +1104,17 @@ sub test_change_network_bridge($vm, $domain, $index) {
 
     skip("No bridges found in this system",6) if !scalar @bridges;
     my $info = $domain->info(user_admin);
-    is ($info->{hardware}->{network}->[$index]->{type}, 'NAT') or exit;
+    if ($info->{hardware}->{network}->[$index]->{type} eq 'bridge') {
+        my $req = Ravada::Request->change_hardware(
+            id_domain => $domain->id
+            ,hardware => 'network'
+            ,index => $index
+            ,data => { type => 'NAT', network => 'default'}
+            ,uid => user_admin->id
+        );
+        wait_request();
+
+    }
 
     ok(scalar @bridges,"No network bridges defined in this host") or return;
 
@@ -1509,6 +1519,8 @@ sub _remove_usbs($domain, $hardware) {
 
 sub test_change_drivers($domain, $hardware) {
 
+    return if $domain->type eq 'Void' && $hardware eq 'network';
+
     _remove_usbs($domain, $hardware);
 
     my $info = $domain->info(user_admin);
@@ -1566,6 +1578,9 @@ sub test_change_drivers($domain, $hardware) {
 }
 
 sub test_all_drivers($domain, $hardware) {
+
+    return if $domain->type eq 'Void' && $hardware eq 'network';
+
     my $info = $domain->info(user_admin);
     my $options = $info->{drivers}->{$hardware};
     ok(scalar @$options,"No driver options for $hardware") or exit;
@@ -1712,7 +1727,7 @@ for my $vm_name (vm_names()) {
     my %controllers = $domain_b0->list_controllers;
     lock_hash(%controllers);
 
-    for my $hardware ('display', sort keys %controllers ) {
+    for my $hardware ( sort keys %controllers ) {
         next if $hardware eq 'video';
 	    my $name= new_domain_name();
 	    my $domain_b = $BASE->clone(
diff --git a/t/request/30_hardware_clones.t b/t/request/30_hardware_clones.t
index bd869ab2d..b033fedac 100644
--- a/t/request/30_hardware_clones.t
+++ b/t/request/30_hardware_clones.t
@@ -142,6 +142,8 @@ sub _clean_hw($name, @hw) {
                 $item->{file} = '';
             } elsif ($name eq 'filesystem') {
                 $item->{_id} = '';
+            } elsif ($name eq 'network') {
+                delete $item->{hwaddr};
             }
             lock_hash(%$item);
         }
diff --git a/t/request/70_network.t b/t/request/70_network.t
index 29b81b020..2335bdc72 100644
--- a/t/request/70_network.t
+++ b/t/request/70_network.t
@@ -44,6 +44,7 @@ sub test_list_nats($vm) {
 }
 
 sub _remove_qemu_bridges($vm, $bridges) {
+    return @$bridges if $vm->type eq 'Void';
     my @nat = $vm->list_network_interfaces('nat');
 
     my %bridges = map { $_ => 1 } @$bridges;
diff --git a/t/user/20_grants.t b/t/user/20_grants.t
index 55e119e38..9c2d7b000 100644
--- a/t/user/20_grants.t
+++ b/t/user/20_grants.t
@@ -961,7 +961,6 @@ sub test_view_all($vm) {
     }
     for my $req ( $req_start_admin, $req_prepare_admin, $req_start
     ,$req_refresh, $req_refresh_ports) {
-        diag($req->command);
         is($req->status,'done');
         next if $req->command =~ /refresh_machine_ports/i;
         is($req->error,'', $req->command) or exit;
diff --git a/t/user/networks.t b/t/user/networks.t
new file mode 100644
index 000000000..9e5690532
--- /dev/null
+++ b/t/user/networks.t
@@ -0,0 +1,198 @@
+#!perl
+
+use strict;
+use warnings;
+use Test::More;
+
+use lib 't/lib';
+use Test::Ravada;
+
+use Ravada;
+
+use Data::Dumper;
+use Mojo::JSON qw(decode_json);
+
+no warnings "experimental::signatures";
+use feature qw(signatures);
+
+##############################################################################
+
+sub test_create_network($vm) {
+    my $req_new= Ravada::Request->new_network(
+        uid => user_admin->id
+        ,id_vm => $vm->id
+        ,name => base_domain_name()
+    );
+    wait_request();
+    my $data = decode_json($req_new->output);
+
+    my $req = Ravada::Request->create_network(
+        uid => user_admin->id
+        ,id_vm => $vm->id
+        ,data => $data
+    );
+    wait_request(debug => 0);
+    my @networks = $vm->list_virtual_networks();
+    my ($network) = grep {$_->{name} eq $data->{name} } @networks;
+    ok($network) or die "Error, network $data->{name} not created "
+        .Dumper(\@networks);
+
+    return $network;
+}
+
+sub test_default_owner(@networks) {
+    for my $net (@networks) {
+        is($net->{id_owner},Ravada::Utils::user_daemon->id);
+    }
+}
+
+sub test_grant_access($vm) {
+    my $user = create_user();
+    user_admin->grant($user,'create_networks');
+
+    my $req_new= Ravada::Request->new_network(
+        uid => $user->id
+        ,id_vm => $vm->id
+        ,name => base_domain_name()
+    );
+    wait_request();
+    is($req_new->error,'');
+    my $data = decode_json($req_new->output);
+
+    my $req_create = Ravada::Request->create_network(
+        uid => $user->id
+        ,id_vm => $vm->id
+        ,data => $data
+    );
+
+    wait_request();
+
+    my ($network) = grep {$_->{name} eq $data->{name} }
+        $vm->list_virtual_networks();
+
+        ok($network) or die "Error: network not created ".Dumper($data);
+
+    my $networks = rvd_front->list_networks($vm->id , $user->id);
+    my ($network_f) = grep {$_->{name} eq $data->{name}} @$networks;
+
+    ok($network_f) or die "Network $data->{name} not found ";
+
+    is($network_f->{_owner}->{id}, $user->id);
+    is($network_f->{_owner}->{name}, $user->name);
+}
+
+sub test_deny_access($vm) {
+    my $user = create_user();
+    my $networks = rvd_front->list_networks($vm->id , $user->id);
+    ok(scalar(@$networks));
+
+    $networks = rvd_front->list_networks($vm->id , user_admin->id);
+    ok(scalar(@$networks));
+
+    test_default_owner(@$networks);
+
+    my $network = test_create_network($vm);
+
+    my $req_new= Ravada::Request->new_network(
+        uid => $user->id
+        ,id_vm => $vm->id
+        ,name => base_domain_name()
+    );
+    wait_request(check_error => 0);
+    like($req_new->error,qr/not authorized/);
+
+    my $req_change = Ravada::Request->change_network(
+        uid => $user->id
+        ,data => $network
+    );
+    wait_request(check_error => 0);
+    like($req_change->error,qr/not authorized/);
+
+    my $req_delete = Ravada::Request->remove_network(
+        uid => $user->id
+        ,id => $network->{id}
+    );
+    wait_request(check_error => 0, debug => 0);
+    like($req_delete->error,qr/not authorized/);
+    my $networks2 = rvd_front->list_networks($vm->id , user_admin->id);
+    my ($found2) = grep { $_->{name} eq $network->{name} } @$networks2;
+    ok($found2,"Expecting network $network->{name} $network->{id} not removed ".Dumper($networks2)) or return;
+
+    my $req_create = Ravada::Request->create_network(
+        uid => $user->id
+        ,id_vm => $vm->id
+        ,data => $network
+    );
+    wait_request(check_error => 0);
+    like($req_create->error,qr/not authorized/);
+
+    my $req_list = Ravada::Request->list_networks(
+        uid => $user->id
+        ,id_vm => $vm->id
+    );
+    wait_request(check_error => 0);
+    like($req_list->error,qr/not authorized/);
+
+    user_admin->grant($user,'create_networks');
+    $req_new->status('requested');
+    $req_change->status('requested');
+    $req_list->status('requested');
+
+    wait_request(check_error => 0);
+    is($req_new->error,'');
+    like($req_change->error,qr/not authorized/);
+    is($req_change->status(),'done');
+    is($req_list->error,'');
+
+    $req_delete->status('requested');
+    wait_request(check_error => 0);
+    like($req_delete->error,qr/not authorized/);
+
+    $req_create->status('requested');
+    my $new_data = decode_json($req_new->output);
+    $req_create->arg('data' => $new_data);
+    $req_create->status('requested');
+    wait_request();
+
+    is($req_create->error, '');
+
+    $req_list->status('requested');
+    wait_request();
+
+    my $new_list = decode_json($req_list->output);
+    my ($found) = grep { $_->{name} eq $new_data->{name} } @$new_list;
+
+    ok($found,"Expecting new network $new_data->{name}");
+    $user->remove();
+}
+
+##############################################################################
+
+init();
+
+for my $vm_name( vm_names() ) {
+    my $vm;
+    eval { $vm = rvd_back->search_vm($vm_name) };
+
+    SKIP: {
+        my $msg = "SKIPPED test: No $vm_name VM found ";
+        if ($vm && $vm_name =~ /kvm/i && $>) {
+            $msg = "SKIPPED: Test must run as root";
+            $vm= undef;
+        }
+
+        diag($msg)      if !$vm;
+        skip $msg,10    if !$vm;
+
+        diag("Testing networks access on $vm_name");
+
+        Ravada::Request->list_networks(id_vm => $vm->id, uid => user_admin->id);
+        wait_request( debug => 0);
+        test_deny_access($vm);
+        test_grant_access($vm);
+    }
+
+}
+
+end();
+done_testing();
diff --git a/t/vm/networking.t b/t/vm/networking.t
new file mode 100644
index 000000000..a4d4f231b
--- /dev/null
+++ b/t/vm/networking.t
@@ -0,0 +1,809 @@
+use warnings;
+use strict;
+
+use Carp qw(confess);
+use Data::Dumper;
+use Mojo::JSON qw(decode_json);
+use Storable qw(dclone);
+use Test::More;
+
+use YAML qw(Dump LoadFile DumpFile);
+
+no warnings "experimental::signatures";
+use feature qw(signatures);
+
+use lib 't/lib';
+use Test::Ravada;
+
+my $N = 100;
+########################################################################
+
+sub test_list_networks($vm) {
+    my @list = $vm->list_virtual_networks();
+    my ($default)= @list;
+    ok($default) or exit;
+
+    my $public=0;
+    for my $net ( @list ) {
+        ok($net->{id_vm}) or die "Error: network missing id_vm ".Dumper($net);
+        my ($found) = _search_network(internal_id=>$net->{internal_id});
+        is($found->{name}, $net->{name});
+        is($found->{id_vm}, $net->{id_vm});
+        $public++ if $found->{is_public};
+        ($found) = _search_network(id_vm => $net->{id_vm}, name => $net->{name});
+        die "Error: network not found id_vm= $net->{id_vm}, name=$net->{name}"
+        if !$found;
+        is($found->{internal_id}, $net->{internal_id}) or die Dumper($found);
+    }
+    ok($public,"Expecting at least one public network discovered, got $public") or exit;
+
+}
+
+sub _search_network(%args) {
+    my $sql = "SELECT * FROM virtual_networks "
+    ." WHERE ".join(" AND ",map { "$_=?" } sort keys %args);
+    my $sth = connector->dbh->prepare($sql);
+    $sth->execute(map { $args{$_} } sort keys %args);
+    my $found = $sth->fetchrow_hashref;
+    return $found;
+}
+
+sub _get_active_network($vm) {
+    my($active) = grep { $_->{is_active} } $vm->list_virtual_networks();
+    return $active if $active;
+
+    my @networks = $vm->list_virtual_networks();
+    for my $old (@networks) {
+        Ravada::Request->change_network(
+            uid => user_admin->id
+            ,data => { %$old, is_active => 1 }
+        );
+        wait_request();
+        my ($net) = grep {$_->{id} == $old->{id}} $vm->list_virtual_networks();
+        return $net if $net->{is_active};
+    }
+    die "Error: No network could be activated ".Dumper(\@networks);
+}
+sub test_create_fail ($vm) {
+
+    my @networks = $vm->list_virtual_networks();
+
+    my $net0 = _get_active_network($vm);
+    my $name = new_domain_name;
+    my $net = {
+        name => $name
+        ,id_vm => $vm->id
+        ,ip_address => $net0->{ip_address}
+        ,ip_netmask => '255.255.255.0'
+        ,is_active => 1
+    };
+    my $req = Ravada::Request->create_network(
+        uid => user_admin->id
+        ,id_vm => $vm->id
+        ,data => $net
+    );
+    wait_request(check_error => 0, debug => 0);
+    like($req->error,qr/Network is already in use/) or die $name;
+    my $out = decode_json($req->output);
+    like($out->{id_network},qr/^\d+$/) or exit;
+
+    my ($old) = grep { $_->{id} eq $out->{id_network} } @networks;
+    ok(!$old,"Expecting new network is created");
+}
+
+sub test_duplicate_add($vm, $net) {
+    my $net2 = dclone($net);
+    delete $net2->{id};
+    delete $net2->{internal_id};
+    my $req = Ravada::Request->create_network(
+        data => $net2
+        ,uid => user_admin->id
+        ,id_vm => $vm->id
+    );
+    wait_request( check_error => 0, debug => 0);
+    like($req->error,qr/already exist/);
+}
+
+sub test_duplicate_bridge_add($vm, $net) {
+    my ($net0) = grep { $_->{name} eq $net->{name}} $vm->list_virtual_networks();
+    my $net2 = dclone($net);
+    delete $net2->{id};
+    delete $net2->{internal_id};
+    delete $net2->{dhcp_start};
+    delete $net2->{dhcp_end};
+    $net2->{bridge} = $net0->{bridge};
+    $net2->{name} = new_domain_name();
+    $net2->{ip_address} = '192.51.200.1';
+
+    my $req = Ravada::Request->create_network(
+        data => $net2
+        ,uid => user_admin->id
+        ,id_vm => $vm->id
+    );
+    wait_request( check_error => 0, debug => 0);
+    is($req->output,'{}');
+    like($req->error,qr/already exists/) or exit;
+
+    my ($net_created) = grep {$net2->{name} eq $_->{name} }
+        $vm->list_virtual_networks();
+
+    ok(!$net_created) or die "Expecting $net2->{name} not created";
+
+    Ravada::Request->remove_network(
+        uid => user_admin->id
+        ,id => $net_created->{id}
+    ) if $net_created;
+}
+
+# only admins or users that can manage all networks can do this
+sub test_change_owner($vm) {
+    my $user = create_user();
+    my $new = $vm->new_network(new_domain_name);
+    my $req = Ravada::Request->create_network(
+        uid => user_admin->id
+        ,id_vm => $vm->id
+        ,data => $new
+    );
+    wait_request();
+
+    my($new2) = grep { $_->{name} eq $new->{name} } $vm->list_virtual_networks();
+    ok($new2) or return;
+    $new2->{id_owner} = $user->id;
+    my $req_change = Ravada::Request->change_network(
+        uid => $user->id
+        ,data => $new2
+    );
+
+    wait_request(check_error => 0, debug => 0);
+
+    my($new2b) = grep { $_->{name} eq $new->{name} } $vm->list_virtual_networks();
+    is($new2b->{id_owner}, user_admin->id);
+
+    like($req_change->error,qr/not authorized/) or exit;
+
+    user_admin->grant($user, 'create_networks');
+    $req_change->status('requested');
+
+    wait_request(check_error => 0);
+    like($req_change->error,qr/not authorized/);
+    my($new2c) = grep { $_->{name} eq $new->{name} } $vm->list_virtual_networks();
+    is($new2c->{id_owner}, user_admin->id);
+
+    user_admin->grant($user, 'manage_all_networks');
+    $req_change->status('requested');
+    wait_request(check_error => 0);
+    is($req_change->error, '');
+
+    my($new3) = grep { $_->{name} eq $new->{name} } $vm->list_virtual_networks();
+    is($new3->{id_owner}, $user->id);
+
+    $new2->{id_owner} = user_admin->id;
+    my $req_change2 = Ravada::Request->change_network(
+        uid => $user->id
+        ,data => $new2
+    );
+
+    wait_request(check_error => 0);
+
+    is($req_change2->error, '');
+
+    my($new4) = grep { $_->{name} eq $new->{name} } $vm->list_virtual_networks();
+    is($new4->{id_owner}, user_admin->id);
+
+}
+
+sub test_add_network($vm) {
+    my $req_new = Ravada::Request->new_network(
+        uid => user_admin->id
+        ,id_vm => $vm->id
+        ,name => base_domain_name()
+    );
+    wait_request(debug => 0);
+    like($req_new->output , qr/\d+/) or exit;
+
+    my $net = decode_json($req_new->output);
+    my $name = $net->{name};
+
+    my $user = create_user();
+    my $req = Ravada::Request->create_network(
+        uid => $user->id
+        ,id_vm => $vm->id
+        ,data => $net
+    );
+    wait_request(check_error => 0);
+    like($req->error,qr/not authorized/);
+    my($new0) = grep { $_->{name} eq $name } $vm->list_virtual_networks();
+    ok(!$new0,"Expecting no new network $name created") or return;
+
+    $req = Ravada::Request->create_network(
+        uid => user_admin->id
+        ,id_vm => $vm->id
+        ,data => $net
+    );
+    wait_request( debug => 0);
+
+    my $out = decode_json($req->output);
+    my($new) = grep { $_->{name} eq $name } $vm->list_virtual_networks();
+    ok($new,"Expecting new network $name created") or die Dumper([$vm->list_virtual_networks]);
+    isa_ok($out,'HASH')
+    && is($out->{id_network},$new->{id});
+
+    like($new->{dhcp_start},qr/.*\.2$/);
+    like($new->{dhcp_end},qr/.*\.254$/);
+    ok($new->{internal_id});
+    is($new->{is_active},1);
+    is($new->{autostart},1);
+    is($new->{is_public},0);
+    return $new;
+}
+
+sub test_remove_user($vm) {
+    my $user = create_user();
+    user_admin->make_admin($user->id);
+    my $req = Ravada::Request->new_network(
+        uid => $user->id
+        ,id_vm => $vm->id
+        ,name => base_domain_name()
+    );
+    wait_request(debug => 0);
+
+    my $data = decode_json($req->output);
+    is($data->{id_vm},$vm->id);
+
+    my $req_create = Ravada::Request->create_network(
+        uid => $user->id
+        ,id_vm => $vm->id
+        ,data => $data
+    );
+    wait_request(debug => 0);
+
+    my($new0) = grep { $_->{name} eq $data->{name} } $vm->list_virtual_networks();
+    is($new0->{id_owner}, $user->id) or exit;
+
+    $user->remove();
+    wait_request(debug => 0);
+
+    my($new) = grep { $_->{name} eq $data->{name} } $vm->list_virtual_networks();
+    ok(!$new,"Expecting removed network $new0->{id} $data->{name}") or exit;
+}
+sub test_remove_network($vm, $net) {
+    my $user = create_user();
+    my $req = Ravada::Request->remove_network(
+        uid => $user->id
+        ,id => $net->{id}
+    );
+    wait_request(check_error => 0);
+    like($req->error,qr/not authorized/);
+    user_admin->make_admin($user->id);
+    $req->status('requested');
+
+    wait_request(debug => 0);
+
+    my($new) = grep { $_->{name} eq $net->{name} } $vm->list_virtual_networks();
+    ok(!$new,"Expecting removed network $net->{name}") or exit;
+}
+
+sub _check_network_changed($net, $field) {
+    my $sth = connector->dbh->prepare(
+        "SELECT * FROM virtual_networks WHERE id=?"
+    );
+    $sth->execute($net->{id});
+    my $row = $sth->fetchrow_hashref;
+    is($row->{$field},$net->{$field}, $field) or exit;
+}
+
+sub test_change_network($net) {
+    my %net2 = %$net;
+    $net2{dhcp_end} =~ s/(.*)\.\d+$/$1.100/;
+    my $req = Ravada::Request->change_network(
+        uid => user_admin->id
+        ,data => \%net2
+    );
+    wait_request();
+    my $vm = Ravada::VM->open($net->{id_vm});
+
+    my($new) = grep { $_->{name} eq $net->{name} } $vm->list_virtual_networks();
+    is($new->{dhcp_end},$net2{dhcp_end}) or die $net->{name};
+
+    _check_network_changed($new,'dhcp_end');
+
+    for my $is (0, 1) {
+        $net2{is_active} = $is;
+        $req = Ravada::Request->change_network(
+            uid => user_admin->id
+            ,data => \%net2
+        );
+        wait_request(debug => 0);
+
+        ($new) = grep { $_->{name} eq $net->{name} } $vm->list_virtual_networks();
+        is($new->{is_active},$is);
+        _check_network_changed($new,'is_active');
+    }
+
+    $net2{autostart} = 0;
+    $req = Ravada::Request->change_network(
+        uid => user_admin->id
+        ,data => \%net2
+    );
+    wait_request();
+
+    ($new) = grep { $_->{name} eq $net->{name} } $vm->list_virtual_networks();
+    is($new->{autostart},0);
+    _check_network_changed($new,'autostart');
+
+    for ( 1 .. 2 ) {
+        $net2{is_active} = (!$net2{is_active} or 0);
+        $req = Ravada::Request->change_network(
+            uid => user_admin->id
+            ,data => \%net2
+        );
+        wait_request();
+        my ($new2) = grep { $_->{name} eq $net2{name} } $vm->list_virtual_networks();
+        is($new2->{is_active},$net2{is_active});
+    }
+
+    for ( 1 .. 2 ) {
+        $net2{is_public} = (!$net2{is_public} or 0);
+        $req = Ravada::Request->change_network(
+            uid => user_admin->id
+            ,data => \%net2
+        );
+        wait_request(debug => 0);
+        my ($new2) = grep { $_->{name} eq $net2{name} } $vm->list_virtual_networks();
+        is($new2->{is_public}, $net2{is_public},"Expecting is_public=$net2{is_public}")
+            or die $net2{name};
+    }
+
+    my ($default) = grep { $_->{name} eq 'default' } $vm->list_virtual_networks();
+    $net2{bridge} = $default->{bridge};
+    $req = Ravada::Request->change_network(
+        uid => user_admin->id
+        ,data => \%net2
+    );
+    wait_request(check_error => 0, debug => 0);
+    like($req->error,qr/already in use/);
+
+    $net2{name} = new_domain_name();
+    $net2{bridge}='virbr99';
+    $req = Ravada::Request->change_network(
+        uid => user_admin->id
+        ,data => \%net2
+    );
+    wait_request(check_error => 0 );
+
+    like($req->error,qr/can not be renamed/);
+
+}
+
+sub test_assign_network($vm, $net) {
+    my $id_iso = search_id_iso('Alpine%');
+    my $name = new_domain_name();
+
+    my $req = Ravada::Request->create_domain(
+        id_iso => $id_iso
+        ,vm => $vm->type
+        ,name => $name
+        ,id_owner => user_admin->id
+        ,options => {
+            network => $net->{name}
+        }
+    );
+    wait_request(debug => 1);
+
+    my $domain = rvd_back->search_domain($name);
+    ok($domain);
+
+    _check_domain_network($domain, $net->{name});
+}
+
+sub test_assign_network_clone($vm, $net, $volatile) {
+    my $id_iso = search_id_iso('Alpine%');
+    my $name = new_domain_name();
+
+    my $req = Ravada::Request->create_domain(
+        id_iso => $id_iso
+        ,vm => $vm->type
+        ,name => $name
+        ,id_owner => user_admin->id
+    );
+    wait_request(debug => 0);
+
+    my $domain = rvd_back->search_domain($name);
+    $domain->prepare_base(user_admin);
+    $domain->volatile_clones(1);
+
+    my $name_clone = new_domain_name();
+    my $req2 = Ravada::Request->create_domain(
+        id_base => $domain->id
+        ,id_owner => user_admin->id
+        ,options => { network => $net->{name}}
+        ,name => $name_clone
+    );
+    wait_request(debug => 1);
+    my $clone = rvd_back->search_domain($name_clone);
+    ok($clone);
+
+    _check_domain_network($clone, $net->{name});
+}
+
+sub _check_domain_network_kvm($domain, $net_name) {
+
+    my $doc = XML::LibXML->load_xml(string => $domain->xml_description());
+
+    my ($net_source) = $doc->findnodes("/domain/devices/interface/source");
+    is($net_source->getAttribute('network'),$net_name);
+}
+
+sub _check_domain_network_void($domain, $net_name) {
+    my $config = $domain->get_config();
+    ok($config->{hardware}->{network}->[0]->{name}, $net_name);
+}
+
+sub _check_domain_network($domain, $net_name) {
+    if ($domain->type eq 'KVM') {
+        _check_domain_network_kvm($domain, $net_name);
+    } else {
+        _check_domain_network_void($domain, $net_name);
+    }
+}
+
+sub test_change_network_internal($vm, $net) {
+    if ($vm->type eq 'KVM') {
+        test_change_network_internal_kvm($vm, $net);
+    } elsif ($vm->type eq 'Void') {
+        test_change_network_internal_void($vm, $net);
+    }
+}
+
+sub test_change_network_internal_void($vm, $net) {
+    my $file_out = $vm->dir_img."/networks/".$net->{name}.".yml";
+
+    my $start_new = $net->{dhcp_start};
+    my ($n) = $start_new =~ /.*\.(\d+)/;
+    $n++;
+    $start_new =~ s/(.*)\.(\d+)/$1.$n/;
+    $net->{dhcp_start}=$start_new;
+
+    DumpFile($file_out,$net);
+
+    my ($net2) = grep { $_->{name} eq $net->{name} } $vm->list_virtual_networks();
+    is($net2->{dhcp_start},$start_new) or die Dumper($net2);
+}
+
+sub test_change_network_internal_kvm($vm, $net) {
+    my $network = $vm->vm->get_network_by_name($net->{name});
+    die "Error: I can't find network $net->{name}" if !$network;
+
+    my $doc = XML::LibXML->load_xml( string => $network->get_xml_description );
+    my ($range) = $doc->findnodes("/network/ip/dhcp/range");
+    my $start_new = $range->getAttribute('start');
+    my ($n) = $start_new =~ /.*\.(\d+)/;
+    $n++;
+    $start_new =~ s/(.*)\.(\d+)/$1.$n/;
+    $range->setAttribute('start' , $start_new);
+
+    $network->destroy();
+    $network= $vm->vm->define_network($doc->toString);
+    $network->create();
+
+    my $network2 = $vm->vm->get_network_by_name($net->{name});
+    my ($range2) = $doc->findnodes("/network/ip/dhcp/range");
+    is($range2->getAttribute('start'), $start_new) or exit;
+
+    my ($net2) = grep { $_->{name} eq $net->{name} } $vm->list_virtual_networks();
+    is($net2->{dhcp_start},$start_new) or exit;
+
+}
+
+sub test_changed_uuid($vm) {
+    if($vm->type eq 'KVM') {
+        test_changed_uuid_kvm($vm);
+    }
+}
+
+sub test_changed_uuid_kvm($vm) {
+    my $net = test_add_network($vm);
+
+    my $network = $vm->vm->get_network_by_name($net->{name});
+    my $doc = XML::LibXML->load_xml(string => $network->get_xml_description());
+
+    $network->destroy() if $network->is_active;
+    $network->undefine();
+
+    my ($uuid_xml) = $doc->findnodes("/network/uuid");
+    $uuid_xml->removeChildNodes();
+    my $new_uuid = $vm->_unique_uuid();
+    $uuid_xml->appendText($new_uuid);
+
+    $vm->vm->define_network($doc->toString());
+
+    my ($net2) = grep { $_->{name} eq $net->{name} } $vm->list_virtual_networks();
+    ok($net2,"Expecting $net->{name} found");
+    is($net2->{internal_id},$new_uuid) or die Dumper($net2);
+
+    my $sth = connector->dbh->prepare("SELECT * FROM virtual_networks "
+        ." WHERE name=?"
+    );
+    $sth->execute($net->{name});
+    my $row = $sth->fetchrow_hashref;
+    is($row->{internal_id},$new_uuid) or exit;
+
+}
+
+sub _remove_network_internal($vm,$name) {
+    if ($vm->type eq 'KVM') {
+        _remove_network_internal_kvm($vm, $name);
+    } elsif ($vm->type eq 'Void') {
+        _remove_network_internal_void($vm, $name);
+    } else {
+        die $vm->type;
+    }
+}
+
+sub _remove_network_internal_kvm($vm, $name) {
+    my $network = $vm->vm->get_network_by_name($name);
+    $network->destroy() if $network->is_active;
+    $network->undefine();
+}
+
+sub _remove_network_internal_void($vm, $name) {
+    my $file_out = $vm->dir_img."/networks/$name.yml";
+    unlink $file_out or die "$! $file_out" if $vm->file_exists($file_out);
+}
+
+sub test_disapeared_network($vm) {
+    my ($default0) = $vm->list_virtual_networks();
+
+    my $net = test_add_network($vm);
+    _remove_network_internal($vm, $net->{name});
+
+    my ($net2) = grep { $_->{name} eq $net->{name} } $vm->list_virtual_networks();
+    ok(!$net2, "Expecting $net->{name} removed");
+
+    my $sth = connector->dbh->prepare("SELECT * FROM virtual_networks WHERE name=?");
+    $sth->execute($net->{name});
+    my $row = $sth->fetchrow_hashref;
+    ok(!$row,"Expected $net->{name} removed from db".Dumper($row)) or exit;
+
+    my ($default) = grep { $_->{name} eq $default0->{name} } $vm->list_virtual_networks();
+    ok($default) or exit;
+
+    $sth->execute($default0->{name});
+    $row = $sth->fetchrow_hashref;
+    ok($row,"Expected default not removed from db".Dumper($row)) or exit;
+
+}
+
+sub test_add_down_network($vm) {
+
+    my $test = test_add_network($vm);
+    $test->{is_active} = 0;
+    my $req = Ravada::Request->change_network(
+        uid => user_admin->id
+        ,data => $test
+    );
+    wait_request();
+
+    my $sth = connector->dbh->prepare("DELETE FROM virtual_networks "
+        ." WHERE name=? "
+    );
+    $sth->execute($test->{name});
+
+    my ($net2) = grep { $_->{name} eq $test->{name} } $vm->list_virtual_networks();
+    ok($net2,"Expecting $test->{name} network listed") or exit;
+}
+
+sub test_public_network($vm, $net) {
+
+    my $net2 = dclone($net);
+    my $user2 = create_user();
+
+    my $req_change = Ravada::Request->change_network(
+        uid => $user2->id
+        ,data => {%$net, id_owner => $user2->id }
+    );
+
+    wait_request(check_error => 0);
+    like($req_change->error,qr/not authorized/);
+
+    my $req_change2 = Ravada::Request->change_network(
+        uid => user_admin->id
+        ,data => {%$net, id_owner => $user2->id, is_public=>1 }
+    );
+
+    wait_request(check_error => 0, debug => 0);
+    is($req_change2->error,'');
+
+    my $domain = create_domain($vm);
+    $domain->is_public(1);
+    $domain->prepare_base(user_admin);
+
+    user_admin->grant($user2,'change_settings');
+    user_admin->grant($user2,'create_networks');
+
+    my $clone = $domain->clone(user => $user2,name => new_domain_name);
+
+    my $hw_net = $clone->info(user_admin)->{hardware}->{network}->[0];
+    ok($hw_net) or die $clone->name;
+    my %hw_net2 = %$hw_net;
+
+    my $list_nets = rvd_front->list_networks($vm->id,$user2->id);
+    ok(scalar(@$list_nets) >= 1,"Expecting at least 1 network allowed, got "
+        .scalar(@$list_nets)) or exit;
+
+    $hw_net2{network}=$net->{name};
+    is($user2->can_change_hardware_network($clone, \%hw_net2),1) or exit;
+
+    my $req = Ravada::Request->change_hardware(
+        uid => $user2->id
+        ,id_domain => $clone->id
+        ,hardware => 'network'
+        ,index => 0
+        ,data => \%hw_net2
+    );
+    wait_request(check_error => 0, debug => 0);
+    is($req->error,'');
+
+
+    my $net3 = _search_network(id => $net->{id});
+    is($net3->{id_owner}, $user2->id) or exit;
+
+    is($user2->can_change_hardware_network($clone, {network => $net3->{name}}),1) or exit;
+
+    $req->status('requested');
+    wait_request(check_error => 0);
+    is($req->error, '');
+
+    my $clone4 = Ravada::Front::Domain->open($clone->id);
+    my $hw_net4 = $clone4->info(user_admin)->{hardware}->{network}->[0];
+
+    is($hw_net2{network}, $net->{name});
+}
+
+sub test_manage_all_networks($vm, $net) {
+
+    my $net2 = dclone($net);
+    my $user2 = create_user();
+
+    my $req_change2 = Ravada::Request->change_network(
+        uid => user_admin->id
+        ,data => {%$net, is_public=>0 }
+    );
+
+    wait_request(check_error => 0, debug => 0);
+    is($req_change2->error,'');
+
+    my $domain = create_domain($vm);
+    $domain->is_public(1);
+    $domain->prepare_base(user_admin);
+
+    user_admin->grant($user2,'change_settings');
+    user_admin->grant($user2,'manage_all_networks');
+
+    my $clone = $domain->clone(user => $user2,name => new_domain_name);
+
+    my $hw_net = $clone->info(user_admin)->{hardware}->{network}->[0];
+    ok($hw_net) or die $clone->name;
+    my %hw_net2 = %$hw_net;
+
+    $hw_net2{network}=$net->{name};
+    is($user2->can_change_hardware_network($clone, \%hw_net2),1) or exit;
+
+    my $req = Ravada::Request->change_hardware(
+        uid => $user2->id
+        ,id_domain => $clone->id
+        ,hardware => 'network'
+        ,index => 0
+        ,data => \%hw_net2
+    );
+    wait_request(check_error => 0, debug => 0);
+    is($req->error,'');
+
+    my $clone4 = Ravada::Front::Domain->open($clone->id);
+    my $hw_net4 = $clone4->info(user_admin)->{hardware}->{network}->[0];
+
+    is($hw_net2{network}, $net->{name});
+}
+
+sub test_new_network($vm) {
+    my $req = Ravada::Request->new_network(
+        uid => user_admin->id
+        ,id_vm => $vm->id
+        ,name => base_domain_name()."_"
+    );
+    wait_request();
+    my $data = decode_json($req->output);
+    is($data->{id_vm},$vm->id);
+
+    my $req_create = Ravada::Request->create_network(
+        uid => user_admin->id
+        ,id_vm => $vm->id
+        ,data => $data
+    );
+    wait_request(debug => 0);
+
+    my $req2 = Ravada::Request->new_network(
+        uid => user_admin->id
+        ,id_vm => $vm->id
+        ,name => base_domain_name()."_"
+    );
+    wait_request();
+    my $new_net = decode_json($req_create->output);
+
+    my $data2 = decode_json($req2->output);
+    for my $field( keys %$data) {
+        next if $field =~ /^(id_vm|ip_netmask|is_active|autostart)/;
+
+        isnt($data2->{$field}, $data->{$field},$field);
+    }
+    Ravada::Request->remove_network(
+        uid => user_admin->id
+        ,id => $new_net->{id_network}
+    );
+    wait_request();
+
+}
+
+########################################################################
+
+init();
+clean();
+
+for my $vm_name ( vm_names() ) {
+    diag("testing $vm_name");
+
+    SKIP: {
+
+        my $msg = "SKIPPED test: No $vm_name VM found ";
+        my $vm;
+        if ($vm_name eq 'KVM' && $>) {
+            $msg = "SKIPPED: Test must run as root";
+        } else {
+            $vm = Ravada::VM->open( type => $vm_name );
+        }
+
+        diag($msg)      if !$vm;
+        skip $msg,10    if !$vm;
+
+        is($vm->has_networking,1) if $vm_name eq 'KVM'
+        || $vm_name eq 'Void';
+        next if !$vm->has_networking();
+
+        test_remove_user($vm);
+
+        test_create_fail($vm);
+
+        test_list_networks($vm);
+
+        my $net = test_add_network($vm);
+        test_assign_network($vm, $net);
+        test_assign_network_clone($vm, $net, 0);
+        test_assign_network_clone($vm, $net, 1); # volatile clone
+
+        test_manage_all_networks($vm,$ net);
+        test_public_network($vm, $net);
+
+        test_change_owner($vm);
+
+        test_new_network($vm);
+
+        test_duplicate_add($vm, $net);
+
+        test_duplicate_bridge_add($vm, $net);
+
+        test_change_network_internal($vm, $net);
+        test_change_network($net);
+
+        test_changed_uuid($vm);
+
+        test_disapeared_network($vm);
+        test_add_down_network($vm);
+
+        test_remove_network($vm,$net);
+    }
+}
+
+end();
+
+done_testing();
+
diff --git a/t/vm/v10_volatile.t b/t/vm/v10_volatile.t
index a41de96c1..a3efdc752 100644
--- a/t/vm/v10_volatile.t
+++ b/t/vm/v10_volatile.t
@@ -13,7 +13,7 @@ no warnings "experimental::signatures";
 use feature qw(signatures);
 
 use_ok('Ravada');
-use_ok('Ravada::Network');
+use_ok('Ravada::Route');
 
 use lib 't/lib';
 use Test::Ravada;
diff --git a/templates/bootstrap/navigation.html.ep b/templates/bootstrap/navigation.html.ep
index 81459d664..233e7a5a5 100644
--- a/templates/bootstrap/navigation.html.ep
+++ b/templates/bootstrap/navigation.html.ep
@@ -45,9 +45,13 @@ navbar-dark bg-dark fixed-top navbar-expand-lg navbar-inverse">
 % }
 % if ($_user->is_admin) {
                             <a class="dropdown-item" href="/admin/nodes"><i class="fa fa-server" aria-hidden="true"></i>&nbsp;<%=l 'Nodes' %></a>
-                            <a class="dropdown-item" href="/admin/networks"><i class="fa fa-sitemap" aria-hidden="true"></i>&nbsp;<%=l 'Networks' %></a>
+                            <a class="dropdown-item" href="/admin/routes"><i class="fa fa-globe" aria-hidden="true"></i>&nbsp;<%=l 'Routes' %></a>
                             <a class="dropdown-item" href="/admin/storage"><i class="fa fa-hdd" aria-hidden="true"></i>&nbsp;<%=l 'Storage' %></a>
 % }
+
+% if ($_user->can_create_networks) {
+                            <a class="dropdown-item" href="/admin/networks"><i class="fa fa-sitemap" aria-hidden="true"></i>&nbsp;<%=l 'Networks' %></a>
+% }
                             <a class="dropdown-item" href="/admin/messages"><i class="fa fa-envelope" aria-hidden="true"></i>&nbsp;<%=l 'Messages' %></a>
 % if ($monitoring) {
                             <a class="dropdown-item" href="/admin/monitoring"><i class="fas fa-tachometer-alt" aria-hidden="true"></i>&nbsp;<%=l 'Monitoring' %></a>
diff --git a/templates/main/admin_networks.html.ep b/templates/main/admin_networks.html.ep
index 28fcb576d..69867c8dc 100644
--- a/templates/main/admin_networks.html.ep
+++ b/templates/main/admin_networks.html.ep
@@ -4,7 +4,8 @@
 <body id="page-top" data-spy="scroll" data-target=".navbar-fixed-top" role="document">
 <div id="wrapper">
     %= include 'bootstrap/navigation'
-    <div id="page-wrapper" ng-controller="manage_networks">
+    <div id="page-wrapper" ng-controller='manage_networks'
+        ng-init="init('<%= $id_vm %>')">
     <div id="admin-content">
         <div class="row">
             <div class="col-md-8"><h2><%=l 'Networks' %></h2></div>
@@ -15,36 +16,66 @@
                   </h2>
             </div>
         </div>
+
         <div class="row">
-            <div class="col-md-4"></div>
-            <!-- <div class="col-md-2">enabled</div> -->
-            <div class="col-md-2">
-                <%=l 'requires password' %>
+            <div class="col-md-2"><b><%=l 'Name' %></b></div>
+            <div class="col-md-1"
+                 ng-show="<%= ( $_user->is_admin or $_user->can_manage_all_networks or 0 ) %>"
+                ><b><%=l 'Public' %></b></div>
+
+            <div class="col-md-2"
+                 ng-show="<%= ( $_user->is_admin or $_user->can_manage_all_networks or 0 ) %>"
+                ><b><%=l 'Owner' %></b></div>
+            <div class="col-md-2"><b><%=l 'Bridge' %></b></div>
+            <div class="col-md-2"><b><%=l 'Address' %></b></div>
+            <div class="col-md-1"><b><%=l 'Active' %></b></div>
+            <div class="col-md-2"><b><%=l 'Auto Start' %></b></div>
+        </div>
+
+        <div class="row" ng-show="loaded_networks && networks.length==0">
+            <div class="col-md-12">
+                <div class="alert alert-warning">
+                <%=l 'No networks found' %>
+                </div>
             </div>
         </div>
-        <div ng-repeat="network in networks | orderObjectBy:'n_order'"
-            class="row"
+
+        <div ng-repeat="network in networks | orderObjectBy:'name'"
+             class="row" ng-cloak
         >
             <div class="col-md-2">
-                <a href="/network/settings/{{network.id}}.html">{{network.name}}</a>
+                <a href="/network/settings/{{network.id}}.html"
+                    ng-show="network._can_change">{{network.name}}</a>
+                <span ng-show="!network._can_change">{{network.name}}</span>
             </div>
-            <div class="col-md-2">{{network.address}}</div>
-            <!--
-            <div class="col-md-2">
-                <input type="checkbox" ng-model="network.no_domains"
-                    ng-true-value="0" ng-false-value="1"
-                    ng-change
-                    ="update_network(network.id,'no_domains')"
+            <div class="col-md-1"
+                 ng-show="<%= ( $_user->is_admin or $_user->can_manage_all_networks or 0 ) %>"
                 >
+
+                <span ng-show="network.is_public"
+                      >&#10004;</span>
+                <span ng-show="!network.is_public"
+                      >&#10005;</span>
+            </div>
+
+            <div class="col-md-2"
+                 ng-show="<%= ( $_user->is_admin or $_user->can_manage_all_networks or 0 ) %>"
+                >{{network._owner.name}}</div>
+            <div class="col-md-2">{{network.bridge}}</div>
+            <div class="col-md-2">{{network.ip_address}}</div>
+            <div class="col-md-1">
+                <span ng-show="network.is_active"
+                      >&#10004;</span>
+                <span ng-show="!network.is_active"
+                      >&#10005;</span>
             </div>
-            -->
             <div class="col-md-2">
-                <input type="checkbox" ng-model="network.requires_password"
-                    ng-true-value="1" ng-false-value="0"
-                    ng-change
-                    ="update_network(network.id,'requires_password')"
-                >
+                <span ng-show="network.autostart"
+                      >&#10004;</span>
+                <span ng-show="!network.autostart"
+                      >&#10005;</span>
             </div>
+
         </div>
     </div>
     </div>
diff --git a/templates/main/admin_routes.html.ep b/templates/main/admin_routes.html.ep
new file mode 100644
index 000000000..afed4cbe9
--- /dev/null
+++ b/templates/main/admin_routes.html.ep
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<html ng-app="ravada.app">
+%= include 'bootstrap/header'
+<body id="page-top" data-spy="scroll" data-target=".navbar-fixed-top" role="document">
+<div id="wrapper">
+    %= include 'bootstrap/navigation'
+    <div id="page-wrapper" ng-controller="manage_routes">
+    <div id="admin-content">
+        <div class="row">
+            <div class="col-md-8"><h2><%=l 'Routes' %></h2></div>
+            <div class="col-md-4" align="right">
+                  <h2><a type="button"
+                          class="btn btn-success" href="/route/new">
+                            <b><%=l 'New Route' %></b></a>
+                  </h2>
+            </div>
+        </div>
+        <div class="row">
+            <div class="col-md-4"></div>
+            <!-- <div class="col-md-2">enabled</div> -->
+            <div class="col-md-2">
+                <%=l 'requires password' %>
+            </div>
+        </div>
+        <div ng-repeat="network in routes | orderObjectBy:'n_order'"
+            class="row"
+        >
+            <div class="col-md-2">
+                <a href="/route/settings/{{network.id}}.html">{{network.name}}</a>
+            </div>
+            <div class="col-md-2">{{network.address}}</div>
+            <!--
+            <div class="col-md-2">
+                <input type="checkbox" ng-model="network.no_domains"
+                    ng-true-value="0" ng-false-value="1"
+                    ng-change
+                    ="update_network(network.id,'no_domains')"
+                >
+            </div>
+            -->
+            <div class="col-md-2">
+                <input type="checkbox" ng-model="network.requires_password"
+                    ng-true-value="1" ng-false-value="0"
+                    ng-change
+                    ="update_network(network.id,'requires_password')"
+                >
+            </div>
+        </div>
+    </div>
+    </div>
+</div>
+
+%= include $footer
+%= include 'bootstrap/scripts'
+</body>
+</html>
diff --git a/templates/main/manage_machine_edit_net.html.ep b/templates/main/manage_machine_edit_net.html.ep
index 9afaedc55..8606774e8 100644
--- a/templates/main/manage_machine_edit_net.html.ep
+++ b/templates/main/manage_machine_edit_net.html.ep
@@ -9,17 +9,23 @@
         </li>
         <li class="list-group-item list-group-item-primary"><%=l 'type' %></li>
         <li class="list-group-item">
+
+%       if ($USER->is_admin || $USER->can_create_networks || $USER->can_manage_all_networks ) {
             <select ng-model="item.type"
                     ng-change="network_edit[$index]=true"
                     ng-options="type for type in ['bridge','NAT']"
                     >
                     </select>
+%       } else {
+            {{item.type}}
+%       }
         </li>
         <li class="list-group-item list-group-item-primary">
             <span ng-show="item.type == 'NAT'"><%=l 'nat' %></span>
             <span ng-show="item.type == 'bridge'"><%=l 'bridge' %></span>
         </li>
         <li class="list-group-item">
+%       if ($USER->is_admin || $USER->can_create_networks || $USER->can_manage_all_networks ) {
             <select ng-model="item.network"
                         ng-change="network_edit[$index]=true"
                         ng-show="item.type == 'NAT'"
@@ -35,6 +41,10 @@
             <span ng-hide="item.type == 'NAT' || network_bridges[0]">
               <%=l 'No bridges found' %>
             </span>
+%       } else {
+            <span ng-show="item.type=='NAT'">{{item.network}}</span>
+            <span ng-show="item.type=='bridge'">{{item.bridge}}</span>
+%       }
         </li>
     </ul>
 </div>
diff --git a/templates/main/network_new.html.ep b/templates/main/network_new.html.ep
index b20f5efe4..79eb53170 100644
--- a/templates/main/network_new.html.ep
+++ b/templates/main/network_new.html.ep
@@ -9,7 +9,7 @@
     <div class="page-header">
         <div class="card"
              ng-controller="settings_network"
-             ng-init="init()"
+             ng-init="new_network(<%= $id_vm %>)"
         >
         <div class="card-header">
             <h2><%=l 'New network' %></h2>
@@ -17,9 +17,6 @@
         <div class="card-body">
 %=          include '/main/network_options'
         </div>
-        <div class="col-md-8 alert alert-info" ng-show="error && formNetwork.$pristine">
-            {{error}}
-        </div>
         </div><!-- card -->
     </div> <!-- page-header -->
 
diff --git a/templates/main/network_options.html.ep b/templates/main/network_options.html.ep
index d03ed5842..1376e0fac 100644
--- a/templates/main/network_options.html.ep
+++ b/templates/main/network_options.html.ep
@@ -1,92 +1,97 @@
-<div class="container-fluid" ng-hide="new_saved">
-<form name="formNetwork">
-<div class="row">
-    <div class="col-md-3" align="right"><%=l 'name' %></div>
+<div class="container-fluid" ng-cloak>
+
+<div class="col-md-8 alert alert-warning" ng-show="error">
+{{error}}
+</div>
+
+<i class="fas fa-sync-alt fa-spin" ng-hide="network.id_vm"></i>
+
+<form name="form_network" ng-hide="network._removed || !network.id_vm">
+
+<div class="form-group row">
+    <div class="col-md-3" align="right"><%=l 'Name' %></div>
     <div class="col-md-8">
-        <input type="text" name="name" ng-model="network.name" required
-            ng-change='check_duplicate("name")'
-        >
-        <span ng-show="formNetwork.name.$error.required">
-          <%=l 'Network name is required' %>
-        </span>
-        <span ng-show="network._duplicated_name">
-          <%=l 'This name is duplicated' %>
-        </span>
+        <input type="text" ng-model="network.name" required
+            name="name"
+        ng-disabled="network.id"/>
     </div>
 </div>
 
-<div class="row">
-    <div class="col-md-3" align="right"><%=l 'address' %></div>
-    <div class="col-md-8" align="left">
-        <input type="text" name="address" ng-model="network.address" required ipaddress
-            ng-change='check_duplicate("address")'
-        >
-        <span ng-show="formNetwork.address.$error.required"><%=l 'Network address is required.' %></span>
-        <span ng-show="formNetwork.$error.ipformat"><%=l 'Invalid IP network address. Expecting a.b.c.d/e' %></span>
-        <span ng-show="network._duplicated_address"><%=l 'This address is duplicated' %></span>
+<div class="form-group row">
+    <div class="col-md-3" align="right"><%=l 'Bridge' %></div>
+    <div class="col-md-8">
+        <input type="text" ng-model="network.bridge" required
+            name="bridge"
+        />
     </div>
 </div>
 
-<div class="row">
-    <div class="col-md-3" align="right"><%=l 'password' %></div>
-    <div class="col-md-1" align="left">
-        <input type="checkbox" name="address" ng-model="network.requires_password"
-            ng-true-value="1" ng-false-value="0"
-        >
+<div class="form-group row">
+    <div class="col-md-3" align="right"><%=l 'IP address' %></div>
+    <div class="col-md-3">
+        <input type="text" required ng-model="network.ip_address"/>
+    </div>
+</div>
+
+<div class="form-group row">
+    <div class="col-md-3" align="right"><%=l 'Netmask' %></div>
+    <div class="col-md-3">
+        <input type="text" required ng-model="network.ip_netmask"/>
+    </div>
+</div>
+
+<div class="form-group row">
+    <div class="col-md-3" align="right"><%=l 'DHCP start' %></div>
+    <div class="col-md-2">
+        <input type="text" ng-model="network.dhcp_start"/>
+    </div>
+</div>
+
+<div class="form-group row">
+    <div class="col-md-3" align="right"><%=l 'DHCP end' %></div>
+    <div class="col-md-3">
+        <input type="text" ng-model="network.dhcp_end"/>
     </div>
-    <div class="col-md-8"><%=l 'Requires viewer password when implemented' %></div>
 </div>
 
-<div class="row">
-  <div class="col-md-3" align="right">
-    <%=l 'All machines' %>
-  </div>
-    <div class="col-md-1" align="left">
-        <input type="checkbox" name="address" ng-model="network.all_domains"
+<div class="form-group row">
+    <div class="col-md-3" align="right"><%=l 'Auto Start' %></div>
+    <div class="col-md-3">
+        <input type="checkbox" ng-model="network.autostart"
             ng-true-value="1" ng-false-value="0"
-            ng-change="check_all_domains()"
-        >
+        />
     </div>
-    <div class="col-md-8"><%=l 'Users from this network can run all virtual machines' %></div>
 </div>
 
-<div class="row">
-    <div class="col-md-3" align="right"><%=l 'No machines' %></div>
-    <div class="col-md-1" align="left">
-        <input type="checkbox" name="address" ng-model="network.no_domains"
+<div class="form-group row">
+    <div class="col-md-3" align="right"><%=l 'Active' %></div>
+    <div class="col-md-3">
+        <input type="checkbox" ng-model="network.is_active"
             ng-true-value="1" ng-false-value="0"
-            ng-change="check_no_domains()"
-        >
+        />
     </div>
-    <div class="col-md-8"><%=l 'Users from this network can run no virtual machines' %></div>
 </div>
 
+<div class="form-group row">
+    <div class="col-md-3" align="right"><%=l 'Public' %></div>
+    <div class="col-md-3">
+        <input type="checkbox" ng-model="network.is_public"
+            ng-true-value="1" ng-false-value="0"
+        />
+    </div>
+</div>
 
-<div class="row">
+<div class="form-group row">
     <div class="col-md-3" align="right"></div>
     <div class="col-md-4" align="left">
         <button ng-show="network.id" class="btn btn-outline-secondary"
-            ng-disabled="formNetwork.$pristine"
-        ng-click="load_network(network.id)"><%=l 'Cancel' %></button>
-        <button
-            class="btn btn-primary"
-            ng-click="update_network()"
-            ng-disabled="!formNetwork.$valid || formNetwork.$pristine
-            || (!network.all_domains && !network.no_domains)
-            || network._duplicated_name
-            || network._duplicated_address
-            "><%=l 'Save' %></button>
-    </div>
-    <div ng-show="!network.all_domains && !network.no_domains">
-        <%=l 'Set either' %> <i><%=l 'all machines' %></i> <%=l 'or' %> <i><%=l 'no machines' %></i> <%=l 'for users in this network' %>
+            ng-click="load_network(network.id)"><%=l 'Cancel' %></button>
+        <button class="btn btn-primary"
+            ng-click="update_network(); form_options.$pristine=true"
+            ><%=l 'Save' %></button>
     </div>
 </div>
 
 </form>
 </div>
 
-<div class="row">
-    <div class="col-md-8 alert alert-info" ng-show="saved && formNetwork.$pristine">
-        <%=l 'Network' %> {{network.name}} <%=l 'saved.' %>
-    </div>
-</div>
diff --git a/templates/main/network_remove.html.ep b/templates/main/network_remove.html.ep
index 9043f8044..5d738edd3 100644
--- a/templates/main/network_remove.html.ep
+++ b/templates/main/network_remove.html.ep
@@ -1,8 +1,14 @@
-<div ng-show="network && network.id">
+<div ng-show="network && network.id && !network._removed">
     <%=l 'Are you sure you want to remove the' %> <%= $item %> {{network.name}}
+    <br/>
 
-    <button ng-click="remove_network(network.id)"><%=l 'Yes' %></button>
-    <button onclick="location='/admin/networks'"><%=l 'Cancel' %></button>
+    <button class="btn btn-outline-secondary" ng-click="remove_network(network.id)"><%=l 'Yes' %></button>
+    <button class="btn btn-outline-secondary" onclick="location='/admin/networks'"><%=l 'Cancel' %></button>
+</div>
+
+<div ng-show="network && network._removed">
+    <%=l 'Network removed' %>.
+    <a href='/admin/networks'><%=l 'List Networks' %></a>
 </div>
 
 <div ng-show="message">{{message}}</div>
diff --git a/templates/main/not_found.html.ep b/templates/main/not_found.html.ep
new file mode 100644
index 000000000..d1b308f46
--- /dev/null
+++ b/templates/main/not_found.html.ep
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+%= include 'bootstrap/header'
+<body role="document">
+    %= include 'bootstrap/navigation'
+     <header id="page-top" class="intro">
+        <div class="intro-body">
+            <div class="container" ng-controller="">
+                <h2 class="form-signin-heading">ERROR</h2>
+                <p><%= $error %></p>
+            </div>
+        </div>
+    </header>
+
+    %= include 'bootstrap/scripts'
+    %= include $footer
+</body>
+</html>
diff --git a/templates/main/network_machines.html.ep b/templates/main/route_machines.html.ep
similarity index 91%
rename from templates/main/network_machines.html.ep
rename to templates/main/route_machines.html.ep
index 6c4311b11..f84beda01 100644
--- a/templates/main/network_machines.html.ep
+++ b/templates/main/route_machines.html.ep
@@ -5,7 +5,7 @@
 <div class="row">
     <div class="col-md-3" align="right"><%=l 'All machines' %></div>
     <div class="col-md-1" align="left">
-        <input type="checkbox" name="address" ng-model="network.all_domains"
+        <input type="checkbox" name="address" ng-model="route.all_domains"
             ng-true-value="1" ng-false-value="0"
             ng-change="check_all_domains(); update_network()"
         >
@@ -18,7 +18,7 @@
 <div class="row">
     <div class="col-md-3" align="right"><%=l 'No machines' %></div>
     <div class="col-md-1" align="left">
-        <input type="checkbox" name="address" ng-model="network.no_domains"
+        <input type="checkbox" name="address" ng-model="route.no_domains"
             ng-true-value="1" ng-false-value="0"
             ng-change="check_no_domains(); update_network()"
         >
@@ -30,7 +30,7 @@
 <hr/>
 
 
-<div ng-show="network.no_domains == 0">
+<div ng-show="route.no_domains == 0">
 <div class="row">
     <div class="col-md-2">
       <%=l 'Public' %>
diff --git a/templates/main/route_new.html.ep b/templates/main/route_new.html.ep
new file mode 100644
index 000000000..4f00021a2
--- /dev/null
+++ b/templates/main/route_new.html.ep
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html ng-app="ravada.app">
+%= include 'bootstrap/header'
+<body id="page-top" data-spy="scroll" data-target="fixed-top" role="document">
+<div id="wrapper">
+    %= include 'bootstrap/navigation'
+<div id="page-wrapper">
+        <!--BASES AND DOMAINS LIST-->
+    <div class="page-header">
+        <div class="card"
+             ng-controller="settings_route"
+             ng-init="init()"
+        >
+        <div class="card-header">
+            <h2><%=l 'New Route' %></h2>
+        </div>
+        <div class="card-body">
+%=          include '/main/route_options'
+        </div>
+        <div class="col-md-8 alert alert-info" ng-show="error && formNetwork.$pristine">
+            {{error}}
+        </div>
+        </div><!-- card -->
+    </div> <!-- page-header -->
+
+</div><!-- page-wrapper -->
+</div><!-- wrapper -->
+%= include $footer
+%= include 'bootstrap/scripts'
+</body>
+</html>
diff --git a/templates/main/route_options.html.ep b/templates/main/route_options.html.ep
new file mode 100644
index 000000000..910c7daa5
--- /dev/null
+++ b/templates/main/route_options.html.ep
@@ -0,0 +1,92 @@
+<div class="container-fluid" ng-hide="new_saved">
+<form name="formNetwork">
+<div class="row">
+    <div class="col-md-3" align="right"><%=l 'name' %></div>
+    <div class="col-md-8">
+        <input type="text" name="name" ng-model="route.name" required
+            ng-change='check_duplicate("name")'
+        >
+        <span ng-show="formNetwork.name.$error.required">
+          <%=l 'Network name is required' %>
+        </span>
+        <span ng-show="route._duplicated_name">
+          <%=l 'This name is duplicated' %>
+        </span>
+    </div>
+</div>
+
+<div class="row">
+    <div class="col-md-3" align="right"><%=l 'address' %></div>
+    <div class="col-md-8" align="left">
+        <input type="text" name="address" ng-model="route.address" required ipaddress
+            ng-change='check_duplicate("address")'
+        >
+        <span ng-show="formNetwork.address.$error.required"><%=l 'Network address is required.' %></span>
+        <span ng-show="formNetwork.$error.ipformat"><%=l 'Invalid IP network address. Expecting a.b.c.d/e' %></span>
+        <span ng-show="route._duplicated_address"><%=l 'This address is duplicated' %></span>
+    </div>
+</div>
+
+<div class="row">
+    <div class="col-md-3" align="right"><%=l 'password' %></div>
+    <div class="col-md-1" align="left">
+        <input type="checkbox" name="address" ng-model="route.requires_password"
+            ng-true-value="1" ng-false-value="0"
+        >
+    </div>
+    <div class="col-md-8"><%=l 'Requires viewer password when implemented' %></div>
+</div>
+
+<div class="row">
+  <div class="col-md-3" align="right">
+    <%=l 'All machines' %>
+  </div>
+    <div class="col-md-1" align="left">
+        <input type="checkbox" name="address" ng-model="route.all_domains"
+            ng-true-value="1" ng-false-value="0"
+            ng-change="check_all_domains()"
+        >
+    </div>
+    <div class="col-md-8"><%=l 'Users from this network can run all virtual machines' %></div>
+</div>
+
+<div class="row">
+    <div class="col-md-3" align="right"><%=l 'No machines' %></div>
+    <div class="col-md-1" align="left">
+        <input type="checkbox" name="address" ng-model="route.no_domains"
+            ng-true-value="1" ng-false-value="0"
+            ng-change="check_no_domains()"
+        >
+    </div>
+    <div class="col-md-8"><%=l 'Users from this network can run no virtual machines' %></div>
+</div>
+
+
+<div class="row">
+    <div class="col-md-3" align="right"></div>
+    <div class="col-md-4" align="left">
+        <button ng-show="route.id" class="btn btn-outline-secondary"
+            ng-disabled="formNetwork.$pristine"
+        ng-click="load_network(route.id)"><%=l 'Cancel' %></button>
+        <button
+            class="btn btn-primary"
+            ng-click="update_network()"
+            ng-disabled="!formNetwork.$valid || formNetwork.$pristine
+            || (!route.all_domains && !route.no_domains)
+            || route._duplicated_name
+            || route._duplicated_address
+            "><%=l 'Save' %></button>
+    </div>
+    <div ng-show="!route.all_domains && !route.no_domains">
+        <%=l 'Set either' %> <i><%=l 'all machines' %></i> <%=l 'or' %> <i><%=l 'no machines' %></i> <%=l 'for users in this network' %>
+    </div>
+</div>
+
+</form>
+</div>
+
+<div class="row">
+    <div class="col-md-8 alert alert-info" ng-show="saved && formNetwork.$pristine">
+        <%=l 'Network' %> {{route.name}} <%=l 'saved.' %>
+    </div>
+</div>
diff --git a/templates/main/route_remove.html.ep b/templates/main/route_remove.html.ep
new file mode 100644
index 000000000..8fcfd8243
--- /dev/null
+++ b/templates/main/route_remove.html.ep
@@ -0,0 +1,8 @@
+<div ng-show="route && route.id">
+    <%=l 'Are you sure you want to remove the' %> <%= $item %> {{route.name}}
+
+    <button ng-click="remove_route(route.id)"><%=l 'Yes' %></button>
+    <button onclick="location='/admin/routes'"><%=l 'Cancel' %></button>
+</div>
+
+<div ng-show="message">{{message}}</div>
diff --git a/templates/main/settings_generic.html.ep b/templates/main/settings_generic.html.ep
index 0760b8ae4..0f19a787d 100644
--- a/templates/main/settings_generic.html.ep
+++ b/templates/main/settings_generic.html.ep
@@ -10,9 +10,22 @@
         <div class="card"
              ng-cloak="1"
              ng-controller="settings_<%= $item %>"
-             ng-init="init('<%= $id %>', '<%= url_for('ws_subscribe')->to_abs %>')"
+             ng-init="init('<%= $id %>', '<%= url_for('ws_subscribe')->to_abs %>'
+                      ,'<%= $id_vm %>')"
         >
-            <div class="card-header"><h2><%= $item  %> {{<%= $item %>._old_name}}</h2></div>
+            <div class="card-header">
+                <div class="row">
+                    <div class="col">
+                        <span class="title">
+                            <%= $item  %> {{<%= $item %>._old_name}}
+                        </span>
+                    </div>
+                    <div class="col" align="right">
+                        <a type="button" class="btn btn-outline-secondary" align="right"
+                            href="/admin/<%= $item %>s">back</a>
+                    </div>
+                </div>
+            </div>
             <div class="card-body">
 				<div class="row">
                     %= include "main/settings_generic_tabs"
diff --git a/templates/main/settings_generic_tabs.html.ep b/templates/main/settings_generic_tabs.html.ep
index 925f3246f..8ef327dee 100644
--- a/templates/main/settings_generic_tabs.html.ep
+++ b/templates/main/settings_generic_tabs.html.ep
@@ -1,7 +1,8 @@
 % my %tabs_item = (
 %    node => ['options' , 'bases', 'remove' , 'hostdev']
-%   ,network => ['options', 'machines', 'remove']
+%   ,route => ['options', 'machines', 'remove']
 %   ,storage => ['list', 'options', 'purge']
+%   ,network => ['options','remove']
 % );
 % my $tabs = $tabs_item{$item};
 
@@ -12,7 +13,7 @@
 % my $active="active";
 % my $selected="true";
 <div class="col-2">
-<div class="card-body" ng-show="<%= $item %>.id">
+<div class="card-body">
 <div class="nav flex-column nav-pills bg-light" id="v-pills-tab"
     role="tablist" aria-orientation="vertical">
 % for my $current (@$tabs) {
@@ -28,6 +29,7 @@
     }
 %>
 <a class="nav-link <%= $active %>" id="v-pills-<%= $current %>-tab"
+    ng-hide="<%= $item %>._removed"
     href="#v-pills-<%= $current %>" data-toggle="pill" role="tab"
         aria-controls="v-pills-bases" aria-selected="<%= $selected %>"><%=l $current %></a>
 % $active = '';
@@ -52,6 +54,7 @@
     }
 %>
     <div class="tab-pane fade show <%= $active %>"
+    ng-show="<%= $item %>.id"
     id="v-pills-<%= $current %>" role="tabpanel"
     aria-labelledby="v-pills-<%= $current %>-tab">
         %= include "main/${item}_$current"