Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PoC: Implement optional host proxy for lima cache #2367

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions examples/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,11 @@ hostResolver:
# - 1.1.1.1
# - 1.0.0.1

# The host proxy implements a HTTP and HTTPS proxy that can cache downloads on the host.
hostProxy:
# 🟢 Builtin default: false
enabled: null

# Prefix to use for installing guest agent, and containerd with dependencies (if configured)
# 🟢 Builtin default: /usr/local
guestInstallPrefix: null
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ require (
github.com/diskfs/go-diskfs v1.4.1
github.com/docker/go-units v0.5.0
github.com/elastic/go-libaudit/v2 v2.5.0
github.com/elazarl/goproxy v0.0.0-20240909085733-6741dbfc16a1
github.com/foxcpp/go-mockdns v1.1.0
github.com/goccy/go-yaml v1.12.0
github.com/google/go-cmp v0.6.0
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ github.com/elastic/go-libaudit/v2 v2.5.0 h1:5OK919QRnGtcjVBz3n/cs5F42im1mPlVTA9T
github.com/elastic/go-libaudit/v2 v2.5.0/go.mod h1:AjlnhinP+kKQuUJoXLVrqxBM8uyhQmkzoV6jjsCFP4Q=
github.com/elastic/go-licenser v0.4.1 h1:1xDURsc8pL5zYT9R29425J3vkHdt4RT5TNEMeRN48x4=
github.com/elastic/go-licenser v0.4.1/go.mod h1:V56wHMpmdURfibNBggaSBfqgPxyT1Tldns1i87iTEvU=
github.com/elazarl/goproxy v0.0.0-20240909085733-6741dbfc16a1 h1:g7YUigN4dW2+zpdusdTTghZ+5Py3BaUMAStvL8Nk+FY=
github.com/elazarl/goproxy v0.0.0-20240909085733-6741dbfc16a1/go.mod h1:thX175TtLTzLj3p7N/Q9IiKZ7NF+p72cvL91emV0hzo=
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 h1:dWB6v3RcOy03t/bUadywsbyrQwCqZeNIEX6M1OtSZOM=
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8=
github.com/elliotchance/orderedmap v1.6.0 h1:xjn+kbbKXeDq6v9RVE+WYwRbYfAZKvlWfcJNxM8pvEw=
github.com/elliotchance/orderedmap v1.6.0/go.mod h1:wsDwEaX5jEoyhbs7x93zk2H/qv0zwuhg4inXhDkYqys=
github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab h1:h1UgjJdAAhj+uPL68n7XASS6bU+07ZX1WJvVS2eyoeY=
Expand Down
1 change: 1 addition & 0 deletions pkg/cidata/cidata.TEMPLATE.d/lima.env
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ LIMA_CIDATA_SLIRP_GATEWAY={{.SlirpGateway}}
LIMA_CIDATA_SLIRP_IP_ADDRESS={{.SlirpIPAddress}}
LIMA_CIDATA_UDP_DNS_LOCAL_PORT={{.UDPDNSLocalPort}}
LIMA_CIDATA_TCP_DNS_LOCAL_PORT={{.TCPDNSLocalPort}}
LIMA_CIDATA_HTTP_PROXY_LOCAL_PORT={{.HTTPProxyLocalPort}}
LIMA_CIDATA_ROSETTA_ENABLED={{.RosettaEnabled}}
LIMA_CIDATA_ROSETTA_BINFMT={{.RosettaBinFmt}}
{{- if .SkipDefaultDependencyResolution}}
Expand Down
3 changes: 3 additions & 0 deletions pkg/cidata/cidata.TEMPLATE.d/proxy.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{{ range $line := .HTTPProxyCACert.Lines -}}
{{ $line }}
{{ end -}}
13 changes: 9 additions & 4 deletions pkg/cidata/cidata.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ func setupEnv(instConfigEnv map[string]string, propagateProxyEnv bool, slirpGate
return env, nil
}

func templateArgs(bootScripts bool, instDir, name string, instConfig *limayaml.LimaYAML, udpDNSLocalPort, tcpDNSLocalPort, vsockPort int, virtioPort string) (*TemplateArgs, error) {
func templateArgs(bootScripts bool, instDir, name string, instConfig *limayaml.LimaYAML, udpDNSLocalPort, tcpDNSLocalPort, vsockPort int, virtioPort string, proxyPort int, proxyCert string) (*TemplateArgs, error) {
if err := limayaml.Validate(instConfig, false); err != nil {
return nil, err
}
Expand Down Expand Up @@ -290,6 +290,11 @@ func templateArgs(bootScripts bool, instDir, name string, instConfig *limayaml.L
}
}

if *instConfig.HostProxy.Enabled {
args.HTTPProxyLocalPort = proxyPort
args.HTTPProxyCACert = getCert(proxyCert)
}

args.CACerts.RemoveDefaults = instConfig.CACertificates.RemoveDefaults

for _, path := range instConfig.CACertificates.Files {
Expand Down Expand Up @@ -330,7 +335,7 @@ func templateArgs(bootScripts bool, instDir, name string, instConfig *limayaml.L
}

func GenerateCloudConfig(instDir, name string, instConfig *limayaml.LimaYAML) error {
args, err := templateArgs(false, instDir, name, instConfig, 0, 0, 0, "")
args, err := templateArgs(false, instDir, name, instConfig, 0, 0, 0, "", 0, "")
if err != nil {
return err
}
Expand All @@ -352,8 +357,8 @@ func GenerateCloudConfig(instDir, name string, instConfig *limayaml.LimaYAML) er
return os.WriteFile(filepath.Join(instDir, filenames.CloudConfig), config, 0o444)
}

func GenerateISO9660(instDir, name string, instConfig *limayaml.LimaYAML, udpDNSLocalPort, tcpDNSLocalPort int, nerdctlArchive string, vsockPort int, virtioPort string) error {
args, err := templateArgs(true, instDir, name, instConfig, udpDNSLocalPort, tcpDNSLocalPort, vsockPort, virtioPort)
func GenerateISO9660(instDir, name string, instConfig *limayaml.LimaYAML, udpDNSLocalPort, tcpDNSLocalPort int, nerdctlArchive string, vsockPort int, virtioPort string, proxyPort int, proxyCert string) error {
args, err := templateArgs(true, instDir, name, instConfig, udpDNSLocalPort, tcpDNSLocalPort, vsockPort, virtioPort, proxyPort, proxyCert)
if err != nil {
return err
}
Expand Down
2 changes: 2 additions & 0 deletions pkg/cidata/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ type TemplateArgs struct {
SlirpIPAddress string
UDPDNSLocalPort int
TCPDNSLocalPort int
HTTPProxyLocalPort int
HTTPProxyCACert Cert
Env map[string]string
Param map[string]string
BootScripts bool
Expand Down
23 changes: 22 additions & 1 deletion pkg/hostagent/hostagent.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
hostagentapi "github.com/lima-vm/lima/pkg/hostagent/api"
"github.com/lima-vm/lima/pkg/hostagent/dns"
"github.com/lima-vm/lima/pkg/hostagent/events"
"github.com/lima-vm/lima/pkg/hostagent/proxy"
"github.com/lima-vm/lima/pkg/limayaml"
"github.com/lima-vm/lima/pkg/networks"
"github.com/lima-vm/lima/pkg/osutil"
Expand All @@ -44,6 +45,7 @@ type HostAgent struct {
sshLocalPort int
udpDNSLocalPort int
tcpDNSLocalPort int
proxyLocalPort int
instDir string
instName string
instSSHAddress string
Expand Down Expand Up @@ -117,6 +119,13 @@ func New(instName string, stdout io.Writer, signalCh chan os.Signal, opts ...Opt
return nil, err
}
}
var proxyLocalPort int
if *inst.Config.HostProxy.Enabled {
proxyLocalPort, err = findFreeUDPLocalPort()
if err != nil {
return nil, err
}
}

vSockPort := 0
virtioPort := ""
Expand All @@ -136,7 +145,7 @@ func New(instName string, stdout io.Writer, signalCh chan os.Signal, opts ...Opt
if err := cidata.GenerateCloudConfig(inst.Dir, instName, inst.Config); err != nil {
return nil, err
}
if err := cidata.GenerateISO9660(inst.Dir, instName, inst.Config, udpDNSLocalPort, tcpDNSLocalPort, o.nerdctlArchive, vSockPort, virtioPort); err != nil {
if err := cidata.GenerateISO9660(inst.Dir, instName, inst.Config, udpDNSLocalPort, tcpDNSLocalPort, o.nerdctlArchive, vSockPort, virtioPort, proxyLocalPort, proxy.CACert); err != nil {
return nil, err
}

Expand Down Expand Up @@ -201,6 +210,7 @@ func New(instName string, stdout io.Writer, signalCh chan os.Signal, opts ...Opt
sshLocalPort: sshLocalPort,
udpDNSLocalPort: udpDNSLocalPort,
tcpDNSLocalPort: tcpDNSLocalPort,
proxyLocalPort: proxyLocalPort,
instDir: inst.Dir,
instName: instName,
instSSHAddress: inst.SSHAddress,
Expand Down Expand Up @@ -348,6 +358,17 @@ func (a *HostAgent) Run(ctx context.Context) error {
defer dnsServer.Shutdown()
}

if *a.instConfig.HostProxy.Enabled {
srvOpts := proxy.ServerOptions{
TCPPort: a.proxyLocalPort,
}
proxyServer, err := proxy.Start(srvOpts)
if err != nil {
return fmt.Errorf("cannot start proxy server: %w", err)
}
defer proxyServer.Shutdown()
}

errCh, err := a.driver.Start(ctx)
if err != nil {
return err
Expand Down
166 changes: 166 additions & 0 deletions pkg/hostagent/proxy/proxy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// This file has been adapted from https://github.com/elazarl/goproxy/blob/6741dbfc16a1/examples/goproxy-eavesdropper/main.go

package proxy

import (
"bufio"
"context"
"net"
"net/http"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"

"github.com/lima-vm/lima/pkg/downloader"

"github.com/elazarl/goproxy"
"github.com/sirupsen/logrus"
)

// CACert has the CA certificate text.
var CACert = string(goproxy.CA_CERT)

type ServerOptions struct {
Address string
TCPPort int
}

type Server struct {
srv *http.Server
}

func (s *Server) Shutdown() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if s.srv != nil {
_ = s.srv.Shutdown(ctx)
}
}

func Start(opts ServerOptions) (*Server, error) {
server := &Server{}
if opts.TCPPort > 0 {
srv, err := listenAndServe(opts)
if err != nil {
return nil, err
}
server.srv = srv
}
return server, nil
}

func sendFile(req *http.Request, path string, lastModified time.Time, contentType string) (*http.Response, error) {
resp := &http.Response{}
resp.Request = req
resp.TransferEncoding = req.TransferEncoding
resp.Header = make(http.Header)
status := http.StatusOK
resp.StatusCode = status
resp.Status = http.StatusText(status)
st, err := os.Stat(path)
if err != nil {
return nil, err
}
f, err := os.Open(path)
if err != nil {
return nil, err
}
resp.Body = f
if contentType == "" {
contentType = "application/octet-stream"
}
resp.Header.Set("Content-Type", contentType)
if !lastModified.IsZero() {
resp.Header.Set("Last-Modified", lastModified.Format(http.TimeFormat))
}
resp.ContentLength = st.Size()
return resp, nil
}

func listenAndServe(opts ServerOptions) (*http.Server, error) {
ucd, err := os.UserCacheDir()
if err != nil {
return nil, err
}
cacheDir := filepath.Join(ucd, "lima")
downloader.HideProgress = true

addr := net.JoinHostPort(opts.Address, strconv.Itoa(opts.TCPPort))
proxy := goproxy.NewProxyHttpServer()
proxy.OnRequest(goproxy.ReqHostMatches(regexp.MustCompile("^.*$"))).
HandleConnect(goproxy.AlwaysMitm)
proxy.OnRequest().DoFunc(func(req *http.Request, _ *goproxy.ProxyCtx) (*http.Request, *http.Response) {
u := req.URL
if strings.Contains(u.Host, ":") {
host, port, err := net.SplitHostPort(u.Host)
if err != nil {
return nil, nil
}
if u.Scheme == "http" && port == "80" || u.Scheme == "https" && port == "443" {
u.Host = host
}
}
url := u.String()
if res, err := downloader.Cached(url, downloader.WithCacheDir(cacheDir)); err == nil {
if resp, err := sendFile(req, res.CachePath, res.LastModified, res.ContentType); err == nil {
return nil, resp
}
}
if res, err := downloader.Download(context.Background(), "", url, downloader.WithCacheDir(cacheDir)); err == nil {
if resp, err := sendFile(req, res.CachePath, res.LastModified, res.ContentType); err == nil {
return nil, resp
}
}
return req, nil
})
proxy.OnRequest(goproxy.ReqHostMatches(regexp.MustCompile("^.*:80$"))).
HijackConnect(func(req *http.Request, client net.Conn, ctx *goproxy.ProxyCtx) {
defer func() {
if e := recover(); e != nil {
ctx.Logf("error connecting to remote: %v", e)
_, _ = client.Write([]byte("HTTP/1.1 500 Cannot reach destination\r\n\r\n"))
}
client.Close()
}()
clientBuf := bufio.NewReadWriter(bufio.NewReader(client), bufio.NewWriter(client))
remote, err := net.Dial("tcp", req.URL.Host)
if err != nil {
ctx.Logf("%v", err)
return
}
_, _ = client.Write([]byte("HTTP/1.1 200 Ok\r\n\r\n"))
remoteBuf := bufio.NewReadWriter(bufio.NewReader(remote), bufio.NewWriter(remote))
for {
req, err := http.ReadRequest(clientBuf.Reader)
if err != nil {
ctx.Logf("%v", err)
return
}
_ = req.Write(remoteBuf)
_ = remoteBuf.Flush()
resp, err := http.ReadResponse(remoteBuf.Reader, req)
if err != nil {
ctx.Logf("%v", err)
return
}
_ = resp.Write(clientBuf.Writer)
_ = clientBuf.Flush()
resp.Body.Close()
}
})
proxy.Verbose = true
s := &http.Server{Addr: addr, Handler: proxy}
go func() {
logrus.Debugf("Start HTTP proxy listening on: %v", addr)
if e := s.ListenAndServe(); e != nil {
if e != http.ErrServerClosed {
logrus.Fatal(e)
}
}
}()

return s, nil
}
10 changes: 10 additions & 0 deletions pkg/limayaml/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,16 @@ func FillDefault(y, d, o *LimaYAML, filePath string) {
y.HostResolver.IPv6 = ptr.Of(false)
}

if y.HostProxy.Enabled == nil {
y.HostProxy.Enabled = d.HostProxy.Enabled
}
if o.HostProxy.Enabled != nil {
y.HostProxy.Enabled = o.HostProxy.Enabled
}
if y.HostProxy.Enabled == nil {
y.HostProxy.Enabled = ptr.Of(false)
}

if y.PropagateProxyEnv == nil {
y.PropagateProxyEnv = d.PropagateProxyEnv
}
Expand Down
9 changes: 9 additions & 0 deletions pkg/limayaml/defaults_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ func TestFillDefault(t *testing.T) {
Enabled: ptr.Of(true),
IPv6: ptr.Of(false),
},
HostProxy: HostProxy{
Enabled: ptr.Of(false),
},
PropagateProxyEnv: ptr.Of(true),
CACertificates: CACertificates{
RemoveDefaults: ptr.Of(false),
Expand Down Expand Up @@ -365,6 +368,9 @@ func TestFillDefault(t *testing.T) {
"default": "localhost",
},
},
HostProxy: HostProxy{
Enabled: ptr.Of(true),
},
PropagateProxyEnv: ptr.Of(false),

Mounts: []Mount{
Expand Down Expand Up @@ -564,6 +570,9 @@ func TestFillDefault(t *testing.T) {
"override.": "underflow",
},
},
HostProxy: HostProxy{
Enabled: ptr.Of(true),
},
PropagateProxyEnv: ptr.Of(false),

Mounts: []Mount{
Expand Down
5 changes: 5 additions & 0 deletions pkg/limayaml/limayaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ type LimaYAML struct {
Param map[string]string `yaml:"param,omitempty" json:"param,omitempty"`
DNS []net.IP `yaml:"dns,omitempty" json:"dns,omitempty"`
HostResolver HostResolver `yaml:"hostResolver,omitempty" json:"hostResolver,omitempty"`
HostProxy HostProxy `yaml:"hostProxy,omitempty" json:"hostProxy,omitempty"`
// `useHostResolver` was deprecated in Lima v0.8.1, removed in Lima v0.14.0. Use `hostResolver.enabled` instead.
PropagateProxyEnv *bool `yaml:"propagateProxyEnv,omitempty" json:"propagateProxyEnv,omitempty"`
CACertificates CACertificates `yaml:"caCerts,omitempty" json:"caCerts,omitempty"`
Expand Down Expand Up @@ -269,6 +270,10 @@ type HostResolver struct {
Hosts map[string]string `yaml:"hosts,omitempty" json:"hosts,omitempty"`
}

type HostProxy struct {
Enabled *bool `yaml:"enabled,omitempty" json:"enabled,omitempty"`
}

type CACertificates struct {
RemoveDefaults *bool `yaml:"removeDefaults,omitempty" json:"removeDefaults,omitempty"` // default: false
Files []string `yaml:"files,omitempty" json:"files,omitempty"`
Expand Down
Loading