436 lines
13 KiB
Go
436 lines
13 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"crypto/tls"
|
|
"encoding/base64"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const browserProxyHost = "docker-gateway"
|
|
|
|
type memoryProxyRegistry struct {
|
|
mu sync.Mutex
|
|
proxies map[string]*memoryProxy
|
|
}
|
|
|
|
type memoryProxy struct {
|
|
mu sync.RWMutex
|
|
exit gatewayProxyExit
|
|
bindingVersion int64
|
|
runtimeID string
|
|
networkID string
|
|
bindHost string
|
|
listener net.Listener
|
|
server *http.Server
|
|
tunnels map[net.Conn]net.Conn
|
|
url string
|
|
}
|
|
|
|
func newMemoryProxyRegistry() *memoryProxyRegistry {
|
|
return &memoryProxyRegistry{proxies: map[string]*memoryProxy{}}
|
|
}
|
|
|
|
func (registry *memoryProxyRegistry) configure(alias string, bindingVersion int64, bindHost string, port int, exit gatewayProxyExit,
|
|
networkIDs ...string) (string, func(), error) {
|
|
registry.mu.Lock()
|
|
defer registry.mu.Unlock()
|
|
networkID := ""
|
|
if len(networkIDs) == 1 {
|
|
networkID = networkIDs[0]
|
|
}
|
|
if proxy := registry.proxies[alias]; proxy != nil {
|
|
if proxy.bindingVersion == bindingVersion && proxy.bindHost == bindHost &&
|
|
(port == 0 || proxy.listener.Addr().(*net.TCPAddr).Port == port) && proxy.exit == exit && proxy.networkID == networkID {
|
|
return proxy.url, func() { registry.removeObject(alias, proxy) }, nil
|
|
}
|
|
delete(registry.proxies, alias)
|
|
// A gateway replica may still hold the previous runtime generation. Close
|
|
// it before rebinding a restored listener, while removeObject's identity
|
|
// check keeps old cleanup callbacks from deleting the replacement.
|
|
closeMemoryProxy(proxy)
|
|
}
|
|
listener, err := net.Listen("tcp4", net.JoinHostPort(bindHost, strconv.Itoa(port)))
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
actualPort := listener.Addr().(*net.TCPAddr).Port
|
|
proxy := &memoryProxy{exit: exit, bindingVersion: bindingVersion, networkID: networkID, bindHost: bindHost, listener: listener,
|
|
tunnels: make(map[net.Conn]net.Conn), url: "http://" + net.JoinHostPort(browserProxyHost, strconv.Itoa(actualPort))}
|
|
proxy.server = &http.Server{Handler: proxy, ReadHeaderTimeout: 10 * time.Second, IdleTimeout: 60 * time.Second}
|
|
registry.proxies[alias] = proxy
|
|
go func() { _ = proxy.server.Serve(listener) }()
|
|
undo := func() {
|
|
registry.removeObject(alias, proxy)
|
|
}
|
|
return proxy.url, undo, nil
|
|
}
|
|
|
|
func (registry *memoryProxyRegistry) removeObject(alias string, proxy *memoryProxy) {
|
|
registry.mu.Lock()
|
|
if registry.proxies[alias] == proxy {
|
|
delete(registry.proxies, alias)
|
|
} else {
|
|
proxy = nil
|
|
}
|
|
registry.mu.Unlock()
|
|
if proxy != nil {
|
|
closeMemoryProxy(proxy)
|
|
}
|
|
}
|
|
|
|
func closeMemoryProxy(proxy *memoryProxy) {
|
|
proxy.mu.Lock()
|
|
tunnels := proxy.tunnels
|
|
proxy.tunnels = nil
|
|
proxy.mu.Unlock()
|
|
_ = proxy.listener.Close()
|
|
_ = proxy.server.Close()
|
|
for client, upstream := range tunnels {
|
|
_ = client.Close()
|
|
_ = upstream.Close()
|
|
}
|
|
}
|
|
|
|
func (registry *memoryProxyRegistry) ready(alias string, port int, runtimeID string, networkIDs ...string) bool {
|
|
registry.mu.Lock()
|
|
defer registry.mu.Unlock()
|
|
proxy := registry.proxies[alias]
|
|
return proxy != nil && runtimeID != "" && proxy.runtimeID == runtimeID && proxy.listener.Addr().(*net.TCPAddr).Port == port &&
|
|
(len(networkIDs) == 0 || proxy.networkID == networkIDs[0])
|
|
}
|
|
|
|
func (registry *memoryProxyRegistry) bind(alias string, bindingVersion int64, proxyURL, runtimeID string, networkIDs ...string) bool {
|
|
registry.mu.Lock()
|
|
defer registry.mu.Unlock()
|
|
proxy := registry.proxies[alias]
|
|
if proxy == nil || runtimeID == "" || proxy.bindingVersion != bindingVersion || proxy.url != proxyURL {
|
|
return false
|
|
}
|
|
proxy.runtimeID = runtimeID
|
|
if len(networkIDs) == 1 {
|
|
proxy.networkID = networkIDs[0]
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (registry *memoryProxyRegistry) remove(alias string, bindingVersion int64, runtimeID string, networkIDs ...string) bool {
|
|
registry.mu.Lock()
|
|
proxy := registry.proxies[alias]
|
|
if proxy != nil && runtimeID != "" && proxy.bindingVersion == bindingVersion && proxy.runtimeID == runtimeID &&
|
|
(len(networkIDs) == 0 || proxy.networkID == networkIDs[0]) {
|
|
delete(registry.proxies, alias)
|
|
} else if proxy != nil {
|
|
registry.mu.Unlock()
|
|
return false
|
|
} else {
|
|
proxy = nil
|
|
}
|
|
registry.mu.Unlock()
|
|
if proxy != nil {
|
|
closeMemoryProxy(proxy)
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (proxy *memoryProxy) ServeHTTP(response http.ResponseWriter, request *http.Request) {
|
|
if request.Method == http.MethodConnect {
|
|
proxy.tunnel(response, request)
|
|
return
|
|
}
|
|
proxy.mu.RLock()
|
|
exit := proxy.exit
|
|
proxy.mu.RUnlock()
|
|
transport := &http.Transport{DisableKeepAlives: true}
|
|
if exit.Protocol == "http" || exit.Protocol == "https" {
|
|
upstream := &url.URL{Scheme: exit.Protocol, Host: net.JoinHostPort(exit.Host, strconv.Itoa(exit.Port))}
|
|
if exit.Username != "" {
|
|
upstream.User = url.UserPassword(exit.Username, exit.Password)
|
|
}
|
|
transport.Proxy = http.ProxyURL(upstream)
|
|
} else {
|
|
transport.DialContext = proxy.dialContext
|
|
}
|
|
defer transport.CloseIdleConnections()
|
|
outbound := request.Clone(request.Context())
|
|
outbound.RequestURI = ""
|
|
outbound.Header.Del("Proxy-Authorization")
|
|
result, err := transport.RoundTrip(outbound)
|
|
if err != nil {
|
|
http.Error(response, "proxy connection failed", http.StatusBadGateway)
|
|
return
|
|
}
|
|
defer result.Body.Close()
|
|
for key, values := range result.Header {
|
|
for _, value := range values {
|
|
response.Header().Add(key, value)
|
|
}
|
|
}
|
|
response.WriteHeader(result.StatusCode)
|
|
_, _ = io.Copy(response, result.Body)
|
|
}
|
|
|
|
func (proxy *memoryProxy) tunnel(response http.ResponseWriter, request *http.Request) {
|
|
upstream, err := proxy.dialContext(request.Context(), "tcp", request.Host)
|
|
if err != nil {
|
|
http.Error(response, "proxy connection failed", http.StatusBadGateway)
|
|
return
|
|
}
|
|
client, buffered, err := http.NewResponseController(response).Hijack()
|
|
if err != nil {
|
|
_ = upstream.Close()
|
|
http.Error(response, "proxy tunnel unavailable", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if _, err := buffered.WriteString("HTTP/1.1 200 Connection Established\r\n\r\n"); err != nil || buffered.Flush() != nil {
|
|
_ = client.Close()
|
|
_ = upstream.Close()
|
|
return
|
|
}
|
|
proxy.mu.Lock()
|
|
if proxy.tunnels == nil {
|
|
proxy.mu.Unlock()
|
|
_ = client.Close()
|
|
_ = upstream.Close()
|
|
return
|
|
}
|
|
proxy.tunnels[client] = upstream
|
|
proxy.mu.Unlock()
|
|
defer func() {
|
|
proxy.mu.Lock()
|
|
delete(proxy.tunnels, client)
|
|
proxy.mu.Unlock()
|
|
_ = client.Close()
|
|
_ = upstream.Close()
|
|
}()
|
|
done := make(chan struct{}, 2)
|
|
go func() { _, _ = io.Copy(upstream, client); done <- struct{}{} }()
|
|
go func() { _, _ = io.Copy(client, upstream); done <- struct{}{} }()
|
|
<-done
|
|
}
|
|
|
|
func (proxy *memoryProxy) dialContext(ctx context.Context, _, target string) (net.Conn, error) {
|
|
ctx, cancel := context.WithTimeout(ctx, 20*time.Second)
|
|
defer cancel()
|
|
proxy.mu.RLock()
|
|
exit := proxy.exit
|
|
proxy.mu.RUnlock()
|
|
switch exit.Protocol {
|
|
case "http", "https":
|
|
return dialHTTPProxy(ctx, exit, target)
|
|
case "socks4":
|
|
return dialSOCKS4Proxy(ctx, exit, target)
|
|
case "socks5":
|
|
return dialSOCKS5Proxy(ctx, exit, target)
|
|
default:
|
|
return nil, errors.New("unsupported proxy protocol")
|
|
}
|
|
}
|
|
|
|
func dialHTTPProxy(ctx context.Context, exit gatewayProxyExit, target string) (net.Conn, error) {
|
|
address := net.JoinHostPort(exit.Host, strconv.Itoa(exit.Port))
|
|
connection, err := (&net.Dialer{}).DialContext(ctx, "tcp", address)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if exit.Protocol == "https" {
|
|
tlsConnection := tls.Client(connection, &tls.Config{ServerName: exit.Host, MinVersion: tls.VersionTLS12})
|
|
if err := tlsConnection.HandshakeContext(ctx); err != nil {
|
|
_ = connection.Close()
|
|
return nil, err
|
|
}
|
|
connection = tlsConnection
|
|
}
|
|
request := &http.Request{Method: http.MethodConnect, URL: &url.URL{Opaque: target}, Host: target, Header: make(http.Header)}
|
|
if exit.Username != "" {
|
|
request.Header.Set("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(exit.Username+":"+exit.Password)))
|
|
}
|
|
if deadline, ok := ctx.Deadline(); ok {
|
|
_ = connection.SetDeadline(deadline)
|
|
}
|
|
if err := request.Write(connection); err != nil {
|
|
_ = connection.Close()
|
|
return nil, err
|
|
}
|
|
result, err := http.ReadResponse(bufio.NewReader(connection), request)
|
|
if err != nil {
|
|
_ = connection.Close()
|
|
return nil, err
|
|
}
|
|
if result.StatusCode != http.StatusOK {
|
|
_ = result.Body.Close()
|
|
_ = connection.Close()
|
|
return nil, fmt.Errorf("upstream proxy returned %s", result.Status)
|
|
}
|
|
_ = connection.SetDeadline(time.Time{})
|
|
return connection, nil
|
|
}
|
|
|
|
func dialSOCKS4Proxy(ctx context.Context, exit gatewayProxyExit, target string) (net.Conn, error) {
|
|
connection, err := (&net.Dialer{}).DialContext(ctx, "tcp", net.JoinHostPort(exit.Host, strconv.Itoa(exit.Port)))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
host, portText, err := net.SplitHostPort(target)
|
|
if err != nil {
|
|
_ = connection.Close()
|
|
return nil, err
|
|
}
|
|
port, err := strconv.Atoi(portText)
|
|
if err != nil || port < 1 || port > 65535 {
|
|
_ = connection.Close()
|
|
return nil, errors.New("invalid SOCKS4 target")
|
|
}
|
|
payload := []byte{4, 1, byte(port >> 8), byte(port), 0, 0, 0, 1}
|
|
if ip := net.ParseIP(host).To4(); ip != nil {
|
|
copy(payload[4:8], ip)
|
|
}
|
|
payload = append(payload, exit.Username...)
|
|
payload = append(payload, 0)
|
|
if net.ParseIP(host).To4() == nil {
|
|
payload = append(payload, host...)
|
|
payload = append(payload, 0)
|
|
}
|
|
if err := exchangeSOCKS(ctx, connection, payload, 8); err != nil {
|
|
return nil, err
|
|
}
|
|
return connection, nil
|
|
}
|
|
|
|
func dialSOCKS5Proxy(ctx context.Context, exit gatewayProxyExit, target string) (net.Conn, error) {
|
|
connection, err := (&net.Dialer{}).DialContext(ctx, "tcp", net.JoinHostPort(exit.Host, strconv.Itoa(exit.Port)))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
methods := []byte{5, 1, 0}
|
|
if exit.Username != "" {
|
|
methods = []byte{5, 1, 2}
|
|
}
|
|
if deadline, ok := ctx.Deadline(); ok {
|
|
_ = connection.SetDeadline(deadline)
|
|
}
|
|
if _, err := connection.Write(methods); err != nil {
|
|
_ = connection.Close()
|
|
return nil, err
|
|
}
|
|
selection := make([]byte, 2)
|
|
if _, err := io.ReadFull(connection, selection); err != nil || selection[0] != 5 || selection[1] == 0xff {
|
|
_ = connection.Close()
|
|
return nil, errors.New("SOCKS5 authentication method rejected")
|
|
}
|
|
if selection[1] == 2 {
|
|
if len(exit.Username) > 255 || len(exit.Password) > 255 {
|
|
_ = connection.Close()
|
|
return nil, errors.New("SOCKS5 credentials too long")
|
|
}
|
|
auth := append([]byte{1, byte(len(exit.Username))}, exit.Username...)
|
|
auth = append(auth, byte(len(exit.Password)))
|
|
auth = append(auth, exit.Password...)
|
|
if _, err := connection.Write(auth); err != nil {
|
|
_ = connection.Close()
|
|
return nil, err
|
|
}
|
|
result := make([]byte, 2)
|
|
if _, err := io.ReadFull(connection, result); err != nil || result[1] != 0 {
|
|
_ = connection.Close()
|
|
return nil, errors.New("SOCKS5 authentication rejected")
|
|
}
|
|
} else if exit.Username != "" {
|
|
_ = connection.Close()
|
|
return nil, errors.New("SOCKS5 proxy skipped required authentication")
|
|
}
|
|
host, portText, err := net.SplitHostPort(target)
|
|
if err != nil {
|
|
_ = connection.Close()
|
|
return nil, err
|
|
}
|
|
port, err := strconv.Atoi(portText)
|
|
if err != nil || port < 1 || port > 65535 {
|
|
_ = connection.Close()
|
|
return nil, errors.New("invalid SOCKS5 target")
|
|
}
|
|
request := []byte{5, 1, 0}
|
|
if ip := net.ParseIP(host); ip != nil && ip.To4() != nil {
|
|
request = append(request, 1)
|
|
request = append(request, ip.To4()...)
|
|
} else if ip != nil {
|
|
request = append(request, 4)
|
|
request = append(request, ip.To16()...)
|
|
} else {
|
|
if len(host) > 255 {
|
|
_ = connection.Close()
|
|
return nil, errors.New("SOCKS5 target too long")
|
|
}
|
|
request = append(request, 3, byte(len(host)))
|
|
request = append(request, host...)
|
|
}
|
|
portBytes := make([]byte, 2)
|
|
binary.BigEndian.PutUint16(portBytes, uint16(port))
|
|
request = append(request, portBytes...)
|
|
if _, err := connection.Write(request); err != nil {
|
|
_ = connection.Close()
|
|
return nil, err
|
|
}
|
|
header := make([]byte, 4)
|
|
if _, err := io.ReadFull(connection, header); err != nil || header[0] != 5 || header[1] != 0 {
|
|
_ = connection.Close()
|
|
return nil, errors.New("SOCKS5 proxy rejected connection")
|
|
}
|
|
addressLength := 0
|
|
switch header[3] {
|
|
case 1:
|
|
addressLength = 4
|
|
case 4:
|
|
addressLength = 16
|
|
case 3:
|
|
var length [1]byte
|
|
if _, err := io.ReadFull(connection, length[:]); err != nil {
|
|
_ = connection.Close()
|
|
return nil, err
|
|
}
|
|
addressLength = int(length[0])
|
|
default:
|
|
_ = connection.Close()
|
|
return nil, errors.New("invalid SOCKS5 response")
|
|
}
|
|
if _, err := io.CopyN(io.Discard, connection, int64(addressLength+2)); err != nil {
|
|
_ = connection.Close()
|
|
return nil, err
|
|
}
|
|
_ = connection.SetDeadline(time.Time{})
|
|
return connection, nil
|
|
}
|
|
|
|
func exchangeSOCKS(ctx context.Context, connection net.Conn, request []byte, responseBytes int) error {
|
|
if deadline, ok := ctx.Deadline(); ok {
|
|
_ = connection.SetDeadline(deadline)
|
|
}
|
|
if _, err := connection.Write(request); err != nil {
|
|
_ = connection.Close()
|
|
return err
|
|
}
|
|
if responseBytes > 0 {
|
|
response := make([]byte, responseBytes)
|
|
if _, err := io.ReadFull(connection, response); err != nil {
|
|
_ = connection.Close()
|
|
return err
|
|
}
|
|
if responseBytes == 8 && response[1] != 90 {
|
|
_ = connection.Close()
|
|
return errors.New("SOCKS4 proxy rejected connection")
|
|
}
|
|
}
|
|
_ = connection.SetDeadline(time.Time{})
|
|
return nil
|
|
}
|