]> git.proxmox.com Git - pve-container.git/blobdiff - src/PVE/LXC/Setup/Base.pm
configure IPv6AcceptRA in systemd-networkd files
[pve-container.git] / src / PVE / LXC / Setup / Base.pm
index e1fd0f3daa8dade2f2b7f9eb19d45f8cc75f671b..f5f8b0d7a74351ec17034645145c7114da57bbd2 100644 (file)
@@ -10,15 +10,16 @@ use Encode;
 use Fcntl;
 use File::Path;
 use File::Spec;
+use File::Basename;
 
 use PVE::INotify;
 use PVE::Tools;
 use PVE::Network;
 
 sub new {
-    my ($class, $conf, $rootdir) = @_;
+    my ($class, $conf, $rootdir, $os_release) = @_;
 
-    return bless { conf => $conf, rootdir => $rootdir }, $class;
+    return bless { conf => $conf, rootdir => $rootdir, os_release => $os_release }, $class;
 }
 
 sub lookup_dns_conf {
@@ -27,27 +28,24 @@ sub lookup_dns_conf {
     my $nameserver = $conf->{nameserver};
     my $searchdomains = $conf->{searchdomain};
 
-    if (!($nameserver && $searchdomains)) {
-
-       if ($conf->{'testmode'}) {
-           
-           $nameserver = "8.8.8.8 8.8.8.9";
-           $searchdomains = "proxmox.com";
-       
-       } else {
-
-           my $host_resolv_conf = $self->{host_resolv_conf};
+    if ($conf->{'testmode'}) {
+       return ('proxmox.com', '8.8.8.8 8.8.8.9');
+    }
 
-           $searchdomains = $host_resolv_conf->{search};
+    my $host_resolv_conf = $self->{host_resolv_conf};
 
-           my @list = ();
-           foreach my $k ("dns1", "dns2", "dns3") {
-               if (my $ns = $host_resolv_conf->{$k}) {
-                   push @list, $ns;
-               }
+    if (!defined($nameserver)) {
+       my @list = ();
+       foreach my $k ("dns1", "dns2", "dns3") {
+           if (my $ns = $host_resolv_conf->{$k}) {
+               push @list, $ns;
            }
-           $nameserver = join(' ', @list);
        }
+       $nameserver = join(' ', @list);
+    }
+
+    if (!defined($searchdomains)) {
+       $searchdomains = $host_resolv_conf->{search};
     }
 
     return ($searchdomains, $nameserver);
@@ -56,7 +54,8 @@ sub lookup_dns_conf {
 sub update_etc_hosts {
     my ($self, $hostip, $oldname, $newname, $searchdomains) = @_;
 
-    my $done = 0;
+    my $hosts_fn = '/etc/hosts';
+    return if $self->ct_is_file_ignored($hosts_fn);
 
     my $namepart = ($newname =~ s/\..*$//r);
 
@@ -74,7 +73,6 @@ sub update_etc_hosts {
 
     # Prepare section:
     my $section = '';
-    my $hosts_fn = '/etc/hosts';
 
     my $lo4 = "127.0.0.1 localhost.localnet localhost\n";
     my $lo6 = "::1 localhost.localnet localhost\n";
@@ -92,6 +90,8 @@ sub update_etc_hosts {
 
     if (defined($hostip)) {
        $section .= "$hostip $all_names\n";
+    } elsif ($namepart ne 'localhost') {
+       $section .= "127.0.1.1 $all_names\n";
     } else {
        $section .= "127.0.1.1 $namepart\n";
     }
@@ -158,7 +158,7 @@ sub setup_init {
 sub setup_systemd_console {
     my ($self, $conf) = @_;
 
-    my $systemd_dir_rel = -x "/lib/systemd/systemd" ?
+    my $systemd_dir_rel = $self->ct_is_executable("/lib/systemd/systemd") ?
        "/lib/systemd/system" : "/usr/lib/systemd/system";
 
     my $systemd_getty_service_rel = "$systemd_dir_rel/getty\@.service";
@@ -197,11 +197,22 @@ sub setup_systemd_console {
     }
 }
 
+# A few distros as well as unprivileged containers cannot deal with the
+# /dev/lxc/ tty subdirectory.
+sub devttydir {
+    my ($self, $conf) = @_;
+    return $conf->{unprivileged} ? '' : 'lxc/';
+}
+
 sub setup_container_getty_service {
-    my ($self) = @_;
-    my $servicefile = '/usr/lib/systemd/system/container-getty@.service';
+    my ($self, $conf) = @_;
+
+    my $systemd_dir_rel = $self->ct_is_executable("/lib/systemd/systemd") ?
+       "/lib/systemd/system" : "/usr/lib/systemd/system";
+    my $servicefile = "$systemd_dir_rel/container-getty\@.service";
     my $raw = $self->ct_file_get_contents($servicefile);
-    if ($raw =~ s@pts/%I@lxc/tty%I@g) {
+    my $ttyname = $self->devttydir($conf) . 'tty%I';
+    if ($raw =~ s@pts/%I|lxc/tty%I@$ttyname@g) {
        $self->ct_file_set_contents($servicefile, $raw);
     }
 }
@@ -231,6 +242,7 @@ DATA
        my @DHCPMODES = ('none', 'v4', 'v6', 'both');
        my ($NONE, $DHCP4, $DHCP6, $BOTH) = (0, 1, 2, 3);
        my $dhcp = $NONE;
+       my $accept_ra = 'false';
 
        if (defined(my $ip = $d->{ip})) {
            if ($ip eq 'dhcp') {
@@ -250,19 +262,24 @@ DATA
        if (defined(my $ip = $d->{ip6})) {
            if ($ip eq 'dhcp') {
                $dhcp |= $DHCP6;
+           } elsif ($ip eq 'auto') {
+               $accept_ra = 'true';
            } elsif ($ip ne 'manual') {
                $has_ipv6 = 1;
                $data .= "Address = $ip\n";
            }
        }
        if (defined(my $gw = $d->{gw6})) {
+           $accept_ra = 'false';
            $data .= "Gateway = $gw\n";
-           if ($has_ipv6 && !PVE::Network::is_ip_in_cidr($gw, $d->{ip6}, 6)) {
+           if ($has_ipv6 && !PVE::Network::is_ip_in_cidr($gw, $d->{ip6}, 6) &&
+               !PVE::Network::is_ip_in_cidr($gw, 'fe80::/10', 6)) {
                $routes .= "\n[Route]\nDestination = $gw/128\nScope = link\n";
            }
        }
 
        $data .= "DHCP = $DHCPMODES[$dhcp]\n";
+       $data .= "IPv6AcceptRA = $accept_ra\n";
        $data .= $routes if $routes;
 
        $self->ct_file_set_contents($filename, $data);
@@ -273,6 +290,16 @@ sub setup_securetty {
     my ($self, $conf, @add) = @_;
 
     my $filename = "/etc/securetty";
+    # root login is already allowed on every device if no securetty present
+    return if !$self->ct_file_exists($filename);
+
+    if (!scalar(@add)) {
+       @add = qw(console tty1 tty2 tty3 tty4);
+       if (my $dir = $self->devttydir($conf)) {
+           @add = map { "${dir}$_" } @add;
+       }
+    }
+
     my $data = $self->ct_file_get_contents($filename);
     chomp $data; $data .= "\n";
     foreach my $dev (@add) {
@@ -304,10 +331,6 @@ my $replacepw  = sub {
 
        my $last_change = int(time()/(60*60*24));
 
-       if ($epw =~ m/^\$TEST\$/) { # for regression tests
-           $last_change = 12345;
-       }
-       
        while (defined (my $line = <$src>)) {
            if ($shadow) {
                $line =~ s/^${user}:[^:]*:[^:]*:/${user}:${epw}:${last_change}:/;
@@ -338,9 +361,9 @@ sub set_user_password {
     my $shadow = "/etc/shadow";
     
     if (defined($opt_password)) {
-       if ($opt_password !~ m/^\$/) {
+       if ($opt_password !~ m/^\$(?:1|2[axy]?|5|6)\$[a-zA-Z0-9.\/]{1,16}\$[a-zA-Z0-9.\/]+$/) {
            my $time = substr (Digest::SHA::sha1_base64 (time), 0, 8);
-           $opt_password = crypt(encode("utf8", $opt_password), "\$1\$$time\$");
+           $opt_password = crypt(encode("utf8", $opt_password), "\$6\$$time\$");
        };
     } else {
        $opt_password = '*';
@@ -354,6 +377,34 @@ sub set_user_password {
     }
 }
 
+my $parse_home_dir = sub {
+    my ($self, $passwdfile, $user) = @_;
+
+    my $fh = $self->ct_open_file_read($passwdfile);
+    while (defined (my $line = <$fh>)) {
+       return $2
+           if $line =~ m/^${user}:([^:]*:){4}([^:]*):/;
+    }
+};
+
+sub set_user_authorized_ssh_keys {
+    my ($self, $conf, $user, $ssh_keys) = @_;
+
+    my $passwd = "/etc/passwd";
+    my $home = $user eq "root" ? "/root/" : "/home/$user/";
+
+    $home = &$parse_home_dir($self, $passwd, $user)
+       if $self->ct_file_exists($passwd);
+
+    die "home directory '$home' of $user does not exist!"
+       if ! ($self->ct_is_directory($home) || $self->ct_is_symlink($home));
+
+    $self->ct_mkdir("$home/.ssh", 0700)
+       if ! $self->ct_is_directory("$home/.ssh");
+
+    $self->ct_modify_file("$home/.ssh/authorized_keys", $ssh_keys, perms => 0700);
+}
+
 my $randomize_crontab = sub {
     my ($self, $conf) = @_;
 
@@ -396,13 +447,14 @@ sub pre_start_hook {
 }
 
 sub post_create_hook {
-    my ($self, $conf, $root_password) = @_;
+    my ($self, $conf, $root_password, $ssh_keys) = @_;
 
     $self->template_fixup($conf);
     
     &$randomize_crontab($self, $conf);
     
     $self->set_user_password($conf, 'root', $root_password);
+    $self->set_user_authorized_ssh_keys($conf, 'root', $ssh_keys) if $ssh_keys;
     $self->setup_init($conf);
     $self->setup_network($conf);
     $self->set_hostname($conf);
@@ -414,10 +466,20 @@ sub post_create_hook {
 # File access wrappers for container setup code.
 # For user-namespace support these might need to take uid and gid maps into account.
 
+sub ct_is_file_ignored {
+    my ($self, $file) = @_;
+    my ($name, $path) = fileparse($file);
+    return -f "$path/.pve-ignore.$name";
+}
+
 sub ct_reset_ownership {
     my ($self, @files) = @_;
     my $conf = $self->{conf};
     return if !$self->{id_map};
+
+    @files = grep { !$self->ct_is_file_ignored($_) } @files;
+    return if !@files;
+
     my $uid = $self->{rootuid};
     my $gid = $self->{rootgid};
     chown($uid, $gid, @files);
@@ -436,12 +498,14 @@ sub ct_mkdir {
 sub ct_unlink {
     my ($self, @files) = @_;
     foreach my $file (@files) {
+       next if $self->ct_is_file_ignored($file);
        CORE::unlink($file);
     }
 }
 
 sub ct_rename {
     my ($self, $old, $new) = @_;
+    return if $self->ct_is_file_ignored($new);
     CORE::rename($old, $new);
 }
 
@@ -454,6 +518,7 @@ sub ct_open_file_read {
 sub ct_open_file_write {
     my $self = shift;
     my $file = shift;
+    $file = '/dev/null' if $self->ct_is_file_ignored($file);
     my $fh = IO::File->new($file, O_WRONLY | O_CREAT, @_);
     $self->ct_reset_ownership($fh);
     return $fh;
@@ -475,9 +540,15 @@ sub ct_make_path {
 
 sub ct_symlink {
     my ($self, $old, $new) = @_;
+    return if $self->ct_is_file_ignored($new);
     return CORE::symlink($old, $new);
 }
 
+sub ct_readlink {
+    my ($self, $name) = @_;
+    return CORE::readlink($name);
+}
+
 sub ct_file_exists {
     my ($self, $file) = @_;
     return -f $file;
@@ -493,6 +564,11 @@ sub ct_is_symlink {
     return -l $file;
 }
 
+sub ct_is_executable {
+    my ($self, $file) = @_;
+    return -x $file
+}
+
 sub ct_stat {
     my ($self, $file) = @_;
     return File::stat::stat($file);
@@ -510,6 +586,7 @@ sub ct_file_get_contents {
 
 sub ct_file_set_contents {
     my ($self, $file, $data, $perms) = @_;
+    return if $self->ct_is_file_ignored($file);
     PVE::Tools::file_set_contents($file, $data, $perms);
     $self->ct_reset_ownership($file);
 }
@@ -518,10 +595,12 @@ sub ct_file_set_contents {
 # Optionally if the file becomes empty it will be deleted.
 sub ct_modify_file {
     my ($self, $file, $data, %options) = @_;
+    return if $self->ct_is_file_ignored($file);
 
     my $head = "# --- BEGIN PVE ---\n";
     my $tail = "# --- END PVE ---\n";
     my $perms = $options{perms};
+    $data .= "\n" if $data && $data !~ /\n$/;
 
     if (!$self->ct_file_exists($file)) {
        $self->ct_file_set_contents($file, $head.$data.$tail, $perms) if $data;