diff --git a/api/client/webclient/webclient.go b/api/client/webclient/webclient.go index 650a325d235..12286e33338 100644 --- a/api/client/webclient/webclient.go +++ b/api/client/webclient/webclient.go @@ -262,6 +262,10 @@ type DBProxySettings struct { MySQLListenAddr string `json:"mysql_listen_addr,omitempty"` // MySQLPublicAddr is advertised to MySQL clients. MySQLPublicAddr string `json:"mysql_public_addr,omitempty"` + // MongoListenAddr is Mongo proxy listen address. + MongoListenAddr string `json:"mongo_listen_addr,omitempty"` + // MongoPublicAddr is advertised to Mongo clients. + MongoPublicAddr string `json:"mongo_public_addr,omitempty"` } // AuthenticationSettings contains information about server authentication diff --git a/api/profile/profile.go b/api/profile/profile.go index 1941cc63255..d4b4923a72f 100644 --- a/api/profile/profile.go +++ b/api/profile/profile.go @@ -65,6 +65,9 @@ type Profile struct { // MySQLProxyAddr is the host:port the MySQL proxy can be accessed at. MySQLProxyAddr string `yaml:"mysql_proxy_addr,omitempty"` + // MongoProxyAddr is the host:port the Mongo proxy can be accessed at. + MongoProxyAddr string `yaml:"mongo_proxy_addr,omitempty"` + // Username is the Teleport username for the client. Username string `yaml:"user,omitempty"` diff --git a/integration/db_integration_test.go b/integration/db_integration_test.go index 98f506ce462..95ed3c882de 100644 --- a/integration/db_integration_test.go +++ b/integration/db_integration_test.go @@ -388,6 +388,36 @@ func TestDatabaseAccessPostgresSeparateListener(t *testing.T) { require.NoError(t, err) } +// TestDatabaseAccessMongoSeparateListener tests mongo proxy listener running on separate port. +func TestDatabaseAccessMongoSeparateListener(t *testing.T) { + pack := setupDatabaseTest(t, + withPortSetupDatabaseTest(separateMongoPortSetup), + ) + + // Connect to the database service in root cluster. + client, err := mongodb.MakeTestClient(context.Background(), common.TestClientConfig{ + AuthClient: pack.root.cluster.GetSiteAPI(pack.root.cluster.Secrets.SiteName), + AuthServer: pack.root.cluster.Process.GetAuthServer(), + Address: net.JoinHostPort(Loopback, pack.root.cluster.GetPortMongo()), + Cluster: pack.root.cluster.Secrets.SiteName, + Username: pack.root.user.GetName(), + RouteToDatabase: tlsca.RouteToDatabase{ + ServiceName: pack.root.mongoService.Name, + Protocol: pack.root.mongoService.Protocol, + Username: "admin", + }, + }) + require.NoError(t, err) + + // Execute a query. + _, err = client.Database("test").Collection("test").Find(context.Background(), bson.M{}) + require.NoError(t, err) + + // Disconnect. + err = client.Disconnect(context.Background()) + require.NoError(t, err) +} + func waitForAuditEventTypeWithBackoff(t *testing.T, cli *auth.Server, startTime time.Time, eventType string) []apievents.AuditEvent { max := time.Second timeout := time.After(max) diff --git a/integration/helpers.go b/integration/helpers.go index acaac805866..0896e77b26a 100644 --- a/integration/helpers.go +++ b/integration/helpers.go @@ -599,6 +599,10 @@ func (i *TeleInstance) GenerateConfig(t *testing.T, trustedSecrets []*InstanceSe // Postgres proxy port was configured on a separate listener. tconf.Proxy.PostgresAddr.Addr = net.JoinHostPort(i.Hostname, i.GetPortPostgres()) } + if i.Mongo != nil { + // Mongo proxy port was configured on a separate listener. + tconf.Proxy.MongoAddr.Addr = net.JoinHostPort(i.Hostname, i.GetPortMongo()) + } } tconf.AuthServers = append(tconf.AuthServers, tconf.Auth.SSHAddr) tconf.Auth.StorageConfig = backend.Config{ diff --git a/integration/ports.go b/integration/ports.go index 0e078d33ad9..03a76593075 100644 --- a/integration/ports.go +++ b/integration/ports.go @@ -99,6 +99,18 @@ func separatePostgresPortSetup() *InstancePorts { } } +func separateMongoPortSetup() *InstancePorts { + return &InstancePorts{ + Web: newInstancePort(), + SSH: newInstancePort(), + Auth: newInstancePort(), + SSHProxy: newInstancePort(), + ReverseTunnel: newInstancePort(), + MySQL: newInstancePort(), + Mongo: newInstancePort(), + } +} + type InstancePorts struct { Host string Web *InstancePort @@ -110,6 +122,7 @@ type InstancePorts struct { ReverseTunnel *InstancePort MySQL *InstancePort Postgres *InstancePort + Mongo *InstancePort isSinglePortSetup bool } @@ -121,6 +134,7 @@ func (i *InstancePorts) GetPortProxy() string { return i.SSHProxy.String func (i *InstancePorts) GetPortWeb() string { return i.Web.String() } func (i *InstancePorts) GetPortMySQL() string { return i.MySQL.String() } func (i *InstancePorts) GetPortPostgres() string { return i.Postgres.String() } +func (i *InstancePorts) GetPortMongo() string { return i.Mongo.String() } func (i *InstancePorts) GetPortReverseTunnel() string { return i.ReverseTunnel.String() } func (i *InstancePorts) GetSSHAddr() string { diff --git a/lib/client/api.go b/lib/client/api.go index 5997d83fc8f..1ab3a3d8176 100644 --- a/lib/client/api.go +++ b/lib/client/api.go @@ -199,6 +199,9 @@ type Config struct { // PostgresProxyAddr is the host:port the Postgres proxy can be accessed at. PostgresProxyAddr string + // MongoProxyAddr is the host:port the Mongo proxy can be accessed at. + MongoProxyAddr string + // MySQLProxyAddr is the host:port the MySQL proxy can be accessed at. MySQLProxyAddr string @@ -800,6 +803,7 @@ func (c *Config) LoadProfile(profileDir string, proxyName string) error { c.SSHProxyAddr = cp.SSHProxyAddr c.PostgresProxyAddr = cp.PostgresProxyAddr c.MySQLProxyAddr = cp.MySQLProxyAddr + c.MongoProxyAddr = cp.MongoProxyAddr c.TLSRoutingEnabled = cp.TLSRoutingEnabled c.LocalForwardPorts, err = ParsePortForwardSpec(cp.ForwardedPorts) @@ -831,6 +835,7 @@ func (c *Config) SaveProfile(dir string, makeCurrent bool) error { cp.KubeProxyAddr = c.KubeProxyAddr cp.PostgresProxyAddr = c.PostgresProxyAddr cp.MySQLProxyAddr = c.MySQLProxyAddr + cp.MongoProxyAddr = c.MongoProxyAddr cp.ForwardedPorts = c.LocalForwardPorts.String() cp.SiteName = c.SiteName cp.TLSRoutingEnabled = c.TLSRoutingEnabled @@ -996,6 +1001,17 @@ func (c *Config) PostgresProxyHostPort() (string, int) { return c.WebProxyHostPort() } +// MongoProxyHostPort returns the host and port of Mongo proxy. +func (c *Config) MongoProxyHostPort() (string, int) { + if c.MongoProxyAddr != "" { + addr, err := utils.ParseAddr(c.MongoProxyAddr) + if err == nil { + return addr.Host(), addr.Port(defaults.MongoListenPort) + } + } + return c.WebProxyHostPort() +} + // MySQLProxyHostPort returns the host and port of MySQL proxy. func (c *Config) MySQLProxyHostPort() (string, int) { if c.MySQLProxyAddr != "" { @@ -1016,7 +1032,7 @@ func (c *Config) DatabaseProxyHostPort(db tlsca.RouteToDatabase) (string, int) { case defaults.ProtocolMySQL: return c.MySQLProxyHostPort() case defaults.ProtocolMongoDB: - return c.WebProxyHostPort() + return c.MongoProxyHostPort() } return c.WebProxyHostPort() } @@ -2663,6 +2679,24 @@ func (tc *TeleportClient) applyProxySettings(proxySettings webclient.ProxySettin tc.PostgresProxyAddr = net.JoinHostPort(webProxyHost, strconv.Itoa(webProxyPort)) } + // Read Mongo proxy settings. + switch { + case proxySettings.DB.MongoPublicAddr != "": + addr, err := utils.ParseAddr(proxySettings.DB.MongoPublicAddr) + if err != nil { + return trace.BadParameter("failed to parse Mongo public address received from server: %q, contact your administrator for help", + proxySettings.DB.MongoPublicAddr) + } + tc.MongoProxyAddr = net.JoinHostPort(addr.Host(), strconv.Itoa(addr.Port(tc.WebProxyPort()))) + case proxySettings.DB.MongoListenAddr != "": + addr, err := utils.ParseAddr(proxySettings.DB.MongoListenAddr) + if err != nil { + return trace.BadParameter("failed to parse Mongo listen address received from server: %q, contact your administrator for help", + proxySettings.DB.MongoListenAddr) + } + tc.MongoProxyAddr = net.JoinHostPort(tc.WebProxyHost(), strconv.Itoa(addr.Port(defaults.MongoListenPort))) + } + // Read MySQL proxy settings if enabled on the server. switch { case proxySettings.DB.MySQLPublicAddr != "": diff --git a/lib/config/configuration.go b/lib/config/configuration.go index 0c9e20e84c1..7a6c3e36750 100644 --- a/lib/config/configuration.go +++ b/lib/config/configuration.go @@ -671,6 +671,13 @@ func applyProxyConfig(fc *FileConfig, cfg *service.Config) error { } cfg.Proxy.PostgresAddr = *addr } + if fc.Proxy.MongoAddr != "" { + addr, err := utils.ParseHostPortAddr(fc.Proxy.MongoAddr, int(defaults.MongoListenPort)) + if err != nil { + return trace.Wrap(err) + } + cfg.Proxy.MongoAddr = *addr + } // This is the legacy format. Continue to support it forever, but ideally // users now use the list format below. @@ -815,6 +822,17 @@ func applyProxyConfig(fc *FileConfig, cfg *service.Config) error { cfg.Proxy.MySQLPublicAddrs = addrs } + if len(fc.Proxy.MongoPublicAddr) != 0 { + if fc.Proxy.MongoAddr == "" { + return trace.BadParameter("mongo_listen_addr must be set when mongo_public_addr is set") + } + addrs, err := utils.AddrsFromStrings(fc.Proxy.MongoPublicAddr, defaults.MongoListenPort) + if err != nil { + return trace.Wrap(err) + } + cfg.Proxy.MongoPublicAddrs = addrs + } + acme, err := fc.Proxy.ACME.Parse() if err != nil { return trace.Wrap(err) diff --git a/lib/config/configuration_test.go b/lib/config/configuration_test.go index 3da807d9255..69691b91c1c 100644 --- a/lib/config/configuration_test.go +++ b/lib/config/configuration_test.go @@ -679,10 +679,13 @@ func TestApplyConfig(t *testing.T) { require.Equal(t, "tcp://webhost:3080", cfg.Proxy.WebAddr.FullAddress()) require.Equal(t, "tcp://tunnelhost:1001", cfg.Proxy.ReverseTunnelListenAddr.FullAddress()) require.Equal(t, "tcp://webhost:3336", cfg.Proxy.MySQLAddr.FullAddress()) + require.Equal(t, "tcp://webhost:27017", cfg.Proxy.MongoAddr.FullAddress()) require.Len(t, cfg.Proxy.PostgresPublicAddrs, 1) require.Equal(t, "tcp://postgres.example:5432", cfg.Proxy.PostgresPublicAddrs[0].FullAddress()) require.Len(t, cfg.Proxy.MySQLPublicAddrs, 1) require.Equal(t, "tcp://mysql.example:3306", cfg.Proxy.MySQLPublicAddrs[0].FullAddress()) + require.Len(t, cfg.Proxy.MongoPublicAddrs, 1) + require.Equal(t, "tcp://mongo.example:27017", cfg.Proxy.MongoPublicAddrs[0].FullAddress()) require.Equal(t, "tcp://127.0.0.1:3000", cfg.DiagnosticAddr.FullAddress()) diff --git a/lib/config/fileconf.go b/lib/config/fileconf.go index 265e957656b..a9ff294f7ad 100644 --- a/lib/config/fileconf.go +++ b/lib/config/fileconf.go @@ -1121,6 +1121,12 @@ type Proxy struct { // PostgresPublicAddr is the hostport the proxy advertises for Postgres // client connections. PostgresPublicAddr apiutils.Strings `yaml:"postgres_public_addr,omitempty"` + + // MongoAddr is Mongo proxy listen address. + MongoAddr string `yaml:"mongo_listen_addr,omitempty"` + // MongoPublicAddr is the hostport the proxy advertises for Mongo + // client connections. + MongoPublicAddr apiutils.Strings `yaml:"mongo_public_addr,omitempty"` } // ACME configures ACME protocol - automatic X.509 certificates diff --git a/lib/config/testdata_test.go b/lib/config/testdata_test.go index d4dbb358d19..e613396e233 100644 --- a/lib/config/testdata_test.go +++ b/lib/config/testdata_test.go @@ -159,6 +159,8 @@ proxy_service: postgres_public_addr: postgres.example:5432 mysql_listen_addr: webhost:3336 mysql_public_addr: mysql.example:3306 + mongo_listen_addr: webhost:27017 + mongo_public_addr: mongo.example:27017 ` // NoServicesConfigString is a configuration file with no services enabled diff --git a/lib/defaults/defaults.go b/lib/defaults/defaults.go index d0e302594d3..7faaced63ba 100644 --- a/lib/defaults/defaults.go +++ b/lib/defaults/defaults.go @@ -63,6 +63,9 @@ const ( // PostgresListenPort is the default listen port for PostgreSQL proxy. PostgresListenPort = 5432 + // MongoListenPort is the default listen port for Mongo proxy. + MongoListenPort = 27017 + // MetricsListenPort is the default listen port for the metrics service. MetricsListenPort = 3081 diff --git a/lib/service/cfg.go b/lib/service/cfg.go index 7c922ae4acb..9c2a74db498 100644 --- a/lib/service/cfg.go +++ b/lib/service/cfg.go @@ -361,6 +361,9 @@ type ProxyConfig struct { // PostgresAddr is address of Postgres proxy. PostgresAddr utils.NetAddr + // MongoAddr is address of Mongo proxy. + MongoAddr utils.NetAddr + Limiter limiter.Config // PublicAddrs is a list of the public addresses the proxy advertises @@ -386,6 +389,10 @@ type ProxyConfig struct { // advertises for MySQL clients. MySQLPublicAddrs []utils.NetAddr + // MongoPublicAddrs is a list of the public addresses the proxy + // advertises for Mongo clients. + MongoPublicAddrs []utils.NetAddr + // Kube specifies kubernetes proxy configuration Kube KubeProxyConfig diff --git a/lib/service/listeners.go b/lib/service/listeners.go index 7c38cc9262e..eab12a57aa2 100644 --- a/lib/service/listeners.go +++ b/lib/service/listeners.go @@ -41,6 +41,7 @@ var ( listenerProxyTunnel = listenerType(teleport.Component(teleport.ComponentProxy, "tunnel")) listenerProxyMySQL = listenerType(teleport.Component(teleport.ComponentProxy, "mysql")) listenerProxyPostgres = listenerType(teleport.Component(teleport.ComponentProxy, "postgres")) + listenerProxyMongo = listenerType(teleport.Component(teleport.ComponentProxy, "mongo")) listenerMetrics = listenerType(teleport.ComponentMetrics) listenerWindowsDesktop = listenerType(teleport.ComponentWindowsDesktop) ) diff --git a/lib/service/proxy_settings.go b/lib/service/proxy_settings.go index 7599a928636..b5671436c73 100644 --- a/lib/service/proxy_settings.go +++ b/lib/service/proxy_settings.go @@ -81,6 +81,10 @@ func (p *proxySettings) buildProxySettings(proxyListenerMode types.ProxyListener proxySettings.DB.PostgresListenAddr = p.cfg.Proxy.PostgresAddr.String() } + if !p.cfg.Proxy.MongoAddr.IsEmpty() { + proxySettings.DB.MongoListenAddr = p.cfg.Proxy.MongoAddr.String() + } + if p.cfg.Proxy.Kube.Enabled { proxySettings.Kube.ListenAddr = p.cfg.Proxy.Kube.ListenAddr.String() } @@ -117,6 +121,9 @@ func (p *proxySettings) setProxyPublicAddressesSettings(settings *webclient.Prox if len(p.cfg.Proxy.MySQLPublicAddrs) > 0 { settings.DB.MySQLPublicAddr = p.cfg.Proxy.MySQLPublicAddrs[0].String() } + if len(p.cfg.Proxy.MongoPublicAddrs) > 0 { + settings.DB.MongoPublicAddr = p.cfg.Proxy.MongoPublicAddrs[0].String() + } settings.DB.PostgresPublicAddr = p.getPostgresPublicAddr() } diff --git a/lib/service/service.go b/lib/service/service.go index 9fb41b8fc1a..4033f9b89c1 100644 --- a/lib/service/service.go +++ b/lib/service/service.go @@ -2526,6 +2526,8 @@ type dbListeners struct { postgres net.Listener // mysql serves MySQL clients. mysql net.Listener + // mongo serves Mongo clients. + mongo net.Listener // tls serves database clients that use plain TLS handshake. tls net.Listener } @@ -2546,6 +2548,9 @@ func (l *dbListeners) Close() { if l.tls != nil { l.tls.Close() } + if l.mongo != nil { + l.mongo.Close() + } } // Close closes all proxy listeners. @@ -2603,6 +2608,15 @@ func (process *TeleportProcess) setupProxyListeners() (*proxyListeners, error) { listeners.db.mysql = listener } + if !cfg.Proxy.MongoAddr.IsEmpty() && !cfg.Proxy.DisableDatabaseProxy { + process.log.Debugf("Setup Proxy: Mongo proxy address: %v.", cfg.Proxy.MongoAddr.Addr) + listener, err := process.importOrCreateListener(listenerProxyMongo, cfg.Proxy.MongoAddr.Addr) + if err != nil { + return nil, trace.Wrap(err) + } + listeners.db.mongo = listener + } + switch { case cfg.Proxy.DisableWebService && cfg.Proxy.DisableReverseTunnel: process.log.Debugf("Setup Proxy: Reverse tunnel proxy and web proxy are disabled.") @@ -3170,6 +3184,16 @@ func (process *TeleportProcess) initProxyEndpoint(conn *Connector) error { return nil }) } + + if listeners.db.mongo != nil { + process.RegisterCriticalFunc("proxy.db.mongo", func() error { + log.Infof("Starting Database Mongo proxy server on %v.", cfg.Proxy.MongoAddr.Addr) + if err := dbProxyServer.ServeMongo(listeners.db.mongo, tlsConfigWeb.Clone()); err != nil { + log.WithError(err).Warn("Database Mongo proxy server exited with error.") + } + return nil + }) + } } var alpnServer *alpnproxy.Proxy diff --git a/lib/srv/db/proxyserver.go b/lib/srv/db/proxyserver.go index a069bba19b7..a9cc444750d 100644 --- a/lib/srv/db/proxyserver.go +++ b/lib/srv/db/proxyserver.go @@ -193,6 +193,33 @@ func (s *ProxyServer) ServeMySQL(listener net.Listener) error { } } +// ServeMongo starts accepting Mongo client connections. +func (s *ProxyServer) ServeMongo(listener net.Listener, tlsConfig *tls.Config) error { + s.log.Debug("Started Mongo proxy.") + defer s.log.Debug("Mongo proxy exited.") + for { + clientConn, err := listener.Accept() + if err != nil { + if utils.IsOKNetworkError(err) || trace.IsConnectionProblem(err) { + return nil + } + return trace.Wrap(err) + } + go func() { + defer clientConn.Close() + tlsConn := tls.Server(clientConn, tlsConfig) + if err := tlsConn.Handshake(); err != nil { + s.log.WithError(err).Error("Mongo TLS handshake failed.") + return + } + err := s.handleConnection(tlsConn) + if err != nil { + s.log.WithError(err).Error("Failed to handle Mongo client connection.") + } + }() + } +} + // ServeTLS starts accepting database connections that use plain TLS connection. func (s *ProxyServer) ServeTLS(listener net.Listener) error { s.log.Debug("Started database TLS proxy.")