Banhammer!

This commit is contained in:
Andrey Petrov 2014-12-12 14:50:14 -08:00
parent 8f408b7674
commit af20d755e7
4 changed files with 104 additions and 17 deletions

View File

@ -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 .

View File

@ -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)
}

7
cmd.go
View File

@ -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()

View File

@ -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)
}