| package main | |
| import ( | |
| "context" | |
| "crypto/tls" | |
| "fmt" | |
| "net" | |
| "net/http" | |
| "os" | |
| "path/filepath" | |
| "runtime" | |
| "sort" | |
| "strings" | |
| "sync" | |
| "time" | |
| containerddefaults "github.com/containerd/containerd/defaults" | |
| "github.com/docker/docker/api" | |
| apiserver "github.com/docker/docker/api/server" | |
| buildbackend "github.com/docker/docker/api/server/backend/build" | |
| "github.com/docker/docker/api/server/middleware" | |
| "github.com/docker/docker/api/server/router" | |
| "github.com/docker/docker/api/server/router/build" | |
| checkpointrouter "github.com/docker/docker/api/server/router/checkpoint" | |
| "github.com/docker/docker/api/server/router/container" | |
| distributionrouter "github.com/docker/docker/api/server/router/distribution" | |
| grpcrouter "github.com/docker/docker/api/server/router/grpc" | |
| "github.com/docker/docker/api/server/router/image" | |
| "github.com/docker/docker/api/server/router/network" | |
| pluginrouter "github.com/docker/docker/api/server/router/plugin" | |
| sessionrouter "github.com/docker/docker/api/server/router/session" | |
| swarmrouter "github.com/docker/docker/api/server/router/swarm" | |
| systemrouter "github.com/docker/docker/api/server/router/system" | |
| "github.com/docker/docker/api/server/router/volume" | |
| buildkit "github.com/docker/docker/builder/builder-next" | |
| "github.com/docker/docker/builder/dockerfile" | |
| "github.com/docker/docker/cli/debug" | |
| "github.com/docker/docker/cmd/dockerd/trap" | |
| "github.com/docker/docker/daemon" | |
| "github.com/docker/docker/daemon/cluster" | |
| "github.com/docker/docker/daemon/config" | |
| "github.com/docker/docker/daemon/listeners" | |
| "github.com/docker/docker/dockerversion" | |
| "github.com/docker/docker/libcontainerd/supervisor" | |
| dopts "github.com/docker/docker/opts" | |
| "github.com/docker/docker/pkg/authorization" | |
| "github.com/docker/docker/pkg/homedir" | |
| "github.com/docker/docker/pkg/jsonmessage" | |
| "github.com/docker/docker/pkg/pidfile" | |
| "github.com/docker/docker/pkg/plugingetter" | |
| "github.com/docker/docker/pkg/rootless" | |
| "github.com/docker/docker/pkg/sysinfo" | |
| "github.com/docker/docker/pkg/system" | |
| "github.com/docker/docker/plugin" | |
| "github.com/docker/docker/runconfig" | |
| "github.com/docker/go-connections/tlsconfig" | |
| "github.com/moby/buildkit/session" | |
| swarmapi "github.com/moby/swarmkit/v2/api" | |
| "github.com/pkg/errors" | |
| "github.com/sirupsen/logrus" | |
| "github.com/spf13/pflag" | |
| ) | |
| // DaemonCli represents the daemon CLI. | |
| type DaemonCli struct { | |
| *config.Config | |
| configFile *string | |
| flags *pflag.FlagSet | |
| d *daemon.Daemon | |
| authzMiddleware *authorization.Middleware // authzMiddleware enables to dynamically reload the authorization plugins | |
| stopOnce sync.Once | |
| apiShutdown chan struct{} | |
| } | |
| // NewDaemonCli returns a daemon CLI | |
| func NewDaemonCli() *DaemonCli { | |
| return &DaemonCli{ | |
| apiShutdown: make(chan struct{}), | |
| } | |
| } | |
| func (cli *DaemonCli) start(opts *daemonOptions) (err error) { | |
| if cli.Config, err = loadDaemonCliConfig(opts); err != nil { | |
| return err | |
| } | |
| tlsConfig, err := newAPIServerTLSConfig(cli.Config) | |
| if err != nil { | |
| return err | |
| } | |
| if opts.Validate { | |
| // If config wasn't OK we wouldn't have made it this far. | |
| _, _ = fmt.Fprintln(os.Stderr, "configuration OK") | |
| return nil | |
| } | |
| configureProxyEnv(cli.Config) | |
| configureDaemonLogs(cli.Config) | |
| logrus.Info("Starting up") | |
| cli.configFile = &opts.configFile | |
| cli.flags = opts.flags | |
| if cli.Config.Debug { | |
| debug.Enable() | |
| } | |
| if cli.Config.Experimental { | |
| logrus.Warn("Running experimental build") | |
| } | |
| if cli.Config.IsRootless() { | |
| logrus.Warn("Running in rootless mode. This mode has feature limitations.") | |
| } | |
| if rootless.RunningWithRootlessKit() { | |
| logrus.Info("Running with RootlessKit integration") | |
| if !cli.Config.IsRootless() { | |
| return fmt.Errorf("rootless mode needs to be enabled for running with RootlessKit") | |
| } | |
| } | |
| // return human-friendly error before creating files | |
| if runtime.GOOS == "linux" && os.Geteuid() != 0 { | |
| return fmt.Errorf("dockerd needs to be started with root privileges. To run dockerd in rootless mode as an unprivileged user, see https://docs.docker.com/go/rootless/") | |
| } | |
| if err := setDefaultUmask(); err != nil { | |
| return err | |
| } | |
| // Create the daemon root before we create ANY other files (PID, or migrate keys) | |
| // to ensure the appropriate ACL is set (particularly relevant on Windows) | |
| if err := daemon.CreateDaemonRoot(cli.Config); err != nil { | |
| return err | |
| } | |
| if err := system.MkdirAll(cli.Config.ExecRoot, 0700); err != nil { | |
| return err | |
| } | |
| potentiallyUnderRuntimeDir := []string{cli.Config.ExecRoot} | |
| if cli.Pidfile != "" { | |
| if err = system.MkdirAll(filepath.Dir(cli.Pidfile), 0o755); err != nil { | |
| return errors.Wrap(err, "failed to create pidfile directory") | |
| } | |
| if err = pidfile.Write(cli.Pidfile, os.Getpid()); err != nil { | |
| return errors.Wrapf(err, "failed to start daemon, ensure docker is not running or delete %s", cli.Pidfile) | |
| } | |
| potentiallyUnderRuntimeDir = append(potentiallyUnderRuntimeDir, cli.Pidfile) | |
| defer func() { | |
| if err := os.Remove(cli.Pidfile); err != nil { | |
| logrus.Error(err) | |
| } | |
| }() | |
| } | |
| if cli.Config.IsRootless() { | |
| // Set sticky bit if XDG_RUNTIME_DIR is set && the file is actually under XDG_RUNTIME_DIR | |
| if _, err := homedir.StickRuntimeDirContents(potentiallyUnderRuntimeDir); err != nil { | |
| // StickRuntimeDirContents returns nil error if XDG_RUNTIME_DIR is just unset | |
| logrus.WithError(err).Warn("cannot set sticky bit on files under XDG_RUNTIME_DIR") | |
| } | |
| } | |
| lss, hosts, err := loadListeners(cli.Config, tlsConfig) | |
| if err != nil { | |
| return errors.Wrap(err, "failed to load listeners") | |
| } | |
| ctx, cancel := context.WithCancel(context.Background()) | |
| waitForContainerDShutdown, err := cli.initContainerd(ctx) | |
| if waitForContainerDShutdown != nil { | |
| defer waitForContainerDShutdown(10 * time.Second) | |
| } | |
| if err != nil { | |
| cancel() | |
| return err | |
| } | |
| defer cancel() | |
| httpServer := &http.Server{ | |
| ReadHeaderTimeout: 5 * time.Minute, // "G112: Potential Slowloris Attack (gosec)"; not a real concern for our use, so setting a long timeout. | |
| } | |
| apiShutdownCtx, apiShutdownCancel := context.WithCancel(context.Background()) | |
| apiShutdownDone := make(chan struct{}) | |
| trap.Trap(cli.stop) | |
| go func() { | |
| // Block until cli.stop() has been called. | |
| // It may have already been called, and that's okay. | |
| // Any httpServer.Serve() calls made after | |
| // httpServer.Shutdown() will return immediately, | |
| // which is what we want. | |
| <-cli.apiShutdown | |
| err := httpServer.Shutdown(apiShutdownCtx) | |
| if err != nil { | |
| logrus.WithError(err).Error("Error shutting down http server") | |
| } | |
| close(apiShutdownDone) | |
| }() | |
| defer func() { | |
| select { | |
| case <-cli.apiShutdown: | |
| // cli.stop() has been called and the daemon has completed | |
| // shutting down. Give the HTTP server a little more time to | |
| // finish handling any outstanding requests if needed. | |
| tmr := time.AfterFunc(5*time.Second, apiShutdownCancel) | |
| defer tmr.Stop() | |
| <-apiShutdownDone | |
| default: | |
| // cli.start() has returned without cli.stop() being called, | |
| // e.g. because the daemon failed to start. | |
| // Stop the HTTP server with no grace period. | |
| if closeErr := httpServer.Close(); closeErr != nil { | |
| logrus.WithError(closeErr).Error("Error closing http server") | |
| } | |
| } | |
| }() | |
| // Notify that the API is active, but before daemon is set up. | |
| preNotifyReady() | |
| pluginStore := plugin.NewStore() | |
| var apiServer apiserver.Server | |
| cli.authzMiddleware = initMiddlewares(&apiServer, cli.Config, pluginStore) | |
| d, err := daemon.NewDaemon(ctx, cli.Config, pluginStore, cli.authzMiddleware) | |
| if err != nil { | |
| return errors.Wrap(err, "failed to start daemon") | |
| } | |
| d.StoreHosts(hosts) | |
| // validate after NewDaemon has restored enabled plugins. Don't change order. | |
| if err := validateAuthzPlugins(cli.Config.AuthorizationPlugins, pluginStore); err != nil { | |
| return errors.Wrap(err, "failed to validate authorization plugin") | |
| } | |
| cli.d = d | |
| if err := startMetricsServer(cli.Config.MetricsAddress); err != nil { | |
| return errors.Wrap(err, "failed to start metrics server") | |
| } | |
| c, err := createAndStartCluster(cli, d) | |
| if err != nil { | |
| logrus.Fatalf("Error starting cluster component: %v", err) | |
| } | |
| // Restart all autostart containers which has a swarm endpoint | |
| // and is not yet running now that we have successfully | |
| // initialized the cluster. | |
| d.RestartSwarmContainers() | |
| logrus.Info("Daemon has completed initialization") | |
| routerCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) | |
| defer cancel() | |
| routerOptions, err := newRouterOptions(routerCtx, cli.Config, d) | |
| if err != nil { | |
| return err | |
| } | |
| routerOptions.cluster = c | |
| httpServer.Handler = apiServer.CreateMux(routerOptions.Build()...) | |
| go d.ProcessClusterNotifications(ctx, c.GetWatchStream()) | |
| cli.setupConfigReloadTrap() | |
| // after the daemon is done setting up we can notify systemd api | |
| notifyReady() | |
| // Daemon is fully initialized. Start handling API traffic | |
| // and wait for serve API to complete. | |
| var ( | |
| apiWG sync.WaitGroup | |
| errAPI = make(chan error, 1) | |
| ) | |
| for _, ls := range lss { | |
| apiWG.Add(1) | |
| go func(ls net.Listener) { | |
| defer apiWG.Done() | |
| logrus.Infof("API listen on %s", ls.Addr()) | |
| if err := httpServer.Serve(ls); err != http.ErrServerClosed { | |
| logrus.WithFields(logrus.Fields{ | |
| logrus.ErrorKey: err, | |
| "listener": ls.Addr(), | |
| }).Error("ServeAPI error") | |
| select { | |
| case errAPI <- err: | |
| default: | |
| } | |
| } | |
| }(ls) | |
| } | |
| apiWG.Wait() | |
| close(errAPI) | |
| c.Cleanup() | |
| // notify systemd that we're shutting down | |
| notifyStopping() | |
| shutdownDaemon(ctx, d) | |
| // Stop notification processing and any background processes | |
| cancel() | |
| if err, ok := <-errAPI; ok { | |
| return errors.Wrap(err, "shutting down due to ServeAPI error") | |
| } | |
| logrus.Info("Daemon shutdown complete") | |
| return nil | |
| } | |
| type routerOptions struct { | |
| sessionManager *session.Manager | |
| buildBackend *buildbackend.Backend | |
| features *map[string]bool | |
| buildkit *buildkit.Builder | |
| daemon *daemon.Daemon | |
| cluster *cluster.Cluster | |
| } | |
| func newRouterOptions(ctx context.Context, config *config.Config, d *daemon.Daemon) (routerOptions, error) { | |
| opts := routerOptions{} | |
| sm, err := session.NewManager() | |
| if err != nil { | |
| return opts, errors.Wrap(err, "failed to create sessionmanager") | |
| } | |
| manager, err := dockerfile.NewBuildManager(d.BuilderBackend(), d.IdentityMapping()) | |
| if err != nil { | |
| return opts, err | |
| } | |
| cgroupParent := newCgroupParent(config) | |
| ro := routerOptions{ | |
| sessionManager: sm, | |
| features: d.Features(), | |
| daemon: d, | |
| } | |
| bk, err := buildkit.New(ctx, buildkit.Opt{ | |
| SessionManager: sm, | |
| Root: filepath.Join(config.Root, "buildkit"), | |
| EngineID: d.ID(), | |
| Dist: d.DistributionServices(), | |
| ImageTagger: d.ImageService(), | |
| NetworkController: d.NetworkController(), | |
| DefaultCgroupParent: cgroupParent, | |
| RegistryHosts: d.RegistryHosts(), | |
| BuilderConfig: config.Builder, | |
| Rootless: d.Rootless(), | |
| IdentityMapping: d.IdentityMapping(), | |
| DNSConfig: config.DNSConfig, | |
| ApparmorProfile: daemon.DefaultApparmorProfile(), | |
| UseSnapshotter: d.UsesSnapshotter(), | |
| Snapshotter: d.ImageService().StorageDriver(), | |
| ContainerdAddress: config.ContainerdAddr, | |
| ContainerdNamespace: config.ContainerdNamespace, | |
| }) | |
| if err != nil { | |
| return opts, err | |
| } | |
| bb, err := buildbackend.NewBackend(d.ImageService(), manager, bk, d.EventsService) | |
| if err != nil { | |
| return opts, errors.Wrap(err, "failed to create buildmanager") | |
| } | |
| ro.buildBackend = bb | |
| ro.buildkit = bk | |
| return ro, nil | |
| } | |
| func (cli *DaemonCli) reloadConfig() { | |
| reload := func(c *config.Config) { | |
| // Revalidate and reload the authorization plugins | |
| if err := validateAuthzPlugins(c.AuthorizationPlugins, cli.d.PluginStore); err != nil { | |
| logrus.Fatalf("Error validating authorization plugin: %v", err) | |
| return | |
| } | |
| cli.authzMiddleware.SetPlugins(c.AuthorizationPlugins) | |
| if err := cli.d.Reload(c); err != nil { | |
| logrus.Errorf("Error reconfiguring the daemon: %v", err) | |
| return | |
| } | |
| if c.IsValueSet("debug") { | |
| debugEnabled := debug.IsEnabled() | |
| switch { | |
| case debugEnabled && !c.Debug: // disable debug | |
| debug.Disable() | |
| case c.Debug && !debugEnabled: // enable debug | |
| debug.Enable() | |
| } | |
| } | |
| } | |
| if err := config.Reload(*cli.configFile, cli.flags, reload); err != nil { | |
| logrus.Error(err) | |
| } | |
| } | |
| func (cli *DaemonCli) stop() { | |
| // Signal that the API server should shut down as soon as possible. | |
| // This construct is used rather than directly shutting down the HTTP | |
| // server to avoid any issues if this method is called before the server | |
| // has been instantiated in cli.start(). If this method is called first, | |
| // the HTTP server will be shut down immediately upon instantiation. | |
| cli.stopOnce.Do(func() { | |
| close(cli.apiShutdown) | |
| }) | |
| } | |
| // shutdownDaemon just wraps daemon.Shutdown() to handle a timeout in case | |
| // d.Shutdown() is waiting too long to kill container or worst it's | |
| // blocked there | |
| func shutdownDaemon(ctx context.Context, d *daemon.Daemon) { | |
| var cancel context.CancelFunc | |
| if timeout := d.ShutdownTimeout(); timeout >= 0 { | |
| ctx, cancel = context.WithTimeout(ctx, time.Duration(timeout)*time.Second) | |
| } else { | |
| ctx, cancel = context.WithCancel(ctx) | |
| } | |
| go func() { | |
| defer cancel() | |
| d.Shutdown(ctx) | |
| }() | |
| <-ctx.Done() | |
| if errors.Is(ctx.Err(), context.DeadlineExceeded) { | |
| logrus.Error("Force shutdown daemon") | |
| } else { | |
| logrus.Debug("Clean shutdown succeeded") | |
| } | |
| } | |
| func loadDaemonCliConfig(opts *daemonOptions) (*config.Config, error) { | |
| if !opts.flags.Parsed() { | |
| return nil, errors.New(`cannot load CLI config before flags are parsed`) | |
| } | |
| opts.setDefaultOptions() | |
| conf := opts.daemonConfig | |
| flags := opts.flags | |
| conf.Debug = opts.Debug | |
| conf.Hosts = opts.Hosts | |
| conf.LogLevel = opts.LogLevel | |
| if flags.Changed(FlagTLS) { | |
| conf.TLS = &opts.TLS | |
| } | |
| if flags.Changed(FlagTLSVerify) { | |
| conf.TLSVerify = &opts.TLSVerify | |
| v := true | |
| conf.TLS = &v | |
| } | |
| if opts.TLSOptions != nil { | |
| conf.TLSOptions = config.TLSOptions{ | |
| CAFile: opts.TLSOptions.CAFile, | |
| CertFile: opts.TLSOptions.CertFile, | |
| KeyFile: opts.TLSOptions.KeyFile, | |
| } | |
| } else { | |
| conf.TLSOptions = config.TLSOptions{} | |
| } | |
| if opts.configFile != "" { | |
| c, err := config.MergeDaemonConfigurations(conf, flags, opts.configFile) | |
| if err != nil { | |
| if flags.Changed("config-file") || !os.IsNotExist(err) { | |
| return nil, errors.Wrapf(err, "unable to configure the Docker daemon with file %s", opts.configFile) | |
| } | |
| } | |
| // the merged configuration can be nil if the config file didn't exist. | |
| // leave the current configuration as it is if when that happens. | |
| if c != nil { | |
| conf = c | |
| } | |
| } | |
| if err := normalizeHosts(conf); err != nil { | |
| return nil, err | |
| } | |
| if err := config.Validate(conf); err != nil { | |
| return nil, err | |
| } | |
| // Check if duplicate label-keys with different values are found | |
| newLabels, err := config.GetConflictFreeLabels(conf.Labels) | |
| if err != nil { | |
| return nil, err | |
| } | |
| conf.Labels = newLabels | |
| // Regardless of whether the user sets it to true or false, if they | |
| // specify TLSVerify at all then we need to turn on TLS | |
| if conf.IsValueSet(FlagTLSVerify) { | |
| v := true | |
| conf.TLS = &v | |
| } | |
| if conf.TLSVerify == nil && conf.TLS != nil { | |
| conf.TLSVerify = conf.TLS | |
| } | |
| err = validateCPURealtimeOptions(conf) | |
| if err != nil { | |
| return nil, err | |
| } | |
| return conf, nil | |
| } | |
| // normalizeHosts normalizes the configured config.Hosts and remove duplicates. | |
| // It returns an error if it fails to parse a host. | |
| func normalizeHosts(config *config.Config) error { | |
| if len(config.Hosts) == 0 { | |
| // if no hosts are configured, create a single entry slice, so that the | |
| // default is used. | |
| // | |
| // TODO(thaJeztah) implement a cleaner way for this; this depends on a | |
| // side-effect of how we parse empty/partial hosts. | |
| config.Hosts = make([]string, 1) | |
| } | |
| hosts := make([]string, 0, len(config.Hosts)) | |
| seen := make(map[string]struct{}, len(config.Hosts)) | |
| useTLS := DefaultTLSValue | |
| if config.TLS != nil { | |
| useTLS = *config.TLS | |
| } | |
| for _, h := range config.Hosts { | |
| host, err := dopts.ParseHost(useTLS, honorXDG, h) | |
| if err != nil { | |
| return err | |
| } | |
| if _, ok := seen[host]; ok { | |
| continue | |
| } | |
| seen[host] = struct{}{} | |
| hosts = append(hosts, host) | |
| } | |
| sort.Strings(hosts) | |
| config.Hosts = hosts | |
| return nil | |
| } | |
| func (opts routerOptions) Build() []router.Router { | |
| decoder := runconfig.ContainerDecoder{ | |
| GetSysInfo: func() *sysinfo.SysInfo { | |
| return opts.daemon.RawSysInfo() | |
| }, | |
| } | |
| routers := []router.Router{ | |
| // we need to add the checkpoint router before the container router or the DELETE gets masked | |
| checkpointrouter.NewRouter(opts.daemon, decoder), | |
| container.NewRouter(opts.daemon, decoder, opts.daemon.RawSysInfo().CgroupUnified), | |
| image.NewRouter( | |
| opts.daemon.ImageService(), | |
| opts.daemon.RegistryService(), | |
| opts.daemon.ReferenceStore, | |
| opts.daemon.ImageService().DistributionServices().ImageStore, | |
| opts.daemon.ImageService().DistributionServices().LayerStore, | |
| ), | |
| systemrouter.NewRouter(opts.daemon, opts.cluster, opts.buildkit, opts.features), | |
| volume.NewRouter(opts.daemon.VolumesService(), opts.cluster), | |
| build.NewRouter(opts.buildBackend, opts.daemon, opts.features), | |
| sessionrouter.NewRouter(opts.sessionManager), | |
| swarmrouter.NewRouter(opts.cluster), | |
| pluginrouter.NewRouter(opts.daemon.PluginManager()), | |
| distributionrouter.NewRouter(opts.daemon.ImageBackend()), | |
| } | |
| if opts.buildBackend != nil { | |
| routers = append(routers, grpcrouter.NewRouter(opts.buildBackend)) | |
| } | |
| if opts.daemon.NetworkControllerEnabled() { | |
| routers = append(routers, network.NewRouter(opts.daemon, opts.cluster)) | |
| } | |
| if opts.daemon.HasExperimental() { | |
| for _, r := range routers { | |
| for _, route := range r.Routes() { | |
| if experimental, ok := route.(router.ExperimentalRoute); ok { | |
| experimental.Enable() | |
| } | |
| } | |
| } | |
| } | |
| return routers | |
| } | |
| func initMiddlewares(s *apiserver.Server, cfg *config.Config, pluginStore plugingetter.PluginGetter) *authorization.Middleware { | |
| v := dockerversion.Version | |
| exp := middleware.NewExperimentalMiddleware(cfg.Experimental) | |
| s.UseMiddleware(exp) | |
| vm := middleware.NewVersionMiddleware(v, api.DefaultVersion, api.MinVersion) | |
| s.UseMiddleware(vm) | |
| if cfg.CorsHeaders != "" { | |
| c := middleware.NewCORSMiddleware(cfg.CorsHeaders) | |
| s.UseMiddleware(c) | |
| } | |
| authzMiddleware := authorization.NewMiddleware(cfg.AuthorizationPlugins, pluginStore) | |
| s.UseMiddleware(authzMiddleware) | |
| return authzMiddleware | |
| } | |
| func (cli *DaemonCli) getContainerdDaemonOpts() ([]supervisor.DaemonOpt, error) { | |
| opts, err := cli.getPlatformContainerdDaemonOpts() | |
| if err != nil { | |
| return nil, err | |
| } | |
| if cli.Debug { | |
| opts = append(opts, supervisor.WithLogLevel("debug")) | |
| } else { | |
| opts = append(opts, supervisor.WithLogLevel(cli.LogLevel)) | |
| } | |
| if !cli.CriContainerd { | |
| // CRI support in the managed daemon is currently opt-in. | |
| // | |
| // It's disabled by default, originally because it was listening on | |
| // a TCP connection at 0.0.0.0:10010, which was considered a security | |
| // risk, and could conflict with user's container ports. | |
| // | |
| // Current versions of containerd started now listen on localhost on | |
| // an ephemeral port instead, but could still conflict with container | |
| // ports, and running kubernetes using the static binaries is not a | |
| // common scenario, so we (for now) continue disabling it by default. | |
| // | |
| // Also see https://github.com/containerd/containerd/issues/2483#issuecomment-407530608 | |
| opts = append(opts, supervisor.WithCRIDisabled()) | |
| } | |
| return opts, nil | |
| } | |
| func newAPIServerTLSConfig(config *config.Config) (*tls.Config, error) { | |
| var tlsConfig *tls.Config | |
| if config.TLS != nil && *config.TLS { | |
| var ( | |
| clientAuth tls.ClientAuthType | |
| err error | |
| ) | |
| if config.TLSVerify == nil || *config.TLSVerify { | |
| // server requires and verifies client's certificate | |
| clientAuth = tls.RequireAndVerifyClientCert | |
| } | |
| tlsConfig, err = tlsconfig.Server(tlsconfig.Options{ | |
| CAFile: config.TLSOptions.CAFile, | |
| CertFile: config.TLSOptions.CertFile, | |
| KeyFile: config.TLSOptions.KeyFile, | |
| ExclusiveRootPools: true, | |
| ClientAuth: clientAuth, | |
| }) | |
| if err != nil { | |
| return nil, errors.Wrap(err, "invalid TLS configuration") | |
| } | |
| } | |
| return tlsConfig, nil | |
| } | |
| // checkTLSAuthOK checks basically for an explicitly disabled TLS/TLSVerify | |
| // Going forward we do not want to support a scenario where dockerd listens | |
| // on TCP without either TLS client auth (or an explicit opt-in to disable it) | |
| func checkTLSAuthOK(c *config.Config) bool { | |
| if c.TLS == nil { | |
| // Either TLS is enabled by default, in which case TLS verification should be enabled by default, or explicitly disabled | |
| // Or TLS is disabled by default... in any of these cases, we can just take the default value as to how to proceed | |
| return DefaultTLSValue | |
| } | |
| if !*c.TLS { | |
| // TLS is explicitly disabled, which is supported | |
| return true | |
| } | |
| if c.TLSVerify == nil { | |
| // this actually shouldn't happen since we set TLSVerify on the config object anyway | |
| // But in case it does get here, be cautious and assume this is not supported. | |
| return false | |
| } | |
| // Either TLSVerify is explicitly enabled or disabled, both cases are supported | |
| return true | |
| } | |
| func loadListeners(cfg *config.Config, tlsConfig *tls.Config) ([]net.Listener, []string, error) { | |
| if len(cfg.Hosts) == 0 { | |
| return nil, nil, errors.New("no hosts configured") | |
| } | |
| var ( | |
| hosts []string | |
| lss []net.Listener | |
| ) | |
| for i := 0; i < len(cfg.Hosts); i++ { | |
| protoAddr := cfg.Hosts[i] | |
| proto, addr, ok := strings.Cut(protoAddr, "://") | |
| if !ok { | |
| return nil, nil, fmt.Errorf("bad format %s, expected PROTO://ADDR", protoAddr) | |
| } | |
| // It's a bad idea to bind to TCP without tlsverify. | |
| authEnabled := tlsConfig != nil && tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert | |
| if proto == "tcp" && !authEnabled { | |
| logrus.WithField("host", protoAddr).Warn("Binding to IP address without --tlsverify is insecure and gives root access on this machine to everyone who has access to your network.") | |
| logrus.WithField("host", protoAddr).Warn("Binding to an IP address, even on localhost, can also give access to scripts run in a browser. Be safe out there!") | |
| time.Sleep(time.Second) | |
| // If TLSVerify is explicitly set to false we'll take that as "Please let me shoot myself in the foot" | |
| // We do not want to continue to support a default mode where tls verification is disabled, so we do some extra warnings here and eventually remove support | |
| if !checkTLSAuthOK(cfg) { | |
| ipAddr, _, err := net.SplitHostPort(addr) | |
| if err != nil { | |
| return nil, nil, errors.Wrap(err, "error parsing tcp address") | |
| } | |
| // shortcut all this extra stuff for literal "localhost" | |
| // -H supports specifying hostnames, since we want to bypass this on loopback interfaces we'll look it up here. | |
| if ipAddr != "localhost" { | |
| ip := net.ParseIP(ipAddr) | |
| if ip == nil { | |
| ipA, err := net.ResolveIPAddr("ip", ipAddr) | |
| if err != nil { | |
| logrus.WithError(err).WithField("host", ipAddr).Error("Error looking up specified host address") | |
| } | |
| if ipA != nil { | |
| ip = ipA.IP | |
| } | |
| } | |
| if ip == nil || !ip.IsLoopback() { | |
| logrus.WithField("host", protoAddr).Warn("Binding to an IP address without --tlsverify is deprecated. Startup is intentionally being slowed down to show this message") | |
| logrus.WithField("host", protoAddr).Warn("Please consider generating tls certificates with client validation to prevent exposing unauthenticated root access to your network") | |
| logrus.WithField("host", protoAddr).Warnf("You can override this by explicitly specifying '--%s=false' or '--%s=false'", FlagTLS, FlagTLSVerify) | |
| logrus.WithField("host", protoAddr).Warnf("Support for listening on TCP without authentication or explicit intent to run without authentication will be removed in the next release") | |
| time.Sleep(15 * time.Second) | |
| } | |
| } | |
| } | |
| } | |
| // If we're binding to a TCP port, make sure that a container doesn't try to use it. | |
| if proto == "tcp" { | |
| if err := allocateDaemonPort(addr); err != nil { | |
| return nil, nil, err | |
| } | |
| } | |
| ls, err := listeners.Init(proto, addr, cfg.SocketGroup, tlsConfig) | |
| if err != nil { | |
| return nil, nil, err | |
| } | |
| logrus.Debugf("Listener created for HTTP on %s (%s)", proto, addr) | |
| hosts = append(hosts, addr) | |
| lss = append(lss, ls...) | |
| } | |
| return lss, hosts, nil | |
| } | |
| func createAndStartCluster(cli *DaemonCli, d *daemon.Daemon) (*cluster.Cluster, error) { | |
| name, _ := os.Hostname() | |
| // Use a buffered channel to pass changes from store watch API to daemon | |
| // A buffer allows store watch API and daemon processing to not wait for each other | |
| watchStream := make(chan *swarmapi.WatchMessage, 32) | |
| c, err := cluster.New(cluster.Config{ | |
| Root: cli.Config.Root, | |
| Name: name, | |
| Backend: d, | |
| VolumeBackend: d.VolumesService(), | |
| ImageBackend: d.ImageBackend(), | |
| PluginBackend: d.PluginManager(), | |
| NetworkSubnetsProvider: d, | |
| DefaultAdvertiseAddr: cli.Config.SwarmDefaultAdvertiseAddr, | |
| RaftHeartbeatTick: cli.Config.SwarmRaftHeartbeatTick, | |
| RaftElectionTick: cli.Config.SwarmRaftElectionTick, | |
| RuntimeRoot: cli.getSwarmRunRoot(), | |
| WatchStream: watchStream, | |
| }) | |
| if err != nil { | |
| return nil, err | |
| } | |
| d.SetCluster(c) | |
| err = c.Start() | |
| return c, err | |
| } | |
| // validates that the plugins requested with the --authorization-plugin flag are valid AuthzDriver | |
| // plugins present on the host and available to the daemon | |
| func validateAuthzPlugins(requestedPlugins []string, pg plugingetter.PluginGetter) error { | |
| for _, reqPlugin := range requestedPlugins { | |
| if _, err := pg.Get(reqPlugin, authorization.AuthZApiImplements, plugingetter.Lookup); err != nil { | |
| return err | |
| } | |
| } | |
| return nil | |
| } | |
| func systemContainerdRunning(honorXDG bool) (string, bool, error) { | |
| addr := containerddefaults.DefaultAddress | |
| if honorXDG { | |
| runtimeDir, err := homedir.GetRuntimeDir() | |
| if err != nil { | |
| return "", false, err | |
| } | |
| addr = filepath.Join(runtimeDir, "containerd", "containerd.sock") | |
| } | |
| _, err := os.Lstat(addr) | |
| return addr, err == nil, nil | |
| } | |
| // configureDaemonLogs sets the logrus logging level and formatting. It expects | |
| // the passed configuration to already be validated, and ignores invalid options. | |
| func configureDaemonLogs(conf *config.Config) { | |
| if conf.LogLevel != "" { | |
| lvl, err := logrus.ParseLevel(conf.LogLevel) | |
| if err == nil { | |
| logrus.SetLevel(lvl) | |
| } | |
| } else { | |
| logrus.SetLevel(logrus.InfoLevel) | |
| } | |
| logrus.SetFormatter(&logrus.TextFormatter{ | |
| TimestampFormat: jsonmessage.RFC3339NanoFixed, | |
| DisableColors: conf.RawLogs, | |
| FullTimestamp: true, | |
| }) | |
| } | |
| func configureProxyEnv(conf *config.Config) { | |
| if p := conf.HTTPProxy; p != "" { | |
| overrideProxyEnv("HTTP_PROXY", p) | |
| overrideProxyEnv("http_proxy", p) | |
| } | |
| if p := conf.HTTPSProxy; p != "" { | |
| overrideProxyEnv("HTTPS_PROXY", p) | |
| overrideProxyEnv("https_proxy", p) | |
| } | |
| if p := conf.NoProxy; p != "" { | |
| overrideProxyEnv("NO_PROXY", p) | |
| overrideProxyEnv("no_proxy", p) | |
| } | |
| } | |
| func overrideProxyEnv(name, val string) { | |
| if oldVal := os.Getenv(name); oldVal != "" && oldVal != val { | |
| logrus.WithFields(logrus.Fields{ | |
| "name": name, | |
| "old-value": config.MaskCredentials(oldVal), | |
| "new-value": config.MaskCredentials(val), | |
| }).Warn("overriding existing proxy variable with value from configuration") | |
| } | |
| _ = os.Setenv(name, val) | |
| } |