~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
200 lines · 5.4 KB Raw History
package gatewaykcp

import (
	"context"
	"errors"
	"fmt"
	"io"
	"log/slog"
	"net"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"github.com/johnandersen777/socialweb-computer-kcp/api/v1alpha1"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/devinstance"
)

const (
	ListenerEgress = "egress"
	egressDialTime = 10 * time.Second
	egressRecord   = "egress.addr"
)

type egressMap struct {
	host   string
	target string
}

func guestTLSEgress(guestTLSURL string) (egressMap, bool, error) {
	if guestTLSURL == "" {
		return egressMap{}, false, nil
	}
	u, err := url.Parse(guestTLSURL)
	if err != nil {
		return egressMap{}, false, fmt.Errorf("gatewaykcp: the network value's guestTlsUrl %q: %w", guestTLSURL, err)
	}
	if u.Hostname() == "" || u.Port() == "" {
		return egressMap{}, false, fmt.Errorf("gatewaykcp: the network value's guestTlsUrl %q names no host and port", guestTLSURL)
	}
	host, target := strings.ToLower(u.Hostname()), u.Host
	if host == "localhost" || strings.HasSuffix(host, ".localhost") {
		target = net.JoinHostPort("127.0.0.1", u.Port())
	}
	return egressMap{host: host, target: target}, true, nil
}

func (m egressMap) dialAddress(hostport string) string {
	host, port, err := net.SplitHostPort(hostport)
	if err != nil || port != "443" {
		return hostport
	}
	h := strings.ToLower(strings.TrimSuffix(host, "."))
	if h == m.host || strings.HasSuffix(h, "."+m.host) {
		return m.target
	}
	return hostport
}

func (m egressMap) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodConnect {
		http.Error(w, "only CONNECT", http.StatusMethodNotAllowed)
		return
	}
	upstream, err := net.DialTimeout("tcp", m.dialAddress(r.Host), egressDialTime)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadGateway)
		return
	}
	hj, ok := w.(http.Hijacker)
	if !ok {
		upstream.Close()
		http.Error(w, "no hijack", http.StatusInternalServerError)
		return
	}
	client, buf, err := hj.Hijack()
	if err != nil {
		upstream.Close()
		return
	}
	if _, err := client.Write([]byte("HTTP/1.1 200 Connection established\r\n\r\n")); err != nil {
		client.Close()
		upstream.Close()
		return
	}
	done := make(chan struct{}, 2)
	go func() {
		if n := buf.Reader.Buffered(); n > 0 {
			b, _ := buf.Reader.Peek(n)
			upstream.Write(b)
		}
		io.Copy(upstream, client)
		done <- struct{}{}
	}()
	go func() {
		io.Copy(client, upstream)
		done <- struct{}{}
	}()
	<-done
	client.Close()
	upstream.Close()
	<-done
}

func listenEgress(record string) (net.Listener, error) {
	if b, err := os.ReadFile(record); err == nil {
		if ln, err := net.Listen("tcp", strings.TrimSpace(string(b))); err == nil {
			return ln, nil
		}
	}
	ln, err := net.Listen("tcp", DefaultAddress)
	if err != nil {
		return nil, err
	}
	if err := os.WriteFile(record, []byte(ln.Addr().String()+"\n"), 0o600); err != nil {
		ln.Close()
		return nil, err
	}
	return ln, nil
}

func runEgress(ctx context.Context, m egressMap, record string, log *slog.Logger) (net.Addr, func(), error) {
	ln, err := listenEgress(record)
	if err != nil {
		return nil, nil, err
	}
	srv := &http.Server{Handler: m, ReadHeaderTimeout: egressDialTime}
	closeOnDone := context.AfterFunc(ctx, func() { srv.Close() })
	var wg sync.WaitGroup
	wg.Go(func() {
		if err := srv.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) {
			log.Error("the egress proxy stopped", "err", err.Error())
		}
	})
	stop := func() {
		closeOnDone()
		srv.Close()
		wg.Wait()
	}
	return ln.Addr(), stop, nil
}

const outboundCAFile = "outbound-ca-bundle.pem"

var systemCertFiles = []string{"/etc/ssl/certs/ca-certificates.crt", "/etc/pki/tls/certs/ca-bundle.crt", "/etc/ssl/ca-bundle.pem",
	"/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", "/etc/ssl/cert.pem"}

func outboundCABundle(dir, networkCA string) (string, error) {
	ca := []byte(strings.TrimSpace(networkCA) + "\n")
	var system []byte
	for _, f := range systemCertFiles {
		if b, err := os.ReadFile(f); err == nil {
			system = b
			break
		}
	}
	if err := os.MkdirAll(dir, 0o700); err != nil {
		return "", err
	}
	out := filepath.Join(dir, outboundCAFile)
	body := append(append(append([]byte{}, system...), '\n'), ca...)
	if err := os.WriteFile(out+".tmp", body, 0o600); err != nil {
		return "", err
	}
	return out, os.Rename(out+".tmp", out)
}

func NetworkOutbound(ctx context.Context, n *v1alpha1.MarketNetworkValue, tlsDir, record string, log *slog.Logger) (net.Addr, func(), error) {
	if n != nil && n.CABundle != "" && os.Getenv("SSL_CERT_FILE") == "" {
		bundle, err := outboundCABundle(tlsDir, n.CABundle)
		if err != nil {
			return nil, func() {}, err
		}
		os.Setenv("SSL_CERT_FILE", bundle)
		log.Info("the system roots and the market network's CA are this gateway's outbound trust: every vault it places verifies a bidder's issuer with them",
			"bundle", bundle)
	}
	if n == nil {
		return nil, func() {}, nil
	}
	m, ok, err := guestTLSEgress(n.GuestTLSURL())
	if err != nil || !ok {
		return nil, func() {}, err
	}
	addr, stop, err := runEgress(ctx, m, record, log)
	if err != nil {
		return nil, func() {}, err
	}
	os.Setenv("HTTPS_PROXY", "http://"+addr.String())
	log.Info("the market network's guest TLS front is this gateway's egress for its port-less names: every vault it places fetches a bidder's issuer through it",
		"egress", addr.String(), "names", "*."+m.host+":443", "target", m.target)
	return addr, stop, nil
}

func RequesterEgressRecord(l devinstance.Layout) string {
	return filepath.Join(l.KcpDir(), egressRecord)
}