diff --git a/.gitignore b/.gitignore index 91d19a1..e1539d3 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,8 @@ # ignore configuration file /config.yml /config*.yml +/config.yaml +/config*.yaml # Ignore Vagrant stuff /.vagrant diff --git a/cmd/diagnostics.go b/cmd/diagnostics.go index 001b6c4..c7362e1 100644 --- a/cmd/diagnostics.go +++ b/cmd/diagnostics.go @@ -65,7 +65,7 @@ func newDiagnosticsCommand() *cobra.Command { // - the docker debug output // - running docker containers // - logs -func diagnosticsCmdRun(cmd *cobra.Command, args []string) { +func diagnosticsCmdRun(*cobra.Command, []string) { questions := []*survey.Question{ { Name: "IncludeEndpoints", diff --git a/cmd/root.go b/cmd/root.go index 43cc97f..6aa7d6d 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -16,9 +16,6 @@ import ( "strings" "time" - "github.com/pterodactyl/wings/internal/cron" - "github.com/pterodactyl/wings/internal/database" - "github.com/NYTimes/logrotate" "github.com/apex/log" "github.com/apex/log/handlers/multi" @@ -31,6 +28,8 @@ import ( "github.com/pterodactyl/wings/config" "github.com/pterodactyl/wings/environment" + "github.com/pterodactyl/wings/internal/cron" + "github.com/pterodactyl/wings/internal/database" "github.com/pterodactyl/wings/loggers/cli" "github.com/pterodactyl/wings/remote" "github.com/pterodactyl/wings/router" @@ -111,7 +110,6 @@ func rootCmdRun(cmd *cobra.Command, _ []string) { log.WithField("error", err).Fatal("failed to configure system directories for pterodactyl") return } - log.WithField("username", config.Get().System.User).Info("checking for pterodactyl system user") if err := config.EnsurePterodactylUser(); err != nil { log.WithField("error", err).Fatal("failed to create pterodactyl system user") } @@ -364,7 +362,7 @@ func rootCmdRun(cmd *cobra.Command, _ []string) { return } - // Check if main http server should run with TLS. Otherwise reset the TLS + // Check if main http server should run with TLS. Otherwise, reset the TLS // config on the server and then serve it over normal HTTP. if api.Ssl.Enabled { if err := s.ListenAndServeTLS(api.Ssl.CertificateFile, api.Ssl.KeyFile); err != nil { diff --git a/config/config.go b/config/config.go index 54f24c1..19e58fe 100644 --- a/config/config.go +++ b/config/config.go @@ -152,9 +152,23 @@ type SystemConfiguration struct { // Definitions for the user that gets created to ensure that we can quickly access // this information without constantly having to do a system lookup. User struct { - Uid int - Gid int - } + // Rootless controls settings related to rootless container daemons. + Rootless struct { + // Enabled controls whether rootless containers are enabled. + Enabled bool `yaml:"enabled" default:"false"` + // ContainerUID controls the UID of the user inside the container. + // This should likely be set to 0 so the container runs as the user + // running Wings. + ContainerUID int `yaml:"container_uid" default:"0"` + // ContainerGID controls the GID of the user inside the container. + // This should likely be set to 0 so the container runs as the user + // running Wings. + ContainerGID int `yaml:"container_gid" default:"0"` + } `yaml:"rootless"` + + Uid int `yaml:"uid"` + Gid int `yaml:"gid"` + } `yaml:"user"` // The amount of time in seconds that can elapse before a server's disk space calculation is // considered stale and a re-check should occur. DANGER: setting this value too low can seriously @@ -425,6 +439,19 @@ func EnsurePterodactylUser() error { return nil } + if _config.System.User.Rootless.Enabled { + log.Info("rootless mode is enabled, skipping user creation...") + u, err := user.Current() + if err != nil { + return err + } + _config.System.Username = u.Username + _config.System.User.Uid = system.MustInt(u.Uid) + _config.System.User.Gid = system.MustInt(u.Gid) + return nil + } + + log.WithField("username", _config.System.Username).Info("checking for pterodactyl system user") u, err := user.Lookup(_config.System.Username) // If an error is returned but it isn't the unknown user error just abort // the process entirely. If we did find a user, return it immediately. diff --git a/config/config_docker.go b/config/config_docker.go index d509703..dafe6bf 100644 --- a/config/config_docker.go +++ b/config/config_docker.go @@ -5,6 +5,7 @@ import ( "sort" "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" "github.com/goccy/go-json" ) @@ -86,6 +87,22 @@ type DockerConfiguration struct { // if the value is "host", then the pterodactyl containers are started with user namespace // remapping disabled UsernsMode string `default:"" json:"userns_mode" yaml:"userns_mode"` + + LogConfig struct { + Type string `default:"local" json:"type" yaml:"type"` + Config map[string]string `default:"{\"max-size\":\"5m\",\"max-file\":\"1\",\"compress\":\"false\",\"mode\":\"non-blocking\"}" json:"config" yaml:"config"` + } `json:"log_config" yaml:"log_config"` +} + +func (c DockerConfiguration) ContainerLogConfig() container.LogConfig { + if c.LogConfig.Type == "" { + return container.LogConfig{} + } + + return container.LogConfig{ + Type: c.LogConfig.Type, + Config: c.LogConfig.Config, + } } // RegistryConfiguration defines the authentication credentials for a given diff --git a/environment/docker/container.go b/environment/docker/container.go index d6a9d14..05e198f 100644 --- a/environment/docker/container.go +++ b/environment/docker/container.go @@ -16,7 +16,6 @@ import ( "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/mount" "github.com/docker/docker/client" - "github.com/docker/docker/daemon/logger/local" "github.com/pterodactyl/wings/config" "github.com/pterodactyl/wings/environment" @@ -43,17 +42,13 @@ func (nw noopWriter) Write(b []byte) (int, error) { // // Calling this function will poll resources for the container in the background // until the container is stopped. The context provided to this function is used -// for the purposes of attaching to the container, a seecond context is created +// for the purposes of attaching to the container, a second context is created // within the function for managing polling. func (e *Environment) Attach(ctx context.Context) error { if e.IsAttached() { return nil } - if err := e.followOutput(); err != nil { - return err - } - opts := types.ContainerAttachOptions{ Stdin: true, Stdout: true, @@ -90,20 +85,13 @@ func (e *Environment) Attach(ctx context.Context) error { } }() - // Block the completion of this routine until the container is no longer running. This allows - // the pollResources function to run until it needs to be stopped. Because the container - // can be polled for resource usage, even when stopped, we need to have this logic present - // in order to cancel the context and therefore stop the routine that is spawned. - // - // For now, DO NOT use client#ContainerWait from the Docker package. There is a nasty - // bug causing containers to hang on deletion and cause servers to lock up on the system. - // - // This weird code isn't intuitive, but it keeps the function from ending until the container - // is stopped and therefore the stream reader ends up closed. - // @see https://github.com/moby/moby/issues/41827 - c := new(noopWriter) - if _, err := io.Copy(c, e.stream.Reader); err != nil { - e.log().WithField("error", err).Error("could not copy from environment stream to noop writer") + if err := system.ScanReader(e.stream.Reader, func(v []byte) { + e.logCallbackMx.Lock() + defer e.logCallbackMx.Unlock() + e.logCallback(v) + }); err != nil && err != io.EOF { + log.WithField("error", err).WithField("container_id", e.Id).Warn("error processing scanner line in console output") + return } }() @@ -163,14 +151,14 @@ func (e *Environment) Create() error { return errors.WithStackIf(err) } + cfg := config.Get() a := e.Configuration.Allocations() - evs := e.Configuration.EnvironmentVariables() for i, v := range evs { // Convert 127.0.0.1 to the pterodactyl0 network interface if the environment is Docker // so that the server operates as expected. if v == "SERVER_IP=127.0.0.1" { - evs[i] = "SERVER_IP=" + config.Get().Docker.Network.Interface + evs[i] = "SERVER_IP=" + cfg.Docker.Network.Interface } } @@ -186,8 +174,7 @@ func (e *Environment) Create() error { conf := &container.Config{ Hostname: e.Id, - Domainname: config.Get().Docker.Domainname, - User: strconv.Itoa(config.Get().System.User.Uid) + ":" + strconv.Itoa(config.Get().System.User.Gid), + Domainname: cfg.Docker.Domainname, AttachStdin: true, AttachStdout: true, AttachStderr: true, @@ -199,7 +186,14 @@ func (e *Environment) Create() error { Labels: labels, } - networkMode := container.NetworkMode(config.Get().Docker.Network.Mode) + // Set the user running the container properly depending on what mode we are operating in. + if cfg.System.User.Rootless.Enabled { + conf.User = fmt.Sprintf("%d:%d", cfg.System.User.Rootless.ContainerUID, cfg.System.User.Rootless.ContainerGID) + } else { + conf.User = strconv.Itoa(cfg.System.User.Uid) + ":" + strconv.Itoa(cfg.System.User.Gid) + } + + networkMode := container.NetworkMode(cfg.Docker.Network.Mode) if a.ForceOutgoingIP { e.log().Debug("environment/docker: forcing outgoing IP address") networkName := strings.ReplaceAll(e.Id, "-", "") @@ -238,28 +232,20 @@ func (e *Environment) Create() error { // Configure the /tmp folder mapping in containers. This is necessary for some // games that need to make use of it for downloads and other installation processes. Tmpfs: map[string]string{ - "/tmp": "rw,exec,nosuid,size=" + strconv.Itoa(int(config.Get().Docker.TmpfsSize)) + "M", + "/tmp": "rw,exec,nosuid,size=" + strconv.Itoa(int(cfg.Docker.TmpfsSize)) + "M", }, // Define resource limits for the container based on the data passed through // from the Panel. Resources: e.Configuration.Limits().AsContainerResources(), - DNS: config.Get().Docker.Network.Dns, + DNS: cfg.Docker.Network.Dns, // Configure logging for the container to make it easier on the Daemon to grab // the server output. Ensure that we don't use too much space on the host machine // since we only need it for the last few hundred lines of output and don't care // about anything else in it. - LogConfig: container.LogConfig{ - Type: local.Name, - Config: map[string]string{ - "max-size": "5m", - "max-file": "1", - "compress": "false", - "mode": "non-blocking", - }, - }, + LogConfig: cfg.Docker.ContainerLogConfig(), SecurityOpt: []string{"no-new-privileges"}, ReadonlyRootfs: true, @@ -268,7 +254,7 @@ func (e *Environment) Create() error { "fowner", "fsetid", "net_bind_service", "sys_chroot", "setfcap", }, NetworkMode: networkMode, - UsernsMode: container.UsernsMode(config.Get().Docker.UsernsMode), + UsernsMode: container.UsernsMode(cfg.Docker.UsernsMode), } if _, err := e.client.ContainerCreate(ctx, conf, hostConf, nil, nil, e.Id); err != nil { @@ -349,59 +335,6 @@ func (e *Environment) Readlog(lines int) ([]string, error) { return out, nil } -// Attaches to the log for the container. This avoids us missing crucial output -// that happens in the split seconds before the code moves from 'Starting' to -// 'Attaching' on the process. -func (e *Environment) followOutput() error { - if exists, err := e.Exists(); !exists { - if err != nil { - return err - } - return errors.New(fmt.Sprintf("no such container: %s", e.Id)) - } - - opts := types.ContainerLogsOptions{ - ShowStderr: true, - ShowStdout: true, - Follow: true, - Since: time.Now().Format(time.RFC3339), - } - - reader, err := e.client.ContainerLogs(context.Background(), e.Id, opts) - if err != nil { - return err - } - - go e.scanOutput(reader) - - return nil -} - -func (e *Environment) scanOutput(reader io.ReadCloser) { - defer reader.Close() - - if err := system.ScanReader(reader, func(v []byte) { - e.logCallbackMx.Lock() - defer e.logCallbackMx.Unlock() - e.logCallback(v) - }); err != nil && err != io.EOF { - log.WithField("error", err).WithField("container_id", e.Id).Warn("error processing scanner line in console output") - return - } - - // Return here if the server is offline or currently stopping. - if e.State() == environment.ProcessStoppingState || e.State() == environment.ProcessOfflineState { - return - } - - // Close the current reader before starting a new one, the defer will still run, - // but it will do nothing if we already closed the stream. - _ = reader.Close() - - // Start following the output of the server again. - go e.followOutput() -} - // Pulls the image from Docker. If there is an error while pulling the image // from the source but the image already exists locally, we will report that // error to the logger but continue with the process. diff --git a/environment/docker/environment.go b/environment/docker/environment.go index 423f2ec..d7e143a 100644 --- a/environment/docker/environment.go +++ b/environment/docker/environment.go @@ -96,7 +96,7 @@ func (e *Environment) SetStream(s *types.HijackedResponse) { e.mu.Unlock() } -// IsAttached determine if the this process is currently attached to the +// IsAttached determines if this process is currently attached to the // container instance by checking if the stream is nil or not. func (e *Environment) IsAttached() bool { e.mu.RLock() diff --git a/environment/docker/power.go b/environment/docker/power.go index d00aa1b..9b50274 100644 --- a/environment/docker/power.go +++ b/environment/docker/power.go @@ -39,7 +39,7 @@ func (e *Environment) OnBeforeStart(ctx context.Context) error { // // This won't actually run an installation process however, it is just here to ensure the // environment gets created properly if it is missing and the server is started. We're making - // an assumption that all of the files will still exist at this point. + // an assumption that all the files will still exist at this point. if err := e.Create(); err != nil { return err } @@ -107,7 +107,7 @@ func (e *Environment) Start(ctx context.Context) error { } // If we cannot start & attach to the container in 30 seconds something has gone - // quite sideways and we should stop trying to avoid a hanging situation. + // quite sideways, and we should stop trying to avoid a hanging situation. actx, cancel := context.WithTimeout(ctx, time.Second*30) defer cancel() diff --git a/environment/docker/stats.go b/environment/docker/stats.go index 54f97da..41da4cf 100644 --- a/environment/docker/stats.go +++ b/environment/docker/stats.go @@ -134,7 +134,11 @@ func calculateDockerAbsoluteCpu(pStats types.CPUStats, stats types.CPUStats) flo percent := 0.0 if systemDelta > 0.0 && cpuDelta > 0.0 { - percent = (cpuDelta / systemDelta) * cpus * 100.0 + percent = (cpuDelta / systemDelta) * 100.0 + + if cpus > 0 { + percent *= cpus + } } return math.Round(percent*1000) / 1000 diff --git a/server/install.go b/server/install.go index 6918919..16d8c1e 100644 --- a/server/install.go +++ b/server/install.go @@ -4,6 +4,7 @@ import ( "bufio" "bytes" "context" + "fmt" "html/template" "io" "os" @@ -419,7 +420,12 @@ func (ip *InstallationProcess) Execute() (string, error) { }, } - tmpfsSize := strconv.Itoa(int(config.Get().Docker.TmpfsSize)) + cfg := config.Get() + if cfg.System.User.Rootless.Enabled { + conf.User = fmt.Sprintf("%d:%d", cfg.System.User.Rootless.ContainerUID, cfg.System.User.Rootless.ContainerGID) + } + + tmpfsSize := strconv.Itoa(int(cfg.Docker.TmpfsSize)) hostConf := &container.HostConfig{ Mounts: []mount.Mount{ { @@ -439,18 +445,11 @@ func (ip *InstallationProcess) Execute() (string, error) { Tmpfs: map[string]string{ "/tmp": "rw,exec,nosuid,size=" + tmpfsSize + "M", }, - DNS: config.Get().Docker.Network.Dns, - LogConfig: container.LogConfig{ - Type: "local", - Config: map[string]string{ - "max-size": "5m", - "max-file": "1", - "compress": "false", - }, - }, + DNS: cfg.Docker.Network.Dns, + LogConfig: cfg.Docker.ContainerLogConfig(), Privileged: true, - NetworkMode: container.NetworkMode(config.Get().Docker.Network.Mode), - UsernsMode: container.UsernsMode(config.Get().Docker.UsernsMode), + NetworkMode: container.NetworkMode(cfg.Docker.Network.Mode), + UsernsMode: container.UsernsMode(cfg.Docker.UsernsMode), } // Ensure the root directory for the server exists properly before attempting