Skip to content

Commit e37d10e

Browse files
committed
Spoof client source IP
1 parent 10bc187 commit e37d10e

File tree

3 files changed

+143
-31
lines changed

3 files changed

+143
-31
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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.NewSearchRequest(
47+
c.baseDn,
48+
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
49+
fmt.Sprintf("(|(scriptsVhostName=%s)(scriptsVhostAlias=%s))", escapedHostname, escapedHostname),
50+
[]string{"scriptsVhostPoolIPv4"},
51+
nil,
52+
)
53+
sr, err := c.conn.Search(req)
54+
if err != nil {
55+
return "", err
56+
}
57+
for _, entry := range sr.Entries {
58+
return entry.GetAttributeValue("scriptsVhostPoolIPv4"), nil
59+
}
60+
// Not found is not an error
61+
return "", nil
62+
}
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: 33 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", ":80", "comma-separated addresses to listen for HTTP traffic on")
1817
sniAddrs = flag.String("sni_addrs", ":443,: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,13 +39,13 @@ 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
}
@@ -52,44 +55,43 @@ func (l *ldapTarget) HandleConn(netConn net.Conn) {
5255
return
5356
}
5457
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)
58+
destAddrStr := net.JoinHostPort(pool, fmt.Sprintf("%d", laddr.Port))
59+
destAddr, err := net.ResolveTCPAddr("tcp", destAddrStr)
7260
if err != nil {
73-
return "", err
61+
netConn.Close()
62+
log.Printf("parsing pool address %q: %v", pool, err)
63+
return
64+
}
65+
dp := &tcpproxy.DialProxy{
66+
Addr: destAddrStr,
7467
}
75-
for _, entry := range sr.Entries {
76-
return entry.GetAttributeValue("scriptsVhostPoolIPv4"), nil
68+
raddr := netConn.RemoteAddr().(*net.TCPAddr)
69+
if l.localPoolRange.Contains(destAddr.IP) {
70+
sourceAddr := &net.TCPAddr{
71+
IP: raddr.IP,
72+
}
73+
dp.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) {
74+
return net.DialTCP(network, sourceAddr, destAddr)
75+
}
7776
}
78-
// Not found is not an error
79-
return "", nil
77+
dp.HandleConn(netConn)
8078
}
8179

8280
func main() {
8381
flag.Parse()
8482

85-
l, err := ldap.Dial("tcp", *ldapServer)
83+
_, ipnet, err := net.ParseCIDR(*localRange)
8684
if err != nil {
8785
log.Fatal(err)
8886
}
89-
defer l.Close()
87+
88+
ldapPool := ldap.NewPool(strings.Split(*ldapServers, ","), *baseDn, ldapRetries)
9089

9190
var p tcpproxy.Proxy
92-
t := &ldapTarget{ldap: l}
91+
t := &ldapTarget{
92+
localPoolRange: ipnet,
93+
ldap: ldapPool,
94+
}
9395
for _, addr := range strings.Split(*httpAddrs, ",") {
9496
p.AddHTTPHostMatchRoute(addr, always, t)
9597
}

0 commit comments

Comments
 (0)