diff --git a/Makefile b/Makefile index 2850b22..54f516c 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ $(KEY): ssh-keygen -f $(KEY) -P '' run: $(BINARY) $(KEY) - ./$(BINARY) -i $(KEY) -b ":$(PORT)" -vv + ./$(BINARY) -i $(KEY) --bind ":$(PORT)" -vv test: go test . diff --git a/client.go b/client.go index 2476a6d..a82e1a3 100644 --- a/client.go +++ b/client.go @@ -86,6 +86,10 @@ func (c *Client) Rename(name string) { c.term.SetPrompt(fmt.Sprintf("[%s] ", name)) } +func (c *Client) Fingerprint() string { + return c.Conn.Permissions.Extensions["fingerprint"] +} + func (c *Client) handleShell(channel ssh.Channel) { defer channel.Close() @@ -129,13 +133,34 @@ func (c *Client) handleShell(channel ssh.Channel) { case "/whois": if len(parts) == 2 { client := c.Server.Who(parts[1]) - c.Msg <- fmt.Sprintf("-> %s is %s via %s", client.Name, client.Conn.RemoteAddr(), client.Conn.ClientVersion()) + if client != nil { + c.Msg <- fmt.Sprintf("-> %s is %s via %s", client.Name, client.Fingerprint(), client.Conn.ClientVersion()) + } else { + c.Msg <- fmt.Sprintf("-> No such name: %s", parts[1]) + } } else { c.Msg <- fmt.Sprintf("-> Missing $NAME from: /whois $NAME") } case "/list": names := c.Server.List(nil) c.Msg <- fmt.Sprintf("-> %d connected: %s", len(names), strings.Join(names, ", ")) + case "/ban": + if !c.Server.IsOp(c) { + c.Msg <- fmt.Sprintf("-> You're not an admin.") + } else if len(parts) != 2 { + c.Msg <- fmt.Sprintf("-> Missing $NAME from: /ban $NAME") + } else { + client := c.Server.Who(parts[1]) + if client == nil { + c.Msg <- fmt.Sprintf("-> No such name: %s", parts[1]) + } else { + fingerprint := client.Fingerprint() + client.Write(fmt.Sprintf("-> Banned by %s.", c.Name)) + c.Server.Ban(fingerprint, nil) + client.Conn.Close() + c.Server.Broadcast(fmt.Sprintf("* %s was banned by %s", parts[1], c.Name), nil) + } + } default: c.Msg <- fmt.Sprintf("-> Invalid command: %s", line) } diff --git a/cmd.go b/cmd.go index 08329ce..f3461b7 100644 --- a/cmd.go +++ b/cmd.go @@ -13,8 +13,9 @@ import ( type Options struct { Verbose []bool `short:"v" long:"verbose" description:"Show verbose logging."` - Bind string `short:"b" long:"bind" description:"Host and port to listen on." default:"0.0.0.0:22"` Identity string `short:"i" long:"identity" description:"Private key to identify server with." default:"~/.ssh/id_rsa"` + Bind string `long:"bind" description:"Host and port to listen on." default:"0.0.0.0:22"` + Admin string `long:"admin" description:"Fingerprint of pubkey to mark as admin."` } var logLevels = []log.Level{ @@ -66,6 +67,10 @@ func main() { return } + if options.Admin != "" { + server.Op(options.Admin) + } + <-sig // Wait for ^C signal logger.Warningf("Interrupt signal detected, shutting down.") server.Stop() diff --git a/server.go b/server.go index c103a24..1f342a5 100644 --- a/server.go +++ b/server.go @@ -1,11 +1,13 @@ package main import ( + "crypto/md5" "fmt" "net" "regexp" "strings" "sync" + "time" "golang.org/x/crypto/ssh" ) @@ -19,12 +21,13 @@ type Clients map[string]*Client type Server struct { sshConfig *ssh.ServerConfig - sshSigner *ssh.Signer done chan struct{} clients Clients lock sync.Mutex count int history *History + admins map[string]struct{} // fingerprint lookup + banned map[string]*time.Time // fingerprint lookup } func NewServer(privateKey []byte) (*Server, error) { @@ -33,26 +36,30 @@ func NewServer(privateKey []byte) (*Server, error) { return nil, err } + server := Server{ + done: make(chan struct{}), + clients: Clients{}, + count: 0, + history: NewHistory(HISTORY_LEN), + admins: map[string]struct{}{}, + banned: map[string]*time.Time{}, + } + config := ssh.ServerConfig{ NoClientAuth: false, - PasswordCallback: func(conn ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) { - return nil, nil - }, + // Auth-related things should be constant-time to avoid timing attacks. PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) { - // fingerprint := md5.Sum(key.Marshal()) - return nil, nil + fingerprint := Fingerprint(key) + if server.IsBanned(fingerprint) { + return nil, fmt.Errorf("Banned.") + } + perm := &ssh.Permissions{Extensions: map[string]string{"fingerprint": fingerprint}} + return perm, nil }, } config.AddHostKey(signer) - server := Server{ - sshConfig: &config, - sshSigner: &signer, - done: make(chan struct{}), - clients: Clients{}, - count: 0, - history: NewHistory(HISTORY_LEN), - } + server.sshConfig = &config return &server, nil } @@ -160,6 +167,50 @@ func (s *Server) Who(name string) *Client { return s.clients[name] } +func (s *Server) Op(fingerprint string) { + logger.Infof("Adding admin: %s", fingerprint) + s.lock.Lock() + s.admins[fingerprint] = struct{}{} + s.lock.Unlock() +} + +func (s *Server) IsOp(client *Client) bool { + _, r := s.admins[client.Fingerprint()] + return r +} + +func (s *Server) IsBanned(fingerprint string) bool { + ban, hasBan := s.banned[fingerprint] + if !hasBan { + return false + } + if ban == nil { + return true + } + if ban.Before(time.Now()) { + s.Unban(fingerprint) + return false + } + return true +} + +func (s *Server) Ban(fingerprint string, duration *time.Duration) { + var until *time.Time + s.lock.Lock() + if duration != nil { + when := time.Now().Add(*duration) + until = &when + } + s.banned[fingerprint] = until + s.lock.Unlock() +} + +func (s *Server) Unban(fingerprint string) { + s.lock.Lock() + delete(s.banned, fingerprint) + s.lock.Unlock() +} + func (s *Server) Start(laddr string) error { // Once a ServerConfig has been configured, connections can be // accepted. @@ -214,3 +265,9 @@ func (s *Server) Stop() { close(s.done) } + +func Fingerprint(k ssh.PublicKey) string { + hash := md5.Sum(k.Marshal()) + r := fmt.Sprintf("% x", hash) + return strings.Replace(r, " ", ":", -1) +}