forked from dorkamotorka/transparent-proxy-ebpf
-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.go
174 lines (151 loc) · 5.42 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
package main
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -type Config proxy proxy.c
import (
"fmt"
"io"
"log"
"net"
"os"
"syscall"
"time"
"unsafe"
"github.com/cilium/ebpf"
"github.com/cilium/ebpf/link"
"github.com/cilium/ebpf/rlimit"
)
const (
CGROUP_PATH = "/sys/fs/cgroup" // Root cgroup path
PROXY_PORT = 18000 // Port where the proxy server listens
SO_ORIGINAL_DST = 80 // Socket option to get the original destination address
)
// SockAddrIn is a struct to hold the sockaddr_in structure for IPv4 "retrieved" by the SO_ORIGINAL_DST.
type SockAddrIn struct {
SinFamily uint16
SinPort [2]byte
SinAddr [4]byte
// Pad to match the size of sockaddr_in
Pad [8]byte
}
// helper function for getsockopt
func getsockopt(s int, level int, optname int, optval unsafe.Pointer, optlen *uint32) (err error) {
_, _, e := syscall.Syscall6(syscall.SYS_GETSOCKOPT, uintptr(s), uintptr(level), uintptr(optname), uintptr(optval), uintptr(unsafe.Pointer(optlen)), 0)
if e != 0 {
return e
}
return
}
// HTTP proxy request handler
func handleConnection(conn net.Conn) {
defer conn.Close()
// Using RawConn is necessary to perform low-level operations on the underlying socket file descriptor in Go.
// This allows us to use getsockopt to retrieve the original destination address set by the SO_ORIGINAL_DST option,
// which isn't directly accessible through Go's higher-level networking API.
rawConn, err := conn.(*net.TCPConn).SyscallConn()
if err != nil {
log.Printf("Failed to get raw connection: %v", err)
return
}
var originalDst SockAddrIn
// If Control is not nil, it is called after creating the network connection but before binding it to the operating system.
rawConn.Control(func(fd uintptr) {
optlen := uint32(unsafe.Sizeof(originalDst))
// Retrieve the original destination address by making a syscall with the SO_ORIGINAL_DST option.
err = getsockopt(int(fd), syscall.SOL_IP, SO_ORIGINAL_DST, unsafe.Pointer(&originalDst), &optlen)
if err != nil {
log.Printf("getsockopt SO_ORIGINAL_DST failed: %v", err)
}
})
targetAddr := net.IPv4(originalDst.SinAddr[0], originalDst.SinAddr[1], originalDst.SinAddr[2], originalDst.SinAddr[3]).String()
targetPort := (uint16(originalDst.SinPort[0]) << 8) | uint16(originalDst.SinPort[1])
fmt.Printf("Original destination: %s:%d\n", targetAddr, targetPort)
// Check that the original destination address is reachable from the proxy
targetConn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", targetAddr, targetPort), 5*time.Second)
if err != nil {
log.Printf("Failed to connect to original destination: %v", err)
return
}
defer targetConn.Close()
fmt.Printf("Proxying connection from %s to %s\n", conn.RemoteAddr(), targetConn.RemoteAddr())
// The following code creates two data transfer channels:
// - From the client to the target server (handled by a separate goroutine).
// - From the target server to the client (handled by the main goroutine).
go func() {
_, err = io.Copy(targetConn, conn)
if err != nil {
log.Printf("Failed copying data to target: %v", err)
}
}()
_, err = io.Copy(conn, targetConn)
if err != nil {
log.Printf("Failed copying data from target: %v", err)
}
}
func main() {
// Remove resource limits for kernels <5.11.
if err := rlimit.RemoveMemlock(); err != nil {
log.Print("Removing memlock:", err)
}
// Load the compiled eBPF ELF and load it into the kernel
// NOTE: we could also pin the eBPF program
var objs proxyObjects
if err := loadProxyObjects(&objs, nil); err != nil {
log.Print("Error loading eBPF objects:", err)
}
defer objs.Close()
// Attach eBPF programs to the root cgroup
connect4Link, err := link.AttachCgroup(link.CgroupOptions{
Path: CGROUP_PATH,
Attach: ebpf.AttachCGroupInet4Connect,
Program: objs.CgConnect4,
})
if err != nil {
log.Print("Attaching CgConnect4 program to Cgroup:", err)
}
defer connect4Link.Close()
sockopsLink, err := link.AttachCgroup(link.CgroupOptions{
Path: CGROUP_PATH,
Attach: ebpf.AttachCGroupSockOps,
Program: objs.CgSockOps,
})
if err != nil {
log.Print("Attaching CgSockOps program to Cgroup:", err)
}
defer sockopsLink.Close()
sockoptLink, err := link.AttachCgroup(link.CgroupOptions{
Path: CGROUP_PATH,
Attach: ebpf.AttachCGroupGetsockopt,
Program: objs.CgSockOpt,
})
if err != nil {
log.Print("Attaching CgSockOpt program to Cgroup:", err)
}
defer sockoptLink.Close()
// Start the proxy server on the localhost
// We only demonstrate IPv4 in this example, but the same approach can be used for IPv6
proxyAddr := fmt.Sprintf("127.0.0.1:%d", PROXY_PORT)
listener, err := net.Listen("tcp", proxyAddr)
if err != nil {
log.Fatalf("Failed to start proxy server: %v", err)
}
defer listener.Close()
// Update the proxyMaps map with the proxy server configuration, because we need to know the proxy server PID in order
// to filter out eBPF events generated by the proxy server itself so it would not proxy its own packets in a loop.
var key uint32 = 0
config := proxyConfig{
ProxyPort: PROXY_PORT,
ProxyPid: uint64(os.Getpid()),
}
err = objs.proxyMaps.MapConfig.Update(&key, &config, ebpf.UpdateAny)
if err != nil {
log.Fatalf("Failed to update proxyMaps map: %v", err)
}
log.Printf("Proxy server with PID %d listening on %s", os.Getpid(), proxyAddr)
for {
conn, err := listener.Accept()
if err != nil {
log.Printf("Failed to accept connection: %v", err)
continue
}
go handleConnection(conn)
}
}