Skip to content

Commit 879aa10

Browse files
committed
Spoof client source IP
1 parent f6cf9b7 commit 879aa10

File tree

3 files changed

+143
-31
lines changed

3 files changed

+143
-31
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package ldap
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"sync"
7+
"time"
8+
9+
ldap "gopkg.in/ldap.v3"
10+
)
11+
12+
type conn struct {
13+
server, baseDn string
14+
// mu protects conn during reconnect cycles
15+
// TODO: The ldap package supports multiple in-flight queries;
16+
// by using a Mutex we are only going to issue one at a
17+
// time. We should figure out how to do retry/reconnect
18+
// behavior with parallel queries.
19+
mu sync.Mutex
20+
conn *ldap.Conn
21+
}
22+
23+
func (c *conn) reconnect() {
24+
c.mu.Lock()
25+
defer c.mu.Unlock()
26+
if c.conn != nil {
27+
c.conn.Close()
28+
}
29+
var err error
30+
for {
31+
log.Printf("connecting to %s", c.server)
32+
c.conn, err = ldap.Dial("tcp", c.server)
33+
if err == nil {
34+
return
35+
}
36+
log.Printf("connecting to %s: %v", c.server, err)
37+
time.Sleep(100 * time.Millisecond)
38+
}
39+
}
40+
41+
func (c *conn) resolvePool(hostname string) (string, error) {
42+
c.mu.Lock()
43+
defer c.mu.Unlock()
44+
45+
escapedHostname := ldap.EscapeFilter(hostname)
46+
req := &ldap.SearchRequest{
47+
BaseDN: c.baseDn,
48+
Scope: ldap.ScopeWholeSubtree,
49+
Filter: fmt.Sprintf("(|(scriptsVhostName=%s)(scriptsVhostAlias=%s))", escapedHostname, escapedHostname),
50+
Attributes: []string{"scriptsVhostPoolIPv4"},
51+
}
52+
sr, err := c.conn.Search(req)
53+
if err != nil {
54+
return "", err
55+
}
56+
for _, entry := range sr.Entries {
57+
return entry.GetAttributeValue("scriptsVhostPoolIPv4"), nil
58+
}
59+
// Not found is not an error
60+
return "", nil
61+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package ldap
2+
3+
import "log"
4+
5+
// Pool handles a concurrency-safe pool of connections to LDAP servers.
6+
type Pool struct {
7+
retries int
8+
// connCh holds open connections to servers.
9+
connCh chan *conn
10+
}
11+
12+
// NewPool constructs a connection pool that queries for baseDn from servers.
13+
func NewPool(servers []string, baseDn string, retries int) *Pool {
14+
p := &Pool{
15+
retries: retries,
16+
connCh: make(chan *conn, len(servers)),
17+
}
18+
for _, s := range servers {
19+
c := &conn{
20+
server: s,
21+
baseDn: baseDn,
22+
}
23+
go p.reconnect(c)
24+
}
25+
return p
26+
}
27+
28+
func (p *Pool) reconnect(c *conn) {
29+
c.reconnect()
30+
p.connCh <- c
31+
}
32+
33+
// ResolvePool attempts to resolve the pool for hostname to an IP address, returned as a string.
34+
func (p *Pool) ResolvePool(hostname string) (string, error) {
35+
var ip string
36+
var err error
37+
for i := 0; i < p.retries; i++ {
38+
c := <-p.connCh
39+
ip, err = c.resolvePool(hostname)
40+
if err == nil {
41+
p.connCh <- c
42+
return ip, err
43+
}
44+
log.Printf("resolving %q on %s: %v", hostname, c.server, err)
45+
go p.reconnect(c)
46+
}
47+
return ip, err
48+
}

server/common/oursrc/scripts-proxy/main.go

Lines changed: 34 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,28 @@ import (
88
"net"
99
"strings"
1010

11+
"github.com/mit-scripts/scripts/server/common/oursrc/scripts-proxy/ldap"
1112
"inet.af/tcpproxy"
12-
13-
ldap "gopkg.in/ldap.v3"
1413
)
1514

1615
var (
1716
httpAddrs = flag.String("http_addrs", "0.0.0.0:80", "comma-separated addresses to listen for HTTP traffic on")
1817
sniAddrs = flag.String("sni_addrs", "0.0.0.0:443,0.0.0.0:444", "comma-separated addresses to listen for SNI traffic on")
19-
ldapServer = flag.String("ldap_server", "scripts-ldap.mit.edu:389", "LDAP server to query")
18+
ldapServers = flag.String("ldap_servers", "scripts-ldap.mit.edu:389", "comma-spearated LDAP servers to query")
2019
defaultHost = flag.String("default_host", "scripts.mit.edu", "default host to route traffic to if SNI/Host header cannot be parsed or cannot be found in LDAP")
2120
baseDn = flag.String("base_dn", "ou=VirtualHosts,dc=scripts,dc=mit,dc=edu", "base DN to query for hosts")
21+
localRange = flag.String("local_range", "18.4.86.0/24", "IP block for client IP spoofing. If the resolved destination address is in this subnet, the source IP address of the backend connection will be spoofed to match the client IP. This subnet needs to be local to the proxy.")
2222
)
2323

24+
const ldapRetries = 3
25+
2426
func always(context.Context, string) bool {
2527
return true
2628
}
2729

2830
type ldapTarget struct {
29-
ldap *ldap.Conn
31+
localPoolRange *net.IPNet
32+
ldap *ldap.Pool
3033
}
3134

3235
// HandleConn is called by tcpproxy after receiving a connection and sniffing the host.
@@ -36,60 +39,60 @@ func (l *ldapTarget) HandleConn(netConn net.Conn) {
3639
var pool string
3740
var err error
3841
if conn, ok := netConn.(*tcpproxy.Conn); ok {
39-
pool, err = l.resolvePool(conn.HostName)
42+
pool, err = l.ldap.ResolvePool(conn.HostName)
4043
if err != nil {
4144
log.Printf("resolving %q: %v", conn.HostName, err)
4245
}
4346
}
4447
if pool == "" {
45-
pool, err = l.resolvePool(*defaultHost)
48+
pool, err = l.ldap.ResolvePool(*defaultHost)
4649
if err != nil {
4750
log.Printf("resolving default pool: %v", err)
4851
}
4952
}
53+
// TODO: Serve an error page? Forward to scripts-director?
5054
if pool == "" {
5155
netConn.Close()
5256
return
5357
}
5458
laddr := netConn.LocalAddr().(*net.TCPAddr)
55-
dp := &tcpproxy.DialProxy{
56-
Addr: fmt.Sprintf("%s:%d", pool, laddr.Port),
57-
// TODO: Set DialContext to override the source address
58-
}
59-
dp.HandleConn(netConn)
60-
}
61-
62-
func (l *ldapTarget) resolvePool(hostname string) (string, error) {
63-
escapedHostname := ldap.EscapeFilter(hostname)
64-
req := ldap.NewSearchRequest(
65-
*baseDn,
66-
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
67-
fmt.Sprintf("(|(scriptsVhostName=%s)(scriptsVhostAlias=%s))", escapedHostname, escapedHostname),
68-
[]string{"scriptsVhostPoolIPv4"},
69-
nil,
70-
)
71-
sr, err := l.ldap.Search(req)
59+
destAddrStr := net.JoinHostPort(pool, fmt.Sprintf("%d", laddr.Port))
60+
destAddr, err := net.ResolveTCPAddr("tcp", destAddrStr)
7261
if err != nil {
73-
return "", err
62+
netConn.Close()
63+
log.Printf("parsing pool address %q: %v", pool, err)
64+
return
65+
}
66+
dp := &tcpproxy.DialProxy{
67+
Addr: destAddrStr,
7468
}
75-
for _, entry := range sr.Entries {
76-
return entry.GetAttributeValue("scriptsVhostPoolIPv4"), nil
69+
if l.localPoolRange.Contains(destAddr.IP) {
70+
raddr := netConn.RemoteAddr().(*net.TCPAddr)
71+
sourceAddr := &net.TCPAddr{
72+
IP: raddr.IP,
73+
}
74+
dp.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) {
75+
return net.DialTCP(network, sourceAddr, destAddr)
76+
}
7777
}
78-
// Not found is not an error
79-
return "", nil
78+
dp.HandleConn(netConn)
8079
}
8180

8281
func main() {
8382
flag.Parse()
8483

85-
l, err := ldap.Dial("tcp", *ldapServer)
84+
_, ipnet, err := net.ParseCIDR(*localRange)
8685
if err != nil {
8786
log.Fatal(err)
8887
}
89-
defer l.Close()
88+
89+
ldapPool := ldap.NewPool(strings.Split(*ldapServers, ","), *baseDn, ldapRetries)
9090

9191
var p tcpproxy.Proxy
92-
t := &ldapTarget{ldap: l}
92+
t := &ldapTarget{
93+
localPoolRange: ipnet,
94+
ldap: ldapPool,
95+
}
9396
for _, addr := range strings.Split(*httpAddrs, ",") {
9497
p.AddHTTPHostMatchRoute(addr, always, t)
9598
}

0 commit comments

Comments
 (0)