Files
creator-hub/cmd/docker-gateway/proxy.go
T

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
}