194 lines
4.5 KiB
Go
194 lines
4.5 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/gorilla/websocket"
|
|
)
|
|
|
|
// ldapTunnel is the agent's local LDAP socket (DESIGN.md §4). It is a pure byte
|
|
// pump: bytes from a local LDAP client (SSSD) are forwarded to the SSO over the
|
|
// WSS channel as `ldap_tunnel` messages, and the SSO's responses are written
|
|
// back to the socket. The agent never parses LDAP — it does not know or care
|
|
// what the bytes mean.
|
|
//
|
|
// Each local connection gets a conn_id. The agent reads the socket and sends
|
|
// chunks up; the SSO relays them into its real OpenLDAP and sends the response
|
|
// chunks back down; the agent writes them to the socket. `close:true` ends a
|
|
// connection.
|
|
type ldapTunnel struct {
|
|
mu sync.Mutex
|
|
conns map[string]net.Conn
|
|
send func(WSMessage) error
|
|
}
|
|
|
|
func newLdapTunnel(send func(WSMessage) error) *ldapTunnel {
|
|
return &ldapTunnel{
|
|
conns: make(map[string]net.Conn),
|
|
send: send,
|
|
}
|
|
}
|
|
|
|
// start binds both unix socket and TCP loopback, accepting connections until stopCh closes.
|
|
func (t *ldapTunnel) start(socketPath string, stopCh <-chan struct{}) {
|
|
os.Remove(socketPath)
|
|
if dir := filepath.Dir(socketPath); dir != "." && dir != "/" {
|
|
os.MkdirAll(dir, 0755)
|
|
}
|
|
|
|
// 1. UNIX Domain Socket Listener
|
|
lnUnix, err := net.Listen("unix", socketPath)
|
|
if err == nil {
|
|
os.Chmod(socketPath, 0666)
|
|
log.Printf("LDAP tunnel: listening on unix socket %s", socketPath)
|
|
go t.acceptLoop(lnUnix, stopCh)
|
|
} else {
|
|
log.Printf("LDAP tunnel: cannot bind unix socket %s: %v", socketPath, err)
|
|
}
|
|
|
|
// 2. TCP Loopback Listener (127.0.0.1:389 with fallback to 127.0.0.1:3890)
|
|
lnTcp, errTcp := net.Listen("tcp", "127.0.0.1:389")
|
|
if errTcp != nil {
|
|
lnTcp, errTcp = net.Listen("tcp", "127.0.0.1:3890")
|
|
}
|
|
|
|
if errTcp == nil {
|
|
log.Printf("LDAP tunnel: listening on tcp %s", lnTcp.Addr().String())
|
|
go t.acceptLoop(lnTcp, stopCh)
|
|
} else {
|
|
log.Printf("LDAP tunnel: cannot bind tcp loopback: %v", errTcp)
|
|
}
|
|
}
|
|
|
|
func (t *ldapTunnel) acceptLoop(ln net.Listener, stopCh <-chan struct{}) {
|
|
defer ln.Close()
|
|
go func() {
|
|
<-stopCh
|
|
ln.Close()
|
|
}()
|
|
for {
|
|
conn, err := ln.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
go t.handleConn(conn)
|
|
}
|
|
}
|
|
|
|
// handleConn pumps one local LDAP connection up to the SSO.
|
|
func (t *ldapTunnel) handleConn(conn net.Conn) {
|
|
connID := newConnID()
|
|
t.mu.Lock()
|
|
t.conns[connID] = conn
|
|
t.mu.Unlock()
|
|
|
|
buf := make([]byte, 32*1024)
|
|
for {
|
|
n, err := conn.Read(buf)
|
|
if n > 0 {
|
|
msg := WSMessage{
|
|
Type: "ldap_tunnel",
|
|
Payload: map[string]interface{}{
|
|
"conn_id": connID,
|
|
"data": base64.StdEncoding.EncodeToString(buf[:n]),
|
|
},
|
|
}
|
|
if err := t.send(msg); err != nil {
|
|
break
|
|
}
|
|
}
|
|
if err != nil {
|
|
break
|
|
}
|
|
}
|
|
|
|
// Signal end of connection to the SSO so it can close its OpenLDAP relay.
|
|
t.send(WSMessage{
|
|
Type: "ldap_tunnel",
|
|
Payload: map[string]interface{}{
|
|
"conn_id": connID,
|
|
"close": true,
|
|
},
|
|
})
|
|
|
|
t.mu.Lock()
|
|
delete(t.conns, connID)
|
|
t.mu.Unlock()
|
|
conn.Close()
|
|
}
|
|
|
|
// handleMessage writes SSO→agent tunnel bytes to the matching local socket.
|
|
// Called from handleCommand when an `ldap_tunnel` message arrives.
|
|
func (t *ldapTunnel) handleMessage(payload map[string]interface{}) {
|
|
connID, _ := payload["conn_id"].(string)
|
|
if connID == "" {
|
|
return
|
|
}
|
|
|
|
if closeFlag, _ := payload["close"].(bool); closeFlag {
|
|
t.mu.Lock()
|
|
conn := t.conns[connID]
|
|
delete(t.conns, connID)
|
|
t.mu.Unlock()
|
|
if conn != nil {
|
|
conn.Close()
|
|
}
|
|
return
|
|
}
|
|
|
|
dataStr, _ := payload["data"].(string)
|
|
if dataStr == "" {
|
|
return
|
|
}
|
|
data, err := base64.StdEncoding.DecodeString(dataStr)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
t.mu.Lock()
|
|
conn := t.conns[connID]
|
|
t.mu.Unlock()
|
|
if conn != nil {
|
|
conn.Write(data)
|
|
}
|
|
}
|
|
|
|
var connCounter uint64
|
|
|
|
func newConnID() string {
|
|
connCounter++
|
|
return fmt.Sprintf("%d-%d", time.Now().UnixNano(), connCounter)
|
|
}
|
|
|
|
// safeWriter serializes writes to the WebSocket. Gorilla allows only one
|
|
// concurrent writer, but the agent has several (telemetry, heartbeat, the LDAP
|
|
// tunnel, command responses) — without this, concurrent WriteMessage calls
|
|
// corrupt the stream.
|
|
type safeWriter struct {
|
|
mu sync.Mutex
|
|
c *websocket.Conn
|
|
}
|
|
|
|
func (w *safeWriter) WriteMessage(messageType int, data []byte) error {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
return w.c.WriteMessage(messageType, data)
|
|
}
|
|
|
|
// sendTunnelMessage marshals a WSMessage and writes it as a text frame.
|
|
func sendTunnelMessage(w MessageWriter, msg WSMessage) error {
|
|
payload, err := json.Marshal(msg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return w.WriteMessage(websocket.TextMessage, payload)
|
|
}
|