diff --git a/cmd/cloudflared/tunnel/cmd.go b/cmd/cloudflared/tunnel/cmd.go index 8e5c2400..2de58774 100644 --- a/cmd/cloudflared/tunnel/cmd.go +++ b/cmd/cloudflared/tunnel/cmd.go @@ -23,6 +23,7 @@ import ( "github.com/cloudflare/cloudflared/metrics" "github.com/cloudflare/cloudflared/origin" "github.com/cloudflare/cloudflared/signal" + "github.com/cloudflare/cloudflared/sshlog" "github.com/cloudflare/cloudflared/sshserver" "github.com/cloudflare/cloudflared/supervisor" "github.com/cloudflare/cloudflared/tlsconfig" @@ -383,8 +384,9 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan uploadManager.Start() } + logManager := sshlog.New() sshServerAddress := "127.0.0.1:" + c.String(sshPortFlag) - server, err := sshserver.New(logger, sshServerAddress, shutdownC, c.Duration(sshIdleTimeoutFlag), c.Duration(sshMaxTimeoutFlag)) + server, err := sshserver.New(logManager, logger, sshServerAddress, shutdownC, c.Duration(sshIdleTimeoutFlag), c.Duration(sshMaxTimeoutFlag)) if err != nil { logger.WithError(err).Error("Cannot create new SSH Server") return errors.Wrap(err, "Cannot create new SSH Server") diff --git a/sshlog/logger.go b/sshlog/logger.go new file mode 100644 index 00000000..d08882b8 --- /dev/null +++ b/sshlog/logger.go @@ -0,0 +1,156 @@ +package sshlog + +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "sync" + "time" + + "github.com/sirupsen/logrus" +) + +const ( + logTimeFormat = "2006-01-02T15-04-05.000" + megabyte = 1024 * 1024 +) + +// Logger will buffer and write events to disk +type Logger struct { + sync.Mutex + filename string + file *os.File + writeBuffer *bufio.Writer + logger *logrus.Logger + done chan struct{} + once sync.Once +} + +// NewLogger creates a Logger instance. A buffer is created that needs to be +// drained and closed when the caller is finished, so instances should call +// Close when finished with this Logger instance. Writes will be flushed to disk +// every second (fsync). filename is the name of the logfile to be created. The +// logger variable is a logrus that will log all i/o, filesystem error etc, that +// that shouldn't end execution of the logger, but are useful to report to the +// caller. +func NewLogger(filename string, logger *logrus.Logger) (*Logger, error) { + f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(0600)) + if err != nil { + return nil, err + } + l := &Logger{filename: filename, + file: f, + writeBuffer: bufio.NewWriter(f), + logger: logger, + done: make(chan struct{})} + go l.writer() + return l, nil +} + +// Writes to a log buffer. Implements the io.Writer interface. +func (l *Logger) Write(p []byte) (n int, err error) { + l.Lock() + defer l.Unlock() + return l.writeBuffer.Write(p) +} + +// Close drains anything left in the buffer and cleans up any resources still +// in use. +func (l *Logger) Close() error { + l.once.Do(func() { + close(l.done) + }) + if err := l.write(); err != nil { + return err + } + return l.file.Close() +} + +// writer is the run loop that handles draining the write buffer and syncing +// data to disk. +func (l *Logger) writer() { + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + for { + select { + case <-ticker.C: + if err := l.write(); err != nil { + l.logger.Errorln(err) + } + case <-l.done: + return + } + } +} + +// write does the actual system write calls to disk and does a rotation if the +// file size limit has been reached. Since the rotation happens at the end, +// the rotation is a soft limit (aka the file can be bigger than the max limit +// because of the final buffer flush) +func (l *Logger) write() error { + l.Lock() + defer l.Unlock() + + if l.writeBuffer.Buffered() <= 0 { + return nil + } + + if err := l.writeBuffer.Flush(); err != nil { + return err + } + + if err := l.file.Sync(); err != nil { + return err + } + + if l.shouldRotate() { + return l.rotate() + } + return nil +} + +// shouldRotate checks to see if the current file should be rotated to a new +// logfile. +func (l *Logger) shouldRotate() bool { + info, err := l.file.Stat() + if err != nil { + return false + } + + return info.Size() >= 100*megabyte +} + +// rotate creates a new logfile with the existing filename and renames the +// existing file with a current timestamp. +func (l *Logger) rotate() error { + if err := l.file.Close(); err != nil { + return err + } + + // move the existing file + newname := rotationName(l.filename) + if err := os.Rename(l.filename, newname); err != nil { + return fmt.Errorf("can't rename log file: %s", err) + } + + f, err := os.OpenFile(l.filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(0600)) + if err != nil { + return fmt.Errorf("failed to open new logfile %s", err) + } + l.file = f + l.writeBuffer = bufio.NewWriter(f) + return nil +} + +// rotationName creates a new filename from the given name, inserting a timestamp +// between the filename and the extension. +func rotationName(name string) string { + dir := filepath.Dir(name) + filename := filepath.Base(name) + ext := filepath.Ext(filename) + prefix := filename[:len(filename)-len(ext)] + t := time.Now() + timestamp := t.Format(logTimeFormat) + return filepath.Join(dir, fmt.Sprintf("%s-%s%s", prefix, timestamp, ext)) +} diff --git a/sshlog/manager.go b/sshlog/manager.go new file mode 100644 index 00000000..3f5fb664 --- /dev/null +++ b/sshlog/manager.go @@ -0,0 +1,23 @@ +package sshlog + +import ( + "io" + + "github.com/sirupsen/logrus" +) + +// Manager be managing logs bruh +type Manager interface { + NewLogger(string, *logrus.Logger) (io.WriteCloser, error) +} + +type manager struct{} + +// New creates a new instance of a log manager +func New() Manager { + return &manager{} +} + +func (m *manager) NewLogger(name string, logger *logrus.Logger) (io.WriteCloser, error) { + return NewLogger(name, logger) +} diff --git a/sshserver/sshserver_unix.go b/sshserver/sshserver_unix.go index 965104c9..59d3ff16 100644 --- a/sshserver/sshserver_unix.go +++ b/sshserver/sshserver_unix.go @@ -3,7 +3,6 @@ package sshserver import ( - "bufio" "errors" "fmt" "io" @@ -15,8 +14,11 @@ import ( "time" "unsafe" + "github.com/cloudflare/cloudflared/sshlog" + "github.com/creack/pty" "github.com/gliderlabs/ssh" + "github.com/google/uuid" "github.com/sirupsen/logrus" ) @@ -26,9 +28,10 @@ type SSHServer struct { shutdownC chan struct{} caCert ssh.PublicKey getUserFunc func(string) (*User, error) + logManager sshlog.Manager } -func New(logger *logrus.Logger, address string, shutdownC chan struct{}, idleTimeout, maxTimeout time.Duration) (*SSHServer, error) { +func New(logManager sshlog.Manager, logger *logrus.Logger, address string, shutdownC chan struct{}, idleTimeout, maxTimeout time.Duration) (*SSHServer, error) { currentUser, err := user.Current() if err != nil { return nil, err @@ -42,6 +45,7 @@ func New(logger *logrus.Logger, address string, shutdownC chan struct{}, idleTim logger: logger, shutdownC: shutdownC, getUserFunc: lookupUser, + logManager: logManager, } if err := sshServer.configureHostKeys(); err != nil { @@ -67,6 +71,14 @@ func (s *SSHServer) Start() error { } func (s *SSHServer) connectionHandler(session ssh.Session) { + sessionID, err := uuid.NewRandom() + if err != nil { + if _, err := io.WriteString(session, "Failed to generate session ID\n"); err != nil { + s.logger.WithError(err).Error("Failed to generate session ID: Failed to write to SSH session") + } + s.CloseSession(session) + } + // Get uid and gid of user attempting to login sshUser, ok := session.Context().Value("sshUser").(*User) if !ok || sshUser == nil { @@ -134,11 +146,16 @@ func (s *SSHServer) connectionHandler(session ssh.Session) { defer pr.Close() defer pw.Close() - scanner := bufio.NewScanner(pr) - go func() { - for scanner.Scan() { - s.logger.Info(scanner.Text()) + logger, err := s.logManager.NewLogger(fmt.Sprintf("%s-session.log", sessionID), s.logger) + if err != nil { + if _, err := io.WriteString(session, "Failed to create log\n"); err != nil { + s.logger.WithError(err).Error("Failed to create log: Failed to write to SSH session") } + s.CloseSession(session) + } + defer logger.Close() + go func() { + io.Copy(logger, pr) }() // Write outgoing command output to both the command recorder, and remote user diff --git a/sshserver/sshserver_windows.go b/sshserver/sshserver_windows.go index ed72025a..5cf6c741 100644 --- a/sshserver/sshserver_windows.go +++ b/sshserver/sshserver_windows.go @@ -5,13 +5,15 @@ package sshserver import ( "errors" - "github.com/sirupsen/logrus" "time" + + "github.com/cloudflare/cloudflared/sshlog" + "github.com/sirupsen/logrus" ) type SSHServer struct{} -func New(_ *logrus.Logger, _ string, _ chan struct{}, _, _ time.Duration) (*SSHServer, error) { +func New(_ sshlog.Manager, _ *logrus.Logger, _ string, _ chan struct{}, _, _ time.Duration) (*SSHServer, error) { return nil, errors.New("cloudflared ssh server is not supported on windows") }