RemoteAddr is preserved in SSH->HTTP bridge for Auth API

Got rid of MemoryListener, instead replacing it with a closely tied pair
of "fake socket" and "fake connection", where the connection wraps
around the undrlying SSH channel. This allows me:

- Know exactly when connection.Close() is called (no wait for in-memory pipes)
- Keep the original remote addr around

Most of the changes are in api_with_roles.go
This commit is contained in:
Ev Kontsevoy
2016-03-01 16:45:10 -08:00
parent 25878badbc
commit 4680296756
4 changed files with 120 additions and 114 deletions
+113 -7
View File
@@ -21,6 +21,7 @@ import (
"net"
"net/http"
"sync"
"time"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/lib/events"
@@ -30,10 +31,12 @@ import (
log "github.com/Sirupsen/logrus"
"github.com/gravitational/trace"
"golang.org/x/crypto/ssh"
)
type APIWithRoles struct {
listeners map[teleport.Role]*utils.MemoryListener
listeners map[teleport.Role]*fakeSocket
servers map[teleport.Role]*APIServer
}
@@ -45,7 +48,7 @@ func NewAPIWithRoles(authServer *AuthServer,
roles []teleport.Role) *APIWithRoles {
api := APIWithRoles{}
api.listeners = make(map[teleport.Role]*utils.MemoryListener)
api.listeners = make(map[teleport.Role]*fakeSocket)
api.servers = make(map[teleport.Role]*APIServer)
for _, role := range roles {
@@ -58,7 +61,7 @@ func NewAPIWithRoles(authServer *AuthServer,
role: role,
}
api.servers[role] = NewAPIServer(&a)
api.listeners[role] = utils.NewMemoryListener()
api.listeners[role] = makefakeSocket()
}
return &api
}
@@ -68,6 +71,7 @@ func (api *APIWithRoles) Serve() {
for role, _ := range api.listeners {
wg.Add(1)
go func(listener net.Listener, handler http.Handler) {
log.Debugf("[api_with_roles] about to call http.Serve() on a listener")
if err := http.Serve(listener, handler); (err != nil) && (err != io.EOF) {
log.Errorf(err.Error())
}
@@ -76,19 +80,121 @@ func (api *APIWithRoles) Serve() {
wg.Wait()
}
func (api *APIWithRoles) HandleConn(conn net.Conn, role teleport.Role) error {
// HandleNewChannel is called when a new SSH channel (SSH connection) wants to communicate via HTTP API
// to one of the API servers
func (api *APIWithRoles) HandleNewChannel(remoteAddr net.Addr, channel ssh.Channel, role teleport.Role) error {
// find a listener mapped to the requested role:
listener, ok := api.listeners[role]
if !ok {
conn.Close()
channel.Close()
return trace.Errorf("no such role '%v'", role)
}
log.Debugf("---------> APIWithRoles.HandleConn remote addr: %v", conn.RemoteAddr())
return listener.Handle(conn)
// create a bridge between the incoming SSH channel to the HTTP-based API server
return listener.CreateBridge(remoteAddr, channel)
}
func (api *APIWithRoles) Close() {
for _, listener := range api.listeners {
listener.Close()
}
}
// Implements a fake "socket" (net.Listener interface) on top of exisitng ssh.Channel
type fakeSocket struct {
closed chan int
connections chan net.Conn
}
func makefakeSocket() *fakeSocket {
return &fakeSocket{
closed: make(chan int),
connections: make(chan net.Conn),
}
}
type FakeSSHConnection struct {
remoteAddr net.Addr
sshChan ssh.Channel
closed chan int
}
func (conn *FakeSSHConnection) Read(b []byte) (n int, err error) {
return conn.sshChan.Read(b)
}
func (conn *FakeSSHConnection) Write(b []byte) (n int, err error) {
return conn.sshChan.Write(b)
}
func (conn *FakeSSHConnection) Close() error {
conn.closed <- 1
return conn.sshChan.Close()
}
func (conn *FakeSSHConnection) RemoteAddr() net.Addr {
return conn.remoteAddr
}
func (conn *FakeSSHConnection) LocalAddr() net.Addr {
return &utils.NetAddr{AddrNetwork: "tcp", Addr: "socket.over.ssh"}
}
// SetDeadline is needed to implement net.Conn interface
func (conn *FakeSSHConnection) SetDeadline(t time.Time) error {
return nil
}
// SetReadDeadline is needed to implement net.Conn interface
func (conn *FakeSSHConnection) SetReadDeadline(t time.Time) error {
return nil
}
// SetWriteDeadline is needed to implement net.Conn interface
func (conn *FakeSSHConnection) SetWriteDeadline(t time.Time) error {
return nil
}
// CreateBridge takes an incoming SSH connection and creates an SSH-to-HTTP "bridge connection"
// and waits for that connection to be closed either by the client or by the server
func (socket *fakeSocket) CreateBridge(remoteAddr net.Addr, sshChan ssh.Channel) error {
log.Debugf("SocketOverSSH.Handle(from=%v) is called", remoteAddr)
// wrap sshChan into a 'fake connection' which allows us to
// a) preserve the original address of the connected client
// b) sit and wait until client closes the ssh channel, so we'll close this fake socket
connection := &FakeSSHConnection{
remoteAddr: remoteAddr,
sshChan: sshChan,
closed: make(chan int),
}
socket.connections <- connection // Accept() unblocks here
// wait for the connection to close:
select {
case <-connection.closed:
break
}
log.Debugf("SocketOverSSH.Handle(from=%v) is done", remoteAddr)
return nil
}
// Accept waits for new connections to arrive (via CreateBridge) and returns them to
// the blocked http.Serve()
func (socket *fakeSocket) Accept() (c net.Conn, err error) {
select {
case newConnection := <-socket.connections:
return newConnection, nil
case <-socket.closed:
return nil, io.EOF
}
}
// Close closes the listener.
// Any blocked Accept operations will be unblocked and return errors.
func (socket *fakeSocket) Close() error {
return nil
}
// Addr returns the listener's network address.
func (socket *fakeSocket) Addr() net.Addr {
return &utils.NetAddr{AddrNetwork: "tcp", Addr: "socket.over.ssh"}
}
+6 -9
View File
@@ -277,23 +277,20 @@ func (s *AuthTunnel) handleWebAgentRequest(sconn *ssh.ServerConn, ch ssh.Channel
// handleDirectTCPIPRequest accepts an incoming SSH connection via TCP/IP and forwards
// it to the local auth server which listens on local UNIX pipe
func (s *AuthTunnel) handleDirectTCPIPRequest(sconn *ssh.ServerConn, ch ssh.Channel, req *sshutils.DirectTCPIPReq) {
func (s *AuthTunnel) handleDirectTCPIPRequest(sconn *ssh.ServerConn, sshChannel ssh.Channel, req *sshutils.DirectTCPIPReq) {
defer sconn.Close()
log.Debugf("[TUNNEL] ------> Remote address: %v", sconn.RemoteAddr())
// retreive the role from thsi connection's permissions (make sure it's a valid role)
role := teleport.Role(sconn.Permissions.Extensions[ExtRole])
if err := role.Check(); err != nil {
log.Errorf(err.Error())
return
}
// forward this to API-with-roles server, it will try to proxy this connection
// to an API service mapped to this role:
addr := &utils.NetAddr{Addr: "localhost", AddrNetwork: "tcp://"}
conn := utils.NewPipeNetConn(ch, ch, ch, addr, sconn.RemoteAddr())
if err := s.apiServer.HandleConn(conn, role); err != nil {
log.Errorf(err.Error())
// Forward this new SSH channel to API-with-roles server. It will try to proxy this
// connection to the API service mapped to this role:
if err := s.apiServer.HandleNewChannel(sconn.RemoteAddr(), sshChannel, role); err != nil {
log.Error(err)
return
}
}
-97
View File
@@ -1,97 +0,0 @@
/*
Copyright 2015 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// package auth implements certificate signing authority and access control server
// Authority server is composed of several parts:
//
// * Authority server itself that implements signing and acl logic
// * HTTP server wrapper for authority server
// * HTTP client wrapper
//
package utils
import (
"github.com/gravitational/trace"
"io"
"net"
"sync"
"sync/atomic"
)
// Memory listener implements net.Listener using net.Conn
type MemoryListener struct {
connections chan net.Conn
state chan int
closed uint32
}
func NewMemoryListener() *MemoryListener {
ml := &MemoryListener{}
ml.connections = make(chan net.Conn)
ml.state = make(chan int)
return ml
}
func (ml *MemoryListener) Accept() (net.Conn, error) {
select {
case newConnection := <-ml.connections:
return newConnection, nil
case <-ml.state:
return nil, io.EOF
}
}
func (ml *MemoryListener) Close() error {
if atomic.CompareAndSwapUint32(&ml.closed, 0, 1) {
close(ml.state)
}
return nil
}
func (ml *MemoryListener) Handle(conn net.Conn) error {
select {
case <-ml.state:
return trace.Errorf("MemoryListener is closed")
default:
}
local, remote := net.Pipe()
wg := &sync.WaitGroup{}
wg.Add(2)
go func() {
defer wg.Done()
defer local.Close()
io.Copy(local, conn)
}()
go func() {
defer wg.Done()
defer conn.Close()
io.Copy(conn, local)
}()
ml.connections <- remote
wg.Wait()
return nil
}
func (ml *MemoryListener) Addr() net.Addr {
addr := NetAddr{
AddrNetwork: "tcp",
Addr: "memoryListener",
}
return &addr
}
+1 -1
View File
@@ -1 +1 @@
/home/ekontsevoy/go/src/github.com/gravitational/teleport/web/dist/app
/var/go/src/github.com/gravitational/teleport/web/dist/app